Skip to main content

perforce_cli/cmd/
client.rs

1use std::{
2    ffi::OsStr,
3    marker::PhantomData,
4    path::{Path, PathBuf},
5    process::{Child, Command, Stdio},
6};
7
8use super::{ExclusiveOption, SubCommand, Unselected};
9
10use crate::global::GlobalOpts;
11use crate::spawn::ParameterizedSpawn;
12
13// ---------------------------------------------------------------------------
14// Option dimensions: `-f` (force), `-t` (template), `-T` (workspace type)
15// ---------------------------------------------------------------------------
16
17/// The `-f` (force) flag is set.
18///
19/// Entered with [`Client::force`]; valid in the [`GeneralMode`],
20/// [`DeleteMode`], [`SwitchViewMode`], and [`StdinMode`] states.
21#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
22pub struct ForceSet;
23
24impl ExclusiveOption for ForceSet {
25    fn inject_args(&self, command: &mut Command) {
26        command.arg("-f");
27    }
28}
29
30/// The `-t template` option is set: copy client workspace `template`'s view
31/// and options into the `View:` and `Options:` field of this workspace.
32///
33/// Entered with [`Client::template`]; valid in the [`GeneralMode`] and
34/// [`StdoutMode`] states.
35#[derive(Debug, Clone, Default, PartialEq, Eq)]
36pub struct TemplateSet {
37    template: String,
38}
39
40impl ExclusiveOption for TemplateSet {
41    fn inject_args(&self, command: &mut Command) {
42        command.arg("-t").arg(&self.template);
43    }
44}
45
46/// The `-T type` option is set: create the client workspace with a specific
47/// [`WorkSpaceType`].
48///
49/// Entered with [`Client::workspace_type`]; valid in the [`GeneralMode`]
50/// and [`StdoutMode`] states.
51#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
52pub struct TypeSet {
53    workspace_type: WorkSpaceType,
54}
55
56impl ExclusiveOption for TypeSet {
57    fn inject_args(&self, command: &mut Command) {
58        command.arg("-T").arg(self.workspace_type.as_str());
59    }
60}
61
62/// The type of a client workspace, set with `-T type` or the `Type:` field.
63///
64/// By default, clients are [`Writable`](WorkSpaceType::Writable).
65#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
66pub enum WorkSpaceType {
67    /// `writeable`: for general use; the have list is tracked in the
68    /// central `db.have` table.
69    #[default]
70    Writable,
71    /// `readonly`: for short lived clients used in build automation scripts
72    /// that do not edit or submit files.
73    Readonly,
74    /// `partitioned`: for short lived clients used in build automation
75    /// scripts that do edit or submit files.
76    Partitioned,
77    /// `partitioned-jnl`: like `partitioned`, but journaled, checkpointed,
78    /// and recoverable.
79    PartitionedJnl,
80    /// `graph`: for use with depots and repos of type graph and Git
81    /// Connector.
82    Graph,
83}
84
85impl WorkSpaceType {
86    fn as_str(&self) -> &'static str {
87        match self {
88            WorkSpaceType::Writable => "writeable",
89            WorkSpaceType::Readonly => "readonly",
90            WorkSpaceType::Partitioned => "partitioned",
91            WorkSpaceType::PartitionedJnl => "partitioned-jnl",
92            WorkSpaceType::Graph => "graph",
93        }
94    }
95}
96
97// ---------------------------------------------------------------------------
98// Command modes
99// ---------------------------------------------------------------------------
100
101/// The default form: `p4 client [-f] [-t template] [-T type] [name]`.
102///
103/// Create or edit a client workspace specification. This is the initial
104/// state of [`Client`]; from here the command can transition into any of
105/// the other modes, subject to the options already set:
106///
107/// - with `-f` set, [`Client::delete`], [`Client::switch_view_stream`],
108///   [`Client::switch_view_template`], [`Client::switch_view`], and
109///   [`Client::stdin`] remain available (all of them accept `-f`)
110/// - with `-t` / `-T` set, [`Client::stdout`] remains available (the `-o`
111///   form accepts both)
112#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
113pub struct GeneralMode;
114
115/// How `-d -f` treats shelved changes of the deleted client workspace.
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub enum ShelvedHandling {
118    /// `-Fs`: the client is deleted and the shelved changes are left
119    /// intact.
120    Keep,
121    /// `-Fd`: the client and the shelved changes are both deleted.
122    Delete,
123}
124
125/// Marker: the `--serverid=X` option is set on the delete form.
126#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
127pub struct ServerIdSet;
128
129/// Marker: the `-Fs` option is set; the client is deleted and the shelved
130/// changes are left intact.
131#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
132pub struct ShelvedKept;
133
134/// Marker: the `-Fd` option is set; the client and the shelved changes
135/// are both deleted.
136#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
137pub struct ShelvedDeleted;
138
139/// Tracks whether the `--serverid=X` option is present on the delete form.
140#[doc(hidden)]
141pub trait ServerIdState {
142    /// Whether the `--serverid=X` option has been selected.
143    const SET: bool;
144}
145
146impl ServerIdState for Unselected {
147    const SET: bool = false;
148}
149
150impl ServerIdState for ServerIdSet {
151    const SET: bool = true;
152}
153
154/// Tracks the shelved-changes handling (`-Fs` / `-Fd`) on the delete form.
155#[doc(hidden)]
156pub trait ShelvedState {
157    /// The selected shelved-changes handling, if any.
158    fn handling() -> Option<ShelvedHandling>;
159
160    /// Inject the `-Fs` / `-Fd` argument, if selected.
161    fn inject_shelved(command: &mut Command);
162}
163
164impl ShelvedState for Unselected {
165    fn handling() -> Option<ShelvedHandling> {
166        None
167    }
168
169    fn inject_shelved(_: &mut Command) {}
170}
171
172impl ShelvedState for ShelvedKept {
173    fn handling() -> Option<ShelvedHandling> {
174        Some(ShelvedHandling::Keep)
175    }
176
177    fn inject_shelved(command: &mut Command) {
178        command.arg("-Fs");
179    }
180}
181
182impl ShelvedState for ShelvedDeleted {
183    fn handling() -> Option<ShelvedHandling> {
184        Some(ShelvedHandling::Delete)
185    }
186
187    fn inject_shelved(command: &mut Command) {
188        command.arg("-Fd");
189    }
190}
191
192/// The shelved states with which `--serverid=X` may be combined: no
193/// shelved option, or `-Fs`. The documented form is
194/// `p4 client -d -f --serverid=X [-Fs] name`, so `-Fd` is excluded.
195#[doc(hidden)]
196pub trait ServerIdCompatible: ShelvedState {}
197
198impl ServerIdCompatible for Unselected {}
199impl ServerIdCompatible for ShelvedKept {}
200
201/// Marks the `(serverid state, shelved state, force state)` triples of the
202/// delete form that match a documented command line and are therefore
203/// spawnable:
204///
205/// - `(Unselected, Unselected, Unselected)` — `p4 client -d name`
206/// - `(Unselected, Unselected, ForceSet)` — `p4 client -d -f name`
207/// - `(Unselected, ShelvedKept, ForceSet)` — `p4 client -d -f -Fs name`
208/// - `(Unselected, ShelvedDeleted, ForceSet)` — `p4 client -d -f -Fd name`
209/// - `(ServerIdSet, Unselected, ForceSet)` — `p4 client -d -f --serverid=X name`
210/// - `(ServerIdSet, ShelvedKept, ForceSet)` — `p4 client -d -f --serverid=X -Fs name`
211///
212/// States where `-Fs`, `-Fd`, or `--serverid=X` is selected without `-f`
213/// are reachable as intermediate builder states (the options may be
214/// selected in any order) but are not marked here, so they cannot be
215/// spawned until [`Client::force`] is called.
216#[doc(hidden)]
217pub trait ValidDeleteCombination {}
218
219impl ValidDeleteCombination for (Unselected, Unselected, Unselected) {}
220impl ValidDeleteCombination for (Unselected, Unselected, ForceSet) {}
221impl ValidDeleteCombination for (Unselected, ShelvedKept, ForceSet) {}
222impl ValidDeleteCombination for (Unselected, ShelvedDeleted, ForceSet) {}
223impl ValidDeleteCombination for (ServerIdSet, Unselected, ForceSet) {}
224impl ValidDeleteCombination for (ServerIdSet, ShelvedKept, ForceSet) {}
225
226/// The delete form: `p4 client -d [-f [-Fs|-Fd]] name` and
227/// `p4 client -d -f --serverid=X [-Fs] name`.
228///
229/// The two type parameters encode the documented compatibility matrix at
230/// compile time:
231///
232/// - `I` tracks `--serverid=X`: [`Unselected`] or [`ServerIdSet`]
233/// - `S` tracks the shelved handling: [`Unselected`], [`ShelvedKept`]
234///   (`-Fs`), or [`ShelvedDeleted`] (`-Fd`)
235///
236/// `-Fs` and `-Fd` are mutually exclusive, and `--serverid=X` can only be
237/// combined with `-Fs`, never with `-Fd`; the illegal combinations have no
238/// reachable state. The options may be selected in any order (for example
239/// `delete_shelved().force()` is equivalent to `force().delete_shelved()`);
240/// a state where `-Fs`, `-Fd`, or `--serverid=X` is set without `-f` is an
241/// intermediate state that cannot be spawned until [`Client::force`] is
242/// called. Entered with [`Client::delete`], or directly from the
243/// [`GeneralMode`] state via [`Client::server_id`],
244/// [`Client::keep_shelved`], and [`Client::delete_shelved`].
245#[derive(Debug, Clone, Default, PartialEq, Eq)]
246pub struct DeleteMode<I = Unselected, S = Unselected> {
247    server_id: Option<String>,
248    serverid_state: PhantomData<I>,
249    shelved: S,
250}
251
252/// The output form: `p4 client -o [-t template] [-T type] [name]`.
253///
254/// Write the client workspace specification to standard output. Entered
255/// with [`Client::stdout`]; carries the `-t` / `-T` options over from
256/// [`GeneralMode`].
257#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
258pub struct StdoutMode;
259
260/// The `-o` part of the stream form: display the workspace specification
261/// that would have been created for the stream.
262///
263/// Entered with [`Client::stdout`] in the [`StreamMode`] state; unlocks
264/// the `-c change` option.
265#[derive(Debug, Clone, Default, PartialEq, Eq)]
266pub struct StdoutOutput {
267    change: Option<String>,
268}
269
270impl ExclusiveOption for StdoutOutput {
271    fn inject_args(&self, command: &mut Command) {
272        if let Some(change) = &self.change {
273            command.arg("-c").arg(change);
274        }
275        command.arg("-o");
276    }
277}
278
279/// The stream form: `p4 client -S stream [[-c change] -o] [name]`.
280///
281/// Associate the workspace with the specified stream, which is used to
282/// generate its workspace view. Entered with [`Client::stream`]; before
283/// `-o` is selected, [`Client::switch_view`] can add `-s` to switch the
284/// stream associated with an existing workspace.
285#[derive(Debug, Clone, Default, PartialEq, Eq)]
286pub struct StreamMode<O = Unselected> {
287    stream: String,
288    output: O,
289}
290
291/// Switch the workspace view to a stream: `p4 client -s [-f] -S stream
292/// [name]`.
293///
294/// Entered with [`Client::switch_view_stream`] or [`Client::switch_view`].
295#[derive(Debug, Clone, Default, PartialEq, Eq)]
296pub struct StreamSwitch {
297    stream: String,
298}
299
300impl ExclusiveOption for StreamSwitch {
301    fn inject_args(&self, command: &mut Command) {
302        command.arg("-S").arg(&self.stream);
303    }
304}
305
306/// Switch the workspace view to the view defined for another workspace:
307/// `p4 client -s [-f] -t template [name]`.
308///
309/// Entered with [`Client::switch_view_template`] or [`Client::switch_view`].
310#[derive(Debug, Clone, Default, PartialEq, Eq)]
311pub struct TemplateSwitch {
312    template: String,
313}
314
315impl ExclusiveOption for TemplateSwitch {
316    fn inject_args(&self, command: &mut Command) {
317        command.arg("-t").arg(&self.template);
318    }
319}
320
321/// The switch-view form: `p4 client -s [-f] (-S stream | -t template)
322/// [name]`.
323///
324/// Switching views is not allowed in a client that has opened files; the
325/// `-f` option can be used with `-s` to force switching with opened files.
326/// View switching has no effect on files in a client workspace until
327/// `p4 sync` is run.
328#[derive(Debug, Clone, Default, PartialEq, Eq)]
329pub struct SwitchViewMode<V> {
330    view: V,
331}
332
333/// The input form: `p4 client -i [-f]`.
334///
335/// Read the client workspace specification from standard input. Entered
336/// with [`Client::stdin`].
337#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
338pub struct StdinMode;
339
340// ---------------------------------------------------------------------------
341// Mode argument injection
342// ---------------------------------------------------------------------------
343
344/// Injects the mode-specific arguments in syntax order, given the
345/// orthogonal option dimensions `F` (`-f`), `T` (`-t`), and `W` (`-T`).
346pub trait ClientMode<F, T, W> {
347    /// Inject the mode flag(s) and the carried options into `command`.
348    fn inject_mode_args(&self, command: &mut Command, force: &F, template: &T, workspace_type: &W);
349}
350
351impl<F, T, W> ClientMode<F, T, W> for GeneralMode
352where
353    F: ExclusiveOption,
354    T: ExclusiveOption,
355    W: ExclusiveOption,
356{
357    fn inject_mode_args(&self, command: &mut Command, force: &F, template: &T, workspace_type: &W) {
358        force.inject_args(command);
359        template.inject_args(command);
360        workspace_type.inject_args(command);
361    }
362}
363
364impl<I, S, F> ClientMode<F, Unselected, Unselected> for DeleteMode<I, S>
365where
366    I: ServerIdState,
367    S: ShelvedState,
368    F: ExclusiveOption,
369{
370    fn inject_mode_args(&self, command: &mut Command, force: &F, _: &Unselected, _: &Unselected) {
371        command.arg("-d");
372        force.inject_args(command);
373
374        if I::SET
375            && let Some(server_id) = &self.server_id
376        {
377            command.arg(format!("--serverid={server_id}"));
378        }
379
380        S::inject_shelved(command);
381    }
382}
383
384impl<T, W> ClientMode<Unselected, T, W> for StdoutMode
385where
386    T: ExclusiveOption,
387    W: ExclusiveOption,
388{
389    fn inject_mode_args(
390        &self,
391        command: &mut Command,
392        _: &Unselected,
393        template: &T,
394        workspace_type: &W,
395    ) {
396        command.arg("-o");
397        template.inject_args(command);
398        workspace_type.inject_args(command);
399    }
400}
401
402impl<O> ClientMode<Unselected, Unselected, Unselected> for StreamMode<O>
403where
404    O: ExclusiveOption,
405{
406    fn inject_mode_args(
407        &self,
408        command: &mut Command,
409        _: &Unselected,
410        _: &Unselected,
411        _: &Unselected,
412    ) {
413        command.arg("-S").arg(&self.stream);
414        self.output.inject_args(command);
415    }
416}
417
418impl<F> ClientMode<F, Unselected, Unselected> for SwitchViewMode<StreamSwitch>
419where
420    F: ExclusiveOption,
421{
422    fn inject_mode_args(&self, command: &mut Command, force: &F, _: &Unselected, _: &Unselected) {
423        command.arg("-s");
424        force.inject_args(command);
425        self.view.inject_args(command);
426    }
427}
428
429impl<F> ClientMode<F, Unselected, Unselected> for SwitchViewMode<TemplateSwitch>
430where
431    F: ExclusiveOption,
432{
433    fn inject_mode_args(&self, command: &mut Command, force: &F, _: &Unselected, _: &Unselected) {
434        command.arg("-s");
435        force.inject_args(command);
436        self.view.inject_args(command);
437    }
438}
439
440impl<F> ClientMode<F, Unselected, Unselected> for StdinMode
441where
442    F: ExclusiveOption,
443{
444    fn inject_mode_args(&self, command: &mut Command, force: &F, _: &Unselected, _: &Unselected) {
445        command.arg("-i");
446        force.inject_args(command);
447    }
448}
449
450// ---------------------------------------------------------------------------
451// Client
452// ---------------------------------------------------------------------------
453
454/// ```text
455/// p4 client [-f] [-t template] [-T type] [name]
456/// p4 client -d [-f [-Fs|-Fd]] name
457/// p4 client -o [-t template] [-T type] [name]
458/// p4 client -S stream [[-c change] -o] [name]
459/// p4 client -s [-f] -S stream [name]
460/// p4 client -s [-f] -t template [name]
461/// p4 client -i [-f]
462/// p4 client -d -f --serverid=X [-Fs] name
463/// ```
464///
465/// Create or edit a client workspace specification and its view.
466///
467/// The type parameters track the mode `M` and the orthogonal option
468/// dimensions `F` (`-f`), `T` (`-t template`), and `W` (`-T type`) at
469/// compile time, so a transition is only available when every option
470/// already set is valid in the target form:
471///
472/// - [`Self::delete`] requires `-t` / `-T` to be unset and ends in the
473///   terminal [`DeleteMode`] state, from which no further transitions are
474///   possible
475/// - [`Self::stdout`] requires `-f` to be unset; `-t` / `-T` are carried
476///   into the [`StdoutMode`] state
477/// - [`Self::stream`] requires `-f` / `-t` / `-T` to be unset
478/// - [`Self::switch_view_stream`] / [`Self::switch_view_template`] /
479///   [`Self::switch_view`] require `-T` to be unset; for the stream form
480///   `-t` must also be unset, while the template form requires `-t` to be
481///   set. `-f` is carried into the [`SwitchViewMode`] state
482/// - the `-s -S stream` form has a second entry path: select the stream
483///   with [`Self::stream`] and then [`Self::switch_view`] (only before
484///   `-o` is selected); `-f` is added afterwards if needed
485/// - [`Self::stdin`] requires `-t` / `-T` to be unset; `-f` is carried
486///   into the [`StdinMode`] state
487#[derive(Debug, Clone, Default)]
488pub struct Client<M = GeneralMode, F = Unselected, T = Unselected, W = Unselected> {
489    bin: PathBuf,
490
491    global_opts: GlobalOpts,
492
493    mode: M,
494
495    force: F,
496
497    template: T,
498
499    workspace_type: W,
500}
501
502impl<M, F, T, W> SubCommand for Client<M, F, T, W>
503where
504    M: ClientMode<F, T, W>,
505{
506    fn name(&self) -> &str {
507        "client"
508    }
509
510    fn inject_local_args(&self, command: &mut Command) {
511        self.mode
512            .inject_mode_args(command, &self.force, &self.template, &self.workspace_type);
513    }
514
515    fn global_opts(&self) -> Option<&GlobalOpts> {
516        Some(&self.global_opts)
517    }
518}
519
520// ---------------------------------------------------------------------------
521// Spawning: GeneralMode — `p4 client [-f] [-t template] [-T type] [name]`
522// ---------------------------------------------------------------------------
523
524impl<F, T, W> ParameterizedSpawn<()> for Client<GeneralMode, F, T, W>
525where
526    F: ExclusiveOption,
527    T: ExclusiveOption,
528    W: ExclusiveOption,
529{
530    type Output = Child;
531    type Error = std::io::Error;
532
533    /// Spawns `p4 client` for the workspace named by `P4CLIENT` as a child
534    /// process; the editor configured by `P4EDITOR` is invoked on the
535    /// client workspace specification form.
536    ///
537    /// The child process has its standard output and error streams piped;
538    /// use the returned [`Child`] handle to wait for it or interact with it.
539    fn spawn_with(&mut self, (): ()) -> Result<Self::Output, Self::Error> {
540        self.setup_command(&self.bin)
541            .stdout(Stdio::piped())
542            .stderr(Stdio::piped())
543            .spawn()
544    }
545}
546
547impl<F, T, W, I> ParameterizedSpawn<(I,)> for Client<GeneralMode, F, T, W>
548where
549    F: ExclusiveOption,
550    T: ExclusiveOption,
551    W: ExclusiveOption,
552    I: AsRef<OsStr>,
553{
554    type Output = Child;
555    type Error = std::io::Error;
556
557    /// Spawns `p4 client` for the named workspace as a child process; the
558    /// editor configured by `P4EDITOR` is invoked on the client workspace
559    /// specification form.
560    ///
561    /// The child process has its standard output and error streams piped;
562    /// use the returned [`Child`] handle to wait for it or interact with it.
563    fn spawn_with(&mut self, (name,): (I,)) -> Result<Self::Output, Self::Error> {
564        self.setup_command(&self.bin)
565            .arg(name)
566            .stdout(Stdio::piped())
567            .stderr(Stdio::piped())
568            .spawn()
569    }
570}
571
572// ---------------------------------------------------------------------------
573// Spawning: StdoutMode — `p4 client -o [-t template] [-T type] [name]`
574// ---------------------------------------------------------------------------
575
576impl<T, W> ParameterizedSpawn<()> for Client<StdoutMode, Unselected, T, W>
577where
578    T: ExclusiveOption,
579    W: ExclusiveOption,
580{
581    type Output = Child;
582    type Error = std::io::Error;
583
584    /// Spawns `p4 client -o` for the workspace named by `P4CLIENT` as a
585    /// child process, writing the client workspace specification to
586    /// standard output.
587    ///
588    /// The child process has its standard output and error streams piped;
589    /// use the returned [`Child`] handle to wait for it or interact with it.
590    fn spawn_with(&mut self, (): ()) -> Result<Self::Output, Self::Error> {
591        self.setup_command(&self.bin)
592            .stdout(Stdio::piped())
593            .stderr(Stdio::piped())
594            .spawn()
595    }
596}
597
598impl<T, W, I> ParameterizedSpawn<(I,)> for Client<StdoutMode, Unselected, T, W>
599where
600    T: ExclusiveOption,
601    W: ExclusiveOption,
602    I: AsRef<OsStr>,
603{
604    type Output = Child;
605    type Error = std::io::Error;
606
607    /// Spawns `p4 client -o` for the named workspace as a child process,
608    /// writing the client workspace specification to standard output.
609    ///
610    /// The child process has its standard output and error streams piped;
611    /// use the returned [`Child`] handle to wait for it or interact with it.
612    fn spawn_with(&mut self, (name,): (I,)) -> Result<Self::Output, Self::Error> {
613        self.setup_command(&self.bin)
614            .arg(name)
615            .stdout(Stdio::piped())
616            .stderr(Stdio::piped())
617            .spawn()
618    }
619}
620
621// ---------------------------------------------------------------------------
622// Spawning: DeleteMode — `p4 client -d [-f [-Fs|-Fd]] name`
623// ---------------------------------------------------------------------------
624
625impl<I, S, F, A> ParameterizedSpawn<(A,)> for Client<DeleteMode<I, S>, F, Unselected, Unselected>
626where
627    I: ServerIdState,
628    S: ShelvedState,
629    F: ExclusiveOption,
630    (I, S, F): ValidDeleteCombination,
631    A: AsRef<OsStr>,
632{
633    type Output = Child;
634    type Error = std::io::Error;
635
636    /// Spawns `p4 client -d` for the named workspace as a child process.
637    ///
638    /// The child process has its standard output and error streams piped;
639    /// use the returned [`Child`] handle to wait for it or interact with it.
640    fn spawn_with(&mut self, (name,): (A,)) -> Result<Self::Output, Self::Error> {
641        self.setup_command(&self.bin)
642            .arg(name)
643            .stdout(Stdio::piped())
644            .stderr(Stdio::piped())
645            .spawn()
646    }
647}
648
649// ---------------------------------------------------------------------------
650// Spawning: StreamMode — `p4 client -S stream [[-c change] -o] [name]`
651// ---------------------------------------------------------------------------
652
653impl<O> ParameterizedSpawn<()> for Client<StreamMode<O>, Unselected, Unselected, Unselected>
654where
655    O: ExclusiveOption,
656{
657    type Output = Child;
658    type Error = std::io::Error;
659
660    /// Spawns `p4 client -S stream` for the workspace named by `P4CLIENT`
661    /// as a child process, associating the workspace with the stream.
662    ///
663    /// The child process has its standard output and error streams piped;
664    /// use the returned [`Child`] handle to wait for it or interact with it.
665    fn spawn_with(&mut self, (): ()) -> Result<Self::Output, Self::Error> {
666        self.setup_command(&self.bin)
667            .stdout(Stdio::piped())
668            .stderr(Stdio::piped())
669            .spawn()
670    }
671}
672
673impl<O, I> ParameterizedSpawn<(I,)> for Client<StreamMode<O>, Unselected, Unselected, Unselected>
674where
675    O: ExclusiveOption,
676    I: AsRef<OsStr>,
677{
678    type Output = Child;
679    type Error = std::io::Error;
680
681    /// Spawns `p4 client -S stream` for the named workspace as a child
682    /// process, associating the workspace with the stream.
683    ///
684    /// The child process has its standard output and error streams piped;
685    /// use the returned [`Child`] handle to wait for it or interact with it.
686    fn spawn_with(&mut self, (name,): (I,)) -> Result<Self::Output, Self::Error> {
687        self.setup_command(&self.bin)
688            .arg(name)
689            .stdout(Stdio::piped())
690            .stderr(Stdio::piped())
691            .spawn()
692    }
693}
694
695// ---------------------------------------------------------------------------
696// Spawning: SwitchViewMode — `p4 client -s [-f] (-S stream | -t template)
697// [name]`
698// ---------------------------------------------------------------------------
699
700impl<V, F> ParameterizedSpawn<()> for Client<SwitchViewMode<V>, F, Unselected, Unselected>
701where
702    SwitchViewMode<V>: ClientMode<F, Unselected, Unselected>,
703    F: ExclusiveOption,
704{
705    type Output = Child;
706    type Error = std::io::Error;
707
708    /// Spawns `p4 client -s` for the workspace named by `P4CLIENT` as a
709    /// child process, switching its view.
710    ///
711    /// The child process has its standard output and error streams piped;
712    /// use the returned [`Child`] handle to wait for it or interact with it.
713    fn spawn_with(&mut self, (): ()) -> Result<Self::Output, Self::Error> {
714        self.setup_command(&self.bin)
715            .stdout(Stdio::piped())
716            .stderr(Stdio::piped())
717            .spawn()
718    }
719}
720
721impl<V, F, I> ParameterizedSpawn<(I,)> for Client<SwitchViewMode<V>, F, Unselected, Unselected>
722where
723    SwitchViewMode<V>: ClientMode<F, Unselected, Unselected>,
724    F: ExclusiveOption,
725    I: AsRef<OsStr>,
726{
727    type Output = Child;
728    type Error = std::io::Error;
729
730    /// Spawns `p4 client -s` for the named workspace as a child process,
731    /// switching its view.
732    ///
733    /// The child process has its standard output and error streams piped;
734    /// use the returned [`Child`] handle to wait for it or interact with it.
735    fn spawn_with(&mut self, (name,): (I,)) -> Result<Self::Output, Self::Error> {
736        self.setup_command(&self.bin)
737            .arg(name)
738            .stdout(Stdio::piped())
739            .stderr(Stdio::piped())
740            .spawn()
741    }
742}
743
744// ---------------------------------------------------------------------------
745// Spawning: StdinMode — `p4 client -i [-f]`
746// ---------------------------------------------------------------------------
747
748impl<F> ParameterizedSpawn<()> for Client<StdinMode, F, Unselected, Unselected>
749where
750    F: ExclusiveOption,
751{
752    type Output = Child;
753    type Error = std::io::Error;
754
755    /// Spawns `p4 client -i` with a piped standard input stream (and piped
756    /// standard output and error streams); write the client workspace
757    /// specification to the returned [`Child`]'s stdin handle.
758    fn spawn_with(&mut self, (): ()) -> Result<Self::Output, Self::Error> {
759        self.setup_command(&self.bin)
760            .stdin(Stdio::piped())
761            .stdout(Stdio::piped())
762            .stderr(Stdio::piped())
763            .spawn()
764    }
765}
766
767impl<F> ParameterizedSpawn<(Stdio,)> for Client<StdinMode, F, Unselected, Unselected>
768where
769    F: ExclusiveOption,
770{
771    type Output = Child;
772    type Error = std::io::Error;
773
774    /// Spawns `p4 client -i` with the given standard input configuration
775    /// and piped standard output and error streams; use the returned
776    /// [`Child`] handle to wait for it or interact with it.
777    ///
778    /// Pass `Stdio::piped()` to obtain a writable `child.stdin` handle and
779    /// feed the client workspace specification into it; pass
780    /// `Stdio::inherit()` to read the specification from the spawning
781    /// process's own standard input.
782    fn spawn_with(&mut self, (stdio,): (Stdio,)) -> Result<Self::Output, Self::Error> {
783        self.setup_command(&self.bin)
784            .stdin(stdio)
785            .stdout(Stdio::piped())
786            .stderr(Stdio::piped())
787            .spawn()
788    }
789}
790
791// ---------------------------------------------------------------------------
792// General accessors and option builders
793// ---------------------------------------------------------------------------
794
795impl Client<GeneralMode, Unselected, Unselected, Unselected> {
796    /// Creates a new `p4 client` command in the [`GeneralMode`] state.
797    ///
798    /// `bin` is the path to the Perforce command-line executable.
799    pub fn new(bin: impl Into<PathBuf>, global_opts: GlobalOpts) -> Self {
800        Self {
801            bin: bin.into(),
802            global_opts,
803            mode: GeneralMode,
804            force: Unselected,
805            template: Unselected,
806            workspace_type: Unselected,
807        }
808    }
809}
810
811impl<M, F, T, W> Client<M, F, T, W> {
812    /// Get the Perforce command path.
813    pub fn get_bin(&self) -> &Path {
814        &self.bin
815    }
816
817    /// Set the Perforce command path.
818    pub fn set_bin(&mut self, bin: impl Into<PathBuf>) -> &mut Self {
819        self.bin = bin.into();
820        self
821    }
822
823    /// Set the Perforce command path.
824    pub fn bin(mut self, bin: impl Into<PathBuf>) -> Self {
825        self.set_bin(bin);
826        self
827    }
828
829    /// Get the global options.
830    pub fn get_global_opts(&self) -> &GlobalOpts {
831        &self.global_opts
832    }
833
834    /// Set the global options.
835    pub fn set_global_opts(&mut self, v: GlobalOpts) -> &mut Self {
836        self.global_opts = v;
837        self
838    }
839
840    /// Set the global options.
841    pub fn global_opts(mut self, v: GlobalOpts) -> Self {
842        self.set_global_opts(v);
843        self
844    }
845}
846
847// `-f` is valid in the GeneralMode, DeleteMode, SwitchViewMode, and
848// StdinMode states.
849
850impl<T, W> Client<GeneralMode, Unselected, T, W> {
851    /// # Description
852    ///
853    /// -f
854    ///
855    /// Allows the last modification date, which is normally read-only, to
856    /// be set. Administrators can use the `-f` option to delete or modify
857    /// locked workspaces owned by other users. Use of this option requires
858    /// `admin` access granted by `p4 protect`.
859    pub fn force(self) -> Client<GeneralMode, ForceSet, T, W> {
860        Client {
861            bin: self.bin,
862            global_opts: self.global_opts,
863            mode: self.mode,
864            force: ForceSet,
865            template: self.template,
866            workspace_type: self.workspace_type,
867        }
868    }
869}
870
871impl<I, S> Client<DeleteMode<I, S>, Unselected, Unselected, Unselected>
872where
873    I: ServerIdState,
874    S: ShelvedState,
875{
876    /// # Description
877    ///
878    /// -f
879    ///
880    /// The combination of `-d` with `-f` allows the P4 Server
881    /// administrator to delete a locked workspace that is owned by other
882    /// user. This also reverts any files opened on the workspace.
883    ///
884    /// `-f` is required by the `-Fs`, `-Fd`, and `--serverid=X` options,
885    /// but it may be selected before or after them; calling `force` on a
886    /// state carrying those options completes the documented form and
887    /// makes the command spawnable.
888    pub fn force(self) -> Client<DeleteMode<I, S>, ForceSet, Unselected, Unselected> {
889        Client {
890            bin: self.bin,
891            global_opts: self.global_opts,
892            mode: self.mode,
893            force: ForceSet,
894            template: self.template,
895            workspace_type: self.workspace_type,
896        }
897    }
898}
899
900impl<V> Client<SwitchViewMode<V>, Unselected, Unselected, Unselected> {
901    /// # Description
902    ///
903    /// -f
904    ///
905    /// The `-f` option can be used with `-s` to force switching with
906    /// opened files.
907    pub fn force(self) -> Client<SwitchViewMode<V>, ForceSet, Unselected, Unselected> {
908        Client {
909            bin: self.bin,
910            global_opts: self.global_opts,
911            mode: self.mode,
912            force: ForceSet,
913            template: self.template,
914            workspace_type: self.workspace_type,
915        }
916    }
917}
918
919impl Client<StdinMode, Unselected, Unselected, Unselected> {
920    /// # Description
921    ///
922    /// -f
923    ///
924    /// Allows the last modification date, which is normally read-only, to
925    /// be set. Use of this option requires `admin` access granted by
926    /// `p4 protect`.
927    pub fn force(self) -> Client<StdinMode, ForceSet, Unselected, Unselected> {
928        Client {
929            bin: self.bin,
930            global_opts: self.global_opts,
931            mode: self.mode,
932            force: ForceSet,
933            template: self.template,
934            workspace_type: self.workspace_type,
935        }
936    }
937}
938
939// `-t` is valid in the GeneralMode and StdoutMode states.
940
941impl<F, W> Client<GeneralMode, F, Unselected, W> {
942    /// # Description
943    ///
944    /// -t name
945    ///
946    /// Copy client workspace `template`'s view and options into the
947    /// `View:` and `Options:` field of this workspace. If you specify a
948    /// default client template using the `template.client` configurable,
949    /// you do not have to specify this option.
950    pub fn template(self, template: impl Into<String>) -> Client<GeneralMode, F, TemplateSet, W> {
951        Client {
952            bin: self.bin,
953            global_opts: self.global_opts,
954            mode: self.mode,
955            force: self.force,
956            template: TemplateSet {
957                template: template.into(),
958            },
959            workspace_type: self.workspace_type,
960        }
961    }
962}
963
964impl<W> Client<StdoutMode, Unselected, Unselected, W> {
965    /// # Description
966    ///
967    /// -t name
968    ///
969    /// Copy client workspace `template`'s view and options into the
970    /// `View:` and `Options:` field of this workspace.
971    pub fn template(
972        self,
973        template: impl Into<String>,
974    ) -> Client<StdoutMode, Unselected, TemplateSet, W> {
975        Client {
976            bin: self.bin,
977            global_opts: self.global_opts,
978            mode: self.mode,
979            force: self.force,
980            template: TemplateSet {
981                template: template.into(),
982            },
983            workspace_type: self.workspace_type,
984        }
985    }
986}
987
988// `-T` is valid in the GeneralMode and StdoutMode states.
989
990impl<F, T> Client<GeneralMode, F, T, Unselected> {
991    /// # Description
992    ///
993    /// -T type
994    ///
995    /// By default, clients are `writeable`. When you create a client
996    /// workspace, use `-T type` to set a specific type.
997    pub fn workspace_type(
998        self,
999        workspace_type: WorkSpaceType,
1000    ) -> Client<GeneralMode, F, T, TypeSet> {
1001        Client {
1002            bin: self.bin,
1003            global_opts: self.global_opts,
1004            mode: self.mode,
1005            force: self.force,
1006            template: self.template,
1007            workspace_type: TypeSet { workspace_type },
1008        }
1009    }
1010}
1011
1012impl<T> Client<StdoutMode, Unselected, T, Unselected> {
1013    /// # Description
1014    ///
1015    /// -T type
1016    ///
1017    /// By default, clients are `writeable`. When you create a client
1018    /// workspace, use `-T type` to set a specific type.
1019    pub fn workspace_type(
1020        self,
1021        workspace_type: WorkSpaceType,
1022    ) -> Client<StdoutMode, Unselected, T, TypeSet> {
1023        Client {
1024            bin: self.bin,
1025            global_opts: self.global_opts,
1026            mode: self.mode,
1027            force: self.force,
1028            template: self.template,
1029            workspace_type: TypeSet { workspace_type },
1030        }
1031    }
1032}
1033
1034// Option accessors for the set states.
1035
1036impl<M, F, W> Client<M, F, TemplateSet, W> {
1037    /// Get the template client workspace name set with `-t`.
1038    pub fn get_template(&self) -> &str {
1039        &self.template.template
1040    }
1041
1042    /// # Description
1043    ///
1044    /// -t name
1045    ///
1046    /// Copy client workspace `template`'s view and options into the
1047    /// `View:` and `Options:` field of this workspace.
1048    pub fn set_template(&mut self, template: impl Into<String>) -> &mut Self {
1049        self.template.template = template.into();
1050        self
1051    }
1052}
1053
1054impl<M, F, T> Client<M, F, T, TypeSet> {
1055    /// Get the client workspace type set with `-T`.
1056    pub fn get_workspace_type(&self) -> WorkSpaceType {
1057        self.workspace_type.workspace_type
1058    }
1059
1060    /// # Description
1061    ///
1062    /// -T type
1063    ///
1064    /// By default, clients are `writeable`. When you create a client
1065    /// workspace, use `-T type` to set a specific type.
1066    pub fn set_workspace_type(&mut self, workspace_type: WorkSpaceType) -> &mut Self {
1067        self.workspace_type.workspace_type = workspace_type;
1068        self
1069    }
1070}
1071
1072// ---------------------------------------------------------------------------
1073// Mode transitions out of GeneralMode
1074// ---------------------------------------------------------------------------
1075
1076impl<F> Client<GeneralMode, F, Unselected, Unselected> {
1077    /// # Description
1078    ///
1079    /// -d name
1080    ///
1081    /// Delete the specified client workspace whether or not the workspace
1082    /// is owned by the user. The workspace must be unlocked and must have
1083    /// no opened files or pending changes.
1084    ///
1085    /// This transitions into the terminal [`DeleteMode`] state, from which
1086    /// no further mode transitions are possible.
1087    pub fn delete(self) -> Client<DeleteMode<Unselected, Unselected>, F, Unselected, Unselected> {
1088        Client {
1089            bin: self.bin,
1090            global_opts: self.global_opts,
1091            mode: DeleteMode {
1092                server_id: None,
1093                serverid_state: PhantomData,
1094                shelved: Unselected,
1095            },
1096            force: self.force,
1097            template: self.template,
1098            workspace_type: self.workspace_type,
1099        }
1100    }
1101
1102    /// # Description
1103    ///
1104    /// -i
1105    ///
1106    /// Read the client workspace specification from standard input.
1107    ///
1108    /// This transitions into the [`StdinMode`] state, carrying the `-f`
1109    /// option.
1110    pub fn stdin(self) -> Client<StdinMode, F, Unselected, Unselected> {
1111        Client {
1112            bin: self.bin,
1113            global_opts: self.global_opts,
1114            mode: StdinMode,
1115            force: self.force,
1116            template: self.template,
1117            workspace_type: self.workspace_type,
1118        }
1119    }
1120
1121    /// # Description
1122    ///
1123    /// -s -S stream
1124    ///
1125    /// Switch the workspace view to the specified stream.
1126    ///
1127    /// This transitions into the [`SwitchViewMode`] state, carrying the
1128    /// `-f` option.
1129    pub fn switch_view_stream(
1130        self,
1131        stream: impl Into<String>,
1132    ) -> Client<SwitchViewMode<StreamSwitch>, F, Unselected, Unselected> {
1133        Client {
1134            bin: self.bin,
1135            global_opts: self.global_opts,
1136            mode: SwitchViewMode {
1137                view: StreamSwitch {
1138                    stream: stream.into(),
1139                },
1140            },
1141            force: self.force,
1142            template: self.template,
1143            workspace_type: self.workspace_type,
1144        }
1145    }
1146
1147    /// # Description
1148    ///
1149    /// -s -t template
1150    ///
1151    /// Switch the workspace view to the view defined for another
1152    /// workspace.
1153    ///
1154    /// This transitions into the [`SwitchViewMode`] state, carrying the
1155    /// `-f` option.
1156    pub fn switch_view_template(
1157        self,
1158        template: impl Into<String>,
1159    ) -> Client<SwitchViewMode<TemplateSwitch>, F, Unselected, Unselected> {
1160        Client {
1161            bin: self.bin,
1162            global_opts: self.global_opts,
1163            mode: SwitchViewMode {
1164                view: TemplateSwitch {
1165                    template: template.into(),
1166                },
1167            },
1168            force: self.force,
1169            template: self.template,
1170            workspace_type: self.workspace_type,
1171        }
1172    }
1173}
1174
1175impl<F> Client<GeneralMode, F, TemplateSet, Unselected> {
1176    /// # Description
1177    ///
1178    /// -s -t template
1179    ///
1180    /// Switch the workspace view to the view defined for the template
1181    /// workspace already selected with [`Client::template`].
1182    ///
1183    /// This transitions into the [`SwitchViewMode`] state, carrying the
1184    /// `-f` option.
1185    pub fn switch_view(self) -> Client<SwitchViewMode<TemplateSwitch>, F, Unselected, Unselected> {
1186        Client {
1187            bin: self.bin,
1188            global_opts: self.global_opts,
1189            mode: SwitchViewMode {
1190                view: TemplateSwitch {
1191                    template: self.template.template,
1192                },
1193            },
1194            force: self.force,
1195            template: Unselected,
1196            workspace_type: self.workspace_type,
1197        }
1198    }
1199}
1200
1201impl<T, W> Client<GeneralMode, Unselected, T, W> {
1202    /// # Description
1203    ///
1204    /// -o
1205    ///
1206    /// Write the client workspace specification to standard output.
1207    ///
1208    /// This transitions into the [`StdoutMode`] state, carrying the `-t`
1209    /// and `-T` options.
1210    pub fn stdout(self) -> Client<StdoutMode, Unselected, T, W> {
1211        Client {
1212            bin: self.bin,
1213            global_opts: self.global_opts,
1214            mode: StdoutMode,
1215            force: self.force,
1216            template: self.template,
1217            workspace_type: self.workspace_type,
1218        }
1219    }
1220}
1221
1222impl Client<GeneralMode, Unselected, Unselected, Unselected> {
1223    /// # Description
1224    ///
1225    /// -S stream
1226    ///
1227    /// Associates the workspace with the specified stream, which is used
1228    /// to generate its workspace view.
1229    ///
1230    /// This transitions into the [`StreamMode`] state.
1231    pub fn stream(
1232        self,
1233        stream: impl Into<String>,
1234    ) -> Client<StreamMode<Unselected>, Unselected, Unselected, Unselected> {
1235        Client {
1236            bin: self.bin,
1237            global_opts: self.global_opts,
1238            mode: StreamMode {
1239                stream: stream.into(),
1240                output: Unselected,
1241            },
1242            force: self.force,
1243            template: self.template,
1244            workspace_type: self.workspace_type,
1245        }
1246    }
1247}
1248
1249// One-shot transitions from `GeneralMode` into a delete form. Since
1250// `--serverid`, `-Fs`, and `-Fd` only occur on a `-d -f` form, selecting
1251// any of them implies `-d` and transitions directly into the
1252// [`DeleteMode`] state, carrying along `-f` if it was already selected.
1253// `-f` may also be selected afterwards via [`Client::force`]; until then
1254// the state is an intermediate, non-spawnable state.
1255
1256impl<F> Client<GeneralMode, F, Unselected, Unselected>
1257where
1258    F: ExclusiveOption,
1259{
1260    /// # Description
1261    ///
1262    /// -d [-f] --serverid=X
1263    ///
1264    /// If you try to forcibly delete a client bound to another server, you
1265    /// need to specify the `--serverid` option and specify the server id
1266    /// of the other server. This ensures that you do not accidentally
1267    /// delete the client believing it to be connected to your own server.
1268    /// This variant of the `p4 client` command must be issued directly to
1269    /// the commit server.
1270    ///
1271    /// Selecting this option implies `-d`; `-f` may be selected before or
1272    /// after it. The resulting command is spawnable only once `-f` is set.
1273    pub fn server_id(
1274        self,
1275        server_id: impl Into<String>,
1276    ) -> Client<DeleteMode<ServerIdSet, Unselected>, F, Unselected, Unselected> {
1277        Client {
1278            bin: self.bin,
1279            global_opts: self.global_opts,
1280            mode: DeleteMode {
1281                server_id: Some(server_id.into()),
1282                serverid_state: PhantomData,
1283                shelved: Unselected,
1284            },
1285            force: self.force,
1286            template: self.template,
1287            workspace_type: self.workspace_type,
1288        }
1289    }
1290
1291    /// # Description
1292    ///
1293    /// -d [-f] -Fs
1294    ///
1295    /// Allows the deletion with `-d` of a client even when that client
1296    /// contains shelved changes. The client is deleted and the shelved
1297    /// changes are left intact. (You must use the `-f` option with the
1298    /// `-Fs` option.)
1299    ///
1300    /// Selecting this option implies `-d`; `-f` may be selected before or
1301    /// after it. The resulting command is spawnable only once `-f` is set.
1302    pub fn keep_shelved(
1303        self,
1304    ) -> Client<DeleteMode<Unselected, ShelvedKept>, F, Unselected, Unselected> {
1305        Client {
1306            bin: self.bin,
1307            global_opts: self.global_opts,
1308            mode: DeleteMode {
1309                server_id: None,
1310                serverid_state: PhantomData,
1311                shelved: ShelvedKept,
1312            },
1313            force: self.force,
1314            template: self.template,
1315            workspace_type: self.workspace_type,
1316        }
1317    }
1318
1319    /// # Description
1320    ///
1321    /// -d [-f] -Fd
1322    ///
1323    /// Allows the deletion with `-d` of a client even when that client
1324    /// contains shelved changes. The client and the shelved changes are
1325    /// both deleted. (You must use the `-f` option with the `-Fd`
1326    /// option.)
1327    ///
1328    /// Selecting this option implies `-d`; `-f` may be selected before or
1329    /// after it. The resulting command is spawnable only once `-f` is set.
1330    pub fn delete_shelved(
1331        self,
1332    ) -> Client<DeleteMode<Unselected, ShelvedDeleted>, F, Unselected, Unselected> {
1333        Client {
1334            bin: self.bin,
1335            global_opts: self.global_opts,
1336            mode: DeleteMode {
1337                server_id: None,
1338                serverid_state: PhantomData,
1339                shelved: ShelvedDeleted,
1340            },
1341            force: self.force,
1342            template: self.template,
1343            workspace_type: self.workspace_type,
1344        }
1345    }
1346}
1347
1348// ---------------------------------------------------------------------------
1349// DeleteMode options
1350// ---------------------------------------------------------------------------
1351
1352// The `--serverid=X` transition: available when it has not been selected
1353// yet, and only in shelved states compatible with the documented form
1354// `p4 client -d -f --serverid=X [-Fs] name` (never after `-Fd`).
1355
1356impl<S, F> Client<DeleteMode<Unselected, S>, F, Unselected, Unselected>
1357where
1358    S: ServerIdCompatible,
1359    F: ExclusiveOption,
1360{
1361    /// # Description
1362    ///
1363    /// --serverid=X
1364    ///
1365    /// If you try to forcibly delete a client bound to another server, you
1366    /// need to specify the `--serverid` option and specify the server id
1367    /// of the other server. This ensures that you do not accidentally
1368    /// delete the client believing it to be connected to your own server.
1369    /// This variant of the `p4 client` command must be issued directly to
1370    /// the commit server.
1371    ///
1372    /// Only the `-Fs` shelved handling can be combined with `--serverid`;
1373    /// this transition is not available once `-Fd` has been selected. The
1374    /// required `-f` may be selected before or afterwards.
1375    pub fn server_id(
1376        self,
1377        server_id: impl Into<String>,
1378    ) -> Client<DeleteMode<ServerIdSet, S>, F, Unselected, Unselected> {
1379        Client {
1380            bin: self.bin,
1381            global_opts: self.global_opts,
1382            mode: DeleteMode {
1383                server_id: Some(server_id.into()),
1384                serverid_state: PhantomData,
1385                shelved: self.mode.shelved,
1386            },
1387            force: self.force,
1388            template: self.template,
1389            workspace_type: self.workspace_type,
1390        }
1391    }
1392}
1393
1394// The `--serverid=X` value is always present in this state.
1395
1396impl<S, F> Client<DeleteMode<ServerIdSet, S>, F, Unselected, Unselected>
1397where
1398    S: ServerIdCompatible,
1399    F: ExclusiveOption,
1400{
1401    /// Get the server ID set with `--serverid=X`.
1402    pub fn get_server_id(&self) -> &str {
1403        self.mode.server_id.as_deref().expect("server ID is set")
1404    }
1405
1406    /// # Description
1407    ///
1408    /// --serverid=X
1409    ///
1410    /// Update the server ID of the other server to which the deleted
1411    /// client workspace is bound.
1412    pub fn set_server_id(&mut self, server_id: impl Into<String>) -> &mut Self {
1413        self.mode.server_id = Some(server_id.into());
1414        self
1415    }
1416}
1417
1418// Shelved handling getters are available in every delete state.
1419
1420impl<I, S, F> Client<DeleteMode<I, S>, F, Unselected, Unselected>
1421where
1422    I: ServerIdState,
1423    S: ShelvedState,
1424    F: ExclusiveOption,
1425{
1426    /// Get how shelved changes of the deleted client workspace are
1427    /// handled.
1428    pub fn get_shelved(&self) -> Option<ShelvedHandling> {
1429        S::handling()
1430    }
1431}
1432
1433// The `-Fs` transition: available when neither `-Fs` nor `-Fd` is set,
1434// regardless of whether `--serverid=X` is set (the documented
1435// `--serverid=X [-Fs]` form accepts it).
1436
1437impl<I, F> Client<DeleteMode<I, Unselected>, F, Unselected, Unselected>
1438where
1439    I: ServerIdState,
1440    F: ExclusiveOption,
1441{
1442    /// # Description
1443    ///
1444    /// -Fs
1445    ///
1446    /// Allows the deletion with `-d` of a client even when that client
1447    /// contains shelved changes. The client is deleted and the shelved
1448    /// changes are left intact. (You must use the `-f` option with the
1449    /// `-Fs` option.)
1450    ///
1451    /// After this transition neither `-Fs` nor `-Fd` can be selected
1452    /// again; the required `-f` may be selected before or afterwards.
1453    pub fn keep_shelved(self) -> Client<DeleteMode<I, ShelvedKept>, F, Unselected, Unselected> {
1454        Client {
1455            bin: self.bin,
1456            global_opts: self.global_opts,
1457            mode: DeleteMode {
1458                server_id: self.mode.server_id,
1459                serverid_state: PhantomData,
1460                shelved: ShelvedKept,
1461            },
1462            force: self.force,
1463            template: self.template,
1464            workspace_type: self.workspace_type,
1465        }
1466    }
1467}
1468
1469// The `-Fd` transition: only available without `--serverid=X` and without
1470// a previously selected shelved handling.
1471
1472impl<F> Client<DeleteMode<Unselected, Unselected>, F, Unselected, Unselected>
1473where
1474    F: ExclusiveOption,
1475{
1476    /// # Description
1477    ///
1478    /// -Fd
1479    ///
1480    /// Allows the deletion with `-d` of a client even when that client
1481    /// contains shelved changes. The client and the shelved changes are
1482    /// both deleted. (You must use the `-f` option with the `-Fd`
1483    /// option.)
1484    ///
1485    /// This is the terminal shelved state: it cannot be combined with
1486    /// `--serverid=X`, and neither `-Fs` nor `-Fd` can be selected
1487    /// again; the required `-f` may be selected before or afterwards.
1488    pub fn delete_shelved(
1489        self,
1490    ) -> Client<DeleteMode<Unselected, ShelvedDeleted>, F, Unselected, Unselected> {
1491        Client {
1492            bin: self.bin,
1493            global_opts: self.global_opts,
1494            mode: DeleteMode {
1495                server_id: None,
1496                serverid_state: PhantomData,
1497                shelved: ShelvedDeleted,
1498            },
1499            force: self.force,
1500            template: self.template,
1501            workspace_type: self.workspace_type,
1502        }
1503    }
1504}
1505
1506// ---------------------------------------------------------------------------
1507// StreamMode options
1508// ---------------------------------------------------------------------------
1509
1510impl<O> Client<StreamMode<O>, Unselected, Unselected, Unselected> {
1511    /// Get the stream set with `-S`.
1512    pub fn get_stream(&self) -> &str {
1513        &self.mode.stream
1514    }
1515
1516    /// # Description
1517    ///
1518    /// -S stream
1519    ///
1520    /// Associates the workspace with the specified stream, which is used
1521    /// to generate its workspace view.
1522    pub fn set_stream(&mut self, stream: impl Into<String>) -> &mut Self {
1523        self.mode.stream = stream.into();
1524        self
1525    }
1526
1527    /// # Description
1528    ///
1529    /// -S stream
1530    ///
1531    /// Associates the workspace with the specified stream, which is used
1532    /// to generate its workspace view.
1533    pub fn stream(mut self, stream: impl Into<String>) -> Self {
1534        self.set_stream(stream);
1535        self
1536    }
1537}
1538
1539// Adding `-s` to a selected `-S stream` reaches the switch form
1540// `p4 client -s [-f] -S stream [name]`; this is only possible before the
1541// `-o` option is selected, since the `-S stream -o` form carries no `-s`.
1542
1543impl Client<StreamMode<Unselected>, Unselected, Unselected, Unselected> {
1544    /// # Description
1545    ///
1546    /// -s -S stream
1547    ///
1548    /// Switch the workspace view to the stream already selected with
1549    /// [`Client::stream`], changing the stream associated with the
1550    /// workspace.
1551    ///
1552    /// This transitions into the [`SwitchViewMode`] state. The
1553    /// intermediate `-S stream` form does not accept `-f`; the optional
1554    /// `-f` can be added after this transition with [`Client::force`].
1555    pub fn switch_view(
1556        self,
1557    ) -> Client<SwitchViewMode<StreamSwitch>, Unselected, Unselected, Unselected> {
1558        Client {
1559            bin: self.bin,
1560            global_opts: self.global_opts,
1561            mode: SwitchViewMode {
1562                view: StreamSwitch {
1563                    stream: self.mode.stream,
1564                },
1565            },
1566            force: self.force,
1567            template: self.template,
1568            workspace_type: self.workspace_type,
1569        }
1570    }
1571
1572    /// # Description
1573    ///
1574    /// -o
1575    ///
1576    /// When used with `-S stream`, displays the workspace specification
1577    /// that would have been created for the stream.
1578    ///
1579    /// Unlocks the [`Client::change`] option.
1580    pub fn stdout(self) -> Client<StreamMode<StdoutOutput>, Unselected, Unselected, Unselected> {
1581        Client {
1582            bin: self.bin,
1583            global_opts: self.global_opts,
1584            mode: StreamMode {
1585                stream: self.mode.stream,
1586                output: StdoutOutput { change: None },
1587            },
1588            force: self.force,
1589            template: self.template,
1590            workspace_type: self.workspace_type,
1591        }
1592    }
1593}
1594
1595impl Client<StreamMode<StdoutOutput>, Unselected, Unselected, Unselected> {
1596    /// Get the changelist number set with `-c`.
1597    pub fn get_change(&self) -> Option<&str> {
1598        self.mode.output.change.as_deref()
1599    }
1600
1601    /// # Description
1602    ///
1603    /// -c change
1604    ///
1605    /// When used with `-S stream` and `-o`, displays the workspace
1606    /// specification that would have been created for a stream at the
1607    /// moment the change was submitted.
1608    pub fn set_change(&mut self, change: impl Into<String>) -> &mut Self {
1609        self.mode.output.change = Some(change.into());
1610        self
1611    }
1612
1613    /// # Description
1614    ///
1615    /// -c change
1616    ///
1617    /// When used with `-S stream` and `-o`, displays the workspace
1618    /// specification that would have been created for a stream at the
1619    /// moment the change was submitted.
1620    pub fn change(mut self, change: impl Into<String>) -> Self {
1621        self.set_change(change);
1622        self
1623    }
1624}
1625
1626// ---------------------------------------------------------------------------
1627// SwitchViewMode accessors
1628// ---------------------------------------------------------------------------
1629
1630impl<F> Client<SwitchViewMode<StreamSwitch>, F, Unselected, Unselected> {
1631    /// Get the stream the workspace view is switched to.
1632    pub fn get_stream(&self) -> &str {
1633        &self.mode.view.stream
1634    }
1635
1636    /// # Description
1637    ///
1638    /// -S stream
1639    ///
1640    /// Switch the workspace view to the specified stream.
1641    pub fn set_stream(&mut self, stream: impl Into<String>) -> &mut Self {
1642        self.mode.view.stream = stream.into();
1643        self
1644    }
1645
1646    /// # Description
1647    ///
1648    /// -S stream
1649    ///
1650    /// Switch the workspace view to the specified stream.
1651    pub fn stream(mut self, stream: impl Into<String>) -> Self {
1652        self.set_stream(stream);
1653        self
1654    }
1655}
1656
1657impl<F> Client<SwitchViewMode<TemplateSwitch>, F, Unselected, Unselected> {
1658    /// Get the template workspace whose view is switched to.
1659    pub fn get_template(&self) -> &str {
1660        &self.mode.view.template
1661    }
1662
1663    /// # Description
1664    ///
1665    /// -t template
1666    ///
1667    /// Switch the workspace view to the view defined for another
1668    /// workspace.
1669    pub fn set_template(&mut self, template: impl Into<String>) -> &mut Self {
1670        self.mode.view.template = template.into();
1671        self
1672    }
1673
1674    /// # Description
1675    ///
1676    /// -t template
1677    ///
1678    /// Switch the workspace view to the view defined for another
1679    /// workspace.
1680    pub fn template(mut self, template: impl Into<String>) -> Self {
1681        self.set_template(template);
1682        self
1683    }
1684}
1685
1686#[cfg(test)]
1687mod tests {
1688    use super::*;
1689    use crate::cmd::args_of;
1690
1691    /// Dry-run checks of the assembled `p4 client` command line; no process
1692    /// is spawned.
1693    #[test]
1694    fn general_mode_without_options() {
1695        let client = Client::new("p4", GlobalOpts::new());
1696
1697        assert_eq!(args_of(&client.setup_command("p4")), ["client"]);
1698    }
1699
1700    #[test]
1701    fn general_mode_with_all_options() {
1702        let client = Client::new("p4", GlobalOpts::new())
1703            .force()
1704            .template("tpl")
1705            .workspace_type(WorkSpaceType::Readonly);
1706
1707        assert_eq!(
1708            args_of(&client.setup_command("p4")),
1709            ["client", "-f", "-t", "tpl", "-T", "readonly"]
1710        );
1711        assert_eq!(client.get_template(), "tpl");
1712        assert_eq!(client.get_workspace_type(), WorkSpaceType::Readonly);
1713    }
1714
1715    #[test]
1716    fn workspace_type_values() {
1717        for (ty, s) in [
1718            (WorkSpaceType::Writable, "writeable"),
1719            (WorkSpaceType::Readonly, "readonly"),
1720            (WorkSpaceType::Partitioned, "partitioned"),
1721            (WorkSpaceType::PartitionedJnl, "partitioned-jnl"),
1722            (WorkSpaceType::Graph, "graph"),
1723        ] {
1724            let client = Client::new("p4", GlobalOpts::new()).workspace_type(ty);
1725            assert_eq!(args_of(&client.setup_command("p4")), ["client", "-T", s]);
1726        }
1727    }
1728
1729    #[test]
1730    fn stdout_mode_carries_template_and_type() {
1731        let client = Client::new("p4", GlobalOpts::new())
1732            .template("tpl")
1733            .workspace_type(WorkSpaceType::Graph)
1734            .stdout();
1735
1736        assert_eq!(
1737            args_of(&client.setup_command("p4")),
1738            ["client", "-o", "-t", "tpl", "-T", "graph"]
1739        );
1740    }
1741
1742    #[test]
1743    fn delete_mode_is_terminal() {
1744        let client = Client::new("p4", GlobalOpts::new()).delete();
1745
1746        assert_eq!(args_of(&client.setup_command("p4")), ["client", "-d"]);
1747    }
1748
1749    #[test]
1750    fn delete_mode_with_force_unlocks_shelved_and_server_id() {
1751        // `--serverid=X` followed by `-Fs`: the documented
1752        // `-d -f --serverid=X [-Fs]` form.
1753        let mut client = Client::new("p4", GlobalOpts::new())
1754            .delete()
1755            .force()
1756            .server_id("edge.1667");
1757
1758        assert_eq!(client.get_server_id(), "edge.1667");
1759        assert_eq!(client.get_shelved(), None);
1760        client.set_server_id("edge.42");
1761
1762        let client = client.keep_shelved();
1763
1764        assert_eq!(client.get_server_id(), "edge.42");
1765        assert_eq!(client.get_shelved(), Some(ShelvedHandling::Keep));
1766        assert_eq!(
1767            args_of(&client.setup_command("p4")),
1768            ["client", "-d", "-f", "--serverid=edge.42", "-Fs"]
1769        );
1770
1771        // `-Fs` selected first, then `--serverid=X`: the same documented
1772        // form reached in the opposite order.
1773        let client = Client::new("p4", GlobalOpts::new())
1774            .delete()
1775            .force()
1776            .keep_shelved()
1777            .server_id("edge.1667");
1778
1779        assert_eq!(client.get_server_id(), "edge.1667");
1780        assert_eq!(
1781            args_of(&client.setup_command("p4")),
1782            ["client", "-d", "-f", "--serverid=edge.1667", "-Fs"]
1783        );
1784
1785        // `-Fd` without `--serverid=X`: the `-d -f -Fd` form.
1786        let client = Client::new("p4", GlobalOpts::new())
1787            .delete()
1788            .force()
1789            .delete_shelved();
1790
1791        assert_eq!(client.get_shelved(), Some(ShelvedHandling::Delete));
1792        assert_eq!(
1793            args_of(&client.setup_command("p4")),
1794            ["client", "-d", "-f", "-Fd"]
1795        );
1796    }
1797
1798    #[test]
1799    fn force_set_in_general_mode_can_delete() {
1800        let client = Client::new("p4", GlobalOpts::new()).force().delete();
1801
1802        assert_eq!(args_of(&client.setup_command("p4")), ["client", "-d", "-f"]);
1803    }
1804
1805    #[test]
1806    fn force_unlocks_one_shot_delete_transitions() {
1807        let client = Client::new("p4", GlobalOpts::new())
1808            .force()
1809            .server_id("edge.1667")
1810            .keep_shelved();
1811
1812        assert_eq!(client.get_server_id(), "edge.1667");
1813        assert_eq!(client.get_shelved(), Some(ShelvedHandling::Keep));
1814        assert_eq!(
1815            args_of(&client.setup_command("p4")),
1816            ["client", "-d", "-f", "--serverid=edge.1667", "-Fs"]
1817        );
1818
1819        let client = Client::new("p4", GlobalOpts::new())
1820            .force()
1821            .delete_shelved();
1822
1823        assert_eq!(client.get_shelved(), Some(ShelvedHandling::Delete));
1824        assert_eq!(
1825            args_of(&client.setup_command("p4")),
1826            ["client", "-d", "-f", "-Fd"]
1827        );
1828
1829        let client = Client::new("p4", GlobalOpts::new()).force().keep_shelved();
1830
1831        assert_eq!(client.get_shelved(), Some(ShelvedHandling::Keep));
1832        assert_eq!(
1833            args_of(&client.setup_command("p4")),
1834            ["client", "-d", "-f", "-Fs"]
1835        );
1836    }
1837
1838    #[test]
1839    fn stream_mode() {
1840        let client = Client::new("p4", GlobalOpts::new()).stream("//stream/main");
1841
1842        assert_eq!(
1843            args_of(&client.setup_command("p4")),
1844            ["client", "-S", "//stream/main"]
1845        );
1846        assert_eq!(client.get_stream(), "//stream/main");
1847
1848        let client = client.stdout();
1849
1850        assert_eq!(
1851            args_of(&client.setup_command("p4")),
1852            ["client", "-S", "//stream/main", "-o"]
1853        );
1854
1855        let client = client.change("4321");
1856
1857        assert_eq!(client.get_change(), Some("4321"));
1858        assert_eq!(
1859            args_of(&client.setup_command("p4")),
1860            ["client", "-S", "//stream/main", "-c", "4321", "-o"]
1861        );
1862    }
1863
1864    #[test]
1865    fn switch_view_stream_carries_force() {
1866        let client = Client::new("p4", GlobalOpts::new())
1867            .force()
1868            .switch_view_stream("//stream/dev");
1869
1870        assert_eq!(
1871            args_of(&client.setup_command("p4")),
1872            ["client", "-s", "-f", "-S", "//stream/dev"]
1873        );
1874        assert_eq!(client.get_stream(), "//stream/dev");
1875    }
1876
1877    #[test]
1878    fn switch_view_reuses_stream() {
1879        // `-S stream` followed by `switch_view`: the documented
1880        // `-s -S stream` form reached on the two-step path.
1881        let client = Client::new("p4", GlobalOpts::new())
1882            .stream("//stream/dev")
1883            .switch_view();
1884
1885        assert_eq!(
1886            args_of(&client.setup_command("p4")),
1887            ["client", "-s", "-S", "//stream/dev"]
1888        );
1889        assert_eq!(client.get_stream(), "//stream/dev");
1890
1891        // The intermediate `-S` form rejects `-f`; it is added after the
1892        // switch.
1893        let client = Client::new("p4", GlobalOpts::new())
1894            .stream("//stream/dev")
1895            .switch_view()
1896            .force();
1897
1898        assert_eq!(
1899            args_of(&client.setup_command("p4")),
1900            ["client", "-s", "-f", "-S", "//stream/dev"]
1901        );
1902
1903        // The stream value remains editable on the intermediate form.
1904        let client = Client::new("p4", GlobalOpts::new())
1905            .stream("old")
1906            .stream("//stream/dev")
1907            .switch_view();
1908
1909        assert_eq!(
1910            args_of(&client.setup_command("p4")),
1911            ["client", "-s", "-S", "//stream/dev"]
1912        );
1913    }
1914
1915    #[test]
1916    fn switch_view_template() {
1917        let client = Client::new("p4", GlobalOpts::new()).switch_view_template("tpl");
1918
1919        assert_eq!(
1920            args_of(&client.setup_command("p4")),
1921            ["client", "-s", "-t", "tpl"]
1922        );
1923        assert_eq!(client.get_template(), "tpl");
1924    }
1925
1926    #[test]
1927    fn switch_view_reuses_template() {
1928        let client = Client::new("p4", GlobalOpts::new())
1929            .template("tpl")
1930            .switch_view();
1931
1932        assert_eq!(
1933            args_of(&client.setup_command("p4")),
1934            ["client", "-s", "-t", "tpl"]
1935        );
1936        assert_eq!(client.get_template(), "tpl");
1937    }
1938
1939    #[test]
1940    fn stdin_mode() {
1941        let client = Client::new("p4", GlobalOpts::new()).stdin();
1942
1943        assert_eq!(args_of(&client.setup_command("p4")), ["client", "-i"]);
1944
1945        let client = Client::new("p4", GlobalOpts::new()).force().stdin();
1946
1947        assert_eq!(args_of(&client.setup_command("p4")), ["client", "-i", "-f"]);
1948    }
1949
1950    #[test]
1951    fn global_opts_are_injected_once() {
1952        let client = Client::new("p4", GlobalOpts::new().port("localhost:1666"));
1953
1954        assert_eq!(
1955            args_of(&client.setup_command("p4")),
1956            ["-p", "localhost:1666", "client"]
1957        );
1958    }
1959}