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