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/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(())