Skip to main content

perforce_cli/
cmd.rs

1pub mod add;
2pub mod admin;
3#[cfg(not(feature = "lt2016_1"))]
4pub mod aliases;
5pub mod annotate;
6pub mod archive;
7pub mod attribute;
8#[cfg(not(feature = "lt2017_2"))]
9pub mod bgtask;
10pub mod branch;
11pub mod branches;
12pub mod cachepurge;
13pub mod change;
14pub mod changes;
15pub mod describe;
16pub mod diff;
17pub mod diff2;
18pub mod edit;
19pub mod filelog;
20pub mod print;
21pub mod sync;
22pub mod r#where;
23
24pub use add::Add;
25pub use admin::AdminEntry;
26#[cfg(not(feature = "lt2016_1"))]
27pub use aliases::Aliases;
28pub use annotate::Annotate;
29pub use archive::Archive;
30pub use attribute::Attribute;
31#[cfg(not(feature = "lt2017_2"))]
32pub use bgtask::BackGroundTask;
33pub use branch::Branch;
34pub use branches::Branches;
35pub use cachepurge::CachePurge;
36pub use change::Change;
37pub use changes::Changes;
38pub use describe::Describe;
39pub use diff::Diff;
40pub use diff::DisplayOptions;
41pub use diff2::Diff2;
42pub use edit::Edit;
43pub use filelog::FileLog;
44pub use print::Print;
45pub use sync::Sync;
46pub use r#where::Where;
47
48use std::{ffi::OsStr, process::Command};
49
50use crate::global::GlobalOpts;
51
52/// Long output mode for changelist descriptions shared by commands such as
53/// `p4 changes` and `p4 filelog`.
54///
55/// By default, only the first 30 (or 31) characters of the description are
56/// shown.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum LongOutput {
59    /// Show the full text of each changelist description (`-l`).
60    Default,
61    /// Show the full text truncated at 250 characters (`-L`).
62    Truncated,
63}
64
65impl LongOutput {
66    /// Returns the command-line flag for this long output mode.
67    pub fn as_str(&self) -> &'static str {
68        match self {
69            LongOutput::Default => "-l",
70            LongOutput::Truncated => "-L",
71        }
72    }
73}
74
75/// Output format of the diff routine passed via `-doptions`.
76///
77/// This is one of two orthogonal dimensions of [`DiffOptions`] (the other
78/// being [`WhitespaceHandling`]). Exactly one format may be selected; the
79/// [`Default`](DiffFormat::Default) variant corresponds to no format flag.
80#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
81pub enum DiffFormat {
82    /// Default diff output format (no format flag).
83    #[default]
84    Default,
85    /// Context output format (`-dc[num]`), showing `num` lines of context
86    /// around the changes.
87    Context(Option<u32>),
88    /// RCS output format (`-dn`), showing additions and deletions with
89    /// associated line ranges.
90    Rcs,
91    /// Summary output format (`-ds`), showing only the number of chunks and
92    /// lines added, deleted, or changed.
93    Summary,
94    /// Unified output format (`-du[num]`), showing added and deleted lines
95    /// with `num` lines of context, in a form compatible with `patch(1)`.
96    Unified(Option<u32>),
97}
98
99/// Whitespace handling of the diff routine passed via `-doptions`.
100///
101/// This is one of two orthogonal dimensions of [`DiffOptions`] (the other
102/// being [`DiffFormat`]). The
103/// [`IgnoreChangesWithinWhitespace`](WhitespaceHandling::IgnoreChangesWithinWhitespace)
104/// and [`IgnoreAllWhitespace`](WhitespaceHandling::IgnoreAllWhitespace)
105/// variants each imply
106/// [`IgnoreLineEndings`](WhitespaceHandling::IgnoreLineEndings).
107#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
108pub enum WhitespaceHandling {
109    /// No whitespace handling.
110    #[default]
111    None,
112    /// Ignore line-ending (CR/LF) convention when finding diffs (`-dl`).
113    IgnoreLineEndings,
114    /// Ignore changes made within whitespace (`-db`); implies `-dl`.
115    IgnoreChangesWithinWhitespace,
116    /// Ignore whitespace altogether (`-dw`); implies `-dl`.
117    IgnoreAllWhitespace,
118}
119
120/// Diff routine options passed via `-doptions`, shared by commands such as
121/// `p4 diff`, `p4 diff2`, `p4 describe`, and `p4 annotate`.
122///
123/// The structured ([`Typed`](DiffOptions::Typed)) options split into two
124/// orthogonal dimensions — [`DiffFormat`] (output format) and
125/// [`WhitespaceHandling`] — each of which is internally mutually exclusive.
126/// All options are assembled with a [`DiffOptionsBuilder`], whose mode type
127/// parameter tracks at compile time whether structured or raw options are
128/// being built; both builder states convert into this type via [`From`], and
129/// command methods accept `impl Into<DiffOptions>` so a builder can be
130/// passed directly.
131///
132/// # Examples
133///
134/// ```
135/// use perforce_cli::cmd::{DiffOptions, DiffOptionsBuilder};
136///
137/// // `-dub`: unified format, ignore changes within whitespace
138/// let opts = DiffOptions::from(
139///     DiffOptionsBuilder::unified(None).ignore_changes_within_whitespace(),
140/// );
141///
142/// // `-dc3`: context format with 3 lines of context
143/// let opts = DiffOptions::from(DiffOptionsBuilder::context(Some(3)));
144///
145/// // `-d-C 25`: pass `-C 25` straight to an external diff program
146/// let opts = DiffOptions::from(DiffOptionsBuilder::raw("-C 25"));
147/// ```
148#[derive(Debug, Clone, PartialEq, Eq)]
149pub enum DiffOptions {
150    /// Structured diff options (the documented subset of the standard UNIX
151    /// diff flags).
152    Typed {
153        format: DiffFormat,
154        whitespace: WhitespaceHandling,
155    },
156    /// Raw option string passed directly to the underlying diff routine
157    /// (useful with an external diff program configured via `P4DIFF`).
158    Raw(String),
159}
160
161impl Default for DiffOptions {
162    fn default() -> Self {
163        DiffOptions::Typed {
164            format: DiffFormat::Default,
165            whitespace: WhitespaceHandling::None,
166        }
167    }
168}
169
170impl DiffOptions {
171    /// Returns the selected output format, or `None` if this is a
172    /// [`Raw`](DiffOptions::Raw) value.
173    pub fn format(&self) -> Option<DiffFormat> {
174        match self {
175            DiffOptions::Typed { format, .. } => Some(*format),
176            DiffOptions::Raw(_) => None,
177        }
178    }
179
180    /// Returns the selected whitespace handling, or `None` if this is a
181    /// [`Raw`](DiffOptions::Raw) value.
182    pub fn whitespace_handling(&self) -> Option<WhitespaceHandling> {
183        match self {
184            DiffOptions::Typed { whitespace, .. } => Some(*whitespace),
185            DiffOptions::Raw(_) => None,
186        }
187    }
188
189    /// Returns the raw option string, or `None` if this is a
190    /// [`Typed`](DiffOptions::Typed) value.
191    pub fn raw_str(&self) -> Option<&str> {
192        match self {
193            DiffOptions::Typed { .. } => None,
194            DiffOptions::Raw(s) => Some(s),
195        }
196    }
197
198    /// Injects the `-doptions` argument into `command`.
199    ///
200    /// If this is a [`Typed`](DiffOptions::Typed) value with both the format
201    /// and whitespace handling at their defaults, no `-d` argument is emitted.
202    pub fn inject_arg(&self, command: &mut Command) {
203        use DiffFormat::*;
204        use WhitespaceHandling::*;
205
206        let (format, whitespace) = match self {
207            DiffOptions::Typed { format, whitespace } => (*format, *whitespace),
208            DiffOptions::Raw(s) => {
209                command.arg(format!("-d{s}"));
210                return;
211            }
212        };
213
214        if matches!(format, Default) && matches!(whitespace, None) {
215            return;
216        }
217
218        let mut s = String::from("-d");
219
220        match format {
221            Default => {}
222            Context(num) => {
223                s.push('c');
224                if let Some(n) = num {
225                    s.push_str(&n.to_string());
226                }
227            }
228            Rcs => s.push('n'),
229            Summary => s.push('s'),
230            Unified(num) => {
231                s.push('u');
232                if let Some(n) = num {
233                    s.push_str(&n.to_string());
234                }
235            }
236        }
237
238        match whitespace {
239            None => {}
240            IgnoreLineEndings => s.push('l'),
241            IgnoreChangesWithinWhitespace => s.push('b'),
242            IgnoreAllWhitespace => s.push('w'),
243        }
244
245        command.arg(s);
246    }
247}
248
249impl From<String> for DiffOptions {
250    fn from(s: String) -> Self {
251        DiffOptions::Raw(s)
252    }
253}
254
255/// The structured ([`Typed`](DiffOptions::Typed)) state of a
256/// [`DiffOptionsBuilder`], holding the selected [`DiffFormat`] and
257/// [`WhitespaceHandling`].
258#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
259pub struct DiffOptionsTypedMode {
260    format: DiffFormat,
261    whitespace: WhitespaceHandling,
262}
263
264/// The raw ([`Raw`](DiffOptions::Raw)) state of a [`DiffOptionsBuilder`],
265/// holding the option string passed verbatim to the underlying diff routine.
266#[derive(Debug, Clone, PartialEq, Eq)]
267pub struct DiffOptionsRawMode {
268    raw: String,
269}
270
271/// Builder for [`DiffOptions`], shared by commands such as `p4 diff`,
272/// `p4 diff2`, `p4 describe`, and `p4 annotate`.
273///
274/// The mode type parameter tracks at compile time which kind of options are
275/// being assembled. The format constructors ([`DiffOptionsBuilder::context`],
276/// [`DiffOptionsBuilder::rcs`], [`DiffOptionsBuilder::summary`],
277/// [`DiffOptionsBuilder::unified`], and [`DiffOptionsBuilder::new`]) produce
278/// the [`DiffOptionsTypedMode`] state, where the whitespace builders
279/// ([`DiffOptionsBuilder::ignore_line_endings`],
280/// [`DiffOptionsBuilder::ignore_changes_within_whitespace`],
281/// [`DiffOptionsBuilder::ignore_all_whitespace`]) become available;
282/// [`DiffOptionsBuilder::raw`] produces the [`DiffOptionsRawMode`] state,
283/// which offers no further options — the two states never mix.
284///
285/// Both states convert into [`DiffOptions`] via [`From`], so command methods
286/// that accept `impl Into<DiffOptions>` take a builder directly.
287///
288/// # Examples
289///
290/// ```
291/// use perforce_cli::cmd::DiffOptionsBuilder;
292///
293/// // `-dub`: unified format, ignore changes within whitespace
294/// let builder =
295///     DiffOptionsBuilder::unified(None).ignore_changes_within_whitespace();
296///
297/// // `-dc3`: context format with 3 lines of context
298/// let builder = DiffOptionsBuilder::context(Some(3));
299///
300/// // `-d-C 25`: pass `-C 25` straight to an external diff program
301/// let builder = DiffOptionsBuilder::raw("-C 25");
302/// ```
303#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
304pub struct DiffOptionsBuilder<M> {
305    mode: M,
306}
307
308impl DiffOptionsBuilder<DiffOptionsTypedMode> {
309    /// Creates a new builder in the [`DiffOptionsTypedMode`] state with the
310    /// default format and no whitespace handling; the resulting
311    /// [`DiffOptions`] emits no `-d` flag.
312    pub fn new() -> Self {
313        Self {
314            mode: DiffOptionsTypedMode::default(),
315        }
316    }
317
318    /// `-dc[num]`: context output format with `num` lines of context.
319    pub fn context(num: Option<u32>) -> Self {
320        Self {
321            mode: DiffOptionsTypedMode {
322                format: DiffFormat::Context(num),
323                whitespace: WhitespaceHandling::None,
324            },
325        }
326    }
327
328    /// `-dn`: RCS output format.
329    pub fn rcs() -> Self {
330        Self {
331            mode: DiffOptionsTypedMode {
332                format: DiffFormat::Rcs,
333                whitespace: WhitespaceHandling::None,
334            },
335        }
336    }
337
338    /// `-ds`: summary output format.
339    pub fn summary() -> Self {
340        Self {
341            mode: DiffOptionsTypedMode {
342                format: DiffFormat::Summary,
343                whitespace: WhitespaceHandling::None,
344            },
345        }
346    }
347
348    /// `-du[num]`: unified output format with `num` lines of context.
349    pub fn unified(num: Option<u32>) -> Self {
350        Self {
351            mode: DiffOptionsTypedMode {
352                format: DiffFormat::Unified(num),
353                whitespace: WhitespaceHandling::None,
354            },
355        }
356    }
357
358    /// `-dl`: ignore line-ending (CR/LF) convention when finding diffs.
359    pub fn ignore_line_endings(mut self) -> Self {
360        self.mode.whitespace = WhitespaceHandling::IgnoreLineEndings;
361        self
362    }
363
364    /// `-db`: ignore changes made within whitespace (implies `-dl`).
365    pub fn ignore_changes_within_whitespace(mut self) -> Self {
366        self.mode.whitespace = WhitespaceHandling::IgnoreChangesWithinWhitespace;
367        self
368    }
369
370    /// `-dw`: ignore whitespace altogether (implies `-dl`).
371    pub fn ignore_all_whitespace(mut self) -> Self {
372        self.mode.whitespace = WhitespaceHandling::IgnoreAllWhitespace;
373        self
374    }
375}
376
377impl DiffOptionsBuilder<DiffOptionsRawMode> {
378    /// Passes an arbitrary option string directly to the underlying diff
379    /// routine (for use with an external diff program).
380    ///
381    /// The string is appended to `-d` verbatim. For example,
382    /// `DiffOptionsBuilder::raw("-C 25")` produces `-d-C 25`.
383    pub fn raw(s: impl Into<String>) -> Self {
384        Self {
385            mode: DiffOptionsRawMode { raw: s.into() },
386        }
387    }
388}
389
390impl From<DiffOptionsBuilder<DiffOptionsTypedMode>> for DiffOptions {
391    fn from(builder: DiffOptionsBuilder<DiffOptionsTypedMode>) -> Self {
392        DiffOptions::Typed {
393            format: builder.mode.format,
394            whitespace: builder.mode.whitespace,
395        }
396    }
397}
398
399impl From<DiffOptionsBuilder<DiffOptionsRawMode>> for DiffOptions {
400    fn from(builder: DiffOptionsBuilder<DiffOptionsRawMode>) -> Self {
401        DiffOptions::Raw(builder.mode.raw)
402    }
403}
404
405/// The default "no option selected" variant, shared by every
406/// [`ExclusiveOption`] group.
407///
408/// It is a zero-sized type whose [`ExclusiveOption::inject_args`] is a no-op,
409/// so any mutually exclusive option group can use it as its default type
410/// parameter instead of defining its own empty variant.
411#[derive(Debug, Clone, Copy, Default)]
412pub struct Unselected;
413
414impl ExclusiveOption for Unselected {
415    fn inject_args(&self, _: &mut Command) {}
416}
417
418/// Marker trait for a mutually exclusive option group.
419///
420/// Some Perforce commands accept a set of options where only one may be
421/// selected at a time (for example `p4 admin checkpoint [-z | -Z]` or
422/// `p4 admin updatespecdepot [-a | -s type]`). Each variant of such a group
423/// implements this trait to inject its own CLI arguments; the selected
424/// variant is encoded in a type parameter so that the alternatives are
425/// unavailable at compile time.
426///
427/// Variants may carry their own data (for example the `type` argument of
428/// `-s type`) and inject any number of arguments, keeping the trait open to
429/// option groups more complex than a single flag.
430pub trait ExclusiveOption {
431    #[allow(unused_variables)]
432    /// Inject the CLI arguments corresponding to this selection into
433    /// `command`.
434    fn inject_args(&self, command: &mut Command) {}
435}
436
437pub trait SubCommand {
438    fn name(&self) -> &str;
439
440    fn inject_local_args(&self, command: &mut Command);
441
442    fn global_opts(&self) -> Option<&GlobalOpts> {
443        None
444    }
445
446    fn inject_args(&self, command: &mut Command) {
447        // inject global opts
448        if let Some(global_opts) = self.global_opts() {
449            global_opts.setup_args(command);
450        };
451        // inject local opts
452        self.inject_local_args(command.arg(self.name()));
453    }
454
455    fn setup_command<S: AsRef<OsStr>>(&self, bin: S) -> Command {
456        let mut cmd = Command::new(bin);
457
458        self.inject_args(&mut cmd);
459        cmd
460    }
461}
462
463/// Test-only helper: collects the arguments assembled on a [`Command`] as
464/// strings for easy comparison.
465#[cfg(test)]
466pub(crate) fn args_of(command: &Command) -> Vec<String> {
467    command
468        .get_args()
469        .map(|arg| arg.to_string_lossy().into_owned())
470        .collect()
471}
472
473#[cfg(test)]
474mod diff_options_tests {
475    use super::*;
476
477    fn injected(opts: impl Into<DiffOptions>) -> Vec<String> {
478        let mut cmd = Command::new("p4");
479        opts.into().inject_arg(&mut cmd);
480        args_of(&cmd)
481    }
482
483    #[test]
484    fn default_emits_nothing() {
485        assert!(injected(DiffOptionsBuilder::new()).is_empty());
486        assert!(injected(DiffOptions::default()).is_empty());
487    }
488
489    #[test]
490    fn unified_format() {
491        assert_eq!(injected(DiffOptionsBuilder::unified(None)), ["-du"]);
492    }
493
494    #[test]
495    fn unified_format_with_context() {
496        assert_eq!(injected(DiffOptionsBuilder::unified(Some(3))), ["-du3"]);
497    }
498
499    #[test]
500    fn context_format() {
501        assert_eq!(injected(DiffOptionsBuilder::context(None)), ["-dc"]);
502        assert_eq!(injected(DiffOptionsBuilder::context(Some(5))), ["-dc5"]);
503    }
504
505    #[test]
506    fn rcs_format() {
507        assert_eq!(injected(DiffOptionsBuilder::rcs()), ["-dn"]);
508    }
509
510    #[test]
511    fn summary_format() {
512        assert_eq!(injected(DiffOptionsBuilder::summary()), ["-ds"]);
513    }
514
515    #[test]
516    fn whitespace_only() {
517        assert_eq!(
518            injected(DiffOptionsBuilder::new().ignore_line_endings()),
519            ["-dl"]
520        );
521    }
522
523    #[test]
524    fn unified_with_ignore_changes_within_whitespace() {
525        assert_eq!(
526            injected(DiffOptionsBuilder::unified(None).ignore_changes_within_whitespace()),
527            ["-dub"]
528        );
529    }
530
531    #[test]
532    fn context_with_ignore_all_whitespace() {
533        assert_eq!(
534            injected(DiffOptionsBuilder::context(Some(2)).ignore_all_whitespace()),
535            ["-dc2w"]
536        );
537    }
538
539    #[test]
540    fn raw_passthrough() {
541        assert_eq!(injected(DiffOptionsBuilder::raw("-C 25")), ["-d-C 25"]);
542        assert_eq!(injected(DiffOptionsBuilder::raw("--brief")), ["-d--brief"]);
543    }
544
545    #[test]
546    fn getters_reflect_variant() {
547        let typed = DiffOptions::from(DiffOptionsBuilder::unified(Some(3)).ignore_line_endings());
548        assert_eq!(typed.format(), Some(DiffFormat::Unified(Some(3))));
549        assert_eq!(
550            typed.whitespace_handling(),
551            Some(WhitespaceHandling::IgnoreLineEndings)
552        );
553        assert_eq!(typed.raw_str(), None);
554
555        let raw = DiffOptions::from(DiffOptionsBuilder::raw("abc"));
556        assert_eq!(raw.format(), None);
557        assert_eq!(raw.whitespace_handling(), None);
558        assert_eq!(raw.raw_str(), Some("abc"));
559    }
560}