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