upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2024-02-21 11:07:38 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2024-02-21 11:07:38 +0000
commit125ead4bb64d9e4a76266aabe5e826fc23551edc (patch)
treead6807af12e11f9fba9d2e04e15e1c56bb5f64b3 /src
parent141ebf0cc0c6cfea640debc9b9073303509a8bc7 (diff)
feat(send): specify commits eg HEAD~2
specifiy commits or commit ranges in the same way that `git format-patch` allows
Diffstat (limited to 'src')
-rw-r--r--src/git.rs139
-rw-r--r--src/sub_commands/send.rs65
2 files changed, 179 insertions, 25 deletions
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 {
67 branch_name: &str, 67 branch_name: &str,
68 patch_and_ancestors: Vec<nostr::Event>, 68 patch_and_ancestors: Vec<nostr::Event>,
69 ) -> Result<Vec<nostr::Event>>; 69 ) -> Result<Vec<nostr::Event>>;
70 fn parse_starting_commits(&self, starting_commits: &str) -> Result<Vec<Sha1Hash>>;
70} 71}
71 72
72impl RepoActions for Repo { 73impl RepoActions for Repo {
@@ -407,6 +408,49 @@ impl RepoActions for Repo {
407 } 408 }
408 Ok(patches_to_apply) 409 Ok(patches_to_apply)
409 } 410 }
411
412 fn parse_starting_commits(&self, starting_commits: &str) -> Result<Vec<Sha1Hash>> {
413 let revspec = self
414 .git_repo
415 .revparse(starting_commits)
416 .context("specified value not in a valid format")?;
417 if revspec.mode().is_no_single() {
418 let (ahead, _) = self
419 .get_commits_ahead_behind(
420 &oid_to_sha1(
421 &revspec
422 .from()
423 .context("cannot get starting commit from specified value")?
424 .id(),
425 ),
426 &self
427 .get_head_commit()
428 .context("cannot get head commit with gitlib2")?,
429 )
430 .context("specified commit is not an ancestor of current head")?;
431 Ok(ahead)
432 } else if revspec.mode().is_range() {
433 let (ahead, _) = self
434 .get_commits_ahead_behind(
435 &oid_to_sha1(
436 &revspec
437 .from()
438 .context("cannot get starting commit of range from specified value")?
439 .id(),
440 ),
441 &oid_to_sha1(
442 &revspec
443 .to()
444 .context("cannot get end of range commit from specified value")?
445 .id(),
446 ),
447 )
448 .context("specified commit is not an ancestor of current head")?;
449 Ok(ahead)
450 } else {
451 bail!("specified value not in a supported format")
452 }
453 }
410} 454}
411 455
412fn oid_to_u8_20_bytes(oid: &Oid) -> [u8; 20] { 456fn oid_to_u8_20_bytes(oid: &Oid) -> [u8; 20] {
@@ -1753,4 +1797,99 @@ mod tests {
1753 } 1797 }
1754 } 1798 }
1755 } 1799 }
1800 mod parse_starting_commits {
1801 use super::*;
1802
1803 mod head_1_returns_latest_commit {
1804 use super::*;
1805
1806 #[test]
1807 fn when_on_main_and_other_commits_are_more_recent_on_feature_branch() -> Result<()> {
1808 let test_repo = GitTestRepo::default();
1809 let git_repo = Repo::from_path(&test_repo.dir)?;
1810 test_repo.populate_with_test_branch()?;
1811 test_repo.checkout("main")?;
1812
1813 assert_eq!(
1814 git_repo.parse_starting_commits("HEAD~1")?,
1815 vec![str_to_sha1("431b84edc0d2fa118d63faa3c2db9c73d630a5ae")?],
1816 );
1817 Ok(())
1818 }
1819
1820 #[test]
1821 fn when_checked_out_branch_ahead_of_main() -> Result<()> {
1822 let test_repo = GitTestRepo::default();
1823 let git_repo = Repo::from_path(&test_repo.dir)?;
1824 test_repo.populate_with_test_branch()?;
1825
1826 assert_eq!(
1827 git_repo.parse_starting_commits("HEAD~1")?,
1828 vec![str_to_sha1("82ff2bcc9aa94d1bd8faee723d4c8cc190d6061c")?],
1829 );
1830 Ok(())
1831 }
1832 }
1833 mod head_2_returns_latest_2_commits_youngest_first {
1834 use super::*;
1835
1836 #[test]
1837 fn when_on_main_and_other_commits_are_more_recent_on_feature_branch() -> Result<()> {
1838 let test_repo = GitTestRepo::default();
1839 let git_repo = Repo::from_path(&test_repo.dir)?;
1840 test_repo.populate_with_test_branch()?;
1841 test_repo.checkout("main")?;
1842
1843 assert_eq!(
1844 git_repo.parse_starting_commits("HEAD~2")?,
1845 vec![
1846 str_to_sha1("431b84edc0d2fa118d63faa3c2db9c73d630a5ae")?,
1847 str_to_sha1("af474d8d271490e5c635aad337abdc050034b16a")?,
1848 ],
1849 );
1850 Ok(())
1851 }
1852 }
1853 mod head_3_returns_latest_3_commits_youngest_first {
1854 use super::*;
1855
1856 #[test]
1857 fn when_checked_out_branch_ahead_of_main() -> Result<()> {
1858 let test_repo = GitTestRepo::default();
1859 let git_repo = Repo::from_path(&test_repo.dir)?;
1860 test_repo.populate_with_test_branch()?;
1861
1862 assert_eq!(
1863 git_repo.parse_starting_commits("HEAD~3")?,
1864 vec![
1865 str_to_sha1("82ff2bcc9aa94d1bd8faee723d4c8cc190d6061c")?,
1866 str_to_sha1("a23e6b05aaeb7d1471b4a838b51f337d5644eeb0")?,
1867 str_to_sha1("7ab82116068982671a8111f27dc10599172334b2")?,
1868 ],
1869 );
1870 Ok(())
1871 }
1872 }
1873 mod range_of_3_commits_not_in_branch_history_returns_3_commits_youngest_first {
1874 use super::*;
1875
1876 #[test]
1877 fn when_checked_out_branch_ahead_of_main() -> Result<()> {
1878 let test_repo = GitTestRepo::default();
1879 let git_repo = Repo::from_path(&test_repo.dir)?;
1880 test_repo.populate_with_test_branch()?;
1881 test_repo.checkout("main")?;
1882
1883 assert_eq!(
1884 git_repo.parse_starting_commits("af474d8..a23e6b0")?,
1885 vec![
1886 str_to_sha1("a23e6b05aaeb7d1471b4a838b51f337d5644eeb0")?,
1887 str_to_sha1("7ab82116068982671a8111f27dc10599172334b2")?,
1888 str_to_sha1("431b84edc0d2fa118d63faa3c2db9c73d630a5ae")?,
1889 ],
1890 );
1891 Ok(())
1892 }
1893 }
1894 }
1756} 1895}
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::{
21 21
22#[derive(Debug, clap::Args)] 22#[derive(Debug, clap::Args)]
23pub struct SubCommandArgs { 23pub struct SubCommandArgs {
24 #[clap(short, long)] 24 #[arg(default_value = "")]
25 /// starting commit (commits since in current branch) or commit range, like
26 /// in `git format-patch`
27 starting_commit: String,
25 /// optional cover letter title 28 /// optional cover letter title
29 #[clap(short, long)]
26 title: Option<String>, 30 title: Option<String>,
27 #[clap(short, long)] 31 #[clap(short, long)]
28 /// optional cover letter description 32 /// optional cover letter description
@@ -42,22 +46,24 @@ pub struct SubCommandArgs {
42pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { 46pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> {
43 let git_repo = Repo::discover().context("cannot find a git repository")?; 47 let git_repo = Repo::discover().context("cannot find a git repository")?;
44 48
45 let (from_branch, to_branch, mut ahead, behind) = 49 let mut commits: Vec<Sha1Hash> = {
46 identify_ahead_behind(&git_repo, &args.from_branch, &args.to_branch)?; 50 if args.starting_commit.is_empty() {
51 let (from_branch, to_branch, ahead, behind) =
52 identify_ahead_behind(&git_repo, &args.from_branch, &args.to_branch)?;
47 53
48 if ahead.is_empty() { 54 if ahead.is_empty() {
49 bail!(format!( 55 bail!(format!(
50 "'{from_branch}' is 0 commits ahead of '{to_branch}' so no patches were created" 56 "'{from_branch}' is 0 commits ahead of '{to_branch}' so no patches were created"
51 )); 57 ));
52 } 58 }
53 59
54 if behind.is_empty() { 60 if behind.is_empty() {
55 println!( 61 println!(
56 "creating patch for {} commits from '{from_branch}' that can be merged into '{to_branch}'", 62 "creating patch for {} commits from '{from_branch}' that can be merged into '{to_branch}'",
57 ahead.len(), 63 ahead.len(),
58 ); 64 );
59 } else { 65 } else {
60 if !Interactor::default().confirm( 66 if !Interactor::default().confirm(
61 PromptConfirmParms::default() 67 PromptConfirmParms::default()
62 .with_prompt( 68 .with_prompt(
63 format!( 69 format!(
@@ -70,14 +76,23 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> {
70 ).context("failed to get confirmation response from interactor confirm")? { 76 ).context("failed to get confirmation response from interactor confirm")? {
71 bail!("aborting so branch can be rebased"); 77 bail!("aborting so branch can be rebased");
72 } 78 }
73 println!( 79 println!(
74 "creating patch for {} commit{} from '{from_branch}' that {} {} behind '{to_branch}'", 80 "creating patch for {} commit{} from '{from_branch}' that {} {} behind '{to_branch}'",
75 ahead.len(), 81 ahead.len(),
76 if ahead.len() > 1 { "s" } else { "" }, 82 if ahead.len() > 1 { "s" } else { "" },
77 if ahead.len() > 1 { "are" } else { "is" }, 83 if ahead.len() > 1 { "are" } else { "is" },
78 behind.len(), 84 behind.len(),
79 ); 85 );
80 } 86 }
87 ahead
88 } else {
89 let ahead = git_repo
90 .parse_starting_commits(&args.starting_commit)
91 .context("cannot parse specified starting commit or range")?;
92 println!("creating patch for {} commits", ahead.len(),);
93 ahead
94 }
95 };
81 96
82 let title = if args.no_cover_letter { 97 let title = if args.no_cover_letter {
83 None 98 None
@@ -138,12 +153,12 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> {
138 .await?; 153 .await?;
139 154
140 // oldest first 155 // oldest first
141 ahead.reverse(); 156 commits.reverse();
142 157
143 let events = generate_cover_letter_and_patch_events( 158 let events = generate_cover_letter_and_patch_events(
144 cover_letter_title_description.clone(), 159 cover_letter_title_description.clone(),
145 &git_repo, 160 &git_repo,
146 &ahead, 161 &commits,
147 &keys, 162 &keys,
148 &repo_ref, 163 &repo_ref,
149 )?; 164 )?;