Skip to main content

perforce_cli/cmd/
archive.rs

1use std::ffi::OsStr;
2use std::path::{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, S, I> ParameterizedSpawn<(S,)> for Archive<M>
202where
203    Archive<M>: SubCommand,
204    S: IntoIterator<Item = I>,
205    I: AsRef<OsStr>,
206{
207    type Output = Child;
208    type Error = std::io::Error;
209
210    /// Spawns `p4 archive` for the given file specs as a child process with
211    /// piped standard output and error streams; use the returned [`Child`]
212    /// handle to wait for it or interact with it.
213    fn spawn_with(&mut self, (files,): (S,)) -> Result<Self::Output, Self::Error> {
214        self.setup_command(&self.bin)
215            .args(files)
216            .stdout(Stdio::piped())
217            .stderr(Stdio::piped())
218            .spawn()
219    }
220}
221
222impl<M> Archive<M>
223where
224    Archive<M>: SubCommand,
225{
226    /// Get the Perforce command path.
227    pub fn get_bin(&self) -> &Path {
228        &self.bin
229    }
230
231    /// Set the Perforce command path.
232    pub fn set_bin(&mut self, bin: impl Into<PathBuf>) -> &mut Self {
233        self.bin = bin.into();
234        self
235    }
236
237    /// Set the Perforce command path.
238    pub fn bin(mut self, bin: impl Into<PathBuf>) -> Self {
239        self.set_bin(bin);
240        self
241    }
242
243    /// # Description
244    ///
245    /// g-opts
246    ///
247    #[cfg_attr(
248        feature = "lt2014_2",
249        doc = "See the [Global Options](GlobalOpts) section."
250    )]
251    #[cfg_attr(
252        all(feature = "lt2015_1", not(feature = "lt2014_2")),
253        doc = "See the [“Global Options”](GlobalOpts) section."
254    )]
255    #[cfg_attr(
256        all(feature = "lt2017_1", not(feature = "lt2015_1")),
257        doc = "See [“Global Options”](GlobalOpts)."
258    )]
259    #[cfg_attr(
260        all(feature = "lt2018_2", not(feature = "lt2017_1")),
261        doc = "See [Global Options](GlobalOpts)."
262    )]
263    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
264    pub fn get_global_opts(&self) -> &GlobalOpts {
265        &self.global_opts
266    }
267
268    /// # Description
269    ///
270    /// g-opts
271    ///
272    #[cfg_attr(
273        feature = "lt2014_2",
274        doc = "See the [Global Options](GlobalOpts) section."
275    )]
276    #[cfg_attr(
277        all(feature = "lt2015_1", not(feature = "lt2014_2")),
278        doc = "See the [“Global Options”](GlobalOpts) section."
279    )]
280    #[cfg_attr(
281        all(feature = "lt2017_1", not(feature = "lt2015_1")),
282        doc = "See [“Global Options”](GlobalOpts)."
283    )]
284    #[cfg_attr(
285        all(feature = "lt2018_2", not(feature = "lt2017_1")),
286        doc = "See [Global Options](GlobalOpts)."
287    )]
288    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
289    pub fn set_global_opts(&mut self, v: GlobalOpts) -> &mut Self {
290        self.global_opts = v;
291        self
292    }
293
294    /// # Description
295    ///
296    /// g-opts
297    ///
298    #[cfg_attr(
299        feature = "lt2014_2",
300        doc = "See the [Global Options](GlobalOpts) section."
301    )]
302    #[cfg_attr(
303        all(feature = "lt2015_1", not(feature = "lt2014_2")),
304        doc = "See the [“Global Options”](GlobalOpts) section."
305    )]
306    #[cfg_attr(
307        all(feature = "lt2017_1", not(feature = "lt2015_1")),
308        doc = "See [“Global Options”](GlobalOpts)."
309    )]
310    #[cfg_attr(
311        all(feature = "lt2018_2", not(feature = "lt2017_1")),
312        doc = "See [Global Options](GlobalOpts)."
313    )]
314    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
315    pub fn global_opts(mut self, v: GlobalOpts) -> Self {
316        self.global_opts = v;
317        self
318    }
319
320    /// # Description
321    ///
322    /// -D depot
323    ///
324    /// Specify an archive depot to which files are to be archived.
325    pub fn get_depot(&self) -> Option<&String> {
326        self.depot.as_ref()
327    }
328
329    /// # Description
330    ///
331    /// -D depot
332    ///
333    /// Specify an archive depot to which files are to be archived.
334    pub fn set_depot(&mut self, v: impl Into<String>) -> &mut Self {
335        self.depot = Some(v.into());
336        self
337    }
338
339    /// # Description
340    ///
341    /// -D depot
342    ///
343    /// Specify an archive depot to which files are to be archived.
344    pub fn depot(mut self, v: impl Into<String>) -> Self {
345        self.depot = Some(v.into());
346        self
347    }
348
349    /// # Description
350    ///
351    /// -n
352    ///
353    #[cfg_attr(
354        feature = "lt2018_1",
355        doc = "Do not archive revisions; report on which revisions would have been archived."
356    )]
357    #[cfg_attr(
358        not(feature = "lt2018_1"),
359        doc = "Do not archive revisions. Instead, report on which revisions would have been",
360        doc = "archived."
361    )]
362    pub fn get_preview(&self) -> bool {
363        self.preview
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 set_preview(&mut self, v: bool) -> &mut Self {
380        self.preview = v;
381        self
382    }
383
384    /// # Description
385    ///
386    /// -n
387    ///
388    #[cfg_attr(
389        feature = "lt2018_1",
390        doc = "Do not archive revisions; report on which revisions would have been archived."
391    )]
392    #[cfg_attr(
393        not(feature = "lt2018_1"),
394        doc = "Do not archive revisions. Instead, report on which revisions would have been",
395        doc = "archived."
396    )]
397    pub fn preview(mut self, v: bool) -> Self {
398        self.preview = v;
399        self
400    }
401
402    /// # Description
403    ///
404    /// -q
405    ///
406    #[cfg_attr(
407        feature = "lt2018_1",
408        doc = "Quiet mode; suppress messages about skipped revisions."
409    )]
410    #[cfg_attr(
411        not(feature = "lt2018_1"),
412        doc = "Quiet mode, which suppresses messages about skipped revisions."
413    )]
414    pub fn get_quiet_mode(&self) -> bool {
415        self.quiet_mode
416    }
417
418    /// # Description
419    ///
420    /// -q
421    ///
422    #[cfg_attr(
423        feature = "lt2018_1",
424        doc = "Quiet mode; suppress messages about skipped revisions."
425    )]
426    #[cfg_attr(
427        not(feature = "lt2018_1"),
428        doc = "Quiet mode, which suppresses messages about skipped revisions."
429    )]
430    pub fn set_quiet_mode(&mut self, v: bool) -> &mut Self {
431        self.quiet_mode = v;
432        self
433    }
434
435    /// # Description
436    ///
437    /// -q
438    ///
439    #[cfg_attr(
440        feature = "lt2018_1",
441        doc = "Quiet mode; suppress messages about skipped revisions."
442    )]
443    #[cfg_attr(
444        not(feature = "lt2018_1"),
445        doc = "Quiet mode, which suppresses messages about skipped revisions."
446    )]
447    pub fn quiet_mode(mut self, v: bool) -> Self {
448        self.quiet_mode = v;
449        self
450    }
451
452    /// # Description
453    ///
454    /// -t
455    ///
456    /// Archive text files (or other revisions stored in delta format, such as
457    /// files of type `binary+D`)
458    pub fn get_archive_delta(&self) -> bool {
459        self.archive_delta
460    }
461
462    /// # Description
463    ///
464    /// -t
465    ///
466    /// Archive text files (or other revisions stored in delta format, such as
467    /// files of type `binary+D`)
468    pub fn set_archive_delta(&mut self, v: bool) -> &mut Self {
469        self.archive_delta = v;
470        self
471    }
472
473    /// # Description
474    ///
475    /// -t
476    ///
477    /// Archive text files (or other revisions stored in delta format, such as
478    /// files of type `binary+D`)
479    pub fn archive_delta(mut self, v: bool) -> Self {
480        self.archive_delta = v;
481        self
482    }
483}
484
485impl Archive<SafeArchive> {
486    /// # Description
487    ///
488    /// -h
489    ///
490    /// Do not archive head revisions.
491    pub fn get_skip_head_revisions(&self) -> bool {
492        self.archive_mode.skip_head_revisions
493    }
494
495    /// # Description
496    ///
497    /// -h
498    ///
499    /// Do not archive head revisions.
500    pub fn set_skip_head_revisions(&mut self, v: bool) -> &mut Self {
501        self.archive_mode.skip_head_revisions = v;
502        self
503    }
504
505    /// # Description
506    ///
507    /// -h
508    ///
509    /// Do not archive head revisions.
510    pub fn skip_head_revisions(mut self, v: bool) -> Self {
511        self.archive_mode.skip_head_revisions = v;
512        self
513    }
514
515    /// # Description
516    ///
517    /// -z
518    ///
519    #[cfg_attr(
520        all(feature = "lt2021_1", not(feature = "lt2019_1")),
521        doc = "Can reduce the use of disk space because it includes in the archive any files",
522        doc = "that have lazy copies or are lazy copies. (A lazy copy is a reference to the",
523        doc = "location of the full file.) With this flag, only Criteria 1 and 2 must be",
524        doc = "met. Unless all copies are archived, the original file remains in the depot."
525    )]
526    #[cfg_attr(
527        not(feature = "lt2021_1"),
528        doc = "Includes any files that have lazy copies or are lazy copies. See Criteria",
529        doc = "with `-z`."
530    )]
531    #[cfg(not(feature = "lt2019_1"))]
532    pub fn get_include_lazy_copies(&self) -> bool {
533        self.archive_mode.include_lazy_copies
534    }
535
536    /// # Description
537    ///
538    /// -z
539    ///
540    #[cfg_attr(
541        all(feature = "lt2021_1", not(feature = "lt2019_1")),
542        doc = "Can reduce the use of disk space because it includes in the archive any files",
543        doc = "that have lazy copies or are lazy copies. (A lazy copy is a reference to the",
544        doc = "location of the full file.) With this flag, only Criteria 1 and 2 must be",
545        doc = "met. Unless all copies are archived, the original file remains in the depot."
546    )]
547    #[cfg_attr(
548        not(feature = "lt2021_1"),
549        doc = "Includes any files that have lazy copies or are lazy copies. See Criteria",
550        doc = "with `-z`."
551    )]
552    #[cfg(not(feature = "lt2019_1"))]
553    pub fn set_include_lazy_copies(&mut self, v: bool) -> &mut Self {
554        self.archive_mode.include_lazy_copies = v;
555        self
556    }
557
558    /// # Description
559    ///
560    /// -z
561    ///
562    #[cfg_attr(
563        all(feature = "lt2021_1", not(feature = "lt2019_1")),
564        doc = "Can reduce the use of disk space because it includes in the archive any files",
565        doc = "that have lazy copies or are lazy copies. (A lazy copy is a reference to the",
566        doc = "location of the full file.) With this flag, only Criteria 1 and 2 must be",
567        doc = "met. Unless all copies are archived, the original file remains in the depot."
568    )]
569    #[cfg_attr(
570        not(feature = "lt2021_1"),
571        doc = "Includes any files that have lazy copies or are lazy copies. See Criteria",
572        doc = "with `-z`."
573    )]
574    #[cfg(not(feature = "lt2019_1"))]
575    pub fn include_lazy_copies(mut self, v: bool) -> Self {
576        self.archive_mode.include_lazy_copies = v;
577        self
578    }
579}
580
581#[cfg(test)]
582mod tests {
583    use super::*;
584    use crate::cmd::args_of;
585
586    #[test]
587    fn bin_accessors_update_executable_path() {
588        let mut command = Archive::new("p4", GlobalOpts::new());
589        assert_eq!(command.get_bin(), Path::new("p4"));
590
591        command.set_bin("/usr/local/bin/p4");
592        assert_eq!(command.get_bin(), Path::new("/usr/local/bin/p4"));
593
594        let command = command.bin("p4.exe");
595        assert_eq!(command.get_bin(), Path::new("p4.exe"));
596    }
597
598    /// Dry-run checks of the assembled `p4 archive` command line; no process
599    /// is spawned.
600    #[test]
601    fn without_options() {
602        let archive = Archive::new("p4", GlobalOpts::new());
603
604        assert_eq!(args_of(&archive.setup_command("p4")), ["archive"]);
605    }
606
607    #[test]
608    fn all_safe_archive_options() {
609        let mut archive = Archive::new("p4", GlobalOpts::new());
610        archive
611            .set_skip_head_revisions(true)
612            .set_preview(true)
613            .set_quiet_mode(true)
614            .set_archive_delta(true)
615            .set_depot("archives");
616        #[cfg(not(feature = "lt2019_1"))]
617        archive.set_include_lazy_copies(true);
618
619        #[cfg_attr(feature = "lt2019_1", allow(unused_mut))]
620        let mut expected = vec!["archive", "-h"];
621        #[cfg(not(feature = "lt2019_1"))]
622        expected.push("-z");
623        expected.extend(["-n", "-q", "-t", "-D", "archives"]);
624
625        assert_eq!(args_of(&archive.setup_command("p4")), expected);
626    }
627
628    #[test]
629    fn physical_purge_mode() {
630        let mut archive = Archive::new("p4", GlobalOpts::new());
631        archive
632            .set_preview(true)
633            .set_quiet_mode(true)
634            .set_archive_delta(true)
635            .set_depot("archives");
636
637        let archive = archive.purge_physical_files();
638
639        assert_eq!(
640            args_of(&archive.setup_command("p4")),
641            ["archive", "-p", "-n", "-q", "-t", "-D", "archives"]
642        );
643    }
644
645    /// `-h` and `-z` are not available in the physical purge mode; values set
646    /// before the transition must not leak into the command line.
647    #[test]
648    fn archive_selection_flags_not_injected_after_purge() {
649        let mut archive = Archive::new("p4", GlobalOpts::new());
650        archive.set_skip_head_revisions(true);
651        #[cfg(not(feature = "lt2019_1"))]
652        archive.set_include_lazy_copies(true);
653
654        let archive = archive.purge_physical_files();
655
656        assert_eq!(args_of(&archive.setup_command("p4")), ["archive", "-p"]);
657    }
658
659    #[cfg(not(feature = "lt2019_1"))]
660    #[test]
661    fn lazy_copies_flag() {
662        let archive = Archive::new("p4", GlobalOpts::new())
663            .depot("archives")
664            .include_lazy_copies(true);
665
666        assert!(archive.get_include_lazy_copies());
667        assert_eq!(
668            args_of(&archive.setup_command("p4")),
669            ["archive", "-z", "-D", "archives"]
670        );
671    }
672
673    #[test]
674    fn builder_style_with_str_and_global_opts() {
675        let archive = Archive::new("p4", GlobalOpts::new().port("localhost:1666"))
676            .depot("archives")
677            .preview(true);
678
679        assert_eq!(archive.get_depot(), Some(&"archives".to_string()));
680        assert_eq!(
681            args_of(&archive.setup_command("p4")),
682            ["-p", "localhost:1666", "archive", "-n", "-D", "archives"]
683        );
684    }
685}