upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/git.rs139
-rw-r--r--src/sub_commands/send.rs65
-rw-r--r--test_utils/src/git.rs1
-rw-r--r--tests/send.rs226
4 files changed, 406 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 )?;
diff --git a/test_utils/src/git.rs b/test_utils/src/git.rs
index 7f0b4c7..76656df 100644
--- a/test_utils/src/git.rs
+++ b/test_utils/src/git.rs
@@ -56,6 +56,7 @@ impl GitTestRepo {
56 pub fn populate_with_test_branch(&self) -> Result<Oid> { 56 pub fn populate_with_test_branch(&self) -> Result<Oid> {
57 self.populate()?; 57 self.populate()?;
58 self.create_branch("add-example-feature")?; 58 self.create_branch("add-example-feature")?;
59 self.checkout("add-example-feature")?;
59 fs::write(self.dir.join("f1.md"), "some content")?; 60 fs::write(self.dir.join("f1.md"), "some content")?;
60 self.stage_and_commit("add f1.md")?; 61 self.stage_and_commit("add f1.md")?;
61 fs::write(self.dir.join("f2.md"), "some content")?; 62 fs::write(self.dir.join("f2.md"), "some content")?;
diff --git a/tests/send.rs b/tests/send.rs
index 9c8561a..d8186bd 100644
--- a/tests/send.rs
+++ b/tests/send.rs
@@ -1130,4 +1130,230 @@ mod sends_2_patches_without_cover_letter {
1130 } 1130 }
1131 Ok(()) 1131 Ok(())
1132 } 1132 }
1133 mod specify_starting_commits {
1134 use super::*;
1135 fn cli_tester_create_proposal(git_repo: &GitTestRepo) -> CliTester {
1136 let args = vec![
1137 "--nsec",
1138 TEST_KEY_1_NSEC,
1139 "--password",
1140 TEST_PASSWORD,
1141 "--disable-cli-spinners",
1142 "send",
1143 "HEAD~3",
1144 "--no-cover-letter",
1145 ];
1146 CliTester::new_from_dir(&git_repo.dir, args)
1147 }
1148 fn expect_msgs_first(p: &mut CliTester) -> Result<()> {
1149 p.expect("creating patch for 3 commits\r\n")?;
1150 p.expect("searching for profile and relay updates...\r\n")?;
1151 p.expect("\r")?;
1152 p.expect("logged in as fred\r\n")?;
1153 p.expect("posting 3 patches without a covering letter...\r\n")?;
1154 Ok(())
1155 }
1156 async fn prep_run_create_proposal() -> Result<(
1157 Relay<'static>,
1158 Relay<'static>,
1159 Relay<'static>,
1160 Relay<'static>,
1161 Relay<'static>,
1162 )> {
1163 let git_repo = prep_git_repo()?;
1164 // fallback (51,52) user write (53, 55) repo (55, 56)
1165 let (mut r51, mut r52, mut r53, mut r55, mut r56) = (
1166 Relay::new(
1167 8051,
1168 None,
1169 Some(&|relay, client_id, subscription_id, _| -> Result<()> {
1170 relay.respond_events(
1171 client_id,
1172 &subscription_id,
1173 &vec![
1174 generate_test_key_1_metadata_event("fred"),
1175 generate_test_key_1_relay_list_event(),
1176 ],
1177 )?;
1178 Ok(())
1179 }),
1180 ),
1181 Relay::new(8052, None, None),
1182 Relay::new(8053, None, None),
1183 Relay::new(
1184 8055,
1185 None,
1186 Some(&|relay, client_id, subscription_id, _| -> Result<()> {
1187 relay.respond_events(
1188 client_id,
1189 &subscription_id,
1190 &vec![generate_repo_ref_event()],
1191 )?;
1192 Ok(())
1193 }),
1194 ),
1195 Relay::new(8056, None, None),
1196 );
1197
1198 // // check relay had the right number of events
1199 let cli_tester_handle = std::thread::spawn(move || -> Result<()> {
1200 let mut p = cli_tester_create_proposal(&git_repo);
1201 p.expect_end_eventually()?;
1202 for p in [51, 52, 53, 55, 56] {
1203 relay::shutdown_relay(8000 + p)?;
1204 }
1205 Ok(())
1206 });
1207
1208 // launch relay
1209 let _ = join!(
1210 r51.listen_until_close(),
1211 r52.listen_until_close(),
1212 r53.listen_until_close(),
1213 r55.listen_until_close(),
1214 r56.listen_until_close(),
1215 );
1216 cli_tester_handle.join().unwrap()?;
1217 Ok((r51, r52, r53, r55, r56))
1218 }
1219 mod cli_ouput {
1220 use super::*;
1221
1222 async fn run_test_async() -> Result<()> {
1223 let git_repo = prep_git_repo()?;
1224
1225 let (mut r51, mut r52, mut r53, mut r55, mut r56) = (
1226 Relay::new(
1227 8051,
1228 None,
1229 Some(&|relay, client_id, subscription_id, _| -> Result<()> {
1230 relay.respond_events(
1231 client_id,
1232 &subscription_id,
1233 &vec![
1234 generate_test_key_1_metadata_event("fred"),
1235 generate_test_key_1_relay_list_event(),
1236 ],
1237 )?;
1238 Ok(())
1239 }),
1240 ),
1241 Relay::new(8052, None, None),
1242 Relay::new(8053, None, None),
1243 Relay::new(
1244 8055,
1245 None,
1246 Some(&|relay, client_id, subscription_id, _| -> Result<()> {
1247 relay.respond_events(
1248 client_id,
1249 &subscription_id,
1250 &vec![generate_repo_ref_event()],
1251 )?;
1252 Ok(())
1253 }),
1254 ),
1255 Relay::new(8056, None, None),
1256 );
1257
1258 // // check relay had the right number of events
1259 let cli_tester_handle = std::thread::spawn(move || -> Result<()> {
1260 let mut p = cli_tester_create_proposal(&git_repo);
1261
1262 expect_msgs_first(&mut p)?;
1263 relay::expect_send_with_progress(
1264 &mut p,
1265 vec![
1266 (" [my-relay] [repo-relay] ws://localhost:8055", true, ""),
1267 (" [my-relay] ws://localhost:8053", true, ""),
1268 (" [repo-relay] ws://localhost:8056", true, ""),
1269 (" [default] ws://localhost:8051", true, ""),
1270 (" [default] ws://localhost:8052", true, ""),
1271 ],
1272 3,
1273 )?;
1274 p.expect_end_with_whitespace()?;
1275 for p in [51, 52, 53, 55, 56] {
1276 relay::shutdown_relay(8000 + p)?;
1277 }
1278 Ok(())
1279 });
1280
1281 // launch relay
1282 let _ = join!(
1283 r51.listen_until_close(),
1284 r52.listen_until_close(),
1285 r53.listen_until_close(),
1286 r55.listen_until_close(),
1287 r56.listen_until_close(),
1288 );
1289 cli_tester_handle.join().unwrap()?;
1290 Ok(())
1291 }
1292
1293 #[tokio::test]
1294 #[serial]
1295 async fn check_cli_output() -> Result<()> {
1296 run_test_async().await?;
1297 Ok(())
1298 }
1299 }
1300
1301 #[tokio::test]
1302 #[serial]
1303 async fn three_patch_events() -> Result<()> {
1304 let (_, _, r53, r55, r56) = prep_run_create_proposal().await?;
1305 for relay in [&r53, &r55, &r56] {
1306 assert_eq!(relay.events.iter().filter(|e| is_patch(e)).count(), 3);
1307 }
1308 Ok(())
1309 }
1310
1311 #[tokio::test]
1312 #[serial]
1313 async fn first_patch_is_ancestor_and_root_others_in_correct_order() -> Result<()> {
1314 let (_, _, r53, r55, r56) = prep_run_create_proposal().await?;
1315 for relay in [&r53, &r55, &r56] {
1316 let patch_events = relay
1317 .events
1318 .iter()
1319 .filter(|e| is_patch(e))
1320 .collect::<Vec<&nostr::Event>>();
1321
1322 // first patch tagged as root
1323 assert!(
1324 patch_events[0]
1325 .iter_tags()
1326 .any(|t| t.as_vec()[0].eq("t") && t.as_vec()[1].eq("root"))
1327 );
1328 // first patch is ancestor
1329 assert_eq!(
1330 patch_events[0]
1331 .iter_tags()
1332 .find(|t| t.as_vec()[0].eq("commit"))
1333 .unwrap()
1334 .as_vec()[1],
1335 "431b84edc0d2fa118d63faa3c2db9c73d630a5ae"
1336 );
1337 // second patch not tagged as root
1338 assert_eq!(
1339 patch_events[1]
1340 .iter_tags()
1341 .find(|t| t.as_vec()[0].eq("commit"))
1342 .unwrap()
1343 .as_vec()[1],
1344 "232efb37ebc67692c9e9ff58b83c0d3d63971a0a"
1345 );
1346 // second patch not tagged as root
1347 assert_eq!(
1348 patch_events[2]
1349 .iter_tags()
1350 .find(|t| t.as_vec()[0].eq("commit"))
1351 .unwrap()
1352 .as_vec()[1],
1353 "fe973a840fba2a8ab37dd505c154854a69a6505c"
1354 );
1355 }
1356 Ok(())
1357 }
1358 }
1133} 1359}