Skip to main content

perforce_cli/cmd/
diff.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/// 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    /// Get the Perforce command path.
405    pub fn get_bin(&self) -> &Path {
406        &self.bin
407    }
408
409    /// Set the Perforce command path.
410    pub fn set_bin(&mut self, bin: impl Into<PathBuf>) -> &mut Self {
411        self.bin = bin.into();
412        self
413    }
414
415    /// Set the Perforce command path.
416    pub fn bin(mut self, bin: impl Into<PathBuf>) -> Self {
417        self.set_bin(bin);
418        self
419    }
420
421    /// # Description
422    ///
423    /// g-opts
424    ///
425    #[cfg_attr(
426        feature = "lt2014_2",
427        doc = "See the [Global Options](GlobalOpts) section."
428    )]
429    #[cfg_attr(
430        all(feature = "lt2015_1", not(feature = "lt2014_2")),
431        doc = "See the [“Global Options”](GlobalOpts) section."
432    )]
433    #[cfg_attr(
434        all(feature = "lt2017_1", not(feature = "lt2015_1")),
435        doc = "See [“Global Options”](GlobalOpts)."
436    )]
437    #[cfg_attr(
438        all(feature = "lt2018_2", not(feature = "lt2017_1")),
439        doc = "See [Global Options](GlobalOpts)."
440    )]
441    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
442    pub fn get_global_opts(&self) -> &GlobalOpts {
443        &self.global_opts
444    }
445
446    /// # Description
447    ///
448    /// g-opts
449    ///
450    #[cfg_attr(
451        feature = "lt2014_2",
452        doc = "See the [Global Options](GlobalOpts) section."
453    )]
454    #[cfg_attr(
455        all(feature = "lt2015_1", not(feature = "lt2014_2")),
456        doc = "See the [“Global Options”](GlobalOpts) section."
457    )]
458    #[cfg_attr(
459        all(feature = "lt2017_1", not(feature = "lt2015_1")),
460        doc = "See [“Global Options”](GlobalOpts)."
461    )]
462    #[cfg_attr(
463        all(feature = "lt2018_2", not(feature = "lt2017_1")),
464        doc = "See [Global Options](GlobalOpts)."
465    )]
466    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
467    pub fn set_global_opts(&mut self, v: GlobalOpts) -> &mut Self {
468        self.global_opts = v;
469        self
470    }
471
472    /// # Description
473    ///
474    /// g-opts
475    ///
476    #[cfg_attr(
477        feature = "lt2014_2",
478        doc = "See the [Global Options](GlobalOpts) section."
479    )]
480    #[cfg_attr(
481        all(feature = "lt2015_1", not(feature = "lt2014_2")),
482        doc = "See the [“Global Options”](GlobalOpts) section."
483    )]
484    #[cfg_attr(
485        all(feature = "lt2017_1", not(feature = "lt2015_1")),
486        doc = "See [“Global Options”](GlobalOpts)."
487    )]
488    #[cfg_attr(
489        all(feature = "lt2018_2", not(feature = "lt2017_1")),
490        doc = "See [Global Options](GlobalOpts)."
491    )]
492    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
493    pub fn global_opts(mut self, v: GlobalOpts) -> Self {
494        self.global_opts = v;
495        self
496    }
497}
498
499impl<M> Diff<WorkspaceMode<M>> {
500    /// # Description
501    ///
502    /// -f
503    ///
504    /// Force the diff (if no revision is specified, against the head
505    /// revision), even when the client file is not open for `edit`.
506    pub fn get_force(&self) -> bool {
507        self.mode.force
508    }
509
510    /// # Description
511    ///
512    /// -f
513    ///
514    /// Force the diff (if no revision is specified, against the head
515    /// revision), even when the client file is not open for `edit`.
516    pub fn set_force(&mut self, v: bool) -> &mut Self {
517        self.mode.force = v;
518        self
519    }
520
521    /// # Description
522    ///
523    /// -f
524    ///
525    /// Force the diff (if no revision is specified, against the head
526    /// revision), even when the client file is not open for `edit`.
527    pub fn force(mut self, v: bool) -> Self {
528        self.mode.force = v;
529        self
530    }
531
532    /// # Description
533    ///
534    /// -Od
535    ///
536    /// Limit output to only those files that differ.
537    pub fn get_differing_only(&self) -> bool {
538        self.mode.differing_only
539    }
540
541    /// # Description
542    ///
543    /// -Od
544    ///
545    /// Limit output to only those files that differ.
546    pub fn set_differing_only(&mut self, v: bool) -> &mut Self {
547        self.mode.differing_only = v;
548        self
549    }
550
551    /// # Description
552    ///
553    /// -Od
554    ///
555    /// Limit output to only those files that differ.
556    pub fn differing_only(mut self, v: bool) -> Self {
557        self.mode.differing_only = v;
558        self
559    }
560
561    /// # Description
562    ///
563    /// -t
564    ///
565    /// Diff the revisions even if the files are not of type `text`.
566    pub fn get_diff_nontext(&self) -> bool {
567        self.mode.diff_nontext
568    }
569
570    /// # Description
571    ///
572    /// -t
573    ///
574    /// Diff the revisions even if the files are not of type `text`.
575    pub fn set_diff_nontext(&mut self, v: bool) -> &mut Self {
576        self.mode.diff_nontext = v;
577        self
578    }
579
580    /// # Description
581    ///
582    /// -t
583    ///
584    /// Diff the revisions even if the files are not of type `text`.
585    pub fn diff_nontext(mut self, v: bool) -> Self {
586        self.mode.diff_nontext = v;
587        self
588    }
589}
590
591impl Diff<WorkspaceMode<Unselected>> {
592    /// # Description
593    ///
594    #[cfg_attr(feature = "lt2015_1", doc = "-sa, -sb, -sd, -se, -sr, -sl file ...")]
595    #[cfg_attr(not(feature = "lt2015_1"), doc = "-soptions")]
596    ///
597    #[cfg_attr(
598        feature = "lt2015_1",
599        doc = "Pass one of the display options; see the individual option descriptions for details."
600    )]
601    #[cfg_attr(
602        all(feature = "lt2024_1", not(feature = "lt2015_1")),
603        doc = "Pass display options to the underlying diff routine (see Usage Notes for details)."
604    )]
605    #[cfg_attr(
606        not(feature = "lt2024_1"),
607        doc = "Pass display options to the underlying diff routine (see Usage notes for details)."
608    )]
609    ///
610    /// Transitions this command to the [`WorkspaceDisplayMode`] state with
611    /// the display `options` set; `-m max` is unavailable in this state.
612    pub fn display_options(self, v: DisplayOptions) -> Diff<WorkspaceMode<WorkspaceDisplayMode>> {
613        Diff {
614            bin: self.bin,
615            global_opts: self.global_opts,
616            diff_opts: self.diff_opts,
617            mode: WorkspaceMode {
618                force: self.mode.force,
619                differing_only: self.mode.differing_only,
620                diff_nontext: self.mode.diff_nontext,
621                mode: WorkspaceDisplayMode { display_opts: v },
622            },
623        }
624    }
625
626    /// # Description
627    ///
628    /// -m max
629    ///
630    #[cfg_attr(
631        feature = "lt2014_2",
632        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."
633    )]
634    #[cfg_attr(
635        not(feature = "lt2014_2"),
636        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."
637    )]
638    ///
639    /// Transitions this command to the [`WorkspaceRegularMode`] state with
640    /// the limit set to `max`; `-soptions` is unavailable in this state.
641    pub fn limit(self, max: u64) -> Diff<WorkspaceMode<WorkspaceRegularMode>> {
642        Diff {
643            bin: self.bin,
644            global_opts: self.global_opts,
645            diff_opts: self.diff_opts,
646            mode: WorkspaceMode {
647                force: self.mode.force,
648                differing_only: self.mode.differing_only,
649                diff_nontext: self.mode.diff_nontext,
650                mode: WorkspaceRegularMode { limit: max },
651            },
652        }
653    }
654}
655
656impl Diff<WorkspaceMode<WorkspaceDisplayMode>> {
657    /// # Description
658    ///
659    #[cfg_attr(feature = "lt2015_1", doc = "-sa, -sb, -sd, -se, -sr, -sl file ...")]
660    #[cfg_attr(not(feature = "lt2015_1"), doc = "-soptions")]
661    ///
662    #[cfg_attr(
663        feature = "lt2015_1",
664        doc = "Pass one of the display options; see the individual option descriptions for details."
665    )]
666    #[cfg_attr(
667        all(feature = "lt2024_1", not(feature = "lt2015_1")),
668        doc = "Pass display options to the underlying diff routine (see Usage Notes for details)."
669    )]
670    #[cfg_attr(
671        not(feature = "lt2024_1"),
672        doc = "Pass display options to the underlying diff routine (see Usage notes for details)."
673    )]
674    pub fn get_display_options(&self) -> &DisplayOptions {
675        &self.mode.mode.display_opts
676    }
677
678    /// # Description
679    ///
680    #[cfg_attr(feature = "lt2015_1", doc = "-sa, -sb, -sd, -se, -sr, -sl file ...")]
681    #[cfg_attr(not(feature = "lt2015_1"), doc = "-soptions")]
682    ///
683    #[cfg_attr(
684        feature = "lt2015_1",
685        doc = "Pass one of the display options; see the individual option descriptions for details."
686    )]
687    #[cfg_attr(
688        all(feature = "lt2024_1", not(feature = "lt2015_1")),
689        doc = "Pass display options to the underlying diff routine (see Usage Notes for details)."
690    )]
691    #[cfg_attr(
692        not(feature = "lt2024_1"),
693        doc = "Pass display options to the underlying diff routine (see Usage notes for details)."
694    )]
695    pub fn set_display_options(&mut self, v: DisplayOptions) -> &mut Self {
696        self.mode.mode.display_opts = v;
697        self
698    }
699}
700
701impl Diff<WorkspaceMode<WorkspaceRegularMode>> {
702    /// # Description
703    ///
704    /// -m max
705    ///
706    #[cfg_attr(
707        feature = "lt2014_2",
708        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."
709    )]
710    #[cfg_attr(
711        not(feature = "lt2014_2"),
712        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."
713    )]
714    pub fn get_limit(&self) -> u64 {
715        self.mode.mode.limit
716    }
717
718    /// # Description
719    ///
720    /// -m max
721    ///
722    #[cfg_attr(
723        feature = "lt2014_2",
724        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."
725    )]
726    #[cfg_attr(
727        not(feature = "lt2014_2"),
728        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."
729    )]
730    pub fn set_limit(&mut self, v: u64) -> &mut Self {
731        self.mode.mode.limit = v;
732        self
733    }
734}
735
736impl<M: ExclusiveOption> SubCommand for Diff<M> {
737    fn name(&self) -> &str {
738        "diff"
739    }
740
741    fn inject_local_args(&self, command: &mut Command) {
742        if let Some(diff_opts) = &self.diff_opts {
743            diff_opts.inject_arg(command);
744        }
745
746        self.mode.inject_args(command);
747    }
748
749    fn global_opts(&self) -> Option<&GlobalOpts> {
750        Some(&self.global_opts)
751    }
752}
753
754impl<S, I> ParameterizedSpawn<(S,)> for Diff<Unselected>
755where
756    S: IntoIterator<Item = I>,
757    I: AsRef<OsStr>,
758{
759    type Output = Child;
760    type Error = std::io::Error;
761
762    /// Spawns `p4 diff` for the given file arguments as a child process with
763    /// piped standard output and error streams; use the returned [`Child`]
764    /// handle to wait for it or interact with it.
765    fn spawn_with(&mut self, (files,): (S,)) -> Result<Self::Output, Self::Error> {
766        self.setup_command(&self.bin)
767            .args(files)
768            .stdout(Stdio::piped())
769            .stderr(Stdio::piped())
770            .spawn()
771    }
772}
773
774impl<M: ExclusiveOption, S, I> ParameterizedSpawn<(S,)> for Diff<WorkspaceMode<M>>
775where
776    S: IntoIterator<Item = I>,
777    I: AsRef<OsStr>,
778{
779    type Output = Child;
780    type Error = std::io::Error;
781
782    /// Spawns `p4 diff` for the given file arguments as a child process with
783    /// piped standard output and error streams; use the returned [`Child`]
784    /// handle to wait for it or interact with it.
785    fn spawn_with(&mut self, (files,): (S,)) -> Result<Self::Output, Self::Error> {
786        self.setup_command(&self.bin)
787            .args(files)
788            .stdout(Stdio::piped())
789            .stderr(Stdio::piped())
790            .spawn()
791    }
792}
793
794#[cfg(not(feature = "lt2019_1"))]
795impl<I> ParameterizedSpawn<(I,)> for Diff<StreamSpecMode>
796where
797    I: AsRef<OsStr>,
798{
799    type Output = Child;
800    type Error = std::io::Error;
801
802    /// Spawns `p4 diff -As` as a child process with piped standard output
803    /// and error streams; use the returned [`Child`] handle to wait for it
804    /// or interact with it.
805    ///
806    /// The stream spec is a streamname, optionally at a specific changelist
807    /// number: `@head` selects the head version, `@change` the version at a
808    /// specific change, and `@=change` the shelved version at a specific
809    /// change.
810    ///
811    /// Use [`spawn()`](crate::spawn::SpawnExt::spawn) (no arguments) to diff the opened
812    /// stream spec against its have version.
813    fn spawn_with(&mut self, (stream_spec,): (I,)) -> Result<Self::Output, Self::Error> {
814        self.setup_command(&self.bin)
815            .arg(stream_spec)
816            .stdout(Stdio::piped())
817            .stderr(Stdio::piped())
818            .spawn()
819    }
820}
821
822#[cfg(not(feature = "lt2019_1"))]
823impl ParameterizedSpawn<()> for Diff<StreamSpecMode> {
824    type Output = Child;
825    type Error = std::io::Error;
826
827    /// Spawns `p4 diff -As` without a stream spec as a child process with
828    /// piped standard output and error streams; the opened stream spec is
829    /// diffed against its have version. Use the returned [`Child`] handle
830    /// to wait for it or interact with it.
831    fn spawn_with(&mut self, _: ()) -> Result<Self::Output, Self::Error> {
832        self.setup_command(&self.bin)
833            .stdout(Stdio::piped())
834            .stderr(Stdio::piped())
835            .spawn()
836    }
837}
838
839#[cfg(test)]
840mod tests {
841    use super::*;
842    use crate::cmd::DiffOptionsBuilder;
843    use crate::cmd::args_of;
844
845    #[test]
846    fn bin_accessors_update_executable_path() {
847        let mut command = Diff::new("p4", GlobalOpts::new());
848        assert_eq!(command.get_bin(), Path::new("p4"));
849
850        command.set_bin("/usr/local/bin/p4");
851        assert_eq!(command.get_bin(), Path::new("/usr/local/bin/p4"));
852
853        let command = command.bin("p4.exe");
854        assert_eq!(command.get_bin(), Path::new("p4.exe"));
855    }
856
857    /// Dry-run checks of the assembled `p4 diff` command line; no process is
858    /// spawned.
859    #[test]
860    fn without_options() {
861        let diff = Diff::new("p4", GlobalOpts::new());
862
863        assert_eq!(args_of(&diff.setup_command("p4")), ["diff"]);
864    }
865
866    #[test]
867    fn workspace_mode_via_force() {
868        let diff = Diff::new("p4", GlobalOpts::new()).force(true);
869
870        assert!(diff.get_force());
871        assert_eq!(args_of(&diff.setup_command("p4")), ["diff", "-f"]);
872    }
873
874    #[test]
875    fn workspace_mode_via_differing_only() {
876        let diff = Diff::new("p4", GlobalOpts::new()).differing_only(true);
877
878        assert_eq!(args_of(&diff.setup_command("p4")), ["diff", "-Od"]);
879    }
880
881    #[test]
882    fn workspace_mode_via_diff_nontext() {
883        let diff = Diff::new("p4", GlobalOpts::new()).diff_nontext(true);
884
885        assert_eq!(args_of(&diff.setup_command("p4")), ["diff", "-t"]);
886    }
887
888    #[test]
889    fn workspace_mode_combines_options() {
890        let mut diff = Diff::new("p4", GlobalOpts::new()).diff_nontext(true);
891        diff.set_force(true).set_differing_only(true);
892
893        assert_eq!(
894            args_of(&diff.setup_command("p4")),
895            ["diff", "-f", "-t", "-Od"]
896        );
897    }
898
899    #[test]
900    fn workspace_mode_preserves_diff_options() {
901        let mut diff = Diff::new("p4", GlobalOpts::new())
902            .diff_options(DiffOptionsBuilder::unified(None))
903            .force(true);
904        diff.set_diff_options(DiffOptionsBuilder::summary());
905
906        assert_eq!(args_of(&diff.setup_command("p4")), ["diff", "-ds", "-f"]);
907    }
908
909    #[test]
910    fn display_mode_injects_filter() {
911        let diff = Diff::new("p4", GlobalOpts::new())
912            .force(true)
913            .display_options(DisplayOptions::UnopenedChanged);
914
915        assert_eq!(diff.get_display_options(), &DisplayOptions::UnopenedChanged);
916        assert_eq!(args_of(&diff.setup_command("p4")), ["diff", "-f", "-se"]);
917    }
918
919    #[test]
920    fn display_mode_flag_mapping() {
921        assert_eq!(DisplayOptions::OpenedChangedOrMissing.as_str(), "-sa");
922        assert_eq!(DisplayOptions::ResolvedThenModified.as_str(), "-sb");
923        assert_eq!(DisplayOptions::UnopenedMissing.as_str(), "-sd");
924        assert_eq!(DisplayOptions::UnopenedChanged.as_str(), "-se");
925        assert_eq!(DisplayOptions::ListWithStatus.as_str(), "-sl");
926        assert_eq!(DisplayOptions::OpenedIdentical.as_str(), "-sr");
927    }
928
929    #[test]
930    fn regular_mode_injects_limit() {
931        let diff = Diff::new("p4", GlobalOpts::new()).force(false).limit(10);
932
933        assert_eq!(diff.get_limit(), 10);
934        assert_eq!(args_of(&diff.setup_command("p4")), ["diff", "-m", "10"]);
935    }
936
937    #[cfg(not(feature = "lt2019_1"))]
938    #[test]
939    fn stream_spec_mode_bare() {
940        let diff = Diff::new("p4", GlobalOpts::new()).stream_spec_mode();
941
942        assert_eq!(args_of(&diff.setup_command("p4")), ["diff", "-As"]);
943    }
944
945    #[cfg(not(feature = "lt2019_1"))]
946    #[test]
947    fn stream_spec_mode_with_spec() {
948        let diff = Diff::new("p4", GlobalOpts::new()).stream_spec_mode();
949
950        // Mirrors `spawn_with`, which appends the stream spec after the
951        // assembled command.
952        let mut command = diff.setup_command("p4");
953        command.arg("myStream@head");
954
955        assert_eq!(args_of(&command), ["diff", "-As", "myStream@head"]);
956    }
957
958    #[cfg(not(feature = "lt2019_1"))]
959    #[test]
960    fn stream_spec_mode_preserves_diff_options() {
961        let diff = Diff::new("p4", GlobalOpts::new())
962            .diff_options(DiffOptionsBuilder::unified(None))
963            .stream_spec_mode();
964
965        // Mirrors `spawn_with`, which appends the stream spec after the
966        // assembled command.
967        let mut command = diff.setup_command("p4");
968        command.arg("myStream");
969
970        assert_eq!(args_of(&command), ["diff", "-du", "-As", "myStream"]);
971    }
972}