diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2024-02-20 12:00:22 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2024-02-20 12:00:22 +0000 |
| commit | 32a3ca5b3c5fa202ffb6b5670f3aa1e77c331f8d (patch) | |
| tree | f1168bb488e60ed364910a1e794bb93d8817b771 /src/sub_commands | |
| parent | e472f83e1fa280025234b6c1eeda1ecfce443e79 (diff) | |
feat(list): download or apply with git am
add the option to download patches or apply them with git am
give more granular messages about the state of proposals.
add support for replacing old proposal version with a new one
Diffstat (limited to 'src/sub_commands')
| -rw-r--r-- | src/sub_commands/list.rs | 627 | ||||
| -rw-r--r-- | src/sub_commands/send.rs | 7 |
2 files changed, 582 insertions, 52 deletions
diff --git a/src/sub_commands/list.rs b/src/sub_commands/list.rs index b8c2919..9d02eb1 100644 --- a/src/sub_commands/list.rs +++ b/src/sub_commands/list.rs | |||
| @@ -1,3 +1,5 @@ | |||
| 1 | use std::{io::Write, ops::Add}; | ||
| 2 | |||
| 1 | use anyhow::{bail, Context, Result}; | 3 | use anyhow::{bail, Context, Result}; |
| 2 | 4 | ||
| 3 | use super::send::event_is_patch_set_root; | 5 | use super::send::event_is_patch_set_root; |
| @@ -8,9 +10,12 @@ use crate::client::MockConnect; | |||
| 8 | use crate::{ | 10 | use crate::{ |
| 9 | cli_interactor::{Interactor, InteractorPrompt, PromptChoiceParms, PromptConfirmParms}, | 11 | cli_interactor::{Interactor, InteractorPrompt, PromptChoiceParms, PromptConfirmParms}, |
| 10 | client::Connect, | 12 | client::Connect, |
| 11 | git::{Repo, RepoActions}, | 13 | git::{str_to_sha1, Repo, RepoActions}, |
| 12 | repo_ref::{self, RepoRef, REPO_REF_KIND}, | 14 | repo_ref::{self, RepoRef, REPO_REF_KIND}, |
| 13 | sub_commands::send::{event_is_cover_letter, event_to_cover_letter, PATCH_KIND}, | 15 | sub_commands::send::{ |
| 16 | commit_msg_from_patch_oneliner, event_is_cover_letter, event_to_cover_letter, | ||
| 17 | patch_supports_commit_ids, PATCH_KIND, | ||
| 18 | }, | ||
| 14 | Cli, | 19 | Cli, |
| 15 | }; | 20 | }; |
| 16 | 21 | ||
| @@ -56,64 +61,582 @@ pub async fn launch(_cli_args: &Cli, _args: &SubCommandArgs) -> Result<()> { | |||
| 56 | return Ok(()); | 61 | return Ok(()); |
| 57 | } | 62 | } |
| 58 | 63 | ||
| 59 | let selected_index = Interactor::default().choice( | 64 | loop { |
| 60 | PromptChoiceParms::default() | 65 | let selected_index = Interactor::default().choice( |
| 61 | .with_prompt("all proposals") | 66 | PromptChoiceParms::default() |
| 62 | .with_choices( | 67 | .with_prompt("all proposals") |
| 63 | proposal_events | 68 | .with_choices( |
| 64 | .iter() | 69 | proposal_events |
| 65 | .map(|e| { | 70 | .iter() |
| 66 | if let Ok(cl) = event_to_cover_letter(e) { | 71 | .map(|e| { |
| 67 | cl.title | 72 | if let Ok(cl) = event_to_cover_letter(e) { |
| 68 | } else if let Ok(msg) = tag_value(e, "description") { | 73 | cl.title |
| 69 | msg.split('\n').collect::<Vec<&str>>()[0].to_string() | 74 | } else if let Ok(msg) = tag_value(e, "description") { |
| 70 | } else { | 75 | msg.split('\n').collect::<Vec<&str>>()[0].to_string() |
| 71 | e.id.to_string() | 76 | } else { |
| 72 | } | 77 | e.id.to_string() |
| 73 | }) | 78 | } |
| 74 | .collect(), | 79 | }) |
| 75 | ), | 80 | .collect(), |
| 76 | )?; | 81 | ), |
| 77 | 82 | )?; | |
| 78 | println!("finding commits..."); | 83 | |
| 79 | 84 | let cover_letter = event_to_cover_letter(&proposal_events[selected_index]) | |
| 80 | let commits_events: Vec<nostr::Event> = | 85 | .context("cannot extract proposal details from proposal root event")?; |
| 81 | find_commits_for_proposal_root_event(&client, &proposal_events[selected_index], &repo_ref) | 86 | |
| 82 | .await?; | 87 | println!("finding commits..."); |
| 83 | 88 | ||
| 84 | confirm_checkout(&git_repo)?; | 89 | let commits_events: Vec<nostr::Event> = find_commits_for_proposal_root_event( |
| 85 | 90 | &client, | |
| 86 | let most_recent_proposal_patch_chain = get_most_recent_patch_with_ancestors(commits_events) | 91 | &proposal_events[selected_index], |
| 87 | .context("cannot get most recent patch for proposal")?; | 92 | &repo_ref, |
| 88 | 93 | ) | |
| 89 | let branch_name: String = event_to_cover_letter(&proposal_events[selected_index]) | 94 | .await?; |
| 90 | .context("cannot assign a branch name as event is not a patch set root")? | 95 | |
| 91 | .branch_name; | 96 | let Ok(most_recent_proposal_patch_chain) = |
| 92 | 97 | get_most_recent_patch_with_ancestors(commits_events.clone()) | |
| 93 | let applied = git_repo | 98 | else { |
| 94 | .apply_patch_chain(&branch_name, most_recent_proposal_patch_chain) | 99 | if Interactor::default().confirm( |
| 95 | .context("cannot apply patch chain")?; | 100 | PromptConfirmParms::default() |
| 96 | 101 | .with_default(true) | |
| 97 | if applied.is_empty() { | 102 | .with_prompt( |
| 98 | println!("checked out proposal branch. no new commits to pull"); | 103 | "cannot find any patches on this proposal. choose another proposal?", |
| 99 | } else { | 104 | ), |
| 105 | )? { | ||
| 106 | continue; | ||
| 107 | } | ||
| 108 | return Ok(()); | ||
| 109 | }; | ||
| 110 | |||
| 111 | let binding_patch_text_ref = format!("{} commits", most_recent_proposal_patch_chain.len()); | ||
| 112 | let patch_text_ref = if most_recent_proposal_patch_chain.len().gt(&1) { | ||
| 113 | binding_patch_text_ref.as_str() | ||
| 114 | } else { | ||
| 115 | "1 commit" | ||
| 116 | }; | ||
| 117 | |||
| 118 | let no_support_for_patches_as_branch = most_recent_proposal_patch_chain | ||
| 119 | .iter() | ||
| 120 | .any(|event| !patch_supports_commit_ids(event)); | ||
| 121 | |||
| 122 | if no_support_for_patches_as_branch { | ||
| 123 | println!("{patch_text_ref}"); | ||
| 124 | return match Interactor::default().choice(PromptChoiceParms::default().with_choices( | ||
| 125 | vec![ | ||
| 126 | "learn why 'patch only' proposals can't be checked out".to_string(), | ||
| 127 | format!("apply to current branch with `git am`"), | ||
| 128 | format!("download to ./patches"), | ||
| 129 | "back".to_string(), | ||
| 130 | ], | ||
| 131 | ))? { | ||
| 132 | 0 => { | ||
| 133 | println!("Some proposals are posted as 'patch only'\n"); | ||
| 134 | println!( | ||
| 135 | "they are not anchored against a particular state of the code base like a standard proposal or a GitHub Pull Request can be\n" | ||
| 136 | ); | ||
| 137 | println!( | ||
| 138 | "they are designed to reviewed by studying the diff (in a tool like gitworkshop.dev) and if acceptable by a maintainer, applied to the latest version of master with any conflicts resolved as the do so\n" | ||
| 139 | ); | ||
| 140 | println!( | ||
| 141 | "this has proven to be a smoother workflow for large scale projects with a high frequency of changes, even when patches are exchanged via email\n" | ||
| 142 | ); | ||
| 143 | println!( | ||
| 144 | "by default ngit posts proposals that support both the branch and patch model so either workflow can be used" | ||
| 145 | ); | ||
| 146 | Interactor::default().choice( | ||
| 147 | PromptChoiceParms::default().with_choices(vec!["back".to_string()]), | ||
| 148 | )?; | ||
| 149 | continue; | ||
| 150 | } | ||
| 151 | 1 => launch_git_am_with_patches(most_recent_proposal_patch_chain), | ||
| 152 | 2 => save_patches_to_dir(most_recent_proposal_patch_chain, &git_repo), | ||
| 153 | 3 => continue, | ||
| 154 | _ => { | ||
| 155 | bail!("unexpected choice") | ||
| 156 | } | ||
| 157 | }; | ||
| 158 | } | ||
| 159 | |||
| 160 | let branch_exists = git_repo | ||
| 161 | .get_local_branch_names() | ||
| 162 | .context("gitlib2 will not show a list of local branch names")? | ||
| 163 | .iter() | ||
| 164 | .any(|n| n.eq(&cover_letter.branch_name)); | ||
| 165 | |||
| 166 | let checked_out_proposal_branch = git_repo | ||
| 167 | .get_checked_out_branch_name()? | ||
| 168 | .eq(&cover_letter.branch_name); | ||
| 169 | |||
| 170 | let proposal_base_commit = str_to_sha1(&tag_value( | ||
| 171 | most_recent_proposal_patch_chain.last().context( | ||
| 172 | "there should be at least one patch as we have already checked for this", | ||
| 173 | )?, | ||
| 174 | "parent-commit", | ||
| 175 | )?) | ||
| 176 | .context("cannot get valid parent commit id from patch")?; | ||
| 177 | |||
| 178 | let (main_branch_name, master_tip) = git_repo.get_main_or_master_branch()?; | ||
| 179 | |||
| 180 | if !git_repo.does_commit_exist(&proposal_base_commit.to_string())? { | ||
| 181 | println!("your '{main_branch_name}' branch may not be up-to-date."); | ||
| 182 | println!("the proposal parent commit doesnt exist in your local repository."); | ||
| 183 | return match Interactor::default().choice(PromptChoiceParms::default().with_choices( | ||
| 184 | vec![ | ||
| 185 | format!( | ||
| 186 | "manually run `git pull` on '{main_branch_name}' and select proposal again" | ||
| 187 | ), | ||
| 188 | format!("apply to current branch with `git am`"), | ||
| 189 | format!("download to ./patches"), | ||
| 190 | "back".to_string(), | ||
| 191 | ], | ||
| 192 | ))? { | ||
| 193 | 0 | 3 => continue, | ||
| 194 | 1 => launch_git_am_with_patches(most_recent_proposal_patch_chain), | ||
| 195 | 2 => save_patches_to_dir(most_recent_proposal_patch_chain, &git_repo), | ||
| 196 | _ => { | ||
| 197 | bail!("unexpected choice") | ||
| 198 | } | ||
| 199 | }; | ||
| 200 | } | ||
| 201 | |||
| 202 | let proposal_tip = str_to_sha1( | ||
| 203 | &get_commit_id_from_patch(most_recent_proposal_patch_chain.first().context( | ||
| 204 | "there should be at least one patch as we have already checked for this", | ||
| 205 | )?) | ||
| 206 | .context("cannot get valid commit_id from patch")?, | ||
| 207 | ) | ||
| 208 | .context("cannot get valid commit_id from patch")?; | ||
| 209 | |||
| 210 | let (_, proposal_behind_main) = | ||
| 211 | git_repo.get_commits_ahead_behind(&master_tip, &proposal_base_commit)?; | ||
| 212 | |||
| 213 | // branch doesnt exist | ||
| 214 | if !branch_exists { | ||
| 215 | return match Interactor::default() | ||
| 216 | .choice(PromptChoiceParms::default().with_choices(vec![ | ||
| 217 | format!( | ||
| 218 | "create and checkout proposal branch ({} ahead {} behind '{main_branch_name}')", | ||
| 219 | most_recent_proposal_patch_chain.len(), | ||
| 220 | proposal_behind_main.len(), | ||
| 221 | ), | ||
| 222 | format!("apply to current branch with `git am`"), | ||
| 223 | format!("download to ./patches"), | ||
| 224 | "back".to_string(), | ||
| 225 | ]))? { | ||
| 226 | 0 => { | ||
| 227 | check_clean(&git_repo)?; | ||
| 228 | let _ = git_repo | ||
| 229 | .apply_patch_chain( | ||
| 230 | &cover_letter.branch_name, | ||
| 231 | most_recent_proposal_patch_chain, | ||
| 232 | ) | ||
| 233 | .context("cannot apply patch chain")?; | ||
| 234 | |||
| 235 | println!( | ||
| 236 | "checked out proposal as '{}' branch", | ||
| 237 | cover_letter.branch_name | ||
| 238 | ); | ||
| 239 | Ok(()) | ||
| 240 | } | ||
| 241 | 1 => launch_git_am_with_patches(most_recent_proposal_patch_chain), | ||
| 242 | 2 => save_patches_to_dir(most_recent_proposal_patch_chain, &git_repo), | ||
| 243 | 3 => continue, | ||
| 244 | _ => { | ||
| 245 | bail!("unexpected choice") | ||
| 246 | } | ||
| 247 | }; | ||
| 248 | } | ||
| 249 | |||
| 250 | let local_branch_tip = git_repo.get_tip_of_local_branch(&cover_letter.branch_name)?; | ||
| 251 | |||
| 252 | // up-to-date | ||
| 253 | if proposal_tip.eq(&local_branch_tip) { | ||
| 254 | if checked_out_proposal_branch { | ||
| 255 | println!("branch checked out and up-to-date"); | ||
| 256 | return match Interactor::default().choice( | ||
| 257 | PromptChoiceParms::default() | ||
| 258 | .with_choices(vec!["exit".to_string(), "back".to_string()]), | ||
| 259 | )? { | ||
| 260 | 0 => Ok(()), | ||
| 261 | 1 => continue, | ||
| 262 | _ => { | ||
| 263 | bail!("unexpected choice") | ||
| 264 | } | ||
| 265 | }; | ||
| 266 | } | ||
| 267 | |||
| 268 | return match Interactor::default().choice(PromptChoiceParms::default().with_choices( | ||
| 269 | vec![ | ||
| 270 | format!( | ||
| 271 | "checkout proposal branch ({} ahead {} behind '{main_branch_name}')", | ||
| 272 | most_recent_proposal_patch_chain.len(), | ||
| 273 | proposal_behind_main.len(), | ||
| 274 | ), | ||
| 275 | format!("apply to current branch with `git am`"), | ||
| 276 | format!("download to ./patches"), | ||
| 277 | "back".to_string(), | ||
| 278 | ], | ||
| 279 | ))? { | ||
| 280 | 0 => { | ||
| 281 | check_clean(&git_repo)?; | ||
| 282 | git_repo.checkout(&cover_letter.branch_name)?; | ||
| 283 | println!( | ||
| 284 | "checked out proposal as '{}' branch", | ||
| 285 | cover_letter.branch_name | ||
| 286 | ); | ||
| 287 | Ok(()) | ||
| 288 | } | ||
| 289 | 1 => launch_git_am_with_patches(most_recent_proposal_patch_chain), | ||
| 290 | 2 => save_patches_to_dir(most_recent_proposal_patch_chain, &git_repo), | ||
| 291 | 3 => continue, | ||
| 292 | _ => { | ||
| 293 | bail!("unexpected choice") | ||
| 294 | } | ||
| 295 | }; | ||
| 296 | } | ||
| 297 | |||
| 298 | let (local_ahead_of_main, local_beind_main) = | ||
| 299 | git_repo.get_commits_ahead_behind(&master_tip, &local_branch_tip)?; | ||
| 300 | |||
| 301 | // new appendments to proposal | ||
| 302 | if let Some(index) = most_recent_proposal_patch_chain.iter().position(|patch| { | ||
| 303 | get_commit_id_from_patch(patch) | ||
| 304 | .unwrap_or_default() | ||
| 305 | .eq(&local_branch_tip.to_string()) | ||
| 306 | }) { | ||
| 307 | return match Interactor::default().choice(PromptChoiceParms::default().with_choices( | ||
| 308 | vec![ | ||
| 309 | format!("checkout proposal branch and apply {} appendments", &index,), | ||
| 310 | format!("apply to current branch with `git am`"), | ||
| 311 | format!("download to ./patches"), | ||
| 312 | "back".to_string(), | ||
| 313 | ], | ||
| 314 | ))? { | ||
| 315 | 0 => { | ||
| 316 | check_clean(&git_repo)?; | ||
| 317 | git_repo.checkout(&cover_letter.branch_name)?; | ||
| 318 | let _ = git_repo | ||
| 319 | .apply_patch_chain( | ||
| 320 | &cover_letter.branch_name, | ||
| 321 | most_recent_proposal_patch_chain, | ||
| 322 | ) | ||
| 323 | .context("cannot apply patch chain")?; | ||
| 324 | println!( | ||
| 325 | "checked out proposal branch and applied {} appendments ({} ahead {} behind '{main_branch_name}')", | ||
| 326 | &index, | ||
| 327 | local_ahead_of_main.len().add(&index), | ||
| 328 | local_beind_main.len(), | ||
| 329 | ); | ||
| 330 | Ok(()) | ||
| 331 | } | ||
| 332 | 1 => launch_git_am_with_patches(most_recent_proposal_patch_chain), | ||
| 333 | 2 => save_patches_to_dir(most_recent_proposal_patch_chain, &git_repo), | ||
| 334 | 3 => continue, | ||
| 335 | _ => { | ||
| 336 | bail!("unexpected choice") | ||
| 337 | } | ||
| 338 | }; | ||
| 339 | } | ||
| 340 | |||
| 341 | // tip of local in proposal history (new, ammended or rebased version but no | ||
| 342 | // local changes) | ||
| 343 | if commits_events.iter().any(|patch| { | ||
| 344 | get_commit_id_from_patch(patch) | ||
| 345 | .unwrap_or_default() | ||
| 346 | .eq(&local_branch_tip.to_string()) | ||
| 347 | }) { | ||
| 348 | return match Interactor::default().choice( | ||
| 349 | PromptChoiceParms::default() | ||
| 350 | .with_choices( | ||
| 351 | vec![ | ||
| 352 | format!( | ||
| 353 | "checkout new version of proposal branch ({} ahead {} behind '{main_branch_name}'), replacing old version ({} ahead {} behind '{main_branch_name}')", | ||
| 354 | most_recent_proposal_patch_chain.len(), | ||
| 355 | proposal_behind_main.len(), | ||
| 356 | local_ahead_of_main.len(), | ||
| 357 | local_beind_main.len(), | ||
| 358 | ), | ||
| 359 | format!( | ||
| 360 | "checkout existing outdated proposal branch ({} ahead {} behind '{main_branch_name}')", | ||
| 361 | local_ahead_of_main.len(), | ||
| 362 | local_beind_main.len(), | ||
| 363 | ), | ||
| 364 | format!("apply to current branch with `git am`"), | ||
| 365 | format!("download to ./patches"), | ||
| 366 | "back".to_string(), | ||
| 367 | ], | ||
| 368 | ) | ||
| 369 | )? { | ||
| 370 | 0 => { | ||
| 371 | check_clean(&git_repo)?; | ||
| 372 | git_repo.create_branch_at_commit(&cover_letter.branch_name, &proposal_base_commit.to_string())?; | ||
| 373 | git_repo.checkout(&cover_letter.branch_name)?; | ||
| 374 | let chain_length = most_recent_proposal_patch_chain.len(); | ||
| 375 | let _ = git_repo | ||
| 376 | .apply_patch_chain(&cover_letter.branch_name, most_recent_proposal_patch_chain) | ||
| 377 | .context("cannot apply patch chain")?; | ||
| 378 | format!( | ||
| 379 | "checked out new version of proposal ({} ahead {} behind '{main_branch_name}'), replacing old version ({} ahead {} behind '{main_branch_name}')", | ||
| 380 | chain_length, | ||
| 381 | proposal_behind_main.len(), | ||
| 382 | local_ahead_of_main.len(), | ||
| 383 | local_beind_main.len(), | ||
| 384 | ); | ||
| 385 | Ok(()) | ||
| 386 | }, | ||
| 387 | 1 => { | ||
| 388 | check_clean(&git_repo)?; | ||
| 389 | git_repo.checkout(&cover_letter.branch_name)?; | ||
| 390 | format!( | ||
| 391 | "checked out old proposal in existing branch ({} ahead {} behind '{main_branch_name}')", | ||
| 392 | local_ahead_of_main.len(), | ||
| 393 | local_beind_main.len(), | ||
| 394 | ); | ||
| 395 | Ok(()) | ||
| 396 | }, | ||
| 397 | 2 => {launch_git_am_with_patches(most_recent_proposal_patch_chain)}, | ||
| 398 | 3 => {save_patches_to_dir(most_recent_proposal_patch_chain, &git_repo)}, | ||
| 399 | 4 => { continue }, | ||
| 400 | _ => { bail!("unexpected choice")} | ||
| 401 | }; | ||
| 402 | } | ||
| 403 | |||
| 404 | // tip of proposal in branch in history (local appendments made) | ||
| 405 | if let Ok((local_ahead_of_proposal, _)) = | ||
| 406 | git_repo.get_commits_ahead_behind(&proposal_tip, &local_branch_tip) | ||
| 407 | { | ||
| 408 | println!( | ||
| 409 | "local proposal branch exists with {} unpublished commits on top of the most up-to-date version of the proposal", | ||
| 410 | local_ahead_of_proposal.len() | ||
| 411 | ); | ||
| 412 | return match Interactor::default().choice( | ||
| 413 | PromptChoiceParms::default() | ||
| 414 | .with_choices( | ||
| 415 | vec![ | ||
| 416 | format!( | ||
| 417 | "checkout proposal branch with {} unpublished commits ({} ahead {} behind '{main_branch_name}')", | ||
| 418 | local_ahead_of_proposal.len(), | ||
| 419 | local_ahead_of_main.len(), | ||
| 420 | proposal_behind_main.len(), | ||
| 421 | ), | ||
| 422 | "back".to_string(), | ||
| 423 | ], | ||
| 424 | ) | ||
| 425 | )? { | ||
| 426 | 0 => { | ||
| 427 | git_repo.checkout(&cover_letter.branch_name)?; | ||
| 428 | format!( | ||
| 429 | "checked out proposal branch with {} unpublished commits ({} ahead {} behind '{main_branch_name}')", | ||
| 430 | local_ahead_of_proposal.len(), | ||
| 431 | local_ahead_of_main.len(), | ||
| 432 | proposal_behind_main.len(), | ||
| 433 | ); | ||
| 434 | Ok(()) | ||
| 435 | |||
| 436 | }, | ||
| 437 | 1 => { continue }, | ||
| 438 | _ => { bail!("unexpected choice")} | ||
| 439 | }; | ||
| 440 | } | ||
| 441 | |||
| 442 | // if tip of proposal commits exist (were once part of branch but have been | ||
| 443 | // ammended and git clean up job hasn't removed them) | ||
| 444 | if git_repo.does_commit_exist(&proposal_tip.to_string())? { | ||
| 445 | println!( | ||
| 446 | "you have previously applied the latest version of the proposal ({} ahead {} behind '{main_branch_name}') but your local proposal branch has other unpublished changes ({} ahead {} behind '{main_branch_name}')", | ||
| 447 | most_recent_proposal_patch_chain.len(), | ||
| 448 | proposal_behind_main.len(), | ||
| 449 | local_ahead_of_main.len(), | ||
| 450 | local_beind_main.len(), | ||
| 451 | ); | ||
| 452 | return match Interactor::default().choice( | ||
| 453 | PromptChoiceParms::default() | ||
| 454 | .with_choices( | ||
| 455 | vec![ | ||
| 456 | format!( | ||
| 457 | "checkout local branch with unpublished changes ({} ahead {} behind '{main_branch_name}')", | ||
| 458 | local_ahead_of_main.len(), | ||
| 459 | local_beind_main.len(), | ||
| 460 | ), | ||
| 461 | format!( | ||
| 462 | "discard local branch with old version ({} ahead {} behind '{main_branch_name}') and checkout latest published version ({} ahead {} behind '{main_branch_name}')", | ||
| 463 | most_recent_proposal_patch_chain.len(), | ||
| 464 | proposal_behind_main.len(), | ||
| 465 | local_ahead_of_main.len(), | ||
| 466 | local_beind_main.len(), | ||
| 467 | ), | ||
| 468 | format!("apply to current branch with `git am`"), | ||
| 469 | format!("download to ./patches"), | ||
| 470 | "back".to_string(), | ||
| 471 | ], | ||
| 472 | ) | ||
| 473 | )? { | ||
| 474 | 0 => { | ||
| 475 | check_clean(&git_repo)?; | ||
| 476 | git_repo.checkout(&cover_letter.branch_name)?; | ||
| 477 | format!( | ||
| 478 | "checked out old proposal in existing branch ({} ahead {} behind '{main_branch_name}')", | ||
| 479 | local_ahead_of_main.len(), | ||
| 480 | local_beind_main.len(), | ||
| 481 | ); | ||
| 482 | Ok(()) | ||
| 483 | }, | ||
| 484 | 1 => { | ||
| 485 | check_clean(&git_repo)?; | ||
| 486 | git_repo.create_branch_at_commit(&cover_letter.branch_name, &proposal_base_commit.to_string())?; | ||
| 487 | git_repo.checkout(&cover_letter.branch_name)?; | ||
| 488 | let chain_length = most_recent_proposal_patch_chain.len(); | ||
| 489 | let _ = git_repo | ||
| 490 | .apply_patch_chain(&cover_letter.branch_name, most_recent_proposal_patch_chain) | ||
| 491 | .context("cannot apply patch chain")?; | ||
| 492 | format!( | ||
| 493 | "checked out new version of proposal ({} ahead {} behind '{main_branch_name}'), replacing old version ({} ahead {} behind '{main_branch_name}')", | ||
| 494 | chain_length, | ||
| 495 | proposal_behind_main.len(), | ||
| 496 | local_ahead_of_main.len(), | ||
| 497 | local_beind_main.len(), | ||
| 498 | ); | ||
| 499 | Ok(()) | ||
| 500 | }, | ||
| 501 | 2 => {launch_git_am_with_patches(most_recent_proposal_patch_chain)}, | ||
| 502 | 3 => {save_patches_to_dir(most_recent_proposal_patch_chain, &git_repo)}, | ||
| 503 | 4 => { continue }, | ||
| 504 | _ => { bail!("unexpected choice")} | ||
| 505 | }; | ||
| 506 | } | ||
| 507 | |||
| 100 | println!( | 508 | println!( |
| 101 | "checked out proposal branch. pulled {} new commits", | 509 | "your local proposal branch ({} ahead {} behind '{main_branch_name}') has conflicting changes with the latest published proposal ({} ahead {} behind '{main_branch_name}')", |
| 102 | applied.len(), | 510 | local_ahead_of_main.len(), |
| 511 | local_beind_main.len(), | ||
| 512 | most_recent_proposal_patch_chain.len(), | ||
| 513 | proposal_behind_main.len(), | ||
| 103 | ); | 514 | ); |
| 515 | |||
| 516 | return match Interactor::default().choice( | ||
| 517 | PromptChoiceParms::default() | ||
| 518 | .with_choices( | ||
| 519 | vec![ | ||
| 520 | format!( | ||
| 521 | "checkout local branch with unpublished changes ({} ahead {} behind '{main_branch_name}')", | ||
| 522 | local_ahead_of_main.len(), | ||
| 523 | local_beind_main.len(), | ||
| 524 | ), | ||
| 525 | format!( | ||
| 526 | "discard local branch with unpublished version ({} ahead {} behind '{main_branch_name}') and checkout latest published version ({} ahead {} behind '{main_branch_name}'). consider creating a temporary branch with your existing unchanges first.", | ||
| 527 | most_recent_proposal_patch_chain.len(), | ||
| 528 | proposal_behind_main.len(), | ||
| 529 | local_ahead_of_main.len(), | ||
| 530 | local_beind_main.len(), | ||
| 531 | ), | ||
| 532 | format!("apply to current branch with `git am`"), | ||
| 533 | format!("download to ./patches"), | ||
| 534 | "back".to_string(), | ||
| 535 | ], | ||
| 536 | ) | ||
| 537 | )? { | ||
| 538 | 0 => { | ||
| 539 | check_clean(&git_repo)?; | ||
| 540 | git_repo.checkout(&cover_letter.branch_name)?; | ||
| 541 | format!( | ||
| 542 | "checked out old proposal in existing branch ({} ahead {} behind '{main_branch_name}')", | ||
| 543 | local_ahead_of_main.len(), | ||
| 544 | local_beind_main.len(), | ||
| 545 | ); | ||
| 546 | Ok(()) | ||
| 547 | }, | ||
| 548 | 1 => { | ||
| 549 | check_clean(&git_repo)?; | ||
| 550 | git_repo.create_branch_at_commit(&cover_letter.branch_name, &proposal_base_commit.to_string())?; | ||
| 551 | git_repo.checkout(&cover_letter.branch_name)?; | ||
| 552 | let chain_length = most_recent_proposal_patch_chain.len(); | ||
| 553 | let _ = git_repo | ||
| 554 | .apply_patch_chain(&cover_letter.branch_name, most_recent_proposal_patch_chain) | ||
| 555 | .context("cannot apply patch chain")?; | ||
| 556 | format!( | ||
| 557 | "checked out new version of proposal ({} ahead {} behind '{main_branch_name}'), replacing old version ({} ahead {} behind '{main_branch_name}'). consider creating a temporary branch with your existing unchanges first.", | ||
| 558 | chain_length, | ||
| 559 | proposal_behind_main.len(), | ||
| 560 | local_ahead_of_main.len(), | ||
| 561 | local_beind_main.len(), | ||
| 562 | ); | ||
| 563 | Ok(()) | ||
| 564 | }, | ||
| 565 | 2 => {launch_git_am_with_patches(most_recent_proposal_patch_chain)}, | ||
| 566 | 3 => {save_patches_to_dir(most_recent_proposal_patch_chain, &git_repo)}, | ||
| 567 | 4 => { continue }, | ||
| 568 | _ => { bail!("unexpected choice")} | ||
| 569 | }; | ||
| 104 | } | 570 | } |
| 571 | } | ||
| 572 | |||
| 573 | fn launch_git_am_with_patches(mut patches: Vec<nostr::Event>) -> Result<()> { | ||
| 574 | println!("applying to current branch with `git am`"); | ||
| 575 | // TODO: add PATCH x/n to appended patches | ||
| 576 | patches.reverse(); | ||
| 577 | |||
| 578 | let mut am = std::process::Command::new("git") | ||
| 579 | .arg("am") | ||
| 580 | .stdin(std::process::Stdio::piped()) | ||
| 581 | .stdout(std::process::Stdio::inherit()) | ||
| 582 | .stderr(std::process::Stdio::inherit()) | ||
| 583 | .spawn() | ||
| 584 | .context("failed to spawn git am")?; | ||
| 585 | |||
| 586 | let stdin = am | ||
| 587 | .stdin | ||
| 588 | .as_mut() | ||
| 589 | .context("git am process failed to take stdin")?; | ||
| 590 | |||
| 591 | for patch in patches { | ||
| 592 | stdin | ||
| 593 | .write(format!("{}\n\n", patch.content).as_bytes()) | ||
| 594 | .context("failed to write patch content into git am stdin buffer")?; | ||
| 595 | } | ||
| 596 | stdin.flush()?; | ||
| 597 | let output = am | ||
| 598 | .wait_with_output() | ||
| 599 | .context("failed to read git am stdout")?; | ||
| 600 | print!("{:?}", output.stdout); | ||
| 105 | Ok(()) | 601 | Ok(()) |
| 106 | } | 602 | } |
| 107 | 603 | ||
| 108 | fn confirm_checkout(git_repo: &Repo) -> Result<()> { | 604 | fn event_id_extra_shorthand(event: &nostr::Event) -> String { |
| 109 | if !Interactor::default().confirm( | 605 | event.id.to_string()[..5].to_string() |
| 110 | PromptConfirmParms::default() | 606 | } |
| 111 | .with_prompt("check out branch?") | 607 | |
| 112 | .with_default(true), | 608 | fn save_patches_to_dir(mut patches: Vec<nostr::Event>, git_repo: &Repo) -> Result<()> { |
| 113 | )? { | 609 | // TODO: add PATCH x/n to appended patches |
| 114 | bail!("Exiting..."); | 610 | patches.reverse(); |
| 611 | let path = git_repo.get_path()?.join("patches"); | ||
| 612 | std::fs::create_dir_all(&path)?; | ||
| 613 | let id = event_id_extra_shorthand( | ||
| 614 | patches | ||
| 615 | .first() | ||
| 616 | .context("there must be at least one patch to save")?, | ||
| 617 | ); | ||
| 618 | for (i, patch) in patches.iter().enumerate() { | ||
| 619 | let path = path.join(format!( | ||
| 620 | "{}-{:0>4}-{}.patch", | ||
| 621 | &id, | ||
| 622 | i.add(&1), | ||
| 623 | commit_msg_from_patch_oneliner(patch)? | ||
| 624 | )); | ||
| 625 | let mut file = std::fs::OpenOptions::new() | ||
| 626 | .create(true) | ||
| 627 | .write(true) | ||
| 628 | .truncate(true) | ||
| 629 | .open(path) | ||
| 630 | .context("open new patch file with write and truncate options")?; | ||
| 631 | file.write_all(patch.content().as_bytes())?; | ||
| 632 | file.write_all("\n\n".as_bytes())?; | ||
| 633 | file.flush()?; | ||
| 115 | } | 634 | } |
| 635 | println!("created {} patch files in ./patches/{id}-*", patches.len()); | ||
| 636 | Ok(()) | ||
| 637 | } | ||
| 116 | 638 | ||
| 639 | fn check_clean(git_repo: &Repo) -> Result<()> { | ||
| 117 | if git_repo.has_outstanding_changes()? { | 640 | if git_repo.has_outstanding_changes()? { |
| 118 | bail!( | 641 | bail!( |
| 119 | "cannot pull proposal branch when repository is not clean. discard or stash (un)staged changes and try again." | 642 | "cannot pull proposal branch when repository is not clean. discard or stash (un)staged changes and try again." |
diff --git a/src/sub_commands/send.rs b/src/sub_commands/send.rs index c3b3fda..004d263 100644 --- a/src/sub_commands/send.rs +++ b/src/sub_commands/send.rs | |||
| @@ -532,6 +532,13 @@ pub fn event_is_patch_set_root(event: &nostr::Event) -> bool { | |||
| 532 | event.kind.as_u64().eq(&PATCH_KIND) && event.iter_tags().any(|t| t.as_vec()[1].eq("root")) | 532 | event.kind.as_u64().eq(&PATCH_KIND) && event.iter_tags().any(|t| t.as_vec()[1].eq("root")) |
| 533 | } | 533 | } |
| 534 | 534 | ||
| 535 | pub fn patch_supports_commit_ids(event: &nostr::Event) -> bool { | ||
| 536 | event.kind.as_u64().eq(&PATCH_KIND) | ||
| 537 | && event | ||
| 538 | .iter_tags() | ||
| 539 | .any(|t| t.as_vec()[0].eq("commit-pgp-sig")) | ||
| 540 | } | ||
| 541 | |||
| 535 | #[allow(clippy::too_many_arguments)] | 542 | #[allow(clippy::too_many_arguments)] |
| 536 | pub fn generate_patch_event( | 543 | pub fn generate_patch_event( |
| 537 | git_repo: &Repo, | 544 | git_repo: &Repo, |