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