upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2024-02-22 12:18:01 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2024-02-22 12:18:01 +0000
commit312786fbdacd61fc9f3ed59612d9a6add9112b7f (patch)
tree660300df36b970c46c69d59893856bb6f03142b0
parent79896914357ee322b08b302c3dde08c7a24a0a09 (diff)
feat(pull): support `--in-reply-to` revisions
added tests to cover one of these rebase scenarios
-rw-r--r--src/git.rs34
-rw-r--r--src/sub_commands/pull.rs141
-rw-r--r--tests/pull.rs296
3 files changed, 434 insertions, 37 deletions
diff --git a/src/git.rs b/src/git.rs
index 0a06ab5..6edca0f 100644
--- a/src/git.rs
+++ b/src/git.rs
@@ -344,6 +344,12 @@ impl RepoActions for Repo {
344 } 344 }
345 345
346 fn create_branch_at_commit(&self, branch_name: &str, commit: &str) -> Result<()> { 346 fn create_branch_at_commit(&self, branch_name: &str, commit: &str) -> Result<()> {
347 let branch_checkedout = self.get_checked_out_branch_name()?.eq(branch_name);
348 if branch_checkedout {
349 let (name, _) = self.get_main_or_master_branch()?;
350 self.checkout(name)?;
351 }
352
347 self.git_repo 353 self.git_repo
348 .branch( 354 .branch(
349 branch_name, 355 branch_name,
@@ -351,6 +357,10 @@ impl RepoActions for Repo {
351 true, 357 true,
352 ) 358 )
353 .context("branch could not be created")?; 359 .context("branch could not be created")?;
360
361 if branch_checkedout {
362 self.checkout(branch_name)?;
363 }
354 Ok(()) 364 Ok(())
355 } 365 }
356 /* returns patches applied */ 366 /* returns patches applied */
@@ -1292,6 +1302,30 @@ mod tests {
1292 assert_eq!(test_repo.checkout(branch_name)?, ahead_1_oid); 1302 assert_eq!(test_repo.checkout(branch_name)?, ahead_1_oid);
1293 Ok(()) 1303 Ok(())
1294 } 1304 }
1305
1306 #[test]
1307 fn when_branch_is_checkedout_new_tip_specified_it_is_updated() -> Result<()> {
1308 let test_repo = GitTestRepo::default();
1309 test_repo.populate()?;
1310 // create feature branch and add 2 commits
1311 test_repo.create_branch("feature")?;
1312 test_repo.checkout("feature")?;
1313 std::fs::write(test_repo.dir.join("t3.md"), "some content")?;
1314 let ahead_1_oid = test_repo.stage_and_commit("add t3.md")?;
1315 std::fs::write(test_repo.dir.join("t4.md"), "some content")?;
1316 let ahead_2_oid = test_repo.stage_and_commit("add t4.md")?;
1317
1318 let git_repo = Repo::from_path(&test_repo.dir)?;
1319
1320 let branch_name = "test-name-1";
1321 git_repo.create_branch_at_commit(branch_name, &ahead_1_oid.to_string())?;
1322 test_repo.checkout(branch_name)?;
1323 git_repo.create_branch_at_commit(branch_name, &ahead_2_oid.to_string())?;
1324 test_repo.checkout("main")?;
1325
1326 assert_eq!(test_repo.checkout(branch_name)?, ahead_2_oid);
1327 Ok(())
1328 }
1295 } 1329 }
1296 } 1330 }
1297 1331
diff --git a/src/sub_commands/pull.rs b/src/sub_commands/pull.rs
index de078e3..d832f6e 100644
--- a/src/sub_commands/pull.rs
+++ b/src/sub_commands/pull.rs
@@ -1,12 +1,13 @@
1use anyhow::{bail, Context, Result}; 1use anyhow::{bail, Context, Result};
2 2
3use super::list::{get_commit_id_from_patch, tag_value};
3#[cfg(not(test))] 4#[cfg(not(test))]
4use crate::client::Client; 5use crate::client::Client;
5#[cfg(test)] 6#[cfg(test)]
6use crate::client::MockConnect; 7use crate::client::MockConnect;
7use crate::{ 8use crate::{
8 client::Connect, 9 client::Connect,
9 git::{Repo, RepoActions}, 10 git::{str_to_sha1, Repo, RepoActions},
10 repo_ref, 11 repo_ref,
11 sub_commands::{ 12 sub_commands::{
12 list::get_most_recent_patch_with_ancestors, 13 list::get_most_recent_patch_with_ancestors,
@@ -14,6 +15,7 @@ use crate::{
14 }, 15 },
15}; 16};
16 17
18#[allow(clippy::too_many_lines)]
17pub async fn launch() -> Result<()> { 19pub async fn launch() -> Result<()> {
18 let git_repo = Repo::discover().context("cannot find a git repository")?; 20 let git_repo = Repo::discover().context("cannot find a git repository")?;
19 21
@@ -53,22 +55,139 @@ pub async fn launch() -> Result<()> {
53 ) 55 )
54 .await?; 56 .await?;
55 57
56 if git_repo.has_outstanding_changes()? { 58 let most_recent_proposal_patch_chain =
57 bail!("cannot pull changes when repository is not clean. discard changes and try again."); 59 get_most_recent_patch_with_ancestors(commit_events.clone())
58 } 60 .context("cannot get most recent patch for proposal")?;
61
62 let local_branch_tip = git_repo.get_tip_of_local_branch(&branch_name)?;
63
64 let (main_branch_name, master_tip) = git_repo.get_main_or_master_branch()?;
65
66 let (local_ahead_of_main, local_beind_main) =
67 git_repo.get_commits_ahead_behind(&master_tip, &local_branch_tip)?;
59 68
60 let most_recent_proposal_patch_chain = get_most_recent_patch_with_ancestors(commit_events) 69 let proposal_base_commit = str_to_sha1(&tag_value(
61 .context("cannot get most recent patch for proposal")?; 70 most_recent_proposal_patch_chain
71 .last()
72 .context("there should be at least one patch as we have already checked for this")?,
73 "parent-commit",
74 )?)
75 .context("cannot get valid parent commit id from patch")?;
62 76
63 let applied = git_repo 77 let (_, proposal_behind_main) =
64 .apply_patch_chain(&branch_name, most_recent_proposal_patch_chain) 78 git_repo.get_commits_ahead_behind(&master_tip, &proposal_base_commit)?;
65 .context("cannot apply patch chain")?;
66 79
67 if applied.is_empty() { 80 let proposal_tip =
81 str_to_sha1(
82 &get_commit_id_from_patch(most_recent_proposal_patch_chain.first().context(
83 "there should be at least one patch as we have already checked for this",
84 )?)
85 .context("cannot get valid commit_id from patch")?,
86 )
87 .context("cannot get valid commit_id from patch")?;
88
89 // if uptodate
90 if proposal_tip.eq(&local_branch_tip) {
68 println!("branch already up-to-date"); 91 println!("branch already up-to-date");
69 } else { 92 }
93 // if new appendments
94 else if most_recent_proposal_patch_chain.iter().any(|patch| {
95 get_commit_id_from_patch(patch)
96 .unwrap_or_default()
97 .eq(&local_branch_tip.to_string())
98 }) {
99 check_clean(&git_repo)?;
100 let applied = git_repo
101 .apply_patch_chain(&branch_name, most_recent_proposal_patch_chain)
102 .context("cannot apply patch chain")?;
70 println!("applied {} new commits", applied.len(),); 103 println!("applied {} new commits", applied.len(),);
71 } 104 }
105 // if parent commit doesnt exist
106 else if !git_repo.does_commit_exist(&proposal_base_commit.to_string())? {
107 println!(
108 "a new version of the proposal has a prant commit that doesnt exist in your local repository."
109 );
110 println!("your '{main_branch_name}' branch may not be up-to-date.");
111 println!("manually run `git pull` on '{main_branch_name}' and try again");
112 }
113 // if tip of local in proposal history (new, ammended or rebased version but no
114 // local changes)
115 else if commit_events.iter().any(|patch| {
116 get_commit_id_from_patch(patch)
117 .unwrap_or_default()
118 .eq(&local_branch_tip.to_string())
119 }) {
120 check_clean(&git_repo)?;
121
122 git_repo.create_branch_at_commit(&branch_name, &proposal_base_commit.to_string())?;
123 let applied = git_repo
124 .apply_patch_chain(&branch_name, most_recent_proposal_patch_chain)
125 .context("cannot apply patch chain")?;
126
127 println!(
128 "pulled new version of proposal ({} ahead {} behind '{main_branch_name}'), replacing old version ({} ahead {} behind '{main_branch_name}')",
129 applied.len(),
130 proposal_behind_main.len(),
131 local_ahead_of_main.len(),
132 local_beind_main.len(),
133 );
134 }
135 // if tip of proposal in branch in history (local appendments made to up-to-date
136 // proposal)
137 else if let Ok((local_ahead_of_proposal, _)) =
138 git_repo.get_commits_ahead_behind(&proposal_tip, &local_branch_tip)
139 {
140 println!(
141 "local proposal branch exists with {} unpublished commits on top of the most up-to-date version of the proposal",
142 local_ahead_of_proposal.len()
143 );
144 }
145 // user has probably has an unpublished rebase of the latest proposal version
146 // if tip of proposal commits exist (were once part of branch but have been
147 // ammended and git clean up job hasn't removed them)
148 else if git_repo.does_commit_exist(&proposal_tip.to_string())? {
149 println!(
150 "you have previously applied the latest version of the proposal ({} ahead {} behind '{main_branch_name}') but your local proposal branch has other unpublished changes ({} ahead {} behind '{main_branch_name}')",
151 most_recent_proposal_patch_chain.len(),
152 proposal_behind_main.len(),
153 local_ahead_of_main.len(),
154 local_beind_main.len(),
155 );
156 println!(
157 "if this sounds right then consider publishing your rebase `ngit push --force` or discarding your local branch"
158 );
159 }
160 // user has probaly has an unpublished rebase of an older version of the
161 // proposal
162 else {
163 println!(
164 "your local proposal branch ({} ahead {} behind '{main_branch_name}') has conflicting changes with the latest published proposal ({} ahead {} behind '{main_branch_name}')",
165 local_ahead_of_main.len(),
166 local_beind_main.len(),
167 most_recent_proposal_patch_chain.len(),
168 proposal_behind_main.len(),
169 );
170 println!(
171 "its likely that you are working off an old proposal version because git has no record of the latest proposal commit."
172 );
173 println!(
174 "it is possible that you have ammended the latest version and git has delete this commit as part of a clean up"
175 );
176
177 println!("to view the latest proposal but retain your changes:");
178 println!(" 1) create a new branch off the tip commit of this one to store your changes");
179 println!(" 2) run `ngit list` and checkout the latest published version of this proposal");
180
181 println!("if you are confident in your changes consider running `ngit push --force`");
182 }
183 Ok(())
184}
72 185
186fn check_clean(git_repo: &Repo) -> Result<()> {
187 if git_repo.has_outstanding_changes()? {
188 bail!(
189 "cannot pull proposal branch when repository is not clean. discard or stash (un)staged changes and try again."
190 );
191 }
73 Ok(()) 192 Ok(())
74} 193}
diff --git a/tests/pull.rs b/tests/pull.rs
index c4bc169..39f3ef4 100644
--- a/tests/pull.rs
+++ b/tests/pull.rs
@@ -18,22 +18,22 @@ fn cli_tester_create_proposals() -> Result<GitTestRepo> {
18 &git_repo, 18 &git_repo,
19 FEATURE_BRANCH_NAME_1, 19 FEATURE_BRANCH_NAME_1,
20 "a", 20 "a",
21 PROPOSAL_TITLE_1, 21 Some((PROPOSAL_TITLE_1, "proposal a description")),
22 "proposal a description", 22 None,
23 )?; 23 )?;
24 cli_tester_create_proposal( 24 cli_tester_create_proposal(
25 &git_repo, 25 &git_repo,
26 FEATURE_BRANCH_NAME_2, 26 FEATURE_BRANCH_NAME_2,
27 "b", 27 "b",
28 PROPOSAL_TITLE_2, 28 Some((PROPOSAL_TITLE_2, "proposal b description")),
29 "proposal b description", 29 None,
30 )?; 30 )?;
31 cli_tester_create_proposal( 31 cli_tester_create_proposal(
32 &git_repo, 32 &git_repo,
33 FEATURE_BRANCH_NAME_3, 33 FEATURE_BRANCH_NAME_3,
34 "c", 34 "c",
35 PROPOSAL_TITLE_3, 35 Some((PROPOSAL_TITLE_3, "proposal c description")),
36 "proposal c description", 36 None,
37 )?; 37 )?;
38 Ok(git_repo) 38 Ok(git_repo)
39} 39}
@@ -66,27 +66,59 @@ fn cli_tester_create_proposal(
66 test_repo: &GitTestRepo, 66 test_repo: &GitTestRepo,
67 branch_name: &str, 67 branch_name: &str,
68 prefix: &str, 68 prefix: &str,
69 title: &str, 69 cover_letter_title_and_description: Option<(&str, &str)>,
70 description: &str, 70 in_reply_to: Option<String>,
71) -> Result<()> { 71) -> Result<()> {
72 create_and_populate_branch(test_repo, branch_name, prefix, false)?; 72 create_and_populate_branch(test_repo, branch_name, prefix, false)?;
73 73
74 let mut p = CliTester::new_from_dir( 74 if let Some(in_reply_to) = in_reply_to {
75 &test_repo.dir, 75 let mut p = CliTester::new_from_dir(
76 [ 76 &test_repo.dir,
77 "--nsec", 77 [
78 TEST_KEY_1_NSEC, 78 "--nsec",
79 "--password", 79 TEST_KEY_1_NSEC,
80 TEST_PASSWORD, 80 "--password",
81 "--disable-cli-spinners", 81 TEST_PASSWORD,
82 "send", 82 "--disable-cli-spinners",
83 "--title", 83 "send",
84 format!("\"{title}\"").as_str(), 84 "--no-cover-letter",
85 "--description", 85 "--in-reply-to",
86 format!("\"{description}\"").as_str(), 86 in_reply_to.as_str(),
87 ], 87 ],
88 ); 88 );
89 p.expect_end_eventually()?; 89 p.expect_end_eventually()?;
90 } else if let Some((title, description)) = cover_letter_title_and_description {
91 let mut p = CliTester::new_from_dir(
92 &test_repo.dir,
93 [
94 "--nsec",
95 TEST_KEY_1_NSEC,
96 "--password",
97 TEST_PASSWORD,
98 "--disable-cli-spinners",
99 "send",
100 "--title",
101 format!("\"{title}\"").as_str(),
102 "--description",
103 format!("\"{description}\"").as_str(),
104 ],
105 );
106 p.expect_end_eventually()?;
107 } else {
108 let mut p = CliTester::new_from_dir(
109 &test_repo.dir,
110 [
111 "--nsec",
112 TEST_KEY_1_NSEC,
113 "--password",
114 TEST_PASSWORD,
115 "--disable-cli-spinners",
116 "send",
117 "--no-cover-letter",
118 ],
119 );
120 p.expect_end_eventually()?;
121 }
90 Ok(()) 122 Ok(())
91} 123}
92 124
@@ -415,7 +447,219 @@ mod when_branch_is_checked_out {
415 } 447 }
416 448
417 mod when_latest_event_rebases_branch { 449 mod when_latest_event_rebases_branch {
418 // use super::*; 450 use std::time::Duration;
419 // TODO 451
452 use nostr_sdk::blocking::Client;
453
454 use super::*;
455
456 async fn prep_and_run() -> Result<(GitTestRepo, GitTestRepo)> {
457 // fallback (51,52) user write (53, 55) repo (55, 56)
458 let (mut r51, mut r52, mut r53, mut r55, mut r56) = (
459 Relay::new(8051, None, None),
460 Relay::new(8052, None, None),
461 Relay::new(8053, None, None),
462 Relay::new(8055, None, None),
463 Relay::new(8056, None, None),
464 );
465
466 r51.events.push(generate_test_key_1_relay_list_event());
467 r51.events.push(generate_test_key_1_metadata_event("fred"));
468 r51.events.push(generate_repo_ref_event());
469
470 r55.events.push(generate_repo_ref_event());
471 r55.events.push(generate_test_key_1_metadata_event("fred"));
472 r55.events.push(generate_test_key_1_relay_list_event());
473
474 let cli_tester_handle =
475 std::thread::spawn(move || -> Result<(GitTestRepo, GitTestRepo)> {
476 // create 3 proposals
477 let _ = cli_tester_create_proposals()?;
478 // get proposal id of first
479 let client = Client::new(&nostr::Keys::generate());
480 client.add_relay("ws://localhost:8055")?;
481 client.connect_relay("ws://localhost:8055")?;
482 let proposals = client.get_events_of(
483 vec![
484 nostr::Filter::default()
485 .kind(nostr::Kind::Custom(PATCH_KIND))
486 .custom_tag(nostr::Alphabet::T, vec!["root"]),
487 ],
488 Some(Duration::from_millis(500)),
489 )?;
490 client.disconnect()?;
491
492 let proposal_1_id = proposals
493 .iter()
494 .find(|e| {
495 e.tags
496 .iter()
497 .any(|t| t.as_vec()[1].eq(&FEATURE_BRANCH_NAME_1))
498 })
499 .unwrap()
500 .id;
501 // recreate proposal 1 on top of a another commit (like a rebase on top
502 // of one extra commit)
503 let second_originating_repo = GitTestRepo::default();
504 second_originating_repo.populate()?;
505 std::fs::write(
506 second_originating_repo.dir.join("amazing.md"),
507 "some content",
508 )?;
509 second_originating_repo.stage_and_commit("commit for rebasing on top of")?;
510 cli_tester_create_proposal(
511 &second_originating_repo,
512 FEATURE_BRANCH_NAME_1,
513 "a",
514 Some((PROPOSAL_TITLE_1, "proposal a description")),
515 Some(proposal_1_id.to_string()),
516 )?;
517
518 // pretend we have downloaded the origianl version of the first proposal
519 let test_repo = GitTestRepo::default();
520 test_repo.populate()?;
521 create_and_populate_branch(&test_repo, FEATURE_BRANCH_NAME_1, "a", false)?;
522 // pretend we have pulled the updated main branch
523 test_repo.checkout("main")?;
524 std::fs::write(test_repo.dir.join("amazing.md"), "some content")?;
525 test_repo.stage_and_commit("commit for rebasing on top of")?;
526 test_repo.checkout(FEATURE_BRANCH_NAME_1)?;
527
528 let mut p = CliTester::new_from_dir(&test_repo.dir, ["pull"]);
529 p.expect_end_eventually_and_print()?;
530
531 for p in [51, 52, 53, 55, 56] {
532 relay::shutdown_relay(8000 + p)?;
533 }
534 Ok((second_originating_repo, test_repo))
535 });
536
537 // launch relay
538 let _ = join!(
539 r51.listen_until_close(),
540 r52.listen_until_close(),
541 r53.listen_until_close(),
542 r55.listen_until_close(),
543 r56.listen_until_close(),
544 );
545 let res = cli_tester_handle.join().unwrap()?;
546
547 Ok(res)
548 }
549
550 mod cli_prompts {
551 use super::*;
552 async fn run_async_prompts_to_choose_from_proposal_titles() -> Result<()> {
553 let (mut r51, mut r52, mut r53, mut r55, mut r56) = (
554 Relay::new(8051, None, None),
555 Relay::new(8052, None, None),
556 Relay::new(8053, None, None),
557 Relay::new(8055, None, None),
558 Relay::new(8056, None, None),
559 );
560
561 r51.events.push(generate_test_key_1_relay_list_event());
562 r51.events.push(generate_test_key_1_metadata_event("fred"));
563 r51.events.push(generate_repo_ref_event());
564
565 r55.events.push(generate_repo_ref_event());
566 r55.events.push(generate_test_key_1_metadata_event("fred"));
567 r55.events.push(generate_test_key_1_relay_list_event());
568
569 let cli_tester_handle = std::thread::spawn(move || -> Result<()> {
570 // create 3 proposals
571 let _ = cli_tester_create_proposals()?;
572 // get proposal id of first
573 let client = Client::new(&nostr::Keys::generate());
574 client.add_relay("ws://localhost:8055")?;
575 client.connect_relay("ws://localhost:8055")?;
576 let proposals = client.get_events_of(
577 vec![
578 nostr::Filter::default()
579 .kind(nostr::Kind::Custom(PATCH_KIND))
580 .custom_tag(nostr::Alphabet::T, vec!["root"]),
581 ],
582 Some(Duration::from_millis(500)),
583 )?;
584 client.disconnect()?;
585
586 let proposal_1_id = proposals
587 .iter()
588 .find(|e| {
589 e.tags
590 .iter()
591 .any(|t| t.as_vec()[1].eq(&FEATURE_BRANCH_NAME_1))
592 })
593 .unwrap()
594 .id;
595 // recreate proposal 1 on top of a another commit (like a rebase on top
596 // of one extra commit)
597 let second_originating_repo = GitTestRepo::default();
598 second_originating_repo.populate()?;
599 std::fs::write(
600 second_originating_repo.dir.join("amazing.md"),
601 "some content",
602 )?;
603 second_originating_repo.stage_and_commit("commit for rebasing on top of")?;
604 cli_tester_create_proposal(
605 &second_originating_repo,
606 FEATURE_BRANCH_NAME_1,
607 "a",
608 Some((PROPOSAL_TITLE_1, "proposal a description")),
609 Some(proposal_1_id.to_string()),
610 )?;
611
612 // pretend we have downloaded the origianl version of the first proposal
613 let test_repo = GitTestRepo::default();
614 test_repo.populate()?;
615 create_and_populate_branch(&test_repo, FEATURE_BRANCH_NAME_1, "a", false)?;
616 // pretend we have pulled the updated main branch
617 test_repo.checkout("main")?;
618 std::fs::write(test_repo.dir.join("amazing.md"), "some content")?;
619 test_repo.stage_and_commit("commit for rebasing on top of")?;
620 test_repo.checkout(FEATURE_BRANCH_NAME_1)?;
621
622 let mut p = CliTester::new_from_dir(&test_repo.dir, ["pull"]);
623 p.expect("finding proposal root event...\r\n")?;
624 p.expect("found proposal root event. finding commits...\r\n")?;
625 p.expect_end_with("pulled new version of proposal (2 ahead 0 behind 'main'), replacing old version (2 ahead 1 behind 'main')\r\n")?;
626
627 for p in [51, 52, 53, 55, 56] {
628 relay::shutdown_relay(8000 + p)?;
629 }
630 Ok(())
631 });
632
633 // launch relay
634 let _ = join!(
635 r51.listen_until_close(),
636 r52.listen_until_close(),
637 r53.listen_until_close(),
638 r55.listen_until_close(),
639 r56.listen_until_close(),
640 );
641 cli_tester_handle.join().unwrap()?;
642 println!("{:?}", r55.events);
643 Ok(())
644 }
645
646 #[tokio::test]
647 #[serial]
648 async fn prompts_to_choose_from_proposal_titles() -> Result<()> {
649 let _ = run_async_prompts_to_choose_from_proposal_titles().await;
650 Ok(())
651 }
652 }
653
654 #[tokio::test]
655 #[serial]
656 async fn proposal_branch_tip_is_most_recent_proposal_revision_tip() -> Result<()> {
657 let (originating_repo, test_repo) = prep_and_run().await?;
658 assert_eq!(
659 originating_repo.get_tip_of_local_branch(FEATURE_BRANCH_NAME_1)?,
660 test_repo.get_tip_of_local_branch(FEATURE_BRANCH_NAME_1)?,
661 );
662 Ok(())
663 }
420 } 664 }
421} 665}