Skip to main content

perforce_cli/cmd/
change.rs

1use std::{
2    ffi::OsStr,
3    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/// - `F`: 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    /// Returns the global options of this command.
483    pub fn get_global_opts(&self) -> &GlobalOpts {
484        &self.global_opts
485    }
486
487    /// Sets the global options of this command.
488    pub fn set_global_opts(&mut self, v: GlobalOpts) -> &mut Self {
489        self.global_opts = v;
490        self
491    }
492
493    /// Builder for the global options of this command.
494    pub fn global_opts(mut self, v: GlobalOpts) -> Self {
495        self.global_opts = v;
496        self
497    }
498}
499
500/// Operation modes allowed to enter the `-o` form.
501///
502/// Only `Unselected` and [`ForceOperationMode`] are allowed: `-o` does not
503/// accept `-u`.
504#[doc(hidden)]
505pub trait TransferStdoutMode {}
506
507impl TransferStdoutMode for Unselected {}
508impl TransferStdoutMode for ForceOperationMode {}
509
510// ---------------------------------------------------------------------------
511// RegularOperationMode: selecting options and transitioning to other forms
512// ---------------------------------------------------------------------------
513
514impl<P, C, S> Change<RegularOperationMode, P, C, S> {
515    /// Transitions to the local delete form (`-d`), preserving every
516    /// selected option accepted by that form.
517    ///
518    /// Available when no `-u` or `-I` is selected (`-d` accepts only `-f`,
519    /// `-s`, and `-O`).
520    pub fn delete(self) -> Change<DeleteMode<LocalDeleteMode>, P, C, S>
521    where
522        P: TransferDeleteMode,
523        C: TransferDeleteMode,
524    {
525        Change {
526            bin: self.bin,
527            global_opts: self.global_opts,
528            mode: DeleteMode {
529                loc: LocalDeleteMode,
530            },
531            permission_mode: self.permission_mode,
532            changelist_mode: self.changelist_mode,
533            job_status_mode: self.job_status_mode,
534        }
535    }
536
537    /// Transitions to the stdout form (`-o`), preserving every selected
538    /// option accepted by that form.
539    ///
540    /// Available when no `-u` is selected (`-o` accepts `-f`, `-s`, `-O`,
541    /// and `-I`).
542    pub fn stdout(self) -> Change<StdoutMode, P, C, S>
543    where
544        P: TransferStdoutMode,
545    {
546        Change {
547            bin: self.bin,
548            global_opts: self.global_opts,
549            mode: StdoutMode,
550            permission_mode: self.permission_mode,
551            changelist_mode: self.changelist_mode,
552            job_status_mode: self.job_status_mode,
553        }
554    }
555}
556
557// Regular -> remote delete: no `-u`, `-s`, `-O`, or `-I`; `-f` is optional
558// (it is folded into the form's built-in `-f`).
559#[cfg(not(feature = "lt2015_2"))]
560impl<P: TransferRemoteDeleteMode> Change<RegularOperationMode, P, Unselected, Unselected> {
561    /// Transitions to the remote delete form.
562    ///
563    /// Available from the plain edit form as long as neither `-u`, `-s`,
564    /// `-O`, nor `-I` is selected. A previously selected `-f` is folded
565    /// into the form's built-in `-f`.
566    ///
567    /// `server_id` is the server id of the commit server the changelist's
568    /// client is bound to.
569    pub fn server_id(
570        self,
571        server_id: impl Into<String>,
572    ) -> Change<DeleteMode<RemoteDeleteMode>, Unselected, Unselected, Unselected> {
573        Change {
574            bin: self.bin,
575            global_opts: self.global_opts,
576            mode: DeleteMode {
577                loc: RemoteDeleteMode {
578                    server_id: server_id.into(),
579                },
580            },
581            permission_mode: Unselected,
582            changelist_mode: Unselected,
583            job_status_mode: Unselected,
584        }
585    }
586}
587
588// Regular -> stdin: the changelist reference must be unset (`-i` takes no
589// changelist argument and accepts neither `-O` nor `-I`).
590impl<O, S> Change<RegularOperationMode, O, Unselected, S> {
591    /// Transitions to the stdin form (`-i`), preserving every selected
592    /// option accepted by that form.
593    ///
594    /// Available only when no `-O` or `-I` is selected (`-i` accepts `-s`,
595    /// `-f`, and `-u`, but no changelist reference).
596    pub fn stdin(self) -> Change<StdinMode, O, Unselected, S> {
597        Change {
598            bin: self.bin,
599            global_opts: self.global_opts,
600            mode: StdinMode,
601            permission_mode: self.permission_mode,
602            changelist_mode: Unselected,
603            job_status_mode: self.job_status_mode,
604        }
605    }
606}
607
608// -s within the edit form
609impl<O, C> Change<RegularOperationMode, O, C, Unselected> {
610    /// Selects `-s`: jobs may be assigned arbitrary status values on
611    /// submission.
612    pub fn arbitrary_job_status(self) -> Change<RegularOperationMode, O, C, ArbitraryJobStatus> {
613        Change {
614            bin: self.bin,
615            global_opts: self.global_opts,
616            mode: self.mode,
617            permission_mode: self.permission_mode,
618            changelist_mode: self.changelist_mode,
619            job_status_mode: ArbitraryJobStatus,
620        }
621    }
622}
623
624// -f / -u within the edit form
625impl<C, S> Change<RegularOperationMode, Unselected, C, S> {
626    /// Selects `-f` (force editing of submitted changelists).
627    ///
628    /// Mutually exclusive with `-u`.
629    pub fn force(self) -> Change<RegularOperationMode, ForceOperationMode, C, S> {
630        Change {
631            bin: self.bin,
632            global_opts: self.global_opts,
633            mode: self.mode,
634            permission_mode: ForceOperationMode,
635            changelist_mode: self.changelist_mode,
636            job_status_mode: self.job_status_mode,
637        }
638    }
639
640    /// Selects `-u` (update a submitted changelist).
641    ///
642    /// Mutually exclusive with `-f`.
643    pub fn update(self) -> Change<RegularOperationMode, UpdateOperationMode, C, S> {
644        Change {
645            bin: self.bin,
646            global_opts: self.global_opts,
647            mode: self.mode,
648            permission_mode: UpdateOperationMode,
649            changelist_mode: self.changelist_mode,
650            job_status_mode: self.job_status_mode,
651        }
652    }
653}
654
655// -O / -I within the edit form
656impl<O, S> Change<RegularOperationMode, O, Unselected, S> {
657    /// Selects `-O`: interpret the changelist number as the original,
658    /// pre-renumber number.
659    ///
660    /// Mutually exclusive with `-I`.
661    pub fn original(self) -> Change<RegularOperationMode, O, OriginalChangeListMode, S> {
662        Change {
663            bin: self.bin,
664            global_opts: self.global_opts,
665            mode: self.mode,
666            permission_mode: self.permission_mode,
667            changelist_mode: OriginalChangeListMode,
668            job_status_mode: self.job_status_mode,
669        }
670    }
671
672    /// Selects `-I`: interpret the changelist number as the Identity field.
673    ///
674    /// Mutually exclusive with `-O`.
675    #[cfg(not(feature = "lt2022_1"))]
676    pub fn identity(self) -> Change<RegularOperationMode, O, IdentityChangeListMode, S> {
677        Change {
678            bin: self.bin,
679            global_opts: self.global_opts,
680            mode: self.mode,
681            permission_mode: self.permission_mode,
682            changelist_mode: IdentityChangeListMode,
683            job_status_mode: self.job_status_mode,
684        }
685    }
686}
687
688// Regular -> change type / owner transfer: `-s` must be unset; at most one
689// of `-f`/`-u`/`-O`/`-I` may be selected and maps to the corresponding
690// `Only*` state.
691impl<O, C> Change<RegularOperationMode, O, C, Unselected> {
692    /// Transitions to the change-type form (`-t type`), preserving a
693    /// selected `-f`, `-u`, `-O`, or `-I` as the form's single allowed
694    /// extra option.
695    ///
696    /// Available only when `-s` is not selected (the `-t` / `-U` form does
697    /// not accept it), and at most one of `-f`, `-u`, `-O`, `-I` is
698    /// selected.
699    ///
700    /// `visibility` selects the new changelist type.
701    #[allow(clippy::type_complexity)]
702    pub fn visibility(
703        self,
704        visibility: Visibility,
705    ) -> Change<
706        UserOrVisibilityMode,
707        <(O, C) as TransferUVMode>::P,
708        <(O, C) as TransferUVMode>::C,
709        Unselected,
710    >
711    where
712        (O, C): TransferUVMode,
713    {
714        let (operation, change_ref) = (self.permission_mode, self.changelist_mode).transfer();
715        Change {
716            bin: self.bin,
717            global_opts: self.global_opts,
718            mode: UserOrVisibilityMode {
719                visibility: Some(visibility),
720                user: None,
721            },
722            permission_mode: operation,
723            changelist_mode: change_ref,
724            job_status_mode: Unselected,
725        }
726    }
727
728    /// Transitions to the owner-transfer form (`-U user`), preserving a
729    /// selected `-f`, `-u`, `-O`, or `-I` as the form's single allowed
730    /// extra option.
731    ///
732    /// Available only when `-s` is not selected (the `-t` / `-U` form does
733    /// not accept it), and at most one of `-f`, `-u`, `-O`, `-I` is
734    /// selected.
735    ///
736    /// `user` is the new owner of the empty pending changelist.
737    #[allow(clippy::type_complexity)]
738    pub fn user(
739        self,
740        user: impl Into<String>,
741    ) -> Change<
742        UserOrVisibilityMode,
743        <(O, C) as TransferUVMode>::P,
744        <(O, C) as TransferUVMode>::C,
745        Unselected,
746    >
747    where
748        (O, C): TransferUVMode,
749    {
750        let (operation, change_ref) = (self.permission_mode, self.changelist_mode).transfer();
751        Change {
752            bin: self.bin,
753            global_opts: self.global_opts,
754            mode: UserOrVisibilityMode {
755                user: Some(user.into()),
756                visibility: None,
757            },
758            permission_mode: operation,
759            changelist_mode: change_ref,
760            job_status_mode: Unselected,
761        }
762    }
763}
764
765// ---------------------------------------------------------------------------
766// SubCommand
767// ---------------------------------------------------------------------------
768
769impl<F, O, C, S> SubCommand for Change<F, O, C, S>
770where
771    F: ExclusiveOption,
772    O: ExclusiveOption,
773    C: ExclusiveOption,
774    S: ExclusiveOption,
775{
776    fn name(&self) -> &str {
777        "change"
778    }
779
780    fn inject_local_args(&self, command: &mut Command) {
781        self.mode.inject_args(command);
782        self.job_status_mode.inject_args(command);
783        self.permission_mode.inject_args(command);
784        self.changelist_mode.inject_args(command);
785    }
786
787    fn global_opts(&self) -> Option<&GlobalOpts> {
788        Some(&self.global_opts)
789    }
790}
791
792// ---------------------------------------------------------------------------
793// Spawning
794// ---------------------------------------------------------------------------
795
796impl<F, O, C, S, I> ParameterizedSpawn<(I,)> for Change<F, O, C, S>
797where
798    F: ExclusiveOption + TakesChangeList,
799    O: ExclusiveOption,
800    C: ExclusiveOption,
801    S: ExclusiveOption,
802    I: AsRef<OsStr>,
803{
804    type Output = Child;
805    type Error = std::io::Error;
806
807    /// Spawns `p4 change` with the given `changelist` argument as a child
808    /// process with piped standard output and error streams; use the
809    /// returned [`Child`] handle to wait for it or interact with it.
810    ///
811    /// The changelist argument is a changelist number, optionally prefixed
812    /// by the form's reference option semantics (for example an original
813    /// pre-renumber number under `-O`).
814    fn spawn_with(&mut self, (changelist,): (I,)) -> Result<Self::Output, Self::Error> {
815        self.setup_command(&self.bin)
816            .arg(changelist)
817            .stdout(Stdio::piped())
818            .stderr(Stdio::piped())
819            .spawn()
820    }
821}
822
823// RegularOperationMode accepts an optional changelist: without one it
824// opens the editor to create a new pending changelist.
825impl<O, C, S> ParameterizedSpawn<()> for Change<RegularOperationMode, O, C, S>
826where
827    O: ExclusiveOption,
828    C: ExclusiveOption,
829    S: ExclusiveOption,
830{
831    type Output = Child;
832    type Error = std::io::Error;
833
834    /// Spawns `p4 change` (the plain edit form) without a changelist
835    /// argument, opening the editor to create a new pending changelist.
836    fn spawn_with(&mut self, _: ()) -> Result<Self::Output, Self::Error> {
837        self.setup_command(&self.bin)
838            .stdout(Stdio::piped())
839            .stderr(Stdio::piped())
840            .spawn()
841    }
842}
843
844// StdoutMode accepts an optional changelist: without one it dumps the
845// default changelist specification.
846impl<O, C, S> ParameterizedSpawn<()> for Change<StdoutMode, O, C, S>
847where
848    O: ExclusiveOption,
849    C: ExclusiveOption,
850    S: ExclusiveOption,
851{
852    type Output = Child;
853    type Error = std::io::Error;
854
855    /// Spawns `p4 change -o` without a changelist argument, writing the
856    /// default changelist specification to standard output.
857    fn spawn_with(&mut self, _: ()) -> Result<Self::Output, Self::Error> {
858        self.setup_command(&self.bin)
859            .stdout(Stdio::piped())
860            .stderr(Stdio::piped())
861            .spawn()
862    }
863}
864
865impl<O, S> ParameterizedSpawn<()> for Change<StdinMode, O, Unselected, S>
866where
867    O: ExclusiveOption,
868    S: ExclusiveOption,
869{
870    type Output = Child;
871    type Error = std::io::Error;
872
873    /// Spawns `p4 change -i` with a piped standard input stream (and piped
874    /// standard output and error streams); write the changelist
875    /// specification to the returned [`Child`]'s stdin handle.
876    fn spawn_with(&mut self, _: ()) -> Result<Self::Output, Self::Error> {
877        self.setup_command(&self.bin)
878            .stdin(Stdio::piped())
879            .stdout(Stdio::piped())
880            .stderr(Stdio::piped())
881            .spawn()
882    }
883}
884
885impl<O, S> ParameterizedSpawn<(Stdio,)> for Change<StdinMode, O, Unselected, S>
886where
887    O: ExclusiveOption,
888    S: ExclusiveOption,
889{
890    type Output = Child;
891    type Error = std::io::Error;
892
893    /// Spawns `p4 change -i` with the given standard input configuration
894    /// and piped standard output and error streams; use the returned
895    /// [`Child`] handle to wait for it or interact with it.
896    ///
897    /// Pass `Stdio::piped()` to obtain a writable `child.stdin` handle and
898    /// feed the changelist specification into it; pass `Stdio::inherit()`
899    /// to read the specification from the spawning process's own standard
900    /// input.
901    fn spawn_with(&mut self, (stdio,): (Stdio,)) -> Result<Self::Output, Self::Error> {
902        self.setup_command(&self.bin)
903            .stdin(stdio)
904            .stdout(Stdio::piped())
905            .stderr(Stdio::piped())
906            .spawn()
907    }
908}
909
910#[cfg(test)]
911mod tests {
912    use super::*;
913    use crate::cmd::args_of;
914
915    // Compile-time rejection matrix. Every transition below was verified to
916    // fail compilation (trait bound not satisfied or method not found):
917    //
918    // - `.update().delete()`             (`-u` is not accepted by `-d`)
919    // - `.identity().delete()`           (`-I` is not accepted by `-d`)
920    // - `.update().stdout()`             (`-u` is not accepted by `-o`)
921    // - `.original().stdin()`            (`-O` is not accepted by `-i`)
922    // - `.arbitrary_job_status().visibility(..)`   (`-s` rejected by `-t`/`-U`)
923    // - `.force().original().visibility(..)`       (`-f` + `-O` together)
924    // - `.arbitrary_job_status().user(..)`         (`-s` rejected by `-t`/`-U`)
925    // - `.force().original().user(..)`             (`-f` + `-O` together)
926    // - `.force().arbitrary_job_status().server_id(..)` (`-s` rejected by remote `-d`)
927    // - `.visibility(..).force().update()` (a second option in the `-t`/`-U` form)
928    // - `.user(..).original().identity()`  (a second option in the `-t`/`-U` form)
929    // - spawning `.stdin()` with a `changelist` argument (no positional arg)
930
931    /// Dry-run checks of the assembled `p4 change` command line; no process
932    /// is spawned.
933    #[test]
934    fn regular_form_injects_all_options() {
935        let change = Change::new("p4", GlobalOpts::new())
936            .arbitrary_job_status()
937            .force()
938            .original();
939
940        assert_eq!(
941            args_of(&change.setup_command("p4")),
942            ["change", "-s", "-f", "-O"]
943        );
944    }
945
946    #[test]
947    #[cfg(not(feature = "lt2022_1"))]
948    fn regular_form_update_and_identity() {
949        let change = Change::new("p4", GlobalOpts::new())
950            .arbitrary_job_status()
951            .update()
952            .identity();
953
954        assert_eq!(
955            args_of(&change.setup_command("p4")),
956            ["change", "-s", "-u", "-I"]
957        );
958    }
959
960    #[test]
961    fn entry_points_select_form_flags() {
962        let delete = Change::new("p4", GlobalOpts::new()).delete();
963        assert_eq!(args_of(&delete.setup_command("p4")), ["change", "-d"]);
964
965        let stdout = Change::new("p4", GlobalOpts::new()).stdout();
966        assert_eq!(args_of(&stdout.setup_command("p4")), ["change", "-o"]);
967
968        let stdin = Change::new("p4", GlobalOpts::new()).stdin();
969        assert_eq!(args_of(&stdin.setup_command("p4")), ["change", "-i"]);
970
971        #[cfg(not(feature = "lt2015_2"))]
972        {
973            let remote = Change::new("p4", GlobalOpts::new()).server_id("remote-1");
974            #[cfg(feature = "lt2022_1")]
975            assert_eq!(
976                args_of(&remote.setup_command("p4")),
977                ["change", "-d", "-f", "--server=remote-1"]
978            );
979            #[cfg(not(feature = "lt2022_1"))]
980            assert_eq!(
981                args_of(&remote.setup_command("p4")),
982                ["change", "-d", "-f", "--serverid=remote-1"]
983            );
984        }
985    }
986
987    #[test]
988    fn arbitrary_job_status_carries_into_delete_stdout_stdin() {
989        // -s is accepted by the -d, -o, and -i forms.
990        let delete = Change::new("p4", GlobalOpts::new())
991            .arbitrary_job_status()
992            .delete();
993        assert_eq!(args_of(&delete.setup_command("p4")), ["change", "-d", "-s"]);
994
995        let stdout = Change::new("p4", GlobalOpts::new())
996            .arbitrary_job_status()
997            .stdout();
998        assert_eq!(args_of(&stdout.setup_command("p4")), ["change", "-o", "-s"]);
999
1000        let stdin = Change::new("p4", GlobalOpts::new())
1001            .arbitrary_job_status()
1002            .stdin();
1003        assert_eq!(args_of(&stdin.setup_command("p4")), ["change", "-i", "-s"]);
1004    }
1005
1006    #[test]
1007    fn regular_to_delete_preserves_compatible_options() {
1008        let change = Change::new("p4", GlobalOpts::new())
1009            .arbitrary_job_status()
1010            .force()
1011            .original()
1012            .delete();
1013
1014        assert_eq!(
1015            args_of(&change.setup_command("p4")),
1016            ["change", "-d", "-s", "-f", "-O"]
1017        );
1018    }
1019
1020    #[test]
1021    #[cfg(not(feature = "lt2022_1"))]
1022    fn regular_to_stdout_preserves_identity() {
1023        let change = Change::new("p4", GlobalOpts::new())
1024            .arbitrary_job_status()
1025            .force()
1026            .identity()
1027            .stdout();
1028
1029        assert_eq!(
1030            args_of(&change.setup_command("p4")),
1031            ["change", "-o", "-s", "-f", "-I"]
1032        );
1033    }
1034
1035    #[test]
1036    fn regular_to_stdin_preserves_update() {
1037        let change = Change::new("p4", GlobalOpts::new())
1038            .arbitrary_job_status()
1039            .update()
1040            .stdin();
1041
1042        assert_eq!(
1043            args_of(&change.setup_command("p4")),
1044            ["change", "-i", "-s", "-u"]
1045        );
1046    }
1047
1048    #[test]
1049    #[cfg(not(feature = "lt2015_2"))]
1050    fn regular_to_remote_delete_folds_force_into_builtin() {
1051        // `-f` selected in the edit form is folded into the remote delete
1052        // form's built-in `-f`.
1053        let change = Change::new("p4", GlobalOpts::new())
1054            .force()
1055            .server_id("remote-1");
1056
1057        #[cfg(feature = "lt2022_1")]
1058        assert_eq!(
1059            args_of(&change.setup_command("p4")),
1060            ["change", "-d", "-f", "--server=remote-1"]
1061        );
1062        #[cfg(not(feature = "lt2022_1"))]
1063        assert_eq!(
1064            args_of(&change.setup_command("p4")),
1065            ["change", "-d", "-f", "--serverid=remote-1"]
1066        );
1067    }
1068
1069    #[test]
1070    fn regular_visibility_maps_to_only_states() {
1071        // -f maps to the only-force operation
1072        let forced = Change::new("p4", GlobalOpts::new())
1073            .force()
1074            .visibility(Visibility::Restricted);
1075        assert_eq!(
1076            args_of(&forced.setup_command("p4")),
1077            ["change", "-t", "restricted", "-f"]
1078        );
1079
1080        // -I maps to the only-identity reference
1081        #[cfg(not(feature = "lt2022_1"))]
1082        {
1083            let identity = Change::new("p4", GlobalOpts::new())
1084                .identity()
1085                .visibility(Visibility::Public);
1086            assert_eq!(
1087                args_of(&identity.setup_command("p4")),
1088                ["change", "-t", "public", "-I"]
1089            );
1090        }
1091
1092        // no extra option
1093        let plain = Change::new("p4", GlobalOpts::new())
1094            .visibility(Visibility::Restricted)
1095            .user("maria");
1096        assert_eq!(
1097            args_of(&plain.setup_command("p4")),
1098            ["change", "-t", "restricted", "-U", "maria"]
1099        );
1100    }
1101
1102    #[test]
1103    fn regular_user_maps_single_option() {
1104        // -f maps to the only-force operation
1105        let change = Change::new("p4", GlobalOpts::new())
1106            .force()
1107            .user("maria")
1108            .visibility(Visibility::Public);
1109
1110        assert_eq!(
1111            args_of(&change.setup_command("p4")),
1112            ["change", "-t", "public", "-U", "maria", "-f"]
1113        );
1114
1115        // any other single option (`-u`, `-O`, `-I`) maps the same way
1116        let original = Change::new("p4", GlobalOpts::new())
1117            .original()
1118            .user("maria");
1119
1120        assert_eq!(
1121            args_of(&original.setup_command("p4")),
1122            ["change", "-U", "maria", "-O"]
1123        );
1124    }
1125
1126    #[test]
1127    fn user_or_visibility_form_sets_values_and_selects_one_option() {
1128        // Both form values can be set from either entry point.
1129        let both = Change::new("p4", GlobalOpts::new())
1130            .user("maria")
1131            .visibility(Visibility::Public);
1132        assert_eq!(
1133            args_of(&both.setup_command("p4")),
1134            ["change", "-t", "public", "-U", "maria"]
1135        );
1136
1137        // The form accepts one of `-f`, `-u`, `-O`, `-I`.
1138        let forced = Change::new("p4", GlobalOpts::new())
1139            .visibility(Visibility::Restricted)
1140            .force();
1141        assert_eq!(
1142            args_of(&forced.setup_command("p4")),
1143            ["change", "-t", "restricted", "-f"]
1144        );
1145
1146        let updated = Change::new("p4", GlobalOpts::new()).user("maria").update();
1147        assert_eq!(
1148            args_of(&updated.setup_command("p4")),
1149            ["change", "-U", "maria", "-u"]
1150        );
1151
1152        let original = Change::new("p4", GlobalOpts::new())
1153            .user("maria")
1154            .original();
1155        assert_eq!(
1156            args_of(&original.setup_command("p4")),
1157            ["change", "-U", "maria", "-O"]
1158        );
1159
1160        #[cfg(not(feature = "lt2015_2"))]
1161        {
1162            let identity = Change::new("p4", GlobalOpts::new())
1163                .visibility(Visibility::Public)
1164                .identity();
1165            assert_eq!(
1166                args_of(&identity.setup_command("p4")),
1167                ["change", "-t", "public", "-I"]
1168            );
1169        }
1170    }
1171
1172    #[test]
1173    fn options_can_be_selected_inside_forms() {
1174        let delete = Change::new("p4", GlobalOpts::new())
1175            .delete()
1176            .arbitrary_job_status()
1177            .force()
1178            .original();
1179        assert_eq!(
1180            args_of(&delete.setup_command("p4")),
1181            ["change", "-d", "-s", "-f", "-O"]
1182        );
1183
1184        #[cfg(not(feature = "lt2022_1"))]
1185        {
1186            let stdout = Change::new("p4", GlobalOpts::new())
1187                .stdout()
1188                .arbitrary_job_status()
1189                .identity();
1190            assert_eq!(
1191                args_of(&stdout.setup_command("p4")),
1192                ["change", "-o", "-s", "-I"]
1193            );
1194        }
1195        #[cfg(feature = "lt2022_1")]
1196        {
1197            let stdout = Change::new("p4", GlobalOpts::new())
1198                .stdout()
1199                .arbitrary_job_status()
1200                .original();
1201            assert_eq!(
1202                args_of(&stdout.setup_command("p4")),
1203                ["change", "-o", "-s", "-O"]
1204            );
1205        }
1206
1207        let stdin = Change::new("p4", GlobalOpts::new())
1208            .stdin()
1209            .arbitrary_job_status()
1210            .update();
1211        assert_eq!(
1212            args_of(&stdin.setup_command("p4")),
1213            ["change", "-i", "-s", "-u"]
1214        );
1215    }
1216
1217    #[test]
1218    fn global_opts_are_injected_once() {
1219        let change = Change::new("p4", GlobalOpts::new()).delete();
1220
1221        // Only the command name without any global opts by default.
1222        assert_eq!(args_of(&change.setup_command("p4")), ["change", "-d"]);
1223    }
1224
1225    // Spawn argument counts per form. Each call below mirrors what the
1226    // corresponding `ParameterizedSpawn` impl assembles: `setup_command`
1227    // injects the form flags, and the one-argument impls append the
1228    // changelist positional. The forms that take no positional argument
1229    // (Regular without a changelist, Stdout without a changelist, and
1230    // Stdin) are checked against `setup_command` directly.
1231    #[test]
1232    fn spawn_argument_counts_per_form() {
1233        // Regular: 0 or 1 changelist.
1234        let plain = Change::new("p4", GlobalOpts::new())
1235            .arbitrary_job_status()
1236            .force();
1237        assert_eq!(args_of(&plain.setup_command("p4")), ["change", "-s", "-f"]);
1238        let mut with_cl = plain.setup_command("p4");
1239        with_cl.arg("1234");
1240        assert_eq!(args_of(&with_cl), ["change", "-s", "-f", "1234"]);
1241
1242        // DeleteMode (local): exactly 1 changelist.
1243        let delete = Change::new("p4", GlobalOpts::new())
1244            .delete()
1245            .arbitrary_job_status()
1246            .force()
1247            .original();
1248        let mut delete_cl = delete.setup_command("p4");
1249        delete_cl.arg("1234");
1250        assert_eq!(
1251            args_of(&delete_cl),
1252            ["change", "-d", "-s", "-f", "-O", "1234"]
1253        );
1254
1255        // DeleteMode (remote): exactly 1 changelist.
1256        #[cfg(not(feature = "lt2015_2"))]
1257        {
1258            let remote = Change::new("p4", GlobalOpts::new()).server_id("edge-1");
1259            let mut remote_cl = remote.setup_command("p4");
1260            remote_cl.arg("1234");
1261            #[cfg(feature = "lt2022_1")]
1262            assert_eq!(
1263                args_of(&remote_cl),
1264                ["change", "-d", "-f", "--server=edge-1", "1234"]
1265            );
1266            #[cfg(not(feature = "lt2022_1"))]
1267            assert_eq!(
1268                args_of(&remote_cl),
1269                ["change", "-d", "-f", "--serverid=edge-1", "1234"]
1270            );
1271        }
1272
1273        // Stdout: 0 or 1 changelist.
1274        #[cfg(not(feature = "lt2022_1"))]
1275        {
1276            let stdout = Change::new("p4", GlobalOpts::new())
1277                .stdout()
1278                .arbitrary_job_status()
1279                .force()
1280                .identity();
1281            assert_eq!(
1282                args_of(&stdout.setup_command("p4")),
1283                ["change", "-o", "-s", "-f", "-I"]
1284            );
1285            let mut stdout_cl = stdout.setup_command("p4");
1286            stdout_cl.arg("1234");
1287            assert_eq!(
1288                args_of(&stdout_cl),
1289                ["change", "-o", "-s", "-f", "-I", "1234"]
1290            );
1291        }
1292        #[cfg(feature = "lt2022_1")]
1293        {
1294            let stdout = Change::new("p4", GlobalOpts::new())
1295                .stdout()
1296                .arbitrary_job_status()
1297                .force()
1298                .original();
1299            assert_eq!(
1300                args_of(&stdout.setup_command("p4")),
1301                ["change", "-o", "-s", "-f", "-O"]
1302            );
1303            let mut stdout_cl = stdout.setup_command("p4");
1304            stdout_cl.arg("1234");
1305            assert_eq!(
1306                args_of(&stdout_cl),
1307                ["change", "-o", "-s", "-f", "-O", "1234"]
1308            );
1309        }
1310
1311        // Stdin: no changelist; stdin is configured separately.
1312        let stdin = Change::new("p4", GlobalOpts::new())
1313            .stdin()
1314            .arbitrary_job_status()
1315            .update();
1316        assert_eq!(
1317            args_of(&stdin.setup_command("p4")),
1318            ["change", "-i", "-s", "-u"]
1319        );
1320
1321        // UserOrVisibility: exactly 1 changelist.
1322        let uov = Change::new("p4", GlobalOpts::new())
1323            .visibility(Visibility::Restricted)
1324            .user("maria")
1325            .force();
1326        let mut uov_cl = uov.setup_command("p4");
1327        uov_cl.arg("1234");
1328        assert_eq!(
1329            args_of(&uov_cl),
1330            ["change", "-t", "restricted", "-U", "maria", "-f", "1234"]
1331        );
1332    }
1333}