From f08ee98ab7e19d4e42ffa85aa619f012441fbe47 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Mon, 11 Nov 2024 09:06:19 +0000 Subject: Revert "refactor: remove ngit `pull` `push` `fetch`" This reverts commit 43b5e9b38bf5dcfbac85637a2d3efc69ddfe77ac. --- src/bin/ngit/cli.rs | 6 + src/bin/ngit/main.rs | 3 + src/bin/ngit/sub_commands/fetch.rs | 37 +++ src/bin/ngit/sub_commands/mod.rs | 3 + src/bin/ngit/sub_commands/pull.rs | 203 ++++++++++++ src/bin/ngit/sub_commands/push.rs | 219 +++++++++++++ tests/ngit_pull.rs | 615 +++++++++++++++++++++++++++++++++++++ tests/ngit_push.rs | 531 ++++++++++++++++++++++++++++++++ 8 files changed, 1617 insertions(+) create mode 100644 src/bin/ngit/sub_commands/fetch.rs create mode 100644 src/bin/ngit/sub_commands/pull.rs create mode 100644 src/bin/ngit/sub_commands/push.rs create mode 100644 tests/ngit_pull.rs create mode 100644 tests/ngit_push.rs diff --git a/src/bin/ngit/cli.rs b/src/bin/ngit/cli.rs index 13e18d1..d0f934e 100644 --- a/src/bin/ngit/cli.rs +++ b/src/bin/ngit/cli.rs @@ -27,12 +27,18 @@ pub struct Cli { #[derive(Subcommand)] pub enum Commands { + /// update cache with latest updates from nostr + Fetch(sub_commands::fetch::SubCommandArgs), /// signal you are this repo's maintainer accepting proposals via nostr Init(sub_commands::init::SubCommandArgs), /// issue commits as a proposal Send(sub_commands::send::SubCommandArgs), /// list proposals; checkout, apply or download selected List, + /// send proposal revision + Push(sub_commands::push::SubCommandArgs), + /// fetch and apply new proposal commits / revisions linked to branch + Pull, /// run with --nsec flag to change npub Login(sub_commands::login::SubCommandArgs), } diff --git a/src/bin/ngit/main.rs b/src/bin/ngit/main.rs index 38dc2c1..45cbef5 100644 --- a/src/bin/ngit/main.rs +++ b/src/bin/ngit/main.rs @@ -15,9 +15,12 @@ mod sub_commands; async fn main() -> Result<()> { let cli = Cli::parse(); match &cli.command { + Commands::Fetch(args) => sub_commands::fetch::launch(&cli, args).await, Commands::Login(args) => sub_commands::login::launch(&cli, args).await, Commands::Init(args) => sub_commands::init::launch(&cli, args).await, Commands::Send(args) => sub_commands::send::launch(&cli, args, false).await, Commands::List => sub_commands::list::launch().await, + Commands::Pull => sub_commands::pull::launch().await, + Commands::Push(args) => sub_commands::push::launch(&cli, args).await, } } diff --git a/src/bin/ngit/sub_commands/fetch.rs b/src/bin/ngit/sub_commands/fetch.rs new file mode 100644 index 0000000..c69f1c5 --- /dev/null +++ b/src/bin/ngit/sub_commands/fetch.rs @@ -0,0 +1,37 @@ +use std::collections::HashSet; + +use anyhow::{Context, Result}; +use clap; +use nostr::nips::nip01::Coordinate; + +use crate::{ + cli::Cli, + client::{fetching_with_report, Client, Connect}, + git::{Repo, RepoActions}, + repo_ref::get_repo_coordinates, +}; + +#[derive(clap::Args)] +pub struct SubCommandArgs { + /// address pointer to repo announcement + #[arg(long, action)] + repo: Vec, +} + +pub async fn launch(args: &Cli, command_args: &SubCommandArgs) -> Result<()> { + let _ = args; + let git_repo = Repo::discover().context("cannot find a git repository")?; + let client = Client::default(); + let repo_coordinates = if command_args.repo.is_empty() { + get_repo_coordinates(&git_repo, &client).await? + } else { + let mut repo_coordinates = HashSet::new(); + for repo in &command_args.repo { + repo_coordinates.insert(Coordinate::parse(repo.clone())?); + } + repo_coordinates + }; + fetching_with_report(git_repo.get_path()?, &client, &repo_coordinates).await?; + client.disconnect().await?; + Ok(()) +} diff --git a/src/bin/ngit/sub_commands/mod.rs b/src/bin/ngit/sub_commands/mod.rs index a53197f..29a60f9 100644 --- a/src/bin/ngit/sub_commands/mod.rs +++ b/src/bin/ngit/sub_commands/mod.rs @@ -1,4 +1,7 @@ +pub mod fetch; pub mod init; pub mod list; pub mod login; +pub mod pull; +pub mod push; pub mod send; diff --git a/src/bin/ngit/sub_commands/pull.rs b/src/bin/ngit/sub_commands/pull.rs new file mode 100644 index 0000000..d79b7b1 --- /dev/null +++ b/src/bin/ngit/sub_commands/pull.rs @@ -0,0 +1,203 @@ +use anyhow::{bail, Context, Result}; +use ngit::git_events::is_event_proposal_root_for_branch; +use nostr_sdk::PublicKey; + +use crate::{ + client::{ + fetching_with_report, get_all_proposal_patch_events_from_cache, + get_proposals_and_revisions_from_cache, get_repo_ref_from_cache, Client, Connect, + }, + git::{str_to_sha1, Repo, RepoActions}, + git_events::{get_commit_id_from_patch, get_most_recent_patch_with_ancestors, tag_value}, + repo_ref::get_repo_coordinates, +}; + +#[allow(clippy::too_many_lines)] +pub async fn launch() -> Result<()> { + let git_repo = Repo::discover().context("cannot find a git repository")?; + let git_repo_path = git_repo.get_path()?; + + let (main_or_master_branch_name, _) = git_repo + .get_main_or_master_branch() + .context("no main or master branch")?; + + let branch_name = git_repo + .get_checked_out_branch_name() + .context("cannot get checked out branch name")?; + + if branch_name == main_or_master_branch_name { + bail!("checkout a branch associated with a proposal first") + } + let client = Client::default(); + + let repo_coordinates = get_repo_coordinates(&git_repo, &client).await?; + fetching_with_report(git_repo_path, &client, &repo_coordinates).await?; + + let repo_ref = get_repo_ref_from_cache(git_repo_path, &repo_coordinates).await?; + + let logged_in_public_key = + if let Ok(Some(npub)) = git_repo.get_git_config_item("nostr.npub", None) { + PublicKey::parse(npub).ok() + } else { + None + }; + + let proposal_root_event = + get_proposals_and_revisions_from_cache(git_repo_path, repo_ref.coordinates()) + .await? + .iter() + .find(|e| { + is_event_proposal_root_for_branch(e, &branch_name, &logged_in_public_key) + .unwrap_or(false) + }) + .context("cannot find proposal that matches the current branch name")? + .clone(); + + let commit_events = + get_all_proposal_patch_events_from_cache(git_repo_path, &repo_ref, &proposal_root_event.id) + .await?; + + let most_recent_proposal_patch_chain = + get_most_recent_patch_with_ancestors(commit_events.clone()) + .context("cannot get most recent patch for proposal")?; + + let local_branch_tip = git_repo.get_tip_of_branch(&branch_name)?; + + let (main_branch_name, master_tip) = git_repo.get_main_or_master_branch()?; + + let (local_ahead_of_main, local_beind_main) = + git_repo.get_commits_ahead_behind(&master_tip, &local_branch_tip)?; + + let proposal_base_commit = str_to_sha1(&tag_value( + most_recent_proposal_patch_chain + .last() + .context("there should be at least one patch as we have already checked for this")?, + "parent-commit", + )?) + .context("cannot get valid parent commit id from patch")?; + + let (_, proposal_behind_main) = + git_repo.get_commits_ahead_behind(&master_tip, &proposal_base_commit)?; + + let proposal_tip = + str_to_sha1( + &get_commit_id_from_patch(most_recent_proposal_patch_chain.first().context( + "there should be at least one patch as we have already checked for this", + )?) + .context("cannot get valid commit_id from patch")?, + ) + .context("cannot get valid commit_id from patch")?; + + // if uptodate + if proposal_tip.eq(&local_branch_tip) { + println!("branch already up-to-date"); + } + // if new appendments + else if most_recent_proposal_patch_chain.iter().any(|patch| { + get_commit_id_from_patch(patch) + .unwrap_or_default() + .eq(&local_branch_tip.to_string()) + }) { + check_clean(&git_repo)?; + let applied = git_repo + .apply_patch_chain(&branch_name, most_recent_proposal_patch_chain) + .context("cannot apply patch chain")?; + println!("applied {} new commits", applied.len(),); + } + // if parent commit doesnt exist + else if !git_repo.does_commit_exist(&proposal_base_commit.to_string())? { + println!( + "a new version of the proposal has a prant commit that doesnt exist in your local repository." + ); + println!("your '{main_branch_name}' branch may not be up-to-date."); + println!("manually run `git pull` on '{main_branch_name}' and try again"); + } + // if new revision and no local changes (tip of local in proposal history) + else if commit_events.iter().any(|patch| { + get_commit_id_from_patch(patch) + .unwrap_or_default() + .eq(&local_branch_tip.to_string()) + }) { + check_clean(&git_repo)?; + + git_repo.create_branch_at_commit(&branch_name, &proposal_base_commit.to_string())?; + let applied = git_repo + .apply_patch_chain(&branch_name, most_recent_proposal_patch_chain) + .context("cannot apply patch chain")?; + + println!( + "pulled new version of proposal ({} ahead {} behind '{main_branch_name}'), replacing old version ({} ahead {} behind '{main_branch_name}')", + applied.len(), + proposal_behind_main.len(), + local_ahead_of_main.len(), + local_beind_main.len(), + ); + } + // if tip of proposal in branch in history (local appendments made to up-to-date + // proposal) + else if git_repo.ancestor_of(&local_branch_tip, &proposal_tip)? { + let (local_ahead_of_proposal, _) = git_repo + .get_commits_ahead_behind(&proposal_tip, &local_branch_tip) + .context("cannot get commits ahead behind for propsal_top and local_branch_tip")?; + println!( + "local proposal branch exists with {} unpublished commits on top of the most up-to-date version of the proposal", + local_ahead_of_proposal.len() + ); + } else { + println!("you have an amended/rebase version the proposal that is unpublished"); + // user probably has a unpublished amended or rebase version of the latest + // proposal version + // if tip of proposal commits exist (were once part of branch but have been + // amended and git clean up job hasn't removed them) + if git_repo.does_commit_exist(&proposal_tip.to_string())? { + println!( + "you have previously applied the latest version of the proposal ({} ahead {} behind '{main_branch_name}') but your local proposal branch has amended or rebased it ({} ahead {} behind '{main_branch_name}')", + most_recent_proposal_patch_chain.len(), + proposal_behind_main.len(), + local_ahead_of_main.len(), + local_beind_main.len(), + ); + } + // user probably has a unpublished amended or rebase version of an older + // proposal version + else { + println!( + "your local proposal branch ({} ahead {} behind '{main_branch_name}') has conflicting changes with the latest published proposal ({} ahead {} behind '{main_branch_name}')", + local_ahead_of_main.len(), + local_beind_main.len(), + most_recent_proposal_patch_chain.len(), + proposal_behind_main.len(), + ); + + println!( + "its likely that you have rebased / amended an old proposal version because git has no record of the latest proposal commit." + ); + println!( + "it is possible that you have been working off the latest version and git has delete this commit as part of a clean up" + ); + } + println!("to view the latest proposal but retain your changes:"); + println!(" 1) create a new branch off the tip commit of this one to store your changes"); + println!(" 2) run `ngit list` and checkout the latest published version of this proposal"); + + println!("if you are confident in your changes consider running `ngit push --force`"); + + // TODO: this copy could be refined further based on this: + // - amended commits in the proposal + // - if local_base eq proposal base + // - amended an older version of proposal + // - if local_base is behind proposal_base + // - rebased the proposal + // - if local_base is ahead of proposal_base + } + Ok(()) +} + +fn check_clean(git_repo: &Repo) -> Result<()> { + if git_repo.has_outstanding_changes()? { + bail!( + "cannot pull proposal branch when repository is not clean. discard or stash (un)staged changes and try again." + ); + } + Ok(()) +} diff --git a/src/bin/ngit/sub_commands/push.rs b/src/bin/ngit/sub_commands/push.rs new file mode 100644 index 0000000..a77f356 --- /dev/null +++ b/src/bin/ngit/sub_commands/push.rs @@ -0,0 +1,219 @@ +use anyhow::{bail, Context, Result}; +use ngit::{ + client::send_events, + git_events::{is_event_proposal_root_for_branch, tag_value}, +}; +use nostr_sdk::PublicKey; + +use crate::{ + cli::Cli, + client::{ + fetching_with_report, get_all_proposal_patch_events_from_cache, + get_proposals_and_revisions_from_cache, get_repo_ref_from_cache, Client, Connect, + }, + git::{identify_ahead_behind, str_to_sha1, Repo, RepoActions}, + git_events::{ + generate_patch_event, get_commit_id_from_patch, get_most_recent_patch_with_ancestors, + }, + login, + repo_ref::get_repo_coordinates, + sub_commands, +}; + +#[derive(Debug, clap::Args)] +pub struct SubCommandArgs { + #[arg(long, action)] + /// send proposal revision from checked out proposal branch + force: bool, +} + +#[allow(clippy::too_many_lines)] +pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { + let git_repo = Repo::discover().context("cannot find a git repository")?; + let git_repo_path = git_repo.get_path()?; + + let (main_or_master_branch_name, _) = git_repo + .get_main_or_master_branch() + .context("no main or master branch")?; + + let root_commit = git_repo + .get_root_commit() + .context("failed to get root commit of the repository")?; + + let branch_name = git_repo + .get_checked_out_branch_name() + .context("cannot get checked out branch name")?; + + if branch_name == main_or_master_branch_name { + bail!("checkout a branch associated with a proposal first") + } + let mut client = Client::default(); + + let repo_coordinates = get_repo_coordinates(&git_repo, &client).await?; + + fetching_with_report(git_repo_path, &client, &repo_coordinates).await?; + + let repo_ref = get_repo_ref_from_cache(git_repo_path, &repo_coordinates).await?; + + let logged_in_public_key = + if let Ok(Some(npub)) = git_repo.get_git_config_item("nostr.npub", None) { + PublicKey::parse(npub).ok() + } else { + None + }; + + let proposal_root_event = + get_proposals_and_revisions_from_cache(git_repo_path, repo_ref.coordinates()) + .await? + .iter() + .find(|e| { + is_event_proposal_root_for_branch(e, &branch_name, &logged_in_public_key) + .unwrap_or(false) + }) + .context("cannot find proposal that matches the current branch name")? + .clone(); + + let commit_events = + get_all_proposal_patch_events_from_cache(git_repo_path, &repo_ref, &proposal_root_event.id) + .await?; + + let most_recent_proposal_patch_chain = get_most_recent_patch_with_ancestors(commit_events) + .context("cannot get most recent patch for proposal")?; + + let branch_tip = git_repo.get_tip_of_branch(&branch_name)?; + + let most_recent_patch_commit_id = str_to_sha1( + &get_commit_id_from_patch( + most_recent_proposal_patch_chain + .first() + .context("no patches found")?, + ) + .context("latest patch event doesnt have a commit tag")?, + ) + .context("latest patch event commit tag isn't a valid SHA1 hash")?; + + let proposal_base_commit_id = str_to_sha1( + &tag_value( + most_recent_proposal_patch_chain + .last() + .context("no patches found")?, + "parent-commit", + ) + .context("patch is incorrectly formatted")?, + ) + .context("latest patch event parent-commit tag isn't a valid SHA1 hash")?; + + if most_recent_patch_commit_id.eq(&branch_tip) { + bail!("proposal already up-to-date with local branch"); + } + + if args.force { + println!("preparing to force push proposal revision..."); + sub_commands::send::launch( + cli_args, + &sub_commands::send::SubCommandArgs { + // if not ahead of master prompt, otherwise assume proposal revision is all commits + // ahead + since_or_range: if let Ok((_, _, ahead, _)) = + identify_ahead_behind(&git_repo, &None, &None) + { + if ahead.is_empty() { + String::new() + } else { + format!("HEAD~{}", ahead.len()) + } + } else { + String::new() + }, + in_reply_to: vec![proposal_root_event.id.to_string()], + title: None, + description: None, + no_cover_letter: true, + }, + true, + ) + .await?; + println!("force pushed proposal revision"); + return Ok(()); + } + + if most_recent_proposal_patch_chain.iter().any(|e| { + let c = tag_value(e, "parent-commit").unwrap_or_default(); + c.eq(&branch_tip.to_string()) + }) { + bail!("proposal is ahead of local branch"); + } + + let Ok((ahead, behind)) = git_repo + .get_commits_ahead_behind(&most_recent_patch_commit_id, &branch_tip) + .context("the latest patch in proposal doesnt share an ancestor with your branch.") + else { + if git_repo.ancestor_of(&proposal_base_commit_id, &branch_tip)? { + bail!("local unpublished proposal ammendments. consider force pushing."); + } + bail!("local unpublished proposal has been rebased. consider force pushing"); + }; + + if !behind.is_empty() { + bail!( + "your local proposal branch is {} behind patches on nostr. consider rebasing or force pushing", + behind.len() + ) + } + + println!( + "{} commits ahead. preparing to create creating patch events.", + ahead.len() + ); + + let (signer, user_ref) = login::launch( + &git_repo, + &cli_args.bunker_uri, + &cli_args.bunker_app_key, + &cli_args.nsec, + &cli_args.password, + Some(&client), + false, + false, + ) + .await?; + + let mut patch_events: Vec = vec![]; + for commit in &ahead { + patch_events.push( + generate_patch_event( + &git_repo, + &root_commit, + commit, + Some(proposal_root_event.id), + &signer, + &repo_ref, + patch_events.last().map(|e| e.id), + None, + None, + &None, + &[], + ) + .await + .context("cannot make patch event from commit")?, + ); + } + println!("pushing {} commits", ahead.len()); + + client.set_signer(signer).await; + + send_events( + &client, + git_repo_path, + patch_events, + user_ref.relays.write(), + repo_ref.relays.clone(), + !cli_args.disable_cli_spinners, + false, + ) + .await?; + + println!("pushed {} commits", ahead.len()); + + Ok(()) +} diff --git a/tests/ngit_pull.rs b/tests/ngit_pull.rs new file mode 100644 index 0000000..6637859 --- /dev/null +++ b/tests/ngit_pull.rs @@ -0,0 +1,615 @@ +use anyhow::Result; +use futures::join; +use serial_test::serial; +use test_utils::{git::GitTestRepo, relay::Relay, *}; + +mod when_main_is_checked_out { + use super::*; + + mod cli_prompts { + use super::*; + + #[tokio::test] + #[serial] + async fn cli_show_error() -> Result<()> { + let (mut r51, mut r52, mut r53, mut r55, mut r56) = ( + Relay::new(8051, None, None), + Relay::new(8052, None, None), + Relay::new(8053, None, None), + Relay::new(8055, None, None), + Relay::new(8056, None, None), + ); + + r51.events.push(generate_test_key_1_relay_list_event()); + r51.events.push(generate_test_key_1_metadata_event("fred")); + r51.events.push(generate_repo_ref_event()); + + r55.events.push(generate_repo_ref_event()); + r55.events.push(generate_test_key_1_metadata_event("fred")); + r55.events.push(generate_test_key_1_relay_list_event()); + + let cli_tester_handle = std::thread::spawn(move || -> Result<()> { + cli_tester_create_proposals()?; + + let test_repo = create_repo_with_proposal_branch_pulled_and_checkedout(1)?; + + test_repo.checkout("main")?; + + let mut p = CliTester::new_from_dir(&test_repo.dir, ["pull"]); + p.expect("Error: checkout a branch associated with a proposal first\r\n")?; + p.expect_end()?; + + for p in [51, 52, 53, 55, 56] { + relay::shutdown_relay(8000 + p)?; + } + Ok(()) + }); + + // launch relay + let _ = join!( + r51.listen_until_close(), + r52.listen_until_close(), + r53.listen_until_close(), + r55.listen_until_close(), + r56.listen_until_close(), + ); + cli_tester_handle.join().unwrap()?; + Ok(()) + } + } +} + +mod when_branch_doesnt_exist { + use super::*; + + mod cli_prompts { + use super::*; + + #[tokio::test] + #[serial] + async fn cli_show_error() -> Result<()> { + let (mut r51, mut r52, mut r53, mut r55, mut r56) = ( + Relay::new(8051, None, None), + Relay::new(8052, None, None), + Relay::new(8053, None, None), + Relay::new(8055, None, None), + Relay::new(8056, None, None), + ); + + r51.events.push(generate_test_key_1_relay_list_event()); + r51.events.push(generate_test_key_1_metadata_event("fred")); + r51.events.push(generate_repo_ref_event()); + + r55.events.push(generate_repo_ref_event()); + r55.events.push(generate_test_key_1_metadata_event("fred")); + r55.events.push(generate_test_key_1_relay_list_event()); + + let cli_tester_handle = std::thread::spawn(move || -> Result<()> { + cli_tester_create_proposals()?; + + let test_repo = GitTestRepo::default(); + test_repo.populate()?; + + test_repo.create_branch("random-name")?; + test_repo.checkout("random-name")?; + + let mut p = CliTester::new_from_dir(&test_repo.dir, ["pull"]); + p.expect("fetching updates...\r\n")?; + p.expect_eventually("\r\n")?; // some updates listed here + p.expect("Error: cannot find proposal that matches the current branch name\r\n")?; + + p.expect_end()?; + + for p in [51, 52, 53, 55, 56] { + relay::shutdown_relay(8000 + p)?; + } + Ok(()) + }); + + // launch relay + let _ = join!( + r51.listen_until_close(), + r52.listen_until_close(), + r53.listen_until_close(), + r55.listen_until_close(), + r56.listen_until_close(), + ); + cli_tester_handle.join().unwrap()?; + Ok(()) + } + } +} + +mod when_branch_is_checked_out { + use super::*; + + mod when_branch_is_up_to_date { + use super::*; + + mod cli_prompts { + use super::*; + #[tokio::test] + #[serial] + async fn cli_show_up_to_date() -> Result<()> { + let (mut r51, mut r52, mut r53, mut r55, mut r56) = ( + Relay::new(8051, None, None), + Relay::new(8052, None, None), + Relay::new(8053, None, None), + Relay::new(8055, None, None), + Relay::new(8056, None, None), + ); + + r51.events.push(generate_test_key_1_relay_list_event()); + r51.events.push(generate_test_key_1_metadata_event("fred")); + r51.events.push(generate_repo_ref_event()); + + r55.events.push(generate_repo_ref_event()); + r55.events.push(generate_test_key_1_metadata_event("fred")); + r55.events.push(generate_test_key_1_relay_list_event()); + + let cli_tester_handle = std::thread::spawn(move || -> Result<()> { + let (_, test_repo) = + create_proposals_and_repo_with_proposal_pulled_and_checkedout(1)?; + + let mut p = CliTester::new_from_dir(&test_repo.dir, ["pull"]); + p.expect("fetching updates...\r\n")?; + p.expect_eventually("\r\n")?; // some updates listed here + p.expect("branch already up-to-date\r\n")?; + p.expect_end()?; + + for p in [51, 52, 53, 55, 56] { + relay::shutdown_relay(8000 + p)?; + } + Ok(()) + }); + + // launch relay + let _ = join!( + r51.listen_until_close(), + r52.listen_until_close(), + r53.listen_until_close(), + r55.listen_until_close(), + r56.listen_until_close(), + ); + cli_tester_handle.join().unwrap()?; + Ok(()) + } + } + } + + mod when_branch_is_behind { + use super::*; + + async fn prep_and_run() -> Result<(GitTestRepo, GitTestRepo)> { + // fallback (51,52) user write (53, 55) repo (55, 56) + let (mut r51, mut r52, mut r53, mut r55, mut r56) = ( + Relay::new(8051, None, None), + Relay::new(8052, None, None), + Relay::new(8053, None, None), + Relay::new(8055, None, None), + Relay::new(8056, None, None), + ); + + r51.events.push(generate_test_key_1_relay_list_event()); + r51.events.push(generate_test_key_1_metadata_event("fred")); + r51.events.push(generate_repo_ref_event()); + + r55.events.push(generate_repo_ref_event()); + r55.events.push(generate_test_key_1_metadata_event("fred")); + r55.events.push(generate_test_key_1_relay_list_event()); + + let cli_tester_handle = + std::thread::spawn(move || -> Result<(GitTestRepo, GitTestRepo)> { + let (originating_repo, test_repo) = + create_proposals_and_repo_with_proposal_pulled_and_checkedout(1)?; + + let branch_name = + remove_latest_commit_so_proposal_branch_is_behind_and_checkout_main( + &test_repo, + )?; + test_repo.checkout(&branch_name)?; + + let mut p = CliTester::new_from_dir(&test_repo.dir, ["pull"]); + p.expect_end_eventually()?; + + for p in [51, 52, 53, 55, 56] { + relay::shutdown_relay(8000 + p)?; + } + Ok((originating_repo, test_repo)) + }); + + // launch relay + let _ = join!( + r51.listen_until_close(), + r52.listen_until_close(), + r53.listen_until_close(), + r55.listen_until_close(), + r56.listen_until_close(), + ); + let res = cli_tester_handle.join().unwrap()?; + + Ok(res) + } + + mod cli_prompts { + use super::*; + + #[tokio::test] + #[serial] + async fn cli_applied_1_commit() -> Result<()> { + // fallback (51,52) user write (53, 55) repo (55, 56) + let (mut r51, mut r52, mut r53, mut r55, mut r56) = ( + Relay::new(8051, None, None), + Relay::new(8052, None, None), + Relay::new(8053, None, None), + Relay::new(8055, None, None), + Relay::new(8056, None, None), + ); + + r51.events.push(generate_test_key_1_relay_list_event()); + r51.events.push(generate_test_key_1_metadata_event("fred")); + r51.events.push(generate_repo_ref_event()); + + r55.events.push(generate_repo_ref_event()); + r55.events.push(generate_test_key_1_metadata_event("fred")); + r55.events.push(generate_test_key_1_relay_list_event()); + + let cli_tester_handle = + std::thread::spawn(move || -> Result<(GitTestRepo, GitTestRepo)> { + let (originating_repo, test_repo) = + create_proposals_and_repo_with_proposal_pulled_and_checkedout(1)?; + + let branch_name = + remove_latest_commit_so_proposal_branch_is_behind_and_checkout_main( + &test_repo, + )?; + test_repo.checkout(&branch_name)?; + + let mut p = CliTester::new_from_dir(&test_repo.dir, ["pull"]); + p.expect("fetching updates...\r\n")?; + p.expect_eventually("\r\n")?; // some updates listed here + p.expect_end_with("applied 1 new commits\r\n")?; + + for p in [51, 52, 53, 55, 56] { + relay::shutdown_relay(8000 + p)?; + } + Ok((originating_repo, test_repo)) + }); + + // launch relay + let _ = join!( + r51.listen_until_close(), + r52.listen_until_close(), + r53.listen_until_close(), + r55.listen_until_close(), + r56.listen_until_close(), + ); + cli_tester_handle.join().unwrap()?; + + Ok(()) + } + } + + #[tokio::test] + #[serial] + async fn proposal_branch_tip_is_most_recent_patch() -> Result<()> { + let (originating_repo, test_repo) = prep_and_run().await?; + assert_eq!( + originating_repo.get_tip_of_local_branch(FEATURE_BRANCH_NAME_1)?, + test_repo.get_tip_of_local_branch(&get_proposal_branch_name( + &test_repo, + FEATURE_BRANCH_NAME_1 + )?)?, + ); + Ok(()) + } + } + + mod when_latest_proposal_amended_locally { + use super::*; + + mod cli_prompts { + use super::*; + + #[tokio::test] + #[serial] + async fn cli_output_correct() -> Result<()> { + let (mut r51, mut r52, mut r53, mut r55, mut r56) = ( + Relay::new(8051, None, None), + Relay::new(8052, None, None), + Relay::new(8053, None, None), + Relay::new(8055, None, None), + Relay::new(8056, None, None), + ); + + r51.events.push(generate_test_key_1_relay_list_event()); + r51.events.push(generate_test_key_1_metadata_event("fred")); + r51.events.push(generate_repo_ref_event()); + + r55.events.push(generate_repo_ref_event()); + r55.events.push(generate_test_key_1_metadata_event("fred")); + r55.events.push(generate_test_key_1_relay_list_event()); + + let cli_tester_handle = std::thread::spawn(move || -> Result<()> { + let (_, test_repo) = + create_proposals_and_repo_with_proposal_pulled_and_checkedout(1)?; + + amend_last_commit(&test_repo, "add ammended-commit.md")?; + + // run test + let mut p = CliTester::new_from_dir(&test_repo.dir, ["pull"]); + p.expect("fetching updates...\r\n")?; + p.expect_eventually("\r\n")?; // some updates listed here + p.expect( + "you have an amended/rebase version the proposal that is unpublished\r\n", + )?; + p.expect("you have previously applied the latest version of the proposal (2 ahead 0 behind 'main') but your local proposal branch has amended or rebased it (2 ahead 0 behind 'main')\r\n")?; + p.expect("to view the latest proposal but retain your changes:\r\n")?; + p.expect(" 1) create a new branch off the tip commit of this one to store your changes\r\n")?; + p.expect(" 2) run `ngit list` and checkout the latest published version of this proposal\r\n")?; + p.expect("if you are confident in your changes consider running `ngit push --force`\r\n")?; + p.expect_end()?; + + for p in [51, 52, 53, 55, 56] { + relay::shutdown_relay(8000 + p)?; + } + Ok(()) + }); + + // launch relay + let _ = join!( + r51.listen_until_close(), + r52.listen_until_close(), + r53.listen_until_close(), + r55.listen_until_close(), + r56.listen_until_close(), + ); + cli_tester_handle.join().unwrap()?; + println!("{:?}", r55.events); + Ok(()) + } + } + } + + mod when_local_commits_on_uptodate_proposal { + use super::*; + async fn prep_and_run() -> Result<(GitTestRepo, GitTestRepo)> { + // fallback (51,52) user write (53, 55) repo (55, 56) + let (mut r51, mut r52, mut r53, mut r55, mut r56) = ( + Relay::new(8051, None, None), + Relay::new(8052, None, None), + Relay::new(8053, None, None), + Relay::new(8055, None, None), + Relay::new(8056, None, None), + ); + + r51.events.push(generate_test_key_1_relay_list_event()); + r51.events.push(generate_test_key_1_metadata_event("fred")); + r51.events.push(generate_repo_ref_event()); + + r55.events.push(generate_repo_ref_event()); + r55.events.push(generate_test_key_1_metadata_event("fred")); + r55.events.push(generate_test_key_1_relay_list_event()); + + let cli_tester_handle = std::thread::spawn( + move || -> Result<(GitTestRepo, GitTestRepo)> { + let (originating_repo, test_repo) = + create_proposals_and_repo_with_proposal_pulled_and_checkedout(1)?; + + // add another commit (so we have a local branch 1 ahead) + std::fs::write(test_repo.dir.join("ammended-commit.md"), "some content")?; + test_repo.stage_and_commit("add ammended-commit.md")?; + + // run test + let mut p = CliTester::new_from_dir(&test_repo.dir, ["pull"]); + p.expect("fetching updates...\r\n")?; + p.expect_eventually("\r\n")?; // some updates listed here + p.expect("local proposal branch exists with 1 unpublished commits on top of the most up-to-date version of the proposal\r\n")?; + p.expect_end()?; + + for p in [51, 52, 53, 55, 56] { + relay::shutdown_relay(8000 + p)?; + } + Ok((originating_repo, test_repo)) + }, + ); + + // launch relay + let _ = join!( + r51.listen_until_close(), + r52.listen_until_close(), + r53.listen_until_close(), + r55.listen_until_close(), + r56.listen_until_close(), + ); + let res = cli_tester_handle.join().unwrap()?; + + Ok(res) + } + + mod cli_prompts { + use super::*; + + #[tokio::test] + #[serial] + async fn prompts_to_choose_from_proposal_titles() -> Result<()> { + let (mut r51, mut r52, mut r53, mut r55, mut r56) = ( + Relay::new(8051, None, None), + Relay::new(8052, None, None), + Relay::new(8053, None, None), + Relay::new(8055, None, None), + Relay::new(8056, None, None), + ); + + r51.events.push(generate_test_key_1_relay_list_event()); + r51.events.push(generate_test_key_1_metadata_event("fred")); + r51.events.push(generate_repo_ref_event()); + + r55.events.push(generate_repo_ref_event()); + r55.events.push(generate_test_key_1_metadata_event("fred")); + r55.events.push(generate_test_key_1_relay_list_event()); + + let cli_tester_handle = std::thread::spawn(move || -> Result<()> { + let (_, test_repo) = + create_proposals_and_repo_with_proposal_pulled_and_checkedout(1)?; + + // add another commit (so we have a local branch 1 ahead) + std::fs::write(test_repo.dir.join("ammended-commit.md"), "some content")?; + test_repo.stage_and_commit("add ammended-commit.md")?; + + // run test + let mut p = CliTester::new_from_dir(&test_repo.dir, ["pull"]); + p.expect("fetching updates...\r\n")?; + p.expect_eventually("\r\n")?; // some updates listed here + p.expect("local proposal branch exists with 1 unpublished commits on top of the most up-to-date version of the proposal\r\n")?; + p.expect_end()?; + + for p in [51, 52, 53, 55, 56] { + relay::shutdown_relay(8000 + p)?; + } + Ok(()) + }); + + // launch relay + let _ = join!( + r51.listen_until_close(), + r52.listen_until_close(), + r53.listen_until_close(), + r55.listen_until_close(), + r56.listen_until_close(), + ); + cli_tester_handle.join().unwrap()?; + println!("{:?}", r55.events); + Ok(()) + } + } + + #[tokio::test] + #[serial] + async fn didnt_overwrite_local_appendments() -> Result<()> { + let (originating_repo, test_repo) = prep_and_run().await?; + assert_ne!( + test_repo.get_tip_of_local_branch(&get_proposal_branch_name( + &test_repo, + FEATURE_BRANCH_NAME_1 + )?)?, + originating_repo.get_tip_of_local_branch(FEATURE_BRANCH_NAME_1)?, + ); + Ok(()) + } + } + mod when_latest_event_rebases_branch { + use tokio::task::JoinHandle; + + use super::*; + + async fn prep_and_run() -> Result<(GitTestRepo, GitTestRepo)> { + // fallback (51,52) user write (53, 55) repo (55, 56) + let (mut r51, mut r52, mut r53, mut r55, mut r56) = ( + Relay::new(8051, None, None), + Relay::new(8052, None, None), + Relay::new(8053, None, None), + Relay::new(8055, None, None), + Relay::new(8056, None, None), + ); + + r51.events.push(generate_test_key_1_relay_list_event()); + r51.events.push(generate_test_key_1_metadata_event("fred")); + r51.events.push(generate_repo_ref_event()); + + r55.events.push(generate_repo_ref_event()); + r55.events.push(generate_test_key_1_metadata_event("fred")); + r55.events.push(generate_test_key_1_relay_list_event()); + + let cli_tester_handle: JoinHandle> = + tokio::task::spawn_blocking(move || { + let (originating_repo, test_repo) = create_proposals_with_first_rebased_and_repo_with_latest_main_and_unrebased_proposal()?; + + let mut p = CliTester::new_from_dir(&test_repo.dir, ["pull"]); + p.expect_end_eventually_and_print()?; + + for p in [51, 52, 53, 55, 56] { + relay::shutdown_relay(8000 + p)?; + } + Ok((originating_repo, test_repo)) + }); + + // launch relay + let _ = join!( + r51.listen_until_close(), + r52.listen_until_close(), + r53.listen_until_close(), + r55.listen_until_close(), + r56.listen_until_close(), + ); + let res = cli_tester_handle.await??; + + Ok(res) + } + + mod cli_prompts { + use super::*; + + #[tokio::test] + #[serial] + async fn prompts_to_choose_from_proposal_titles() -> Result<()> { + let (mut r51, mut r52, mut r53, mut r55, mut r56) = ( + Relay::new(8051, None, None), + Relay::new(8052, None, None), + Relay::new(8053, None, None), + Relay::new(8055, None, None), + Relay::new(8056, None, None), + ); + + r51.events.push(generate_test_key_1_relay_list_event()); + r51.events.push(generate_test_key_1_metadata_event("fred")); + r51.events.push(generate_repo_ref_event()); + + r55.events.push(generate_repo_ref_event()); + r55.events.push(generate_test_key_1_metadata_event("fred")); + r55.events.push(generate_test_key_1_relay_list_event()); + + let cli_tester_handle: JoinHandle> = tokio::task::spawn_blocking( + move || { + let (_, test_repo) = create_proposals_with_first_rebased_and_repo_with_latest_main_and_unrebased_proposal()?; + + let mut p = CliTester::new_from_dir(&test_repo.dir, ["pull"]); + p.expect("fetching updates...\r\n")?; + p.expect_eventually("\r\n")?; // some updates listed here + p.expect_end_with("pulled new version of proposal (2 ahead 0 behind 'main'), replacing old version (2 ahead 1 behind 'main')\r\n")?; + for p in [51, 52, 53, 55, 56] { + relay::shutdown_relay(8000 + p)?; + } + Ok(()) + }, + ); + + // launch relay + let _ = join!( + r51.listen_until_close(), + r52.listen_until_close(), + r53.listen_until_close(), + r55.listen_until_close(), + r56.listen_until_close(), + ); + cli_tester_handle.await??; + println!("{:?}", r55.events); + Ok(()) + } + } + + #[tokio::test] + #[serial] + async fn proposal_branch_tip_is_most_recent_proposal_revision_tip() -> Result<()> { + let (originating_repo, test_repo) = prep_and_run().await?; + assert_eq!( + originating_repo.get_tip_of_local_branch(FEATURE_BRANCH_NAME_1)?, + test_repo.get_tip_of_local_branch(&get_proposal_branch_name( + &test_repo, + FEATURE_BRANCH_NAME_1 + )?)?, + ); + Ok(()) + } + } +} diff --git a/tests/ngit_push.rs b/tests/ngit_push.rs new file mode 100644 index 0000000..3d89f6b --- /dev/null +++ b/tests/ngit_push.rs @@ -0,0 +1,531 @@ +use anyhow::Result; +use futures::join; +use serial_test::serial; +use test_utils::{git::GitTestRepo, relay::Relay, *}; + +mod when_main_is_checked_out { + use super::*; + + #[test] + fn cli_returns_error() -> Result<()> { + let test_repo = GitTestRepo::default(); + test_repo.populate()?; + create_and_populate_branch(&test_repo, FEATURE_BRANCH_NAME_1, "a", false)?; + test_repo.checkout("main")?; + let mut p = CliTester::new_from_dir(&test_repo.dir, ["push"]); + p.expect("Error: checkout a branch associated with a proposal first\r\n")?; + p.expect_end()?; + Ok(()) + } +} + +mod when_proposal_isnt_associated_with_branch_name { + use super::*; + + mod cli_prompts { + + use super::*; + + #[tokio::test] + #[serial] + async fn cli_show_error() -> Result<()> { + let (mut r51, mut r52, mut r53, mut r55, mut r56) = ( + Relay::new(8051, None, None), + Relay::new(8052, None, None), + Relay::new(8053, None, None), + Relay::new(8055, None, None), + Relay::new(8056, None, None), + ); + + r51.events.push(generate_test_key_1_relay_list_event()); + r51.events.push(generate_test_key_1_metadata_event("fred")); + r51.events.push(generate_repo_ref_event()); + + r55.events.push(generate_repo_ref_event()); + r55.events.push(generate_test_key_1_metadata_event("fred")); + r55.events.push(generate_test_key_1_relay_list_event()); + + let cli_tester_handle = std::thread::spawn(move || -> Result<()> { + cli_tester_create_proposals()?; + + let test_repo = GitTestRepo::default(); + test_repo.populate()?; + + test_repo.create_branch("random-name")?; + test_repo.checkout("random-name")?; + + let mut p = CliTester::new_from_dir(&test_repo.dir, ["push"]); + p.expect("fetching updates...\r\n")?; + p.expect_eventually("\r\n")?; // some updates listed here + p.expect_end_with( + "Error: cannot find proposal that matches the current branch name\r\n", + )?; + for p in [51, 52, 53, 55, 56] { + relay::shutdown_relay(8000 + p)?; + } + Ok(()) + }); + + // launch relay + let _ = join!( + r51.listen_until_close(), + r52.listen_until_close(), + r53.listen_until_close(), + r55.listen_until_close(), + r56.listen_until_close(), + ); + cli_tester_handle.join().unwrap()?; + Ok(()) + } + } +} + +mod when_branch_is_checked_out { + use super::*; + + mod when_branch_is_up_to_date { + use super::*; + + mod cli_prompts { + use super::*; + #[tokio::test] + #[serial] + async fn cli_show_up_to_date() -> Result<()> { + let (mut r51, mut r52, mut r53, mut r55, mut r56) = ( + Relay::new(8051, None, None), + Relay::new(8052, None, None), + Relay::new(8053, None, None), + Relay::new(8055, None, None), + Relay::new(8056, None, None), + ); + + r51.events.push(generate_test_key_1_relay_list_event()); + r51.events.push(generate_test_key_1_metadata_event("fred")); + r51.events.push(generate_repo_ref_event()); + + r55.events.push(generate_repo_ref_event()); + r55.events.push(generate_test_key_1_metadata_event("fred")); + r55.events.push(generate_test_key_1_relay_list_event()); + + let cli_tester_handle = std::thread::spawn(move || -> Result<()> { + let (_, test_repo) = + create_proposals_and_repo_with_proposal_pulled_and_checkedout(1)?; + + let mut p = CliTester::new_from_dir(&test_repo.dir, ["push"]); + p.expect("fetching updates...\r\n")?; + p.expect_eventually("\r\n")?; // some updates listed here + p.expect_end_with("Error: proposal already up-to-date with local branch\r\n")?; + + for p in [51, 52, 53, 55, 56] { + relay::shutdown_relay(8000 + p)?; + } + Ok(()) + }); + + // launch relay + let _ = join!( + r51.listen_until_close(), + r52.listen_until_close(), + r53.listen_until_close(), + r55.listen_until_close(), + r56.listen_until_close(), + ); + cli_tester_handle.join().unwrap()?; + Ok(()) + } + } + } + + mod when_branch_is_behind { + use super::*; + + mod cli_prompts { + use super::*; + + #[tokio::test] + #[serial] + async fn cli_show_proposal_ahead_error() -> Result<()> { + let (mut r51, mut r52, mut r53, mut r55, mut r56) = ( + Relay::new(8051, None, None), + Relay::new(8052, None, None), + Relay::new(8053, None, None), + Relay::new(8055, None, None), + Relay::new(8056, None, None), + ); + + r51.events.push(generate_test_key_1_relay_list_event()); + r51.events.push(generate_test_key_1_metadata_event("fred")); + r51.events.push(generate_repo_ref_event()); + + r55.events.push(generate_repo_ref_event()); + r55.events.push(generate_test_key_1_metadata_event("fred")); + r55.events.push(generate_test_key_1_relay_list_event()); + + let cli_tester_handle = std::thread::spawn(move || -> Result<()> { + let (_, test_repo) = + create_proposals_and_repo_with_proposal_pulled_and_checkedout(1)?; + + let branch_name = + remove_latest_commit_so_proposal_branch_is_behind_and_checkout_main( + &test_repo, + )?; + + test_repo.checkout(&branch_name)?; + // run test + let mut p = CliTester::new_from_dir(&test_repo.dir, ["push"]); + p.expect("fetching updates...\r\n")?; + p.expect_eventually("\r\n")?; // some updates listed here + p.expect("Error: proposal is ahead of local branch\r\n")?; + p.expect_end()?; + + for p in [51, 52, 53, 55, 56] { + relay::shutdown_relay(8000 + p)?; + } + Ok(()) + }); + + // launch relay + let _ = join!( + r51.listen_until_close(), + r52.listen_until_close(), + r53.listen_until_close(), + r55.listen_until_close(), + r56.listen_until_close(), + ); + cli_tester_handle.join().unwrap()?; + Ok(()) + } + } + } + + mod when_branch_is_ahead { + use super::*; + + mod cli_prompts { + use test_utils::relay::expect_send_with_progress; + + use super::*; + + #[tokio::test] + #[serial] + async fn cli_applied_1_commit() -> Result<()> { + // fallback (51,52) user write (53, 55) repo (55, 56) + let (mut r51, mut r52, mut r53, mut r55, mut r56) = ( + Relay::new(8051, None, None), + Relay::new(8052, None, None), + Relay::new(8053, None, None), + Relay::new(8055, None, None), + Relay::new(8056, None, None), + ); + + r51.events.push(generate_test_key_1_relay_list_event()); + r51.events.push(generate_test_key_1_metadata_event("fred")); + r51.events.push(generate_repo_ref_event()); + + r55.events.push(generate_repo_ref_event()); + r55.events.push(generate_test_key_1_metadata_event("fred")); + r55.events.push(generate_test_key_1_relay_list_event()); + + let cli_tester_handle = + std::thread::spawn(move || -> Result<(GitTestRepo, GitTestRepo)> { + let (originating_repo, test_repo) = + create_proposals_and_repo_with_proposal_pulled_and_checkedout(1)?; + + // add another commit (so we have an ammened local branch) + std::fs::write(test_repo.dir.join("ammended-commit.md"), "some content")?; + test_repo.stage_and_commit("add ammended-commit.md")?; + + // run test + let mut p = CliTester::new_from_dir( + &test_repo.dir, + [ + "--nsec", + TEST_KEY_1_NSEC, + "--password", + TEST_PASSWORD, + "--disable-cli-spinners", + "push", + ], + ); + p.expect("fetching updates...\r\n")?; + p.expect_eventually("\r\n")?; // some updates listed here + p.expect( + "1 commits ahead. preparing to create creating patch events.\r\n", + )?; + p.expect("logged in as fred\r\n")?; + p.expect("pushing 1 commits\r\n")?; + + expect_send_with_progress( + &mut p, + vec![ + (" [my-relay] [repo-relay] ws://localhost:8055", true, ""), + (" [my-relay] ws://localhost:8053", true, ""), + (" [repo-relay] ws://localhost:8056", true, ""), + (" [default] ws://localhost:8051", true, ""), + (" [default] ws://localhost:8052", true, ""), + ], + 1, + )?; + p.expect_eventually("pushed 1 commits\r\n")?; + p.expect_end()?; + + for p in [51, 52, 53, 55, 56] { + relay::shutdown_relay(8000 + p)?; + } + Ok((originating_repo, test_repo)) + }); + + // launch relay + let _ = join!( + r51.listen_until_close(), + r52.listen_until_close(), + r53.listen_until_close(), + r55.listen_until_close(), + r56.listen_until_close(), + ); + cli_tester_handle.join().unwrap()?; + + Ok(()) + } + } + + async fn prep_and_run() -> Result<(GitTestRepo, Vec)> { + // fallback (51,52) user write (53, 55) repo (55, 56) + let (mut r51, mut r52, mut r53, mut r55, mut r56) = ( + Relay::new(8051, None, None), + Relay::new(8052, None, None), + Relay::new(8053, None, None), + Relay::new(8055, None, None), + Relay::new(8056, None, None), + ); + + r51.events.push(generate_test_key_1_relay_list_event()); + r51.events.push(generate_test_key_1_metadata_event("fred")); + r51.events.push(generate_repo_ref_event()); + + r55.events.push(generate_repo_ref_event()); + r55.events.push(generate_test_key_1_metadata_event("fred")); + r55.events.push(generate_test_key_1_relay_list_event()); + + let cli_tester_handle = std::thread::spawn(move || -> Result { + let (_, test_repo) = + create_proposals_and_repo_with_proposal_pulled_and_checkedout(1)?; + + // add another commit (so we have an ammened local branch) + std::fs::write(test_repo.dir.join("ammended-commit.md"), "some content")?; + test_repo.stage_and_commit("add ammended-commit.md")?; + + // run test + + let mut p = CliTester::new_from_dir( + &test_repo.dir, + [ + "--nsec", + TEST_KEY_1_NSEC, + "--password", + TEST_PASSWORD, + "--disable-cli-spinners", + "push", + ], + ); + p.expect_end_eventually()?; + + for p in [51, 52, 53, 55, 56] { + relay::shutdown_relay(8000 + p)?; + } + Ok(test_repo) + }); + + // launch relay + let _ = join!( + r51.listen_until_close(), + r52.listen_until_close(), + r53.listen_until_close(), + r55.listen_until_close(), + r56.listen_until_close(), + ); + let res = cli_tester_handle.join().unwrap()?; + + Ok((res, r55.events.clone())) + } + #[tokio::test] + #[serial] + async fn commits_issued_as_patch_event() -> Result<()> { + let (test_repo, r55_events) = prep_and_run().await?; + + let commit_id = test_repo + .get_tip_of_local_branch(&test_repo.get_checked_out_branch_name()?)? + .to_string(); + assert!(r55_events.iter().any(|e| { + e.tags + .iter() + .any(|t| t.as_slice()[0].eq("commit") && t.as_slice()[1].eq(&commit_id)) + })); + Ok(()) + } + } + + mod when_branch_has_been_rebased { + use super::*; + + mod cli_prompts { + use super::*; + + #[tokio::test] + #[serial] + async fn cli_shows_unpublished_rebase_error() -> Result<()> { + let (mut r51, mut r52, mut r53, mut r55, mut r56) = ( + Relay::new(8051, None, None), + Relay::new(8052, None, None), + Relay::new(8053, None, None), + Relay::new(8055, None, None), + Relay::new(8056, None, None), + ); + + r51.events.push(generate_test_key_1_relay_list_event()); + r51.events.push(generate_test_key_1_metadata_event("fred")); + r51.events.push(generate_repo_ref_event()); + + r55.events.push(generate_repo_ref_event()); + r55.events.push(generate_test_key_1_metadata_event("fred")); + r55.events.push(generate_test_key_1_relay_list_event()); + + let cli_tester_handle = std::thread::spawn(move || -> Result<()> { + let (_, tmp_repo) = + create_proposals_and_repo_with_proposal_pulled_and_checkedout(1)?; + let branch_name = tmp_repo.get_checked_out_branch_name()?; + + let test_repo = GitTestRepo::default(); + test_repo.populate()?; + + // simulate rebase + std::fs::write(test_repo.dir.join("amazing.md"), "some content")?; + test_repo.stage_and_commit("commit for rebasing on top of")?; + create_and_populate_branch(&test_repo, &branch_name, "a", true)?; + + let mut p = CliTester::new_from_dir(&test_repo.dir, ["push"]); + // p.expect_end_eventually_and_print()?; + + p.expect("fetching updates...\r\n")?; + p.expect_eventually("\r\n")?; // some updates listed here + p.expect( + "Error: local unpublished proposal has been rebased. consider force pushing\r\n", + )?; + p.expect_end()?; + + for p in [51, 52, 53, 55, 56] { + relay::shutdown_relay(8000 + p)?; + } + Ok(()) + }); + + // launch relay + let _ = join!( + r51.listen_until_close(), + r52.listen_until_close(), + r53.listen_until_close(), + r55.listen_until_close(), + r56.listen_until_close(), + ); + cli_tester_handle.join().unwrap()?; + Ok(()) + } + } + mod with_force_flag { + use super::*; + + mod cli_prompts { + use super::*; + + #[tokio::test] + #[serial] + async fn cli_shows_revision_sent() -> Result<()> { + let (mut r51, mut r52, mut r53, mut r55, mut r56) = ( + Relay::new(8051, None, None), + Relay::new(8052, None, None), + Relay::new(8053, None, None), + Relay::new(8055, None, None), + Relay::new(8056, None, None), + ); + + r51.events.push(generate_test_key_1_relay_list_event()); + r51.events.push(generate_test_key_1_metadata_event("fred")); + r51.events.push(generate_repo_ref_event()); + + r55.events.push(generate_repo_ref_event()); + r55.events.push(generate_test_key_1_metadata_event("fred")); + r55.events.push(generate_test_key_1_relay_list_event()); + + let cli_tester_handle = std::thread::spawn(move || -> Result<()> { + let (_, tmp_repo) = + create_proposals_and_repo_with_proposal_pulled_and_checkedout(1)?; + let branch_name = tmp_repo.get_checked_out_branch_name()?; + + let test_repo = GitTestRepo::default(); + test_repo.populate()?; + + // simulate rebase + std::fs::write(test_repo.dir.join("amazing.md"), "some content")?; + test_repo.stage_and_commit("commit for rebasing on top of")?; + create_and_populate_branch(&test_repo, &branch_name, "a", false)?; + let mut p = CliTester::new_from_dir( + &test_repo.dir, + [ + "--nsec", + TEST_KEY_1_NSEC, + "--password", + TEST_PASSWORD, + "--disable-cli-spinners", + "push", + "--force", + ], + ); + p.expect("fetching updates...\r\n")?; + p.expect_eventually("\r\n")?; // some updates listed here + p.expect("preparing to force push proposal revision...\r\n")?; + // standard output from `ngit send` + p.expect("creating proposal revision for: ")?; + // proposal id will be printed in this gap + p.expect_eventually("\r\n")?; + p.expect("creating proposal from 2 commits:\r\n")?; + p.expect("355bdf1 add a4.md\r\n")?; + p.expect("dbd1115 add a3.md\r\n")?; + p.expect("logged in as fred\r\n")?; + p.expect("posting 2 patches without a covering letter...\r\n")?; + + relay::expect_send_with_progress( + &mut p, + vec![ + (" [my-relay] [repo-relay] ws://localhost:8055", true, ""), + (" [my-relay] ws://localhost:8053", true, ""), + (" [repo-relay] ws://localhost:8056", true, ""), + (" [default] ws://localhost:8051", true, ""), + (" [default] ws://localhost:8052", true, ""), + ], + 2, + )?; + // end standard `ngit send output` + p.expect_after_whitespace("force pushed proposal revision\r\n")?; + p.expect_end()?; + + for p in [51, 52, 53, 55, 56] { + relay::shutdown_relay(8000 + p)?; + } + Ok(()) + }); + + // launch relay + let _ = join!( + r51.listen_until_close(), + r52.listen_until_close(), + r53.listen_until_close(), + r55.listen_until_close(), + r56.listen_until_close(), + ); + cli_tester_handle.join().unwrap()?; + Ok(()) + } + } + } + } +} -- cgit v1.2.3