upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2024-02-23 08:15:24 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2024-02-23 08:15:24 +0000
commit84d8f03cf2471d3530f4657055f272474880b6b5 (patch)
tree36d1e57526fa9b201315552cada843a357334a55
parentedb7bf7ee2ffbd718b927c5431d3c9fa5305ec06 (diff)
feat(push): add `--force` to issue revision
wrapping `send --in-reply-to` unless branch up-to-date
-rw-r--r--src/main.rs4
-rw-r--r--src/sub_commands/push.rs67
-rw-r--r--src/sub_commands/send.rs14
-rw-r--r--tests/push.rs170
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
23pub async fn launch(cli_args: &Cli) -> Result<()> { 24#[derive(Debug, clap::Args)]
25pub 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)]
35pub 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}