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-12-06 22:16:44 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2024-12-06 22:16:44 +0000
commitf0d0e1ba1cba11d3a98a5ab0c7f1dc72b6bc4e17 (patch)
treea7f459091dd2fa6da7f3805a864dbc530f422e24
parentcabbdea4ae39a57cbff10d24084a888ca9948083 (diff)
feat(push): send fast-forward merge status event
when a proposal was merged using fast-forward status rather than by creating a three way merge commit. if there are multiple revisions, the first one that contains merged proposal tip will be referenced. if there are multiple proposals that contain one of the commits, they will all be marked as merged. the nip needs to be updated as there is no single `merge-commit-id` so that tag needs to support multiple values
-rw-r--r--src/bin/git_remote_nostr/push.rs249
-rw-r--r--tests/git_remote_nostr/push.rs193
2 files changed, 369 insertions, 73 deletions
diff --git a/src/bin/git_remote_nostr/push.rs b/src/bin/git_remote_nostr/push.rs
index dde4ab0..40e9584 100644
--- a/src/bin/git_remote_nostr/push.rs
+++ b/src/bin/git_remote_nostr/push.rs
@@ -25,7 +25,7 @@ use ngit::{
25 nostr_url::{CloneUrl, NostrUrlDecoded}, 25 nostr_url::{CloneUrl, NostrUrlDecoded},
26 oid_to_shorthand_string, 26 oid_to_shorthand_string,
27 }, 27 },
28 git_events::{self, get_event_root}, 28 git_events::{self, event_to_cover_letter, get_event_root},
29 login::{self, get_curent_user}, 29 login::{self, get_curent_user},
30 repo_ref::{self, get_repo_config_from_yaml}, 30 repo_ref::{self, get_repo_config_from_yaml},
31 repo_state, 31 repo_state,
@@ -965,72 +965,181 @@ async fn get_merged_status_events(
965 }; 965 };
966 let (ahead, _) = 966 let (ahead, _) =
967 git_repo.get_commits_ahead_behind(&tip_of_remote_branch, &tip_of_pushed_branch)?; 967 git_repo.get_commits_ahead_behind(&tip_of_remote_branch, &tip_of_pushed_branch)?;
968 for commit_hash in ahead {
969 let commit = git_repo.git_repo.find_commit(sha1_to_oid(&commit_hash)?)?;
970 if commit.parent_count() > 1 {
971 // merge commit
972 for parent in commit.parents() {
973 // lookup parent id
974 let commit_events = get_events_from_local_cache(
975 git_repo.get_path()?,
976 vec![
977 nostr::Filter::default()
978 .kind(nostr::Kind::GitPatch)
979 .reference(parent.id().to_string()),
980 ],
981 )
982 .await?;
983 if let Some(commit_event) = commit_events.iter().find(|e| {
984 e.tags.iter().any(|t| {
985 t.as_slice()[0].eq("commit")
986 && t.as_slice()[1].eq(&parent.id().to_string())
987 })
988 }) {
989 let (proposal_id, revision_id) =
990 get_proposal_and_revision_root_from_patch(git_repo, commit_event)
991 .await?;
992 term.write_line(
993 format!(
994 "merge commit {}: create nostr proposal status event",
995 &commit.id().to_string()[..7],
996 )
997 .as_str(),
998 )?;
999 968
1000 events.push( 969 let commit_events = get_events_from_local_cache(
1001 create_merge_status( 970 git_repo.get_path()?,
1002 signer, 971 vec![
1003 repo_ref, 972 nostr::Filter::default().kind(nostr::Kind::GitPatch),
1004 &get_event_from_cache_by_id(git_repo, &proposal_id).await?, 973 // TODO: limit by repo_ref
1005 &if let Some(revision_id) = revision_id { 974 ],
1006 Some( 975 )
1007 get_event_from_cache_by_id(git_repo, &revision_id) 976 .await?;
1008 .await?, 977
1009 ) 978 let merged_proposals_info =
1010 } else { 979 get_merged_proposals_info(git_repo, &ahead, &commit_events).await?;
1011 None 980
1012 }, 981 for event in
1013 &commit_hash, 982 create_merge_events(term, git_repo, repo_ref, signer, &merged_proposals_info)
1014 commit_event.id, 983 .await?
1015 ) 984 {
1016 .await?, 985 events.push(event);
1017 ); 986 }
987 }
988 }
989 Ok(events)
990}
991
992/// (`proposal_id`, `revision_id`)
993type MergedProposalsInfo =
994 HashMap<EventId, (Option<EventId>, HashMap<Sha1Hash, MergedPRCommitType>)>;
995
996async fn get_merged_proposals_info(
997 git_repo: &Repo,
998 ahead: &Vec<Sha1Hash>,
999 available_patches: &[Event],
1000) -> Result<MergedProposalsInfo> {
1001 let mut proposals: MergedProposalsInfo = HashMap::new();
1002
1003 for commit_hash in ahead {
1004 let commit = git_repo.git_repo.find_commit(sha1_to_oid(commit_hash)?)?;
1005 // three-way merge - just to set merge commit id as the merged branch commits
1006 // are in ahead
1007 if commit.parent_count() > 1 {
1008 for parent in commit.parents() {
1009 for patch_event in available_patches
1010 .iter()
1011 .filter(|e| {
1012 e.tags.iter().any(|t| {
1013 t.as_slice()[0].eq("commit")
1014 && t.as_slice()[1].eq(&parent.id().to_string())
1015 })
1016 })
1017 .collect::<Vec<&Event>>()
1018 {
1019 if let Ok((proposal_id, revision_id)) =
1020 get_proposal_and_revision_root_from_patch(git_repo, patch_event).await
1021 {
1022 let (entry_revision_id, merged_patches) =
1023 proposals.entry(proposal_id).or_default();
1024 if entry_revision_id == &revision_id {
1025 merged_patches.insert(*commit_hash, MergedPRCommitType::MergeCommit);
1018 } 1026 }
1019 } 1027 }
1020 } 1028 }
1021 } 1029 }
1030 } else {
1031 // three way merge or fast forward merge commits
1032 // note: ahead included commits of three-way merged branches
1033 for patch_event in available_patches
1034 .iter()
1035 .filter(|e| {
1036 e.tags.iter().any(|t| {
1037 t.as_slice()[0].eq("commit") && t.as_slice()[1].eq(&commit_hash.to_string())
1038 })
1039 })
1040 .collect::<Vec<&Event>>()
1041 {
1042 if let Ok((proposal_id, revision_id)) =
1043 get_proposal_and_revision_root_from_patch(git_repo, patch_event).await
1044 {
1045 let (entry_revision_id, merged_patches) =
1046 proposals.entry(proposal_id).or_default();
1047 // ignore revisions without all the merged commits
1048 if entry_revision_id == &revision_id {
1049 merged_patches.insert(
1050 *commit_hash,
1051 MergedPRCommitType::PatchCommit {
1052 event_id: patch_event.id,
1053 },
1054 );
1055 }
1056 }
1057 }
1058 }
1059 }
1060 Ok(proposals)
1061}
1062
1063async fn create_merge_events(
1064 term: &console::Term,
1065 git_repo: &Repo,
1066 repo_ref: &RepoRef,
1067 signer: &Arc<dyn NostrSigner>,
1068 merged_proposals_info: &MergedProposalsInfo,
1069) -> Result<Vec<Event>> {
1070 let mut events = vec![];
1071 for (proposal_id, (revision_id, merged_patches)) in merged_proposals_info {
1072 let proposal = get_event_from_cache_by_id(git_repo, proposal_id).await?;
1073
1074 if merged_patches
1075 .values()
1076 .any(|m| *m == MergedPRCommitType::MergeCommit)
1077 {
1078 term.write_line(
1079 format!(
1080 "merge commit {}: create nostr proposal status event",
1081 &merged_patches.keys().next().unwrap().to_string()[..7],
1082 )
1083 .as_str(),
1084 )?;
1085 } else {
1086 term.write_line(
1087 format!(
1088 "fast-forward merge: create nostr proposal status event for {}",
1089 event_to_cover_letter(&proposal)?.get_branch_name()?,
1090 )
1091 .as_str(),
1092 )?;
1022 } 1093 }
1094 events.push(
1095 create_merge_status(
1096 signer,
1097 repo_ref,
1098 &proposal,
1099 &if let Some(revision_id) = revision_id {
1100 Some(get_event_from_cache_by_id(git_repo, revision_id).await?)
1101 } else {
1102 None
1103 },
1104 if merged_patches
1105 .values()
1106 .any(|m| m == &MergedPRCommitType::MergeCommit)
1107 {
1108 vec![*merged_patches.keys().next().unwrap()]
1109 } else {
1110 let mut t: Vec<Sha1Hash> = merged_patches.keys().copied().collect();
1111 t.reverse();
1112 t
1113 },
1114 merged_patches
1115 .values()
1116 .filter_map(|m| match m {
1117 MergedPRCommitType::MergeCommit => None,
1118 MergedPRCommitType::PatchApplied { event_id }
1119 | MergedPRCommitType::PatchCommit { event_id } => Some(*event_id),
1120 })
1121 .collect(),
1122 )
1123 .await?,
1124 );
1023 } 1125 }
1024 Ok(events) 1126 Ok(events)
1025} 1127}
1026 1128
1129#[derive(PartialEq)]
1130enum MergedPRCommitType {
1131 MergeCommit,
1132 PatchCommit { event_id: EventId },
1133 PatchApplied { event_id: EventId },
1134}
1135
1027async fn create_merge_status( 1136async fn create_merge_status(
1028 signer: &Arc<dyn NostrSigner>, 1137 signer: &Arc<dyn NostrSigner>,
1029 repo_ref: &RepoRef, 1138 repo_ref: &RepoRef,
1030 proposal: &Event, 1139 proposal: &Event,
1031 revision: &Option<Event>, 1140 revision: &Option<Event>,
1032 merge_commit: &Sha1Hash, 1141 merge_commits: Vec<Sha1Hash>,
1033 merged_patch: EventId, 1142 merged_patches: Vec<EventId>,
1034) -> Result<Event> { 1143) -> Result<Event> {
1035 let mut public_keys = repo_ref 1144 let mut public_keys = repo_ref
1036 .maintainers 1145 .maintainers
@@ -1056,14 +1165,20 @@ async fn create_merge_status(
1056 public_key: None, 1165 public_key: None,
1057 uppercase: false, 1166 uppercase: false,
1058 }), 1167 }),
1059 Tag::from_standardized(nostr::TagStandard::Event {
1060 event_id: merged_patch,
1061 relay_url: repo_ref.relays.first().cloned(),
1062 marker: Some(Marker::Mention),
1063 public_key: None,
1064 uppercase: false,
1065 }),
1066 ], 1168 ],
1169 // Tags for merged patches
1170 merged_patches
1171 .iter()
1172 .map(|merged_patch| {
1173 Tag::from_standardized(nostr::TagStandard::Event {
1174 event_id: *merged_patch,
1175 relay_url: repo_ref.relays.first().cloned(),
1176 marker: Some(Marker::Mention),
1177 public_key: None,
1178 uppercase: false,
1179 })
1180 })
1181 .collect::<Vec<Tag>>(),
1067 if let Some(revision) = revision { 1182 if let Some(revision) = revision {
1068 vec![Tag::from_standardized(nostr::TagStandard::Event { 1183 vec![Tag::from_standardized(nostr::TagStandard::Event {
1069 event_id: revision.id, 1184 event_id: revision.id,
@@ -1085,14 +1200,22 @@ async fn create_merge_status(
1085 Tag::from_standardized(nostr::TagStandard::Reference( 1200 Tag::from_standardized(nostr::TagStandard::Reference(
1086 repo_ref.root_commit.to_string(), 1201 repo_ref.root_commit.to_string(),
1087 )), 1202 )),
1088 Tag::from_standardized(nostr::TagStandard::Reference(format!(
1089 "{merge_commit}"
1090 ))),
1091 Tag::custom( 1203 Tag::custom(
1092 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("merge-commit-id")), 1204 nostr::TagKind::Custom(std::borrow::Cow::Borrowed("merge-commit-id")),
1093 vec![format!("{merge_commit}")], 1205 merge_commits
1206 .iter()
1207 .map(|merge_commit| format!("{merge_commit}"))
1208 .collect::<Vec<String>>(),
1094 ), 1209 ),
1095 ], 1210 ],
1211 merge_commits
1212 .iter()
1213 .map(|merge_commit| {
1214 Tag::from_standardized(nostr::TagStandard::Reference(format!(
1215 "{merge_commit}"
1216 )))
1217 })
1218 .collect::<Vec<Tag>>(),
1096 ] 1219 ]
1097 .concat(), 1220 .concat(),
1098 ), 1221 ),
diff --git a/tests/git_remote_nostr/push.rs b/tests/git_remote_nostr/push.rs
index b93475c..4dc2c1d 100644
--- a/tests/git_remote_nostr/push.rs
+++ b/tests/git_remote_nostr/push.rs
@@ -894,7 +894,8 @@ async fn pushes_to_all_git_servers_listed_and_ok_printed() -> Result<()> {
894 894
895#[tokio::test] 895#[tokio::test]
896#[serial] 896#[serial]
897async fn proposal_merge_commit_pushed_to_main_leads_to_status_event_issued() -> Result<()> { 897async fn proposal_three_way_merge_commit_pushed_to_main_leads_to_status_event_issued() -> Result<()>
898{
898 // 899 //
899 let (events, source_git_repo) = prep_source_repo_and_events_including_proposals().await?; 900 let (events, source_git_repo) = prep_source_repo_and_events_including_proposals().await?;
900 let source_path = source_git_repo.dir.to_str().unwrap().to_string(); 901 let source_path = source_git_repo.dir.to_str().unwrap().to_string();
@@ -958,11 +959,14 @@ async fn proposal_merge_commit_pushed_to_main_leads_to_status_event_issued() ->
958 r57.listen_until_close(), 959 r57.listen_until_close(),
959 ); 960 );
960 961
961 let (output, oid) = cli_tester_handle.join().unwrap()?; 962 let (output, merge_oid) = cli_tester_handle.join().unwrap()?;
962 963
963 assert_eq!( 964 assert_eq!(
964 output, 965 output,
965 format!(" 431b84e..{} main -> main\r\n", &oid.to_string()[..7]) 966 format!(
967 " 431b84e..{} main -> main\r\n",
968 &merge_oid.to_string()[..7]
969 )
966 ); 970 );
967 971
968 let new_events = r55 972 let new_events = r55
@@ -976,7 +980,7 @@ async fn proposal_merge_commit_pushed_to_main_leads_to_status_event_issued() ->
976 980
977 assert_eq!(new_events.len(), 2, "{new_events:?}"); 981 assert_eq!(new_events.len(), 2, "{new_events:?}");
978 982
979 let proposal = r55 983 let proposal_cover_letter_event = r55
980 .events 984 .events
981 .iter() 985 .iter()
982 .find(|e| { 986 .find(|e| {
@@ -993,14 +997,15 @@ async fn proposal_merge_commit_pushed_to_main_leads_to_status_event_issued() ->
993 .unwrap(); 997 .unwrap();
994 998
995 assert_eq!( 999 assert_eq!(
996 oid.to_string(), 1000 vec!["merge-commit-id".to_string(), merge_oid.to_string()],
997 merge_status 1001 merge_status
998 .tags 1002 .tags
999 .iter() 1003 .iter()
1000 .find(|t| t.as_slice()[0].eq("merge-commit-id")) 1004 .find(|t| t.as_slice()[0].eq("merge-commit-id"))
1001 .unwrap() 1005 .unwrap()
1002 .as_slice()[1], 1006 .clone()
1003 "status sets correct merge-commit-id tag" 1007 .to_vec(),
1008 "status sets correct merge-commit-id tag {merge_status:?}"
1004 ); 1009 );
1005 1010
1006 let proposal_tip = r55 1011 let proposal_tip = r55
@@ -1009,7 +1014,7 @@ async fn proposal_merge_commit_pushed_to_main_leads_to_status_event_issued() ->
1009 .filter(|e| { 1014 .filter(|e| {
1010 e.tags 1015 e.tags
1011 .iter() 1016 .iter()
1012 .any(|t| t.as_slice()[1].eq(&proposal.id.to_string())) 1017 .any(|t| t.as_slice()[1].eq(&proposal_cover_letter_event.id.to_string()))
1013 && e.kind.eq(&Kind::GitPatch) 1018 && e.kind.eq(&Kind::GitPatch)
1014 }) 1019 })
1015 .last() 1020 .last()
@@ -1029,7 +1034,175 @@ async fn proposal_merge_commit_pushed_to_main_leads_to_status_event_issued() ->
1029 ); 1034 );
1030 1035
1031 assert_eq!( 1036 assert_eq!(
1032 proposal.id.to_string(), 1037 proposal_cover_letter_event.id.to_string(),
1038 merge_status
1039 .tags
1040 .iter()
1041 .find(|t| t.is_root())
1042 .unwrap()
1043 .as_slice()[1],
1044 "status tags proposal id as root \r\nmerge status:\r\n{}\r\nproposal:\r\n{}",
1045 merge_status.as_json(),
1046 proposal_cover_letter_event.as_json(),
1047 );
1048
1049 Ok(())
1050}
1051
1052#[tokio::test]
1053#[serial]
1054async fn proposal_fast_forward_merge_commits_pushed_to_main_leads_to_status_event_issued()
1055-> Result<()> {
1056 //
1057 let (events, source_git_repo) = prep_source_repo_and_events_including_proposals().await?;
1058 let source_path = source_git_repo.dir.to_str().unwrap().to_string();
1059
1060 let (mut r51, mut r52, mut r53, mut r55, mut r56, mut r57) = (
1061 Relay::new(8051, None, None),
1062 Relay::new(8052, None, None),
1063 Relay::new(8053, None, None),
1064 Relay::new(8055, None, None),
1065 Relay::new(8056, None, None),
1066 Relay::new(8057, None, None),
1067 );
1068 r51.events = events.clone();
1069 r55.events = events.clone();
1070
1071 #[allow(clippy::mutable_key_type)]
1072 let before = r55.events.iter().cloned().collect::<HashSet<Event>>();
1073
1074 let cli_tester_handle = std::thread::spawn(move || -> Result<(String, Oid)> {
1075 let branch_name = get_proposal_branch_name_from_events(&events, FEATURE_BRANCH_NAME_1)?;
1076
1077 let git_repo = clone_git_repo_with_nostr_url()?;
1078 git_repo.checkout_remote_branch(&branch_name)?;
1079 git_repo.checkout("refs/heads/main")?;
1080
1081 CliTester::new_git_with_remote_helper_from_dir(
1082 &git_repo.dir,
1083 ["merge", &branch_name, "-m", "proposal merge commit message"],
1084 )
1085 .expect_end_eventually_and_print()?;
1086
1087 let oid = git_repo.get_tip_of_local_branch("main")?;
1088
1089 let mut p = CliTester::new_git_with_remote_helper_from_dir(&git_repo.dir, ["push"]);
1090 cli_expect_nostr_fetch(&mut p)?;
1091 p.expect(format!("fetching {} ref list over filesystem...\r\n", source_path).as_str())?;
1092 p.expect("list: connecting...\r\n")?;
1093 p.expect_eventually(format!(
1094 "fast-forward merge: create nostr proposal status event for {branch_name}\r\n"
1095 ))?;
1096 // status updates printed here
1097 p.expect_eventually(format!("To {}\r\n", get_nostr_remote_url()?).as_str())?;
1098 let output = p.expect_end_eventually()?;
1099
1100 for p in [51, 52, 53, 55, 56, 57] {
1101 relay::shutdown_relay(8000 + p)?;
1102 }
1103
1104 Ok((output, oid))
1105 });
1106 // launch relays
1107 let _ = join!(
1108 r51.listen_until_close(),
1109 r52.listen_until_close(),
1110 r53.listen_until_close(),
1111 r55.listen_until_close(),
1112 r56.listen_until_close(),
1113 r57.listen_until_close(),
1114 );
1115
1116 let (output, tip_oid) = cli_tester_handle.join().unwrap()?;
1117
1118 assert_eq!(
1119 output,
1120 format!(
1121 " 431b84e..{} main -> main\r\n",
1122 &tip_oid.to_string()[..7]
1123 )
1124 );
1125
1126 let new_events = r55
1127 .events
1128 .iter()
1129 .cloned()
1130 .collect::<HashSet<Event>>()
1131 .difference(&before)
1132 .cloned()
1133 .collect::<Vec<Event>>();
1134
1135 assert_eq!(new_events.len(), 2, "{new_events:?}");
1136
1137 let proposal_cover_letter_event = r55
1138 .events
1139 .iter()
1140 .find(|e| {
1141 e.tags
1142 .iter()
1143 .find(|t| t.as_slice()[0].eq("branch-name"))
1144 .is_some_and(|t| t.as_slice()[1].eq(FEATURE_BRANCH_NAME_1))
1145 })
1146 .unwrap();
1147
1148 let proposal_patches: Vec<&Event> = r55
1149 .events
1150 .iter()
1151 .filter(|e| {
1152 e.kind == Kind::GitPatch
1153 && e.tags
1154 .iter()
1155 .any(|t| t.as_slice()[1].eq(&proposal_cover_letter_event.id.to_string()))
1156 })
1157 .collect();
1158
1159 let merge_status = new_events
1160 .iter()
1161 .find(|e| e.kind.eq(&Kind::GitStatusApplied))
1162 .unwrap();
1163 // println!("{:?}", proposal_cover_letter_event);
1164 // println!("merge status");
1165 // println!("{:?}", merge_status);
1166
1167 let patch_commit_ids = proposal_patches
1168 .iter()
1169 .map(|e| {
1170 e.tags
1171 .iter()
1172 .find(|t| t.as_slice()[0].eq("commit"))
1173 .unwrap()
1174 .as_slice()[1]
1175 .to_string()
1176 })
1177 .collect::<Vec<String>>();
1178 assert_eq!(
1179 [vec!["merge-commit-id".to_string()], patch_commit_ids].concat(),
1180 merge_status
1181 .tags
1182 .iter()
1183 .find(|t| t.as_slice()[0].eq("merge-commit-id"))
1184 .unwrap()
1185 .clone()
1186 .to_vec(),
1187 "status sets correct merge-commit-id tag {merge_status:?}"
1188 );
1189
1190 for patch_id in proposal_patches
1191 .iter()
1192 .map(|e| e.id.to_string())
1193 .collect::<Vec<String>>()
1194 {
1195 assert!(
1196 merge_status.tags.iter().any(|t| t.as_slice().len().eq(&4)
1197 && t.as_slice()[1] == patch_id
1198 && t.as_slice()[3].eq("mention")),
1199 "merge status doesnt mention proposal patch {patch_id} \r\nmerge status:\r\n{}",
1200 merge_status.as_json(),
1201 );
1202 }
1203
1204 assert_eq!(
1205 proposal_cover_letter_event.id.to_string(),
1033 merge_status 1206 merge_status
1034 .tags 1207 .tags
1035 .iter() 1208 .iter()
@@ -1038,7 +1211,7 @@ async fn proposal_merge_commit_pushed_to_main_leads_to_status_event_issued() ->
1038 .as_slice()[1], 1211 .as_slice()[1],
1039 "status tags proposal id as root \r\nmerge status:\r\n{}\r\nproposal:\r\n{}", 1212 "status tags proposal id as root \r\nmerge status:\r\n{}\r\nproposal:\r\n{}",
1040 merge_status.as_json(), 1213 merge_status.as_json(),
1041 proposal.as_json(), 1214 proposal_cover_letter_event.as_json(),
1042 ); 1215 );
1043 1216
1044 Ok(()) 1217 Ok(())