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