Skip to main content

perforce_cli/cmd/
diff2.rs

1use std::{
2    ffi::OsStr,
3    path::{Path, PathBuf},
4    process::{Child, Command, Stdio},
5};
6
7use super::{DiffOptions, ExclusiveOption, SubCommand, Unselected};
8
9use crate::global::GlobalOpts;
10use crate::spawn::ParameterizedSpawn;
11
12/// The `-b branch` sub-mode of the [`DepotContent`] state: diff files in
13/// two branched codelines through a branch mapping.
14///
15/// Entered with [`Diff2::branch`].
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct BranchMode {
18    branch: String,
19}
20
21impl ExclusiveOption for BranchMode {
22    fn inject_args(&self, command: &mut Command) {
23        command.arg("-b").arg(&self.branch);
24    }
25}
26
27/// The `-S stream` sub-mode of the [`DepotContent`] state: diff a stream
28/// with its parent.
29///
30/// Entered with [`Diff2::stream`].
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct StreamMode {
33    stream: String,
34
35    parent: Option<String>,
36}
37
38impl ExclusiveOption for StreamMode {
39    fn inject_args(&self, command: &mut Command) {
40        command.arg("-S").arg(&self.stream);
41
42        if let Some(parent) = &self.parent {
43            command.arg("-P").arg(parent);
44        }
45    }
46}
47
48/// Stream spec mode of `p4 diff2` (`-As`): diff two arbitrary stream specs
49/// against each other.
50///
51/// Entered with [`Diff2::stream_spec_mode`]; the two stream specs to compare
52/// are passed as the arguments of the spawned command. As the `-As` form
53/// accepts only `-doptions` besides g-opts, this mode offers neither the
54/// depot content options nor a sub-mode.
55#[cfg(not(feature = "lt2019_1"))]
56#[derive(Debug, Clone, Default, PartialEq, Eq)]
57pub struct StreamSpecMode;
58
59#[cfg(not(feature = "lt2019_1"))]
60impl ExclusiveOption for StreamSpecMode {
61    fn inject_args(&self, command: &mut Command) {
62        command.arg("-As");
63    }
64}
65
66/// The depot content modes of `p4 diff2`: the default form comparing two
67/// depot paths, or the `-b branch` / `-S stream` sub-modes.
68///
69/// Entered with [`Diff2::differing_only`], [`Diff2::quiet_mode`],
70/// [`Diff2::diff_nontext`], [`Diff2::unified_patch`], [`Diff2::branch`], or
71/// [`Diff2::stream`].
72///
73/// The `M` type parameter tracks the sub-mode at compile time. The default
74/// [`Unselected`] state diffs the file pair given as spawn arguments;
75/// [`Diff2::branch`] transitions to the [`BranchMode`] sub-mode, and
76/// [`Diff2::stream`] transitions to the [`StreamMode`] sub-mode. The
77/// `[-Od -q -t -u]` options are shared by all sub-modes and therefore
78/// stored here.
79#[derive(Debug, Clone, Default)]
80pub struct DepotContent<M = Unselected> {
81    differing_only: bool,
82
83    quiet_mode: bool,
84
85    diff_nontext: bool,
86
87    unified_patch: bool,
88
89    mode: M,
90}
91
92impl<M: ExclusiveOption> ExclusiveOption for DepotContent<M> {
93    fn inject_args(&self, command: &mut Command) {
94        if self.differing_only {
95            command.arg("-Od");
96        }
97
98        if self.quiet_mode {
99            command.arg("-q");
100        }
101
102        if self.diff_nontext {
103            command.arg("-t");
104        }
105
106        if self.unified_patch {
107            command.arg("-u");
108        }
109
110        self.mode.inject_args(command);
111    }
112}
113
114#[cfg_attr(
115    feature = "lt2014_2",
116    doc = "`p4 [g-opts] diff2 [-dflags -Od -q -t -u] file1[rev] file2[rev]`",
117    doc = "",
118    doc = "`p4 [g-opts] diff2 [-dflags -Od -q -t -u] -b branch [[fromfile[rev]] tofile[rev]]`",
119    doc = "",
120    doc = "`p4 [g-opts] diff2 [-dflags -Od -q -t -u] -S stream [-P parent] [[fromfile[rev]] tofile[rev]]`"
121)]
122#[cfg_attr(
123    all(feature = "lt2015_1", not(feature = "lt2014_2")),
124    doc = "`p4 [g-opts] diff2 [-doptions -Od -q -t -u] file1[rev] file2[rev]`",
125    doc = "",
126    doc = "`p4 [g-opts] diff2 [-doptions -Od -q -t -u] -b branch [[fromfile[rev]] tofile[rev]]`",
127    doc = "",
128    doc = "`p4 [g-opts] diff2 [-doptions -Od -q -t -u] -S stream [-P parent] [[fromfile[rev]] tofile[rev]]`"
129)]
130#[cfg_attr(
131    all(feature = "lt2019_1", not(feature = "lt2015_1")),
132    doc = "`p4 [g-opts] diff2 [-doptions] [-Od -q -t -u] file1[rev] file2[rev]`",
133    doc = "",
134    doc = "`p4 [g-opts] diff2 [-doptions] [-Od -q -t -u] -b branch [[fromfile[rev]] tofile[rev]]`",
135    doc = "",
136    doc = "`p4 [g-opts] diff2 [-doptions] [-Od -q -t -u] [-S stream] [-P parent] [[fromfile[rev]] tofile[rev]]`"
137)]
138#[cfg_attr(
139    not(feature = "lt2019_1"),
140    doc = "`p4 [g-opts] diff2 [-doptions] [-Od -q -t -u] file1[rev] file2[rev]`",
141    doc = "",
142    doc = "`p4 [g-opts] diff2 [-doptions] [-Od -q -t -u] -b branch [[fromfile[rev]] tofile[rev]]`",
143    doc = "",
144    doc = "`p4 [g-opts] diff2 [-doptions] [-Od -q -t -u] [-S stream] [-P parent] [[fromfile[rev]] tofile[rev]]`",
145    doc = "",
146    doc = "`p4 [g-opts] diff2 [-doptions] -As streamname1[@change1] streamname2[@change2]`"
147)]
148///
149/// Diff utility for comparing the content at two depot paths. (For
150/// comparing workspace content to depot content, see `p4 diff`.)
151///
152#[cfg_attr(
153    not(feature = "lt2019_1"),
154    doc = "Also compares two arbitrary stream specs with the -As option."
155)]
156///
157#[cfg_attr(
158    feature = "lt2019_1",
159    doc = "The `M` type parameter tracks the command mode at compile time. The default [`Unselected`] state diffs the two depot paths given as spawn arguments; [`Self::differing_only`], [`Self::quiet_mode`], [`Self::diff_nontext`], and [`Self::unified_patch`] transition to the [`DepotContent`] state, and [`Self::branch`] and [`Self::stream`] transition to the depot content [`BranchMode`] and [`StreamMode`] sub-modes."
160)]
161#[cfg_attr(
162    not(feature = "lt2019_1"),
163    doc = "The `M` type parameter tracks the command mode at compile time. The default [`Unselected`] state diffs the two depot paths given as spawn arguments; [`Self::differing_only`], [`Self::quiet_mode`], [`Self::diff_nontext`], and [`Self::unified_patch`] transition to the [`DepotContent`] state, [`Self::branch`] and [`Self::stream`] transition to the depot content [`BranchMode`] and [`StreamMode`] sub-modes, and [`Self::stream_spec_mode`] transitions to the [`StreamSpecMode`] state."
164)]
165#[derive(Debug, Clone, Default)]
166pub struct Diff2<M = Unselected> {
167    bin: PathBuf,
168
169    global_opts: GlobalOpts,
170
171    diff_opts: Option<DiffOptions>,
172
173    mode: M,
174}
175
176impl Diff2<Unselected> {
177    /// Creates a new `p4 diff2` command.
178    ///
179    /// `bin` is the path to the Perforce command-line executable.
180    pub fn new(bin: impl Into<PathBuf>, global_opts: GlobalOpts) -> Self {
181        Self {
182            bin: bin.into(),
183            global_opts,
184            diff_opts: None,
185            mode: Unselected,
186        }
187    }
188
189    /// # Description
190    ///
191    /// -Od
192    ///
193    /// Limit output to only those files that differ.
194    ///
195    /// Transitions this command to the [`DepotContent`] state with `-Od`
196    /// set according to `v`.
197    pub fn differing_only(self, v: bool) -> Diff2<DepotContent<Unselected>> {
198        Diff2 {
199            bin: self.bin,
200            global_opts: self.global_opts,
201            diff_opts: self.diff_opts,
202            mode: DepotContent {
203                differing_only: v,
204                quiet_mode: false,
205                diff_nontext: false,
206                unified_patch: false,
207                mode: Unselected,
208            },
209        }
210    }
211
212    /// # Description
213    ///
214    /// -q
215    ///
216    /// Quiet diff. Display only the header; if `file1` and `file2` are
217    /// identical, display only "`file1` - no differing files" as the output.
218    ///
219    /// Transitions this command to the [`DepotContent`] state with `-q` set
220    /// according to `v`.
221    pub fn quiet_mode(self, v: bool) -> Diff2<DepotContent<Unselected>> {
222        Diff2 {
223            bin: self.bin,
224            global_opts: self.global_opts,
225            diff_opts: self.diff_opts,
226            mode: DepotContent {
227                differing_only: false,
228                quiet_mode: v,
229                diff_nontext: false,
230                unified_patch: false,
231                mode: Unselected,
232            },
233        }
234    }
235
236    /// # Description
237    ///
238    /// -t
239    ///
240    /// Diff the file revisions even if the file(s) are not of type `text`.
241    ///
242    /// Transitions this command to the [`DepotContent`] state with `-t` set
243    /// according to `v`.
244    pub fn diff_nontext(self, v: bool) -> Diff2<DepotContent<Unselected>> {
245        Diff2 {
246            bin: self.bin,
247            global_opts: self.global_opts,
248            diff_opts: self.diff_opts,
249            mode: DepotContent {
250                differing_only: false,
251                quiet_mode: false,
252                diff_nontext: v,
253                unified_patch: false,
254                mode: Unselected,
255            },
256        }
257    }
258
259    /// # Description
260    ///
261    /// -u
262    ///
263    /// Generate unified output format, showing added and deleted lines with
264    /// sufficient context for compatibility with the `patch(1)` utility.
265    /// Only those files that differ are included. File names and dates
266    #[cfg_attr(feature = "lt2017_2", doc = "remain in Perforce syntax.")]
267    #[cfg_attr(
268        all(feature = "lt2024_1", not(feature = "lt2017_2")),
269        doc = "remain in Helix Server syntax."
270    )]
271    #[cfg_attr(
272        all(feature = "lt2024_2", not(feature = "lt2024_1")),
273        doc = "remain in Helix Core Server syntax."
274    )]
275    #[cfg_attr(not(feature = "lt2024_2"), doc = "remain in P4 Server syntax.")]
276    ///
277    /// Transitions this command to the [`DepotContent`] state with `-u` set
278    /// according to `v`.
279    pub fn unified_patch(self, v: bool) -> Diff2<DepotContent<Unselected>> {
280        Diff2 {
281            bin: self.bin,
282            global_opts: self.global_opts,
283            diff_opts: self.diff_opts,
284            mode: DepotContent {
285                differing_only: false,
286                quiet_mode: false,
287                diff_nontext: false,
288                unified_patch: v,
289                mode: Unselected,
290            },
291        }
292    }
293
294    /// # Description
295    ///
296    /// -b branch
297    ///
298    /// Use a branch mapping to diff files in two branched codelines. The
299    /// files that are compared can be limited by file patterns in either
300    /// the `from` or `to` file specifications.
301    ///
302    /// Transitions this command to the [`DepotContent`] state with the
303    /// [`BranchMode`] sub-mode selected and the branch mapping set to
304    /// `name`.
305    pub fn branch(self, name: impl Into<String>) -> Diff2<DepotContent<BranchMode>> {
306        Diff2 {
307            bin: self.bin,
308            global_opts: self.global_opts,
309            diff_opts: self.diff_opts,
310            mode: DepotContent {
311                differing_only: false,
312                quiet_mode: false,
313                diff_nontext: false,
314                unified_patch: false,
315                mode: BranchMode {
316                    branch: name.into(),
317                },
318            },
319        }
320    }
321
322    /// # Description
323    ///
324    /// -S stream
325    ///
326    /// Diff a stream with its parent. To diff the stream with a stream
327    /// other than its configured parent, specify [`Diff2::parent`] or
328    /// [`Diff2::set_parent`].
329    ///
330    /// Transitions this command to the [`DepotContent`] state with the
331    /// [`StreamMode`] sub-mode selected and the stream set to `name`.
332    pub fn stream(self, name: impl Into<String>) -> Diff2<DepotContent<StreamMode>> {
333        Diff2 {
334            bin: self.bin,
335            global_opts: self.global_opts,
336            diff_opts: self.diff_opts,
337            mode: DepotContent {
338                differing_only: false,
339                quiet_mode: false,
340                diff_nontext: false,
341                unified_patch: false,
342                mode: StreamMode {
343                    stream: name.into(),
344                    parent: None,
345                },
346            },
347        }
348    }
349
350    /// # Description
351    ///
352    /// -As
353    ///
354    /// Allows two arbitrary stream specs to be diffed against each other.
355    /// Can be used with a streamname, or with a streamname at a specific
356    /// changelist number.
357    ///
358    /// Although this option requires the user have at least the `list`
359    /// access to the stream path, it ignores any other entry in the
360    /// protections table, including any minus sign (`-`) that would
361    /// otherwise block the operation.
362    ///
363    /// Transitions this command to the [`StreamSpecMode`] state. The two
364    /// stream specs to compare are passed as the arguments of the spawned
365    /// command ([`ParameterizedSpawn::spawn_with`]). As the `-As` form
366    /// accepts only `-doptions` besides g-opts, this transition is
367    /// unavailable once the command has entered the [`DepotContent`] state.
368    #[cfg(not(feature = "lt2019_1"))]
369    pub fn stream_spec_mode(self) -> Diff2<StreamSpecMode> {
370        Diff2 {
371            bin: self.bin,
372            global_opts: self.global_opts,
373            diff_opts: self.diff_opts,
374            mode: StreamSpecMode,
375        }
376    }
377}
378
379impl<M> Diff2<M> {
380    /// # Description
381    ///
382    /// -doptions
383    ///
384    #[cfg_attr(
385        feature = "lt2014_2",
386        doc = "Runs the diff routine with one of a subset of the standard UNIX diff flags. See the Usage Notes below for a listing of these flags."
387    )]
388    #[cfg_attr(
389        all(feature = "lt2015_1", not(feature = "lt2014_2")),
390        doc = "Runs the diff routine with one of a subset of the standard UNIX diff options. See the Usage Notes below for a listing of these options."
391    )]
392    #[cfg_attr(
393        all(feature = "lt2024_1", not(feature = "lt2015_1")),
394        doc = "Runs the diff routine with one of a subset of the standard UNIX diff options. See Usage Notes for a listing of these options."
395    )]
396    #[cfg_attr(
397        not(feature = "lt2024_1"),
398        doc = "Runs the diff routine with one of a subset of the standard UNIX diff options. See Usage notes for a listing of these options."
399    )]
400    pub fn get_diff_options(&self) -> Option<&DiffOptions> {
401        self.diff_opts.as_ref()
402    }
403
404    /// # Description
405    ///
406    /// -doptions
407    ///
408    #[cfg_attr(
409        feature = "lt2014_2",
410        doc = "Runs the diff routine with one of a subset of the standard UNIX diff flags. See the Usage Notes below for a listing of these flags."
411    )]
412    #[cfg_attr(
413        all(feature = "lt2015_1", not(feature = "lt2014_2")),
414        doc = "Runs the diff routine with one of a subset of the standard UNIX diff options. See the Usage Notes below for a listing of these options."
415    )]
416    #[cfg_attr(
417        all(feature = "lt2024_1", not(feature = "lt2015_1")),
418        doc = "Runs the diff routine with one of a subset of the standard UNIX diff options. See Usage Notes for a listing of these options."
419    )]
420    #[cfg_attr(
421        not(feature = "lt2024_1"),
422        doc = "Runs the diff routine with one of a subset of the standard UNIX diff options. See Usage notes for a listing of these options."
423    )]
424    pub fn set_diff_options(&mut self, v: impl Into<DiffOptions>) -> &mut Self {
425        self.diff_opts = Some(v.into());
426        self
427    }
428
429    /// # Description
430    ///
431    /// -doptions
432    ///
433    #[cfg_attr(
434        feature = "lt2014_2",
435        doc = "Runs the diff routine with one of a subset of the standard UNIX diff flags. See the Usage Notes below for a listing of these flags."
436    )]
437    #[cfg_attr(
438        all(feature = "lt2015_1", not(feature = "lt2014_2")),
439        doc = "Runs the diff routine with one of a subset of the standard UNIX diff options. See the Usage Notes below for a listing of these options."
440    )]
441    #[cfg_attr(
442        all(feature = "lt2024_1", not(feature = "lt2015_1")),
443        doc = "Runs the diff routine with one of a subset of the standard UNIX diff options. See Usage Notes for a listing of these options."
444    )]
445    #[cfg_attr(
446        not(feature = "lt2024_1"),
447        doc = "Runs the diff routine with one of a subset of the standard UNIX diff options. See Usage notes for a listing of these options."
448    )]
449    pub fn diff_options(mut self, v: impl Into<DiffOptions>) -> Self {
450        self.diff_opts = Some(v.into());
451        self
452    }
453}
454
455impl<M: ExclusiveOption> Diff2<M> {
456    /// Get the Perforce command path.
457    pub fn get_bin(&self) -> &Path {
458        &self.bin
459    }
460
461    /// Set the Perforce command path.
462    pub fn set_bin(&mut self, bin: impl Into<PathBuf>) -> &mut Self {
463        self.bin = bin.into();
464        self
465    }
466
467    /// Set the Perforce command path.
468    pub fn bin(mut self, bin: impl Into<PathBuf>) -> Self {
469        self.set_bin(bin);
470        self
471    }
472
473    /// # Description
474    ///
475    /// g-opts
476    ///
477    #[cfg_attr(
478        feature = "lt2014_2",
479        doc = "See the [Global Options](GlobalOpts) section."
480    )]
481    #[cfg_attr(
482        all(feature = "lt2015_1", not(feature = "lt2014_2")),
483        doc = "See the [“Global Options”](GlobalOpts) section."
484    )]
485    #[cfg_attr(
486        all(feature = "lt2017_1", not(feature = "lt2015_1")),
487        doc = "See [“Global Options”](GlobalOpts)."
488    )]
489    #[cfg_attr(
490        all(feature = "lt2018_2", not(feature = "lt2017_1")),
491        doc = "See [Global Options](GlobalOpts)."
492    )]
493    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
494    pub fn get_global_opts(&self) -> &GlobalOpts {
495        &self.global_opts
496    }
497
498    /// # Description
499    ///
500    /// g-opts
501    ///
502    #[cfg_attr(
503        feature = "lt2014_2",
504        doc = "See the [Global Options](GlobalOpts) section."
505    )]
506    #[cfg_attr(
507        all(feature = "lt2015_1", not(feature = "lt2014_2")),
508        doc = "See the [“Global Options”](GlobalOpts) section."
509    )]
510    #[cfg_attr(
511        all(feature = "lt2017_1", not(feature = "lt2015_1")),
512        doc = "See [“Global Options”](GlobalOpts)."
513    )]
514    #[cfg_attr(
515        all(feature = "lt2018_2", not(feature = "lt2017_1")),
516        doc = "See [Global Options](GlobalOpts)."
517    )]
518    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
519    pub fn set_global_opts(&mut self, v: GlobalOpts) -> &mut Self {
520        self.global_opts = v;
521        self
522    }
523
524    /// # Description
525    ///
526    /// g-opts
527    ///
528    #[cfg_attr(
529        feature = "lt2014_2",
530        doc = "See the [Global Options](GlobalOpts) section."
531    )]
532    #[cfg_attr(
533        all(feature = "lt2015_1", not(feature = "lt2014_2")),
534        doc = "See the [“Global Options”](GlobalOpts) section."
535    )]
536    #[cfg_attr(
537        all(feature = "lt2017_1", not(feature = "lt2015_1")),
538        doc = "See [“Global Options”](GlobalOpts)."
539    )]
540    #[cfg_attr(
541        all(feature = "lt2018_2", not(feature = "lt2017_1")),
542        doc = "See [Global Options](GlobalOpts)."
543    )]
544    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
545    pub fn global_opts(mut self, v: GlobalOpts) -> Self {
546        self.global_opts = v;
547        self
548    }
549}
550
551impl<M> Diff2<DepotContent<M>> {
552    /// # Description
553    ///
554    /// -Od
555    ///
556    /// Limit output to only those files that differ.
557    pub fn get_differing_only(&self) -> bool {
558        self.mode.differing_only
559    }
560
561    /// # Description
562    ///
563    /// -Od
564    ///
565    /// Limit output to only those files that differ.
566    pub fn set_differing_only(&mut self, v: bool) -> &mut Self {
567        self.mode.differing_only = v;
568        self
569    }
570
571    /// # Description
572    ///
573    /// -Od
574    ///
575    /// Limit output to only those files that differ.
576    pub fn differing_only(mut self, v: bool) -> Self {
577        self.mode.differing_only = v;
578        self
579    }
580
581    /// # Description
582    ///
583    /// -q
584    ///
585    /// Quiet diff. Display only the header; if `file1` and `file2` are
586    /// identical, display only "`file1` - no differing files" as the output.
587    pub fn get_quiet_mode(&self) -> bool {
588        self.mode.quiet_mode
589    }
590
591    /// # Description
592    ///
593    /// -q
594    ///
595    /// Quiet diff. Display only the header; if `file1` and `file2` are
596    /// identical, display only "`file1` - no differing files" as the output.
597    pub fn set_quiet_mode(&mut self, v: bool) -> &mut Self {
598        self.mode.quiet_mode = v;
599        self
600    }
601
602    /// # Description
603    ///
604    /// -q
605    ///
606    /// Quiet diff. Display only the header; if `file1` and `file2` are
607    /// identical, display only "`file1` - no differing files" as the output.
608    pub fn quiet_mode(mut self, v: bool) -> Self {
609        self.mode.quiet_mode = v;
610        self
611    }
612
613    /// # Description
614    ///
615    /// -t
616    ///
617    /// Diff the file revisions even if the file(s) are not of type `text`.
618    pub fn get_diff_nontext(&self) -> bool {
619        self.mode.diff_nontext
620    }
621
622    /// # Description
623    ///
624    /// -t
625    ///
626    /// Diff the file revisions even if the file(s) are not of type `text`.
627    pub fn set_diff_nontext(&mut self, v: bool) -> &mut Self {
628        self.mode.diff_nontext = v;
629        self
630    }
631
632    /// # Description
633    ///
634    /// -t
635    ///
636    /// Diff the file revisions even if the file(s) are not of type `text`.
637    pub fn diff_nontext(mut self, v: bool) -> Self {
638        self.mode.diff_nontext = v;
639        self
640    }
641
642    /// # Description
643    ///
644    /// -u
645    ///
646    /// Generate unified output format, showing added and deleted lines with
647    /// sufficient context for compatibility with the `patch(1)` utility.
648    /// Only those files that differ are included. File names and dates
649    #[cfg_attr(feature = "lt2017_2", doc = "remain in Perforce syntax.")]
650    #[cfg_attr(
651        all(feature = "lt2024_1", not(feature = "lt2017_2")),
652        doc = "remain in Helix Server syntax."
653    )]
654    #[cfg_attr(
655        all(feature = "lt2024_2", not(feature = "lt2024_1")),
656        doc = "remain in Helix Core Server syntax."
657    )]
658    #[cfg_attr(not(feature = "lt2024_2"), doc = "remain in P4 Server syntax.")]
659    pub fn get_unified_patch(&self) -> bool {
660        self.mode.unified_patch
661    }
662
663    /// # Description
664    ///
665    /// -u
666    ///
667    /// Generate unified output format, showing added and deleted lines with
668    /// sufficient context for compatibility with the `patch(1)` utility.
669    /// Only those files that differ are included. File names and dates
670    #[cfg_attr(feature = "lt2017_2", doc = "remain in Perforce syntax.")]
671    #[cfg_attr(
672        all(feature = "lt2024_1", not(feature = "lt2017_2")),
673        doc = "remain in Helix Server syntax."
674    )]
675    #[cfg_attr(
676        all(feature = "lt2024_2", not(feature = "lt2024_1")),
677        doc = "remain in Helix Core Server syntax."
678    )]
679    #[cfg_attr(not(feature = "lt2024_2"), doc = "remain in P4 Server syntax.")]
680    pub fn set_unified_patch(&mut self, v: bool) -> &mut Self {
681        self.mode.unified_patch = v;
682        self
683    }
684
685    /// # Description
686    ///
687    /// -u
688    ///
689    /// Generate unified output format, showing added and deleted lines with
690    /// sufficient context for compatibility with the `patch(1)` utility.
691    /// Only those files that differ are included. File names and dates
692    #[cfg_attr(feature = "lt2017_2", doc = "remain in Perforce syntax.")]
693    #[cfg_attr(
694        all(feature = "lt2024_1", not(feature = "lt2017_2")),
695        doc = "remain in Helix Server syntax."
696    )]
697    #[cfg_attr(
698        all(feature = "lt2024_2", not(feature = "lt2024_1")),
699        doc = "remain in Helix Core Server syntax."
700    )]
701    #[cfg_attr(not(feature = "lt2024_2"), doc = "remain in P4 Server syntax.")]
702    pub fn unified_patch(mut self, v: bool) -> Self {
703        self.mode.unified_patch = v;
704        self
705    }
706}
707
708impl Diff2<DepotContent<Unselected>> {
709    /// # Description
710    ///
711    /// -b branch
712    ///
713    /// Use a branch mapping to diff files in two branched codelines. The
714    /// files that are compared can be limited by file patterns in either
715    /// the `from` or `to` file specifications.
716    ///
717    /// Transitions this command to the [`DepotContent`] state with the
718    /// [`BranchMode`] sub-mode selected and the branch mapping set to
719    /// `name`.
720    pub fn branch(self, name: impl Into<String>) -> Diff2<DepotContent<BranchMode>> {
721        Diff2 {
722            bin: self.bin,
723            global_opts: self.global_opts,
724            diff_opts: self.diff_opts,
725            mode: DepotContent {
726                differing_only: self.mode.differing_only,
727                quiet_mode: self.mode.quiet_mode,
728                diff_nontext: self.mode.diff_nontext,
729                unified_patch: self.mode.unified_patch,
730                mode: BranchMode {
731                    branch: name.into(),
732                },
733            },
734        }
735    }
736
737    /// # Description
738    ///
739    /// -S stream
740    ///
741    /// Diff a stream with its parent. To diff the stream with a stream
742    /// other than its configured parent, specify [`Diff2::parent`] or
743    /// [`Diff2::set_parent`].
744    ///
745    /// Transitions this command to the [`DepotContent`] state with the
746    /// [`StreamMode`] sub-mode selected and the stream set to `name`.
747    pub fn stream(self, name: impl Into<String>) -> Diff2<DepotContent<StreamMode>> {
748        Diff2 {
749            bin: self.bin,
750            global_opts: self.global_opts,
751            diff_opts: self.diff_opts,
752            mode: DepotContent {
753                differing_only: self.mode.differing_only,
754                quiet_mode: self.mode.quiet_mode,
755                diff_nontext: self.mode.diff_nontext,
756                unified_patch: self.mode.unified_patch,
757                mode: StreamMode {
758                    stream: name.into(),
759                    parent: None,
760                },
761            },
762        }
763    }
764}
765
766impl Diff2<DepotContent<BranchMode>> {
767    /// # Description
768    ///
769    /// -b branch
770    ///
771    /// Use a branch mapping to diff files in two branched codelines. The
772    /// files that are compared can be limited by file patterns in either
773    /// the `from` or `to` file specifications.
774    pub fn get_branch(&self) -> &str {
775        &self.mode.mode.branch
776    }
777
778    /// # Description
779    ///
780    /// -b branch
781    ///
782    /// Use a branch mapping to diff files in two branched codelines. The
783    /// files that are compared can be limited by file patterns in either
784    /// the `from` or `to` file specifications.
785    pub fn set_branch(&mut self, v: impl Into<String>) -> &mut Self {
786        self.mode.mode.branch = v.into();
787        self
788    }
789
790    /// # Description
791    ///
792    /// -b branch
793    ///
794    /// Use a branch mapping to diff files in two branched codelines. The
795    /// files that are compared can be limited by file patterns in either
796    /// the `from` or `to` file specifications.
797    pub fn branch(mut self, v: impl Into<String>) -> Self {
798        self.mode.mode.branch = v.into();
799        self
800    }
801}
802
803impl Diff2<DepotContent<StreamMode>> {
804    /// # Description
805    ///
806    /// -S stream
807    ///
808    /// Diff a stream with its parent. To diff the stream with a stream
809    /// other than its configured parent, specify [`Diff2::parent`] or
810    /// [`Diff2::set_parent`].
811    pub fn get_stream(&self) -> &str {
812        &self.mode.mode.stream
813    }
814
815    /// # Description
816    ///
817    /// -S stream
818    ///
819    /// Diff a stream with its parent. To diff the stream with a stream
820    /// other than its configured parent, specify [`Diff2::parent`] or
821    /// [`Diff2::set_parent`].
822    pub fn set_stream(&mut self, v: impl Into<String>) -> &mut Self {
823        self.mode.mode.stream = v.into();
824        self
825    }
826
827    /// # Description
828    ///
829    /// -S stream
830    ///
831    /// Diff a stream with its parent. To diff the stream with a stream
832    /// other than its configured parent, specify [`Diff2::parent`] or
833    /// [`Diff2::set_parent`].
834    pub fn stream(mut self, v: impl Into<String>) -> Self {
835        self.mode.mode.stream = v.into();
836        self
837    }
838
839    /// # Description
840    ///
841    /// -P parent
842    ///
843    /// Diff the stream with a stream other than its configured parent.
844    pub fn get_parent(&self) -> Option<&str> {
845        self.mode.mode.parent.as_deref()
846    }
847
848    /// # Description
849    ///
850    /// -P parent
851    ///
852    /// Diff the stream with a stream other than its configured parent.
853    pub fn set_parent(&mut self, v: impl Into<String>) -> &mut Self {
854        self.mode.mode.parent = Some(v.into());
855        self
856    }
857
858    /// # Description
859    ///
860    /// -P parent
861    ///
862    /// Diff the stream with a stream other than its configured parent.
863    pub fn parent(mut self, v: impl Into<String>) -> Self {
864        self.mode.mode.parent = Some(v.into());
865        self
866    }
867}
868
869impl<M: ExclusiveOption> SubCommand for Diff2<M> {
870    fn name(&self) -> &str {
871        "diff2"
872    }
873
874    fn inject_local_args(&self, command: &mut Command) {
875        if let Some(diff_opts) = &self.diff_opts {
876            diff_opts.inject_arg(command);
877        }
878
879        self.mode.inject_args(command);
880    }
881
882    fn global_opts(&self) -> Option<&GlobalOpts> {
883        Some(&self.global_opts)
884    }
885}
886
887impl<A, B> ParameterizedSpawn<(A, B)> for Diff2<Unselected>
888where
889    A: AsRef<OsStr>,
890    B: AsRef<OsStr>,
891{
892    type Output = Child;
893    type Error = std::io::Error;
894
895    /// Spawns `p4 diff2` for the given pair of file arguments as a child
896    /// process with piped standard output and error streams; use the
897    /// returned [`Child`] handle to wait for it or interact with it.
898    ///
899    /// Each file argument is a file name, optionally with a revision
900    /// specifier (for example `file#2` or `file@34`).
901    fn spawn_with(&mut self, (file1, file2): (A, B)) -> Result<Self::Output, Self::Error> {
902        self.setup_command(&self.bin)
903            .arg(file1)
904            .arg(file2)
905            .stdout(Stdio::piped())
906            .stderr(Stdio::piped())
907            .spawn()
908    }
909}
910
911impl<A, B> ParameterizedSpawn<(A, B)> for Diff2<DepotContent<Unselected>>
912where
913    A: AsRef<OsStr>,
914    B: AsRef<OsStr>,
915{
916    type Output = Child;
917    type Error = std::io::Error;
918
919    /// Spawns `p4 diff2` for the given pair of file arguments as a child
920    /// process with piped standard output and error streams; use the
921    /// returned [`Child`] handle to wait for it or interact with it.
922    ///
923    /// Each file argument is a file name, optionally with a revision
924    /// specifier (for example `file#2` or `file@34`).
925    fn spawn_with(&mut self, (file1, file2): (A, B)) -> Result<Self::Output, Self::Error> {
926        self.setup_command(&self.bin)
927            .arg(file1)
928            .arg(file2)
929            .stdout(Stdio::piped())
930            .stderr(Stdio::piped())
931            .spawn()
932    }
933}
934
935impl ParameterizedSpawn<()> for Diff2<DepotContent<BranchMode>> {
936    type Output = Child;
937    type Error = std::io::Error;
938
939    /// Spawns `p4 diff2 -b branch` without file arguments as a child
940    /// process with piped standard output and error streams; the whole
941    /// branch mapping is diffed. Use the returned [`Child`] handle to wait
942    /// for it or interact with it.
943    fn spawn_with(&mut self, _: ()) -> Result<Self::Output, Self::Error> {
944        self.setup_command(&self.bin)
945            .stdout(Stdio::piped())
946            .stderr(Stdio::piped())
947            .spawn()
948    }
949}
950
951impl<I> ParameterizedSpawn<(I,)> for Diff2<DepotContent<BranchMode>>
952where
953    I: AsRef<OsStr>,
954{
955    type Output = Child;
956    type Error = std::io::Error;
957
958    /// Spawns `p4 diff2 -b branch [tofile[rev]]` as a child process with
959    /// piped standard output and error streams; use the returned [`Child`]
960    /// handle to wait for it or interact with it.
961    ///
962    /// Pass `Some(tofile)` to limit the target side of the branch mapping
963    /// to the given file, or `None` to diff the whole branch mapping.
964    fn spawn_with(&mut self, (tofile,): (I,)) -> Result<Self::Output, Self::Error> {
965        self.setup_command(&self.bin)
966            .arg(tofile)
967            .stdout(Stdio::piped())
968            .stderr(Stdio::piped())
969            .spawn()
970    }
971}
972
973impl<A, B> ParameterizedSpawn<(A, B)> for Diff2<DepotContent<BranchMode>>
974where
975    A: AsRef<OsStr>,
976    B: AsRef<OsStr>,
977{
978    type Output = Child;
979    type Error = std::io::Error;
980
981    /// Spawns `p4 diff2 -b branch fromfile[rev] tofile[rev]` as a child
982    /// process with piped standard output and error streams; the branch
983    /// mapping is diffed between the given files. Use the returned
984    /// [`Child`] handle to wait for it or interact with it.
985    fn spawn_with(&mut self, (fromfile, tofile): (A, B)) -> Result<Self::Output, Self::Error> {
986        self.setup_command(&self.bin)
987            .arg(fromfile)
988            .arg(tofile)
989            .stdout(Stdio::piped())
990            .stderr(Stdio::piped())
991            .spawn()
992    }
993}
994
995impl ParameterizedSpawn<()> for Diff2<DepotContent<StreamMode>> {
996    type Output = Child;
997    type Error = std::io::Error;
998
999    /// Spawns `p4 diff2 -S stream` without file arguments as a child
1000    /// process with piped standard output and error streams; the whole
1001    /// stream is diffed with its parent. Use the returned [`Child`] handle
1002    /// to wait for it or interact with it.
1003    fn spawn_with(&mut self, _: ()) -> Result<Self::Output, Self::Error> {
1004        self.setup_command(&self.bin)
1005            .stdout(Stdio::piped())
1006            .stderr(Stdio::piped())
1007            .spawn()
1008    }
1009}
1010
1011impl<I> ParameterizedSpawn<(I,)> for Diff2<DepotContent<StreamMode>>
1012where
1013    I: AsRef<OsStr>,
1014{
1015    type Output = Child;
1016    type Error = std::io::Error;
1017
1018    /// Spawns `p4 diff2 -S stream [tofile[rev]]` as a child process with
1019    /// piped standard output and error streams; use the returned [`Child`]
1020    /// handle to wait for it or interact with it.
1021    ///
1022    /// Pass `Some(tofile)` to limit the target side of the stream diff to
1023    /// the given file, or `None` to diff the whole stream with its parent.
1024    fn spawn_with(&mut self, (tofile,): (I,)) -> Result<Self::Output, Self::Error> {
1025        self.setup_command(&self.bin)
1026            .arg(tofile)
1027            .stdout(Stdio::piped())
1028            .stderr(Stdio::piped())
1029            .spawn()
1030    }
1031}
1032
1033impl<A, B> ParameterizedSpawn<(A, B)> for Diff2<DepotContent<StreamMode>>
1034where
1035    A: AsRef<OsStr>,
1036    B: AsRef<OsStr>,
1037{
1038    type Output = Child;
1039    type Error = std::io::Error;
1040
1041    /// Spawns `p4 diff2 -S stream fromfile[rev] tofile[rev]` as a child
1042    /// process with piped standard output and error streams; the stream is
1043    /// diffed with its parent between the given files. Use the returned
1044    /// [`Child`] handle to wait for it or interact with it.
1045    fn spawn_with(&mut self, (fromfile, tofile): (A, B)) -> Result<Self::Output, Self::Error> {
1046        self.setup_command(&self.bin)
1047            .arg(fromfile)
1048            .arg(tofile)
1049            .stdout(Stdio::piped())
1050            .stderr(Stdio::piped())
1051            .spawn()
1052    }
1053}
1054
1055#[cfg(not(feature = "lt2019_1"))]
1056impl<A, B> ParameterizedSpawn<(A, B)> for Diff2<StreamSpecMode>
1057where
1058    A: AsRef<OsStr>,
1059    B: AsRef<OsStr>,
1060{
1061    type Output = Child;
1062    type Error = std::io::Error;
1063
1064    /// Spawns `p4 diff2 -As` for the given pair of stream specs as a child
1065    /// process with piped standard output and error streams; use the
1066    /// returned [`Child`] handle to wait for it or interact with it.
1067    ///
1068    /// Each stream spec is a streamname, optionally at a specific changelist
1069    /// number: `@head` selects the head version, `@change` the version at a
1070    /// specific change, and `@=change` the shelved version at a specific
1071    /// change.
1072    fn spawn_with(&mut self, (spec1, spec2): (A, B)) -> Result<Self::Output, Self::Error> {
1073        self.setup_command(&self.bin)
1074            .arg(spec1)
1075            .arg(spec2)
1076            .stdout(Stdio::piped())
1077            .stderr(Stdio::piped())
1078            .spawn()
1079    }
1080}
1081
1082#[cfg(test)]
1083mod tests {
1084    use super::*;
1085    use crate::cmd::DiffOptionsBuilder;
1086    use crate::cmd::args_of;
1087
1088    #[test]
1089    fn bin_accessors_update_executable_path() {
1090        let mut command = Diff2::new("p4", GlobalOpts::new());
1091        assert_eq!(command.get_bin(), Path::new("p4"));
1092
1093        command.set_bin("/usr/local/bin/p4");
1094        assert_eq!(command.get_bin(), Path::new("/usr/local/bin/p4"));
1095
1096        let command = command.bin("p4.exe");
1097        assert_eq!(command.get_bin(), Path::new("p4.exe"));
1098    }
1099
1100    /// Dry-run checks of the assembled `p4 diff2` command line; no process
1101    /// is spawned.
1102    #[test]
1103    fn without_options() {
1104        let diff2 = Diff2::new("p4", GlobalOpts::new());
1105
1106        assert_eq!(args_of(&diff2.setup_command("p4")), ["diff2"]);
1107    }
1108
1109    #[test]
1110    fn depot_content_flags_are_injected() {
1111        let diff2 = Diff2::new("p4", GlobalOpts::new())
1112            .differing_only(true)
1113            .quiet_mode(true)
1114            .diff_nontext(true)
1115            .unified_patch(true);
1116
1117        assert_eq!(
1118            args_of(&diff2.setup_command("p4")),
1119            ["diff2", "-Od", "-q", "-t", "-u"]
1120        );
1121    }
1122
1123    #[test]
1124    fn depot_content_flags_apply_to_branch_mode() {
1125        let diff2 = Diff2::new("p4", GlobalOpts::new())
1126            .quiet_mode(true)
1127            .branch("branch2");
1128
1129        assert!(diff2.get_quiet_mode());
1130        assert_eq!(
1131            args_of(&diff2.setup_command("p4")),
1132            ["diff2", "-q", "-b", "branch2"]
1133        );
1134    }
1135
1136    #[test]
1137    fn depot_content_flags_apply_after_branch_transition() {
1138        let diff2 = Diff2::new("p4", GlobalOpts::new())
1139            .branch("branch2")
1140            .quiet_mode(true);
1141
1142        assert!(diff2.get_quiet_mode());
1143        assert_eq!(
1144            args_of(&diff2.setup_command("p4")),
1145            ["diff2", "-q", "-b", "branch2"]
1146        );
1147    }
1148
1149    #[test]
1150    fn diff_options_are_injected() {
1151        let mut diff2 =
1152            Diff2::new("p4", GlobalOpts::new()).diff_options(DiffOptionsBuilder::unified(None));
1153        diff2.set_diff_options(DiffOptionsBuilder::summary());
1154
1155        assert_eq!(args_of(&diff2.setup_command("p4")), ["diff2", "-ds"]);
1156    }
1157
1158    #[test]
1159    fn branch_mode_injects_branch() {
1160        let diff2 = Diff2::new("p4", GlobalOpts::new()).branch("branch2");
1161
1162        assert_eq!(diff2.get_branch(), "branch2");
1163        assert_eq!(
1164            args_of(&diff2.setup_command("p4")),
1165            ["diff2", "-b", "branch2"]
1166        );
1167    }
1168
1169    #[test]
1170    fn branch_mode_with_files() {
1171        let diff2 = Diff2::new("p4", GlobalOpts::new()).branch("branch2");
1172
1173        // Mirrors `spawn_with`, which appends the file arguments after the
1174        // assembled command.
1175        let mut command = diff2.setup_command("p4");
1176        command.arg(OsStr::new("//depot/rel1/..."));
1177        command.arg(OsStr::new("//depot/rel2/...#4"));
1178
1179        assert_eq!(
1180            args_of(&command),
1181            [
1182                "diff2",
1183                "-b",
1184                "branch2",
1185                "//depot/rel1/...",
1186                "//depot/rel2/...#4"
1187            ]
1188        );
1189    }
1190
1191    #[test]
1192    fn branch_mode_with_tofile() {
1193        let diff2 = Diff2::new("p4", GlobalOpts::new()).branch("branch2");
1194
1195        // Mirrors `spawn_with` for the single-file input, which appends the
1196        // target file after the assembled command.
1197        let mut command = diff2.setup_command("p4");
1198        command.arg(OsStr::new("//depot/rel2/...#4"));
1199
1200        assert_eq!(
1201            args_of(&command),
1202            ["diff2", "-b", "branch2", "//depot/rel2/...#4"]
1203        );
1204    }
1205
1206    #[test]
1207    fn stream_mode_injects_stream() {
1208        let diff2 = Diff2::new("p4", GlobalOpts::new()).stream("myStream");
1209
1210        assert_eq!(diff2.get_stream(), "myStream");
1211        assert_eq!(
1212            args_of(&diff2.setup_command("p4")),
1213            ["diff2", "-S", "myStream"]
1214        );
1215    }
1216
1217    #[test]
1218    fn stream_mode_with_parent() {
1219        let mut diff2 = Diff2::new("p4", GlobalOpts::new()).stream("myStream");
1220        diff2.set_parent("mainStream");
1221
1222        assert_eq!(diff2.get_parent(), Some("mainStream"));
1223        assert_eq!(
1224            args_of(&diff2.setup_command("p4")),
1225            ["diff2", "-S", "myStream", "-P", "mainStream"]
1226        );
1227    }
1228
1229    #[test]
1230    fn stream_mode_with_tofile() {
1231        let diff2 = Diff2::new("p4", GlobalOpts::new()).stream("myStream");
1232
1233        // Mirrors `spawn_with` for the single-file input, which appends the
1234        // target file after the assembled command.
1235        let mut command = diff2.setup_command("p4");
1236        command.arg(OsStr::new("//depot/rel2/...#4"));
1237
1238        assert_eq!(
1239            args_of(&command),
1240            ["diff2", "-S", "myStream", "//depot/rel2/...#4"]
1241        );
1242    }
1243
1244    #[test]
1245    fn stream_mode_preserves_diff_options() {
1246        let diff2 = Diff2::new("p4", GlobalOpts::new())
1247            .diff_options(DiffOptionsBuilder::summary())
1248            .stream("myStream");
1249
1250        assert_eq!(
1251            args_of(&diff2.setup_command("p4")),
1252            ["diff2", "-ds", "-S", "myStream"]
1253        );
1254    }
1255
1256    #[cfg(not(feature = "lt2019_1"))]
1257    #[test]
1258    fn stream_spec_mode_bare() {
1259        let diff2 = Diff2::new("p4", GlobalOpts::new()).stream_spec_mode();
1260
1261        assert_eq!(args_of(&diff2.setup_command("p4")), ["diff2", "-As"]);
1262    }
1263
1264    #[cfg(not(feature = "lt2019_1"))]
1265    #[test]
1266    fn stream_spec_mode_with_specs() {
1267        let diff2 = Diff2::new("p4", GlobalOpts::new()).stream_spec_mode();
1268
1269        // Mirrors `spawn_with`, which appends the two stream specs after
1270        // the assembled command.
1271        let mut command = diff2.setup_command("p4");
1272        command.arg("myStream@=1");
1273        command.arg("yourStream@2");
1274
1275        assert_eq!(
1276            args_of(&command),
1277            ["diff2", "-As", "myStream@=1", "yourStream@2"]
1278        );
1279    }
1280}