Skip to main content

perforce_cli/cmd/
archive.rs

1use std::ffi::OsStr;
2use std::path::PathBuf;
3use std::process::{Child, Command, Stdio};
4
5use super::{ExclusiveOption, SubCommand};
6
7use crate::global::GlobalOpts;
8use crate::spawn::ParameterizedSpawn;
9
10/// Archive files to an archive depot (default mode).
11///
12/// In this mode the revisions to archive can be selected with `-h` and `-z`;
13/// see [`Archive::purge_physical_files`] for the alternative mode.
14#[derive(Debug, Clone, Copy, Default)]
15pub struct SafeArchive {
16    skip_head_revisions: bool,
17
18    #[cfg(not(feature = "lt2019_1"))]
19    include_lazy_copies: bool,
20}
21
22impl ExclusiveOption for SafeArchive {
23    fn inject_args(&self, command: &mut Command) {
24        if self.skip_head_revisions {
25            command.arg("-h");
26        }
27
28        #[cfg(not(feature = "lt2019_1"))]
29        if self.include_lazy_copies {
30            command.arg("-z");
31        }
32    }
33}
34
35/// Remove the physical file revisions of the specified files (`-p`).
36///
37/// Entered with [`Archive::purge_physical_files`]; `-h` and `-z` are not
38/// available in this mode.
39#[derive(Debug, Clone, Copy, Default)]
40pub struct PhysicalPurge;
41
42impl ExclusiveOption for PhysicalPurge {
43    fn inject_args(&self, command: &mut Command) {
44        command.arg("-p");
45    }
46}
47
48#[cfg_attr(
49    feature = "lt2015_1",
50    doc = "`p4 [g-opts] archive [-n -h -p -q -t] -D depot file[revRange] ...`: archive obsolete revisions to an archive depot."
51)]
52#[cfg_attr(
53    all(feature = "lt2018_1", not(feature = "lt2015_1")),
54    doc = "`p4 [g-opts] archive [-h -n -p -q -t] -D depot file[revRange] ...`: archive obsolete revisions to an archive depot."
55)]
56#[cfg_attr(
57    all(feature = "lt2019_1", not(feature = "lt2018_1")),
58    doc = "`p4 [g-opts] archive [-h -n -p -q -t] -D depot File Spec[revSpec]`: archive obsolete revisions to an archive depot."
59)]
60#[cfg_attr(
61    not(feature = "lt2019_1"),
62    doc = "`p4 [g-opts] archive [-n -h -p -q -t -z] -D depot File Spec[revSpec]`: archive obsolete revisions to an archive depot."
63)]
64///
65/// The `S` type parameter tracks the operation mode at compile time; see
66/// [`SafeArchive`], [`PhysicalPurge`], and [`Self::purge_physical_files`].
67#[derive(Debug, Clone, Default)]
68pub struct Archive<S = SafeArchive> {
69    bin: PathBuf,
70
71    global_opts: GlobalOpts,
72
73    preview: bool,
74
75    quiet_mode: bool,
76
77    archive_delta: bool,
78
79    depot: Option<String>,
80
81    archive_mode: S,
82}
83
84impl<S: ExclusiveOption> SubCommand for Archive<S> {
85    fn name(&self) -> &str {
86        "archive"
87    }
88
89    fn inject_local_args(&self, command: &mut Command) {
90        self.archive_mode.inject_args(command);
91
92        if self.preview {
93            command.arg("-n");
94        }
95
96        if self.quiet_mode {
97            command.arg("-q");
98        }
99
100        if self.archive_delta {
101            command.arg("-t");
102        }
103
104        if let Some(depot) = self.depot.as_ref() {
105            command.arg("-D").arg(depot);
106        }
107    }
108
109    fn global_opts(&self) -> Option<&GlobalOpts> {
110        Some(&self.global_opts)
111    }
112}
113
114impl Archive<SafeArchive> {
115    /// Creates a new `p4 archive` command.
116    ///
117    /// `bin` is the path to the Perforce command-line executable.
118    pub fn new(bin: impl Into<PathBuf>, global_opts: GlobalOpts) -> Self {
119        Self {
120            bin: bin.into(),
121            global_opts,
122            ..Default::default()
123        }
124    }
125
126    /// # Description
127    ///
128    /// -p
129    ///
130    #[cfg_attr(
131        feature = "lt2019_1",
132        doc = "Purge any archives of the specified files named in the archive depot.",
133        doc = "(The action for affected revisions is set to `purge` on completion. File",
134        doc = "contents are no longer accessible from `p4 restore`.)"
135    )]
136    #[cfg_attr(
137        all(feature = "lt2019_2", not(feature = "lt2019_1")),
138        doc = "Purge any archives of the specified files named in the archive depot.",
139        doc = "The action for affected revisions is set to `purge` on completion.",
140        doc = "",
141        doc = "**Warning:** File contents are no longer accessible from `p4 restore`."
142    )]
143    #[cfg_attr(
144        all(feature = "lt2024_1", not(feature = "lt2019_2")),
145        doc = "Purge any archives of the specified files named in the archive depot.",
146        doc = "The action for affected revisions is set to `purge` on completion.",
147        doc = "",
148        doc = "**Warning:** File contents are no longer accessible from `p4 restore`.",
149        doc = "",
150        doc = "**Tip:** If you want to retain the metadata of purged files, do one of the",
151        doc = "following:",
152        doc = "",
153        doc = "- use the `-p` option of `p4 obliterate` (recommended)",
154        doc = "- perform the two-step sequence of `p4 archive` followed by",
155        doc = "  `p4 archive -p` (time-consuming)"
156    )]
157    #[cfg_attr(
158        all(feature = "lt2024_2", not(feature = "lt2024_1")),
159        doc = "For the specified files in the archive depot, remove the physical file",
160        doc = "revisions but retain the revision history with the latest action being set",
161        doc = "to `purge`.",
162        doc = "",
163        doc = "**Warning:** File contents are no longer accessible from `p4 restore`.",
164        doc = "",
165        doc = "**Tip:** If you want to retain the metadata of purged files, do one of the",
166        doc = "following:",
167        doc = "",
168        doc = "- use the `-p` option of `p4 obliterate` (recommended)",
169        doc = "- perform the two-step sequence of `p4 archive` followed by",
170        doc = "  `p4 archive -p` (time-consuming)"
171    )]
172    #[cfg_attr(
173        not(feature = "lt2024_2"),
174        doc = "For the specified files in the archive depot, remove the physical file",
175        doc = "revisions but retain the revision history with the latest action being set",
176        doc = "to `purge`. File contents are no longer accessible from `p4 restore`.",
177        doc = "",
178        doc = "If you want to retain the metadata of purged files, do one of the following:",
179        doc = "",
180        doc = "- use the `-p` option of `p4 obliterate` (recommended)",
181        doc = "- perform the two-step sequence of `p4 archive` followed by",
182        doc = "  `p4 archive -p` (time-consuming)"
183    )]
184    ///
185    /// Transitions this command to the [`PhysicalPurge`] state, in which
186    /// the `-h` and `-z` options are not available; any `-h`/`-z` selection
187    /// made in the [`SafeArchive`] state is discarded.
188    pub fn purge_physical_files(self) -> Archive<PhysicalPurge> {
189        Archive {
190            bin: self.bin,
191            global_opts: self.global_opts,
192            preview: self.preview,
193            quiet_mode: self.quiet_mode,
194            archive_delta: self.archive_delta,
195            depot: self.depot,
196            archive_mode: PhysicalPurge,
197        }
198    }
199}
200
201impl<M> ParameterizedSpawn for Archive<M>
202where
203    Archive<M>: SubCommand,
204{
205    type Input<'a> = &'a [&'a OsStr];
206    type Output<'a> = Child;
207    type Error = std::io::Error;
208
209    /// Spawns `p4 archive` for the given file specs as a child process with
210    /// piped standard output and error streams; use the returned [`Child`]
211    /// handle to wait for it or interact with it.
212    fn spawn_with<'a>(&mut self, files: Self::Input<'a>) -> Result<Self::Output<'a>, Self::Error> {
213        self.setup_command(&self.bin)
214            .args(files)
215            .stdout(Stdio::piped())
216            .stderr(Stdio::piped())
217            .spawn()
218    }
219}
220
221impl<M> Archive<M>
222where
223    Archive<M>: SubCommand,
224{
225    /// # Description
226    ///
227    /// g-opts
228    ///
229    #[cfg_attr(
230        feature = "lt2014_2",
231        doc = "See the [Global Options](GlobalOpts) section."
232    )]
233    #[cfg_attr(
234        all(feature = "lt2015_1", not(feature = "lt2014_2")),
235        doc = "See the [“Global Options”](GlobalOpts) section."
236    )]
237    #[cfg_attr(
238        all(feature = "lt2017_1", not(feature = "lt2015_1")),
239        doc = "See [“Global Options”](GlobalOpts)."
240    )]
241    #[cfg_attr(
242        all(feature = "lt2018_2", not(feature = "lt2017_1")),
243        doc = "See [Global Options](GlobalOpts)."
244    )]
245    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
246    pub fn get_global_opts(&self) -> &GlobalOpts {
247        &self.global_opts
248    }
249
250    /// # Description
251    ///
252    /// g-opts
253    ///
254    #[cfg_attr(
255        feature = "lt2014_2",
256        doc = "See the [Global Options](GlobalOpts) section."
257    )]
258    #[cfg_attr(
259        all(feature = "lt2015_1", not(feature = "lt2014_2")),
260        doc = "See the [“Global Options”](GlobalOpts) section."
261    )]
262    #[cfg_attr(
263        all(feature = "lt2017_1", not(feature = "lt2015_1")),
264        doc = "See [“Global Options”](GlobalOpts)."
265    )]
266    #[cfg_attr(
267        all(feature = "lt2018_2", not(feature = "lt2017_1")),
268        doc = "See [Global Options](GlobalOpts)."
269    )]
270    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
271    pub fn set_global_opts(&mut self, v: GlobalOpts) -> &mut Self {
272        self.global_opts = v;
273        self
274    }
275
276    /// # Description
277    ///
278    /// g-opts
279    ///
280    #[cfg_attr(
281        feature = "lt2014_2",
282        doc = "See the [Global Options](GlobalOpts) section."
283    )]
284    #[cfg_attr(
285        all(feature = "lt2015_1", not(feature = "lt2014_2")),
286        doc = "See the [“Global Options”](GlobalOpts) section."
287    )]
288    #[cfg_attr(
289        all(feature = "lt2017_1", not(feature = "lt2015_1")),
290        doc = "See [“Global Options”](GlobalOpts)."
291    )]
292    #[cfg_attr(
293        all(feature = "lt2018_2", not(feature = "lt2017_1")),
294        doc = "See [Global Options](GlobalOpts)."
295    )]
296    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
297    pub fn global_opts(mut self, v: GlobalOpts) -> Self {
298        self.global_opts = v;
299        self
300    }
301
302    /// # Description
303    ///
304    /// -D depot
305    ///
306    /// Specify an archive depot to which files are to be archived.
307    pub fn get_depot(&self) -> Option<&String> {
308        self.depot.as_ref()
309    }
310
311    /// # Description
312    ///
313    /// -D depot
314    ///
315    /// Specify an archive depot to which files are to be archived.
316    pub fn set_depot(&mut self, v: impl Into<String>) -> &mut Self {
317        self.depot = Some(v.into());
318        self
319    }
320
321    /// # Description
322    ///
323    /// -D depot
324    ///
325    /// Specify an archive depot to which files are to be archived.
326    pub fn depot(mut self, v: impl Into<String>) -> Self {
327        self.depot = Some(v.into());
328        self
329    }
330
331    /// # Description
332    ///
333    /// -n
334    ///
335    #[cfg_attr(
336        feature = "lt2018_1",
337        doc = "Do not archive revisions; report on which revisions would have been archived."
338    )]
339    #[cfg_attr(
340        not(feature = "lt2018_1"),
341        doc = "Do not archive revisions. Instead, report on which revisions would have been",
342        doc = "archived."
343    )]
344    pub fn get_preview(&self) -> bool {
345        self.preview
346    }
347
348    /// # Description
349    ///
350    /// -n
351    ///
352    #[cfg_attr(
353        feature = "lt2018_1",
354        doc = "Do not archive revisions; report on which revisions would have been archived."
355    )]
356    #[cfg_attr(
357        not(feature = "lt2018_1"),
358        doc = "Do not archive revisions. Instead, report on which revisions would have been",
359        doc = "archived."
360    )]
361    pub fn set_preview(&mut self, v: bool) -> &mut Self {
362        self.preview = v;
363        self
364    }
365
366    /// # Description
367    ///
368    /// -n
369    ///
370    #[cfg_attr(
371        feature = "lt2018_1",
372        doc = "Do not archive revisions; report on which revisions would have been archived."
373    )]
374    #[cfg_attr(
375        not(feature = "lt2018_1"),
376        doc = "Do not archive revisions. Instead, report on which revisions would have been",
377        doc = "archived."
378    )]
379    pub fn preview(mut self, v: bool) -> Self {
380        self.preview = v;
381        self
382    }
383
384    /// # Description
385    ///
386    /// -q
387    ///
388    #[cfg_attr(
389        feature = "lt2018_1",
390        doc = "Quiet mode; suppress messages about skipped revisions."
391    )]
392    #[cfg_attr(
393        not(feature = "lt2018_1"),
394        doc = "Quiet mode, which suppresses messages about skipped revisions."
395    )]
396    pub fn get_quiet_mode(&self) -> bool {
397        self.quiet_mode
398    }
399
400    /// # Description
401    ///
402    /// -q
403    ///
404    #[cfg_attr(
405        feature = "lt2018_1",
406        doc = "Quiet mode; suppress messages about skipped revisions."
407    )]
408    #[cfg_attr(
409        not(feature = "lt2018_1"),
410        doc = "Quiet mode, which suppresses messages about skipped revisions."
411    )]
412    pub fn set_quiet_mode(&mut self, v: bool) -> &mut Self {
413        self.quiet_mode = v;
414        self
415    }
416
417    /// # Description
418    ///
419    /// -q
420    ///
421    #[cfg_attr(
422        feature = "lt2018_1",
423        doc = "Quiet mode; suppress messages about skipped revisions."
424    )]
425    #[cfg_attr(
426        not(feature = "lt2018_1"),
427        doc = "Quiet mode, which suppresses messages about skipped revisions."
428    )]
429    pub fn quiet_mode(mut self, v: bool) -> Self {
430        self.quiet_mode = v;
431        self
432    }
433
434    /// # Description
435    ///
436    /// -t
437    ///
438    /// Archive text files (or other revisions stored in delta format, such as
439    /// files of type `binary+D`)
440    pub fn get_archive_delta(&self) -> bool {
441        self.archive_delta
442    }
443
444    /// # Description
445    ///
446    /// -t
447    ///
448    /// Archive text files (or other revisions stored in delta format, such as
449    /// files of type `binary+D`)
450    pub fn set_archive_delta(&mut self, v: bool) -> &mut Self {
451        self.archive_delta = v;
452        self
453    }
454
455    /// # Description
456    ///
457    /// -t
458    ///
459    /// Archive text files (or other revisions stored in delta format, such as
460    /// files of type `binary+D`)
461    pub fn archive_delta(mut self, v: bool) -> Self {
462        self.archive_delta = v;
463        self
464    }
465}
466
467impl Archive<SafeArchive> {
468    /// # Description
469    ///
470    /// -h
471    ///
472    /// Do not archive head revisions.
473    pub fn get_skip_head_revisions(&self) -> bool {
474        self.archive_mode.skip_head_revisions
475    }
476
477    /// # Description
478    ///
479    /// -h
480    ///
481    /// Do not archive head revisions.
482    pub fn set_skip_head_revisions(&mut self, v: bool) -> &mut Self {
483        self.archive_mode.skip_head_revisions = v;
484        self
485    }
486
487    /// # Description
488    ///
489    /// -h
490    ///
491    /// Do not archive head revisions.
492    pub fn skip_head_revisions(mut self, v: bool) -> Self {
493        self.archive_mode.skip_head_revisions = v;
494        self
495    }
496
497    /// # Description
498    ///
499    /// -z
500    ///
501    #[cfg_attr(
502        all(feature = "lt2021_1", not(feature = "lt2019_1")),
503        doc = "Can reduce the use of disk space because it includes in the archive any files",
504        doc = "that have lazy copies or are lazy copies. (A lazy copy is a reference to the",
505        doc = "location of the full file.) With this flag, only Criteria 1 and 2 must be",
506        doc = "met. Unless all copies are archived, the original file remains in the depot."
507    )]
508    #[cfg_attr(
509        not(feature = "lt2021_1"),
510        doc = "Includes any files that have lazy copies or are lazy copies. See Criteria",
511        doc = "with `-z`."
512    )]
513    #[cfg(not(feature = "lt2019_1"))]
514    pub fn get_include_lazy_copies(&self) -> bool {
515        self.archive_mode.include_lazy_copies
516    }
517
518    /// # Description
519    ///
520    /// -z
521    ///
522    #[cfg_attr(
523        all(feature = "lt2021_1", not(feature = "lt2019_1")),
524        doc = "Can reduce the use of disk space because it includes in the archive any files",
525        doc = "that have lazy copies or are lazy copies. (A lazy copy is a reference to the",
526        doc = "location of the full file.) With this flag, only Criteria 1 and 2 must be",
527        doc = "met. Unless all copies are archived, the original file remains in the depot."
528    )]
529    #[cfg_attr(
530        not(feature = "lt2021_1"),
531        doc = "Includes any files that have lazy copies or are lazy copies. See Criteria",
532        doc = "with `-z`."
533    )]
534    #[cfg(not(feature = "lt2019_1"))]
535    pub fn set_include_lazy_copies(&mut self, v: bool) -> &mut Self {
536        self.archive_mode.include_lazy_copies = v;
537        self
538    }
539
540    /// # Description
541    ///
542    /// -z
543    ///
544    #[cfg_attr(
545        all(feature = "lt2021_1", not(feature = "lt2019_1")),
546        doc = "Can reduce the use of disk space because it includes in the archive any files",
547        doc = "that have lazy copies or are lazy copies. (A lazy copy is a reference to the",
548        doc = "location of the full file.) With this flag, only Criteria 1 and 2 must be",
549        doc = "met. Unless all copies are archived, the original file remains in the depot."
550    )]
551    #[cfg_attr(
552        not(feature = "lt2021_1"),
553        doc = "Includes any files that have lazy copies or are lazy copies. See Criteria",
554        doc = "with `-z`."
555    )]
556    #[cfg(not(feature = "lt2019_1"))]
557    pub fn include_lazy_copies(mut self, v: bool) -> Self {
558        self.archive_mode.include_lazy_copies = v;
559        self
560    }
561}
562
563#[cfg(test)]
564mod tests {
565    use super::*;
566    use crate::cmd::args_of;
567
568    /// Dry-run checks of the assembled `p4 archive` command line; no process
569    /// is spawned.
570    #[test]
571    fn without_options() {
572        let archive = Archive::new("p4", GlobalOpts::new());
573
574        assert_eq!(args_of(&archive.setup_command("p4")), ["archive"]);
575    }
576
577    #[test]
578    fn all_safe_archive_options() {
579        let mut archive = Archive::new("p4", GlobalOpts::new());
580        archive
581            .set_skip_head_revisions(true)
582            .set_preview(true)
583            .set_quiet_mode(true)
584            .set_archive_delta(true)
585            .set_depot("archives");
586        #[cfg(not(feature = "lt2019_1"))]
587        archive.set_include_lazy_copies(true);
588
589        #[cfg_attr(feature = "lt2019_1", allow(unused_mut))]
590        let mut expected = vec!["archive", "-h"];
591        #[cfg(not(feature = "lt2019_1"))]
592        expected.push("-z");
593        expected.extend(["-n", "-q", "-t", "-D", "archives"]);
594
595        assert_eq!(args_of(&archive.setup_command("p4")), expected);
596    }
597
598    #[test]
599    fn physical_purge_mode() {
600        let mut archive = Archive::new("p4", GlobalOpts::new());
601        archive
602            .set_preview(true)
603            .set_quiet_mode(true)
604            .set_archive_delta(true)
605            .set_depot("archives");
606
607        let archive = archive.purge_physical_files();
608
609        assert_eq!(
610            args_of(&archive.setup_command("p4")),
611            ["archive", "-p", "-n", "-q", "-t", "-D", "archives"]
612        );
613    }
614
615    /// `-h` and `-z` are not available in the physical purge mode; values set
616    /// before the transition must not leak into the command line.
617    #[test]
618    fn archive_selection_flags_not_injected_after_purge() {
619        let mut archive = Archive::new("p4", GlobalOpts::new());
620        archive.set_skip_head_revisions(true);
621        #[cfg(not(feature = "lt2019_1"))]
622        archive.set_include_lazy_copies(true);
623
624        let archive = archive.purge_physical_files();
625
626        assert_eq!(args_of(&archive.setup_command("p4")), ["archive", "-p"]);
627    }
628
629    #[cfg(not(feature = "lt2019_1"))]
630    #[test]
631    fn lazy_copies_flag() {
632        let archive = Archive::new("p4", GlobalOpts::new())
633            .depot("archives")
634            .include_lazy_copies(true);
635
636        assert!(archive.get_include_lazy_copies());
637        assert_eq!(
638            args_of(&archive.setup_command("p4")),
639            ["archive", "-z", "-D", "archives"]
640        );
641    }
642
643    #[test]
644    fn builder_style_with_str_and_global_opts() {
645        let archive = Archive::new("p4", GlobalOpts::new().port("localhost:1666"))
646            .depot("archives")
647            .preview(true);
648
649        assert_eq!(archive.get_depot(), Some(&"archives".to_string()));
650        assert_eq!(
651            args_of(&archive.setup_command("p4")),
652            ["-p", "localhost:1666", "archive", "-n", "-D", "archives"]
653        );
654    }
655}