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