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-18 08:04:49 +0100
committerDanConwayDev <DanConwayDev@protonmail.com>2024-08-18 08:04:49 +0100
commit948c8595acea9a783a38002371c40185868ce923 (patch)
tree10dce3b3dc487e5d27aee706d64738bd72f1e2ce
parent5618fd9883d45de1443a40abada944cbe3bb8dfd (diff)
feat(remote): `push` publish merge event
when a merge commit is being pushed that merges a patch in a proposal
-rw-r--r--src/git.rs2
-rw-r--r--src/git_remote_helper.rs230
-rw-r--r--tests/git_remote_helper.rs152
3 files changed, 378 insertions, 6 deletions
diff --git a/src/git.rs b/src/git.rs
index c351a27..eebca29 100644
--- a/src/git.rs
+++ b/src/git.rs
@@ -787,7 +787,7 @@ pub fn oid_to_sha1(oid: &Oid) -> Sha1Hash {
787} 787}
788 788
789/// `Sha1Hash` to git2 `Oid` object 789/// `Sha1Hash` to git2 `Oid` object
790fn sha1_to_oid(hash: &Sha1Hash) -> Result<Oid> { 790pub fn sha1_to_oid(hash: &Sha1Hash) -> Result<Oid> {
791 Oid::from_bytes(hash.as_byte_array()).context("Sha1Hash bytes failed to produce a valid Oid") 791 Oid::from_bytes(hash.as_byte_array()).context("Sha1Hash bytes failed to produce a valid Oid")
792} 792}
793 793
diff --git a/src/git_remote_helper.rs b/src/git_remote_helper.rs
index 97db69a..a03c6cf 100644
--- a/src/git_remote_helper.rs
+++ b/src/git_remote_helper.rs
@@ -18,9 +18,9 @@ use client::{
18 consolidate_fetch_reports, get_events_from_cache, get_repo_ref_from_cache, 18 consolidate_fetch_reports, get_events_from_cache, get_repo_ref_from_cache,
19 get_state_from_cache, sign_event, Connect, STATE_KIND, 19 get_state_from_cache, sign_event, Connect, STATE_KIND,
20}; 20};
21use git::RepoActions; 21use git::{sha1_to_oid, RepoActions};
22use git2::{Oid, Repository}; 22use git2::{Oid, Repository};
23use nostr::nips::nip01::Coordinate; 23use nostr::nips::{nip01::Coordinate, nip10::Marker};
24use nostr_sdk::{ 24use nostr_sdk::{
25 hashes::sha1::Hash as Sha1Hash, Event, EventBuilder, EventId, Kind, PublicKey, Tag, Url, 25 hashes::sha1::Hash as Sha1Hash, Event, EventBuilder, EventId, Kind, PublicKey, Tag, Url,
26}; 26};
@@ -713,6 +713,18 @@ async fn push(
713 RepoState::build(repo_ref.identifier.clone(), new_state, &signer).await?; 713 RepoState::build(repo_ref.identifier.clone(), new_state, &signer).await?;
714 714
715 events.push(new_repo_state.event); 715 events.push(new_repo_state.event);
716
717 for event in get_merged_status_events(
718 repo_ref,
719 git_repo,
720 nostr_remote_url,
721 &signer,
722 &git_server_refspecs,
723 )
724 .await?
725 {
726 events.push(event);
727 }
716 } 728 }
717 729
718 let mut rejected_proposal_refspecs = vec![]; 730 let mut rejected_proposal_refspecs = vec![];
@@ -1115,6 +1127,220 @@ fn generate_updated_state(
1115 Ok(new_state) 1127 Ok(new_state)
1116} 1128}
1117 1129
1130async fn get_merged_status_events(
1131 repo_ref: &RepoRef,
1132 git_repo: &Repo,
1133 remote_nostr_url: &str,
1134 signer: &NostrSigner,
1135 refspecs_to_git_server: &Vec<String>,
1136) -> Result<Vec<Event>> {
1137 let mut events = vec![];
1138 for refspec in refspecs_to_git_server {
1139 let (from, to) = refspec_to_from_to(refspec)?;
1140 if to.eq("refs/heads/main") || to.eq("refs/heads/master") {
1141 let tip_of_pushed_branch = git_repo.get_commit_or_tip_of_reference(from)?;
1142 let Ok(tip_of_remote_branch) = git_repo.get_commit_or_tip_of_reference(
1143 &refspec_remote_ref_name(&git_repo.git_repo, refspec, remote_nostr_url)?,
1144 ) else {
1145 // branch not on remote
1146 continue;
1147 };
1148 let (ahead, _) =
1149 git_repo.get_commits_ahead_behind(&tip_of_remote_branch, &tip_of_pushed_branch)?;
1150 for commit_hash in ahead {
1151 let commit = git_repo.git_repo.find_commit(sha1_to_oid(&commit_hash)?)?;
1152 if commit.parent_count() > 1 {
1153 // merge commit
1154 for parent in commit.parents() {
1155 // lookup parent id
1156 let commit_events = get_events_from_cache(
1157 git_repo.get_path()?,
1158 vec![
1159 nostr::Filter::default()
1160 .kind(nostr::Kind::GitPatch)
1161 .reference(parent.id().to_string()),
1162 ],
1163 )
1164 .await?;
1165 if let Some(commit_event) = commit_events.iter().find(|e| {
1166 e.tags.iter().any(|t| {
1167 t.as_vec()[0].eq("commit")
1168 && t.as_vec()[1].eq(&parent.id().to_string())
1169 })
1170 }) {
1171 let (proposal_id, revision_id) =
1172 get_proposal_and_revision_root_from_patch(git_repo, commit_event)
1173 .await?;
1174 // TODO: write to terminal to tell user
1175 events.push(
1176 create_merge_status(
1177 signer,
1178 repo_ref,
1179 &get_event_from_cache_by_id(git_repo, &proposal_id).await?,
1180 &if let Some(revision_id) = revision_id {
1181 Some(
1182 get_event_from_cache_by_id(git_repo, &revision_id)
1183 .await?,
1184 )
1185 } else {
1186 None
1187 },
1188 &commit_hash,
1189 commit_event.id(),
1190 )
1191 .await?,
1192 );
1193 }
1194 }
1195 }
1196 }
1197 }
1198 }
1199 Ok(events)
1200}
1201
1202async fn get_event_from_cache_by_id(git_repo: &Repo, event_id: &EventId) -> Result<Event> {
1203 Ok(get_events_from_cache(
1204 git_repo.get_path()?,
1205 vec![nostr::Filter::default().id(*event_id)],
1206 )
1207 .await?
1208 .first()
1209 .context("cannot find event in cache")?
1210 .clone())
1211}
1212
1213async fn create_merge_status(
1214 signer: &NostrSigner,
1215 repo_ref: &RepoRef,
1216 proposal: &Event,
1217 revision: &Option<Event>,
1218 merge_commit: &Sha1Hash,
1219 merged_patch: EventId,
1220) -> Result<Event> {
1221 let mut public_keys = repo_ref
1222 .maintainers
1223 .iter()
1224 .copied()
1225 .collect::<HashSet<PublicKey>>();
1226 public_keys.insert(proposal.author());
1227 if let Some(revision) = revision {
1228 public_keys.insert(revision.author());
1229 }
1230 sign_event(
1231 EventBuilder::new(
1232 nostr::event::Kind::GitStatusApplied,
1233 String::new(),
1234 [
1235 vec![
1236 Tag::custom(
1237 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("alt")),
1238 vec!["git proposal merged / applied".to_string()],
1239 ),
1240 Tag::from_standardized(nostr::TagStandard::Event {
1241 event_id: proposal.id(),
1242 relay_url: repo_ref.relays.first().map(nostr::UncheckedUrl::new),
1243 marker: Some(Marker::Root),
1244 public_key: None,
1245 }),
1246 Tag::from_standardized(nostr::TagStandard::Event {
1247 event_id: merged_patch,
1248 relay_url: repo_ref.relays.first().map(nostr::UncheckedUrl::new),
1249 marker: Some(Marker::Mention),
1250 public_key: None,
1251 }),
1252 ],
1253 if let Some(revision) = revision {
1254 vec![Tag::from_standardized(nostr::TagStandard::Event {
1255 event_id: revision.id(),
1256 relay_url: repo_ref.relays.first().map(nostr::UncheckedUrl::new),
1257 marker: Some(Marker::Root),
1258 public_key: None,
1259 })]
1260 } else {
1261 vec![]
1262 },
1263 public_keys.iter().map(|pk| Tag::public_key(*pk)).collect(),
1264 repo_ref
1265 .coordinates()
1266 .iter()
1267 .map(|c| Tag::coordinate(c.clone()))
1268 .collect::<Vec<Tag>>(),
1269 vec![
1270 Tag::from_standardized(nostr::TagStandard::Reference(
1271 repo_ref.root_commit.to_string(),
1272 )),
1273 Tag::from_standardized(nostr::TagStandard::Reference(format!(
1274 "{merge_commit}"
1275 ))),
1276 Tag::custom(
1277 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("merge-commit-id")),
1278 vec![format!("{merge_commit}")],
1279 ),
1280 ],
1281 ]
1282 .concat(),
1283 ),
1284 signer,
1285 )
1286 .await
1287}
1288
1289async fn get_proposal_and_revision_root_from_patch(
1290 git_repo: &Repo,
1291 patch: &Event,
1292) -> Result<(EventId, Option<EventId>)> {
1293 let proposal_or_revision = if patch.tags.iter().any(|t| t.as_vec()[1].eq("root")) {
1294 patch.clone()
1295 } else {
1296 let proposal_or_revision_id = EventId::parse(
1297 if let Some(t) = patch.tags.iter().find(|t| t.is_root()) {
1298 t.clone()
1299 } else if let Some(t) = patch.tags.iter().find(|t| t.is_reply()) {
1300 t.clone()
1301 } else {
1302 Tag::event(patch.id())
1303 }
1304 .as_vec()[1]
1305 .clone(),
1306 )?;
1307
1308 get_events_from_cache(
1309 git_repo.get_path()?,
1310 vec![nostr::Filter::default().id(proposal_or_revision_id)],
1311 )
1312 .await?
1313 .first()
1314 .unwrap()
1315 .clone()
1316 };
1317
1318 if !proposal_or_revision.kind().eq(&Kind::GitPatch) {
1319 bail!("thread root is not a git patch");
1320 }
1321
1322 if proposal_or_revision
1323 .tags
1324 .iter()
1325 .any(|t| t.as_vec()[1].eq("revision-root"))
1326 {
1327 Ok((
1328 EventId::parse(
1329 proposal_or_revision
1330 .tags
1331 .iter()
1332 .find(|t| t.is_reply())
1333 .unwrap()
1334 .as_vec()[1]
1335 .clone(),
1336 )?,
1337 Some(proposal_or_revision.id()),
1338 ))
1339 } else {
1340 Ok((proposal_or_revision.id(), None))
1341 }
1342}
1343
1118fn update_remote_refs_pushed( 1344fn update_remote_refs_pushed(
1119 git_repo: &Repository, 1345 git_repo: &Repository,
1120 refspec: &str, 1346 refspec: &str,
diff --git a/tests/git_remote_helper.rs b/tests/git_remote_helper.rs
index 17138e4..98637e8 100644
--- a/tests/git_remote_helper.rs
+++ b/tests/git_remote_helper.rs
@@ -2,8 +2,9 @@ use std::{collections::HashSet, env::current_dir};
2 2
3use anyhow::{Context, Result}; 3use anyhow::{Context, Result};
4use futures::join; 4use futures::join;
5use git2::Oid;
5use nostr::nips::nip01::Coordinate; 6use nostr::nips::nip01::Coordinate;
6use nostr_sdk::{secp256k1::rand, Kind, ToBech32}; 7use nostr_sdk::{secp256k1::rand, Event, JsonUtil, Kind, ToBech32};
7use relay::Relay; 8use relay::Relay;
8use serial_test::serial; 9use serial_test::serial;
9use test_utils::{git::GitTestRepo, *}; 10use test_utils::{git::GitTestRepo, *};
@@ -41,6 +42,9 @@ fn set_git_nostr_login_config(test_repo: &GitTestRepo) -> Result<()> {
41 .context("cannot open git config")?; 42 .context("cannot open git config")?;
42 config.set_str("nostr.nsec", TEST_KEY_2_NSEC)?; 43 config.set_str("nostr.nsec", TEST_KEY_2_NSEC)?;
43 config.set_str("nostr.npub", TEST_KEY_2_NPUB)?; 44 config.set_str("nostr.npub", TEST_KEY_2_NPUB)?;
45 config.set_str("user.name", "test name")?;
46 config.set_str("user.email", "test@test.com")?;
47 config.set_bool("commit.gpgSign", false)?;
44 Ok(()) 48 Ok(())
45} 49}
46 50
@@ -808,8 +812,6 @@ mod fetch {
808 812
809mod push { 813mod push {
810 814
811 use nostr_sdk::Event;
812
813 use super::*; 815 use super::*;
814 816
815 #[tokio::test] 817 #[tokio::test]
@@ -1712,6 +1714,150 @@ mod push {
1712 1714
1713 #[tokio::test] 1715 #[tokio::test]
1714 #[serial] 1716 #[serial]
1717 async fn proposal_merge_commit_pushed_to_main_leads_to_status_event_issued() -> Result<()> {
1718 //
1719 let (events, source_git_repo) = prep_source_repo_and_events_including_proposals().await?;
1720 let source_path = source_git_repo.dir.to_str().unwrap().to_string();
1721
1722 let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
1723 Relay::new(8051, None, None),
1724 Relay::new(8052, None, None),
1725 Relay::new(8053, None, None),
1726 Relay::new(8055, None, None),
1727 Relay::new(8056, None, None),
1728 Relay::new(8057, None, None),
1729 );
1730 r51.events = events.clone();
1731 r55.events = events.clone();
1732
1733 let before = r55.events.iter().cloned().collect::<HashSet<Event>>();
1734
1735 let cli_tester_handle = std::thread::spawn(move || -> Result<(String, Oid)> {
1736 let branch_name = get_proposal_branch_name_from_events(&events, FEATURE_BRANCH_NAME_1)?;
1737
1738 let git_repo = clone_git_repo_with_nostr_url()?;
1739 git_repo.checkout_remote_branch(&branch_name)?;
1740 git_repo.checkout("refs/heads/main")?;
1741
1742 std::fs::write(git_repo.dir.join("new.md"), "some content")?;
1743 git_repo.stage_and_commit("new.md")?;
1744
1745 CliTester::new_git_with_remote_helper_from_dir(
1746 &git_repo.dir,
1747 ["merge", &branch_name, "-m", "proposal merge commit message"],
1748 )
1749 .expect_end_eventually_and_print()?;
1750
1751 let oid = git_repo.get_tip_of_local_branch("main")?;
1752
1753 let mut p = CliTester::new_git_with_remote_helper_from_dir(&git_repo.dir, ["push"]);
1754 cli_expect_nostr_fetch(&mut p)?;
1755 p.expect(format!("fetching refs list: {}...\r\n\r", source_path).as_str())?;
1756 p.expect(format!("To {}\r\n", get_nostr_remote_url()?).as_str())?;
1757 let output = p.expect_end_eventually()?;
1758
1759 for p in [51, 52, 53, 55, 56, 57] {
1760 relay::shutdown_relay(8000 + p)?;
1761 }
1762
1763 Ok((output, oid))
1764 });
1765 // launch relays
1766 let _ = join!(
1767 r51.listen_until_close(),
1768 r52.listen_until_close(),
1769 r53.listen_until_close(),
1770 r55.listen_until_close(),
1771 r56.listen_until_close(),
1772 r57.listen_until_close(),
1773 );
1774
1775 let (output, oid) = cli_tester_handle.join().unwrap()?;
1776
1777 assert_eq!(
1778 output,
1779 format!(" 431b84e..{} main -> main\r\n", &oid.to_string()[..7])
1780 );
1781
1782 let new_events = r55
1783 .events
1784 .iter()
1785 .cloned()
1786 .collect::<HashSet<Event>>()
1787 .difference(&before)
1788 .cloned()
1789 .collect::<Vec<Event>>();
1790
1791 assert_eq!(new_events.len(), 2, "{new_events:?}");
1792
1793 let proposal = r55
1794 .events
1795 .iter()
1796 .find(|e| {
1797 e.iter_tags()
1798 .find(|t| t.as_vec()[0].eq("branch-name"))
1799 .is_some_and(|t| t.as_vec()[1].eq(FEATURE_BRANCH_NAME_1))
1800 })
1801 .unwrap();
1802
1803 let merge_status = new_events
1804 .iter()
1805 .find(|e| e.kind().eq(&Kind::GitStatusApplied))
1806 .unwrap();
1807
1808 assert_eq!(
1809 oid.to_string(),
1810 merge_status
1811 .tags
1812 .iter()
1813 .find(|t| t.as_vec()[0].eq("merge-commit-id"))
1814 .unwrap()
1815 .as_vec()[1],
1816 "status sets correct merge-commit-id tag"
1817 );
1818
1819 let proposal_tip = r55
1820 .events
1821 .iter()
1822 .filter(|e| {
1823 e.iter_tags()
1824 .any(|t| t.as_vec()[1].eq(&proposal.id().to_string()))
1825 && e.kind().eq(&Kind::GitPatch)
1826 })
1827 .last()
1828 .unwrap();
1829
1830 assert_eq!(
1831 proposal_tip.id().to_string(),
1832 merge_status
1833 .tags
1834 .iter()
1835 .find(|t| t.as_vec().len().eq(&4) && t.as_vec()[3].eq("mention"))
1836 .unwrap()
1837 .as_vec()[1],
1838 "status mentions proposal tip event \r\nmerge status:\r\n{}\r\nproposal tip:\r\n{}",
1839 merge_status.as_json(),
1840 proposal_tip.as_json(),
1841 );
1842
1843 assert_eq!(
1844 proposal.id().to_string(),
1845 merge_status
1846 .tags
1847 .iter()
1848 .find(|t| t.is_root())
1849 .unwrap()
1850 .as_vec()[1],
1851 "status tags proposal id as root \r\nmerge status:\r\n{}\r\nproposal:\r\n{}",
1852 merge_status.as_json(),
1853 proposal.as_json(),
1854 );
1855
1856 Ok(())
1857 }
1858
1859 #[tokio::test]
1860 #[serial]
1715 async fn push_2_commits_to_existing_proposal() -> Result<()> { 1861 async fn push_2_commits_to_existing_proposal() -> Result<()> {
1716 let (events, source_git_repo) = prep_source_repo_and_events_including_proposals().await?; 1862 let (events, source_git_repo) = prep_source_repo_and_events_including_proposals().await?;
1717 let source_path = source_git_repo.dir.to_str().unwrap().to_string(); 1863 let source_path = source_git_repo.dir.to_str().unwrap().to_string();