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        Branches {
396            bin: self.bin,
397            global_opts: self.global_opts,
398            limit: self.limit,
399            time: self.time,
400            name_filter: self.name_filter,
401            user_filter: User::Named {
402                name: v.into(),
403                #[cfg(not(feature = "lt2024_2"))]
404                case_insensitive,
405            },
406        }
407    }
408
409    /// # Description
410    ///
411    /// --me
412    ///
413    /// Equivalent to `-u $P4USER`.
414    ///
415    /// Transitions this command to the [`User`] state.
416    #[cfg(not(feature = "lt2016_1"))]
417    pub fn filter_me(self) -> Branches<F, User> {
418        Branches {
419            bin: self.bin,
420            global_opts: self.global_opts,
421            limit: self.limit,
422            time: self.time,
423            name_filter: self.name_filter,
424            user_filter: User::Me,
425        }
426    }
427}
428
429// ---------------------------------------------------------------------------
430// SubCommand
431// ---------------------------------------------------------------------------
432
433impl<F: ExclusiveOption, U: ExclusiveOption> SubCommand for Branches<F, U> {
434    fn name(&self) -> &str {
435        "branches"
436    }
437
438    fn inject_local_args(&self, command: &mut Command) {
439        self.name_filter.inject_args(command);
440        if let Some(max) = self.limit {
441            command.arg("-m").arg(max.to_string());
442        }
443        if self.time {
444            command.arg("-t");
445        }
446        self.user_filter.inject_args(command);
447    }
448
449    fn global_opts(&self) -> Option<&GlobalOpts> {
450        Some(&self.global_opts)
451    }
452}
453
454// ---------------------------------------------------------------------------
455// Spawning
456// ---------------------------------------------------------------------------
457
458impl<F: ExclusiveOption, U: ExclusiveOption> ParameterizedSpawn<()> for Branches<F, U> {
459    type Output = Child;
460    type Error = std::io::Error;
461
462    /// Spawns `p4 branches` as a child process with piped standard output
463    /// and error streams; use the returned [`Child`] handle to wait for it
464    /// or interact with it.
465    fn spawn_with(&mut self, (): ()) -> Result<Self::Output, Self::Error> {
466        self.setup_command(&self.bin)
467            .stdout(Stdio::piped())
468            .stderr(Stdio::piped())
469            .spawn()
470    }
471}
472
473#[cfg(test)]
474mod tests {
475    use super::*;
476    use crate::cmd::args_of;
477
478    #[test]
479    fn bin_accessors_update_executable_path() {
480        let mut command = Branches::new("p4", GlobalOpts::new());
481        assert_eq!(command.get_bin(), Path::new("p4"));
482
483        command.set_bin("/usr/local/bin/p4");
484        assert_eq!(command.get_bin(), Path::new("/usr/local/bin/p4"));
485
486        let command = command.bin("p4.exe");
487        assert_eq!(command.get_bin(), Path::new("p4.exe"));
488    }
489
490    // Compile-time rejection: -e and -E are mutually exclusive by
491    // construction (the Filter enum holds exactly one variant).
492    // Compile-time rejection: -u and --me are mutually exclusive by
493    // construction (the User enum holds exactly one variant).
494    // Compile-time rejection: filter can be set only once (no transition or
495    // accessor exists on Branches<Filter, U>).
496    // Compile-time rejection: user/filter_me can be set only once (no
497    // transition or accessor exists on Branches<F, User>).
498    // Compile-time rejection: --user-case-insensitive is passed as an
499    // argument to user(), never settable on its own.
500    // Each filter can be set at most once; limit, display_time, and the
501    // other filter stay available afterwards.
502
503    #[test]
504    fn without_options() {
505        let branches = Branches::new("p4", GlobalOpts::new());
506        assert_eq!(args_of(&branches.setup_command("p4")), ["branches"]);
507    }
508
509    #[test]
510    fn limit() {
511        let branches = Branches::new("p4", GlobalOpts::new()).limit(5);
512        assert_eq!(
513            args_of(&branches.setup_command("p4")),
514            ["branches", "-m", "5"]
515        );
516    }
517
518    #[test]
519    fn display_time() {
520        let branches = Branches::new("p4", GlobalOpts::new()).display_time(true);
521        assert_eq!(args_of(&branches.setup_command("p4")), ["branches", "-t"]);
522    }
523
524    #[test]
525    fn display_time_can_be_unset() {
526        let branches = Branches::new("p4", GlobalOpts::new()).display_time(false);
527        assert_eq!(args_of(&branches.setup_command("p4")), ["branches"]);
528    }
529
530    #[test]
531    fn filter_case_sensitive() {
532        let branches = Branches::new("p4", GlobalOpts::new()).filter("svr-dev-rel*", false);
533        assert_eq!(
534            args_of(&branches.setup_command("p4")),
535            ["branches", "-e", "svr-dev-rel*"]
536        );
537    }
538
539    #[test]
540    fn filter_case_insensitive() {
541        let branches = Branches::new("p4", GlobalOpts::new()).filter("SVR-DEV-REL*", true);
542        assert_eq!(
543            args_of(&branches.setup_command("p4")),
544            ["branches", "-E", "SVR-DEV-REL*"]
545        );
546    }
547
548    #[test]
549    fn user() {
550        let branches = Branches::new("p4", GlobalOpts::new()).user("*bert", false);
551        assert_eq!(
552            args_of(&branches.setup_command("p4")),
553            ["branches", "-u", "*bert"]
554        );
555    }
556
557    #[cfg(not(feature = "lt2016_1"))]
558    #[test]
559    fn filter_me() {
560        let branches = Branches::new("p4", GlobalOpts::new()).filter_me();
561        assert_eq!(args_of(&branches.setup_command("p4")), ["branches", "--me"]);
562    }
563
564    #[cfg(not(feature = "lt2024_2"))]
565    #[test]
566    fn user_case_insensitive() {
567        let branches = Branches::new("p4", GlobalOpts::new()).user("*bert", true);
568        assert_eq!(
569            args_of(&branches.setup_command("p4")),
570            ["branches", "-u", "*bert", "--user-case-insensitive"]
571        );
572    }
573
574    #[test]
575    fn filter_transitions_preserve_owner_filter() {
576        let branches = Branches::new("p4", GlobalOpts::new())
577            .user("*bert", false)
578            .filter("svr-dev-rel*", false);
579        assert_eq!(
580            args_of(&branches.setup_command("p4")),
581            ["branches", "-e", "svr-dev-rel*", "-u", "*bert"]
582        );
583
584        #[cfg(not(feature = "lt2016_1"))]
585        {
586            let branches = Branches::new("p4", GlobalOpts::new())
587                .filter("SVR*", true)
588                .filter_me();
589            assert_eq!(
590                args_of(&branches.setup_command("p4")),
591                ["branches", "-E", "SVR*", "--me"]
592            );
593        }
594    }
595
596    #[test]
597    fn setters_update_limit_and_display_time() {
598        let mut branches = Branches::new("p4", GlobalOpts::new()).filter("svr-dev*", false);
599        assert_eq!(branches.get_limit(), None);
600        branches.set_limit(10);
601        assert_eq!(branches.get_limit(), Some(10));
602        assert!(!branches.get_display_time());
603        branches.set_display_time(true);
604        assert!(branches.get_display_time());
605    }
606
607    #[test]
608    fn all_options_order() {
609        let branches = Branches::new("p4", GlobalOpts::new())
610            .filter("svr-dev-rel*", false)
611            .limit(5)
612            .display_time(true)
613            .user("*bert", false);
614        assert_eq!(
615            args_of(&branches.setup_command("p4")),
616            [
617                "branches",
618                "-e",
619                "svr-dev-rel*",
620                "-m",
621                "5",
622                "-t",
623                "-u",
624                "*bert"
625            ]
626        );
627    }
628
629    #[test]
630    fn global_opts_are_injected_once() {
631        let branches = Branches::new("p4", GlobalOpts::new().port("localhost:1666")).limit(1);
632        assert_eq!(
633            args_of(&branches.setup_command("p4")),
634            ["-p", "localhost:1666", "branches", "-m", "1"]
635        );
636    }
637}