From 125ead4bb64d9e4a76266aabe5e826fc23551edc Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Wed, 21 Feb 2024 11:07:38 +0000 Subject: feat(send): specify commits eg HEAD~2 specifiy commits or commit ranges in the same way that `git format-patch` allows --- src/git.rs | 139 +++++++++++++++++++++++++++++++++++++++++++++++ src/sub_commands/send.rs | 65 +++++++++++++--------- 2 files changed, 179 insertions(+), 25 deletions(-) (limited to 'src') diff --git a/src/git.rs b/src/git.rs index 63bce20..d0eaf03 100644 --- a/src/git.rs +++ b/src/git.rs @@ -67,6 +67,7 @@ pub trait RepoActions { branch_name: &str, patch_and_ancestors: Vec, ) -> Result>; + fn parse_starting_commits(&self, starting_commits: &str) -> Result>; } impl RepoActions for Repo { @@ -407,6 +408,49 @@ impl RepoActions for Repo { } Ok(patches_to_apply) } + + fn parse_starting_commits(&self, starting_commits: &str) -> Result> { + let revspec = self + .git_repo + .revparse(starting_commits) + .context("specified value not in a valid format")?; + if revspec.mode().is_no_single() { + let (ahead, _) = self + .get_commits_ahead_behind( + &oid_to_sha1( + &revspec + .from() + .context("cannot get starting commit from specified value")? + .id(), + ), + &self + .get_head_commit() + .context("cannot get head commit with gitlib2")?, + ) + .context("specified commit is not an ancestor of current head")?; + Ok(ahead) + } else if revspec.mode().is_range() { + let (ahead, _) = self + .get_commits_ahead_behind( + &oid_to_sha1( + &revspec + .from() + .context("cannot get starting commit of range from specified value")? + .id(), + ), + &oid_to_sha1( + &revspec + .to() + .context("cannot get end of range commit from specified value")? + .id(), + ), + ) + .context("specified commit is not an ancestor of current head")?; + Ok(ahead) + } else { + bail!("specified value not in a supported format") + } + } } fn oid_to_u8_20_bytes(oid: &Oid) -> [u8; 20] { @@ -1753,4 +1797,99 @@ mod tests { } } } + mod parse_starting_commits { + use super::*; + + mod head_1_returns_latest_commit { + use super::*; + + #[test] + fn when_on_main_and_other_commits_are_more_recent_on_feature_branch() -> Result<()> { + let test_repo = GitTestRepo::default(); + let git_repo = Repo::from_path(&test_repo.dir)?; + test_repo.populate_with_test_branch()?; + test_repo.checkout("main")?; + + assert_eq!( + git_repo.parse_starting_commits("HEAD~1")?, + vec![str_to_sha1("431b84edc0d2fa118d63faa3c2db9c73d630a5ae")?], + ); + Ok(()) + } + + #[test] + fn when_checked_out_branch_ahead_of_main() -> Result<()> { + let test_repo = GitTestRepo::default(); + let git_repo = Repo::from_path(&test_repo.dir)?; + test_repo.populate_with_test_branch()?; + + assert_eq!( + git_repo.parse_starting_commits("HEAD~1")?, + vec![str_to_sha1("82ff2bcc9aa94d1bd8faee723d4c8cc190d6061c")?], + ); + Ok(()) + } + } + mod head_2_returns_latest_2_commits_youngest_first { + use super::*; + + #[test] + fn when_on_main_and_other_commits_are_more_recent_on_feature_branch() -> Result<()> { + let test_repo = GitTestRepo::default(); + let git_repo = Repo::from_path(&test_repo.dir)?; + test_repo.populate_with_test_branch()?; + test_repo.checkout("main")?; + + assert_eq!( + git_repo.parse_starting_commits("HEAD~2")?, + vec![ + str_to_sha1("431b84edc0d2fa118d63faa3c2db9c73d630a5ae")?, + str_to_sha1("af474d8d271490e5c635aad337abdc050034b16a")?, + ], + ); + Ok(()) + } + } + mod head_3_returns_latest_3_commits_youngest_first { + use super::*; + + #[test] + fn when_checked_out_branch_ahead_of_main() -> Result<()> { + let test_repo = GitTestRepo::default(); + let git_repo = Repo::from_path(&test_repo.dir)?; + test_repo.populate_with_test_branch()?; + + assert_eq!( + git_repo.parse_starting_commits("HEAD~3")?, + vec![ + str_to_sha1("82ff2bcc9aa94d1bd8faee723d4c8cc190d6061c")?, + str_to_sha1("a23e6b05aaeb7d1471b4a838b51f337d5644eeb0")?, + str_to_sha1("7ab82116068982671a8111f27dc10599172334b2")?, + ], + ); + Ok(()) + } + } + mod range_of_3_commits_not_in_branch_history_returns_3_commits_youngest_first { + use super::*; + + #[test] + fn when_checked_out_branch_ahead_of_main() -> Result<()> { + let test_repo = GitTestRepo::default(); + let git_repo = Repo::from_path(&test_repo.dir)?; + test_repo.populate_with_test_branch()?; + test_repo.checkout("main")?; + + assert_eq!( + git_repo.parse_starting_commits("af474d8..a23e6b0")?, + vec![ + str_to_sha1("a23e6b05aaeb7d1471b4a838b51f337d5644eeb0")?, + str_to_sha1("7ab82116068982671a8111f27dc10599172334b2")?, + str_to_sha1("431b84edc0d2fa118d63faa3c2db9c73d630a5ae")?, + ], + ); + Ok(()) + } + } + } } diff --git a/src/sub_commands/send.rs b/src/sub_commands/send.rs index 004d263..105f87a 100644 --- a/src/sub_commands/send.rs +++ b/src/sub_commands/send.rs @@ -21,8 +21,12 @@ use crate::{ #[derive(Debug, clap::Args)] pub struct SubCommandArgs { - #[clap(short, long)] + #[arg(default_value = "")] + /// starting commit (commits since in current branch) or commit range, like + /// in `git format-patch` + starting_commit: String, /// optional cover letter title + #[clap(short, long)] title: Option, #[clap(short, long)] /// optional cover letter description @@ -42,22 +46,24 @@ pub struct SubCommandArgs { pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { let git_repo = Repo::discover().context("cannot find a git repository")?; - let (from_branch, to_branch, mut ahead, behind) = - identify_ahead_behind(&git_repo, &args.from_branch, &args.to_branch)?; + let mut commits: Vec = { + if args.starting_commit.is_empty() { + let (from_branch, to_branch, ahead, behind) = + identify_ahead_behind(&git_repo, &args.from_branch, &args.to_branch)?; - if ahead.is_empty() { - bail!(format!( - "'{from_branch}' is 0 commits ahead of '{to_branch}' so no patches were created" - )); - } + if ahead.is_empty() { + bail!(format!( + "'{from_branch}' is 0 commits ahead of '{to_branch}' so no patches were created" + )); + } - if behind.is_empty() { - println!( - "creating patch for {} commits from '{from_branch}' that can be merged into '{to_branch}'", - ahead.len(), - ); - } else { - if !Interactor::default().confirm( + if behind.is_empty() { + println!( + "creating patch for {} commits from '{from_branch}' that can be merged into '{to_branch}'", + ahead.len(), + ); + } else { + if !Interactor::default().confirm( PromptConfirmParms::default() .with_prompt( format!( @@ -70,14 +76,23 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { ).context("failed to get confirmation response from interactor confirm")? { bail!("aborting so branch can be rebased"); } - println!( - "creating patch for {} commit{} from '{from_branch}' that {} {} behind '{to_branch}'", - ahead.len(), - if ahead.len() > 1 { "s" } else { "" }, - if ahead.len() > 1 { "are" } else { "is" }, - behind.len(), - ); - } + println!( + "creating patch for {} commit{} from '{from_branch}' that {} {} behind '{to_branch}'", + ahead.len(), + if ahead.len() > 1 { "s" } else { "" }, + if ahead.len() > 1 { "are" } else { "is" }, + behind.len(), + ); + } + ahead + } else { + let ahead = git_repo + .parse_starting_commits(&args.starting_commit) + .context("cannot parse specified starting commit or range")?; + println!("creating patch for {} commits", ahead.len(),); + ahead + } + }; let title = if args.no_cover_letter { None @@ -138,12 +153,12 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { .await?; // oldest first - ahead.reverse(); + commits.reverse(); let events = generate_cover_letter_and_patch_events( cover_letter_title_description.clone(), &git_repo, - &ahead, + &commits, &keys, &repo_ref, )?; -- cgit v1.2.3