Skip to main content

perforce_cli/cmd/
diff.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/// Display options passed via the `-soptions` flag, producing a shorthand
13/// list of files that match the filter instead of diffs.
14///
15/// Exactly one filter may be selected; the enum makes the alternatives
16/// unavailable at compile time.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum DisplayOptions {
19    /// `-sa`
20    ///
21    /// Show only the names of opened files that are different from the
22    /// revision in the depot, or are missing.
23    OpenedChangedOrMissing,
24
25    /// `-sb`
26    ///
27    /// Show only the names of files opened for integrate that have been
28    /// resolved, but that have been modified after being resolved.
29    ResolvedThenModified,
30
31    /// `-sd`
32    ///
33    /// Show only the names of unopened files that are missing from the
34    /// client workspace, but present in the depot.
35    UnopenedMissing,
36
37    /// `-se`
38    ///
39    /// Show only the names of unopened files in the client workspace that
40    /// are different than the revision in the depot.
41    UnopenedChanged,
42
43    /// `-sl file ...`
44    ///
45    /// Every unopened `file` is compared with the depot, and listed with a
46    /// status of `same`, `diff`, or `missing`.
47    ///
48    #[cfg_attr(
49        feature = "lt2014_2",
50        doc = "If you use the `-f` flag together with the `-sl` flag, files that are open for edit are also compared and their status is listed."
51    )]
52    #[cfg_attr(
53        not(feature = "lt2014_2"),
54        doc = "If you use the `-f` option together with the `-sl` option, files that are open for edit are also compared and their status is listed."
55    )]
56    ///
57    /// The compared files are passed as the file arguments of the spawned
58    /// command.
59    ListWithStatus,
60
61    /// `-sr`
62    ///
63    /// Show only the names of opened files in the client workspace that are
64    /// identical to the revision in the depot.
65    OpenedIdentical,
66}
67
68impl DisplayOptions {
69    /// Returns the command-line flag for this display option.
70    pub fn as_str(&self) -> &'static str {
71        match self {
72            DisplayOptions::OpenedChangedOrMissing => "-sa",
73            DisplayOptions::ResolvedThenModified => "-sb",
74            DisplayOptions::UnopenedMissing => "-sd",
75            DisplayOptions::UnopenedChanged => "-se",
76            DisplayOptions::ListWithStatus => "-sl",
77            DisplayOptions::OpenedIdentical => "-sr",
78        }
79    }
80}
81
82/// The `-soptions` state of [`WorkspaceMode`]: a display filter is selected,
83/// making `-m max` unavailable.
84///
85/// Entered with [`Diff::display_options`].
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub struct WorkspaceDisplayMode {
88    display_opts: DisplayOptions,
89}
90
91impl ExclusiveOption for WorkspaceDisplayMode {
92    fn inject_args(&self, command: &mut Command) {
93        command.arg(self.display_opts.as_str());
94    }
95}
96
97/// The `-m max` state of [`WorkspaceMode`]: a file limit is selected, making
98/// `-soptions` unavailable.
99///
100/// Entered with [`Diff::limit`].
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub struct WorkspaceRegularMode {
103    limit: u64,
104}
105
106impl ExclusiveOption for WorkspaceRegularMode {
107    fn inject_args(&self, command: &mut Command) {
108        command.arg("-m").arg(self.limit.to_string());
109    }
110}
111
112/// Workspace content mode of `p4 diff`, comparing files in the client
113/// workspace to revisions in the depot.
114///
115/// Entered with [`Diff::force`], [`Diff::differing_only`], or
116/// [`Diff::diff_nontext`].
117///
118/// The `M` type parameter isolates `-m max` and `-soptions` at compile time:
119/// [`Diff::limit`] transitions to the [`WorkspaceRegularMode`] state, while
120/// [`Diff::display_options`] transitions to the [`WorkspaceDisplayMode`]
121/// state.
122#[derive(Debug, Clone, Default)]
123pub struct WorkspaceMode<M = Unselected> {
124    force: bool,
125
126    differing_only: bool,
127
128    diff_nontext: bool,
129
130    mode: M,
131}
132
133impl<M: ExclusiveOption> ExclusiveOption for WorkspaceMode<M> {
134    fn inject_args(&self, command: &mut Command) {
135        if self.force {
136            command.arg("-f");
137        }
138
139        if self.diff_nontext {
140            command.arg("-t");
141        }
142
143        if self.differing_only {
144            command.arg("-Od");
145        }
146
147        self.mode.inject_args(command);
148    }
149}
150
151/// Stream spec mode of `p4 diff` (`-As`): diff a privately edited stream
152/// spec against another version of the same stream spec, or diff two
153/// arbitrary stream specs against each other.
154///
155/// Entered with [`Diff::stream_spec_mode`]; the stream spec to compare
156/// against is passed as the argument of the spawned command.
157#[cfg(not(feature = "lt2019_1"))]
158#[derive(Debug, Clone, Default, PartialEq, Eq)]
159pub struct StreamSpecMode;
160
161#[cfg(not(feature = "lt2019_1"))]
162impl ExclusiveOption for StreamSpecMode {
163    fn inject_args(&self, command: &mut Command) {
164        command.arg("-As");
165    }
166}
167
168#[cfg_attr(
169    feature = "lt2014_2",
170    doc = "`p4 [g-opts] diff [-dflags -f -m max -Od -sa -sb -sd -se -sr -sl -t] [file[rev#]...]`"
171)]
172#[cfg_attr(
173    all(feature = "lt2015_1", not(feature = "lt2014_2")),
174    doc = "`p4 [g-opts] diff [-doptions -f -m max -Od -sa -sb -sd -se -sr -sl -t] [file[rev#]...]`"
175)]
176#[cfg_attr(
177    all(feature = "lt2016_1", not(feature = "lt2015_1")),
178    doc = "`p4 [g-opts] diff [-doptions] [-f -t -Od] [-m max] [-soptions] [file[rev] …]`"
179)]
180#[cfg_attr(
181    all(feature = "lt2019_1", not(feature = "lt2016_1")),
182    doc = "`p4 [g-opts] diff [-doptions] [-f -t -Od] [-m max] [-soptions] [file[rev] ...]`"
183)]
184#[cfg_attr(
185    not(feature = "lt2019_1"),
186    doc = "`p4 [g-opts] diff [-doptions] [-f -t -Od] [-m max] [-soptions] [file[rev] ...]`",
187    doc = "",
188    doc = "`p4 [g-opts] diff [-doptions] -As [streamname[@change]]`"
189)]
190///
191/// Diff utility for comparing workspace content to depot content. (For
192/// comparing two depot paths, see `p4 diff2`.)
193///
194#[cfg_attr(not(feature = "lt2019_1"), doc = "Also for stream spec comparison.")]
195///
196#[cfg_attr(
197    feature = "lt2019_1",
198    doc = "The `M` type parameter tracks the command mode at compile time. The default [`Unselected`] state offers the workspace content options; [`Self::force`], [`Self::differing_only`], and [`Self::diff_nontext`] transition to the [`WorkspaceMode`] state."
199)]
200#[cfg_attr(
201    not(feature = "lt2019_1"),
202    doc = "The `M` type parameter tracks the command mode at compile time. The default [`Unselected`] state offers neither the workspace content options nor `-As`; [`Self::force`], [`Self::differing_only`], and [`Self::diff_nontext`] transition to the [`WorkspaceMode`] state, while [`Self::stream_spec_mode`] transitions to the [`StreamSpecMode`] state."
203)]
204#[derive(Debug, Clone, Default)]
205pub struct Diff<M = Unselected> {
206    bin: PathBuf,
207
208    global_opts: GlobalOpts,
209
210    diff_opts: Option<DiffOptions>,
211
212    mode: M,
213}
214
215impl Diff<Unselected> {
216    /// Creates a new `p4 diff` command.
217    ///
218    /// `bin` is the path to the Perforce command-line executable.
219    pub fn new(bin: impl Into<PathBuf>, global_opts: GlobalOpts) -> Self {
220        Self {
221            bin: bin.into(),
222            global_opts,
223            diff_opts: None,
224            mode: Unselected,
225        }
226    }
227
228    /// # Description
229    ///
230    /// -f
231    ///
232    /// Force the diff (if no revision is specified, against the head
233    /// revision), even when the client file is not open for `edit`.
234    ///
235    /// Transitions this command to the [`WorkspaceMode`] state with `-f` set
236    /// according to `v`.
237    pub fn force(self, v: bool) -> Diff<WorkspaceMode<Unselected>> {
238        Diff {
239            bin: self.bin,
240            global_opts: self.global_opts,
241            diff_opts: self.diff_opts,
242            mode: WorkspaceMode {
243                force: v,
244                differing_only: false,
245                diff_nontext: false,
246                mode: Unselected,
247            },
248        }
249    }
250
251    /// # Description
252    ///
253    /// -Od
254    ///
255    /// Limit output to only those files that differ.
256    ///
257    /// Transitions this command to the [`WorkspaceMode`] state with `-Od`
258    /// set according to `v`.
259    pub fn differing_only(self, v: bool) -> Diff<WorkspaceMode<Unselected>> {
260        Diff {
261            bin: self.bin,
262            global_opts: self.global_opts,
263            diff_opts: self.diff_opts,
264            mode: WorkspaceMode {
265                force: false,
266                differing_only: v,
267                diff_nontext: false,
268                mode: Unselected,
269            },
270        }
271    }
272
273    /// # Description
274    ///
275    /// -t
276    ///
277    /// Diff the revisions even if the files are not of type `text`.
278    ///
279    /// Transitions this command to the [`WorkspaceMode`] state with `-t` set
280    /// according to `v`.
281    pub fn diff_nontext(self, v: bool) -> Diff<WorkspaceMode<Unselected>> {
282        Diff {
283            bin: self.bin,
284            global_opts: self.global_opts,
285            diff_opts: self.diff_opts,
286            mode: WorkspaceMode {
287                force: false,
288                differing_only: false,
289                diff_nontext: v,
290                mode: Unselected,
291            },
292        }
293    }
294
295    /// # Description
296    ///
297    /// -As
298    ///
299    /// Allows two arbitrary stream specs to be diffed against each other.
300    /// Can be used with a streamname, or with a streamname at a specific
301    /// changelist number.
302    ///
303    #[cfg_attr(
304        feature = "lt2024_2",
305        doc = "Limitation: Although this option requires the user have at least the `list` access to the stream path, it ignores any other entry in the protections table, including any minus sign (`-`) that would otherwise block the operation."
306    )]
307    #[cfg_attr(
308        not(feature = "lt2024_2"),
309        doc = "Although this option requires the user have at least the `list` access to the stream path, it ignores any other entry in the protections table, including any minus sign (`-`) that would otherwise block the operation."
310    )]
311    ///
312    /// Transitions this command to the [`StreamSpecMode`] state. The stream
313    /// spec to compare against is passed as the argument of the spawned
314    /// command ([`ParameterizedSpawn::spawn_with`]); without one, the opened
315    /// stream spec is diffed against its have version.
316    #[cfg(not(feature = "lt2019_1"))]
317    pub fn stream_spec_mode(self) -> Diff<StreamSpecMode> {
318        Diff {
319            bin: self.bin,
320            global_opts: self.global_opts,
321            diff_opts: self.diff_opts,
322            mode: StreamSpecMode,
323        }
324    }
325}
326
327impl<M> Diff<M> {
328    /// # Description
329    ///
330    /// -doptions
331    ///
332    #[cfg_attr(
333        feature = "lt2014_2",
334        doc = "Pass flags to the underlying diff routine (see the Usage Notes below for details)"
335    )]
336    #[cfg_attr(
337        all(feature = "lt2015_1", not(feature = "lt2014_2")),
338        doc = "Pass options to the underlying diff routine (see the Usage Notes below for details)."
339    )]
340    #[cfg_attr(
341        all(feature = "lt2024_1", not(feature = "lt2015_1")),
342        doc = "Pass options to the underlying diff routine (see Usage Notes for details)."
343    )]
344    #[cfg_attr(
345        not(feature = "lt2024_1"),
346        doc = "Pass options to the underlying diff routine (see Usage notes for details)."
347    )]
348    pub fn get_diff_options(&self) -> Option<&DiffOptions> {
349        self.diff_opts.as_ref()
350    }
351
352    /// # Description
353    ///
354    /// -doptions
355    ///
356    #[cfg_attr(
357        feature = "lt2014_2",
358        doc = "Pass flags to the underlying diff routine (see the Usage Notes below for details)"
359    )]
360    #[cfg_attr(
361        all(feature = "lt2015_1", not(feature = "lt2014_2")),
362        doc = "Pass options to the underlying diff routine (see the Usage Notes below for details)."
363    )]
364    #[cfg_attr(
365        all(feature = "lt2024_1", not(feature = "lt2015_1")),
366        doc = "Pass options to the underlying diff routine (see Usage Notes for details)."
367    )]
368    #[cfg_attr(
369        not(feature = "lt2024_1"),
370        doc = "Pass options to the underlying diff routine (see Usage notes for details)."
371    )]
372    pub fn set_diff_options(&mut self, v: impl Into<DiffOptions>) -> &mut Self {
373        self.diff_opts = Some(v.into());
374        self
375    }
376
377    /// # Description
378    ///
379    /// -doptions
380    ///
381    #[cfg_attr(
382        feature = "lt2014_2",
383        doc = "Pass flags to the underlying diff routine (see the Usage Notes below for details)"
384    )]
385    #[cfg_attr(
386        all(feature = "lt2015_1", not(feature = "lt2014_2")),
387        doc = "Pass options to the underlying diff routine (see the Usage Notes below for details)."
388    )]
389    #[cfg_attr(
390        all(feature = "lt2024_1", not(feature = "lt2015_1")),
391        doc = "Pass options to the underlying diff routine (see Usage Notes for details)."
392    )]
393    #[cfg_attr(
394        not(feature = "lt2024_1"),
395        doc = "Pass options to the underlying diff routine (see Usage notes for details)."
396    )]
397    pub fn diff_options(mut self, v: impl Into<DiffOptions>) -> Self {
398        self.diff_opts = Some(v.into());
399        self
400    }
401}
402
403impl<M: ExclusiveOption> Diff<M> {
404    /// # Description
405    ///
406    /// g-opts
407    ///
408    #[cfg_attr(
409        feature = "lt2014_2",
410        doc = "See the [Global Options](GlobalOpts) section."
411    )]
412    #[cfg_attr(
413        all(feature = "lt2015_1", not(feature = "lt2014_2")),
414        doc = "See the [“Global Options”](GlobalOpts) section."
415    )]
416    #[cfg_attr(
417        all(feature = "lt2017_1", not(feature = "lt2015_1")),
418        doc = "See [“Global Options”](GlobalOpts)."
419    )]
420    #[cfg_attr(
421        all(feature = "lt2018_2", not(feature = "lt2017_1")),
422        doc = "See [Global Options](GlobalOpts)."
423    )]
424    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
425    pub fn get_global_opts(&self) -> &GlobalOpts {
426        &self.global_opts
427    }
428
429    /// # Description
430    ///
431    /// g-opts
432    ///
433    #[cfg_attr(
434        feature = "lt2014_2",
435        doc = "See the [Global Options](GlobalOpts) section."
436    )]
437    #[cfg_attr(
438        all(feature = "lt2015_1", not(feature = "lt2014_2")),
439        doc = "See the [“Global Options”](GlobalOpts) section."
440    )]
441    #[cfg_attr(
442        all(feature = "lt2017_1", not(feature = "lt2015_1")),
443        doc = "See [“Global Options”](GlobalOpts)."
444    )]
445    #[cfg_attr(
446        all(feature = "lt2018_2", not(feature = "lt2017_1")),
447        doc = "See [Global Options](GlobalOpts)."
448    )]
449    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
450    pub fn set_global_opts(&mut self, v: GlobalOpts) -> &mut Self {
451        self.global_opts = v;
452        self
453    }
454
455    /// # Description
456    ///
457    /// g-opts
458    ///
459    #[cfg_attr(
460        feature = "lt2014_2",
461        doc = "See the [Global Options](GlobalOpts) section."
462    )]
463    #[cfg_attr(
464        all(feature = "lt2015_1", not(feature = "lt2014_2")),
465        doc = "See the [“Global Options”](GlobalOpts) section."
466    )]
467    #[cfg_attr(
468        all(feature = "lt2017_1", not(feature = "lt2015_1")),
469        doc = "See [“Global Options”](GlobalOpts)."
470    )]
471    #[cfg_attr(
472        all(feature = "lt2018_2", not(feature = "lt2017_1")),
473        doc = "See [Global Options](GlobalOpts)."
474    )]
475    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
476    pub fn global_opts(mut self, v: GlobalOpts) -> Self {
477        self.global_opts = v;
478        self
479    }
480}
481
482impl<M> Diff<WorkspaceMode<M>> {
483    /// # Description
484    ///
485    /// -f
486    ///
487    /// Force the diff (if no revision is specified, against the head
488    /// revision), even when the client file is not open for `edit`.
489    pub fn get_force(&self) -> bool {
490        self.mode.force
491    }
492
493    /// # Description
494    ///
495    /// -f
496    ///
497    /// Force the diff (if no revision is specified, against the head
498    /// revision), even when the client file is not open for `edit`.
499    pub fn set_force(&mut self, v: bool) -> &mut Self {
500        self.mode.force = v;
501        self
502    }
503
504    /// # Description
505    ///
506    /// -f
507    ///
508    /// Force the diff (if no revision is specified, against the head
509    /// revision), even when the client file is not open for `edit`.
510    pub fn force(mut self, v: bool) -> Self {
511        self.mode.force = v;
512        self
513    }
514
515    /// # Description
516    ///
517    /// -Od
518    ///
519    /// Limit output to only those files that differ.
520    pub fn get_differing_only(&self) -> bool {
521        self.mode.differing_only
522    }
523
524    /// # Description
525    ///
526    /// -Od
527    ///
528    /// Limit output to only those files that differ.
529    pub fn set_differing_only(&mut self, v: bool) -> &mut Self {
530        self.mode.differing_only = v;
531        self
532    }
533
534    /// # Description
535    ///
536    /// -Od
537    ///
538    /// Limit output to only those files that differ.
539    pub fn differing_only(mut self, v: bool) -> Self {
540        self.mode.differing_only = v;
541        self
542    }
543
544    /// # Description
545    ///
546    /// -t
547    ///
548    /// Diff the revisions even if the files are not of type `text`.
549    pub fn get_diff_nontext(&self) -> bool {
550        self.mode.diff_nontext
551    }
552
553    /// # Description
554    ///
555    /// -t
556    ///
557    /// Diff the revisions even if the files are not of type `text`.
558    pub fn set_diff_nontext(&mut self, v: bool) -> &mut Self {
559        self.mode.diff_nontext = v;
560        self
561    }
562
563    /// # Description
564    ///
565    /// -t
566    ///
567    /// Diff the revisions even if the files are not of type `text`.
568    pub fn diff_nontext(mut self, v: bool) -> Self {
569        self.mode.diff_nontext = v;
570        self
571    }
572}
573
574impl Diff<WorkspaceMode<Unselected>> {
575    /// # Description
576    ///
577    #[cfg_attr(feature = "lt2015_1", doc = "-sa, -sb, -sd, -se, -sr, -sl file ...")]
578    #[cfg_attr(not(feature = "lt2015_1"), doc = "-soptions")]
579    ///
580    #[cfg_attr(
581        feature = "lt2015_1",
582        doc = "Pass one of the display options; see the individual option descriptions for details."
583    )]
584    #[cfg_attr(
585        all(feature = "lt2024_1", not(feature = "lt2015_1")),
586        doc = "Pass display options to the underlying diff routine (see Usage Notes for details)."
587    )]
588    #[cfg_attr(
589        not(feature = "lt2024_1"),
590        doc = "Pass display options to the underlying diff routine (see Usage notes for details)."
591    )]
592    ///
593    /// Transitions this command to the [`WorkspaceDisplayMode`] state with
594    /// the display `options` set; `-m max` is unavailable in this state.
595    pub fn display_options(self, v: DisplayOptions) -> Diff<WorkspaceMode<WorkspaceDisplayMode>> {
596        Diff {
597            bin: self.bin,
598            global_opts: self.global_opts,
599            diff_opts: self.diff_opts,
600            mode: WorkspaceMode {
601                force: self.mode.force,
602                differing_only: self.mode.differing_only,
603                diff_nontext: self.mode.diff_nontext,
604                mode: WorkspaceDisplayMode { display_opts: v },
605            },
606        }
607    }
608
609    /// # Description
610    ///
611    /// -m max
612    ///
613    #[cfg_attr(
614        feature = "lt2014_2",
615        doc = "Limit output to diffs (or status) of only the first `max` files, unless the `-s` flag is used, in which case the `-m` flag is ignored."
616    )]
617    #[cfg_attr(
618        not(feature = "lt2014_2"),
619        doc = "Limit output to diffs (or status) of only the first `max` files, unless the `-s` option is used, in which case the `-m` option is ignored."
620    )]
621    ///
622    /// Transitions this command to the [`WorkspaceRegularMode`] state with
623    /// the limit set to `max`; `-soptions` is unavailable in this state.
624    pub fn limit(self, max: u64) -> Diff<WorkspaceMode<WorkspaceRegularMode>> {
625        Diff {
626            bin: self.bin,
627            global_opts: self.global_opts,
628            diff_opts: self.diff_opts,
629            mode: WorkspaceMode {
630                force: self.mode.force,
631                differing_only: self.mode.differing_only,
632                diff_nontext: self.mode.diff_nontext,
633                mode: WorkspaceRegularMode { limit: max },
634            },
635        }
636    }
637}
638
639impl Diff<WorkspaceMode<WorkspaceDisplayMode>> {
640    /// # Description
641    ///
642    #[cfg_attr(feature = "lt2015_1", doc = "-sa, -sb, -sd, -se, -sr, -sl file ...")]
643    #[cfg_attr(not(feature = "lt2015_1"), doc = "-soptions")]
644    ///
645    #[cfg_attr(
646        feature = "lt2015_1",
647        doc = "Pass one of the display options; see the individual option descriptions for details."
648    )]
649    #[cfg_attr(
650        all(feature = "lt2024_1", not(feature = "lt2015_1")),
651        doc = "Pass display options to the underlying diff routine (see Usage Notes for details)."
652    )]
653    #[cfg_attr(
654        not(feature = "lt2024_1"),
655        doc = "Pass display options to the underlying diff routine (see Usage notes for details)."
656    )]
657    pub fn get_display_options(&self) -> &DisplayOptions {
658        &self.mode.mode.display_opts
659    }
660
661    /// # Description
662    ///
663    #[cfg_attr(feature = "lt2015_1", doc = "-sa, -sb, -sd, -se, -sr, -sl file ...")]
664    #[cfg_attr(not(feature = "lt2015_1"), doc = "-soptions")]
665    ///
666    #[cfg_attr(
667        feature = "lt2015_1",
668        doc = "Pass one of the display options; see the individual option descriptions for details."
669    )]
670    #[cfg_attr(
671        all(feature = "lt2024_1", not(feature = "lt2015_1")),
672        doc = "Pass display options to the underlying diff routine (see Usage Notes for details)."
673    )]
674    #[cfg_attr(
675        not(feature = "lt2024_1"),
676        doc = "Pass display options to the underlying diff routine (see Usage notes for details)."
677    )]
678    pub fn set_display_options(&mut self, v: DisplayOptions) -> &mut Self {
679        self.mode.mode.display_opts = v;
680        self
681    }
682}
683
684impl Diff<WorkspaceMode<WorkspaceRegularMode>> {
685    /// # Description
686    ///
687    /// -m max
688    ///
689    #[cfg_attr(
690        feature = "lt2014_2",
691        doc = "Limit output to diffs (or status) of only the first `max` files, unless the `-s` flag is used, in which case the `-m` flag is ignored."
692    )]
693    #[cfg_attr(
694        not(feature = "lt2014_2"),
695        doc = "Limit output to diffs (or status) of only the first `max` files, unless the `-s` option is used, in which case the `-m` option is ignored."
696    )]
697    pub fn get_limit(&self) -> u64 {
698        self.mode.mode.limit
699    }
700
701    /// # Description
702    ///
703    /// -m max
704    ///
705    #[cfg_attr(
706        feature = "lt2014_2",
707        doc = "Limit output to diffs (or status) of only the first `max` files, unless the `-s` flag is used, in which case the `-m` flag is ignored."
708    )]
709    #[cfg_attr(
710        not(feature = "lt2014_2"),
711        doc = "Limit output to diffs (or status) of only the first `max` files, unless the `-s` option is used, in which case the `-m` option is ignored."
712    )]
713    pub fn set_limit(&mut self, v: u64) -> &mut Self {
714        self.mode.mode.limit = v;
715        self
716    }
717}
718
719impl<M: ExclusiveOption> SubCommand for Diff<M> {
720    fn name(&self) -> &str {
721        "diff"
722    }
723
724    fn inject_local_args(&self, command: &mut Command) {
725        if let Some(diff_opts) = &self.diff_opts {
726            diff_opts.inject_arg(command);
727        }
728
729        self.mode.inject_args(command);
730    }
731
732    fn global_opts(&self) -> Option<&GlobalOpts> {
733        Some(&self.global_opts)
734    }
735}
736
737impl<S, I> ParameterizedSpawn<(S,)> for Diff<Unselected>
738where
739    S: IntoIterator<Item = I>,
740    I: AsRef<OsStr>,
741{
742    type Output = Child;
743    type Error = std::io::Error;
744
745    /// Spawns `p4 diff` for the given file arguments as a child process with
746    /// piped standard output and error streams; use the returned [`Child`]
747    /// handle to wait for it or interact with it.
748    fn spawn_with(&mut self, (files,): (S,)) -> Result<Self::Output, Self::Error> {
749        self.setup_command(&self.bin)
750            .args(files)
751            .stdout(Stdio::piped())
752            .stderr(Stdio::piped())
753            .spawn()
754    }
755}
756
757impl<M: ExclusiveOption, S, I> ParameterizedSpawn<(S,)> for Diff<WorkspaceMode<M>>
758where
759    S: IntoIterator<Item = I>,
760    I: AsRef<OsStr>,
761{
762    type Output = Child;
763    type Error = std::io::Error;
764
765    /// Spawns `p4 diff` for the given file arguments as a child process with
766    /// piped standard output and error streams; use the returned [`Child`]
767    /// handle to wait for it or interact with it.
768    fn spawn_with(&mut self, (files,): (S,)) -> Result<Self::Output, Self::Error> {
769        self.setup_command(&self.bin)
770            .args(files)
771            .stdout(Stdio::piped())
772            .stderr(Stdio::piped())
773            .spawn()
774    }
775}
776
777#[cfg(not(feature = "lt2019_1"))]
778impl<I> ParameterizedSpawn<(I,)> for Diff<StreamSpecMode>
779where
780    I: AsRef<OsStr>,
781{
782    type Output = Child;
783    type Error = std::io::Error;
784
785    /// Spawns `p4 diff -As` as a child process with piped standard output
786    /// and error streams; use the returned [`Child`] handle to wait for it
787    /// or interact with it.
788    ///
789    /// The stream spec is a streamname, optionally at a specific changelist
790    /// number: `@head` selects the head version, `@change` the version at a
791    /// specific change, and `@=change` the shelved version at a specific
792    /// change.
793    ///
794    /// Use [`spawn()`](SpawnExt::spawn) (no arguments) to diff the opened
795    /// stream spec against its have version.
796    fn spawn_with(&mut self, (stream_spec,): (I,)) -> Result<Self::Output, Self::Error> {
797        self.setup_command(&self.bin)
798            .arg(stream_spec)
799            .stdout(Stdio::piped())
800            .stderr(Stdio::piped())
801            .spawn()
802    }
803}
804
805#[cfg(not(feature = "lt2019_1"))]
806impl ParameterizedSpawn<()> for Diff<StreamSpecMode> {
807    type Output = Child;
808    type Error = std::io::Error;
809
810    /// Spawns `p4 diff -As` without a stream spec as a child process with
811    /// piped standard output and error streams; the opened stream spec is
812    /// diffed against its have version. Use the returned [`Child`] handle
813    /// to wait for it or interact with it.
814    fn spawn_with(&mut self, _: ()) -> Result<Self::Output, Self::Error> {
815        self.setup_command(&self.bin)
816            .stdout(Stdio::piped())
817            .stderr(Stdio::piped())
818            .spawn()
819    }
820}
821
822#[cfg(test)]
823mod tests {
824    use super::*;
825    use crate::cmd::DiffOptionsBuilder;
826    use crate::cmd::args_of;
827
828    /// Dry-run checks of the assembled `p4 diff` command line; no process is
829    /// spawned.
830    #[test]
831    fn without_options() {
832        let diff = Diff::new("p4", GlobalOpts::new());
833
834        assert_eq!(args_of(&diff.setup_command("p4")), ["diff"]);
835    }
836
837    #[test]
838    fn workspace_mode_via_force() {
839        let diff = Diff::new("p4", GlobalOpts::new()).force(true);
840
841        assert!(diff.get_force());
842        assert_eq!(args_of(&diff.setup_command("p4")), ["diff", "-f"]);
843    }
844
845    #[test]
846    fn workspace_mode_via_differing_only() {
847        let diff = Diff::new("p4", GlobalOpts::new()).differing_only(true);
848
849        assert_eq!(args_of(&diff.setup_command("p4")), ["diff", "-Od"]);
850    }
851
852    #[test]
853    fn workspace_mode_via_diff_nontext() {
854        let diff = Diff::new("p4", GlobalOpts::new()).diff_nontext(true);
855
856        assert_eq!(args_of(&diff.setup_command("p4")), ["diff", "-t"]);
857    }
858
859    #[test]
860    fn workspace_mode_combines_options() {
861        let mut diff = Diff::new("p4", GlobalOpts::new()).diff_nontext(true);
862        diff.set_force(true).set_differing_only(true);
863
864        assert_eq!(
865            args_of(&diff.setup_command("p4")),
866            ["diff", "-f", "-t", "-Od"]
867        );
868    }
869
870    #[test]
871    fn workspace_mode_preserves_diff_options() {
872        let mut diff = Diff::new("p4", GlobalOpts::new())
873            .diff_options(DiffOptionsBuilder::unified(None))
874            .force(true);
875        diff.set_diff_options(DiffOptionsBuilder::summary());
876
877        assert_eq!(args_of(&diff.setup_command("p4")), ["diff", "-ds", "-f"]);
878    }
879
880    #[test]
881    fn display_mode_injects_filter() {
882        let diff = Diff::new("p4", GlobalOpts::new())
883            .force(true)
884            .display_options(DisplayOptions::UnopenedChanged);
885
886        assert_eq!(diff.get_display_options(), &DisplayOptions::UnopenedChanged);
887        assert_eq!(args_of(&diff.setup_command("p4")), ["diff", "-f", "-se"]);
888    }
889
890    #[test]
891    fn display_mode_flag_mapping() {
892        assert_eq!(DisplayOptions::OpenedChangedOrMissing.as_str(), "-sa");
893        assert_eq!(DisplayOptions::ResolvedThenModified.as_str(), "-sb");
894        assert_eq!(DisplayOptions::UnopenedMissing.as_str(), "-sd");
895        assert_eq!(DisplayOptions::UnopenedChanged.as_str(), "-se");
896        assert_eq!(DisplayOptions::ListWithStatus.as_str(), "-sl");
897        assert_eq!(DisplayOptions::OpenedIdentical.as_str(), "-sr");
898    }
899
900    #[test]
901    fn regular_mode_injects_limit() {
902        let diff = Diff::new("p4", GlobalOpts::new()).force(false).limit(10);
903
904        assert_eq!(diff.get_limit(), 10);
905        assert_eq!(args_of(&diff.setup_command("p4")), ["diff", "-m", "10"]);
906    }
907
908    #[cfg(not(feature = "lt2019_1"))]
909    #[test]
910    fn stream_spec_mode_bare() {
911        let diff = Diff::new("p4", GlobalOpts::new()).stream_spec_mode();
912
913        assert_eq!(args_of(&diff.setup_command("p4")), ["diff", "-As"]);
914    }
915
916    #[cfg(not(feature = "lt2019_1"))]
917    #[test]
918    fn stream_spec_mode_with_spec() {
919        let diff = Diff::new("p4", GlobalOpts::new()).stream_spec_mode();
920
921        // Mirrors `spawn_with`, which appends the stream spec after the
922        // assembled command.
923        let mut command = diff.setup_command("p4");
924        command.arg("myStream@head");
925
926        assert_eq!(args_of(&command), ["diff", "-As", "myStream@head"]);
927    }
928
929    #[cfg(not(feature = "lt2019_1"))]
930    #[test]
931    fn stream_spec_mode_preserves_diff_options() {
932        let diff = Diff::new("p4", GlobalOpts::new())
933            .diff_options(DiffOptionsBuilder::unified(None))
934            .stream_spec_mode();
935
936        // Mirrors `spawn_with`, which appends the stream spec after the
937        // assembled command.
938        let mut command = diff.setup_command("p4");
939        command.arg("myStream");
940
941        assert_eq!(args_of(&command), ["diff", "-du", "-As", "myStream"]);
942    }
943}