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}