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