diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/git.rs | 139 | ||||
| -rw-r--r-- | src/sub_commands/send.rs | 65 |
2 files changed, 179 insertions, 25 deletions
| @@ -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 | ||
| 72 | impl RepoActions for Repo { | 73 | impl 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 | ||
| 412 | fn oid_to_u8_20_bytes(oid: &Oid) -> [u8; 20] { | 456 | fn 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)] |
| 23 | pub struct SubCommandArgs { | 23 | pub 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 { | |||
| 42 | pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { | 46 | pub 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 | )?; |