patchy/commands/
branch_fetch.rs

1use std::process;
2
3use colored::Colorize as _;
4
5use crate::{
6    commands::help,
7    fail,
8    flags::{is_valid_flag, Flag},
9    git_commands::{fetch_branch, is_valid_branch_name, GIT},
10    success,
11    types::CommandArgs,
12};
13
14use super::run::parse_if_maybe_hash;
15
16pub static BRANCH_FETCH_CHECKOUT_FLAG: Flag<'static> = Flag {
17    short: "-c",
18    long: "--checkout",
19    description: "Check out the first fetched branch",
20};
21
22pub static BRANCH_FETCH_BRANCH_NAME_FLAG: Flag<'static> = Flag {
23    short: "-b=",
24    long: "--branch-name=",
25    description: "Choose local name for the branch belonging to the preceding pull request",
26};
27
28pub static BRANCH_FETCH_FLAGS: &[&Flag<'static>; 2] =
29    &[&BRANCH_FETCH_BRANCH_NAME_FLAG, &BRANCH_FETCH_CHECKOUT_FLAG];
30
31pub struct Item {
32    /// # Examples
33    ///
34    /// helix-editor/helix
35    pub repo: String,
36    /// # Examples
37    ///
38    /// master
39    pub branch: String,
40    /// If specified, use a custom branch name instead of a generated one
41    ///
42    /// # Examples
43    ///
44    /// my-custom-branch123
45    pub local_branch_name: Option<String>,
46    /// If specified, do a **hard reset** to this commit when fetching the branch
47    ///
48    /// # Examples
49    ///
50    /// 6049f2035
51    pub commit_hash: Option<String>,
52}
53
54impl Item {
55    pub fn new(
56        repo: String,
57        branch: String,
58        local_branch_name: Option<String>,
59        commit_hash: Option<String>,
60    ) -> Self {
61        Self {
62            repo,
63            branch,
64            local_branch_name,
65            commit_hash,
66        }
67    }
68
69    pub fn create(arg: &str) -> anyhow::Result<Self> {
70        let (remote, hash) = parse_if_maybe_hash(arg, "@");
71
72        let (repo, branch) = remote.rsplit_once('/').ok_or_else(|| {
73            anyhow::anyhow!(
74                "Invalid format: {}, skipping. \
75Valid format is: username/repo/branch. Example: helix-editor/helix/master",
76                remote
77            )
78        })?;
79
80        Ok(Self::new(repo.to_owned(), branch.to_owned(), None, hash))
81    }
82
83    #[must_use]
84    pub fn with_branch_name(mut self, branch_name: Option<String>) -> Self {
85        self.local_branch_name = branch_name;
86        self
87    }
88}
89
90pub async fn branch_fetch(args: &CommandArgs) -> anyhow::Result<()> {
91    if args.is_empty() {
92        let _ = help(Some("branch-fetch"));
93        process::exit(1);
94    }
95
96    let has_checkout_flag = BRANCH_FETCH_CHECKOUT_FLAG.is_in(args);
97
98    let mut args = args.iter().peekable();
99
100    let mut items = vec![];
101
102    let mut no_more_flags = false;
103
104    while let Some(arg) = args.next() {
105        // After "--", each argument is interpreted literally. This way, we can e.g. use filenames that are named exactly the same as flags
106        if arg == "--" {
107            no_more_flags = true;
108            continue;
109        };
110
111        if arg.starts_with('-') && !no_more_flags {
112            if !is_valid_flag(arg, BRANCH_FETCH_FLAGS) {
113                fail!("Invalid flag: {arg}");
114                let _ = help(Some("branch-fetch"));
115                process::exit(1);
116            }
117
118            // Do not consider flags as arguments
119            continue;
120        }
121
122        let Ok(item) = Item::create(arg).map_err(|err| fail!("{err}")) else {
123            continue;
124        };
125
126        let next_arg = args.peek();
127        let maybe_custom_branch_name: Option<String> = next_arg.and_then(|next_arg| {
128            BRANCH_FETCH_BRANCH_NAME_FLAG
129                .extract_from_arg(next_arg)
130                .filter(|branch_name| is_valid_branch_name(branch_name))
131        });
132
133        if maybe_custom_branch_name.is_some() {
134            args.next();
135        };
136
137        let item = item.with_branch_name(maybe_custom_branch_name);
138
139        items.push(item);
140    }
141
142    let client = reqwest::Client::new();
143
144    for (i, item) in items.into_iter().enumerate() {
145        let hash = item.commit_hash.clone();
146        let repo = item.repo.clone();
147        match fetch_branch(item, &client).await {
148            Ok((_, info)) => {
149                success!(
150                    "Fetched branch {}/{} available at branch {}{}",
151                    repo,
152                    info.branch.upstream_branch_name,
153                    info.branch.local_branch_name.bright_cyan(),
154                    hash.map(|commit_hash| format!(", at commit {}", commit_hash.bright_yellow()))
155                        .unwrap_or_default()
156                );
157
158                // Attempt to cleanup after ourselves
159                let _ = GIT(&["remote", "remove", &info.remote.local_remote_alias]);
160
161                // If user uses --checkout flag, we're going to checkout the first fetched branch
162                if i == 0 && has_checkout_flag {
163                    if let Err(cant_checkout) = GIT(&["checkout", &info.branch.local_branch_name]) {
164                        fail!(
165                            "Could not check out branch {}:\n{cant_checkout}",
166                            info.branch.local_branch_name
167                        );
168                    } else {
169                        success!(
170                            "Automatically checked out the first branch: {}",
171                            info.branch.local_branch_name
172                        );
173                    }
174                }
175            }
176            Err(err) => {
177                fail!("{err}");
178                continue;
179            }
180        };
181    }
182
183    Ok(())
184}