Skip to main content

perforce_cli/cmd/
branches.rs

1use std::{
2    path::{Path, PathBuf},
3    process::{Child, Command, Stdio},
4};
5
6use super::{ExclusiveOption, SubCommand, Unselected};
7
8use crate::global::GlobalOpts;
9use crate::spawn::ParameterizedSpawn;
10
11// ---------------------------------------------------------------------------
12// Name filter (-e / -E)
13// ---------------------------------------------------------------------------
14
15/// The `-e` / `-E` name filter: list only branches whose name matches the
16/// filter pattern.
17///
18/// Created by [`Branches::filter`]; the `case_insensitive` parameter selects
19/// [`Filter::CaseSensitiveMode`] (`-e`) or [`Filter::InCaseSensitiveMode`]
20/// (`-E`).
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum Filter {
23    /// `-e`: match the pattern using the server's normal case-sensitivity
24    /// rules.
25    CaseSensitiveMode(String),
26
27    /// `-E`: match the pattern case-insensitively, even on a case-sensitive
28    /// server.
29    InCaseSensitiveMode(String),
30}
31
32impl ExclusiveOption for Filter {
33    fn inject_args(&self, command: &mut Command) {
34        match self {
35            Filter::CaseSensitiveMode(name) => {
36                command.arg("-e").arg(name);
37            }
38            Filter::InCaseSensitiveMode(name) => {
39                command.arg("-E").arg(name);
40            }
41        }
42    }
43}
44
45// ---------------------------------------------------------------------------
46// Owner filter states (-u user / --me)
47// ---------------------------------------------------------------------------
48
49/// The `-u user` / `--me` owner filter: list only branches owned by the
50/// specified user (or the current user).
51///
52/// Created by [`Branches::user`] or [`Branches::filter_me`]; the enum holds
53/// exactly one of the two mutually exclusive choices.
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub enum User {
56    /// `-u user`: list branches owned by the named user; the flag selects
57    /// the `--user-case-insensitive` suboption.
58    Named {
59        name: String,
60
61        #[cfg(not(feature = "lt2024_2"))]
62        case_insensitive: bool,
63    },
64
65    /// `--me`: list branches owned by the current user (equivalent to
66    /// `-u $P4USER`).
67    #[cfg(not(feature = "lt2016_1"))]
68    Me,
69}
70
71impl ExclusiveOption for User {
72    fn inject_args(&self, command: &mut Command) {
73        match self {
74            User::Named {
75                name,
76                #[cfg(not(feature = "lt2024_2"))]
77                case_insensitive,
78            } => {
79                command.arg("-u").arg(name);
80                #[cfg(not(feature = "lt2024_2"))]
81                if *case_insensitive {
82                    command.arg("--user-case-insensitive");
83                }
84            }
85            #[cfg(not(feature = "lt2016_1"))]
86            User::Me => {
87                command.arg("--me");
88            }
89        }
90    }
91}
92
93// ---------------------------------------------------------------------------
94// Command struct
95// ---------------------------------------------------------------------------
96
97#[cfg_attr(
98    feature = "lt2016_1",
99    doc = "`p4 [g-opts] branches [-t] [-u user] [[-e|-E]filter -mmax]`"
100)]
101#[cfg_attr(
102    all(feature = "lt2024_2", not(feature = "lt2016_1")),
103    doc = "`p4 [g-opts] branches [[-e | -E]filter] [-mmax][-t] [-u user | --me]`"
104)]
105#[cfg_attr(
106    not(feature = "lt2024_2"),
107    doc = "`p4 [g-opts] branches [[-e | -E] nameFilter] [-m max][-t] [-u user [--user-case-insensitive] | --me]`"
108)]
109///
110/// List existing branch mappings.
111///
112/// Print the list of all branch mappings currently known to the system.
113///
114/// The `F` type parameter tracks the `-e` / `-E` name filter at compile
115/// time, starting as [`Unselected`]: [`Self::filter`] transitions to the
116/// [`Filter`] state, which carries both the pattern and its case
117/// sensitivity.
118///
119/// The `U` type parameter tracks the owner filter at compile time, starting
120/// as [`Unselected`]:
121#[cfg_attr(feature = "lt2016_1", doc = "[`User`] (entered via [`Self::user`]).")]
122#[cfg_attr(
123    not(feature = "lt2016_1"),
124    doc = "[`User`] (entered via [`Self::user`] or [`Self::filter_me`])."
125)]
126///
127/// Each filter can be set at most once; the remaining options (`-m`, `-t`,
128/// and the other filter) stay available afterwards.
129#[derive(Debug, Clone, Default)]
130pub struct Branches<F = Unselected, U = Unselected> {
131    bin: PathBuf,
132
133    global_opts: GlobalOpts,
134
135    limit: Option<u64>,
136
137    time: bool,
138
139    user_filter: U,
140
141    name_filter: F,
142}
143
144// ---------------------------------------------------------------------------
145// Constructor
146// ---------------------------------------------------------------------------
147
148impl Branches<Unselected, Unselected> {
149    /// Creates a new `p4 branches` command.
150    ///
151    /// `bin` is the path to the Perforce command-line executable.
152    pub fn new(bin: impl Into<PathBuf>, global_opts: GlobalOpts) -> Self {
153        Self {
154            bin: bin.into(),
155            global_opts,
156            ..Default::default()
157        }
158    }
159}
160
161// ---------------------------------------------------------------------------
162// Shared accessors
163// ---------------------------------------------------------------------------
164
165impl<F, U> Branches<F, U> {
166    /// Get the Perforce command path.
167    pub fn get_bin(&self) -> &Path {
168        &self.bin
169    }
170
171    /// Set the Perforce command path.
172    pub fn set_bin(&mut self, bin: impl Into<PathBuf>) -> &mut Self {
173        self.bin = bin.into();
174        self
175    }
176
177    /// Set the Perforce command path.
178    pub fn bin(mut self, bin: impl Into<PathBuf>) -> Self {
179        self.set_bin(bin);
180        self
181    }
182
183    /// # Description
184    ///
185    /// g-opts
186    ///
187    #[cfg_attr(
188        feature = "lt2014_2",
189        doc = "See the [Global Options](GlobalOpts) section."
190    )]
191    #[cfg_attr(
192        all(feature = "lt2015_1", not(feature = "lt2014_2")),
193        doc = "See the [“Global Options”](GlobalOpts) section."
194    )]
195    #[cfg_attr(
196        all(feature = "lt2017_1", not(feature = "lt2015_1")),
197        doc = "See [“Global Options”](GlobalOpts)."
198    )]
199    #[cfg_attr(
200        all(feature = "lt2018_2", not(feature = "lt2017_1")),
201        doc = "See [Global Options](GlobalOpts)."
202    )]
203    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
204    pub fn get_global_opts(&self) -> &GlobalOpts {
205        &self.global_opts
206    }
207
208    /// # Description
209    ///
210    /// g-opts
211    ///
212    #[cfg_attr(
213        feature = "lt2014_2",
214        doc = "See the [Global Options](GlobalOpts) section."
215    )]
216    #[cfg_attr(
217        all(feature = "lt2015_1", not(feature = "lt2014_2")),
218        doc = "See the [“Global Options”](GlobalOpts) section."
219    )]
220    #[cfg_attr(
221        all(feature = "lt2017_1", not(feature = "lt2015_1")),
222        doc = "See [“Global Options”](GlobalOpts)."
223    )]
224    #[cfg_attr(
225        all(feature = "lt2018_2", not(feature = "lt2017_1")),
226        doc = "See [Global Options](GlobalOpts)."
227    )]
228    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
229    pub fn set_global_opts(&mut self, v: GlobalOpts) -> &mut Self {
230        self.global_opts = v;
231        self
232    }
233
234    /// # Description
235    ///
236    /// g-opts
237    ///
238    #[cfg_attr(
239        feature = "lt2014_2",
240        doc = "See the [Global Options](GlobalOpts) section."
241    )]
242    #[cfg_attr(
243        all(feature = "lt2015_1", not(feature = "lt2014_2")),
244        doc = "See the [“Global Options”](GlobalOpts) section."
245    )]
246    #[cfg_attr(
247        all(feature = "lt2017_1", not(feature = "lt2015_1")),
248        doc = "See [“Global Options”](GlobalOpts)."
249    )]
250    #[cfg_attr(
251        all(feature = "lt2018_2", not(feature = "lt2017_1")),
252        doc = "See [Global Options](GlobalOpts)."
253    )]
254    #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
255    pub fn global_opts(mut self, v: GlobalOpts) -> Self {
256        self.global_opts = v;
257        self
258    }
259
260    /// # Description
261    ///
262    /// -m max
263    ///
264    /// List only the first `max` branch mappings.
265    pub fn get_limit(&self) -> Option<u64> {
266        self.limit
267    }
268
269    /// # Description
270    ///
271    /// -m max
272    ///
273    /// List only the first `max` branch mappings.
274    pub fn set_limit(&mut self, v: u64) -> &mut Self {
275        self.limit = Some(v);
276        self
277    }
278
279    /// # Description
280    ///
281    /// -m max
282    ///
283    /// List only the first `max` branch mappings.
284    pub fn limit(mut self, v: u64) -> Self {
285        self.limit = Some(v);
286        self
287    }
288
289    /// # Description
290    ///
291    /// -t
292    ///
293    /// Display the time as well as the date of the last update to the branch.
294    pub fn get_display_time(&self) -> bool {
295        self.time
296    }
297
298    /// # Description
299    ///
300    /// -t
301    ///
302    /// Display the time as well as the date of the last update to the branch.
303    pub fn set_display_time(&mut self, v: bool) -> &mut Self {
304        self.time = v;
305        self
306    }
307
308    /// # Description
309    ///
310    /// -t
311    ///
312    /// Display the time as well as the date of the last update to the branch.
313    pub fn display_time(mut self, v: bool) -> Self {
314        self.time = v;
315        self
316    }
317}
318
319// ---------------------------------------------------------------------------
320// Name filter transition
321// ---------------------------------------------------------------------------
322
323impl<U> Branches<Unselected, U> {
324    /// # Description
325    ///
326    #[cfg_attr(feature = "lt2024_2", doc = "-e filter / -E filter")]
327    #[cfg_attr(not(feature = "lt2024_2"), doc = "-e nameFilter / -E nameFilter")]
328    ///
329    /// `v` is the branch name filter pattern; lists only branch specs whose
330    /// name matches it.
331    ///
332    /// When `case_insensitive` is `false` (`-e`), the pattern is matched
333    /// using the server's normal case-sensitivity rules. When
334    /// `case_insensitive` is `true` (`-E`), the pattern is matched
335    /// case-insensitively, even on a case-sensitive server.
336    #[cfg_attr(
337        not(feature = "lt2024_2"),
338        doc = "",
339        doc = "For example:",
340        doc = "",
341        doc = "`p4 branches -e \"svr-dev-rel*\"`"
342    )]
343    ///
344    /// Transitions this command to the [`Filter`] state.
345    pub fn filter(self, v: impl Into<String>, case_insensitive: bool) -> Branches<Filter, U> {
346        Branches {
347            bin: self.bin,
348            global_opts: self.global_opts,
349            limit: self.limit,
350            time: self.time,
351            user_filter: self.user_filter,
352            name_filter: if case_insensitive {
353                Filter::InCaseSensitiveMode(v.into())
354            } else {
355                Filter::CaseSensitiveMode(v.into())
356            },
357        }
358    }
359}
360
361// ---------------------------------------------------------------------------
362// Owner filter transitions
363// ---------------------------------------------------------------------------
364
365impl<F> Branches<F, Unselected> {
366    /// # Description
367    ///
368    /// -u user
369    ///
370    #[cfg_attr(
371        feature = "lt2024_2",
372        doc = "`v` is the owner user name or search pattern."
373    )]
374    #[cfg_attr(
375        not(feature = "lt2024_2"),
376        doc = "`v` is the owner user name or search pattern; `case_insensitive` selects the `--user-case-insensitive` suboption."
377    )]
378    ///
379    #[cfg_attr(feature = "lt2024_2", doc = "List only branches owned by `user`.")]
380    #[cfg_attr(
381        not(feature = "lt2024_2"),
382        doc = "List branch specs owned by the specified user.",
383        doc = "",
384        doc = "This can include `*` wildcards to form a search pattern. If wildcards are used, enclose the search pattern in double quotes (`\"`).",
385        doc = "",
386        doc = "For example:",
387        doc = "",
388        doc = "`p4 branches -u \"*bert\"`",
389        doc = "",
390        doc = "You can also add the `--user-case-insensitive` suboption to indicate that the `user` value is a case-insensitive search pattern, even for a case-sensitive server."
391    )]
392    ///
393    /// Transitions this command to the [`User`] state.
394    pub fn user(self, v: impl Into<String>, case_insensitive: bool) -> Branches<F, User> {
395        let _ = case_insensitive;
396        Branches {
397            bin: self.bin,
398            global_opts: self.global_opts,
399            limit: self.limit,
400            time: self.time,
401            name_filter: self.name_filter,
402            user_filter: User::Named {
403                name: v.into(),
404                #[cfg(not(feature = "lt2024_2"))]
405                case_insensitive,
406            },
407        }
408    }
409
410    /// # Description
411    ///
412    /// --me
413    ///
414    /// Equivalent to `-u $P4USER`.
415    ///
416    /// Transitions this command to the [`User`] state.
417    #[cfg(not(feature = "lt2016_1"))]
418    pub fn filter_me(self) -> Branches<F, User> {
419        Branches {
420            bin: self.bin,
421            global_opts: self.global_opts,
422            limit: self.limit,
423            time: self.time,
424            name_filter: self.name_filter,
425            user_filter: User::Me,
426        }
427    }
428}
429
430// ---------------------------------------------------------------------------
431// SubCommand
432// ---------------------------------------------------------------------------
433
434impl<F: ExclusiveOption, U: ExclusiveOption> SubCommand for Branches<F, U> {
435    fn name(&self) -> &str {
436        "branches"
437    }
438
439    fn inject_local_args(&self, command: &mut Command) {
440        self.name_filter.inject_args(command);
441        if let Some(max) = self.limit {
442            command.arg("-m").arg(max.to_string());
443        }
444        if self.time {
445            command.arg("-t");
446        }
447        self.user_filter.inject_args(command);
448    }
449
450    fn global_opts(&self) -> Option<&GlobalOpts> {
451        Some(&self.global_opts)
452    }
453}
454
455// ---------------------------------------------------------------------------
456// Spawning
457// ---------------------------------------------------------------------------
458
459impl<F: ExclusiveOption, U: ExclusiveOption> ParameterizedSpawn<()> for Branches<F, U> {
460    type Output = Child;
461    type Error = std::io::Error;
462
463    /// Spawns `p4 branches` as a child process with piped standard output
464    /// and error streams; use the returned [`Child`] handle to wait for it
465    /// or interact with it.
466    fn spawn_with(&mut self, (): ()) -> Result<Self::Output, Self::Error> {
467        self.setup_command(&self.bin)
468            .stdout(Stdio::piped())
469            .stderr(Stdio::piped())
470            .spawn()
471    }
472}
473
474#[cfg(test)]
475mod tests {
476    use super::*;
477    use crate::cmd::args_of;
478
479    #[test]
480    fn bin_accessors_update_executable_path() {
481        let mut command = Branches::new("p4", GlobalOpts::new());
482        assert_eq!(command.get_bin(), Path::new("p4"));
483
484        command.set_bin("/usr/local/bin/p4");
485        assert_eq!(command.get_bin(), Path::new("/usr/local/bin/p4"));
486
487        let command = command.bin("p4.exe");
488        assert_eq!(command.get_bin(), Path::new("p4.exe"));
489    }
490
491    // Compile-time rejection: -e and -E are mutually exclusive by
492    // construction (the Filter enum holds exactly one variant).
493    // Compile-time rejection: -u and --me are mutually exclusive by
494    // construction (the User enum holds exactly one variant).
495    // Compile-time rejection: filter can be set only once (no transition or
496    // accessor exists on Branches<Filter, U>).
497    // Compile-time rejection: user/filter_me can be set only once (no
498    // transition or accessor exists on Branches<F, User>).
499    // Compile-time rejection: --user-case-insensitive is passed as an
500    // argument to user(), never settable on its own.
501    // Each filter can be set at most once; limit, display_time, and the
502    // other filter stay available afterwards.
503
504    #[test]
505    fn without_options() {
506        let branches = Branches::new("p4", GlobalOpts::new());
507        assert_eq!(args_of(&branches.setup_command("p4")), ["branches"]);
508    }
509
510    #[test]
511    fn limit() {
512        let branches = Branches::new("p4", GlobalOpts::new()).limit(5);
513        assert_eq!(
514            args_of(&branches.setup_command("p4")),
515            ["branches", "-m", "5"]
516        );
517    }
518
519    #[test]
520    fn display_time() {
521        let branches = Branches::new("p4", GlobalOpts::new()).display_time(true);
522        assert_eq!(args_of(&branches.setup_command("p4")), ["branches", "-t"]);
523    }
524
525    #[test]
526    fn display_time_can_be_unset() {
527        let branches = Branches::new("p4", GlobalOpts::new()).display_time(false);
528        assert_eq!(args_of(&branches.setup_command("p4")), ["branches"]);
529    }
530
531    #[test]
532    fn filter_case_sensitive() {
533        let branches = Branches::new("p4", GlobalOpts::new()).filter("svr-dev-rel*", false);
534        assert_eq!(
535            args_of(&branches.setup_command("p4")),
536            ["branches", "-e", "svr-dev-rel*"]
537        );
538    }
539
540    #[test]
541    fn filter_case_insensitive() {
542        let branches = Branches::new("p4", GlobalOpts::new()).filter("SVR-DEV-REL*", true);
543        assert_eq!(
544            args_of(&branches.setup_command("p4")),
545            ["branches", "-E", "SVR-DEV-REL*"]
546        );
547    }
548
549    #[test]
550    fn user() {
551        let branches = Branches::new("p4", GlobalOpts::new()).user("*bert", false);
552        assert_eq!(
553            args_of(&branches.setup_command("p4")),
554            ["branches", "-u", "*bert"]
555        );
556    }
557
558    #[cfg(not(feature = "lt2016_1"))]
559    #[test]
560    fn filter_me() {
561        let branches = Branches::new("p4", GlobalOpts::new()).filter_me();
562        assert_eq!(args_of(&branches.setup_command("p4")), ["branches", "--me"]);
563    }
564
565    #[cfg(not(feature = "lt2024_2"))]
566    #[test]
567    fn user_case_insensitive() {
568        let branches = Branches::new("p4", GlobalOpts::new()).user("*bert", true);
569        assert_eq!(
570            args_of(&branches.setup_command("p4")),
571            ["branches", "-u", "*bert", "--user-case-insensitive"]
572        );
573    }
574
575    #[test]
576    fn filter_transitions_preserve_owner_filter() {
577        let branches = Branches::new("p4", GlobalOpts::new())
578            .user("*bert", false)
579            .filter("svr-dev-rel*", false);
580        assert_eq!(
581            args_of(&branches.setup_command("p4")),
582            ["branches", "-e", "svr-dev-rel*", "-u", "*bert"]
583        );
584
585        #[cfg(not(feature = "lt2016_1"))]
586        {
587            let branches = Branches::new("p4", GlobalOpts::new())
588                .filter("SVR*", true)
589                .filter_me();
590            assert_eq!(
591                args_of(&branches.setup_command("p4")),
592                ["branches", "-E", "SVR*", "--me"]
593            );
594        }
595    }
596
597    #[test]
598    fn setters_update_limit_and_display_time() {
599        let mut branches = Branches::new("p4", GlobalOpts::new()).filter("svr-dev*", false);
600        assert_eq!(branches.get_limit(), None);
601        branches.set_limit(10);
602        assert_eq!(branches.get_limit(), Some(10));
603        assert!(!branches.get_display_time());
604        branches.set_display_time(true);
605        assert!(branches.get_display_time());
606    }
607
608    #[test]
609    fn all_options_order() {
610        let branches = Branches::new("p4", GlobalOpts::new())
611            .filter("svr-dev-rel*", false)
612            .limit(5)
613            .display_time(true)
614            .user("*bert", false);
615        assert_eq!(
616            args_of(&branches.setup_command("p4")),
617            [
618                "branches",
619                "-e",
620                "svr-dev-rel*",
621                "-m",
622                "5",
623                "-t",
624                "-u",
625                "*bert"
626            ]
627        );
628    }
629
630    #[test]
631    fn global_opts_are_injected_once() {
632        let branches = Branches::new("p4", GlobalOpts::new().port("localhost:1666")).limit(1);
633        assert_eq!(
634            args_of(&branches.setup_command("p4")),
635            ["-p", "localhost:1666", "branches", "-m", "1"]
636        );
637    }
638}