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, 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    /// # Description
227    ///
228    /// g-opts
229    ///
230    #[cfg_attr(
231        feature = "lt2014_2",
232        doc = "See the [Global Options](GlobalOpts) section."
233    )]
234    #[cfg_attr(
235        all(feature = "lt2015_1", not(feature = "lt2014_2")),
236        doc = "See the [“Global Options”](GlobalOpts) section."
237    )]
238    #[cfg_attr(
239        all(feature = "lt2017_1", not(feature = "lt2015_1")),
240        doc = "See [“Global Options”](GlobalOpts)."
241    )]
242    #[cfg_attr(
243        all(feature = "lt2018_2", not(feature = "lt2017_1")),
244        doc = "See [Global Options](GlobalOpts)."
245    )]
246    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
247    pub fn get_global_opts(&self) -> &GlobalOpts {
248        &self.global_opts
249    }
250
251    /// # Description
252    ///
253    /// g-opts
254    ///
255    #[cfg_attr(
256        feature = "lt2014_2",
257        doc = "See the [Global Options](GlobalOpts) section."
258    )]
259    #[cfg_attr(
260        all(feature = "lt2015_1", not(feature = "lt2014_2")),
261        doc = "See the [“Global Options”](GlobalOpts) section."
262    )]
263    #[cfg_attr(
264        all(feature = "lt2017_1", not(feature = "lt2015_1")),
265        doc = "See [“Global Options”](GlobalOpts)."
266    )]
267    #[cfg_attr(
268        all(feature = "lt2018_2", not(feature = "lt2017_1")),
269        doc = "See [Global Options](GlobalOpts)."
270    )]
271    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
272    pub fn set_global_opts(&mut self, v: GlobalOpts) -> &mut Self {
273        self.global_opts = v;
274        self
275    }
276
277    /// # Description
278    ///
279    /// g-opts
280    ///
281    #[cfg_attr(
282        feature = "lt2014_2",
283        doc = "See the [Global Options](GlobalOpts) section."
284    )]
285    #[cfg_attr(
286        all(feature = "lt2015_1", not(feature = "lt2014_2")),
287        doc = "See the [“Global Options”](GlobalOpts) section."
288    )]
289    #[cfg_attr(
290        all(feature = "lt2017_1", not(feature = "lt2015_1")),
291        doc = "See [“Global Options”](GlobalOpts)."
292    )]
293    #[cfg_attr(
294        all(feature = "lt2018_2", not(feature = "lt2017_1")),
295        doc = "See [Global Options](GlobalOpts)."
296    )]
297    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
298    pub fn global_opts(mut self, v: GlobalOpts) -> Self {
299        self.global_opts = v;
300        self
301    }
302
303    /// # Description
304    ///
305    /// -D depot
306    ///
307    /// Specify an archive depot to which files are to be archived.
308    pub fn get_depot(&self) -> Option<&String> {
309        self.depot.as_ref()
310    }
311
312    /// # Description
313    ///
314    /// -D depot
315    ///
316    /// Specify an archive depot to which files are to be archived.
317    pub fn set_depot(&mut self, v: impl Into<String>) -> &mut Self {
318        self.depot = Some(v.into());
319        self
320    }
321
322    /// # Description
323    ///
324    /// -D depot
325    ///
326    /// Specify an archive depot to which files are to be archived.
327    pub fn depot(mut self, v: impl Into<String>) -> Self {
328        self.depot = Some(v.into());
329        self
330    }
331
332    /// # Description
333    ///
334    /// -n
335    ///
336    #[cfg_attr(
337        feature = "lt2018_1",
338        doc = "Do not archive revisions; report on which revisions would have been archived."
339    )]
340    #[cfg_attr(
341        not(feature = "lt2018_1"),
342        doc = "Do not archive revisions. Instead, report on which revisions would have been",
343        doc = "archived."
344    )]
345    pub fn get_preview(&self) -> bool {
346        self.preview
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 set_preview(&mut self, v: bool) -> &mut Self {
363        self.preview = v;
364        self
365    }
366
367    /// # Description
368    ///
369    /// -n
370    ///
371    #[cfg_attr(
372        feature = "lt2018_1",
373        doc = "Do not archive revisions; report on which revisions would have been archived."
374    )]
375    #[cfg_attr(
376        not(feature = "lt2018_1"),
377        doc = "Do not archive revisions. Instead, report on which revisions would have been",
378        doc = "archived."
379    )]
380    pub fn preview(mut self, v: bool) -> Self {
381        self.preview = v;
382        self
383    }
384
385    /// # Description
386    ///
387    /// -q
388    ///
389    #[cfg_attr(
390        feature = "lt2018_1",
391        doc = "Quiet mode; suppress messages about skipped revisions."
392    )]
393    #[cfg_attr(
394        not(feature = "lt2018_1"),
395        doc = "Quiet mode, which suppresses messages about skipped revisions."
396    )]
397    pub fn get_quiet_mode(&self) -> bool {
398        self.quiet_mode
399    }
400
401    /// # Description
402    ///
403    /// -q
404    ///
405    #[cfg_attr(
406        feature = "lt2018_1",
407        doc = "Quiet mode; suppress messages about skipped revisions."
408    )]
409    #[cfg_attr(
410        not(feature = "lt2018_1"),
411        doc = "Quiet mode, which suppresses messages about skipped revisions."
412    )]
413    pub fn set_quiet_mode(&mut self, v: bool) -> &mut Self {
414        self.quiet_mode = v;
415        self
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 quiet_mode(mut self, v: bool) -> Self {
431        self.quiet_mode = v;
432        self
433    }
434
435    /// # Description
436    ///
437    /// -t
438    ///
439    /// Archive text files (or other revisions stored in delta format, such as
440    /// files of type `binary+D`)
441    pub fn get_archive_delta(&self) -> bool {
442        self.archive_delta
443    }
444
445    /// # Description
446    ///
447    /// -t
448    ///
449    /// Archive text files (or other revisions stored in delta format, such as
450    /// files of type `binary+D`)
451    pub fn set_archive_delta(&mut self, v: bool) -> &mut Self {
452        self.archive_delta = v;
453        self
454    }
455
456    /// # Description
457    ///
458    /// -t
459    ///
460    /// Archive text files (or other revisions stored in delta format, such as
461    /// files of type `binary+D`)
462    pub fn archive_delta(mut self, v: bool) -> Self {
463        self.archive_delta = v;
464        self
465    }
466}
467
468impl Archive<SafeArchive> {
469    /// # Description
470    ///
471    /// -h
472    ///
473    /// Do not archive head revisions.
474    pub fn get_skip_head_revisions(&self) -> bool {
475        self.archive_mode.skip_head_revisions
476    }
477
478    /// # Description
479    ///
480    /// -h
481    ///
482    /// Do not archive head revisions.
483    pub fn set_skip_head_revisions(&mut self, v: bool) -> &mut Self {
484        self.archive_mode.skip_head_revisions = v;
485        self
486    }
487
488    /// # Description
489    ///
490    /// -h
491    ///
492    /// Do not archive head revisions.
493    pub fn skip_head_revisions(mut self, v: bool) -> Self {
494        self.archive_mode.skip_head_revisions = v;
495        self
496    }
497
498    /// # Description
499    ///
500    /// -z
501    ///
502    #[cfg_attr(
503        all(feature = "lt2021_1", not(feature = "lt2019_1")),
504        doc = "Can reduce the use of disk space because it includes in the archive any files",
505        doc = "that have lazy copies or are lazy copies. (A lazy copy is a reference to the",
506        doc = "location of the full file.) With this flag, only Criteria 1 and 2 must be",
507        doc = "met. Unless all copies are archived, the original file remains in the depot."
508    )]
509    #[cfg_attr(
510        not(feature = "lt2021_1"),
511        doc = "Includes any files that have lazy copies or are lazy copies. See Criteria",
512        doc = "with `-z`."
513    )]
514    #[cfg(not(feature = "lt2019_1"))]
515    pub fn get_include_lazy_copies(&self) -> bool {
516        self.archive_mode.include_lazy_copies
517    }
518
519    /// # Description
520    ///
521    /// -z
522    ///
523    #[cfg_attr(
524        all(feature = "lt2021_1", not(feature = "lt2019_1")),
525        doc = "Can reduce the use of disk space because it includes in the archive any files",
526        doc = "that have lazy copies or are lazy copies. (A lazy copy is a reference to the",
527        doc = "location of the full file.) With this flag, only Criteria 1 and 2 must be",
528        doc = "met. Unless all copies are archived, the original file remains in the depot."
529    )]
530    #[cfg_attr(
531        not(feature = "lt2021_1"),
532        doc = "Includes any files that have lazy copies or are lazy copies. See Criteria",
533        doc = "with `-z`."
534    )]
535    #[cfg(not(feature = "lt2019_1"))]
536    pub fn set_include_lazy_copies(&mut self, v: bool) -> &mut Self {
537        self.archive_mode.include_lazy_copies = v;
538        self
539    }
540
541    /// # Description
542    ///
543    /// -z
544    ///
545    #[cfg_attr(
546        all(feature = "lt2021_1", not(feature = "lt2019_1")),
547        doc = "Can reduce the use of disk space because it includes in the archive any files",
548        doc = "that have lazy copies or are lazy copies. (A lazy copy is a reference to the",
549        doc = "location of the full file.) With this flag, only Criteria 1 and 2 must be",
550        doc = "met. Unless all copies are archived, the original file remains in the depot."
551    )]
552    #[cfg_attr(
553        not(feature = "lt2021_1"),
554        doc = "Includes any files that have lazy copies or are lazy copies. See Criteria",
555        doc = "with `-z`."
556    )]
557    #[cfg(not(feature = "lt2019_1"))]
558    pub fn include_lazy_copies(mut self, v: bool) -> Self {
559        self.archive_mode.include_lazy_copies = v;
560        self
561    }
562}
563
564#[cfg(test)]
565mod tests {
566    use super::*;
567    use crate::cmd::args_of;
568
569    /// Dry-run checks of the assembled `p4 archive` command line; no process
570    /// is spawned.
571    #[test]
572    fn without_options() {
573        let archive = Archive::new("p4", GlobalOpts::new());
574
575        assert_eq!(args_of(&archive.setup_command("p4")), ["archive"]);
576    }
577
578    #[test]
579    fn all_safe_archive_options() {
580        let mut archive = Archive::new("p4", GlobalOpts::new());
581        archive
582            .set_skip_head_revisions(true)
583            .set_preview(true)
584            .set_quiet_mode(true)
585            .set_archive_delta(true)
586            .set_depot("archives");
587        #[cfg(not(feature = "lt2019_1"))]
588        archive.set_include_lazy_copies(true);
589
590        #[cfg_attr(feature = "lt2019_1", allow(unused_mut))]
591        let mut expected = vec!["archive", "-h"];
592        #[cfg(not(feature = "lt2019_1"))]
593        expected.push("-z");
594        expected.extend(["-n", "-q", "-t", "-D", "archives"]);
595
596        assert_eq!(args_of(&archive.setup_command("p4")), expected);
597    }
598
599    #[test]
600    fn physical_purge_mode() {
601        let mut archive = Archive::new("p4", GlobalOpts::new());
602        archive
603            .set_preview(true)
604            .set_quiet_mode(true)
605            .set_archive_delta(true)
606            .set_depot("archives");
607
608        let archive = archive.purge_physical_files();
609
610        assert_eq!(
611            args_of(&archive.setup_command("p4")),
612            ["archive", "-p", "-n", "-q", "-t", "-D", "archives"]
613        );
614    }
615
616    /// `-h` and `-z` are not available in the physical purge mode; values set
617    /// before the transition must not leak into the command line.
618    #[test]
619    fn archive_selection_flags_not_injected_after_purge() {
620        let mut archive = Archive::new("p4", GlobalOpts::new());
621        archive.set_skip_head_revisions(true);
622        #[cfg(not(feature = "lt2019_1"))]
623        archive.set_include_lazy_copies(true);
624
625        let archive = archive.purge_physical_files();
626
627        assert_eq!(args_of(&archive.setup_command("p4")), ["archive", "-p"]);
628    }
629
630    #[cfg(not(feature = "lt2019_1"))]
631    #[test]
632    fn lazy_copies_flag() {
633        let archive = Archive::new("p4", GlobalOpts::new())
634            .depot("archives")
635            .include_lazy_copies(true);
636
637        assert!(archive.get_include_lazy_copies());
638        assert_eq!(
639            args_of(&archive.setup_command("p4")),
640            ["archive", "-z", "-D", "archives"]
641        );
642    }
643
644    #[test]
645    fn builder_style_with_str_and_global_opts() {
646        let archive = Archive::new("p4", GlobalOpts::new().port("localhost:1666"))
647            .depot("archives")
648            .preview(true);
649
650        assert_eq!(archive.get_depot(), Some(&"archives".to_string()));
651        assert_eq!(
652            args_of(&archive.setup_command("p4")),
653            ["-p", "localhost:1666", "archive", "-n", "-D", "archives"]
654        );
655    }
656}