diff options
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, |