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}