1use std::{fs, process};
2
3use anyhow::anyhow;
4use colored::Colorize as _;
5
6use crate::{
7 backup::{files, restore},
8 commands::{init, pr_fetch::ignore_octothorpe},
9 confirm_prompt, fail,
10 flags::Flag,
11 git_commands::{
12 add_remote_branch, checkout_from_remote, clean_up_remote, fetch_pull_request,
13 merge_pull_request, GIT, GIT_ROOT,
14 },
15 info, success, trace,
16 types::{Branch, BranchAndRemote, CommandArgs, Configuration, Remote},
17 utils::{display_link, with_uuid},
18 APP_NAME, CONFIG_FILE, CONFIG_ROOT, INDENT,
19};
20
21pub static RUN_YES_FLAG: Flag<'static> = Flag {
22 short: "-y",
23 long: "--yes",
24 description: "Do not prompt when overwriting local-branch specified in the config",
25};
26
27pub fn parse_if_maybe_hash(input: &str, syntax: &str) -> (String, Option<String>) {
31 let parts: Vec<_> = input.split(syntax).collect();
32
33 let len = parts.len();
34
35 if len == 1 {
36 (input.into(), None)
38 } else {
39 let output: String = parts[0..len - 1].iter().map(|s| String::from(*s)).collect();
41 let commit_hash: Option<String> = Some(parts[len - 1].into());
42 (output, commit_hash)
43 }
44}
45
46pub async fn run(args: &CommandArgs) -> anyhow::Result<()> {
47 println!();
48
49 let config_path = GIT_ROOT.join(CONFIG_ROOT);
50 let has_yes_flag = RUN_YES_FLAG.is_in(args);
51
52 let config_file_path = config_path.join(CONFIG_FILE);
53
54 let Ok(config_raw) = fs::read_to_string(config_file_path.clone()) else {
55 fail!("Could not find configuration file at {CONFIG_ROOT}/{CONFIG_FILE}");
56
57 if !has_yes_flag
59 && confirm_prompt!(
60 "Would you like us to run {} {} to initialize it?",
61 "patchy".bright_blue(),
62 "init".bright_yellow(),
63 )
64 {
65 if let Err(err) = init(args) {
66 fail!("{err}");
67 process::exit(1);
68 };
69 } else if has_yes_flag {
70 eprintln!(
71 "You can create it with {} {}",
72 "patchy".bright_blue(),
73 "init".bright_yellow()
74 );
75 } else {
76 }
78
79 process::exit(0);
82 };
83
84 trace!("Using configuration file {config_file_path:?}");
85
86 let config = toml::from_str::<Configuration>(&config_raw).map_err(|err| {
87 anyhow!("Could not parse `{CONFIG_ROOT}/{CONFIG_FILE}` configuration file:\n{err}")
88 })?;
89
90 let (remote_branch, commit_hash) = parse_if_maybe_hash(&config.remote_branch, " @ ");
91
92 if config.repo.is_empty() {
93 return Err(anyhow::anyhow!(
94 r#"You haven't specified a `repo` in your config, which can be for example:
95 - "helix-editor/helix"
96 - "microsoft/vscode"
97
98 For more information see this guide: https://github.com/nik-rev/patchy/blob/main/README.md""#
99 ));
100 }
101
102 let config_files = fs::read_dir(&config_path).map_err(|err| {
103 anyhow!(
104 "Could not read files in directory {:?}\n{err}",
105 &config_path
106 )
107 })?;
108
109 let backed_up_files = files(config_files).map_err(|err| {
110 anyhow!("Could not create backups for configuration files, aborting.\n{err}")
111 })?;
112
113 let info = BranchAndRemote {
114 branch: Branch {
115 upstream_branch_name: remote_branch.clone(),
116 local_branch_name: with_uuid(&remote_branch),
117 },
118 remote: Remote {
119 repository_url: format!("https://github.com/{}.git", config.repo),
120 local_remote_alias: with_uuid(&config.repo),
121 },
122 };
123
124 add_remote_branch(&info, commit_hash.as_deref())?;
125
126 let previous_branch = checkout_from_remote(
127 &info.branch.local_branch_name,
128 &info.remote.local_remote_alias,
129 )?;
130
131 let client = reqwest::Client::new();
132
133 if config.pull_requests.is_empty() {
134 info!(
135 "You haven't specified any pull requests to fetch in your config, {}",
136 display_link(
137 "see the instructions on how to configure patchy.",
138 "https://github.com/nik-rev/patchy?tab=readme-ov-file#config"
139 )
140 );
141 } else {
142 for pull_request in &config.pull_requests {
145 let pull_request = ignore_octothorpe(pull_request);
146 let (pull_request, commit_hash) = parse_if_maybe_hash(&pull_request, " @ ");
147 match fetch_pull_request(
149 &config.repo,
150 &pull_request,
151 &client,
152 None,
153 commit_hash.as_deref(),
154 )
155 .await
156 {
157 Ok((response, info)) => {
158 match merge_pull_request(
159 info,
160 &pull_request,
161 &response.title,
162 &response.html_url,
163 )
164 .await
165 {
166 Ok(()) => {
167 success!(
168 "Merged pull request {}",
169 display_link(
170 &format!(
171 "{}{}{}{}",
172 "#".bright_blue(),
173 pull_request.bright_blue(),
174 " ".bright_blue(),
175 &response.title.bright_blue().italic()
176 ),
177 &response.html_url
178 ),
179 );
180 }
181 Err(err) => {
182 fail!("{err}");
183 continue;
184 }
185 };
186 }
187 Err(err) => {
188 fail!("Could not fetch branch from remote\n{err}");
189 continue;
190 }
191 }
192 }
193 }
194
195 if let Err(err) = fs::create_dir_all(GIT_ROOT.join(CONFIG_ROOT)) {
196 GIT(&["checkout", &previous_branch])?;
197
198 clean_up_remote(
199 &info.remote.local_remote_alias,
200 &info.branch.local_branch_name,
201 )?;
202
203 return Err(anyhow!("Could not create directory {CONFIG_ROOT}\n{err}"));
204 };
205
206 for (file_name, _file, contents) in &backed_up_files {
207 restore(file_name, contents).map_err(|err| anyhow!("Could not restore backups:\n{err}"))?;
208
209 if let Some(patches) = &config.patches {
211 let file_name = file_name
212 .to_str()
213 .and_then(|file_name| file_name.get(0..file_name.len() - 6))
214 .unwrap_or_default();
215
216 if patches.contains(file_name) {
217 if let Err(err) = GIT(&[
218 "am",
219 "--keep-cr",
220 "--signoff",
221 &format!(
222 "{}/{file_name}.patch",
223 GIT_ROOT.join(CONFIG_ROOT).to_str().unwrap_or_default()
224 ),
225 ]) {
226 GIT(&["am", "--abort"])?;
227 return Err(anyhow!(
228 "Could not apply patch {file_name}, skipping\n{err}"
229 ));
230 };
231
232 let last_commit_message = GIT(&["log", "-1", "--format=%B"])?;
233 success!(
234 "Applied patch {file_name} {}",
235 last_commit_message
236 .lines()
237 .next()
238 .unwrap_or_default()
239 .bright_blue()
240 .italic()
241 );
242 }
243 }
244 }
245
246 GIT(&["add", CONFIG_ROOT])?;
247 GIT(&[
248 "commit",
249 "--message",
250 &format!("{APP_NAME}: Restore configuration files"),
251 ])?;
252
253 let temporary_branch = with_uuid("temp-branch");
254
255 GIT(&["switch", "--create", &temporary_branch])?;
256
257 clean_up_remote(
258 &info.remote.local_remote_alias,
259 &info.branch.local_branch_name,
260 )?;
261
262 if has_yes_flag
263 || confirm_prompt!(
264 "Overwrite branch {}? This is irreversible.",
265 config.local_branch.cyan()
266 )
267 {
268 GIT(&[
271 "branch",
272 "--move",
273 "--force",
274 &temporary_branch,
275 &config.local_branch,
276 ])?;
277 if has_yes_flag {
278 info!(
279 "Overwrote branch {} since you supplied the {} flag",
280 config.local_branch.cyan(),
281 "--yes".bright_magenta()
282 );
283 }
284 println!("\n{INDENT}{}", " Success!\n".bright_green().bold());
285 } else {
286 let command = format!(
287 " git branch --move --force {temporary_branch} {}",
288 config.local_branch
289 );
290 let command = format!("\n{INDENT}{}\n", command.bright_magenta());
291 println!(
292 "\n{INDENT} You can still manually overwrite {} with the following command:\n {command}",
293 config.local_branch.cyan(),
294 );
295 process::exit(1)
296 }
297
298 Ok(())
299}