Skip to main content

perforce_cli/cmd/
clean.rs

1use std::{
2    ffi::OsStr,
3    path::{Path, PathBuf},
4    process::{Child, Stdio},
5};
6
7use super::SubCommand;
8
9use crate::{global::GlobalOpts, spawn::ParameterizedSpawn};
10
11/// `p4 [g-opts] clean [-e -a -d -I -l -K -m -n] [file ...]`
12///
13/// Restore client workspace files to match the state of corresponding
14/// depot files. This command is equivalent to `p4 reconcile -w`: files
15/// present in the workspace but missing from the depot are deleted;
16/// files present in the depot but missing from the workspace are added
17/// at the revision synced from the depot; and files modified in the
18/// workspace that are not opened for add, edit, delete, or integrate
19/// are restored to the last revision synced from the depot.
20///
21/// Without file arguments, the files in your local working directory
22/// are used.
23#[derive(Debug, Clone, Default)]
24pub struct Clean {
25    bin: PathBuf,
26
27    global_opts: GlobalOpts,
28
29    /// -e
30    find_edited: bool,
31
32    /// -a
33    find_added: bool,
34
35    /// -d
36    find_deleted: bool,
37
38    /// -I
39    ignore_p4ignore: bool,
40
41    /// -l
42    local_file_syntax: bool,
43
44    /// -K
45    #[cfg(not(feature = "lt2022_1"))]
46    suppress_keyword_expansion: bool,
47
48    /// -m
49    #[cfg(not(feature = "lt2018_1"))]
50    fast_timestamp_check: bool,
51
52    /// -n
53    preview: bool,
54}
55
56impl SubCommand for Clean {
57    fn name(&self) -> &str {
58        "clean"
59    }
60
61    fn inject_local_args(&self, command: &mut std::process::Command) {
62        if self.find_edited {
63            command.arg("-e");
64        }
65        if self.find_added {
66            command.arg("-a");
67        }
68        if self.find_deleted {
69            command.arg("-d");
70        }
71        if self.ignore_p4ignore {
72            command.arg("-I");
73        }
74        if self.local_file_syntax {
75            command.arg("-l");
76        }
77        #[cfg(not(feature = "lt2022_1"))]
78        if self.suppress_keyword_expansion {
79            command.arg("-K");
80        }
81        #[cfg(not(feature = "lt2018_1"))]
82        if self.fast_timestamp_check {
83            command.arg("-m");
84        }
85        if self.preview {
86            command.arg("-n");
87        }
88    }
89
90    fn global_opts(&self) -> Option<&GlobalOpts> {
91        Some(&self.global_opts)
92    }
93}
94
95impl ParameterizedSpawn<()> for Clean {
96    type Output = Child;
97    type Error = std::io::Error;
98
99    /// Spawns `p4 clean` without file arguments as a child process, so the
100    /// files in your local working directory are reconciled.
101    ///
102    /// The child process has its standard output and error streams piped;
103    /// use the returned [`Child`] handle to wait for it or interact with it.
104    fn spawn_with(&mut self, (): ()) -> Result<Self::Output, Self::Error> {
105        self.setup_command(&self.bin)
106            .stdout(Stdio::piped())
107            .stderr(Stdio::piped())
108            .spawn()
109    }
110}
111
112impl<S, I> ParameterizedSpawn<(S,)> for Clean
113where
114    S: IntoIterator<Item = I>,
115    I: AsRef<OsStr>,
116{
117    type Output = Child;
118    type Error = std::io::Error;
119
120    /// Spawns `p4 clean` for the given files as a child process.
121    ///
122    /// The child process has its standard output and error streams piped;
123    /// use the returned [`Child`] handle to wait for it or interact with it.
124    fn spawn_with(&mut self, (files,): (S,)) -> Result<Self::Output, Self::Error> {
125        self.setup_command(&self.bin)
126            .args(files)
127            .stdout(Stdio::piped())
128            .stderr(Stdio::piped())
129            .spawn()
130    }
131}
132
133impl Clean {
134    /// Get the Perforce command path.
135    pub fn get_bin(&self) -> &Path {
136        &self.bin
137    }
138
139    /// Set the Perforce command path.
140    pub fn set_bin(&mut self, bin: impl Into<PathBuf>) -> &mut Self {
141        self.bin = bin.into();
142        self
143    }
144
145    /// Set the Perforce command path.
146    pub fn bin(mut self, bin: impl Into<PathBuf>) -> Self {
147        self.set_bin(bin);
148        self
149    }
150
151    /// Create a new `p4 clean` command.
152    ///
153    /// `bin` is the path to the Perforce command-line executable.
154    pub fn new(bin: impl Into<PathBuf>, global_opts: GlobalOpts) -> Self {
155        Self {
156            bin: bin.into(),
157            global_opts,
158            ..Default::default()
159        }
160    }
161
162    /// # Description
163    ///
164    /// g-opts
165    ///
166    #[cfg_attr(
167        all(feature = "lt2018_2", not(feature = "lt2017_1")),
168        doc = "See [Global Options](GlobalOpts)."
169    )]
170    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
171    pub fn get_global_opts(&self) -> &GlobalOpts {
172        &self.global_opts
173    }
174
175    /// # Description
176    ///
177    /// g-opts
178    ///
179    #[cfg_attr(
180        all(feature = "lt2018_2", not(feature = "lt2017_1")),
181        doc = "See [Global Options](GlobalOpts)."
182    )]
183    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
184    pub fn set_global_opts(&mut self, v: GlobalOpts) -> &mut Self {
185        self.global_opts = v;
186        self
187    }
188
189    /// # Description
190    ///
191    /// g-opts
192    ///
193    #[cfg_attr(
194        all(feature = "lt2018_2", not(feature = "lt2017_1")),
195        doc = "See [Global Options](GlobalOpts)."
196    )]
197    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
198    pub fn global_opts(mut self, v: GlobalOpts) -> Self {
199        self.global_opts = v;
200        self
201    }
202
203    /// # Description
204    ///
205    /// -e
206    ///
207    /// Edited files: Find files in the workspace that have been modified and
208    /// restore them to the last file version that has synced from the depot.
209    pub fn get_find_edited(&self) -> bool {
210        self.find_edited
211    }
212
213    /// # Description
214    ///
215    /// -e
216    ///
217    /// Edited files: Find files in the workspace that have been modified and
218    /// restore them to the last file version that has synced from the depot.
219    pub fn set_find_edited(&mut self, v: bool) -> &mut Self {
220        self.find_edited = v;
221        self
222    }
223
224    /// # Description
225    ///
226    /// -e
227    ///
228    /// Edited files: Find files in the workspace that have been modified and
229    /// restore them to the last file version that has synced from the depot.
230    pub fn find_edited(mut self, v: bool) -> Self {
231        self.find_edited = v;
232        self
233    }
234
235    /// # Description
236    ///
237    /// -a
238    ///
239    /// Added files: Find files in the workspace that have no corresponding
240    /// files in the depot and delete them.
241    pub fn get_find_added(&self) -> bool {
242        self.find_added
243    }
244
245    /// # Description
246    ///
247    /// -a
248    ///
249    /// Added files: Find files in the workspace that have no corresponding
250    /// files in the depot and delete them.
251    pub fn set_find_added(&mut self, v: bool) -> &mut Self {
252        self.find_added = v;
253        self
254    }
255
256    /// # Description
257    ///
258    /// -a
259    ///
260    /// Added files: Find files in the workspace that have no corresponding
261    /// files in the depot and delete them.
262    pub fn find_added(mut self, v: bool) -> Self {
263        self.find_added = v;
264        self
265    }
266
267    /// # Description
268    ///
269    /// -d
270    ///
271    /// Deleted files: Find those files in the depot that do not exist in your
272    /// workspace and add them to the workspace.
273    pub fn get_find_deleted(&self) -> bool {
274        self.find_deleted
275    }
276
277    /// # Description
278    ///
279    /// -d
280    ///
281    /// Deleted files: Find those files in the depot that do not exist in your
282    /// workspace and add them to the workspace.
283    pub fn set_find_deleted(&mut self, v: bool) -> &mut Self {
284        self.find_deleted = v;
285        self
286    }
287
288    /// # Description
289    ///
290    /// -d
291    ///
292    /// Deleted files: Find those files in the depot that do not exist in your
293    /// workspace and add them to the workspace.
294    pub fn find_deleted(mut self, v: bool) -> Self {
295        self.find_deleted = v;
296        self
297    }
298
299    /// # Description
300    ///
301    /// -I
302    ///
303    /// Do not perform any ignore checking, which means to ignore any settings
304    /// specified by P4IGNORE for added files.
305    pub fn get_ignore_p4ignore(&self) -> bool {
306        self.ignore_p4ignore
307    }
308
309    /// # Description
310    ///
311    /// -I
312    ///
313    /// Do not perform any ignore checking, which means to ignore any settings
314    /// specified by P4IGNORE for added files.
315    pub fn set_ignore_p4ignore(&mut self, v: bool) -> &mut Self {
316        self.ignore_p4ignore = v;
317        self
318    }
319
320    /// # Description
321    ///
322    /// -I
323    ///
324    /// Do not perform any ignore checking, which means to ignore any settings
325    /// specified by P4IGNORE for added files.
326    pub fn ignore_p4ignore(mut self, v: bool) -> Self {
327        self.ignore_p4ignore = v;
328        self
329    }
330
331    /// # Description
332    ///
333    /// -l
334    ///
335    /// Display output in local file syntax with relative paths, similar to
336    /// the workspace-centric view of `p4 status`.
337    pub fn get_local_file_syntax(&self) -> bool {
338        self.local_file_syntax
339    }
340
341    /// # Description
342    ///
343    /// -l
344    ///
345    /// Display output in local file syntax with relative paths, similar to
346    /// the workspace-centric view of `p4 status`.
347    pub fn set_local_file_syntax(&mut self, v: bool) -> &mut Self {
348        self.local_file_syntax = v;
349        self
350    }
351
352    /// # Description
353    ///
354    /// -l
355    ///
356    /// Display output in local file syntax with relative paths, similar to
357    /// the workspace-centric view of `p4 status`.
358    pub fn local_file_syntax(mut self, v: bool) -> Self {
359        self.local_file_syntax = v;
360        self
361    }
362
363    /// # Description
364    ///
365    /// -K
366    ///
367    /// Suppress keyword expansion when updating `+k` type files on the
368    /// client. See File type modifiers.
369    #[cfg(not(feature = "lt2022_1"))]
370    pub fn get_suppress_keyword_expansion(&self) -> bool {
371        self.suppress_keyword_expansion
372    }
373
374    /// # Description
375    ///
376    /// -K
377    ///
378    /// Suppress keyword expansion when updating `+k` type files on the
379    /// client. See File type modifiers.
380    #[cfg(not(feature = "lt2022_1"))]
381    pub fn set_suppress_keyword_expansion(&mut self, v: bool) -> &mut Self {
382        self.suppress_keyword_expansion = v;
383        self
384    }
385
386    /// # Description
387    ///
388    /// -K
389    ///
390    /// Suppress keyword expansion when updating `+k` type files on the
391    /// client. See File type modifiers.
392    #[cfg(not(feature = "lt2022_1"))]
393    pub fn suppress_keyword_expansion(mut self, v: bool) -> Self {
394        self.suppress_keyword_expansion = v;
395        self
396    }
397
398    /// # Description
399    ///
400    /// -m
401    ///
402    /// Compare the file sync or submit time (in the depot) with the file
403    /// modification time (in the workspace) to help determine whether the
404    /// file has changed. Normally P4 Server uses file digests to determine
405    /// whether files in the workspace differ from the head revisions of
406    /// these files in the depot. This can be time consuming for large files.
407    /// But when the timestamps are the same, the costly digest comparisons
408    /// can be skipped. This option is only relevant if you are using clean
409    /// to find changed files rather than files that were deleted or added.
410    #[cfg(not(feature = "lt2018_1"))]
411    pub fn get_fast_timestamp_check(&self) -> bool {
412        self.fast_timestamp_check
413    }
414
415    /// # Description
416    ///
417    /// -m
418    ///
419    /// Compare the file sync or submit time (in the depot) with the file
420    /// modification time (in the workspace) to help determine whether the
421    /// file has changed. Normally P4 Server uses file digests to determine
422    /// whether files in the workspace differ from the head revisions of
423    /// these files in the depot. This can be time consuming for large files.
424    /// But when the timestamps are the same, the costly digest comparisons
425    /// can be skipped. This option is only relevant if you are using clean
426    /// to find changed files rather than files that were deleted or added.
427    #[cfg(not(feature = "lt2018_1"))]
428    pub fn set_fast_timestamp_check(&mut self, v: bool) -> &mut Self {
429        self.fast_timestamp_check = v;
430        self
431    }
432
433    /// # Description
434    ///
435    /// -m
436    ///
437    /// Compare the file sync or submit time (in the depot) with the file
438    /// modification time (in the workspace) to help determine whether the
439    /// file has changed. Normally P4 Server uses file digests to determine
440    /// whether files in the workspace differ from the head revisions of
441    /// these files in the depot. This can be time consuming for large files.
442    /// But when the timestamps are the same, the costly digest comparisons
443    /// can be skipped. This option is only relevant if you are using clean
444    /// to find changed files rather than files that were deleted or added.
445    #[cfg(not(feature = "lt2018_1"))]
446    pub fn fast_timestamp_check(mut self, v: bool) -> Self {
447        self.fast_timestamp_check = v;
448        self
449    }
450
451    /// # Description
452    ///
453    /// -n
454    ///
455    /// Preview the results of the operation without performing any action.
456    pub fn get_preview(&self) -> bool {
457        self.preview
458    }
459
460    /// # Description
461    ///
462    /// -n
463    ///
464    /// Preview the results of the operation without performing any action.
465    pub fn set_preview(&mut self, v: bool) -> &mut Self {
466        self.preview = v;
467        self
468    }
469
470    /// # Description
471    ///
472    /// -n
473    ///
474    /// Preview the results of the operation without performing any action.
475    pub fn preview(mut self, v: bool) -> Self {
476        self.preview = v;
477        self
478    }
479}
480
481#[cfg(test)]
482mod tests {
483    use super::*;
484    use crate::cmd::args_of;
485
486    #[test]
487    fn bin_accessors_update_executable_path() {
488        let mut command = Clean::new("p4", GlobalOpts::new());
489        assert_eq!(command.get_bin(), Path::new("p4"));
490
491        command.set_bin("/usr/local/bin/p4");
492        assert_eq!(command.get_bin(), Path::new("/usr/local/bin/p4"));
493
494        let command = command.bin("p4.exe");
495        assert_eq!(command.get_bin(), Path::new("p4.exe"));
496    }
497
498    /// Dry-run check of the assembled `p4 clean` command line; no process is
499    /// spawned.
500    #[test]
501    fn without_options() {
502        let clean = Clean::new("p4", GlobalOpts::new());
503
504        assert_eq!(args_of(&clean.setup_command("p4")), ["clean"]);
505    }
506
507    #[test]
508    fn all_local_options() {
509        let mut clean = Clean::new("p4", GlobalOpts::new());
510        clean
511            .set_find_edited(true)
512            .set_find_added(true)
513            .set_find_deleted(true)
514            .set_ignore_p4ignore(true)
515            .set_local_file_syntax(true)
516            .set_preview(true);
517
518        #[cfg(not(feature = "lt2022_1"))]
519        clean.set_suppress_keyword_expansion(true);
520
521        #[cfg(not(feature = "lt2018_1"))]
522        clean.set_fast_timestamp_check(true);
523
524        let mut expected = vec!["clean", "-e", "-a", "-d", "-I", "-l"];
525        #[cfg(not(feature = "lt2022_1"))]
526        expected.push("-K");
527        #[cfg(not(feature = "lt2018_1"))]
528        expected.push("-m");
529        expected.push("-n");
530
531        assert_eq!(args_of(&clean.setup_command("p4")), expected);
532    }
533
534    #[test]
535    fn all_accessors_report_state() {
536        let clean = Clean::new("p4", GlobalOpts::new())
537            .find_edited(true)
538            .find_added(true)
539            .find_deleted(true)
540            .ignore_p4ignore(true)
541            .local_file_syntax(true)
542            .preview(true);
543
544        #[cfg(not(feature = "lt2022_1"))]
545        let clean = clean.suppress_keyword_expansion(true);
546
547        #[cfg(not(feature = "lt2018_1"))]
548        let clean = clean.fast_timestamp_check(true);
549
550        assert!(clean.get_find_edited());
551        assert!(clean.get_find_added());
552        assert!(clean.get_find_deleted());
553        assert!(clean.get_ignore_p4ignore());
554        assert!(clean.get_local_file_syntax());
555        assert!(clean.get_preview());
556
557        #[cfg(not(feature = "lt2022_1"))]
558        assert!(clean.get_suppress_keyword_expansion());
559
560        #[cfg(not(feature = "lt2018_1"))]
561        assert!(clean.get_fast_timestamp_check());
562    }
563
564    #[test]
565    fn global_opts_are_injected_once() {
566        let clean = Clean::new("p4", GlobalOpts::new().port("localhost:1666"));
567
568        assert_eq!(
569            args_of(&clean.setup_command("p4")),
570            ["-p", "localhost:1666", "clean"]
571        );
572    }
573}