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