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, SpawnExt};
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    /// If you use the `-f` option together with the `-sl` option, files that
49    /// are open for edit are also compared and their status is listed.
50    ///
51    /// The compared files are passed as the file arguments of the spawned
52    /// command.
53    ListWithStatus,
54
55    /// `-sr`
56    ///
57    /// Show only the names of opened files in the client workspace that are
58    /// identical to the revision in the depot.
59    OpenedIdentical,
60}
61
62impl DisplayOptions {
63    /// Returns the command-line flag for this display option.
64    pub fn as_str(&self) -> &'static str {
65        match self {
66            DisplayOptions::OpenedChangedOrMissing => "-sa",
67            DisplayOptions::ResolvedThenModified => "-sb",
68            DisplayOptions::UnopenedMissing => "-sd",
69            DisplayOptions::UnopenedChanged => "-se",
70            DisplayOptions::ListWithStatus => "-sl",
71            DisplayOptions::OpenedIdentical => "-sr",
72        }
73    }
74}
75
76/// The `-soptions` state of [`WorkspaceMode`]: a display filter is selected,
77/// making `-m max` unavailable.
78///
79/// Entered with [`Diff::display_options`].
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub struct WorkspaceDisplayMode {
82    display_opts: DisplayOptions,
83}
84
85impl ExclusiveOption for WorkspaceDisplayMode {
86    fn inject_args(&self, command: &mut Command) {
87        command.arg(self.display_opts.as_str());
88    }
89}
90
91/// The `-m max` state of [`WorkspaceMode`]: a file limit is selected, making
92/// `-soptions` unavailable.
93///
94/// Entered with [`Diff::limit`].
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub struct WorkspaceRegularMode {
97    limit: u64,
98}
99
100impl ExclusiveOption for WorkspaceRegularMode {
101    fn inject_args(&self, command: &mut Command) {
102        command.arg("-m").arg(self.limit.to_string());
103    }
104}
105
106/// Workspace content mode of `p4 diff`, comparing files in the client
107/// workspace to revisions in the depot.
108///
109/// Entered with [`Diff::force`], [`Diff::differing_only`], or
110/// [`Diff::diff_nontext`].
111///
112/// The `M` type parameter isolates `-m max` and `-soptions` at compile time:
113/// [`Diff::limit`] transitions to the [`WorkspaceRegularMode`] state, while
114/// [`Diff::display_options`] transitions to the [`WorkspaceDisplayMode`]
115/// state.
116#[derive(Debug, Clone, Default)]
117pub struct WorkspaceMode<M = Unselected> {
118    force: bool,
119
120    differing_only: bool,
121
122    diff_nontext: bool,
123
124    mode: M,
125}
126
127impl<M: ExclusiveOption> ExclusiveOption for WorkspaceMode<M> {
128    fn inject_args(&self, command: &mut Command) {
129        if self.force {
130            command.arg("-f");
131        }
132
133        if self.diff_nontext {
134            command.arg("-t");
135        }
136
137        if self.differing_only {
138            command.arg("-Od");
139        }
140
141        self.mode.inject_args(command);
142    }
143}
144
145/// Stream spec mode of `p4 diff` (`-As`): diff a privately edited stream
146/// spec against another version of the same stream spec, or diff two
147/// arbitrary stream specs against each other.
148///
149/// Entered with [`Diff::stream_spec_mode`]; the stream spec to compare
150/// against is passed as the argument of the spawned command.
151#[derive(Debug, Clone, Default, PartialEq, Eq)]
152pub struct StreamSpecMode;
153
154impl ExclusiveOption for StreamSpecMode {
155    fn inject_args(&self, command: &mut Command) {
156        command.arg("-As");
157    }
158}
159
160/// `p4 [g-opts] diff [-doptions] [-f -t -Od] [-m max] [-soptions] [file[rev] ...]`
161///
162/// `p4 [g-opts] diff [-doptions] -As [streamname[@change]]`
163///
164/// Diff utility for comparing workspace content to depot content. (For
165/// comparing two depot paths, see `p4 diff2`.) Also for stream spec
166/// comparison.
167///
168/// The `M` type parameter tracks the command mode at compile time. The
169/// default [`Unselected`] state offers neither the workspace content options
170/// nor `-As`; [`Self::force`], [`Self::differing_only`], and
171/// [`Self::diff_nontext`] transition to the [`WorkspaceMode`] state, while
172/// [`Self::stream_spec_mode`] transitions to the [`StreamSpecMode`] state.
173#[derive(Debug, Clone, Default)]
174pub struct Diff<M = Unselected> {
175    bin: PathBuf,
176
177    global_opts: GlobalOpts,
178
179    diff_opts: Option<DiffOptions>,
180
181    mode: M,
182}
183
184impl Diff<Unselected> {
185    /// Creates a new `p4 diff` command.
186    ///
187    /// `bin` is the path to the Perforce command-line executable.
188    pub fn new(bin: impl Into<PathBuf>, global_opts: GlobalOpts) -> Self {
189        Self {
190            bin: bin.into(),
191            global_opts,
192            diff_opts: None,
193            mode: Unselected,
194        }
195    }
196
197    /// # Description
198    ///
199    /// -f
200    ///
201    /// Force the diff (if no revision is specified, against the head
202    /// revision), even when the client file is not open for `edit`.
203    ///
204    /// Transitions this command to the [`WorkspaceMode`] state with `-f` set
205    /// according to `v`.
206    pub fn force(self, v: bool) -> Diff<WorkspaceMode<Unselected>> {
207        Diff {
208            bin: self.bin,
209            global_opts: self.global_opts,
210            diff_opts: self.diff_opts,
211            mode: WorkspaceMode {
212                force: v,
213                differing_only: false,
214                diff_nontext: false,
215                mode: Unselected,
216            },
217        }
218    }
219
220    /// # Description
221    ///
222    /// -Od
223    ///
224    /// Limit output to only those files that differ.
225    ///
226    /// Transitions this command to the [`WorkspaceMode`] state with `-Od`
227    /// set according to `v`.
228    pub fn differing_only(self, v: bool) -> Diff<WorkspaceMode<Unselected>> {
229        Diff {
230            bin: self.bin,
231            global_opts: self.global_opts,
232            diff_opts: self.diff_opts,
233            mode: WorkspaceMode {
234                force: false,
235                differing_only: v,
236                diff_nontext: false,
237                mode: Unselected,
238            },
239        }
240    }
241
242    /// # Description
243    ///
244    /// -t
245    ///
246    /// Diff the revisions even if the files are not of type `text`.
247    ///
248    /// Transitions this command to the [`WorkspaceMode`] state with `-t` set
249    /// according to `v`.
250    pub fn diff_nontext(self, v: bool) -> Diff<WorkspaceMode<Unselected>> {
251        Diff {
252            bin: self.bin,
253            global_opts: self.global_opts,
254            diff_opts: self.diff_opts,
255            mode: WorkspaceMode {
256                force: false,
257                differing_only: false,
258                diff_nontext: v,
259                mode: Unselected,
260            },
261        }
262    }
263
264    /// # Description
265    ///
266    /// -As
267    ///
268    /// Allows two arbitrary stream specs to be diffed against each other.
269    /// Can be used with a streamname, or with a streamname at a specific
270    /// changelist number.
271    ///
272    /// Although this option requires the user have at least the list access
273    /// to the stream path, it ignores any other entry in the protections
274    /// table, including any minus sign (`-`) that would otherwise block the
275    /// operation.
276    ///
277    /// Transitions this command to the [`StreamSpecMode`] state. The stream
278    /// spec to compare against is passed as the argument of the spawned
279    /// command ([`ParameterizedSpawn::spawn_with`]); without one, the opened
280    /// stream spec is diffed against its have version.
281    pub fn stream_spec_mode(self) -> Diff<StreamSpecMode> {
282        Diff {
283            bin: self.bin,
284            global_opts: self.global_opts,
285            diff_opts: self.diff_opts,
286            mode: StreamSpecMode,
287        }
288    }
289}
290
291impl<M> Diff<M> {
292    /// # Description
293    ///
294    /// -doptions
295    ///
296    /// Pass options to the underlying diff routine (see Usage notes for
297    /// details).
298    pub fn get_diff_options(&self) -> Option<&DiffOptions> {
299        self.diff_opts.as_ref()
300    }
301
302    /// # Description
303    ///
304    /// -doptions
305    ///
306    /// Pass options to the underlying diff routine (see Usage notes for
307    /// details).
308    pub fn set_diff_options(&mut self, v: impl Into<DiffOptions>) -> &mut Self {
309        self.diff_opts = Some(v.into());
310        self
311    }
312
313    /// # Description
314    ///
315    /// -doptions
316    ///
317    /// Pass options to the underlying diff routine (see Usage notes for
318    /// details).
319    pub fn diff_options(mut self, v: impl Into<DiffOptions>) -> Self {
320        self.diff_opts = Some(v.into());
321        self
322    }
323}
324
325impl<M: ExclusiveOption> Diff<M> {
326    /// # Description
327    ///
328    /// g-opts
329    ///
330    /// See [Global options](GlobalOpts).
331    pub fn get_global_opts(&self) -> &GlobalOpts {
332        &self.global_opts
333    }
334
335    /// # Description
336    ///
337    /// g-opts
338    ///
339    /// See [Global options](GlobalOpts).
340    pub fn set_global_opts(&mut self, v: GlobalOpts) -> &mut Self {
341        self.global_opts = v;
342        self
343    }
344
345    /// # Description
346    ///
347    /// g-opts
348    ///
349    /// See [Global options](GlobalOpts).
350    pub fn global_opts(mut self, v: GlobalOpts) -> Self {
351        self.global_opts = v;
352        self
353    }
354}
355
356impl<M> Diff<WorkspaceMode<M>> {
357    /// # Description
358    ///
359    /// -f
360    ///
361    /// Force the diff (if no revision is specified, against the head
362    /// revision), even when the client file is not open for `edit`.
363    pub fn get_force(&self) -> bool {
364        self.mode.force
365    }
366
367    /// # Description
368    ///
369    /// -f
370    ///
371    /// Force the diff (if no revision is specified, against the head
372    /// revision), even when the client file is not open for `edit`.
373    pub fn set_force(&mut self, v: bool) -> &mut Self {
374        self.mode.force = v;
375        self
376    }
377
378    /// # Description
379    ///
380    /// -f
381    ///
382    /// Force the diff (if no revision is specified, against the head
383    /// revision), even when the client file is not open for `edit`.
384    pub fn force(mut self, v: bool) -> Self {
385        self.mode.force = v;
386        self
387    }
388
389    /// # Description
390    ///
391    /// -Od
392    ///
393    /// Limit output to only those files that differ.
394    pub fn get_differing_only(&self) -> bool {
395        self.mode.differing_only
396    }
397
398    /// # Description
399    ///
400    /// -Od
401    ///
402    /// Limit output to only those files that differ.
403    pub fn set_differing_only(&mut self, v: bool) -> &mut Self {
404        self.mode.differing_only = v;
405        self
406    }
407
408    /// # Description
409    ///
410    /// -Od
411    ///
412    /// Limit output to only those files that differ.
413    pub fn differing_only(mut self, v: bool) -> Self {
414        self.mode.differing_only = v;
415        self
416    }
417
418    /// # Description
419    ///
420    /// -t
421    ///
422    /// Diff the revisions even if the files are not of type `text`.
423    pub fn get_diff_nontext(&self) -> bool {
424        self.mode.diff_nontext
425    }
426
427    /// # Description
428    ///
429    /// -t
430    ///
431    /// Diff the revisions even if the files are not of type `text`.
432    pub fn set_diff_nontext(&mut self, v: bool) -> &mut Self {
433        self.mode.diff_nontext = v;
434        self
435    }
436
437    /// # Description
438    ///
439    /// -t
440    ///
441    /// Diff the revisions even if the files are not of type `text`.
442    pub fn diff_nontext(mut self, v: bool) -> Self {
443        self.mode.diff_nontext = v;
444        self
445    }
446}
447
448impl Diff<WorkspaceMode<Unselected>> {
449    /// # Description
450    ///
451    /// -soptions
452    ///
453    /// Pass display options to the underlying diff routine (see Usage notes
454    /// for details).
455    ///
456    /// Transitions this command to the [`WorkspaceDisplayMode`] state with
457    /// the display `options` set; `-m max` is unavailable in this state.
458    pub fn display_options(self, v: DisplayOptions) -> Diff<WorkspaceMode<WorkspaceDisplayMode>> {
459        Diff {
460            bin: self.bin,
461            global_opts: self.global_opts,
462            diff_opts: self.diff_opts,
463            mode: WorkspaceMode {
464                force: self.mode.force,
465                differing_only: self.mode.differing_only,
466                diff_nontext: self.mode.diff_nontext,
467                mode: WorkspaceDisplayMode { display_opts: v },
468            },
469        }
470    }
471
472    /// # Description
473    ///
474    /// -m max
475    ///
476    /// Limit output to diffs (or status) of only the first `max` files,
477    /// unless the `-s` option is used, in which case the `-m` option is
478    /// ignored.
479    ///
480    /// Transitions this command to the [`WorkspaceRegularMode`] state with
481    /// the limit set to `max`; `-soptions` is unavailable in this state.
482    pub fn limit(self, max: u64) -> Diff<WorkspaceMode<WorkspaceRegularMode>> {
483        Diff {
484            bin: self.bin,
485            global_opts: self.global_opts,
486            diff_opts: self.diff_opts,
487            mode: WorkspaceMode {
488                force: self.mode.force,
489                differing_only: self.mode.differing_only,
490                diff_nontext: self.mode.diff_nontext,
491                mode: WorkspaceRegularMode { limit: max },
492            },
493        }
494    }
495}
496
497impl Diff<WorkspaceMode<WorkspaceDisplayMode>> {
498    /// # Description
499    ///
500    /// -soptions
501    ///
502    /// Pass display options to the underlying diff routine (see Usage notes
503    /// for details).
504    pub fn get_display_options(&self) -> &DisplayOptions {
505        &self.mode.mode.display_opts
506    }
507
508    /// # Description
509    ///
510    /// -soptions
511    ///
512    /// Pass display options to the underlying diff routine (see Usage notes
513    /// for details).
514    pub fn set_display_options(&mut self, v: DisplayOptions) -> &mut Self {
515        self.mode.mode.display_opts = v;
516        self
517    }
518}
519
520impl Diff<WorkspaceMode<WorkspaceRegularMode>> {
521    /// # Description
522    ///
523    /// -m max
524    ///
525    /// Limit output to diffs (or status) of only the first `max` files,
526    /// unless the `-s` option is used, in which case the `-m` option is
527    /// ignored.
528    pub fn get_limit(&self) -> u64 {
529        self.mode.mode.limit
530    }
531
532    /// # Description
533    ///
534    /// -m max
535    ///
536    /// Limit output to diffs (or status) of only the first `max` files,
537    /// unless the `-s` option is used, in which case the `-m` option is
538    /// ignored.
539    pub fn set_limit(&mut self, v: u64) -> &mut Self {
540        self.mode.mode.limit = v;
541        self
542    }
543}
544
545impl<M: ExclusiveOption> SubCommand for Diff<M> {
546    fn name(&self) -> &str {
547        "diff"
548    }
549
550    fn inject_local_args(&self, command: &mut Command) {
551        if let Some(diff_opts) = &self.diff_opts {
552            diff_opts.inject_arg(command);
553        }
554
555        self.mode.inject_args(command);
556    }
557
558    fn global_opts(&self) -> Option<&GlobalOpts> {
559        Some(&self.global_opts)
560    }
561}
562
563impl ParameterizedSpawn for Diff<Unselected> {
564    type Input<'a> = &'a [&'a OsStr];
565    type Output<'a> = Child;
566    type Error = std::io::Error;
567
568    /// Spawns `p4 diff` for the given file arguments as a child process with
569    /// piped standard output and error streams; use the returned [`Child`]
570    /// handle to wait for it or interact with it.
571    fn spawn_with<'a>(&mut self, files: Self::Input<'a>) -> Result<Self::Output<'a>, Self::Error> {
572        self.setup_command(&self.bin)
573            .args(files)
574            .stdout(Stdio::piped())
575            .stderr(Stdio::piped())
576            .spawn()
577    }
578}
579
580impl<M: ExclusiveOption> ParameterizedSpawn for Diff<WorkspaceMode<M>> {
581    type Input<'a> = &'a [&'a OsStr];
582    type Output<'a> = Child;
583    type Error = std::io::Error;
584
585    /// Spawns `p4 diff` for the given file arguments as a child process with
586    /// piped standard output and error streams; use the returned [`Child`]
587    /// handle to wait for it or interact with it.
588    fn spawn_with<'a>(&mut self, files: Self::Input<'a>) -> Result<Self::Output<'a>, Self::Error> {
589        self.setup_command(&self.bin)
590            .args(files)
591            .stdout(Stdio::piped())
592            .stderr(Stdio::piped())
593            .spawn()
594    }
595}
596
597impl ParameterizedSpawn for Diff<StreamSpecMode> {
598    type Input<'a> = Option<&'a str>;
599    type Output<'a> = Child;
600    type Error = std::io::Error;
601
602    /// Spawns `p4 diff -As` as a child process with piped standard output
603    /// and error streams; use the returned [`Child`] handle to wait for it
604    /// or interact with it.
605    ///
606    /// Pass `Some(stream_spec)` to diff against the given stream spec — a
607    /// streamname, optionally at a specific changelist number (`@head`
608    /// selects the head version, `@change` the version at a specific
609    /// change, and `@=change` the shelved version at a specific change) —
610    /// or `None` to diff the opened stream spec against its have version.
611    fn spawn_with<'a>(
612        &mut self,
613        stream_spec: Self::Input<'a>,
614    ) -> Result<Self::Output<'a>, Self::Error> {
615        let mut command = self.setup_command(&self.bin);
616
617        if let Some(stream_spec) = stream_spec {
618            command.arg(stream_spec);
619        }
620        command
621            .stdout(Stdio::piped())
622            .stderr(Stdio::piped())
623            .spawn()
624    }
625}
626
627impl SpawnExt for Diff<StreamSpecMode> {
628    /// Spawns `p4 diff -As` without a stream spec as a child process with
629    /// piped standard output and error streams; the opened stream spec is
630    /// diffed against its have version. Use the returned [`Child`] handle
631    /// to wait for it or interact with it.
632    fn spawn<'a>(&mut self) -> Result<Self::Output<'a>, Self::Error> {
633        self.spawn_with(None)
634    }
635}
636
637#[cfg(test)]
638mod tests {
639    use super::*;
640    use crate::cmd::DiffOptionsBuilder;
641    use crate::cmd::args_of;
642
643    /// Dry-run checks of the assembled `p4 diff` command line; no process is
644    /// spawned.
645    #[test]
646    fn without_options() {
647        let diff = Diff::new("p4", GlobalOpts::new());
648
649        assert_eq!(args_of(&diff.setup_command("p4")), ["diff"]);
650    }
651
652    #[test]
653    fn workspace_mode_via_force() {
654        let diff = Diff::new("p4", GlobalOpts::new()).force(true);
655
656        assert!(diff.get_force());
657        assert_eq!(args_of(&diff.setup_command("p4")), ["diff", "-f"]);
658    }
659
660    #[test]
661    fn workspace_mode_via_differing_only() {
662        let diff = Diff::new("p4", GlobalOpts::new()).differing_only(true);
663
664        assert_eq!(args_of(&diff.setup_command("p4")), ["diff", "-Od"]);
665    }
666
667    #[test]
668    fn workspace_mode_via_diff_nontext() {
669        let diff = Diff::new("p4", GlobalOpts::new()).diff_nontext(true);
670
671        assert_eq!(args_of(&diff.setup_command("p4")), ["diff", "-t"]);
672    }
673
674    #[test]
675    fn workspace_mode_combines_options() {
676        let mut diff = Diff::new("p4", GlobalOpts::new()).diff_nontext(true);
677        diff.set_force(true).set_differing_only(true);
678
679        assert_eq!(
680            args_of(&diff.setup_command("p4")),
681            ["diff", "-f", "-t", "-Od"]
682        );
683    }
684
685    #[test]
686    fn workspace_mode_preserves_diff_options() {
687        let mut diff = Diff::new("p4", GlobalOpts::new())
688            .diff_options(DiffOptionsBuilder::unified(None))
689            .force(true);
690        diff.set_diff_options(DiffOptionsBuilder::summary());
691
692        assert_eq!(args_of(&diff.setup_command("p4")), ["diff", "-ds", "-f"]);
693    }
694
695    #[test]
696    fn display_mode_injects_filter() {
697        let diff = Diff::new("p4", GlobalOpts::new())
698            .force(true)
699            .display_options(DisplayOptions::UnopenedChanged);
700
701        assert_eq!(diff.get_display_options(), &DisplayOptions::UnopenedChanged);
702        assert_eq!(args_of(&diff.setup_command("p4")), ["diff", "-f", "-se"]);
703    }
704
705    #[test]
706    fn display_mode_flag_mapping() {
707        assert_eq!(DisplayOptions::OpenedChangedOrMissing.as_str(), "-sa");
708        assert_eq!(DisplayOptions::ResolvedThenModified.as_str(), "-sb");
709        assert_eq!(DisplayOptions::UnopenedMissing.as_str(), "-sd");
710        assert_eq!(DisplayOptions::UnopenedChanged.as_str(), "-se");
711        assert_eq!(DisplayOptions::ListWithStatus.as_str(), "-sl");
712        assert_eq!(DisplayOptions::OpenedIdentical.as_str(), "-sr");
713    }
714
715    #[test]
716    fn regular_mode_injects_limit() {
717        let diff = Diff::new("p4", GlobalOpts::new()).force(false).limit(10);
718
719        assert_eq!(diff.get_limit(), 10);
720        assert_eq!(args_of(&diff.setup_command("p4")), ["diff", "-m", "10"]);
721    }
722
723    #[test]
724    fn stream_spec_mode_bare() {
725        let diff = Diff::new("p4", GlobalOpts::new()).stream_spec_mode();
726
727        assert_eq!(args_of(&diff.setup_command("p4")), ["diff", "-As"]);
728    }
729
730    #[test]
731    fn stream_spec_mode_with_spec() {
732        let diff = Diff::new("p4", GlobalOpts::new()).stream_spec_mode();
733
734        // Mirrors `spawn_with`, which appends the stream spec after the
735        // assembled command.
736        let mut command = diff.setup_command("p4");
737        command.arg("myStream@head");
738
739        assert_eq!(args_of(&command), ["diff", "-As", "myStream@head"]);
740    }
741
742    #[test]
743    fn stream_spec_mode_preserves_diff_options() {
744        let diff = Diff::new("p4", GlobalOpts::new())
745            .diff_options(DiffOptionsBuilder::unified(None))
746            .stream_spec_mode();
747
748        // Mirrors `spawn_with`, which appends the stream spec after the
749        // assembled command.
750        let mut command = diff.setup_command("p4");
751        command.arg("myStream");
752
753        assert_eq!(args_of(&command), ["diff", "-du", "-As", "myStream"]);
754    }
755}