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