Skip to main content

perforce_cli/cmd/
change.rs

1use std::{
2    ffi::OsStr,
3    path::{Path, PathBuf},
4    process::{Child, Command, Stdio},
5};
6
7use super::{ExclusiveOption, SubCommand, Unselected};
8
9use crate::global::GlobalOpts;
10use crate::spawn::ParameterizedSpawn;
11
12pub mod delete;
13pub mod inout;
14pub mod userorvis;
15
16pub use delete::{DeleteMode, LocalDeleteMode, TransferDeleteMode};
17#[cfg(not(feature = "lt2015_2"))]
18pub use delete::{RemoteDeleteMode, TransferRemoteDeleteMode};
19pub use userorvis::{TransferUVMode, UserOrVisibilityMode};
20
21// The seven syntax forms of `p4 change`:
22//
23// `p4 [g-opts] change [-s] [-f | -u] [[-O|-I] changelist]`
24// `p4 [g-opts] change -d [-f -s -O] changelist`
25// `p4 [g-opts] change -o [-s] [-f] [[-O|-I] changelist]`
26// `p4 [g-opts] change -i [-s] [-f | -u]`
27// `p4 [g-opts] change -t restricted | public [-U user] [-f|-u|-O|-I] changelist`
28// `p4 [g-opts] change -U user [-t restricted | public] [-f] changelist`
29// `p4 [g-opts] change -d -f --serverid=X changelist`
30//
31// The `-t` and `-U` forms describe the same operation — changing the type
32// and/or the owner of a changelist — so they share a single form type,
33// `UserOrVisibilityMode`, which accepts at most one of `-f`, `-u`, `-O`,
34// and `-I` (and never `-s`).
35//
36// Compatibility matrix of the options across the forms:
37//
38// | option | edit | -d | -o | -i | -t/-U  | -d --serverid |
39// |--------|------|----|----|----|--------|---------------|
40// | -s     |  yes | yes| yes| yes| no     | no            |
41// | -f     |  yes | yes| yes| yes| one of | forced        |
42// | -u     |  yes | no | no | yes| one of | no            |
43// | -O     |  yes | yes| yes| no | one of | no            |
44// | -I     |  yes | no | yes| no | one of | no            |
45//
46// The matrix is encoded in four orthogonal type parameters:
47//
48// - `F`: the command form (edit/delete/stdout/stdin/change-type-or-owner)
49// - `O`: the `-f` / `-u` operation
50// - `C`: the `-O` / `-I` changelist reference
51// - `S`: the `-s` arbitrary job status flag
52//
53// Transitions between forms are only `impl`ed when every selected option is
54// accepted by the target form, so an illegal transition (for example moving
55// `-I` into `change -d`, or `-s` into `change -t`) does not compile.
56
57/// Visibility of a changelist as set with `-t restricted | public`.
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum Visibility {
60    /// The changelist is visible without restrictions (`public`).
61    Public,
62
63    /// The changelist is hidden from users who do not own it or lack `list`
64    /// permission on its files (`restricted`).
65    Restricted,
66}
67
68impl Visibility {
69    /// Returns the command-line representation of this visibility.
70    pub fn as_str(&self) -> &'static str {
71        match self {
72            Visibility::Public => "public",
73            Visibility::Restricted => "restricted",
74        }
75    }
76}
77
78// ---------------------------------------------------------------------------
79// Form dimension (`F`)
80// ---------------------------------------------------------------------------
81
82/// The plain edit/create form: `p4 change [-s] [-f|-u] [[-O|-I] changelist]`.
83#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
84pub struct RegularOperationMode;
85
86/// The stdout form: `p4 change -o [-s] [-f] [[-O|-I] changelist]`.
87#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
88pub struct StdoutMode;
89
90/// The stdin form: `p4 change -i [-s] [-f|-u]`.
91#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
92pub struct StdinMode;
93
94// ---------------------------------------------------------------------------
95// Operation dimension (`O`): `-f` / `-u`
96// ---------------------------------------------------------------------------
97
98/// `-f` selected in a form where it may be combined with other options
99/// (`-s`, `-O`, `-I`): the edit, `-d`, `-o`, and `-i` forms.
100#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
101pub struct ForceOperationMode;
102
103/// `-u` selected in a form where it may be combined with `-s`: the edit and
104/// `-i` forms.
105#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
106pub struct UpdateOperationMode;
107
108/// `-f` selected as the single operation of the `-t` / `-U` forms, where it
109/// is mutually exclusive with `-u`, `-O`, and `-I`.
110#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
111pub struct OnlyForceOperationMode;
112
113/// `-u` selected as the single operation of the `-t` / `-U` forms, where
114/// it is mutually exclusive with `-f`, `-O`, and `-I`.
115#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
116pub struct OnlyUpdateOperationMode;
117
118// ---------------------------------------------------------------------------
119// Changelist-reference dimension (`C`): `-O` / `-I`
120// ---------------------------------------------------------------------------
121
122/// `-O` selected in a form where it may be combined with other options
123/// (`-f`, `-s`): the edit, `-d`, and `-o` forms.
124#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
125pub struct OriginalChangeListMode;
126
127/// `-I` selected in a form where it may be combined with other options
128/// (`-f`, `-s`): the edit and `-o` forms.
129#[cfg(not(feature = "lt2022_1"))]
130#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
131pub struct IdentityChangeListMode;
132
133/// `-O` selected as the single reference of the `-t` / `-U` forms, where
134/// it is mutually exclusive with `-f`, `-u`, and `-I`.
135#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
136pub struct OnlyOriginalChangeListMode;
137
138/// `-I` selected as the single reference of the `-t` / `-U` forms, where
139/// it is mutually exclusive with `-f`, `-u`, and `-O`.
140#[cfg(not(feature = "lt2015_2"))]
141#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
142pub struct OnlyIdentityChangeListMode;
143
144// ---------------------------------------------------------------------------
145// Job-status dimension (`S`): `-s`
146// ---------------------------------------------------------------------------
147
148/// `-s` selected: jobs may be assigned arbitrary status values on submission.
149///
150/// Unlike a plain boolean field, selecting `-s` is its own type state so that
151/// forms that do not accept `-s` (the `-t`, `-U`, and remote `-d` forms)
152/// cannot be entered once it is set.
153#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
154pub struct ArbitraryJobStatus;
155
156// ---------------------------------------------------------------------------
157// ExclusiveOption implementations
158// ---------------------------------------------------------------------------
159
160impl ExclusiveOption for ForceOperationMode {
161    fn inject_args(&self, command: &mut Command) {
162        command.arg("-f");
163    }
164}
165
166impl ExclusiveOption for UpdateOperationMode {
167    fn inject_args(&self, command: &mut Command) {
168        command.arg("-u");
169    }
170}
171
172impl ExclusiveOption for OnlyForceOperationMode {
173    fn inject_args(&self, command: &mut Command) {
174        command.arg("-f");
175    }
176}
177
178impl ExclusiveOption for OnlyUpdateOperationMode {
179    fn inject_args(&self, command: &mut Command) {
180        command.arg("-u");
181    }
182}
183
184impl ExclusiveOption for OriginalChangeListMode {
185    fn inject_args(&self, command: &mut Command) {
186        command.arg("-O");
187    }
188}
189
190#[cfg(not(feature = "lt2022_1"))]
191impl ExclusiveOption for IdentityChangeListMode {
192    fn inject_args(&self, command: &mut Command) {
193        command.arg("-I");
194    }
195}
196
197impl ExclusiveOption for OnlyOriginalChangeListMode {
198    fn inject_args(&self, command: &mut Command) {
199        command.arg("-O");
200    }
201}
202
203#[cfg(not(feature = "lt2015_2"))]
204impl ExclusiveOption for OnlyIdentityChangeListMode {
205    fn inject_args(&self, command: &mut Command) {
206        command.arg("-I");
207    }
208}
209
210impl ExclusiveOption for ArbitraryJobStatus {
211    fn inject_args(&self, command: &mut Command) {
212        command.arg("-s");
213    }
214}
215
216impl ExclusiveOption for RegularOperationMode {}
217
218impl ExclusiveOption for StdoutMode {
219    fn inject_args(&self, command: &mut Command) {
220        command.arg("-o");
221    }
222}
223
224impl ExclusiveOption for StdinMode {
225    fn inject_args(&self, command: &mut Command) {
226        command.arg("-i");
227    }
228}
229
230// ---------------------------------------------------------------------------
231// Marker traits bounding the cross-form transitions
232// ---------------------------------------------------------------------------
233
234/// Forms that take a `changelist` positional argument at spawn time.
235///
236/// [`StdinMode`] (`change -i`) deliberately does not implement this, so no
237/// changelist argument can be passed to it.
238#[doc(hidden)]
239pub trait TakesChangeList {}
240
241impl TakesChangeList for RegularOperationMode {}
242impl TakesChangeList for StdoutMode {}
243
244// ---------------------------------------------------------------------------
245// Command struct
246// ---------------------------------------------------------------------------
247
248/// Type-safe builder for `p4 change`.
249///
250/// Create or edit a changelist specification.
251///
252/// The command `p4 changelist` is an alias for `p4 change`.
253///
254/// # Syntax
255///
256/// ```text
257/// p4 [g-opts] change [-s] [-f | -u] [[-O|-I] changelist]
258/// p4 [g-opts] change -d [-f -s -O] changelist
259/// p4 [g-opts] change -o [-s] [-f] [[-O|-I] changelist]
260/// p4 [g-opts] change -i [-s] [-f | -u]
261/// p4 [g-opts] change -t restricted | public [-U user] [-f|-u|-O|-I] changelist
262/// p4 [g-opts] change -U user [-t restricted | public] [-f] changelist
263/// p4 [g-opts] change -d -f --serverid=X changelist
264/// ```
265///
266/// The four type parameters track, independently and at compile time:
267///
268/// - `M`: the command form — [`RegularOperationMode`], [`DeleteMode`],
269///   [`StdoutMode`], [`StdinMode`], or [`UserOrVisibilityMode`];
270/// - `O`: the `-f` / `-u` operation — [`ForceOperationMode`],
271///   [`UpdateOperationMode`], or the `Only*` variants used inside the
272///   `-t` / `-U` form;
273/// - `C`: the `-O` / `-I` changelist reference —
274///   [`OriginalChangeListMode`]
275#[cfg_attr(
276    not(feature = "lt2022_1"),
277    doc = "   / [`IdentityChangeListMode`], or their"
278)]
279#[cfg_attr(feature = "lt2022_1", doc = "   , or its")]
280///   `Only*` variants;
281/// - `S`: the `-s` arbitrary job status flag ([`ArbitraryJobStatus`]).
282///
283/// Start from [`Change::new`] (or [`crate::P4Cli::change`]), enter a form
284/// with one of [`Self::delete`], [`Self::stdout`], [`Self::stdin`],
285/// [`Self::visibility`], or [`Self::user`] (or one of the
286/// [`Self::force`]/[`Self::update`]/[`Self::original`]/
287#[cfg_attr(not(feature = "lt2022_1"), doc = "[`Self::identity`]/")]
288/// [`Self::arbitrary_job_status`] entry points that select an option of the
289/// plain edit form), and move between compatible forms with the
290/// `delete`, `stdout`, `stdin`, `visibility`, `user`, and `server_id`
291/// transition methods.
292#[derive(Debug, Clone, Default)]
293pub struct Change<M = Unselected, P = Unselected, C = Unselected, S = Unselected> {
294    bin: PathBuf,
295
296    global_opts: GlobalOpts,
297
298    mode: M,
299
300    permission_mode: P,
301
302    changelist_mode: C,
303
304    job_status_mode: S,
305}
306
307// ---------------------------------------------------------------------------
308// Entry points
309// ---------------------------------------------------------------------------
310
311impl Change<Unselected, Unselected, Unselected, Unselected> {
312    /// Creates a new `p4 change` command.
313    ///
314    /// `bin` is the path to the Perforce command-line executable.
315    pub fn new(bin: impl Into<PathBuf>, global_opts: GlobalOpts) -> Self {
316        Self {
317            bin: bin.into(),
318            global_opts,
319            mode: Unselected,
320            permission_mode: Unselected,
321            changelist_mode: Unselected,
322            job_status_mode: Unselected,
323        }
324    }
325
326    /// Enters the local delete form: `p4 change -d changelist`.
327    pub fn delete(self) -> Change<DeleteMode<LocalDeleteMode>, Unselected, Unselected, Unselected> {
328        self.with_mode(DeleteMode {
329            loc: LocalDeleteMode,
330        })
331    }
332
333    /// Enters the remote delete form.
334    ///
335    /// `-f` is fixed in this form and is injected together with the form;
336    /// none of the other options are accepted.
337    ///
338    /// `server_id` is the server id of the commit server the changelist's
339    /// client is bound to.
340    #[cfg(not(feature = "lt2015_2"))]
341    pub fn server_id(
342        self,
343        server_id: impl Into<String>,
344    ) -> Change<DeleteMode<RemoteDeleteMode>, Unselected, Unselected, Unselected> {
345        self.with_mode(DeleteMode {
346            loc: RemoteDeleteMode {
347                server_id: server_id.into(),
348            },
349        })
350    }
351
352    /// Enters the stdout form: `p4 change -o`.
353    pub fn stdout(self) -> Change<StdoutMode, Unselected, Unselected, Unselected> {
354        self.with_mode(StdoutMode)
355    }
356
357    /// Enters the stdin form: `p4 change -i`.
358    pub fn stdin(self) -> Change<StdinMode, Unselected, Unselected, Unselected> {
359        self.with_mode(StdinMode)
360    }
361
362    /// Enters the change-type form: `p4 change -t restricted | public
363    /// changelist`.
364    ///
365    /// `visibility` selects the new changelist type; the optional new owner
366    /// can be added later with the `user` builder of the entered form.
367    pub fn visibility(
368        self,
369        visibility: Visibility,
370    ) -> Change<UserOrVisibilityMode, Unselected, Unselected, Unselected> {
371        self.with_mode(UserOrVisibilityMode {
372            visibility: Some(visibility),
373            user: None,
374        })
375    }
376
377    /// Enters the owner-transfer form: `p4 change -U user changelist`.
378    ///
379    /// `user` is the new owner of the empty pending changelist; the optional
380    /// visibility can be added later with the `visibility` builder of the
381    /// entered form.
382    pub fn user(
383        self,
384        user: impl Into<String>,
385    ) -> Change<UserOrVisibilityMode, Unselected, Unselected, Unselected> {
386        self.with_mode(UserOrVisibilityMode {
387            user: Some(user.into()),
388            visibility: None,
389        })
390    }
391
392    /// Selects `-s` and enters the plain edit form.
393    pub fn arbitrary_job_status(
394        self,
395    ) -> Change<RegularOperationMode, Unselected, Unselected, ArbitraryJobStatus> {
396        Change {
397            bin: self.bin,
398            global_opts: self.global_opts,
399            mode: RegularOperationMode,
400            permission_mode: Unselected,
401            changelist_mode: Unselected,
402            job_status_mode: ArbitraryJobStatus,
403        }
404    }
405
406    /// Selects `-f` and enters the plain edit form.
407    pub fn force(self) -> Change<RegularOperationMode, ForceOperationMode, Unselected, Unselected> {
408        Change {
409            bin: self.bin,
410            global_opts: self.global_opts,
411            mode: RegularOperationMode,
412            permission_mode: ForceOperationMode,
413            changelist_mode: Unselected,
414            job_status_mode: Unselected,
415        }
416    }
417
418    /// Selects `-u` and enters the plain edit form.
419    pub fn update(
420        self,
421    ) -> Change<RegularOperationMode, UpdateOperationMode, Unselected, Unselected> {
422        Change {
423            bin: self.bin,
424            global_opts: self.global_opts,
425            mode: RegularOperationMode,
426            permission_mode: UpdateOperationMode,
427            changelist_mode: Unselected,
428            job_status_mode: Unselected,
429        }
430    }
431
432    /// Selects `-O` and enters the plain edit form.
433    pub fn original(
434        self,
435    ) -> Change<RegularOperationMode, Unselected, OriginalChangeListMode, Unselected> {
436        Change {
437            bin: self.bin,
438            global_opts: self.global_opts,
439            mode: RegularOperationMode,
440            permission_mode: Unselected,
441            changelist_mode: OriginalChangeListMode,
442            job_status_mode: Unselected,
443        }
444    }
445
446    /// Selects `-I` and enters the plain edit form.
447    #[cfg(not(feature = "lt2022_1"))]
448    pub fn identity(
449        self,
450    ) -> Change<RegularOperationMode, Unselected, IdentityChangeListMode, Unselected> {
451        Change {
452            bin: self.bin,
453            global_opts: self.global_opts,
454            mode: RegularOperationMode,
455            permission_mode: Unselected,
456            changelist_mode: IdentityChangeListMode,
457            job_status_mode: Unselected,
458        }
459    }
460}
461
462impl<M, P, C, S> Change<M, P, C, S> {
463    /// Rebuilds the command with another form while preserving the selected
464    /// options and the `bin` / global options.
465    fn with_mode<OTHERMODE>(self, mode: OTHERMODE) -> Change<OTHERMODE, P, C, S> {
466        Change {
467            bin: self.bin,
468            global_opts: self.global_opts,
469            mode,
470            permission_mode: self.permission_mode,
471            changelist_mode: self.changelist_mode,
472            job_status_mode: self.job_status_mode,
473        }
474    }
475}
476
477// ---------------------------------------------------------------------------
478// Shared accessors
479// ---------------------------------------------------------------------------
480
481impl<M, P, C, S> Change<M, P, C, S> {
482    /// Get the Perforce command path.
483    pub fn get_bin(&self) -> &Path {
484        &self.bin
485    }
486
487    /// Set the Perforce command path.
488    pub fn set_bin(&mut self, bin: impl Into<PathBuf>) -> &mut Self {
489        self.bin = bin.into();
490        self
491    }
492
493    /// Set the Perforce command path.
494    pub fn bin(mut self, bin: impl Into<PathBuf>) -> Self {
495        self.set_bin(bin);
496        self
497    }
498
499    /// Returns the global options of this command.
500    pub fn get_global_opts(&self) -> &GlobalOpts {
501        &self.global_opts
502    }
503
504    /// Sets the global options of this command.
505    pub fn set_global_opts(&mut self, v: GlobalOpts) -> &mut Self {
506        self.global_opts = v;
507        self
508    }
509
510    /// Builder for the global options of this command.
511    pub fn global_opts(mut self, v: GlobalOpts) -> Self {
512        self.global_opts = v;
513        self
514    }
515}
516
517/// Operation modes allowed to enter the `-o` form.
518///
519/// Only `Unselected` and [`ForceOperationMode`] are allowed: `-o` does not
520/// accept `-u`.
521#[doc(hidden)]
522pub trait TransferStdoutMode {}
523
524impl TransferStdoutMode for Unselected {}
525impl TransferStdoutMode for ForceOperationMode {}
526
527// ---------------------------------------------------------------------------
528// RegularOperationMode: selecting options and transitioning to other forms
529// ---------------------------------------------------------------------------
530
531impl<P, C, S> Change<RegularOperationMode, P, C, S> {
532    /// Transitions to the local delete form (`-d`), preserving every
533    /// selected option accepted by that form.
534    ///
535    /// Available when no `-u` or `-I` is selected (`-d` accepts only `-f`,
536    /// `-s`, and `-O`).
537    pub fn delete(self) -> Change<DeleteMode<LocalDeleteMode>, P, C, S>
538    where
539        P: TransferDeleteMode,
540        C: TransferDeleteMode,
541    {
542        Change {
543            bin: self.bin,
544            global_opts: self.global_opts,
545            mode: DeleteMode {
546                loc: LocalDeleteMode,
547            },
548            permission_mode: self.permission_mode,
549            changelist_mode: self.changelist_mode,
550            job_status_mode: self.job_status_mode,
551        }
552    }
553
554    /// Transitions to the stdout form (`-o`), preserving every selected
555    /// option accepted by that form.
556    ///
557    /// Available when no `-u` is selected (`-o` accepts `-f`, `-s`, `-O`,
558    /// and `-I`).
559    pub fn stdout(self) -> Change<StdoutMode, P, C, S>
560    where
561        P: TransferStdoutMode,
562    {
563        Change {
564            bin: self.bin,
565            global_opts: self.global_opts,
566            mode: StdoutMode,
567            permission_mode: self.permission_mode,
568            changelist_mode: self.changelist_mode,
569            job_status_mode: self.job_status_mode,
570        }
571    }
572}
573
574// Regular -> remote delete: no `-u`, `-s`, `-O`, or `-I`; `-f` is optional
575// (it is folded into the form's built-in `-f`).
576#[cfg(not(feature = "lt2015_2"))]
577impl<P: TransferRemoteDeleteMode> Change<RegularOperationMode, P, Unselected, Unselected> {
578    /// Transitions to the remote delete form.
579    ///
580    /// Available from the plain edit form as long as neither `-u`, `-s`,
581    /// `-O`, nor `-I` is selected. A previously selected `-f` is folded
582    /// into the form's built-in `-f`.
583    ///
584    /// `server_id` is the server id of the commit server the changelist's
585    /// client is bound to.
586    pub fn server_id(
587        self,
588        server_id: impl Into<String>,
589    ) -> Change<DeleteMode<RemoteDeleteMode>, Unselected, Unselected, Unselected> {
590        Change {
591            bin: self.bin,
592            global_opts: self.global_opts,
593            mode: DeleteMode {
594                loc: RemoteDeleteMode {
595                    server_id: server_id.into(),
596                },
597            },
598            permission_mode: Unselected,
599            changelist_mode: Unselected,
600            job_status_mode: Unselected,
601        }
602    }
603}
604
605// Regular -> stdin: the changelist reference must be unset (`-i` takes no
606// changelist argument and accepts neither `-O` nor `-I`).
607impl<O, S> Change<RegularOperationMode, O, Unselected, S> {
608    /// Transitions to the stdin form (`-i`), preserving every selected
609    /// option accepted by that form.
610    ///
611    /// Available only when no `-O` or `-I` is selected (`-i` accepts `-s`,
612    /// `-f`, and `-u`, but no changelist reference).
613    pub fn stdin(self) -> Change<StdinMode, O, Unselected, S> {
614        Change {
615            bin: self.bin,
616            global_opts: self.global_opts,
617            mode: StdinMode,
618            permission_mode: self.permission_mode,
619            changelist_mode: Unselected,
620            job_status_mode: self.job_status_mode,
621        }
622    }
623}
624
625// -s within the edit form
626impl<O, C> Change<RegularOperationMode, O, C, Unselected> {
627    /// Selects `-s`: jobs may be assigned arbitrary status values on
628    /// submission.
629    pub fn arbitrary_job_status(self) -> Change<RegularOperationMode, O, C, ArbitraryJobStatus> {
630        Change {
631            bin: self.bin,
632            global_opts: self.global_opts,
633            mode: self.mode,
634            permission_mode: self.permission_mode,
635            changelist_mode: self.changelist_mode,
636            job_status_mode: ArbitraryJobStatus,
637        }
638    }
639}
640
641// -f / -u within the edit form
642impl<C, S> Change<RegularOperationMode, Unselected, C, S> {
643    /// Selects `-f` (force editing of submitted changelists).
644    ///
645    /// Mutually exclusive with `-u`.
646    pub fn force(self) -> Change<RegularOperationMode, ForceOperationMode, C, S> {
647        Change {
648            bin: self.bin,
649            global_opts: self.global_opts,
650            mode: self.mode,
651            permission_mode: ForceOperationMode,
652            changelist_mode: self.changelist_mode,
653            job_status_mode: self.job_status_mode,
654        }
655    }
656
657    /// Selects `-u` (update a submitted changelist).
658    ///
659    /// Mutually exclusive with `-f`.
660    pub fn update(self) -> Change<RegularOperationMode, UpdateOperationMode, C, S> {
661        Change {
662            bin: self.bin,
663            global_opts: self.global_opts,
664            mode: self.mode,
665            permission_mode: UpdateOperationMode,
666            changelist_mode: self.changelist_mode,
667            job_status_mode: self.job_status_mode,
668        }
669    }
670}
671
672// -O / -I within the edit form
673impl<O, S> Change<RegularOperationMode, O, Unselected, S> {
674    /// Selects `-O`: interpret the changelist number as the original,
675    /// pre-renumber number.
676    ///
677    /// Mutually exclusive with `-I`.
678    pub fn original(self) -> Change<RegularOperationMode, O, OriginalChangeListMode, S> {
679        Change {
680            bin: self.bin,
681            global_opts: self.global_opts,
682            mode: self.mode,
683            permission_mode: self.permission_mode,
684            changelist_mode: OriginalChangeListMode,
685            job_status_mode: self.job_status_mode,
686        }
687    }
688
689    /// Selects `-I`: interpret the changelist number as the Identity field.
690    ///
691    /// Mutually exclusive with `-O`.
692    #[cfg(not(feature = "lt2022_1"))]
693    pub fn identity(self) -> Change<RegularOperationMode, O, IdentityChangeListMode, S> {
694        Change {
695            bin: self.bin,
696            global_opts: self.global_opts,
697            mode: self.mode,
698            permission_mode: self.permission_mode,
699            changelist_mode: IdentityChangeListMode,
700            job_status_mode: self.job_status_mode,
701        }
702    }
703}
704
705// Regular -> change type / owner transfer: `-s` must be unset; at most one
706// of `-f`/`-u`/`-O`/`-I` may be selected and maps to the corresponding
707// `Only*` state.
708impl<O, C> Change<RegularOperationMode, O, C, Unselected> {
709    /// Transitions to the change-type form (`-t type`), preserving a
710    /// selected `-f`, `-u`, `-O`, or `-I` as the form's single allowed
711    /// extra option.
712    ///
713    /// Available only when `-s` is not selected (the `-t` / `-U` form does
714    /// not accept it), and at most one of `-f`, `-u`, `-O`, `-I` is
715    /// selected.
716    ///
717    /// `visibility` selects the new changelist type.
718    #[allow(clippy::type_complexity)]
719    pub fn visibility(
720        self,
721        visibility: Visibility,
722    ) -> Change<
723        UserOrVisibilityMode,
724        <(O, C) as TransferUVMode>::P,
725        <(O, C) as TransferUVMode>::C,
726        Unselected,
727    >
728    where
729        (O, C): TransferUVMode,
730    {
731        let (operation, change_ref) = (self.permission_mode, self.changelist_mode).transfer();
732        Change {
733            bin: self.bin,
734            global_opts: self.global_opts,
735            mode: UserOrVisibilityMode {
736                visibility: Some(visibility),
737                user: None,
738            },
739            permission_mode: operation,
740            changelist_mode: change_ref,
741            job_status_mode: Unselected,
742        }
743    }
744
745    /// Transitions to the owner-transfer form (`-U user`), preserving a
746    /// selected `-f`, `-u`, `-O`, or `-I` as the form's single allowed
747    /// extra option.
748    ///
749    /// Available only when `-s` is not selected (the `-t` / `-U` form does
750    /// not accept it), and at most one of `-f`, `-u`, `-O`, `-I` is
751    /// selected.
752    ///
753    /// `user` is the new owner of the empty pending changelist.
754    #[allow(clippy::type_complexity)]
755    pub fn user(
756        self,
757        user: impl Into<String>,
758    ) -> Change<
759        UserOrVisibilityMode,
760        <(O, C) as TransferUVMode>::P,
761        <(O, C) as TransferUVMode>::C,
762        Unselected,
763    >
764    where
765        (O, C): TransferUVMode,
766    {
767        let (operation, change_ref) = (self.permission_mode, self.changelist_mode).transfer();
768        Change {
769            bin: self.bin,
770            global_opts: self.global_opts,
771            mode: UserOrVisibilityMode {
772                user: Some(user.into()),
773                visibility: None,
774            },
775            permission_mode: operation,
776            changelist_mode: change_ref,
777            job_status_mode: Unselected,
778        }
779    }
780}
781
782// ---------------------------------------------------------------------------
783// SubCommand
784// ---------------------------------------------------------------------------
785
786impl<M, O, C, S> SubCommand for Change<M, O, C, S>
787where
788    M: ExclusiveOption,
789    O: ExclusiveOption,
790    C: ExclusiveOption,
791    S: ExclusiveOption,
792{
793    fn name(&self) -> &str {
794        "change"
795    }
796
797    fn inject_local_args(&self, command: &mut Command) {
798        self.mode.inject_args(command);
799        self.job_status_mode.inject_args(command);
800        self.permission_mode.inject_args(command);
801        self.changelist_mode.inject_args(command);
802    }
803
804    fn global_opts(&self) -> Option<&GlobalOpts> {
805        Some(&self.global_opts)
806    }
807}
808
809// ---------------------------------------------------------------------------
810// Spawning
811// ---------------------------------------------------------------------------
812
813impl<M, O, C, S, I> ParameterizedSpawn<(I,)> for Change<M, O, C, S>
814where
815    M: ExclusiveOption + TakesChangeList,
816    O: ExclusiveOption,
817    C: ExclusiveOption,
818    S: ExclusiveOption,
819    I: AsRef<OsStr>,
820{
821    type Output = Child;
822    type Error = std::io::Error;
823
824    /// Spawns `p4 change` with the given `changelist` argument as a child
825    /// process with piped standard output and error streams; use the
826    /// returned [`Child`] handle to wait for it or interact with it.
827    ///
828    /// The changelist argument is a changelist number, optionally prefixed
829    /// by the form's reference option semantics (for example an original
830    /// pre-renumber number under `-O`).
831    fn spawn_with(&mut self, (changelist,): (I,)) -> Result<Self::Output, Self::Error> {
832        self.setup_command(&self.bin)
833            .arg(changelist)
834            .stdout(Stdio::piped())
835            .stderr(Stdio::piped())
836            .spawn()
837    }
838}
839
840// RegularOperationMode accepts an optional changelist: without one it
841// opens the editor to create a new pending changelist.
842impl<O, C, S> ParameterizedSpawn<()> for Change<RegularOperationMode, O, C, S>
843where
844    O: ExclusiveOption,
845    C: ExclusiveOption,
846    S: ExclusiveOption,
847{
848    type Output = Child;
849    type Error = std::io::Error;
850
851    /// Spawns `p4 change` (the plain edit form) without a changelist
852    /// argument, opening the editor to create a new pending changelist.
853    fn spawn_with(&mut self, _: ()) -> Result<Self::Output, Self::Error> {
854        self.setup_command(&self.bin)
855            .stdout(Stdio::piped())
856            .stderr(Stdio::piped())
857            .spawn()
858    }
859}
860
861// StdoutMode accepts an optional changelist: without one it dumps the
862// default changelist specification.
863impl<O, C, S> ParameterizedSpawn<()> for Change<StdoutMode, O, C, S>
864where
865    O: ExclusiveOption,
866    C: ExclusiveOption,
867    S: ExclusiveOption,
868{
869    type Output = Child;
870    type Error = std::io::Error;
871
872    /// Spawns `p4 change -o` without a changelist argument, writing the
873    /// default changelist specification to standard output.
874    fn spawn_with(&mut self, _: ()) -> Result<Self::Output, Self::Error> {
875        self.setup_command(&self.bin)
876            .stdout(Stdio::piped())
877            .stderr(Stdio::piped())
878            .spawn()
879    }
880}
881
882impl<O, S> ParameterizedSpawn<()> for Change<StdinMode, O, Unselected, S>
883where
884    O: ExclusiveOption,
885    S: ExclusiveOption,
886{
887    type Output = Child;
888    type Error = std::io::Error;
889
890    /// Spawns `p4 change -i` with a piped standard input stream (and piped
891    /// standard output and error streams); write the changelist
892    /// specification to the returned [`Child`]'s stdin handle.
893    fn spawn_with(&mut self, _: ()) -> Result<Self::Output, Self::Error> {
894        self.setup_command(&self.bin)
895            .stdin(Stdio::piped())
896            .stdout(Stdio::piped())
897            .stderr(Stdio::piped())
898            .spawn()
899    }
900}
901
902impl<O, S> ParameterizedSpawn<(Stdio,)> for Change<StdinMode, O, Unselected, S>
903where
904    O: ExclusiveOption,
905    S: ExclusiveOption,
906{
907    type Output = Child;
908    type Error = std::io::Error;
909
910    /// Spawns `p4 change -i` with the given standard input configuration
911    /// and piped standard output and error streams; use the returned
912    /// [`Child`] handle to wait for it or interact with it.
913    ///
914    /// Pass `Stdio::piped()` to obtain a writable `child.stdin` handle and
915    /// feed the changelist specification into it; pass `Stdio::inherit()`
916    /// to read the specification from the spawning process's own standard
917    /// input.
918    fn spawn_with(&mut self, (stdio,): (Stdio,)) -> Result<Self::Output, Self::Error> {
919        self.setup_command(&self.bin)
920            .stdin(stdio)
921            .stdout(Stdio::piped())
922            .stderr(Stdio::piped())
923            .spawn()
924    }
925}
926
927#[cfg(test)]
928mod tests {
929    use super::*;
930    use crate::cmd::args_of;
931
932    #[test]
933    fn bin_accessors_update_executable_path() {
934        let mut command = Change::new("p4", GlobalOpts::new());
935        assert_eq!(command.get_bin(), Path::new("p4"));
936
937        command.set_bin("/usr/local/bin/p4");
938        assert_eq!(command.get_bin(), Path::new("/usr/local/bin/p4"));
939
940        let command = command.bin("p4.exe");
941        assert_eq!(command.get_bin(), Path::new("p4.exe"));
942    }
943
944    // Compile-time rejection matrix. Every transition below was verified to
945    // fail compilation (trait bound not satisfied or method not found):
946    //
947    // - `.update().delete()`             (`-u` is not accepted by `-d`)
948    // - `.identity().delete()`           (`-I` is not accepted by `-d`)
949    // - `.update().stdout()`             (`-u` is not accepted by `-o`)
950    // - `.original().stdin()`            (`-O` is not accepted by `-i`)
951    // - `.arbitrary_job_status().visibility(..)`   (`-s` rejected by `-t`/`-U`)
952    // - `.force().original().visibility(..)`       (`-f` + `-O` together)
953    // - `.arbitrary_job_status().user(..)`         (`-s` rejected by `-t`/`-U`)
954    // - `.force().original().user(..)`             (`-f` + `-O` together)
955    // - `.force().arbitrary_job_status().server_id(..)` (`-s` rejected by remote `-d`)
956    // - `.visibility(..).force().update()` (a second option in the `-t`/`-U` form)
957    // - `.user(..).original().identity()`  (a second option in the `-t`/`-U` form)
958    // - spawning `.stdin()` with a `changelist` argument (no positional arg)
959
960    /// Dry-run checks of the assembled `p4 change` command line; no process
961    /// is spawned.
962    #[test]
963    fn regular_form_injects_all_options() {
964        let change = Change::new("p4", GlobalOpts::new())
965            .arbitrary_job_status()
966            .force()
967            .original();
968
969        assert_eq!(
970            args_of(&change.setup_command("p4")),
971            ["change", "-s", "-f", "-O"]
972        );
973    }
974
975    #[test]
976    #[cfg(not(feature = "lt2022_1"))]
977    fn regular_form_update_and_identity() {
978        let change = Change::new("p4", GlobalOpts::new())
979            .arbitrary_job_status()
980            .update()
981            .identity();
982
983        assert_eq!(
984            args_of(&change.setup_command("p4")),
985            ["change", "-s", "-u", "-I"]
986        );
987    }
988
989    #[test]
990    fn entry_points_select_form_flags() {
991        let delete = Change::new("p4", GlobalOpts::new()).delete();
992        assert_eq!(args_of(&delete.setup_command("p4")), ["change", "-d"]);
993
994        let stdout = Change::new("p4", GlobalOpts::new()).stdout();
995        assert_eq!(args_of(&stdout.setup_command("p4")), ["change", "-o"]);
996
997        let stdin = Change::new("p4", GlobalOpts::new()).stdin();
998        assert_eq!(args_of(&stdin.setup_command("p4")), ["change", "-i"]);
999
1000        #[cfg(not(feature = "lt2015_2"))]
1001        {
1002            let remote = Change::new("p4", GlobalOpts::new()).server_id("remote-1");
1003            #[cfg(feature = "lt2022_1")]
1004            assert_eq!(
1005                args_of(&remote.setup_command("p4")),
1006                ["change", "-d", "-f", "--server=remote-1"]
1007            );
1008            #[cfg(not(feature = "lt2022_1"))]
1009            assert_eq!(
1010                args_of(&remote.setup_command("p4")),
1011                ["change", "-d", "-f", "--serverid=remote-1"]
1012            );
1013        }
1014    }
1015
1016    #[test]
1017    fn arbitrary_job_status_carries_into_delete_stdout_stdin() {
1018        // -s is accepted by the -d, -o, and -i forms.
1019        let delete = Change::new("p4", GlobalOpts::new())
1020            .arbitrary_job_status()
1021            .delete();
1022        assert_eq!(args_of(&delete.setup_command("p4")), ["change", "-d", "-s"]);
1023
1024        let stdout = Change::new("p4", GlobalOpts::new())
1025            .arbitrary_job_status()
1026            .stdout();
1027        assert_eq!(args_of(&stdout.setup_command("p4")), ["change", "-o", "-s"]);
1028
1029        let stdin = Change::new("p4", GlobalOpts::new())
1030            .arbitrary_job_status()
1031            .stdin();
1032        assert_eq!(args_of(&stdin.setup_command("p4")), ["change", "-i", "-s"]);
1033    }
1034
1035    #[test]
1036    fn regular_to_delete_preserves_compatible_options() {
1037        let change = Change::new("p4", GlobalOpts::new())
1038            .arbitrary_job_status()
1039            .force()
1040            .original()
1041            .delete();
1042
1043        assert_eq!(
1044            args_of(&change.setup_command("p4")),
1045            ["change", "-d", "-s", "-f", "-O"]
1046        );
1047    }
1048
1049    #[test]
1050    #[cfg(not(feature = "lt2022_1"))]
1051    fn regular_to_stdout_preserves_identity() {
1052        let change = Change::new("p4", GlobalOpts::new())
1053            .arbitrary_job_status()
1054            .force()
1055            .identity()
1056            .stdout();
1057
1058        assert_eq!(
1059            args_of(&change.setup_command("p4")),
1060            ["change", "-o", "-s", "-f", "-I"]
1061        );
1062    }
1063
1064    #[test]
1065    fn regular_to_stdin_preserves_update() {
1066        let change = Change::new("p4", GlobalOpts::new())
1067            .arbitrary_job_status()
1068            .update()
1069            .stdin();
1070
1071        assert_eq!(
1072            args_of(&change.setup_command("p4")),
1073            ["change", "-i", "-s", "-u"]
1074        );
1075    }
1076
1077    #[test]
1078    #[cfg(not(feature = "lt2015_2"))]
1079    fn regular_to_remote_delete_folds_force_into_builtin() {
1080        // `-f` selected in the edit form is folded into the remote delete
1081        // form's built-in `-f`.
1082        let change = Change::new("p4", GlobalOpts::new())
1083            .force()
1084            .server_id("remote-1");
1085
1086        #[cfg(feature = "lt2022_1")]
1087        assert_eq!(
1088            args_of(&change.setup_command("p4")),
1089            ["change", "-d", "-f", "--server=remote-1"]
1090        );
1091        #[cfg(not(feature = "lt2022_1"))]
1092        assert_eq!(
1093            args_of(&change.setup_command("p4")),
1094            ["change", "-d", "-f", "--serverid=remote-1"]
1095        );
1096    }
1097
1098    #[test]
1099    fn regular_visibility_maps_to_only_states() {
1100        // -f maps to the only-force operation
1101        let forced = Change::new("p4", GlobalOpts::new())
1102            .force()
1103            .visibility(Visibility::Restricted);
1104        assert_eq!(
1105            args_of(&forced.setup_command("p4")),
1106            ["change", "-t", "restricted", "-f"]
1107        );
1108
1109        // -I maps to the only-identity reference
1110        #[cfg(not(feature = "lt2022_1"))]
1111        {
1112            let identity = Change::new("p4", GlobalOpts::new())
1113                .identity()
1114                .visibility(Visibility::Public);
1115            assert_eq!(
1116                args_of(&identity.setup_command("p4")),
1117                ["change", "-t", "public", "-I"]
1118            );
1119        }
1120
1121        // no extra option
1122        let plain = Change::new("p4", GlobalOpts::new())
1123            .visibility(Visibility::Restricted)
1124            .user("maria");
1125        assert_eq!(
1126            args_of(&plain.setup_command("p4")),
1127            ["change", "-t", "restricted", "-U", "maria"]
1128        );
1129    }
1130
1131    #[test]
1132    fn regular_user_maps_single_option() {
1133        // -f maps to the only-force operation
1134        let change = Change::new("p4", GlobalOpts::new())
1135            .force()
1136            .user("maria")
1137            .visibility(Visibility::Public);
1138
1139        assert_eq!(
1140            args_of(&change.setup_command("p4")),
1141            ["change", "-t", "public", "-U", "maria", "-f"]
1142        );
1143
1144        // any other single option (`-u`, `-O`, `-I`) maps the same way
1145        let original = Change::new("p4", GlobalOpts::new())
1146            .original()
1147            .user("maria");
1148
1149        assert_eq!(
1150            args_of(&original.setup_command("p4")),
1151            ["change", "-U", "maria", "-O"]
1152        );
1153    }
1154
1155    #[test]
1156    fn user_or_visibility_form_sets_values_and_selects_one_option() {
1157        // Both form values can be set from either entry point.
1158        let both = Change::new("p4", GlobalOpts::new())
1159            .user("maria")
1160            .visibility(Visibility::Public);
1161        assert_eq!(
1162            args_of(&both.setup_command("p4")),
1163            ["change", "-t", "public", "-U", "maria"]
1164        );
1165
1166        // The form accepts one of `-f`, `-u`, `-O`, `-I`.
1167        let forced = Change::new("p4", GlobalOpts::new())
1168            .visibility(Visibility::Restricted)
1169            .force();
1170        assert_eq!(
1171            args_of(&forced.setup_command("p4")),
1172            ["change", "-t", "restricted", "-f"]
1173        );
1174
1175        let updated = Change::new("p4", GlobalOpts::new()).user("maria").update();
1176        assert_eq!(
1177            args_of(&updated.setup_command("p4")),
1178            ["change", "-U", "maria", "-u"]
1179        );
1180
1181        let original = Change::new("p4", GlobalOpts::new())
1182            .user("maria")
1183            .original();
1184        assert_eq!(
1185            args_of(&original.setup_command("p4")),
1186            ["change", "-U", "maria", "-O"]
1187        );
1188
1189        #[cfg(not(feature = "lt2015_2"))]
1190        {
1191            let identity = Change::new("p4", GlobalOpts::new())
1192                .visibility(Visibility::Public)
1193                .identity();
1194            assert_eq!(
1195                args_of(&identity.setup_command("p4")),
1196                ["change", "-t", "public", "-I"]
1197            );
1198        }
1199    }
1200
1201    #[test]
1202    fn options_can_be_selected_inside_forms() {
1203        let delete = Change::new("p4", GlobalOpts::new())
1204            .delete()
1205            .arbitrary_job_status()
1206            .force()
1207            .original();
1208        assert_eq!(
1209            args_of(&delete.setup_command("p4")),
1210            ["change", "-d", "-s", "-f", "-O"]
1211        );
1212
1213        #[cfg(not(feature = "lt2022_1"))]
1214        {
1215            let stdout = Change::new("p4", GlobalOpts::new())
1216                .stdout()
1217                .arbitrary_job_status()
1218                .identity();
1219            assert_eq!(
1220                args_of(&stdout.setup_command("p4")),
1221                ["change", "-o", "-s", "-I"]
1222            );
1223        }
1224        #[cfg(feature = "lt2022_1")]
1225        {
1226            let stdout = Change::new("p4", GlobalOpts::new())
1227                .stdout()
1228                .arbitrary_job_status()
1229                .original();
1230            assert_eq!(
1231                args_of(&stdout.setup_command("p4")),
1232                ["change", "-o", "-s", "-O"]
1233            );
1234        }
1235
1236        let stdin = Change::new("p4", GlobalOpts::new())
1237            .stdin()
1238            .arbitrary_job_status()
1239            .update();
1240        assert_eq!(
1241            args_of(&stdin.setup_command("p4")),
1242            ["change", "-i", "-s", "-u"]
1243        );
1244    }
1245
1246    #[test]
1247    fn global_opts_are_injected_once() {
1248        let change = Change::new("p4", GlobalOpts::new()).delete();
1249
1250        // Only the command name without any global opts by default.
1251        assert_eq!(args_of(&change.setup_command("p4")), ["change", "-d"]);
1252    }
1253
1254    // Spawn argument counts per form. Each call below mirrors what the
1255    // corresponding `ParameterizedSpawn` impl assembles: `setup_command`
1256    // injects the form flags, and the one-argument impls append the
1257    // changelist positional. The forms that take no positional argument
1258    // (Regular without a changelist, Stdout without a changelist, and
1259    // Stdin) are checked against `setup_command` directly.
1260    #[test]
1261    fn spawn_argument_counts_per_form() {
1262        // Regular: 0 or 1 changelist.
1263        let plain = Change::new("p4", GlobalOpts::new())
1264            .arbitrary_job_status()
1265            .force();
1266        assert_eq!(args_of(&plain.setup_command("p4")), ["change", "-s", "-f"]);
1267        let mut with_cl = plain.setup_command("p4");
1268        with_cl.arg("1234");
1269        assert_eq!(args_of(&with_cl), ["change", "-s", "-f", "1234"]);
1270
1271        // DeleteMode (local): exactly 1 changelist.
1272        let delete = Change::new("p4", GlobalOpts::new())
1273            .delete()
1274            .arbitrary_job_status()
1275            .force()
1276            .original();
1277        let mut delete_cl = delete.setup_command("p4");
1278        delete_cl.arg("1234");
1279        assert_eq!(
1280            args_of(&delete_cl),
1281            ["change", "-d", "-s", "-f", "-O", "1234"]
1282        );
1283
1284        // DeleteMode (remote): exactly 1 changelist.
1285        #[cfg(not(feature = "lt2015_2"))]
1286        {
1287            let remote = Change::new("p4", GlobalOpts::new()).server_id("edge-1");
1288            let mut remote_cl = remote.setup_command("p4");
1289            remote_cl.arg("1234");
1290            #[cfg(feature = "lt2022_1")]
1291            assert_eq!(
1292                args_of(&remote_cl),
1293                ["change", "-d", "-f", "--server=edge-1", "1234"]
1294            );
1295            #[cfg(not(feature = "lt2022_1"))]
1296            assert_eq!(
1297                args_of(&remote_cl),
1298                ["change", "-d", "-f", "--serverid=edge-1", "1234"]
1299            );
1300        }
1301
1302        // Stdout: 0 or 1 changelist.
1303        #[cfg(not(feature = "lt2022_1"))]
1304        {
1305            let stdout = Change::new("p4", GlobalOpts::new())
1306                .stdout()
1307                .arbitrary_job_status()
1308                .force()
1309                .identity();
1310            assert_eq!(
1311                args_of(&stdout.setup_command("p4")),
1312                ["change", "-o", "-s", "-f", "-I"]
1313            );
1314            let mut stdout_cl = stdout.setup_command("p4");
1315            stdout_cl.arg("1234");
1316            assert_eq!(
1317                args_of(&stdout_cl),
1318                ["change", "-o", "-s", "-f", "-I", "1234"]
1319            );
1320        }
1321        #[cfg(feature = "lt2022_1")]
1322        {
1323            let stdout = Change::new("p4", GlobalOpts::new())
1324                .stdout()
1325                .arbitrary_job_status()
1326                .force()
1327                .original();
1328            assert_eq!(
1329                args_of(&stdout.setup_command("p4")),
1330                ["change", "-o", "-s", "-f", "-O"]
1331            );
1332            let mut stdout_cl = stdout.setup_command("p4");
1333            stdout_cl.arg("1234");
1334            assert_eq!(
1335                args_of(&stdout_cl),
1336                ["change", "-o", "-s", "-f", "-O", "1234"]
1337            );
1338        }
1339
1340        // Stdin: no changelist; stdin is configured separately.
1341        let stdin = Change::new("p4", GlobalOpts::new())
1342            .stdin()
1343            .arbitrary_job_status()
1344            .update();
1345        assert_eq!(
1346            args_of(&stdin.setup_command("p4")),
1347            ["change", "-i", "-s", "-u"]
1348        );
1349
1350        // UserOrVisibility: exactly 1 changelist.
1351        let uov = Change::new("p4", GlobalOpts::new())
1352            .visibility(Visibility::Restricted)
1353            .user("maria")
1354            .force();
1355        let mut uov_cl = uov.setup_command("p4");
1356        uov_cl.arg("1234");
1357        assert_eq!(
1358            args_of(&uov_cl),
1359            ["change", "-t", "restricted", "-U", "maria", "-f", "1234"]
1360        );
1361    }
1362}