Skip to main content

perforce_cli/cmd/
diff2.rs

1use std::{
2    ffi::OsStr,
3    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    /// # Description
457    ///
458    /// g-opts
459    ///
460    #[cfg_attr(
461        feature = "lt2014_2",
462        doc = "See the [Global Options](GlobalOpts) section."
463    )]
464    #[cfg_attr(
465        all(feature = "lt2015_1", not(feature = "lt2014_2")),
466        doc = "See the [“Global Options”](GlobalOpts) section."
467    )]
468    #[cfg_attr(
469        all(feature = "lt2017_1", not(feature = "lt2015_1")),
470        doc = "See [“Global Options”](GlobalOpts)."
471    )]
472    #[cfg_attr(
473        all(feature = "lt2018_2", not(feature = "lt2017_1")),
474        doc = "See [Global Options](GlobalOpts)."
475    )]
476    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
477    pub fn get_global_opts(&self) -> &GlobalOpts {
478        &self.global_opts
479    }
480
481    /// # Description
482    ///
483    /// g-opts
484    ///
485    #[cfg_attr(
486        feature = "lt2014_2",
487        doc = "See the [Global Options](GlobalOpts) section."
488    )]
489    #[cfg_attr(
490        all(feature = "lt2015_1", not(feature = "lt2014_2")),
491        doc = "See the [“Global Options”](GlobalOpts) section."
492    )]
493    #[cfg_attr(
494        all(feature = "lt2017_1", not(feature = "lt2015_1")),
495        doc = "See [“Global Options”](GlobalOpts)."
496    )]
497    #[cfg_attr(
498        all(feature = "lt2018_2", not(feature = "lt2017_1")),
499        doc = "See [Global Options](GlobalOpts)."
500    )]
501    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
502    pub fn set_global_opts(&mut self, v: GlobalOpts) -> &mut Self {
503        self.global_opts = v;
504        self
505    }
506
507    /// # Description
508    ///
509    /// g-opts
510    ///
511    #[cfg_attr(
512        feature = "lt2014_2",
513        doc = "See the [Global Options](GlobalOpts) section."
514    )]
515    #[cfg_attr(
516        all(feature = "lt2015_1", not(feature = "lt2014_2")),
517        doc = "See the [“Global Options”](GlobalOpts) section."
518    )]
519    #[cfg_attr(
520        all(feature = "lt2017_1", not(feature = "lt2015_1")),
521        doc = "See [“Global Options”](GlobalOpts)."
522    )]
523    #[cfg_attr(
524        all(feature = "lt2018_2", not(feature = "lt2017_1")),
525        doc = "See [Global Options](GlobalOpts)."
526    )]
527    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
528    pub fn global_opts(mut self, v: GlobalOpts) -> Self {
529        self.global_opts = v;
530        self
531    }
532}
533
534impl<M> Diff2<DepotContent<M>> {
535    /// # Description
536    ///
537    /// -Od
538    ///
539    /// Limit output to only those files that differ.
540    pub fn get_differing_only(&self) -> bool {
541        self.mode.differing_only
542    }
543
544    /// # Description
545    ///
546    /// -Od
547    ///
548    /// Limit output to only those files that differ.
549    pub fn set_differing_only(&mut self, v: bool) -> &mut Self {
550        self.mode.differing_only = v;
551        self
552    }
553
554    /// # Description
555    ///
556    /// -Od
557    ///
558    /// Limit output to only those files that differ.
559    pub fn differing_only(mut self, v: bool) -> Self {
560        self.mode.differing_only = v;
561        self
562    }
563
564    /// # Description
565    ///
566    /// -q
567    ///
568    /// Quiet diff. Display only the header; if `file1` and `file2` are
569    /// identical, display only "`file1` - no differing files" as the output.
570    pub fn get_quiet_mode(&self) -> bool {
571        self.mode.quiet_mode
572    }
573
574    /// # Description
575    ///
576    /// -q
577    ///
578    /// Quiet diff. Display only the header; if `file1` and `file2` are
579    /// identical, display only "`file1` - no differing files" as the output.
580    pub fn set_quiet_mode(&mut self, v: bool) -> &mut Self {
581        self.mode.quiet_mode = v;
582        self
583    }
584
585    /// # Description
586    ///
587    /// -q
588    ///
589    /// Quiet diff. Display only the header; if `file1` and `file2` are
590    /// identical, display only "`file1` - no differing files" as the output.
591    pub fn quiet_mode(mut self, v: bool) -> Self {
592        self.mode.quiet_mode = v;
593        self
594    }
595
596    /// # Description
597    ///
598    /// -t
599    ///
600    /// Diff the file revisions even if the file(s) are not of type `text`.
601    pub fn get_diff_nontext(&self) -> bool {
602        self.mode.diff_nontext
603    }
604
605    /// # Description
606    ///
607    /// -t
608    ///
609    /// Diff the file revisions even if the file(s) are not of type `text`.
610    pub fn set_diff_nontext(&mut self, v: bool) -> &mut Self {
611        self.mode.diff_nontext = v;
612        self
613    }
614
615    /// # Description
616    ///
617    /// -t
618    ///
619    /// Diff the file revisions even if the file(s) are not of type `text`.
620    pub fn diff_nontext(mut self, v: bool) -> Self {
621        self.mode.diff_nontext = v;
622        self
623    }
624
625    /// # Description
626    ///
627    /// -u
628    ///
629    /// Generate unified output format, showing added and deleted lines with
630    /// sufficient context for compatibility with the `patch(1)` utility.
631    /// Only those files that differ are included. File names and dates
632    #[cfg_attr(feature = "lt2017_2", doc = "remain in Perforce syntax.")]
633    #[cfg_attr(
634        all(feature = "lt2024_1", not(feature = "lt2017_2")),
635        doc = "remain in Helix Server syntax."
636    )]
637    #[cfg_attr(
638        all(feature = "lt2024_2", not(feature = "lt2024_1")),
639        doc = "remain in Helix Core Server syntax."
640    )]
641    #[cfg_attr(not(feature = "lt2024_2"), doc = "remain in P4 Server syntax.")]
642    pub fn get_unified_patch(&self) -> bool {
643        self.mode.unified_patch
644    }
645
646    /// # Description
647    ///
648    /// -u
649    ///
650    /// Generate unified output format, showing added and deleted lines with
651    /// sufficient context for compatibility with the `patch(1)` utility.
652    /// Only those files that differ are included. File names and dates
653    #[cfg_attr(feature = "lt2017_2", doc = "remain in Perforce syntax.")]
654    #[cfg_attr(
655        all(feature = "lt2024_1", not(feature = "lt2017_2")),
656        doc = "remain in Helix Server syntax."
657    )]
658    #[cfg_attr(
659        all(feature = "lt2024_2", not(feature = "lt2024_1")),
660        doc = "remain in Helix Core Server syntax."
661    )]
662    #[cfg_attr(not(feature = "lt2024_2"), doc = "remain in P4 Server syntax.")]
663    pub fn set_unified_patch(&mut self, v: bool) -> &mut Self {
664        self.mode.unified_patch = v;
665        self
666    }
667
668    /// # Description
669    ///
670    /// -u
671    ///
672    /// Generate unified output format, showing added and deleted lines with
673    /// sufficient context for compatibility with the `patch(1)` utility.
674    /// Only those files that differ are included. File names and dates
675    #[cfg_attr(feature = "lt2017_2", doc = "remain in Perforce syntax.")]
676    #[cfg_attr(
677        all(feature = "lt2024_1", not(feature = "lt2017_2")),
678        doc = "remain in Helix Server syntax."
679    )]
680    #[cfg_attr(
681        all(feature = "lt2024_2", not(feature = "lt2024_1")),
682        doc = "remain in Helix Core Server syntax."
683    )]
684    #[cfg_attr(not(feature = "lt2024_2"), doc = "remain in P4 Server syntax.")]
685    pub fn unified_patch(mut self, v: bool) -> Self {
686        self.mode.unified_patch = v;
687        self
688    }
689}
690
691impl Diff2<DepotContent<Unselected>> {
692    /// # Description
693    ///
694    /// -b branch
695    ///
696    /// Use a branch mapping to diff files in two branched codelines. The
697    /// files that are compared can be limited by file patterns in either
698    /// the `from` or `to` file specifications.
699    ///
700    /// Transitions this command to the [`DepotContent`] state with the
701    /// [`BranchMode`] sub-mode selected and the branch mapping set to
702    /// `name`.
703    pub fn branch(self, name: impl Into<String>) -> Diff2<DepotContent<BranchMode>> {
704        Diff2 {
705            bin: self.bin,
706            global_opts: self.global_opts,
707            diff_opts: self.diff_opts,
708            mode: DepotContent {
709                differing_only: self.mode.differing_only,
710                quiet_mode: self.mode.quiet_mode,
711                diff_nontext: self.mode.diff_nontext,
712                unified_patch: self.mode.unified_patch,
713                mode: BranchMode {
714                    branch: name.into(),
715                },
716            },
717        }
718    }
719
720    /// # Description
721    ///
722    /// -S stream
723    ///
724    /// Diff a stream with its parent. To diff the stream with a stream
725    /// other than its configured parent, specify [`Diff2::parent`] or
726    /// [`Diff2::set_parent`].
727    ///
728    /// Transitions this command to the [`DepotContent`] state with the
729    /// [`StreamMode`] sub-mode selected and the stream set to `name`.
730    pub fn stream(self, name: impl Into<String>) -> Diff2<DepotContent<StreamMode>> {
731        Diff2 {
732            bin: self.bin,
733            global_opts: self.global_opts,
734            diff_opts: self.diff_opts,
735            mode: DepotContent {
736                differing_only: self.mode.differing_only,
737                quiet_mode: self.mode.quiet_mode,
738                diff_nontext: self.mode.diff_nontext,
739                unified_patch: self.mode.unified_patch,
740                mode: StreamMode {
741                    stream: name.into(),
742                    parent: None,
743                },
744            },
745        }
746    }
747}
748
749impl Diff2<DepotContent<BranchMode>> {
750    /// # Description
751    ///
752    /// -b branch
753    ///
754    /// Use a branch mapping to diff files in two branched codelines. The
755    /// files that are compared can be limited by file patterns in either
756    /// the `from` or `to` file specifications.
757    pub fn get_branch(&self) -> &str {
758        &self.mode.mode.branch
759    }
760
761    /// # Description
762    ///
763    /// -b branch
764    ///
765    /// Use a branch mapping to diff files in two branched codelines. The
766    /// files that are compared can be limited by file patterns in either
767    /// the `from` or `to` file specifications.
768    pub fn set_branch(&mut self, v: impl Into<String>) -> &mut Self {
769        self.mode.mode.branch = v.into();
770        self
771    }
772
773    /// # Description
774    ///
775    /// -b branch
776    ///
777    /// Use a branch mapping to diff files in two branched codelines. The
778    /// files that are compared can be limited by file patterns in either
779    /// the `from` or `to` file specifications.
780    pub fn branch(mut self, v: impl Into<String>) -> Self {
781        self.mode.mode.branch = v.into();
782        self
783    }
784}
785
786impl Diff2<DepotContent<StreamMode>> {
787    /// # Description
788    ///
789    /// -S stream
790    ///
791    /// Diff a stream with its parent. To diff the stream with a stream
792    /// other than its configured parent, specify [`Diff2::parent`] or
793    /// [`Diff2::set_parent`].
794    pub fn get_stream(&self) -> &str {
795        &self.mode.mode.stream
796    }
797
798    /// # Description
799    ///
800    /// -S stream
801    ///
802    /// Diff a stream with its parent. To diff the stream with a stream
803    /// other than its configured parent, specify [`Diff2::parent`] or
804    /// [`Diff2::set_parent`].
805    pub fn set_stream(&mut self, v: impl Into<String>) -> &mut Self {
806        self.mode.mode.stream = v.into();
807        self
808    }
809
810    /// # Description
811    ///
812    /// -S stream
813    ///
814    /// Diff a stream with its parent. To diff the stream with a stream
815    /// other than its configured parent, specify [`Diff2::parent`] or
816    /// [`Diff2::set_parent`].
817    pub fn stream(mut self, v: impl Into<String>) -> Self {
818        self.mode.mode.stream = v.into();
819        self
820    }
821
822    /// # Description
823    ///
824    /// -P parent
825    ///
826    /// Diff the stream with a stream other than its configured parent.
827    pub fn get_parent(&self) -> Option<&str> {
828        self.mode.mode.parent.as_deref()
829    }
830
831    /// # Description
832    ///
833    /// -P parent
834    ///
835    /// Diff the stream with a stream other than its configured parent.
836    pub fn set_parent(&mut self, v: impl Into<String>) -> &mut Self {
837        self.mode.mode.parent = Some(v.into());
838        self
839    }
840
841    /// # Description
842    ///
843    /// -P parent
844    ///
845    /// Diff the stream with a stream other than its configured parent.
846    pub fn parent(mut self, v: impl Into<String>) -> Self {
847        self.mode.mode.parent = Some(v.into());
848        self
849    }
850}
851
852impl<M: ExclusiveOption> SubCommand for Diff2<M> {
853    fn name(&self) -> &str {
854        "diff2"
855    }
856
857    fn inject_local_args(&self, command: &mut Command) {
858        if let Some(diff_opts) = &self.diff_opts {
859            diff_opts.inject_arg(command);
860        }
861
862        self.mode.inject_args(command);
863    }
864
865    fn global_opts(&self) -> Option<&GlobalOpts> {
866        Some(&self.global_opts)
867    }
868}
869
870impl<A, B> ParameterizedSpawn<(A, B)> for Diff2<Unselected>
871where
872    A: AsRef<OsStr>,
873    B: AsRef<OsStr>,
874{
875    type Output = Child;
876    type Error = std::io::Error;
877
878    /// Spawns `p4 diff2` for the given pair of file arguments as a child
879    /// process with piped standard output and error streams; use the
880    /// returned [`Child`] handle to wait for it or interact with it.
881    ///
882    /// Each file argument is a file name, optionally with a revision
883    /// specifier (for example `file#2` or `file@34`).
884    fn spawn_with(&mut self, (file1, file2): (A, B)) -> Result<Self::Output, Self::Error> {
885        self.setup_command(&self.bin)
886            .arg(file1)
887            .arg(file2)
888            .stdout(Stdio::piped())
889            .stderr(Stdio::piped())
890            .spawn()
891    }
892}
893
894impl<A, B> ParameterizedSpawn<(A, B)> for Diff2<DepotContent<Unselected>>
895where
896    A: AsRef<OsStr>,
897    B: AsRef<OsStr>,
898{
899    type Output = Child;
900    type Error = std::io::Error;
901
902    /// Spawns `p4 diff2` for the given pair of file arguments as a child
903    /// process with piped standard output and error streams; use the
904    /// returned [`Child`] handle to wait for it or interact with it.
905    ///
906    /// Each file argument is a file name, optionally with a revision
907    /// specifier (for example `file#2` or `file@34`).
908    fn spawn_with(&mut self, (file1, file2): (A, B)) -> Result<Self::Output, Self::Error> {
909        self.setup_command(&self.bin)
910            .arg(file1)
911            .arg(file2)
912            .stdout(Stdio::piped())
913            .stderr(Stdio::piped())
914            .spawn()
915    }
916}
917
918impl ParameterizedSpawn<()> for Diff2<DepotContent<BranchMode>> {
919    type Output = Child;
920    type Error = std::io::Error;
921
922    /// Spawns `p4 diff2 -b branch` without file arguments as a child
923    /// process with piped standard output and error streams; the whole
924    /// branch mapping is diffed. Use the returned [`Child`] handle to wait
925    /// for it or interact with it.
926    fn spawn_with(&mut self, _: ()) -> Result<Self::Output, Self::Error> {
927        self.setup_command(&self.bin)
928            .stdout(Stdio::piped())
929            .stderr(Stdio::piped())
930            .spawn()
931    }
932}
933
934impl<I> ParameterizedSpawn<(I,)> for Diff2<DepotContent<BranchMode>>
935where
936    I: AsRef<OsStr>,
937{
938    type Output = Child;
939    type Error = std::io::Error;
940
941    /// Spawns `p4 diff2 -b branch [tofile[rev]]` as a child process with
942    /// piped standard output and error streams; use the returned [`Child`]
943    /// handle to wait for it or interact with it.
944    ///
945    /// Pass `Some(tofile)` to limit the target side of the branch mapping
946    /// to the given file, or `None` to diff the whole branch mapping.
947    fn spawn_with(&mut self, (tofile,): (I,)) -> Result<Self::Output, Self::Error> {
948        self.setup_command(&self.bin)
949            .arg(tofile)
950            .stdout(Stdio::piped())
951            .stderr(Stdio::piped())
952            .spawn()
953    }
954}
955
956impl<A, B> ParameterizedSpawn<(A, B)> for Diff2<DepotContent<BranchMode>>
957where
958    A: AsRef<OsStr>,
959    B: AsRef<OsStr>,
960{
961    type Output = Child;
962    type Error = std::io::Error;
963
964    /// Spawns `p4 diff2 -b branch fromfile[rev] tofile[rev]` as a child
965    /// process with piped standard output and error streams; the branch
966    /// mapping is diffed between the given files. Use the returned
967    /// [`Child`] handle to wait for it or interact with it.
968    fn spawn_with(&mut self, (fromfile, tofile): (A, B)) -> Result<Self::Output, Self::Error> {
969        self.setup_command(&self.bin)
970            .arg(fromfile)
971            .arg(tofile)
972            .stdout(Stdio::piped())
973            .stderr(Stdio::piped())
974            .spawn()
975    }
976}
977
978impl ParameterizedSpawn<()> for Diff2<DepotContent<StreamMode>> {
979    type Output = Child;
980    type Error = std::io::Error;
981
982    /// Spawns `p4 diff2 -S stream` without file arguments as a child
983    /// process with piped standard output and error streams; the whole
984    /// stream is diffed with its parent. Use the returned [`Child`] handle
985    /// to wait for it or interact with it.
986    fn spawn_with(&mut self, _: ()) -> Result<Self::Output, Self::Error> {
987        self.setup_command(&self.bin)
988            .stdout(Stdio::piped())
989            .stderr(Stdio::piped())
990            .spawn()
991    }
992}
993
994impl<I> ParameterizedSpawn<(I,)> for Diff2<DepotContent<StreamMode>>
995where
996    I: AsRef<OsStr>,
997{
998    type Output = Child;
999    type Error = std::io::Error;
1000
1001    /// Spawns `p4 diff2 -S stream [tofile[rev]]` as a child process with
1002    /// piped standard output and error streams; use the returned [`Child`]
1003    /// handle to wait for it or interact with it.
1004    ///
1005    /// Pass `Some(tofile)` to limit the target side of the stream diff to
1006    /// the given file, or `None` to diff the whole stream with its parent.
1007    fn spawn_with(&mut self, (tofile,): (I,)) -> Result<Self::Output, Self::Error> {
1008        self.setup_command(&self.bin)
1009            .arg(tofile)
1010            .stdout(Stdio::piped())
1011            .stderr(Stdio::piped())
1012            .spawn()
1013    }
1014}
1015
1016impl<A, B> ParameterizedSpawn<(A, B)> for Diff2<DepotContent<StreamMode>>
1017where
1018    A: AsRef<OsStr>,
1019    B: AsRef<OsStr>,
1020{
1021    type Output = Child;
1022    type Error = std::io::Error;
1023
1024    /// Spawns `p4 diff2 -S stream fromfile[rev] tofile[rev]` as a child
1025    /// process with piped standard output and error streams; the stream is
1026    /// diffed with its parent between the given files. Use the returned
1027    /// [`Child`] handle to wait for it or interact with it.
1028    fn spawn_with(&mut self, (fromfile, tofile): (A, B)) -> Result<Self::Output, Self::Error> {
1029        self.setup_command(&self.bin)
1030            .arg(fromfile)
1031            .arg(tofile)
1032            .stdout(Stdio::piped())
1033            .stderr(Stdio::piped())
1034            .spawn()
1035    }
1036}
1037
1038#[cfg(not(feature = "lt2019_1"))]
1039impl<A, B> ParameterizedSpawn<(A, B)> for Diff2<StreamSpecMode>
1040where
1041    A: AsRef<OsStr>,
1042    B: AsRef<OsStr>,
1043{
1044    type Output = Child;
1045    type Error = std::io::Error;
1046
1047    /// Spawns `p4 diff2 -As` for the given pair of stream specs as a child
1048    /// process with piped standard output and error streams; use the
1049    /// returned [`Child`] handle to wait for it or interact with it.
1050    ///
1051    /// Each stream spec is a streamname, optionally at a specific changelist
1052    /// number: `@head` selects the head version, `@change` the version at a
1053    /// specific change, and `@=change` the shelved version at a specific
1054    /// change.
1055    fn spawn_with(&mut self, (spec1, spec2): (A, B)) -> Result<Self::Output, Self::Error> {
1056        self.setup_command(&self.bin)
1057            .arg(spec1)
1058            .arg(spec2)
1059            .stdout(Stdio::piped())
1060            .stderr(Stdio::piped())
1061            .spawn()
1062    }
1063}
1064
1065#[cfg(test)]
1066mod tests {
1067    use super::*;
1068    use crate::cmd::DiffOptionsBuilder;
1069    use crate::cmd::args_of;
1070
1071    /// Dry-run checks of the assembled `p4 diff2` command line; no process
1072    /// is spawned.
1073    #[test]
1074    fn without_options() {
1075        let diff2 = Diff2::new("p4", GlobalOpts::new());
1076
1077        assert_eq!(args_of(&diff2.setup_command("p4")), ["diff2"]);
1078    }
1079
1080    #[test]
1081    fn depot_content_flags_are_injected() {
1082        let diff2 = Diff2::new("p4", GlobalOpts::new())
1083            .differing_only(true)
1084            .quiet_mode(true)
1085            .diff_nontext(true)
1086            .unified_patch(true);
1087
1088        assert_eq!(
1089            args_of(&diff2.setup_command("p4")),
1090            ["diff2", "-Od", "-q", "-t", "-u"]
1091        );
1092    }
1093
1094    #[test]
1095    fn depot_content_flags_apply_to_branch_mode() {
1096        let diff2 = Diff2::new("p4", GlobalOpts::new())
1097            .quiet_mode(true)
1098            .branch("branch2");
1099
1100        assert!(diff2.get_quiet_mode());
1101        assert_eq!(
1102            args_of(&diff2.setup_command("p4")),
1103            ["diff2", "-q", "-b", "branch2"]
1104        );
1105    }
1106
1107    #[test]
1108    fn depot_content_flags_apply_after_branch_transition() {
1109        let diff2 = Diff2::new("p4", GlobalOpts::new())
1110            .branch("branch2")
1111            .quiet_mode(true);
1112
1113        assert!(diff2.get_quiet_mode());
1114        assert_eq!(
1115            args_of(&diff2.setup_command("p4")),
1116            ["diff2", "-q", "-b", "branch2"]
1117        );
1118    }
1119
1120    #[test]
1121    fn diff_options_are_injected() {
1122        let mut diff2 =
1123            Diff2::new("p4", GlobalOpts::new()).diff_options(DiffOptionsBuilder::unified(None));
1124        diff2.set_diff_options(DiffOptionsBuilder::summary());
1125
1126        assert_eq!(args_of(&diff2.setup_command("p4")), ["diff2", "-ds"]);
1127    }
1128
1129    #[test]
1130    fn branch_mode_injects_branch() {
1131        let diff2 = Diff2::new("p4", GlobalOpts::new()).branch("branch2");
1132
1133        assert_eq!(diff2.get_branch(), "branch2");
1134        assert_eq!(
1135            args_of(&diff2.setup_command("p4")),
1136            ["diff2", "-b", "branch2"]
1137        );
1138    }
1139
1140    #[test]
1141    fn branch_mode_with_files() {
1142        let diff2 = Diff2::new("p4", GlobalOpts::new()).branch("branch2");
1143
1144        // Mirrors `spawn_with`, which appends the file arguments after the
1145        // assembled command.
1146        let mut command = diff2.setup_command("p4");
1147        command.arg(OsStr::new("//depot/rel1/..."));
1148        command.arg(OsStr::new("//depot/rel2/...#4"));
1149
1150        assert_eq!(
1151            args_of(&command),
1152            [
1153                "diff2",
1154                "-b",
1155                "branch2",
1156                "//depot/rel1/...",
1157                "//depot/rel2/...#4"
1158            ]
1159        );
1160    }
1161
1162    #[test]
1163    fn branch_mode_with_tofile() {
1164        let diff2 = Diff2::new("p4", GlobalOpts::new()).branch("branch2");
1165
1166        // Mirrors `spawn_with` for the single-file input, which appends the
1167        // target file after the assembled command.
1168        let mut command = diff2.setup_command("p4");
1169        command.arg(OsStr::new("//depot/rel2/...#4"));
1170
1171        assert_eq!(
1172            args_of(&command),
1173            ["diff2", "-b", "branch2", "//depot/rel2/...#4"]
1174        );
1175    }
1176
1177    #[test]
1178    fn stream_mode_injects_stream() {
1179        let diff2 = Diff2::new("p4", GlobalOpts::new()).stream("myStream");
1180
1181        assert_eq!(diff2.get_stream(), "myStream");
1182        assert_eq!(
1183            args_of(&diff2.setup_command("p4")),
1184            ["diff2", "-S", "myStream"]
1185        );
1186    }
1187
1188    #[test]
1189    fn stream_mode_with_parent() {
1190        let mut diff2 = Diff2::new("p4", GlobalOpts::new()).stream("myStream");
1191        diff2.set_parent("mainStream");
1192
1193        assert_eq!(diff2.get_parent(), Some("mainStream"));
1194        assert_eq!(
1195            args_of(&diff2.setup_command("p4")),
1196            ["diff2", "-S", "myStream", "-P", "mainStream"]
1197        );
1198    }
1199
1200    #[test]
1201    fn stream_mode_with_tofile() {
1202        let diff2 = Diff2::new("p4", GlobalOpts::new()).stream("myStream");
1203
1204        // Mirrors `spawn_with` for the single-file input, which appends the
1205        // target file after the assembled command.
1206        let mut command = diff2.setup_command("p4");
1207        command.arg(OsStr::new("//depot/rel2/...#4"));
1208
1209        assert_eq!(
1210            args_of(&command),
1211            ["diff2", "-S", "myStream", "//depot/rel2/...#4"]
1212        );
1213    }
1214
1215    #[test]
1216    fn stream_mode_preserves_diff_options() {
1217        let diff2 = Diff2::new("p4", GlobalOpts::new())
1218            .diff_options(DiffOptionsBuilder::summary())
1219            .stream("myStream");
1220
1221        assert_eq!(
1222            args_of(&diff2.setup_command("p4")),
1223            ["diff2", "-ds", "-S", "myStream"]
1224        );
1225    }
1226
1227    #[cfg(not(feature = "lt2019_1"))]
1228    #[test]
1229    fn stream_spec_mode_bare() {
1230        let diff2 = Diff2::new("p4", GlobalOpts::new()).stream_spec_mode();
1231
1232        assert_eq!(args_of(&diff2.setup_command("p4")), ["diff2", "-As"]);
1233    }
1234
1235    #[cfg(not(feature = "lt2019_1"))]
1236    #[test]
1237    fn stream_spec_mode_with_specs() {
1238        let diff2 = Diff2::new("p4", GlobalOpts::new()).stream_spec_mode();
1239
1240        // Mirrors `spawn_with`, which appends the two stream specs after
1241        // the assembled command.
1242        let mut command = diff2.setup_command("p4");
1243        command.arg("myStream@=1");
1244        command.arg("yourStream@2");
1245
1246        assert_eq!(
1247            args_of(&command),
1248            ["diff2", "-As", "myStream@=1", "yourStream@2"]
1249        );
1250    }
1251}