patchy/commands/
pr_fetch.rs1use std::process;
2
3use crate::commands::help;
4use crate::fail;
5use crate::flags::{is_valid_flag, Flag};
6use crate::git_commands::{
7 fetch_pull_request, is_valid_branch_name, GIT, GITHUB_REMOTE_PREFIX, GITHUB_REMOTE_SUFFIX,
8};
9use crate::success;
10use crate::types::CommandArgs;
11use crate::utils::display_link;
12use anyhow::anyhow;
13use colored::Colorize as _;
14
15use super::help::{HELP_FLAG, VERSION_FLAG};
16use super::run::parse_if_maybe_hash;
17
18pub fn ignore_octothorpe(arg: &str) -> String {
21 if arg.starts_with('#') {
22 arg.get(1..).unwrap_or_default()
23 } else {
24 arg
25 }
26 .into()
27}
28
29pub static PR_FETCH_BRANCH_NAME_FLAG: Flag<'static> = Flag {
30 short: "-b=",
31 long: "--branch-name=",
32 description: "Choose local name for the branch belonging to the preceding pull request",
33};
34
35pub static PR_FETCH_CHECKOUT_FLAG: Flag<'static> = Flag {
36 short: "-c",
37 long: "--checkout",
38 description: "Check out the branch belonging to the first pull request",
39};
40
41pub static PR_FETCH_REPO_NAME_FLAG: Flag<'static> = Flag {
42 short: "-r=",
43 long: "--repo-name=",
44 description:
45 "Choose a github repository, using the `origin` remote of the current repository by default",
46};
47
48pub static PR_FETCH_FLAGS: &[&Flag<'static>; 5] = &[
49 &PR_FETCH_BRANCH_NAME_FLAG,
50 &PR_FETCH_CHECKOUT_FLAG,
51 &PR_FETCH_REPO_NAME_FLAG,
52 &HELP_FLAG,
53 &VERSION_FLAG,
54];
55
56pub async fn pr_fetch(args: &CommandArgs) -> anyhow::Result<()> {
57 if args.is_empty() {
58 let _ = help(Some("pr-fetch"));
59 process::exit(1);
60 }
61
62 let has_checkout_flag = PR_FETCH_CHECKOUT_FLAG.is_in(args);
63
64 let mut args = args.iter().peekable();
65
66 let mut pull_requests_with_maybe_custom_branch_names = vec![];
67
68 let mut remote_name: Option<String> = None;
69
70 let mut no_more_flags = false;
71
72 while let Some(arg) = args.next() {
73 if arg == "--" {
75 no_more_flags = true;
76 continue;
77 };
78
79 if let Some(flag) = PR_FETCH_REPO_NAME_FLAG.extract_from_arg(arg) {
80 remote_name = Some(flag);
81 continue;
82 }
83
84 if arg.starts_with('-') && !no_more_flags {
85 if !is_valid_flag(arg, PR_FETCH_FLAGS) {
86 fail!("Invalid flag: {arg}");
87 let _ = help(Some("pr-fetch"));
88 process::exit(1);
89 }
90
91 continue;
93 }
94
95 let arg = ignore_octothorpe(arg);
96
97 let (pull_request, hash) = parse_if_maybe_hash(&arg, "@");
98
99 if !pull_request.chars().all(char::is_numeric) {
100 fail!(
101 "The following argument couldn't be parsed as a pull request number: {arg}
102 Examples of valid pull request numbers (with custom commit hashes supported): 1154, 500, '1001@0b36296f67a80309243ea5c8892c79798c6dcf93'"
103 );
104 continue;
105 }
106
107 let next_arg = args.peek();
108 let maybe_custom_branch_name: Option<String> = next_arg.and_then(|next_arg| {
109 PR_FETCH_BRANCH_NAME_FLAG
110 .extract_from_arg(next_arg)
111 .filter(|branch_name| is_valid_branch_name(branch_name))
112 });
113
114 if maybe_custom_branch_name.is_some() {
115 args.next();
116 };
117
118 pull_requests_with_maybe_custom_branch_names.push((
119 pull_request,
120 maybe_custom_branch_name,
121 hash,
122 ));
123 }
124
125 if remote_name.is_none() {
127 let remote = GIT(&["remote", "get-url", "origin"])?;
128 if remote.starts_with(GITHUB_REMOTE_PREFIX) && remote.ends_with(GITHUB_REMOTE_SUFFIX) {
129 let start = GITHUB_REMOTE_PREFIX.len();
130 let end = remote.len() - GITHUB_REMOTE_SUFFIX.len();
131 remote_name = remote.get(start..end).map(Into::into);
132 };
133 }
134
135 let Some(remote_name) = remote_name else {
136 return Err(anyhow!(
137 "Could not get the remote, it should be in the form e.g. helix-editor/helix.",
138 ));
139 };
140
141 let client = reqwest::Client::new();
142
143 for (i, (pull_request, maybe_custom_branch_name, hash)) in
144 pull_requests_with_maybe_custom_branch_names
145 .iter()
146 .enumerate()
147 {
148 match fetch_pull_request(
149 &remote_name,
150 pull_request,
151 &client,
152 maybe_custom_branch_name.as_deref(),
153 hash.as_deref(),
154 )
155 .await
156 {
157 Ok((response, info)) => {
158 success!(
159 "Fetched pull request {} available at branch {}{}",
160 display_link(
161 &format!(
162 "{}{}{}{}",
163 "#".bright_blue(),
164 pull_request.bright_blue(),
165 " ".bright_blue(),
166 response.title.bright_blue().italic()
167 ),
168 &response.html_url
169 ),
170 info.branch.local_branch_name.bright_cyan(),
171 hash.clone()
172 .map(|commit_hash| format!(", at commit {}", commit_hash.bright_yellow()))
173 .unwrap_or_default()
174 );
175
176 let _ = GIT(&["remote", "remove", &info.remote.local_remote_alias]);
178
179 if i == 0 && has_checkout_flag {
181 if let Err(cant_checkout) = GIT(&["checkout", &info.branch.local_branch_name]) {
182 fail!(
183 "Could not check out branch {}:\n{cant_checkout}",
184 info.branch.local_branch_name
185 );
186 } else {
187 success!(
188 "Automatically checked out the first branch: {}",
189 info.branch.local_branch_name
190 );
191 }
192 }
193 }
194 Err(err) => {
195 fail!("{err}");
196 continue;
197 }
198 };
199 }
200
201 Ok(())
202}