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