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