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}