diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2024-02-23 08:15:24 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2024-02-23 08:15:24 +0000 |
| commit | 84d8f03cf2471d3530f4657055f272474880b6b5 (patch) | |
| tree | 36d1e57526fa9b201315552cada843a357334a55 | |
| parent | edb7bf7ee2ffbd718b927c5431d3c9fa5305ec06 (diff) | |
feat(push): add `--force` to issue revision
wrapping `send --in-reply-to` unless branch up-to-date
| -rw-r--r-- | src/main.rs | 4 | ||||
| -rw-r--r-- | src/sub_commands/push.rs | 67 | ||||
| -rw-r--r-- | src/sub_commands/send.rs | 14 | ||||
| -rw-r--r-- | tests/push.rs | 170 |
4 files changed, 231 insertions, 24 deletions
diff --git a/src/main.rs b/src/main.rs index 9b9b660..4c49280 100644 --- a/src/main.rs +++ b/src/main.rs | |||
| @@ -40,7 +40,7 @@ enum Commands { | |||
| 40 | /// list proposals; optionally apply them as a new branch | 40 | /// list proposals; optionally apply them as a new branch |
| 41 | List(sub_commands::list::SubCommandArgs), | 41 | List(sub_commands::list::SubCommandArgs), |
| 42 | /// send new commits as proposal amendments | 42 | /// send new commits as proposal amendments |
| 43 | Push, | 43 | Push(sub_commands::push::SubCommandArgs), |
| 44 | /// pull latest commits in proposal linked to checked out branch | 44 | /// pull latest commits in proposal linked to checked out branch |
| 45 | Pull, | 45 | Pull, |
| 46 | /// run with --nsec flag to change npub | 46 | /// run with --nsec flag to change npub |
| @@ -56,6 +56,6 @@ async fn main() -> Result<()> { | |||
| 56 | Commands::Send(args) => sub_commands::send::launch(&cli, args).await, | 56 | Commands::Send(args) => sub_commands::send::launch(&cli, args).await, |
| 57 | Commands::List(args) => sub_commands::list::launch(&cli, args).await, | 57 | Commands::List(args) => sub_commands::list::launch(&cli, args).await, |
| 58 | Commands::Pull => sub_commands::pull::launch().await, | 58 | Commands::Pull => sub_commands::pull::launch().await, |
| 59 | Commands::Push => sub_commands::push::launch(&cli).await, | 59 | Commands::Push(args) => sub_commands::push::launch(&cli, args).await, |
| 60 | } | 60 | } |
| 61 | } | 61 | } |
diff --git a/src/sub_commands/push.rs b/src/sub_commands/push.rs index dd32b2c..75bff2d 100644 --- a/src/sub_commands/push.rs +++ b/src/sub_commands/push.rs | |||
| @@ -11,6 +11,7 @@ use crate::{ | |||
| 11 | login, | 11 | login, |
| 12 | repo_ref::{self, RepoRef}, | 12 | repo_ref::{self, RepoRef}, |
| 13 | sub_commands::{ | 13 | sub_commands::{ |
| 14 | self, | ||
| 14 | list::{ | 15 | list::{ |
| 15 | find_commits_for_proposal_root_events, find_proposal_events, get_commit_id_from_patch, | 16 | find_commits_for_proposal_root_events, find_proposal_events, get_commit_id_from_patch, |
| 16 | get_most_recent_patch_with_ancestors, tag_value, | 17 | get_most_recent_patch_with_ancestors, tag_value, |
| @@ -20,7 +21,18 @@ use crate::{ | |||
| 20 | Cli, | 21 | Cli, |
| 21 | }; | 22 | }; |
| 22 | 23 | ||
| 23 | pub async fn launch(cli_args: &Cli) -> Result<()> { | 24 | #[derive(Debug, clap::Args)] |
| 25 | pub struct SubCommandArgs { | ||
| 26 | #[arg(long, action)] | ||
| 27 | /// send proposal revision from checked out proposal branch | ||
| 28 | force: bool, | ||
| 29 | #[arg(long, action)] | ||
| 30 | /// dont prompt for cover letter when force pushing | ||
| 31 | no_cover_letter: bool, | ||
| 32 | } | ||
| 33 | |||
| 34 | #[allow(clippy::too_many_lines)] | ||
| 35 | pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { | ||
| 24 | let git_repo = Repo::discover().context("cannot find a git repository")?; | 36 | let git_repo = Repo::discover().context("cannot find a git repository")?; |
| 25 | 37 | ||
| 26 | let (main_or_master_branch_name, _) = git_repo | 38 | let (main_or_master_branch_name, _) = git_repo |
| @@ -59,26 +71,55 @@ pub async fn launch(cli_args: &Cli) -> Result<()> { | |||
| 59 | ) | 71 | ) |
| 60 | .await?; | 72 | .await?; |
| 61 | 73 | ||
| 62 | // TODO: fix these scenarios: | ||
| 63 | // - local proposal branch is 2 behind and 1 ahead. intructions: ... | ||
| 64 | // - proposal has been rebased. (against commit in main) instructions: ... | ||
| 65 | // - proposal has been rebased. (against commit not in repo) instructions: .. | ||
| 66 | |||
| 67 | let most_recent_proposal_patch_chain = get_most_recent_patch_with_ancestors(commit_events) | 74 | let most_recent_proposal_patch_chain = get_most_recent_patch_with_ancestors(commit_events) |
| 68 | .context("cannot get most recent patch for proposal")?; | 75 | .context("cannot get most recent patch for proposal")?; |
| 69 | 76 | ||
| 70 | let branch_tip = git_repo.get_tip_of_local_branch(&branch_name)?; | 77 | let branch_tip = git_repo.get_tip_of_local_branch(&branch_name)?; |
| 71 | 78 | ||
| 72 | let most_recent_patch_commit_id = str_to_sha1( | 79 | let most_recent_patch_commit_id = str_to_sha1( |
| 73 | &get_commit_id_from_patch(&most_recent_proposal_patch_chain[0]) | 80 | &get_commit_id_from_patch( |
| 74 | .context("latest patch event doesnt have a commit tag")?, | 81 | most_recent_proposal_patch_chain |
| 82 | .first() | ||
| 83 | .context("no patches found")?, | ||
| 84 | ) | ||
| 85 | .context("latest patch event doesnt have a commit tag")?, | ||
| 75 | ) | 86 | ) |
| 76 | .context("latest patch event commit tag isn't a valid SHA1 hash")?; | 87 | .context("latest patch event commit tag isn't a valid SHA1 hash")?; |
| 77 | 88 | ||
| 89 | let proposal_base_commit_id = str_to_sha1( | ||
| 90 | &tag_value( | ||
| 91 | most_recent_proposal_patch_chain | ||
| 92 | .last() | ||
| 93 | .context("no patches found")?, | ||
| 94 | "parent-commit", | ||
| 95 | ) | ||
| 96 | .context("patch is incorrectly formatted")?, | ||
| 97 | ) | ||
| 98 | .context("latest patch event parent-commit tag isn't a valid SHA1 hash")?; | ||
| 99 | |||
| 78 | if most_recent_patch_commit_id.eq(&branch_tip) { | 100 | if most_recent_patch_commit_id.eq(&branch_tip) { |
| 79 | bail!("proposal already up-to-date with local branch"); | 101 | bail!("proposal already up-to-date with local branch"); |
| 80 | } | 102 | } |
| 81 | 103 | ||
| 104 | if args.force { | ||
| 105 | println!("preparing to force push proposal revision..."); | ||
| 106 | sub_commands::send::launch( | ||
| 107 | cli_args, | ||
| 108 | &sub_commands::send::SubCommandArgs { | ||
| 109 | starting_commit: String::new(), | ||
| 110 | in_reply_to: Some(proposal_root_event.id.to_string()), | ||
| 111 | title: None, | ||
| 112 | description: None, | ||
| 113 | from_branch: None, | ||
| 114 | to_branch: None, | ||
| 115 | no_cover_letter: args.no_cover_letter, | ||
| 116 | }, | ||
| 117 | ) | ||
| 118 | .await?; | ||
| 119 | println!("force pushed proposal revision"); | ||
| 120 | return Ok(()); | ||
| 121 | } | ||
| 122 | |||
| 82 | if most_recent_proposal_patch_chain.iter().any(|e| { | 123 | if most_recent_proposal_patch_chain.iter().any(|e| { |
| 83 | let c = tag_value(e, "parent-commit").unwrap_or_default(); | 124 | let c = tag_value(e, "parent-commit").unwrap_or_default(); |
| 84 | c.eq(&branch_tip.to_string()) | 125 | c.eq(&branch_tip.to_string()) |
| @@ -86,9 +127,15 @@ pub async fn launch(cli_args: &Cli) -> Result<()> { | |||
| 86 | bail!("proposal is ahead of local branch"); | 127 | bail!("proposal is ahead of local branch"); |
| 87 | } | 128 | } |
| 88 | 129 | ||
| 89 | let (ahead, behind) = git_repo | 130 | let Ok((ahead, behind)) = git_repo |
| 90 | .get_commits_ahead_behind(&most_recent_patch_commit_id, &branch_tip) | 131 | .get_commits_ahead_behind(&most_recent_patch_commit_id, &branch_tip) |
| 91 | .context("the latest patch in proposal doesnt share an ancestor with your branch.")?; | 132 | .context("the latest patch in proposal doesnt share an ancestor with your branch.") |
| 133 | else { | ||
| 134 | if git_repo.ancestor_of(&proposal_base_commit_id, &branch_tip)? { | ||
| 135 | bail!("local unpublished proposal ammendments. consider force pushing."); | ||
| 136 | } | ||
| 137 | bail!("local unpublished proposal has been rebased. consider force pushing"); | ||
| 138 | }; | ||
| 92 | 139 | ||
| 93 | if !behind.is_empty() { | 140 | if !behind.is_empty() { |
| 94 | bail!( | 141 | bail!( |
diff --git a/src/sub_commands/send.rs b/src/sub_commands/send.rs index 1ccb1f4..ebe23b1 100644 --- a/src/sub_commands/send.rs +++ b/src/sub_commands/send.rs | |||
| @@ -27,26 +27,26 @@ pub struct SubCommandArgs { | |||
| 27 | #[arg(default_value = "")] | 27 | #[arg(default_value = "")] |
| 28 | /// starting commit (commits since in current branch) or commit range, like | 28 | /// starting commit (commits since in current branch) or commit range, like |
| 29 | /// in `git format-patch` | 29 | /// in `git format-patch` |
| 30 | starting_commit: String, | 30 | pub(crate) starting_commit: String, |
| 31 | #[clap(long)] | 31 | #[clap(long)] |
| 32 | /// nevent or event id of an existing proposal for which this is a new | 32 | /// nevent or event id of an existing proposal for which this is a new |
| 33 | /// version | 33 | /// version |
| 34 | in_reply_to: Option<String>, | 34 | pub(crate) in_reply_to: Option<String>, |
| 35 | /// optional cover letter title | 35 | /// optional cover letter title |
| 36 | #[clap(short, long)] | 36 | #[clap(short, long)] |
| 37 | title: Option<String>, | 37 | pub(crate) title: Option<String>, |
| 38 | #[clap(short, long)] | 38 | #[clap(short, long)] |
| 39 | /// optional cover letter description | 39 | /// optional cover letter description |
| 40 | description: Option<String>, | 40 | pub(crate) description: Option<String>, |
| 41 | #[clap(long)] | 41 | #[clap(long)] |
| 42 | /// branch to get changes from (defaults to head) | 42 | /// branch to get changes from (defaults to head) |
| 43 | from_branch: Option<String>, | 43 | pub(crate) from_branch: Option<String>, |
| 44 | #[clap(long)] | 44 | #[clap(long)] |
| 45 | /// destination branch (defaults to main or master) | 45 | /// destination branch (defaults to main or master) |
| 46 | to_branch: Option<String>, | 46 | pub(crate) to_branch: Option<String>, |
| 47 | /// don't ask about a cover letter | 47 | /// don't ask about a cover letter |
| 48 | #[arg(long, action)] | 48 | #[arg(long, action)] |
| 49 | no_cover_letter: bool, | 49 | pub(crate) no_cover_letter: bool, |
| 50 | } | 50 | } |
| 51 | 51 | ||
| 52 | #[allow(clippy::too_many_lines)] | 52 | #[allow(clippy::too_many_lines)] |
diff --git a/tests/push.rs b/tests/push.rs index d1ad0e6..81daf0e 100644 --- a/tests/push.rs +++ b/tests/push.rs | |||
| @@ -243,7 +243,7 @@ mod when_branch_is_checked_out { | |||
| 243 | 243 | ||
| 244 | mod cli_prompts { | 244 | mod cli_prompts { |
| 245 | use super::*; | 245 | use super::*; |
| 246 | async fn run_async_cli_show_up_to_date() -> Result<()> { | 246 | async fn run_async_cli_shows_proposal_ahead_error() -> Result<()> { |
| 247 | let (mut r51, mut r52, mut r53, mut r55, mut r56) = ( | 247 | let (mut r51, mut r52, mut r53, mut r55, mut r56) = ( |
| 248 | Relay::new(8051, None, None), | 248 | Relay::new(8051, None, None), |
| 249 | Relay::new(8052, None, None), | 249 | Relay::new(8052, None, None), |
| @@ -294,8 +294,8 @@ mod when_branch_is_checked_out { | |||
| 294 | 294 | ||
| 295 | #[tokio::test] | 295 | #[tokio::test] |
| 296 | #[serial] | 296 | #[serial] |
| 297 | async fn cli_show_up_to_date() -> Result<()> { | 297 | async fn cli_show_proposal_ahead_error() -> Result<()> { |
| 298 | let _ = run_async_cli_show_up_to_date().await; | 298 | let _ = run_async_cli_shows_proposal_ahead_error().await; |
| 299 | Ok(()) | 299 | Ok(()) |
| 300 | } | 300 | } |
| 301 | } | 301 | } |
| @@ -476,7 +476,167 @@ mod when_branch_is_checked_out { | |||
| 476 | } | 476 | } |
| 477 | 477 | ||
| 478 | mod when_branch_has_been_rebased { | 478 | mod when_branch_has_been_rebased { |
| 479 | // use super::*; | 479 | use super::*; |
| 480 | // TODO | 480 | |
| 481 | mod cli_prompts { | ||
| 482 | use super::*; | ||
| 483 | async fn run_async_cli_shows_unpublished_rebase_error() -> Result<()> { | ||
| 484 | let (mut r51, mut r52, mut r53, mut r55, mut r56) = ( | ||
| 485 | Relay::new(8051, None, None), | ||
| 486 | Relay::new(8052, None, None), | ||
| 487 | Relay::new(8053, None, None), | ||
| 488 | Relay::new(8055, None, None), | ||
| 489 | Relay::new(8056, None, None), | ||
| 490 | ); | ||
| 491 | |||
| 492 | r51.events.push(generate_test_key_1_relay_list_event()); | ||
| 493 | r51.events.push(generate_test_key_1_metadata_event("fred")); | ||
| 494 | r51.events.push(generate_repo_ref_event()); | ||
| 495 | |||
| 496 | r55.events.push(generate_repo_ref_event()); | ||
| 497 | r55.events.push(generate_test_key_1_metadata_event("fred")); | ||
| 498 | r55.events.push(generate_test_key_1_relay_list_event()); | ||
| 499 | |||
| 500 | let cli_tester_handle = std::thread::spawn(move || -> Result<()> { | ||
| 501 | cli_tester_create_proposals()?; | ||
| 502 | |||
| 503 | let test_repo = GitTestRepo::default(); | ||
| 504 | test_repo.populate()?; | ||
| 505 | |||
| 506 | // simulate rebase | ||
| 507 | std::fs::write(test_repo.dir.join("amazing.md"), "some content")?; | ||
| 508 | test_repo.stage_and_commit("commit for rebasing on top of")?; | ||
| 509 | create_and_populate_branch(&test_repo, FEATURE_BRANCH_NAME_1, "a", true)?; | ||
| 510 | |||
| 511 | let mut p = CliTester::new_from_dir(&test_repo.dir, ["push"]); | ||
| 512 | // p.expect_end_eventually_and_print()?; | ||
| 513 | |||
| 514 | p.expect("finding proposal root event...\r\n")?; | ||
| 515 | p.expect("found proposal root event. finding commits...\r\n")?; | ||
| 516 | p.expect("Error: local unpublished proposal has been rebased. consider force pushing\r\n")?; | ||
| 517 | p.expect_end()?; | ||
| 518 | |||
| 519 | for p in [51, 52, 53, 55, 56] { | ||
| 520 | relay::shutdown_relay(8000 + p)?; | ||
| 521 | } | ||
| 522 | Ok(()) | ||
| 523 | }); | ||
| 524 | |||
| 525 | // launch relay | ||
| 526 | let _ = join!( | ||
| 527 | r51.listen_until_close(), | ||
| 528 | r52.listen_until_close(), | ||
| 529 | r53.listen_until_close(), | ||
| 530 | r55.listen_until_close(), | ||
| 531 | r56.listen_until_close(), | ||
| 532 | ); | ||
| 533 | cli_tester_handle.join().unwrap()?; | ||
| 534 | Ok(()) | ||
| 535 | } | ||
| 536 | |||
| 537 | #[tokio::test] | ||
| 538 | #[serial] | ||
| 539 | async fn cli_shows_unpublished_rebase_error() -> Result<()> { | ||
| 540 | let _ = run_async_cli_shows_unpublished_rebase_error().await; | ||
| 541 | Ok(()) | ||
| 542 | } | ||
| 543 | } | ||
| 544 | mod with_force_flag { | ||
| 545 | use super::*; | ||
| 546 | |||
| 547 | mod cli_prompts { | ||
| 548 | use super::*; | ||
| 549 | async fn run_async_cli_shows_revision_sent() -> Result<()> { | ||
| 550 | let (mut r51, mut r52, mut r53, mut r55, mut r56) = ( | ||
| 551 | Relay::new(8051, None, None), | ||
| 552 | Relay::new(8052, None, None), | ||
| 553 | Relay::new(8053, None, None), | ||
| 554 | Relay::new(8055, None, None), | ||
| 555 | Relay::new(8056, None, None), | ||
| 556 | ); | ||
| 557 | |||
| 558 | r51.events.push(generate_test_key_1_relay_list_event()); | ||
| 559 | r51.events.push(generate_test_key_1_metadata_event("fred")); | ||
| 560 | r51.events.push(generate_repo_ref_event()); | ||
| 561 | |||
| 562 | r55.events.push(generate_repo_ref_event()); | ||
| 563 | r55.events.push(generate_test_key_1_metadata_event("fred")); | ||
| 564 | r55.events.push(generate_test_key_1_relay_list_event()); | ||
| 565 | |||
| 566 | let cli_tester_handle = std::thread::spawn(move || -> Result<()> { | ||
| 567 | cli_tester_create_proposals()?; | ||
| 568 | |||
| 569 | let test_repo = GitTestRepo::default(); | ||
| 570 | test_repo.populate()?; | ||
| 571 | |||
| 572 | // simulate rebase | ||
| 573 | std::fs::write(test_repo.dir.join("amazing.md"), "some content")?; | ||
| 574 | test_repo.stage_and_commit("commit for rebasing on top of")?; | ||
| 575 | create_and_populate_branch(&test_repo, FEATURE_BRANCH_NAME_1, "a", false)?; | ||
| 576 | let mut p = CliTester::new_from_dir( | ||
| 577 | &test_repo.dir, | ||
| 578 | [ | ||
| 579 | "--nsec", | ||
| 580 | TEST_KEY_1_NSEC, | ||
| 581 | "--password", | ||
| 582 | TEST_PASSWORD, | ||
| 583 | "--disable-cli-spinners", | ||
| 584 | "push", | ||
| 585 | "--force", | ||
| 586 | "--no-cover-letter", | ||
| 587 | ], | ||
| 588 | ); | ||
| 589 | p.expect("finding proposal root event...\r\n")?; | ||
| 590 | p.expect("found proposal root event. finding commits...\r\n")?; | ||
| 591 | p.expect("preparing to force push proposal revision...\r\n")?; | ||
| 592 | |||
| 593 | // standard output from `ngit send` | ||
| 594 | p.expect("creating patch for 2 commits from 'head' that can be merged into 'main'\r\n")?; | ||
| 595 | p.expect("searching for profile and relay updates...\r\n")?; | ||
| 596 | p.expect("\r")?; | ||
| 597 | p.expect("logged in as fred\r\n")?; | ||
| 598 | p.expect("posting 2 patches without a covering letter...\r\n")?; | ||
| 599 | |||
| 600 | relay::expect_send_with_progress( | ||
| 601 | &mut p, | ||
| 602 | vec![ | ||
| 603 | (" [my-relay] [repo-relay] ws://localhost:8055", true, ""), | ||
| 604 | (" [my-relay] ws://localhost:8053", true, ""), | ||
| 605 | (" [repo-relay] ws://localhost:8056", true, ""), | ||
| 606 | (" [default] ws://localhost:8051", true, ""), | ||
| 607 | (" [default] ws://localhost:8052", true, ""), | ||
| 608 | ], | ||
| 609 | 2, | ||
| 610 | )?; | ||
| 611 | // end standard `ngit send output` | ||
| 612 | p.expect_after_whitespace("force pushed proposal revision\r\n")?; | ||
| 613 | p.expect_end()?; | ||
| 614 | |||
| 615 | for p in [51, 52, 53, 55, 56] { | ||
| 616 | relay::shutdown_relay(8000 + p)?; | ||
| 617 | } | ||
| 618 | Ok(()) | ||
| 619 | }); | ||
| 620 | |||
| 621 | // launch relay | ||
| 622 | let _ = join!( | ||
| 623 | r51.listen_until_close(), | ||
| 624 | r52.listen_until_close(), | ||
| 625 | r53.listen_until_close(), | ||
| 626 | r55.listen_until_close(), | ||
| 627 | r56.listen_until_close(), | ||
| 628 | ); | ||
| 629 | cli_tester_handle.join().unwrap()?; | ||
| 630 | Ok(()) | ||
| 631 | } | ||
| 632 | |||
| 633 | #[tokio::test] | ||
| 634 | #[serial] | ||
| 635 | async fn cli_shows_revision_sent() -> Result<()> { | ||
| 636 | let _ = run_async_cli_shows_revision_sent().await; | ||
| 637 | Ok(()) | ||
| 638 | } | ||
| 639 | } | ||
| 640 | } | ||
| 481 | } | 641 | } |
| 482 | } | 642 | } |