Skip to main content

perforce_cli/cmd/
annotate.rs

1use std::ffi::OsStr;
2use std::path::PathBuf;
3use std::process::{Child, Command, Stdio};
4
5use super::{ExclusiveOption, SubCommand, Unselected};
6
7use crate::global::GlobalOpts;
8use crate::spawn::ParameterizedSpawn;
9
10/// Internal representation of the `-T` / `--tab=N` tab stop setting.
11#[cfg(not(feature = "lt2016_1"))]
12#[derive(Debug, Clone, Copy)]
13enum Tab {
14    /// `-T`: align output to the default tab stop of 8.
15    Default,
16    /// `--tab=N`: align output to a tab stop of N.
17    Custom(u32),
18}
19
20/// Variants of the `[-i | -I]` mutually exclusive option group of
21/// `p4 annotate`.
22pub mod follow {
23    /// Follow file history across branches (`-i`).
24    #[derive(Debug, Clone, Copy, Default)]
25    pub struct Branches;
26
27    /// Follow integrations into the file (`-I`).
28    #[derive(Debug, Clone, Copy, Default)]
29    pub struct Integrations;
30}
31
32impl ExclusiveOption for follow::Branches {
33    fn inject_args(&self, command: &mut Command) {
34        command.arg("-i");
35    }
36}
37
38impl ExclusiveOption for follow::Integrations {
39    fn inject_args(&self, command: &mut Command) {
40        command.arg("-I");
41    }
42}
43
44#[cfg_attr(
45    feature = "lt2014_2",
46    doc = "`p4 [g-opts] annotate [-a -c -i -I -q -t -d flag] file[revRange] ...`: print file lines along with their revisions."
47)]
48#[cfg_attr(
49    all(feature = "lt2015_1", not(feature = "lt2014_2")),
50    doc = "`p4 [g-opts] annotate [-a -c -i -I -q -t -d options] file[revRange] ...`: print file lines along with their revisions."
51)]
52#[cfg_attr(
53    all(feature = "lt2015_2", not(feature = "lt2015_1")),
54    doc = "`p4 [g-opts] annotate [-a -c -i -I -q -t] [-d options] file[revRange] ...`: print file lines along with their revisions."
55)]
56#[cfg_attr(
57    all(feature = "lt2016_1", not(feature = "lt2015_2")),
58    doc = "`p4 [g-opts] annotate [-a -c -i -I -q -t -u] [-d options] file[revRange] ...`: print file lines along with their revisions."
59)]
60#[cfg_attr(
61    all(feature = "lt2018_1", not(feature = "lt2016_1")),
62    doc = "`p4 [g-opts] annotate [-a -c -i -I -q -t -u -T] [-d options] file[revRange] ...`: print file lines along with their revisions."
63)]
64#[cfg_attr(
65    all(feature = "lt2019_1", not(feature = "lt2018_1")),
66    doc = "`p4 [g-opts] annotate [-a -c -i -I -q -t -u -T] [-d options] File Spec[revSpec]`: print file lines along with their revisions."
67)]
68#[cfg_attr(
69    all(feature = "lt2022_2", not(feature = "lt2019_1")),
70    doc = "`p4 [g-opts] annotate [-a -c -i -I -q -t -T -u] [-d options] File Spec[revSpec]`: print file lines along with their revisions."
71)]
72#[cfg_attr(
73    not(feature = "lt2022_2"),
74    doc = "`p4 [g-opts] annotate [-a -c -i -I -q -t -T -u] [-d options] FileSpec[revSpec]`: print file lines along with their revisions."
75)]
76#[derive(Debug, Clone, Default)]
77pub struct Annotate<F = Unselected> {
78    bin: PathBuf,
79
80    global_opts: GlobalOpts,
81
82    all_lines: bool,
83
84    changelist_number: bool,
85
86    diff_options: Option<String>,
87
88    follow: F,
89
90    quiet_mode: bool,
91
92    force_binary: bool,
93
94    #[cfg(not(feature = "lt2015_2"))]
95    user_date: bool,
96
97    #[cfg(not(feature = "lt2016_1"))]
98    tab: Option<Tab>,
99}
100
101impl Annotate<Unselected> {
102    /// Creates a new `p4 annotate` command.
103    ///
104    /// `bin` is the path to the Perforce command-line executable.
105    pub fn new(bin: impl Into<PathBuf>, global_opts: GlobalOpts) -> Self {
106        Self {
107            bin: bin.into(),
108            global_opts,
109            ..Default::default()
110        }
111    }
112
113    /// # Description
114    ///
115    /// -i
116    ///
117    /// Follow file history across branches. If a file was created by
118    /// branching,
119    #[cfg_attr(
120        feature = "lt2017_2",
121        doc = "Perforce includes revisions up to the branch point."
122    )]
123    #[cfg_attr(
124        not(feature = "lt2017_2"),
125        doc = "the output includes revisions up to the branch point."
126    )]
127    /// The use of the -i option implies the -c option. The -i option cannot be
128    /// combined with -I.
129    pub fn follow_branches(self) -> Annotate<follow::Branches> {
130        Annotate {
131            bin: self.bin,
132            global_opts: self.global_opts,
133            all_lines: self.all_lines,
134            changelist_number: self.changelist_number,
135            diff_options: self.diff_options,
136            follow: follow::Branches,
137            quiet_mode: self.quiet_mode,
138            force_binary: self.force_binary,
139            #[cfg(not(feature = "lt2015_2"))]
140            user_date: self.user_date,
141            #[cfg(not(feature = "lt2016_1"))]
142            tab: self.tab,
143        }
144    }
145
146    /// # Description
147    ///
148    /// -I
149    ///
150    /// Follow integrations into the file. If a line was introduced into the
151    /// file by a merge, the source of the merge is indicated as the changelist
152    /// that introduced the line.
153    #[cfg_attr(
154        feature = "lt2017_2",
155        doc = "If that source was itself the result of an integration, that",
156        doc = "source will be used instead, and so on. The use of the -I option",
157        doc = "implies the -c option. The -I option cannot be combined with -i."
158    )]
159    #[cfg_attr(
160        not(feature = "lt2017_2"),
161        doc = "If that source was itself the result of an integration, that",
162        doc = "source will be used instead. The use of the -I option implies the",
163        doc = "-c option. The -I option cannot be combined with -i."
164    )]
165    pub fn follow_integrations(self) -> Annotate<follow::Integrations> {
166        Annotate {
167            bin: self.bin,
168            global_opts: self.global_opts,
169            all_lines: self.all_lines,
170            changelist_number: self.changelist_number,
171            diff_options: self.diff_options,
172            follow: follow::Integrations,
173            quiet_mode: self.quiet_mode,
174            force_binary: self.force_binary,
175            #[cfg(not(feature = "lt2015_2"))]
176            user_date: self.user_date,
177            #[cfg(not(feature = "lt2016_1"))]
178            tab: self.tab,
179        }
180    }
181}
182
183impl<F: ExclusiveOption> ParameterizedSpawn for Annotate<F> {
184    type Input<'a> = &'a [&'a OsStr];
185    type Output<'a> = Child;
186    type Error = std::io::Error;
187
188    /// Spawns `p4 annotate` for the given files as a child process with piped
189    /// standard output and error streams; use the returned [`Child`] handle
190    /// to wait for it or interact with it.
191    fn spawn_with<'a>(&mut self, files: Self::Input<'a>) -> Result<Self::Output<'a>, Self::Error> {
192        self.setup_command(&self.bin)
193            .args(files)
194            .stdout(Stdio::piped())
195            .stderr(Stdio::piped())
196            .spawn()
197    }
198}
199
200impl<F: ExclusiveOption> Annotate<F> {
201    /// # Description
202    ///
203    /// g-opts
204    ///
205    #[cfg_attr(
206        feature = "lt2014_2",
207        doc = "See the [Global Options](GlobalOpts) section."
208    )]
209    #[cfg_attr(
210        all(feature = "lt2015_1", not(feature = "lt2014_2")),
211        doc = "See the [“Global Options”](GlobalOpts) section."
212    )]
213    #[cfg_attr(
214        all(feature = "lt2017_1", not(feature = "lt2015_1")),
215        doc = "See [“Global Options”](GlobalOpts)."
216    )]
217    #[cfg_attr(
218        all(feature = "lt2018_2", not(feature = "lt2017_1")),
219        doc = "See [Global Options](GlobalOpts)."
220    )]
221    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
222    pub fn get_global_opts(&self) -> &GlobalOpts {
223        &self.global_opts
224    }
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 set_global_opts(&mut self, v: GlobalOpts) -> &mut Self {
248        self.global_opts = v;
249        self
250    }
251
252    /// # Description
253    ///
254    /// g-opts
255    ///
256    #[cfg_attr(
257        feature = "lt2014_2",
258        doc = "See the [Global Options](GlobalOpts) section."
259    )]
260    #[cfg_attr(
261        all(feature = "lt2015_1", not(feature = "lt2014_2")),
262        doc = "See the [“Global Options”](GlobalOpts) section."
263    )]
264    #[cfg_attr(
265        all(feature = "lt2017_1", not(feature = "lt2015_1")),
266        doc = "See [“Global Options”](GlobalOpts)."
267    )]
268    #[cfg_attr(
269        all(feature = "lt2018_2", not(feature = "lt2017_1")),
270        doc = "See [Global Options](GlobalOpts)."
271    )]
272    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
273    pub fn global_opts(mut self, v: GlobalOpts) -> Self {
274        self.global_opts = v;
275        self
276    }
277
278    /// # Description
279    ///
280    /// -a
281    ///
282    /// All lines, including deleted lines and lines no longer present at the
283    /// head revision, are included. Each line includes a starting and ending
284    /// revision.
285    pub fn get_all_lines(&self) -> bool {
286        self.all_lines
287    }
288
289    /// # Description
290    ///
291    /// -a
292    ///
293    /// All lines, including deleted lines and lines no longer present at the
294    /// head revision, are included. Each line includes a starting and ending
295    /// revision.
296    pub fn set_all_lines(&mut self, v: bool) -> &mut Self {
297        self.all_lines = v;
298        self
299    }
300
301    /// # Description
302    ///
303    /// -a
304    ///
305    /// All lines, including deleted lines and lines no longer present at the
306    /// head revision, are included. Each line includes a starting and ending
307    /// revision.
308    pub fn all_lines(mut self, v: bool) -> Self {
309        self.all_lines = v;
310        self
311    }
312
313    /// # Description
314    ///
315    /// -c
316    ///
317    /// Display the changelist number, rather than the revision number,
318    /// associated with each line. If you use the -a option and the -c option
319    /// together, each line includes a starting and ending changelist number.
320    pub fn get_changelist_number(&self) -> bool {
321        self.changelist_number
322    }
323
324    /// # Description
325    ///
326    /// -c
327    ///
328    /// Display the changelist number, rather than the revision number,
329    /// associated with each line. If you use the -a option and the -c option
330    /// together, each line includes a starting and ending changelist number.
331    pub fn set_changelist_number(&mut self, v: bool) -> &mut Self {
332        self.changelist_number = v;
333        self
334    }
335
336    /// # Description
337    ///
338    /// -c
339    ///
340    /// Display the changelist number, rather than the revision number,
341    /// associated with each line. If you use the -a option and the -c option
342    /// together, each line includes a starting and ending changelist number.
343    pub fn changelist_number(mut self, v: bool) -> Self {
344        self.changelist_number = v;
345        self
346    }
347
348    /// # Description
349    ///
350    #[cfg_attr(feature = "lt2014_2", doc = "-d flag")]
351    #[cfg_attr(not(feature = "lt2014_2"), doc = "-d options")]
352    ///
353    /// Runs the diff routine with one of a subset of the standard UNIX diff
354    #[cfg_attr(
355        feature = "lt2014_2",
356        doc = "flags. See the Usage Notes below for a listing of these",
357        doc = "flags."
358    )]
359    #[cfg_attr(
360        all(feature = "lt2015_1", not(feature = "lt2014_2")),
361        doc = "options. See Usage Notes below for a listing of these",
362        doc = "options."
363    )]
364    #[cfg_attr(
365        all(feature = "lt2024_1", not(feature = "lt2015_1")),
366        doc = "options. See Usage Notes for a listing of these options."
367    )]
368    #[cfg_attr(
369        not(feature = "lt2024_1"),
370        doc = "options. See Usage notes for a listing of these options."
371    )]
372    pub fn get_diff_options(&self) -> Option<&str> {
373        self.diff_options.as_deref()
374    }
375
376    /// # Description
377    ///
378    #[cfg_attr(feature = "lt2014_2", doc = "-d flag")]
379    #[cfg_attr(not(feature = "lt2014_2"), doc = "-d options")]
380    ///
381    /// Runs the diff routine with one of a subset of the standard UNIX diff
382    #[cfg_attr(
383        feature = "lt2014_2",
384        doc = "flags. See the Usage Notes below for a listing of these",
385        doc = "flags."
386    )]
387    #[cfg_attr(
388        all(feature = "lt2015_1", not(feature = "lt2014_2")),
389        doc = "options. See Usage Notes below for a listing of these",
390        doc = "options."
391    )]
392    #[cfg_attr(
393        all(feature = "lt2024_1", not(feature = "lt2015_1")),
394        doc = "options. See Usage Notes for a listing of these options."
395    )]
396    #[cfg_attr(
397        not(feature = "lt2024_1"),
398        doc = "options. See Usage notes for a listing of these options."
399    )]
400    pub fn set_diff_options(&mut self, v: impl Into<String>) -> &mut Self {
401        self.diff_options = Some(v.into());
402        self
403    }
404
405    /// # Description
406    ///
407    #[cfg_attr(feature = "lt2014_2", doc = "-d flag")]
408    #[cfg_attr(not(feature = "lt2014_2"), doc = "-d options")]
409    ///
410    /// Runs the diff routine with one of a subset of the standard UNIX diff
411    #[cfg_attr(
412        feature = "lt2014_2",
413        doc = "flags. See the Usage Notes below for a listing of these",
414        doc = "flags."
415    )]
416    #[cfg_attr(
417        all(feature = "lt2015_1", not(feature = "lt2014_2")),
418        doc = "options. See Usage Notes below for a listing of these",
419        doc = "options."
420    )]
421    #[cfg_attr(
422        all(feature = "lt2024_1", not(feature = "lt2015_1")),
423        doc = "options. See Usage Notes for a listing of these options."
424    )]
425    #[cfg_attr(
426        not(feature = "lt2024_1"),
427        doc = "options. See Usage notes for a listing of these options."
428    )]
429    pub fn diff_options(mut self, v: impl Into<String>) -> Self {
430        self.diff_options = Some(v.into());
431        self
432    }
433
434    /// # Description
435    ///
436    /// -q
437    ///
438    #[cfg_attr(
439        feature = "lt2017_2",
440        doc = "Quiet mode; suppress the one-line header for each file."
441    )]
442    #[cfg_attr(
443        not(feature = "lt2017_2"),
444        doc = "Quiet mode, which suppresses the one-line header for each file."
445    )]
446    pub fn get_quiet_mode(&self) -> bool {
447        self.quiet_mode
448    }
449
450    /// # Description
451    ///
452    /// -q
453    ///
454    #[cfg_attr(
455        feature = "lt2017_2",
456        doc = "Quiet mode; suppress the one-line header for each file."
457    )]
458    #[cfg_attr(
459        not(feature = "lt2017_2"),
460        doc = "Quiet mode, which suppresses the one-line header for each file."
461    )]
462    pub fn set_quiet_mode(&mut self, v: bool) -> &mut Self {
463        self.quiet_mode = v;
464        self
465    }
466
467    /// # Description
468    ///
469    /// -q
470    ///
471    #[cfg_attr(
472        feature = "lt2017_2",
473        doc = "Quiet mode; suppress the one-line header for each file."
474    )]
475    #[cfg_attr(
476        not(feature = "lt2017_2"),
477        doc = "Quiet mode, which suppresses the one-line header for each file."
478    )]
479    pub fn quiet_mode(mut self, v: bool) -> Self {
480        self.quiet_mode = v;
481        self
482    }
483
484    /// # Description
485    ///
486    /// -t
487    ///
488    /// Force `p4 annotate` to display non-text (binary) files.
489    pub fn get_force_binary(&self) -> bool {
490        self.force_binary
491    }
492
493    /// # Description
494    ///
495    /// -t
496    ///
497    /// Force `p4 annotate` to display non-text (binary) files.
498    pub fn set_force_binary(&mut self, v: bool) -> &mut Self {
499        self.force_binary = v;
500        self
501    }
502
503    /// # Description
504    ///
505    /// -t
506    ///
507    /// Force `p4 annotate` to display non-text (binary) files.
508    pub fn force_binary(mut self, v: bool) -> Self {
509        self.force_binary = v;
510        self
511    }
512
513    /// # Description
514    ///
515    /// -u
516    ///
517    /// Display the name of the user who modified the change and the date when
518    /// the modification occurred.
519    #[cfg(not(feature = "lt2015_2"))]
520    pub fn get_user_date(&self) -> bool {
521        self.user_date
522    }
523
524    /// # Description
525    ///
526    /// -u
527    ///
528    /// Display the name of the user who modified the change and the date when
529    /// the modification occurred.
530    #[cfg(not(feature = "lt2015_2"))]
531    pub fn set_user_date(&mut self, v: bool) -> &mut Self {
532        self.user_date = v;
533        self
534    }
535
536    /// # Description
537    ///
538    /// -u
539    ///
540    /// Display the name of the user who modified the change and the date when
541    /// the modification occurred.
542    #[cfg(not(feature = "lt2015_2"))]
543    pub fn user_date(mut self, v: bool) -> Self {
544        self.user_date = v;
545        self
546    }
547
548    /// # Description
549    ///
550    /// -T | `--tab=N`
551    ///
552    /// Align output to a tab stop of 8. You can specify a different tab value
553    /// using the `--tab` option and specifying the desired value for N.
554    ///
555    /// Returns the tab stop currently set, or `None` if no tab option is set.
556    #[cfg(not(feature = "lt2016_1"))]
557    pub fn get_tab(&self) -> Option<u32> {
558        match self.tab {
559            Some(Tab::Default) => Some(8),
560            Some(Tab::Custom(value)) => Some(value),
561            None => None,
562        }
563    }
564
565    /// # Description
566    ///
567    /// `--tab=N`
568    ///
569    /// Align output to a tab stop of N. Use [`Self::set_default_tab`] to
570    /// align output to the default tab stop of 8 with `-T`.
571    #[cfg(not(feature = "lt2016_1"))]
572    pub fn set_tab(&mut self, v: u32) -> &mut Self {
573        self.tab = Some(Tab::Custom(v));
574        self
575    }
576
577    /// # Description
578    ///
579    /// `--tab=N`
580    ///
581    /// Align output to a tab stop of N. Use [`Self::default_tab`] to align
582    /// output to the default tab stop of 8 with `-T`.
583    #[cfg(not(feature = "lt2016_1"))]
584    pub fn tab(mut self, v: u32) -> Self {
585        self.tab = Some(Tab::Custom(v));
586        self
587    }
588
589    /// # Description
590    ///
591    /// -T
592    ///
593    /// Align output to a tab stop of 8. Use [`Self::set_tab`] to align output
594    /// to a different tab stop with `--tab=N`.
595    #[cfg(not(feature = "lt2016_1"))]
596    pub fn set_default_tab(&mut self) -> &mut Self {
597        self.tab = Some(Tab::Default);
598        self
599    }
600
601    /// # Description
602    ///
603    /// -T
604    ///
605    /// Align output to a tab stop of 8. Use [`Self::tab`] to align output to
606    /// a different tab stop with `--tab=N`.
607    #[cfg(not(feature = "lt2016_1"))]
608    pub fn default_tab(mut self) -> Self {
609        self.tab = Some(Tab::Default);
610        self
611    }
612}
613
614impl<F: ExclusiveOption> SubCommand for Annotate<F> {
615    fn name(&self) -> &str {
616        "annotate"
617    }
618
619    fn inject_local_args(&self, command: &mut Command) {
620        if self.all_lines {
621            command.arg("-a");
622        }
623
624        if self.changelist_number {
625            command.arg("-c");
626        }
627
628        if let Some(diff_options) = &self.diff_options {
629            command.arg(format!("-d{diff_options}"));
630        }
631
632        self.follow.inject_args(command);
633
634        if self.quiet_mode {
635            command.arg("-q");
636        }
637
638        if self.force_binary {
639            command.arg("-t");
640        }
641
642        #[cfg(not(feature = "lt2015_2"))]
643        {
644            if self.user_date {
645                command.arg("-u");
646            }
647        }
648
649        #[cfg(not(feature = "lt2016_1"))]
650        {
651            if let Some(tab) = self.tab {
652                match tab {
653                    Tab::Default => {
654                        command.arg("-T");
655                    }
656                    Tab::Custom(value) => {
657                        command.arg(format!("--tab={value}"));
658                    }
659                }
660            }
661        }
662    }
663
664    fn global_opts(&self) -> Option<&GlobalOpts> {
665        Some(&self.global_opts)
666    }
667}
668
669#[cfg(test)]
670mod tests {
671    use super::*;
672    use crate::cmd::args_of;
673
674    /// Dry-run checks of the assembled `p4 annotate` command line; no process
675    /// is spawned.
676    #[test]
677    fn without_options() {
678        let annotate = Annotate::new("p4", GlobalOpts::new());
679
680        assert_eq!(args_of(&annotate.setup_command("p4")), ["annotate"]);
681    }
682
683    #[test]
684    fn all_options() {
685        let mut annotate = Annotate::new("p4", GlobalOpts::new());
686        annotate
687            .set_all_lines(true)
688            .set_changelist_number(true)
689            .set_diff_options("Su2")
690            .set_quiet_mode(true)
691            .set_force_binary(true);
692        #[cfg(not(feature = "lt2015_2"))]
693        annotate.set_user_date(true);
694
695        // `-i` and `-I` are mutually exclusive; select the branch-following
696        // variant here.
697        #[cfg_attr(feature = "lt2016_1", allow(unused_mut))]
698        let mut annotate = annotate.follow_branches();
699
700        // `mut` is only needed when the tab block below is compiled in.
701        #[cfg_attr(feature = "lt2016_1", allow(unused_mut))]
702        let mut expected = vec!["annotate", "-a", "-c", "-dSu2", "-i", "-q", "-t"];
703        #[cfg(not(feature = "lt2015_2"))]
704        expected.push("-u");
705        #[cfg(not(feature = "lt2016_1"))]
706        {
707            annotate.set_default_tab();
708            expected.push("-T");
709        }
710
711        assert_eq!(args_of(&annotate.setup_command("p4")), expected);
712    }
713
714    #[cfg(not(feature = "lt2016_1"))]
715    #[test]
716    fn default_tab_uses_short_flag() {
717        let mut annotate = Annotate::new("p4", GlobalOpts::new());
718        annotate.set_default_tab();
719
720        assert_eq!(annotate.get_tab(), Some(8));
721        assert_eq!(args_of(&annotate.setup_command("p4")), ["annotate", "-T"]);
722    }
723
724    #[cfg(not(feature = "lt2016_1"))]
725    #[test]
726    fn custom_tab_uses_long_flag() {
727        let mut annotate = Annotate::new("p4", GlobalOpts::new());
728        annotate.set_tab(4);
729
730        assert_eq!(annotate.get_tab(), Some(4));
731        assert_eq!(
732            args_of(&annotate.setup_command("p4")),
733            ["annotate", "--tab=4"]
734        );
735    }
736}