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::{DiffOptions, 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<DiffOptions>,
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, S, I> ParameterizedSpawn<(S,)> for Annotate<F>
184where
185    S: IntoIterator<Item = I>,
186    I: AsRef<OsStr>,
187{
188    type Output = Child;
189    type Error = std::io::Error;
190
191    /// Spawns `p4 annotate` for the given files as a child process with piped
192    /// standard output and error streams; use the returned [`Child`] handle
193    /// to wait for it or interact with it.
194    fn spawn_with(&mut self, (files,): (S,)) -> Result<Self::Output, Self::Error> {
195        self.setup_command(&self.bin)
196            .args(files)
197            .stdout(Stdio::piped())
198            .stderr(Stdio::piped())
199            .spawn()
200    }
201}
202
203impl<F: ExclusiveOption> Annotate<F> {
204    /// # Description
205    ///
206    /// g-opts
207    ///
208    #[cfg_attr(
209        feature = "lt2014_2",
210        doc = "See the [Global Options](GlobalOpts) section."
211    )]
212    #[cfg_attr(
213        all(feature = "lt2015_1", not(feature = "lt2014_2")),
214        doc = "See the [“Global Options”](GlobalOpts) section."
215    )]
216    #[cfg_attr(
217        all(feature = "lt2017_1", not(feature = "lt2015_1")),
218        doc = "See [“Global Options”](GlobalOpts)."
219    )]
220    #[cfg_attr(
221        all(feature = "lt2018_2", not(feature = "lt2017_1")),
222        doc = "See [Global Options](GlobalOpts)."
223    )]
224    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
225    pub fn get_global_opts(&self) -> &GlobalOpts {
226        &self.global_opts
227    }
228
229    /// # Description
230    ///
231    /// g-opts
232    ///
233    #[cfg_attr(
234        feature = "lt2014_2",
235        doc = "See the [Global Options](GlobalOpts) section."
236    )]
237    #[cfg_attr(
238        all(feature = "lt2015_1", not(feature = "lt2014_2")),
239        doc = "See the [“Global Options”](GlobalOpts) section."
240    )]
241    #[cfg_attr(
242        all(feature = "lt2017_1", not(feature = "lt2015_1")),
243        doc = "See [“Global Options”](GlobalOpts)."
244    )]
245    #[cfg_attr(
246        all(feature = "lt2018_2", not(feature = "lt2017_1")),
247        doc = "See [Global Options](GlobalOpts)."
248    )]
249    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
250    pub fn set_global_opts(&mut self, v: GlobalOpts) -> &mut Self {
251        self.global_opts = v;
252        self
253    }
254
255    /// # Description
256    ///
257    /// g-opts
258    ///
259    #[cfg_attr(
260        feature = "lt2014_2",
261        doc = "See the [Global Options](GlobalOpts) section."
262    )]
263    #[cfg_attr(
264        all(feature = "lt2015_1", not(feature = "lt2014_2")),
265        doc = "See the [“Global Options”](GlobalOpts) section."
266    )]
267    #[cfg_attr(
268        all(feature = "lt2017_1", not(feature = "lt2015_1")),
269        doc = "See [“Global Options”](GlobalOpts)."
270    )]
271    #[cfg_attr(
272        all(feature = "lt2018_2", not(feature = "lt2017_1")),
273        doc = "See [Global Options](GlobalOpts)."
274    )]
275    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
276    pub fn global_opts(mut self, v: GlobalOpts) -> Self {
277        self.global_opts = v;
278        self
279    }
280
281    /// # Description
282    ///
283    /// -a
284    ///
285    /// All lines, including deleted lines and lines no longer present at the
286    /// head revision, are included. Each line includes a starting and ending
287    /// revision.
288    pub fn get_all_lines(&self) -> bool {
289        self.all_lines
290    }
291
292    /// # Description
293    ///
294    /// -a
295    ///
296    /// All lines, including deleted lines and lines no longer present at the
297    /// head revision, are included. Each line includes a starting and ending
298    /// revision.
299    pub fn set_all_lines(&mut self, v: bool) -> &mut Self {
300        self.all_lines = v;
301        self
302    }
303
304    /// # Description
305    ///
306    /// -a
307    ///
308    /// All lines, including deleted lines and lines no longer present at the
309    /// head revision, are included. Each line includes a starting and ending
310    /// revision.
311    pub fn all_lines(mut self, v: bool) -> Self {
312        self.all_lines = v;
313        self
314    }
315
316    /// # Description
317    ///
318    /// -c
319    ///
320    /// Display the changelist number, rather than the revision number,
321    /// associated with each line. If you use the -a option and the -c option
322    /// together, each line includes a starting and ending changelist number.
323    pub fn get_changelist_number(&self) -> bool {
324        self.changelist_number
325    }
326
327    /// # Description
328    ///
329    /// -c
330    ///
331    /// Display the changelist number, rather than the revision number,
332    /// associated with each line. If you use the -a option and the -c option
333    /// together, each line includes a starting and ending changelist number.
334    pub fn set_changelist_number(&mut self, v: bool) -> &mut Self {
335        self.changelist_number = v;
336        self
337    }
338
339    /// # Description
340    ///
341    /// -c
342    ///
343    /// Display the changelist number, rather than the revision number,
344    /// associated with each line. If you use the -a option and the -c option
345    /// together, each line includes a starting and ending changelist number.
346    pub fn changelist_number(mut self, v: bool) -> Self {
347        self.changelist_number = v;
348        self
349    }
350
351    /// # Description
352    ///
353    #[cfg_attr(feature = "lt2014_2", doc = "-d flag")]
354    #[cfg_attr(not(feature = "lt2014_2"), doc = "-d options")]
355    ///
356    /// Runs the diff routine with one of a subset of the standard UNIX diff
357    #[cfg_attr(
358        feature = "lt2014_2",
359        doc = "flags. See the Usage Notes below for a listing of these",
360        doc = "flags."
361    )]
362    #[cfg_attr(
363        all(feature = "lt2015_1", not(feature = "lt2014_2")),
364        doc = "options. See Usage Notes below for a listing of these",
365        doc = "options."
366    )]
367    #[cfg_attr(
368        all(feature = "lt2024_1", not(feature = "lt2015_1")),
369        doc = "options. See Usage Notes for a listing of these options."
370    )]
371    #[cfg_attr(
372        not(feature = "lt2024_1"),
373        doc = "options. See Usage notes for a listing of these options."
374    )]
375    pub fn get_diff_options(&self) -> Option<&DiffOptions> {
376        self.diff_options.as_ref()
377    }
378
379    /// # Description
380    ///
381    #[cfg_attr(feature = "lt2014_2", doc = "-d flag")]
382    #[cfg_attr(not(feature = "lt2014_2"), doc = "-d options")]
383    ///
384    /// Runs the diff routine with one of a subset of the standard UNIX diff
385    #[cfg_attr(
386        feature = "lt2014_2",
387        doc = "flags. See the Usage Notes below for a listing of these",
388        doc = "flags."
389    )]
390    #[cfg_attr(
391        all(feature = "lt2015_1", not(feature = "lt2014_2")),
392        doc = "options. See Usage Notes below for a listing of these",
393        doc = "options."
394    )]
395    #[cfg_attr(
396        all(feature = "lt2024_1", not(feature = "lt2015_1")),
397        doc = "options. See Usage Notes for a listing of these options."
398    )]
399    #[cfg_attr(
400        not(feature = "lt2024_1"),
401        doc = "options. See Usage notes for a listing of these options."
402    )]
403    pub fn set_diff_options(&mut self, v: impl Into<DiffOptions>) -> &mut Self {
404        self.diff_options = Some(v.into());
405        self
406    }
407
408    /// # Description
409    ///
410    #[cfg_attr(feature = "lt2014_2", doc = "-d flag")]
411    #[cfg_attr(not(feature = "lt2014_2"), doc = "-d options")]
412    ///
413    /// Runs the diff routine with one of a subset of the standard UNIX diff
414    #[cfg_attr(
415        feature = "lt2014_2",
416        doc = "flags. See the Usage Notes below for a listing of these",
417        doc = "flags."
418    )]
419    #[cfg_attr(
420        all(feature = "lt2015_1", not(feature = "lt2014_2")),
421        doc = "options. See Usage Notes below for a listing of these",
422        doc = "options."
423    )]
424    #[cfg_attr(
425        all(feature = "lt2024_1", not(feature = "lt2015_1")),
426        doc = "options. See Usage Notes for a listing of these options."
427    )]
428    #[cfg_attr(
429        not(feature = "lt2024_1"),
430        doc = "options. See Usage notes for a listing of these options."
431    )]
432    pub fn diff_options(mut self, v: impl Into<DiffOptions>) -> Self {
433        self.diff_options = Some(v.into());
434        self
435    }
436
437    /// # Description
438    ///
439    /// -q
440    ///
441    #[cfg_attr(
442        feature = "lt2017_2",
443        doc = "Quiet mode; suppress the one-line header for each file."
444    )]
445    #[cfg_attr(
446        not(feature = "lt2017_2"),
447        doc = "Quiet mode, which suppresses the one-line header for each file."
448    )]
449    pub fn get_quiet_mode(&self) -> bool {
450        self.quiet_mode
451    }
452
453    /// # Description
454    ///
455    /// -q
456    ///
457    #[cfg_attr(
458        feature = "lt2017_2",
459        doc = "Quiet mode; suppress the one-line header for each file."
460    )]
461    #[cfg_attr(
462        not(feature = "lt2017_2"),
463        doc = "Quiet mode, which suppresses the one-line header for each file."
464    )]
465    pub fn set_quiet_mode(&mut self, v: bool) -> &mut Self {
466        self.quiet_mode = v;
467        self
468    }
469
470    /// # Description
471    ///
472    /// -q
473    ///
474    #[cfg_attr(
475        feature = "lt2017_2",
476        doc = "Quiet mode; suppress the one-line header for each file."
477    )]
478    #[cfg_attr(
479        not(feature = "lt2017_2"),
480        doc = "Quiet mode, which suppresses the one-line header for each file."
481    )]
482    pub fn quiet_mode(mut self, v: bool) -> Self {
483        self.quiet_mode = v;
484        self
485    }
486
487    /// # Description
488    ///
489    /// -t
490    ///
491    /// Force `p4 annotate` to display non-text (binary) files.
492    pub fn get_force_binary(&self) -> bool {
493        self.force_binary
494    }
495
496    /// # Description
497    ///
498    /// -t
499    ///
500    /// Force `p4 annotate` to display non-text (binary) files.
501    pub fn set_force_binary(&mut self, v: bool) -> &mut Self {
502        self.force_binary = v;
503        self
504    }
505
506    /// # Description
507    ///
508    /// -t
509    ///
510    /// Force `p4 annotate` to display non-text (binary) files.
511    pub fn force_binary(mut self, v: bool) -> Self {
512        self.force_binary = v;
513        self
514    }
515
516    /// # Description
517    ///
518    /// -u
519    ///
520    /// Display the name of the user who modified the change and the date when
521    /// the modification occurred.
522    #[cfg(not(feature = "lt2015_2"))]
523    pub fn get_user_date(&self) -> bool {
524        self.user_date
525    }
526
527    /// # Description
528    ///
529    /// -u
530    ///
531    /// Display the name of the user who modified the change and the date when
532    /// the modification occurred.
533    #[cfg(not(feature = "lt2015_2"))]
534    pub fn set_user_date(&mut self, v: bool) -> &mut Self {
535        self.user_date = v;
536        self
537    }
538
539    /// # Description
540    ///
541    /// -u
542    ///
543    /// Display the name of the user who modified the change and the date when
544    /// the modification occurred.
545    #[cfg(not(feature = "lt2015_2"))]
546    pub fn user_date(mut self, v: bool) -> Self {
547        self.user_date = v;
548        self
549    }
550
551    /// # Description
552    ///
553    /// -T | `--tab=N`
554    ///
555    /// Align output to a tab stop of 8. You can specify a different tab value
556    /// using the `--tab` option and specifying the desired value for N.
557    ///
558    /// Returns the tab stop currently set, or `None` if no tab option is set.
559    #[cfg(not(feature = "lt2016_1"))]
560    pub fn get_tab(&self) -> Option<u32> {
561        match self.tab {
562            Some(Tab::Default) => Some(8),
563            Some(Tab::Custom(value)) => Some(value),
564            None => None,
565        }
566    }
567
568    /// # Description
569    ///
570    /// `--tab=N`
571    ///
572    /// Align output to a tab stop of N. Use [`Self::set_default_tab`] to
573    /// align output to the default tab stop of 8 with `-T`.
574    #[cfg(not(feature = "lt2016_1"))]
575    pub fn set_tab(&mut self, v: u32) -> &mut Self {
576        self.tab = Some(Tab::Custom(v));
577        self
578    }
579
580    /// # Description
581    ///
582    /// `--tab=N`
583    ///
584    /// Align output to a tab stop of N. Use [`Self::default_tab`] to align
585    /// output to the default tab stop of 8 with `-T`.
586    #[cfg(not(feature = "lt2016_1"))]
587    pub fn tab(mut self, v: u32) -> Self {
588        self.tab = Some(Tab::Custom(v));
589        self
590    }
591
592    /// # Description
593    ///
594    /// -T
595    ///
596    /// Align output to a tab stop of 8. Use [`Self::set_tab`] to align output
597    /// to a different tab stop with `--tab=N`.
598    #[cfg(not(feature = "lt2016_1"))]
599    pub fn set_default_tab(&mut self) -> &mut Self {
600        self.tab = Some(Tab::Default);
601        self
602    }
603
604    /// # Description
605    ///
606    /// -T
607    ///
608    /// Align output to a tab stop of 8. Use [`Self::tab`] to align output to
609    /// a different tab stop with `--tab=N`.
610    #[cfg(not(feature = "lt2016_1"))]
611    pub fn default_tab(mut self) -> Self {
612        self.tab = Some(Tab::Default);
613        self
614    }
615}
616
617impl<F: ExclusiveOption> SubCommand for Annotate<F> {
618    fn name(&self) -> &str {
619        "annotate"
620    }
621
622    fn inject_local_args(&self, command: &mut Command) {
623        if self.all_lines {
624            command.arg("-a");
625        }
626
627        if self.changelist_number {
628            command.arg("-c");
629        }
630
631        if let Some(diff_options) = &self.diff_options {
632            diff_options.inject_arg(command);
633        }
634
635        self.follow.inject_args(command);
636
637        if self.quiet_mode {
638            command.arg("-q");
639        }
640
641        if self.force_binary {
642            command.arg("-t");
643        }
644
645        #[cfg(not(feature = "lt2015_2"))]
646        {
647            if self.user_date {
648                command.arg("-u");
649            }
650        }
651
652        #[cfg(not(feature = "lt2016_1"))]
653        {
654            if let Some(tab) = self.tab {
655                match tab {
656                    Tab::Default => {
657                        command.arg("-T");
658                    }
659                    Tab::Custom(value) => {
660                        command.arg(format!("--tab={value}"));
661                    }
662                }
663            }
664        }
665    }
666
667    fn global_opts(&self) -> Option<&GlobalOpts> {
668        Some(&self.global_opts)
669    }
670}
671
672#[cfg(test)]
673mod tests {
674    use super::*;
675    use crate::cmd::DiffOptionsBuilder;
676    use crate::cmd::args_of;
677
678    /// Dry-run checks of the assembled `p4 annotate` command line; no process
679    /// is spawned.
680    #[test]
681    fn without_options() {
682        let annotate = Annotate::new("p4", GlobalOpts::new());
683
684        assert_eq!(args_of(&annotate.setup_command("p4")), ["annotate"]);
685    }
686
687    #[test]
688    fn all_options() {
689        let mut annotate = Annotate::new("p4", GlobalOpts::new());
690        annotate
691            .set_all_lines(true)
692            .set_changelist_number(true)
693            .set_diff_options(DiffOptionsBuilder::raw("Su2"))
694            .set_quiet_mode(true)
695            .set_force_binary(true);
696        #[cfg(not(feature = "lt2015_2"))]
697        annotate.set_user_date(true);
698
699        // `-i` and `-I` are mutually exclusive; select the branch-following
700        // variant here.
701        #[cfg_attr(feature = "lt2016_1", allow(unused_mut))]
702        let mut annotate = annotate.follow_branches();
703
704        // `mut` is only needed when the tab block below is compiled in.
705        #[cfg_attr(feature = "lt2016_1", allow(unused_mut))]
706        let mut expected = vec!["annotate", "-a", "-c", "-dSu2", "-i", "-q", "-t"];
707        #[cfg(not(feature = "lt2015_2"))]
708        expected.push("-u");
709        #[cfg(not(feature = "lt2016_1"))]
710        {
711            annotate.set_default_tab();
712            expected.push("-T");
713        }
714
715        assert_eq!(args_of(&annotate.setup_command("p4")), expected);
716    }
717
718    #[cfg(not(feature = "lt2016_1"))]
719    #[test]
720    fn default_tab_uses_short_flag() {
721        let mut annotate = Annotate::new("p4", GlobalOpts::new());
722        annotate.set_default_tab();
723
724        assert_eq!(annotate.get_tab(), Some(8));
725        assert_eq!(args_of(&annotate.setup_command("p4")), ["annotate", "-T"]);
726    }
727
728    #[cfg(not(feature = "lt2016_1"))]
729    #[test]
730    fn custom_tab_uses_long_flag() {
731        let mut annotate = Annotate::new("p4", GlobalOpts::new());
732        annotate.set_tab(4);
733
734        assert_eq!(annotate.get_tab(), Some(4));
735        assert_eq!(
736            args_of(&annotate.setup_command("p4")),
737            ["annotate", "--tab=4"]
738        );
739    }
740}