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