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-08-08 17:34:40 +0100
committerDanConwayDev <DanConwayDev@protonmail.com>2024-08-08 17:36:44 +0100
commit9971f23a184d57600ea9b1962910d32bf6aec185 (patch)
treee4a82036c4785ddb8080be9e99e116cd3a7d10fa
parentfa2ab4b0dfc31e315d53bd9e36ada41ca95ec2d6 (diff)
feat(remote): `push` force push proposal
will issue a proposal revision
-rw-r--r--src/git_remote_helper.rs102
-rw-r--r--test_utils/src/git.rs5
-rw-r--r--tests/git_remote_helper.rs153
3 files changed, 227 insertions, 33 deletions
diff --git a/src/git_remote_helper.rs b/src/git_remote_helper.rs
index f36b23b..be230b1 100644
--- a/src/git_remote_helper.rs
+++ b/src/git_remote_helper.rs
@@ -33,7 +33,10 @@ use sub_commands::{
33 get_most_recent_patch_with_ancestors, get_proposals_and_revisions_from_cache, status_kinds, 33 get_most_recent_patch_with_ancestors, get_proposals_and_revisions_from_cache, status_kinds,
34 tag_value, 34 tag_value,
35 }, 35 },
36 send::{event_is_revision_root, event_to_cover_letter, generate_patch_event, send_events}, 36 send::{
37 event_is_revision_root, event_to_cover_letter, generate_cover_letter_and_patch_events,
38 generate_patch_event, send_events,
39 },
37}; 40};
38 41
39#[cfg(not(test))] 42#[cfg(not(test))]
@@ -656,21 +659,42 @@ async fn push(
656 if let Some((_, (proposal, patches))) = 659 if let Some((_, (proposal, patches))) =
657 find_proposal_and_patches_by_branch_name(to, &open_proposals) 660 find_proposal_and_patches_by_branch_name(to, &open_proposals)
658 { 661 {
659 if to.starts_with('+') { 662 if [repo_ref.maintainers.clone(), vec![proposal.author()]]
660 // TODO do force push - issue as revision 663 .concat()
661 } else { 664 .contains(&user_ref.public_key)
662 let tip_patch = patches.first().unwrap(); 665 {
663 let tip_of_proposal = get_commit_id_from_patch(tip_patch)?;
664 let tip_of_proposal_commit =
665 git_repo.get_commit_or_tip_of_reference(&tip_of_proposal)?;
666 let tip_of_pushed_branch = git_repo.get_commit_or_tip_of_reference(from)?; 666 let tip_of_pushed_branch = git_repo.get_commit_or_tip_of_reference(from)?;
667 let (mut ahead, behind) = git_repo 667 if refspec.starts_with('+') {
668 .get_commits_ahead_behind(&tip_of_proposal_commit, &tip_of_pushed_branch)?; 668 // force push
669 if behind.is_empty() { 669 let (_, main_tip) = git_repo.get_main_or_master_branch()?;
670 if [repo_ref.maintainers.clone(), vec![proposal.author()]] 670 let (mut ahead, _) =
671 .concat() 671 git_repo.get_commits_ahead_behind(&main_tip, &tip_of_pushed_branch)?;
672 .contains(&user_ref.public_key) 672 ahead.reverse();
673 for patch in generate_cover_letter_and_patch_events(
674 None,
675 git_repo,
676 &ahead,
677 &signer,
678 repo_ref,
679 &Some(proposal.id().to_string()),
680 &[],
681 )
682 .await?
673 { 683 {
684 events.push(patch);
685 }
686 } else {
687 // fast forward push
688 let tip_patch = patches.first().unwrap();
689 let tip_of_proposal = get_commit_id_from_patch(tip_patch)?;
690 let tip_of_proposal_commit =
691 git_repo.get_commit_or_tip_of_reference(&tip_of_proposal)?;
692
693 let (mut ahead, behind) = git_repo.get_commits_ahead_behind(
694 &tip_of_proposal_commit,
695 &tip_of_pushed_branch,
696 )?;
697 if behind.is_empty() {
674 let thread_id = if let Ok(root_event_id) = get_event_root(tip_patch) { 698 let thread_id = if let Ok(root_event_id) = get_event_root(tip_patch) {
675 root_event_id 699 root_event_id
676 } else { 700 } else {
@@ -702,25 +726,25 @@ async fn push(
702 parent_patch = new_patch; 726 parent_patch = new_patch;
703 } 727 }
704 } else { 728 } else {
729 // we shouldn't get here
730 term.write_line(
731 format!(
732 "WARNING: failed to push {from} as nostr proposal. Try and force push ",
733 )
734 .as_str(),
735 )
736 .unwrap();
705 println!( 737 println!(
706 "error {to} permission denied. you are not the proposal author or a repo maintainer" 738 "error {to} cannot fastforward as newer patches found on proposal"
707 ); 739 );
708 rejected_proposal_refspecs.push(refspec.to_string()); 740 rejected_proposal_refspecs.push(refspec.to_string());
709 } 741 }
710 } else {
711 // we shouldn't get here
712 term.write_line(
713 format!(
714 "WARNING: failed to push {from} as nostr proposal. Try and force push ",
715 )
716 .as_str(),
717 )
718 .unwrap();
719 println!(
720 "error {to} cannot fastforward as newer patches found on proposal"
721 );
722 rejected_proposal_refspecs.push(refspec.to_string());
723 } 742 }
743 } else {
744 println!(
745 "error {to} permission denied. you are not the proposal author or a repo maintainer"
746 );
747 rejected_proposal_refspecs.push(refspec.to_string());
724 } 748 }
725 } else { 749 } else {
726 // TODO new proposal / proposal no longer open 750 // TODO new proposal / proposal no longer open
@@ -1046,7 +1070,14 @@ fn refspec_to_from_to(refspec: &str) -> Result<(&str, &str)> {
1046 ); 1070 );
1047 } 1071 }
1048 let parts = refspec.split(':').collect::<Vec<&str>>(); 1072 let parts = refspec.split(':').collect::<Vec<&str>>();
1049 Ok((parts.first().unwrap(), parts.get(1).unwrap())) 1073 Ok((
1074 if parts.first().unwrap().starts_with('+') {
1075 &parts.first().unwrap()[1..]
1076 } else {
1077 parts.first().unwrap()
1078 },
1079 parts.get(1).unwrap(),
1080 ))
1050} 1081}
1051 1082
1052fn refspec_remote_ref_name( 1083fn refspec_remote_ref_name(
@@ -1061,7 +1092,8 @@ fn refspec_remote_ref_name(
1061 Ok(format!( 1092 Ok(format!(
1062 "refs/remotes/{}/{}", 1093 "refs/remotes/{}/{}",
1063 nostr_remote.name().context("remote should have a name")?, 1094 nostr_remote.name().context("remote should have a name")?,
1064 to.replace("refs/heads/", ""), // TODO only replace if it begins with this 1095 to.replace("refs/heads/", ""), /* TODO only replace if it begins with this
1096 * TODO what about tags? */
1065 )) 1097 ))
1066} 1098}
1067 1099
@@ -1289,4 +1321,14 @@ mod tests {
1289 } 1321 }
1290 } 1322 }
1291 } 1323 }
1324
1325 mod refspec_to_from_to {
1326 use super::*;
1327
1328 #[test]
1329 fn trailing_plus_stripped() {
1330 let (from, _) = refspec_to_from_to("+testing:testingb").unwrap();
1331 assert_eq!(from, "testing");
1332 }
1333 }
1292} 1334}
diff --git a/test_utils/src/git.rs b/test_utils/src/git.rs
index 5ae7f39..2a3d566 100644
--- a/test_utils/src/git.rs
+++ b/test_utils/src/git.rs
@@ -274,12 +274,11 @@ impl GitTestRepo {
274 Ok(()) 274 Ok(())
275 } 275 }
276 276
277 pub fn checkout_remote_branch(&self, branch_name: &str) -> Result<()> { 277 pub fn checkout_remote_branch(&self, branch_name: &str) -> Result<Oid> {
278 self.checkout(&format!("remotes/origin/{branch_name}"))?; 278 self.checkout(&format!("remotes/origin/{branch_name}"))?;
279 let mut branch = self.create_branch(branch_name)?; 279 let mut branch = self.create_branch(branch_name)?;
280 branch.set_upstream(Some(&format!("origin/{branch_name}")))?; 280 branch.set_upstream(Some(&format!("origin/{branch_name}")))?;
281 self.checkout(branch_name)?; 281 self.checkout(branch_name)
282 Ok(())
283 } 282 }
284} 283}
285 284
diff --git a/tests/git_remote_helper.rs b/tests/git_remote_helper.rs
index dc6d931..379954d 100644
--- a/tests/git_remote_helper.rs
+++ b/tests/git_remote_helper.rs
@@ -1856,4 +1856,157 @@ mod push {
1856 1856
1857 Ok(()) 1857 Ok(())
1858 } 1858 }
1859
1860 #[tokio::test]
1861 #[serial]
1862 async fn force_push_creates_proposal_revision() -> Result<()> {
1863 let (events, source_git_repo) = prep_source_repo_and_events_including_proposals().await?;
1864 let source_path = source_git_repo.dir.to_str().unwrap().to_string();
1865
1866 let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
1867 Relay::new(8051, None, None),
1868 Relay::new(8052, None, None),
1869 Relay::new(8053, None, None),
1870 Relay::new(8055, None, None),
1871 Relay::new(8056, None, None),
1872 Relay::new(8057, None, None),
1873 );
1874 r51.events = events.clone();
1875 r55.events = events.clone();
1876
1877 let before = r55.events.iter().cloned().collect::<HashSet<Event>>();
1878
1879 let cli_tester_handle = std::thread::spawn(move || -> Result<(String, String)> {
1880 let branch_name = get_proposal_branch_name_from_events(&events, FEATURE_BRANCH_NAME_1)?;
1881
1882 let git_repo = clone_git_repo_with_nostr_url()?;
1883 let oid = git_repo.checkout_remote_branch(&branch_name)?;
1884 // remove last commit
1885 git_repo.checkout("main")?;
1886 git_repo.git_repo.branch(
1887 &branch_name,
1888 &git_repo.git_repo.find_commit(oid)?.parent(0)?,
1889 true,
1890 )?;
1891 git_repo.checkout(&branch_name)?;
1892
1893 std::fs::write(git_repo.dir.join("new.md"), "some content")?;
1894 git_repo.stage_and_commit("new.md")?;
1895
1896 std::fs::write(git_repo.dir.join("new2.md"), "some content")?;
1897 git_repo.stage_and_commit("new2.md")?;
1898
1899 let mut p =
1900 CliTester::new_git_with_remote_helper_from_dir(&git_repo.dir, ["push", "--force"]);
1901 cli_expect_nostr_fetch(&mut p)?;
1902 p.expect(format!("fetching refs list: {}...\r\n\r", source_path).as_str())?;
1903 p.expect(format!("To {}\r\n", get_nostr_remote_url()?).as_str())?;
1904 let output = p.expect_end_eventually()?;
1905
1906 for p in [51, 52, 53, 55, 56, 57] {
1907 relay::shutdown_relay(8000 + p)?;
1908 }
1909
1910 Ok((output, branch_name))
1911 });
1912 // launch relays
1913 let _ = join!(
1914 r51.listen_until_close(),
1915 r52.listen_until_close(),
1916 r53.listen_until_close(),
1917 r55.listen_until_close(),
1918 r56.listen_until_close(),
1919 r57.listen_until_close(),
1920 );
1921
1922 let (output, branch_name) = cli_tester_handle.join().unwrap()?;
1923
1924 assert_eq!(
1925 output,
1926 format!(" + eb5d678...8a296c8 {branch_name} -> {branch_name} (forced update)\r\n")
1927 .as_str(),
1928 );
1929
1930 let new_events = r55
1931 .events
1932 .iter()
1933 .cloned()
1934 .collect::<HashSet<Event>>()
1935 .difference(&before)
1936 .cloned()
1937 .collect::<Vec<Event>>();
1938 assert_eq!(new_events.len(), 3);
1939
1940 let proposal = r55
1941 .events
1942 .iter()
1943 .find(|e| {
1944 e.iter_tags()
1945 .find(|t| t.as_vec()[0].eq("branch-name"))
1946 .is_some_and(|t| t.as_vec()[1].eq(FEATURE_BRANCH_NAME_1))
1947 })
1948 .unwrap();
1949
1950 let revision_root_patch = new_events
1951 .iter()
1952 .find(|e| e.iter_tags().any(|t| t.as_vec()[1].eq("revision-root")))
1953 .unwrap();
1954
1955 assert_eq!(
1956 proposal.id().to_string(),
1957 revision_root_patch
1958 .tags
1959 .iter()
1960 .find(|t| t.is_reply())
1961 .unwrap()
1962 .as_vec()[1],
1963 "revision root patch replies to original proposal"
1964 );
1965
1966 assert!(
1967 revision_root_patch.content.contains("[PATCH 1/3]"),
1968 "revision root labeled with [PATCH 1/3]"
1969 );
1970
1971 let second_patch = new_events
1972 .iter()
1973 .find(|e| e.content.contains("new.md"))
1974 .unwrap();
1975 let third_patch = new_events
1976 .iter()
1977 .find(|e| e.content.contains("new2.md"))
1978 .unwrap();
1979 assert!(
1980 second_patch.content.contains("[PATCH 2/3]"),
1981 "second patch labeled with [PATCH 2/3]"
1982 );
1983 assert!(
1984 third_patch.content.contains("[PATCH 3/3]"),
1985 "third patch labeled with [PATCH 3/3]"
1986 );
1987
1988 assert_eq!(
1989 revision_root_patch.id().to_string(),
1990 second_patch
1991 .tags
1992 .iter()
1993 .find(|t| t.is_root())
1994 .unwrap()
1995 .as_vec()[1],
1996 "second patch sets revision id as root"
1997 );
1998
1999 assert_eq!(
2000 second_patch.id().to_string(),
2001 third_patch
2002 .tags
2003 .iter()
2004 .find(|t| t.is_reply())
2005 .unwrap()
2006 .as_vec()[1],
2007 "third patch replies to the second new patch"
2008 );
2009
2010 Ok(())
2011 }
1859} 2012}