Skip to main content

perforce_cli/cmd/
branch.rs

1use std::{
2    ffi::OsStr,
3    path::{Path, PathBuf},
4    process::{Child, Command, Stdio},
5};
6
7use super::{ExclusiveOption, SubCommand, Unselected};
8
9use crate::global::GlobalOpts;
10use crate::spawn::ParameterizedSpawn;
11
12// ---------------------------------------------------------------------------
13// Mode structs
14// ---------------------------------------------------------------------------
15
16/// The general form: `p4 branch [-f] branchspec`.
17///
18/// Create or edit a branch mapping. The `-f` flag permits administrators to
19/// change the modification date of the branch mapping.
20#[derive(Debug, Clone, Default, PartialEq, Eq)]
21pub struct GeneralMode {
22    force: bool,
23}
24
25/// The delete form: `p4 branch -d [-f] branchspec`.
26///
27/// Delete the named branch mapping. The `-f` flag permits P4 Server
28/// administrators to delete branches they don't own.
29#[derive(Debug, Clone, Default, PartialEq, Eq)]
30pub struct DeleteMode {
31    force: bool,
32}
33
34/// The stdin form: `p4 branch -i [-f]`.
35///
36/// Read the branch mapping from standard input without invoking an editor.
37#[derive(Debug, Clone, Default, PartialEq, Eq)]
38pub struct StdinMode {
39    force: bool,
40}
41
42/// The stdout form: `p4 branch [-S stream [-P parent]] -o placeholder`.
43///
44/// Write the branch mapping to standard output without invoking an editor.
45/// The optional `-S stream` displays the mapping generated for the specified
46/// stream; `-P parent` requires `-S` and evaluates the stream as if it were
47/// a child of the given parent.
48#[derive(Debug, Clone, Default, PartialEq, Eq)]
49pub struct StdoutMode {
50    stream: Option<String>,
51    parent: Option<String>,
52}
53
54// ---------------------------------------------------------------------------
55// ExclusiveOption implementations
56// ---------------------------------------------------------------------------
57
58impl ExclusiveOption for GeneralMode {
59    fn inject_args(&self, command: &mut Command) {
60        if self.force {
61            command.arg("-f");
62        }
63    }
64}
65
66impl ExclusiveOption for DeleteMode {
67    fn inject_args(&self, command: &mut Command) {
68        command.arg("-d");
69        if self.force {
70            command.arg("-f");
71        }
72    }
73}
74
75impl ExclusiveOption for StdinMode {
76    fn inject_args(&self, command: &mut Command) {
77        command.arg("-i");
78        if self.force {
79            command.arg("-f");
80        }
81    }
82}
83
84impl ExclusiveOption for StdoutMode {
85    fn inject_args(&self, command: &mut Command) {
86        if let Some(stream) = &self.stream {
87            command.arg("-S").arg(stream);
88        }
89        if let Some(parent) = &self.parent {
90            command.arg("-P").arg(parent);
91        }
92        command.arg("-o");
93    }
94}
95
96// ---------------------------------------------------------------------------
97// Command struct
98// ---------------------------------------------------------------------------
99
100/// Type-safe builder for `p4 branch`.
101///
102/// Create or edit a branch mapping and its view.
103///
104/// # Syntax
105///
106/// ```text
107/// p4 [g-opts] branch [-f] branchspec
108/// p4 [g-opts] branch -d [-f] branchspec
109/// p4 [g-opts] branch -i [-f]
110/// p4 [g-opts] branch [-S stream] -o placeholderForStreamBranchSpecName
111/// p4 [g-opts] branch [-S stream -P parent] -o placeholderForStreamBranchSpecName
112/// ```
113///
114/// The `M` type parameter tracks the command form at compile time, starting
115/// as [`Unselected`] from [`Branch::new`]. Enter a form with one of
116/// [`Self::force`], [`Self::delete`], [`Self::stdin`], or [`Self::stdout`]:
117/// - [`GeneralMode`] (entered via [`Self::force`]): create or edit a branch
118///   mapping; can transition to [`DeleteMode`] or [`StdinMode`] preserving
119///   the `-f` flag.
120/// - [`DeleteMode`] (`-d`, entered via [`Self::delete`]): delete a branch
121///   mapping.
122/// - [`StdinMode`] (`-i`, entered via [`Self::stdin`]): read the branch
123///   mapping from standard input.
124/// - [`StdoutMode`] (`-o`, entered via [`Self::stdout`]): write the branch
125///   mapping to standard output; the `-S stream` and `-P parent` options
126///   are available as fields on this form.
127#[derive(Debug, Clone, Default)]
128pub struct Branch<M = Unselected> {
129    bin: PathBuf,
130
131    global_opts: GlobalOpts,
132
133    mode: M,
134}
135
136// ---------------------------------------------------------------------------
137// Constructor
138// ---------------------------------------------------------------------------
139
140impl Branch<Unselected> {
141    /// Creates a new `p4 branch` command with no form selected.
142    ///
143    /// `bin` is the path to the Perforce command-line executable. Use
144    /// [`Self::force`], [`Self::delete`], [`Self::stdin`], or
145    /// [`Self::stdout`] to select a form before spawning.
146    pub fn new(bin: impl Into<PathBuf>, global_opts: GlobalOpts) -> Self {
147        Self {
148            bin: bin.into(),
149            global_opts,
150            ..Default::default()
151        }
152    }
153
154    /// Enters the general form: `p4 branch [-f] branchspec`.
155    ///
156    /// `v` sets the initial state of the `-f` flag.
157    pub fn force(self, v: bool) -> Branch<GeneralMode> {
158        Branch {
159            bin: self.bin,
160            global_opts: self.global_opts,
161            mode: GeneralMode { force: v },
162        }
163    }
164
165    /// Enters the delete form: `p4 branch -d [-f] branchspec`.
166    pub fn delete(self) -> Branch<DeleteMode> {
167        Branch {
168            bin: self.bin,
169            global_opts: self.global_opts,
170            mode: DeleteMode { force: false },
171        }
172    }
173
174    /// Enters the stdin form: `p4 branch -i [-f]`.
175    pub fn stdin(self) -> Branch<StdinMode> {
176        Branch {
177            bin: self.bin,
178            global_opts: self.global_opts,
179            mode: StdinMode { force: false },
180        }
181    }
182
183    /// Enters the stdout form: `p4 branch [-S stream [-P parent]] -o
184    /// placeholder`.
185    ///
186    /// Use [`Branch::stream`] and [`Branch::parent`] on the entered form to
187    /// add the `-S` and `-P` options.
188    pub fn stdout(self) -> Branch<StdoutMode> {
189        Branch {
190            bin: self.bin,
191            global_opts: self.global_opts,
192            mode: StdoutMode::default(),
193        }
194    }
195}
196
197// ---------------------------------------------------------------------------
198// Shared accessors
199// ---------------------------------------------------------------------------
200
201impl<M> Branch<M> {
202    /// Get the Perforce command path.
203    pub fn get_bin(&self) -> &Path {
204        &self.bin
205    }
206
207    /// Set the Perforce command path.
208    pub fn set_bin(&mut self, bin: impl Into<PathBuf>) -> &mut Self {
209        self.bin = bin.into();
210        self
211    }
212
213    /// Set the Perforce command path.
214    pub fn bin(mut self, bin: impl Into<PathBuf>) -> Self {
215        self.set_bin(bin);
216        self
217    }
218
219    /// # Description
220    ///
221    /// g-opts
222    ///
223    #[cfg_attr(
224        feature = "lt2014_2",
225        doc = "See the [Global Options](GlobalOpts) section."
226    )]
227    #[cfg_attr(
228        all(feature = "lt2015_1", not(feature = "lt2014_2")),
229        doc = "See the [“Global Options”](GlobalOpts) section."
230    )]
231    #[cfg_attr(
232        all(feature = "lt2017_1", not(feature = "lt2015_1")),
233        doc = "See [“Global Options”](GlobalOpts)."
234    )]
235    #[cfg_attr(
236        all(feature = "lt2018_2", not(feature = "lt2017_1")),
237        doc = "See [Global Options](GlobalOpts)."
238    )]
239    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
240    pub fn get_global_opts(&self) -> &GlobalOpts {
241        &self.global_opts
242    }
243
244    /// # Description
245    ///
246    /// g-opts
247    ///
248    #[cfg_attr(
249        feature = "lt2014_2",
250        doc = "See the [Global Options](GlobalOpts) section."
251    )]
252    #[cfg_attr(
253        all(feature = "lt2015_1", not(feature = "lt2014_2")),
254        doc = "See the [“Global Options”](GlobalOpts) section."
255    )]
256    #[cfg_attr(
257        all(feature = "lt2017_1", not(feature = "lt2015_1")),
258        doc = "See [“Global Options”](GlobalOpts)."
259    )]
260    #[cfg_attr(
261        all(feature = "lt2018_2", not(feature = "lt2017_1")),
262        doc = "See [Global Options](GlobalOpts)."
263    )]
264    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
265    pub fn set_global_opts(&mut self, v: GlobalOpts) -> &mut Self {
266        self.global_opts = v;
267        self
268    }
269
270    /// # Description
271    ///
272    /// g-opts
273    ///
274    #[cfg_attr(
275        feature = "lt2014_2",
276        doc = "See the [Global Options](GlobalOpts) section."
277    )]
278    #[cfg_attr(
279        all(feature = "lt2015_1", not(feature = "lt2014_2")),
280        doc = "See the [“Global Options”](GlobalOpts) section."
281    )]
282    #[cfg_attr(
283        all(feature = "lt2017_1", not(feature = "lt2015_1")),
284        doc = "See [“Global Options”](GlobalOpts)."
285    )]
286    #[cfg_attr(
287        all(feature = "lt2018_2", not(feature = "lt2017_1")),
288        doc = "See [Global Options](GlobalOpts)."
289    )]
290    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
291    pub fn global_opts(mut self, v: GlobalOpts) -> Self {
292        self.global_opts = v;
293        self
294    }
295}
296
297// ---------------------------------------------------------------------------
298// GeneralMode: force accessor + transitions
299// ---------------------------------------------------------------------------
300
301impl Branch<GeneralMode> {
302    /// # Description
303    ///
304    /// -f
305    ///
306    /// Force option. Combined with `-d`, permits P4 Server administrators to
307    /// delete branches they don't own. Also permits administrators to change
308    /// the modification date of the branch mapping (the `Update` field is
309    /// writable when using the `-f` option).
310    pub fn get_force(&self) -> bool {
311        self.mode.force
312    }
313
314    /// # Description
315    ///
316    /// -f
317    ///
318    /// Force option. Combined with `-d`, permits P4 Server administrators to
319    /// delete branches they don't own. Also permits administrators to change
320    /// the modification date of the branch mapping (the `Update` field is
321    /// writable when using the `-f` option).
322    pub fn set_force(&mut self, v: bool) -> &mut Self {
323        self.mode.force = v;
324        self
325    }
326
327    /// # Description
328    ///
329    /// -f
330    ///
331    /// Force option. Combined with `-d`, permits P4 Server administrators to
332    /// delete branches they don't own. Also permits administrators to change
333    /// the modification date of the branch mapping (the `Update` field is
334    /// writable when using the `-f` option).
335    pub fn force(mut self, v: bool) -> Self {
336        self.mode.force = v;
337        self
338    }
339
340    /// Transitions to the delete form (`-d`), preserving the `-f` flag.
341    pub fn delete(self) -> Branch<DeleteMode> {
342        Branch {
343            bin: self.bin,
344            global_opts: self.global_opts,
345            mode: DeleteMode {
346                force: self.mode.force,
347            },
348        }
349    }
350
351    /// Transitions to the stdin form (`-i`), preserving the `-f` flag.
352    pub fn stdin(self) -> Branch<StdinMode> {
353        Branch {
354            bin: self.bin,
355            global_opts: self.global_opts,
356            mode: StdinMode {
357                force: self.mode.force,
358            },
359        }
360    }
361}
362
363// ---------------------------------------------------------------------------
364// DeleteMode: force accessor
365// ---------------------------------------------------------------------------
366
367impl Branch<DeleteMode> {
368    /// # Description
369    ///
370    /// -f
371    ///
372    /// Force option. Permits P4 Server administrators to delete branches they
373    /// don't own.
374    pub fn get_force(&self) -> bool {
375        self.mode.force
376    }
377
378    /// # Description
379    ///
380    /// -f
381    ///
382    /// Force option. Permits P4 Server administrators to delete branches they
383    /// don't own.
384    pub fn set_force(&mut self, v: bool) -> &mut Self {
385        self.mode.force = v;
386        self
387    }
388
389    /// # Description
390    ///
391    /// -f
392    ///
393    /// Force option. Permits P4 Server administrators to delete branches they
394    /// don't own.
395    pub fn force(mut self, v: bool) -> Self {
396        self.mode.force = v;
397        self
398    }
399}
400
401// ---------------------------------------------------------------------------
402// StdinMode: force accessor
403// ---------------------------------------------------------------------------
404
405impl Branch<StdinMode> {
406    /// # Description
407    ///
408    /// -f
409    ///
410    /// Force option. Permits administrators to change the modification date of
411    /// the branch mapping.
412    pub fn get_force(&self) -> bool {
413        self.mode.force
414    }
415
416    /// # Description
417    ///
418    /// -f
419    ///
420    /// Force option. Permits administrators to change the modification date of
421    /// the branch mapping.
422    pub fn set_force(&mut self, v: bool) -> &mut Self {
423        self.mode.force = v;
424        self
425    }
426
427    /// # Description
428    ///
429    /// -f
430    ///
431    /// Force option. Permits administrators to change the modification date of
432    /// the branch mapping.
433    pub fn force(mut self, v: bool) -> Self {
434        self.mode.force = v;
435        self
436    }
437}
438
439// ---------------------------------------------------------------------------
440// StdoutMode: stream and parent accessors
441// ---------------------------------------------------------------------------
442
443impl Branch<StdoutMode> {
444    /// # Description
445    ///
446    /// -S stream
447    ///
448    /// Display the mapping generated for the specified stream. This option
449    /// enables you to see how change is propagated between the stream and its
450    /// parent.
451    pub fn get_stream(&self) -> Option<&String> {
452        self.mode.stream.as_ref()
453    }
454
455    /// # Description
456    ///
457    /// -S stream
458    ///
459    /// Display the mapping generated for the specified stream. This option
460    /// enables you to see how change is propagated between the stream and its
461    /// parent.
462    pub fn set_stream(&mut self, v: impl Into<String>) -> &mut Self {
463        self.mode.stream = Some(v.into());
464        self
465    }
466
467    /// # Description
468    ///
469    /// -S stream
470    ///
471    /// Display the mapping generated for the specified stream. This option
472    /// enables you to see how change is propagated between the stream and its
473    /// parent.
474    pub fn stream(mut self, v: impl Into<String>) -> Self {
475        self.mode.stream = Some(v.into());
476        self
477    }
478
479    /// # Description
480    ///
481    /// -P parent
482    ///
483    /// For a specified stream, display the mapping that is generated by
484    /// hypothetically treating the stream as if it were a child of the
485    /// specified parent. Requires `-S`.
486    pub fn get_parent(&self) -> Option<&String> {
487        self.mode.parent.as_ref()
488    }
489
490    /// # Description
491    ///
492    /// -P parent
493    ///
494    /// For a specified stream, display the mapping that is generated by
495    /// hypothetically treating the stream as if it were a child of the
496    /// specified parent. Requires `-S`.
497    pub fn set_parent(&mut self, v: impl Into<String>) -> &mut Self {
498        self.mode.parent = Some(v.into());
499        self
500    }
501
502    /// # Description
503    ///
504    /// -P parent
505    ///
506    /// For a specified stream, display the mapping that is generated by
507    /// hypothetically treating the stream as if it were a child of the
508    /// specified parent. Requires `-S`.
509    pub fn parent(mut self, v: impl Into<String>) -> Self {
510        self.mode.parent = Some(v.into());
511        self
512    }
513}
514
515// ---------------------------------------------------------------------------
516// SubCommand
517// ---------------------------------------------------------------------------
518
519impl<M: ExclusiveOption> SubCommand for Branch<M> {
520    fn name(&self) -> &str {
521        "branch"
522    }
523
524    fn inject_local_args(&self, command: &mut Command) {
525        self.mode.inject_args(command);
526    }
527
528    fn global_opts(&self) -> Option<&GlobalOpts> {
529        Some(&self.global_opts)
530    }
531}
532
533// ---------------------------------------------------------------------------
534// Spawning
535// ---------------------------------------------------------------------------
536
537// General form: 0 args (open editor) or 1 branchspec.
538impl ParameterizedSpawn<()> for Branch<GeneralMode> {
539    type Output = Child;
540    type Error = std::io::Error;
541
542    /// Spawns `p4 branch` (the general form) without a branchspec argument,
543    /// opening the editor to create a new branch mapping.
544    fn spawn_with(&mut self, (): ()) -> Result<Self::Output, Self::Error> {
545        self.setup_command(&self.bin)
546            .stdout(Stdio::piped())
547            .stderr(Stdio::piped())
548            .spawn()
549    }
550}
551
552impl<I: AsRef<OsStr>> ParameterizedSpawn<(I,)> for Branch<GeneralMode> {
553    type Output = Child;
554    type Error = std::io::Error;
555
556    /// Spawns `p4 branch` with the given `branchspec` argument as a child
557    /// process with piped standard output and error streams; use the
558    /// returned [`Child`] handle to wait for it or interact with it.
559    fn spawn_with(&mut self, (branchspec,): (I,)) -> Result<Self::Output, Self::Error> {
560        self.setup_command(&self.bin)
561            .arg(branchspec)
562            .stdout(Stdio::piped())
563            .stderr(Stdio::piped())
564            .spawn()
565    }
566}
567
568// Delete form: exactly 1 branchspec.
569impl<I: AsRef<OsStr>> ParameterizedSpawn<(I,)> for Branch<DeleteMode> {
570    type Output = Child;
571    type Error = std::io::Error;
572
573    /// Spawns `p4 branch -d` with the given `branchspec` argument as a child
574    /// process with piped standard output and error streams; use the
575    /// returned [`Child`] handle to wait for it or interact with it.
576    fn spawn_with(&mut self, (branchspec,): (I,)) -> Result<Self::Output, Self::Error> {
577        self.setup_command(&self.bin)
578            .arg(branchspec)
579            .stdout(Stdio::piped())
580            .stderr(Stdio::piped())
581            .spawn()
582    }
583}
584
585// Stdin form: no positional argument; stdin is piped.
586impl ParameterizedSpawn<()> for Branch<StdinMode> {
587    type Output = Child;
588    type Error = std::io::Error;
589
590    /// Spawns `p4 branch -i` with a piped standard input stream (and piped
591    /// standard output and error streams); write the branch mapping
592    /// specification to the returned [`Child`]'s stdin handle.
593    fn spawn_with(&mut self, (): ()) -> Result<Self::Output, Self::Error> {
594        self.setup_command(&self.bin)
595            .stdin(Stdio::piped())
596            .stdout(Stdio::piped())
597            .stderr(Stdio::piped())
598            .spawn()
599    }
600}
601
602impl ParameterizedSpawn<(Stdio,)> for Branch<StdinMode> {
603    type Output = Child;
604    type Error = std::io::Error;
605
606    /// Spawns `p4 branch -i` with the given standard input configuration and
607    /// piped standard output and error streams; use the returned [`Child`]
608    /// handle to wait for it or interact with it.
609    ///
610    /// Pass `Stdio::piped()` to obtain a writable `child.stdin` handle and
611    /// feed the branch mapping specification into it; pass `Stdio::inherit()`
612    /// to read the specification from the spawning process's own standard
613    /// input.
614    fn spawn_with(&mut self, (stdio,): (Stdio,)) -> Result<Self::Output, Self::Error> {
615        self.setup_command(&self.bin)
616            .stdin(stdio)
617            .stdout(Stdio::piped())
618            .stderr(Stdio::piped())
619            .spawn()
620    }
621}
622
623// Stdout form: exactly 1 placeholder.
624impl<I: AsRef<OsStr>> ParameterizedSpawn<(I,)> for Branch<StdoutMode> {
625    type Output = Child;
626    type Error = std::io::Error;
627
628    /// Spawns `p4 branch [-S stream [-P parent]] -o` with the given placeholder
629    /// argument as a child process with piped standard output and error
630    /// streams; use the returned [`Child`] handle to wait for it or interact
631    /// with it.
632    fn spawn_with(&mut self, (placeholder,): (I,)) -> Result<Self::Output, Self::Error> {
633        self.setup_command(&self.bin)
634            .arg(placeholder)
635            .stdout(Stdio::piped())
636            .stderr(Stdio::piped())
637            .spawn()
638    }
639}
640
641#[cfg(test)]
642mod tests {
643    use super::*;
644    use crate::cmd::args_of;
645
646    #[test]
647    fn bin_accessors_update_executable_path() {
648        let mut command = Branch::new("p4", GlobalOpts::new());
649        assert_eq!(command.get_bin(), Path::new("p4"));
650
651        command.set_bin("/usr/local/bin/p4");
652        assert_eq!(command.get_bin(), Path::new("/usr/local/bin/p4"));
653
654        let command = command.bin("p4.exe");
655        assert_eq!(command.get_bin(), Path::new("p4.exe"));
656    }
657
658    // Compile-time rejection: Branch<Unselected> has no spawn impl.
659    // Compile-time rejection: Branch<StdoutMode> has no `force()` method
660    // (stdout form does not accept -f).
661
662    /// Dry-run checks of the assembled `p4 branch` command line; no process
663    /// is spawned.
664    #[test]
665    fn general_form_without_force() {
666        let branch = Branch::new("p4", GlobalOpts::new()).force(false);
667        assert_eq!(args_of(&branch.setup_command("p4")), ["branch"]);
668    }
669
670    #[test]
671    fn general_form_with_force() {
672        let branch = Branch::new("p4", GlobalOpts::new()).force(true);
673        assert_eq!(args_of(&branch.setup_command("p4")), ["branch", "-f"]);
674    }
675
676    #[test]
677    fn delete_form_from_unselected() {
678        let branch = Branch::new("p4", GlobalOpts::new()).delete();
679        assert_eq!(args_of(&branch.setup_command("p4")), ["branch", "-d"]);
680    }
681
682    #[test]
683    fn delete_form_preserves_force_from_general() {
684        let branch = Branch::new("p4", GlobalOpts::new()).force(true).delete();
685        assert_eq!(args_of(&branch.setup_command("p4")), ["branch", "-d", "-f"]);
686    }
687
688    #[test]
689    fn stdin_form_from_unselected() {
690        let branch = Branch::new("p4", GlobalOpts::new()).stdin();
691        assert_eq!(args_of(&branch.setup_command("p4")), ["branch", "-i"]);
692    }
693
694    #[test]
695    fn stdin_form_preserves_force_from_general() {
696        let branch = Branch::new("p4", GlobalOpts::new()).force(true).stdin();
697        assert_eq!(args_of(&branch.setup_command("p4")), ["branch", "-i", "-f"]);
698    }
699
700    #[test]
701    fn stdout_form_from_unselected() {
702        let branch = Branch::new("p4", GlobalOpts::new()).stdout();
703        assert_eq!(args_of(&branch.setup_command("p4")), ["branch", "-o"]);
704    }
705
706    #[test]
707    fn stdout_with_stream() {
708        let branch = Branch::new("p4", GlobalOpts::new())
709            .stdout()
710            .stream("//depotstream1/5dev1");
711        assert_eq!(
712            args_of(&branch.setup_command("p4")),
713            ["branch", "-S", "//depotstream1/5dev1", "-o"]
714        );
715    }
716
717    #[test]
718    fn stdout_with_stream_and_parent() {
719        let branch = Branch::new("p4", GlobalOpts::new())
720            .stdout()
721            .stream("//stream1/5dev1")
722            .parent("//stream1/main5");
723        assert_eq!(
724            args_of(&branch.setup_command("p4")),
725            [
726                "branch",
727                "-S",
728                "//stream1/5dev1",
729                "-P",
730                "//stream1/main5",
731                "-o"
732            ]
733        );
734    }
735
736    #[test]
737    fn force_can_be_set_inside_general_delete_and_stdin() {
738        let general = Branch::new("p4", GlobalOpts::new())
739            .force(false)
740            .force(true);
741        assert_eq!(args_of(&general.setup_command("p4")), ["branch", "-f"]);
742
743        let delete = Branch::new("p4", GlobalOpts::new()).delete().force(true);
744        assert_eq!(args_of(&delete.setup_command("p4")), ["branch", "-d", "-f"]);
745
746        let stdin = Branch::new("p4", GlobalOpts::new()).stdin().force(true);
747        assert_eq!(args_of(&stdin.setup_command("p4")), ["branch", "-i", "-f"]);
748    }
749
750    #[test]
751    fn force_can_be_unset() {
752        let branch = Branch::new("p4", GlobalOpts::new())
753            .force(true)
754            .force(false);
755        assert_eq!(args_of(&branch.setup_command("p4")), ["branch"]);
756    }
757
758    #[test]
759    fn setters_update_the_same_command() {
760        let mut branch = Branch::new("p4", GlobalOpts::new()).force(false);
761        branch.set_force(true);
762        assert!(branch.get_force());
763
764        let mut stdout = Branch::new("p4", GlobalOpts::new()).stdout();
765        stdout.set_stream("//depot/main");
766        stdout.set_parent("//depot/dev");
767        assert_eq!(stdout.get_stream(), Some(&String::from("//depot/main")));
768        assert_eq!(stdout.get_parent(), Some(&String::from("//depot/dev")));
769    }
770
771    #[test]
772    fn global_opts_are_injected_once() {
773        let branch = Branch::new("p4", GlobalOpts::new().port("localhost:1666")).force(false);
774        assert_eq!(
775            args_of(&branch.setup_command("p4")),
776            ["-p", "localhost:1666", "branch"]
777        );
778    }
779
780    // Spawn argument counts per form.
781    #[test]
782    fn spawn_argument_counts_per_form() {
783        // General: 0 or 1 branchspec.
784        let plain = Branch::new("p4", GlobalOpts::new()).force(false);
785        assert_eq!(args_of(&plain.setup_command("p4")), ["branch"]);
786        let mut with_bs = plain.setup_command("p4");
787        with_bs.arg("mybranch");
788        assert_eq!(args_of(&with_bs), ["branch", "mybranch"]);
789
790        // Delete: exactly 1 branchspec.
791        let delete = Branch::new("p4", GlobalOpts::new()).force(true).delete();
792        let mut delete_bs = delete.setup_command("p4");
793        delete_bs.arg("oldbranch");
794        assert_eq!(args_of(&delete_bs), ["branch", "-d", "-f", "oldbranch"]);
795
796        // Stdin: no positional; stdin is configured separately.
797        let stdin = Branch::new("p4", GlobalOpts::new()).stdin();
798        assert_eq!(args_of(&stdin.setup_command("p4")), ["branch", "-i"]);
799
800        // Stdout: exactly 1 placeholder.
801        let stdout = Branch::new("p4", GlobalOpts::new()).stdout();
802        let mut stdout_ph = stdout.setup_command("p4");
803        stdout_ph.arg("placeholder1");
804        assert_eq!(args_of(&stdout_ph), ["branch", "-o", "placeholder1"]);
805
806        // Stdout with stream and parent: exactly 1 placeholder.
807        let stream = Branch::new("p4", GlobalOpts::new())
808            .stdout()
809            .stream("//depotstream1/5dev1")
810            .parent("//stream1/main5");
811        let mut stream_ph = stream.setup_command("p4");
812        stream_ph.arg("placeholder2");
813        assert_eq!(
814            args_of(&stream_ph),
815            [
816                "branch",
817                "-S",
818                "//depotstream1/5dev1",
819                "-P",
820                "//stream1/main5",
821                "-o",
822                "placeholder2"
823            ]
824        );
825    }
826}