upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/lib/git
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-02-18 14:48:20 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-02-18 14:48:20 +0000
commitfcff4541e1f36b6575596c353637b25aeae9bdcf (patch)
treed897ce824ca49a8ffef9f55f5f36777687573aab /src/lib/git
parente6bb9effa194fe63b5e969c090dbe6e93f13d312 (diff)
feat: handle missing optional patch tags for pr/ flow
- Add mbox_parser module to extract metadata from patch content - Extract author/committer from From: and Date: headers when tags missing - Extract commit message body as fallback for description tag - Implement best-guess parent commit logic using committer timestamps - Update patch_supports_commit_ids to accept mbox-parseable patches - Enable patches without optional tags to appear as pr/ branches
Diffstat (limited to 'src/lib/git')
-rw-r--r--src/lib/git/mod.rs184
1 files changed, 153 insertions, 31 deletions
diff --git a/src/lib/git/mod.rs b/src/lib/git/mod.rs
index 516d9e2..57e8403 100644
--- a/src/lib/git/mod.rs
+++ b/src/lib/git/mod.rs
@@ -62,6 +62,8 @@ pub trait RepoActions {
62 /// returns vector ["name", "email", "unixtime", "offset"] 62 /// returns vector ["name", "email", "unixtime", "offset"]
63 /// eg ["joe bloggs", "joe@pm.me", "12176","-300"] 63 /// eg ["joe bloggs", "joe@pm.me", "12176","-300"]
64 fn get_commit_comitter(&self, commit: &Sha1Hash) -> Result<Vec<String>>; 64 fn get_commit_comitter(&self, commit: &Sha1Hash) -> Result<Vec<String>>;
65 fn get_commit_committer_time(&self, commit: &Sha1Hash) -> Result<i64>;
66 fn find_best_guess_parent_commit(&self, patch_timestamp: i64) -> Result<Option<Sha1Hash>>;
65 fn get_commits_ahead_behind( 67 fn get_commits_ahead_behind(
66 &self, 68 &self,
67 base_commit: &Sha1Hash, 69 base_commit: &Sha1Hash,
@@ -339,6 +341,50 @@ impl RepoActions for Repo {
339 Ok(git_sig_to_tag_vec(&sig)) 341 Ok(git_sig_to_tag_vec(&sig))
340 } 342 }
341 343
344 fn get_commit_committer_time(&self, commit_hash: &Sha1Hash) -> Result<i64> {
345 let commit = self
346 .git_repo
347 .find_commit(sha1_to_oid(commit_hash)?)
348 .context(format!("could not find commit {commit_hash}"))?;
349 let time = commit.committer().when().seconds();
350 Ok(time)
351 }
352
353 fn find_best_guess_parent_commit(&self, patch_timestamp: i64) -> Result<Option<Sha1Hash>> {
354 let (main_branch_name, _) = self
355 .get_main_or_master_branch()
356 .context("failed to get main/master branch")?;
357
358 let mut revwalk = self
359 .git_repo
360 .revwalk()
361 .context("failed to create revwalk")?;
362
363 revwalk
364 .push_ref(&format!("refs/heads/{}", main_branch_name))
365 .context("failed to push main branch to revwalk")?;
366
367 let mut best_commit: Option<(i64, Sha1Hash)> = None;
368
369 for oid_result in revwalk {
370 let oid = oid_result.context("failed to get oid from revwalk")?;
371 let commit = self
372 .git_repo
373 .find_commit(oid)
374 .context("failed to find commit")?;
375
376 let committer_time = commit.committer().when().seconds();
377
378 if committer_time < patch_timestamp
379 && (best_commit.is_none() || committer_time > best_commit.as_ref().unwrap().0)
380 {
381 best_commit = Some((committer_time, oid_to_sha1(&oid)));
382 }
383 }
384
385 Ok(best_commit.map(|(_, sha1)| sha1))
386 }
387
342 fn get_refs(&self, commit: &Sha1Hash) -> Result<Vec<String>> { 388 fn get_refs(&self, commit: &Sha1Hash) -> Result<Vec<String>> {
343 Ok(self 389 Ok(self
344 .git_repo 390 .git_repo
@@ -542,16 +588,16 @@ impl RepoActions for Repo {
542 }) 588 })
543 .collect(); 589 .collect();
544 590
545 let parent_commit_id = tag_value( 591 let parent_commit_id = match patches_to_apply.last() {
546 if let Ok(last_patch) = patches_to_apply.last().context("no patches") { 592 Some(last_patch) => {
547 last_patch 593 crate::git_events::get_parent_commit_from_patch(last_patch, Some(self))?
548 } else { 594 }
595 None => {
549 self.checkout(branch_name) 596 self.checkout(branch_name)
550 .context("no patches and so failed to create a proposal branch")?; 597 .context("no patches and so failed to create a proposal branch")?;
551 return Ok(vec![]); 598 return Ok(vec![]);
552 }, 599 }
553 "parent-commit", 600 };
554 )?;
555 601
556 // check patches can be applied 602 // check patches can be applied
557 if !self.does_commit_exist(&parent_commit_id)? { 603 if !self.does_commit_exist(&parent_commit_id)? {
@@ -590,8 +636,23 @@ impl RepoActions for Repo {
590 636
591 let parent_commit_id = if let Some(commit_id) = parent_commit_id_override.clone() { 637 let parent_commit_id = if let Some(commit_id) = parent_commit_id_override.clone() {
592 commit_id 638 commit_id
639 } else if let Ok(parent) = tag_value(patch, "parent-commit") {
640 parent
593 } else { 641 } else {
594 tag_value(patch, "parent-commit")? 642 let metadata = crate::mbox_parser::parse_mbox_patch(&patch.content)
643 .context("failed to parse patch for timestamp")?;
644 let timestamp = metadata.committer_timestamp.unwrap_or(metadata.author_timestamp);
645
646 let best_guess = self
647 .find_best_guess_parent_commit(timestamp)
648 .context("failed to find best guess parent commit")?;
649
650 match best_guess {
651 Some(sha1) => sha1.to_string(),
652 None => bail!(
653 "no parent-commit tag and could not determine best guess parent from patch timestamp"
654 ),
655 }
595 }; 656 };
596 657
597 let parent_commit = self 658 let parent_commit = self
@@ -623,10 +684,15 @@ impl RepoActions for Repo {
623 None 684 None
624 }; 685 };
625 686
687 let author_data = extract_signature_data_with_fallback(&patch.tags, "author", &patch.content)?;
688 let committer_data = extract_signature_data_with_fallback(&patch.tags, "committer", &patch.content)?;
689 let author_sig = author_data.to_signature()?;
690 let committer_sig = committer_data.to_signature()?;
691
626 let commit_buff = self.git_repo.commit_create_buffer( 692 let commit_buff = self.git_repo.commit_create_buffer(
627 &extract_sig_from_patch_tags(&patch.tags, "author")?, 693 &author_sig,
628 &extract_sig_from_patch_tags(&patch.tags, "committer")?, 694 &committer_sig,
629 tag_value(patch, "description")?.as_str(), 695 extract_description_from_patch(patch)?.as_str(),
630 &tree, 696 &tree,
631 &[&parent_commit], 697 &[&parent_commit],
632 )?; 698 )?;
@@ -897,7 +963,14 @@ fn git_sig_to_tag_vec(sig: &git2::Signature) -> Vec<String> {
897 ] 963 ]
898} 964}
899 965
900fn extract_sig_from_patch_tags<'a>(tags: &'a Tags, tag_name: &str) -> Result<git2::Signature<'a>> { 966struct SignatureData {
967 name: String,
968 email: String,
969 timestamp: i64,
970 offset_minutes: i32,
971}
972
973fn extract_signature_data_from_tags(tags: &Tags, tag_name: &str) -> Result<SignatureData> {
901 let v = tags 974 let v = tags
902 .iter() 975 .iter()
903 .find(|t| !t.as_slice().is_empty() && t.as_slice()[0].eq(tag_name)) 976 .find(|t| !t.as_slice().is_empty() && t.as_slice()[0].eq(tag_name))
@@ -906,16 +979,64 @@ fn extract_sig_from_patch_tags<'a>(tags: &'a Tags, tag_name: &str) -> Result<git
906 if v.len() != 5 { 979 if v.len() != 5 {
907 bail!("tag '{tag_name}' is incorrectly formatted") 980 bail!("tag '{tag_name}' is incorrectly formatted")
908 } 981 }
909 git2::Signature::new( 982 Ok(SignatureData {
910 v[1].as_str(), 983 name: v[1].clone(),
911 v[2].as_str(), 984 email: v[2].clone(),
912 &git2::Time::new( 985 timestamp: v[3].parse().context("tag time is incorrectly formatted")?,
913 v[3].parse().context("tag time is incorrectly formatted")?, 986 offset_minutes: v[4].parse().context("tag time offset is incorrectly formatted")?,
914 v[4].parse() 987 })
915 .context("tag time offset is incorrectly formatted")?, 988}
916 ), 989
917 ) 990fn extract_signature_data_with_fallback(
918 .context("failed to create git signature") 991 tags: &Tags,
992 tag_name: &str,
993 patch_content: &str,
994) -> Result<SignatureData> {
995 if let Ok(data) = extract_signature_data_from_tags(tags, tag_name) {
996 return Ok(data);
997 }
998
999 let metadata = crate::mbox_parser::parse_mbox_patch(patch_content)
1000 .context("failed to parse patch content for fallback metadata")?;
1001
1002 if tag_name == "author" {
1003 Ok(SignatureData {
1004 name: metadata.author_name,
1005 email: metadata.author_email,
1006 timestamp: metadata.author_timestamp,
1007 offset_minutes: metadata.author_offset_minutes,
1008 })
1009 } else if tag_name == "committer" {
1010 let timestamp = metadata.committer_timestamp.unwrap_or(metadata.author_timestamp);
1011 Ok(SignatureData {
1012 name: metadata.author_name,
1013 email: metadata.author_email,
1014 timestamp,
1015 offset_minutes: metadata.author_offset_minutes,
1016 })
1017 } else {
1018 bail!("unknown tag name for signature extraction: {}", tag_name)
1019 }
1020}
1021
1022impl SignatureData {
1023 fn to_signature(&self) -> Result<git2::Signature<'_>> {
1024 git2::Signature::new(
1025 &self.name,
1026 &self.email,
1027 &git2::Time::new(self.timestamp, self.offset_minutes),
1028 )
1029 .context("failed to create git signature")
1030 }
1031}
1032
1033fn extract_description_from_patch(patch: &nostr::Event) -> Result<String> {
1034 if let Ok(desc) = tag_value(patch, "description") {
1035 return Ok(desc);
1036 }
1037
1038 crate::mbox_parser::extract_description_from_patch(&patch.content)
1039 .context("failed to extract description from patch content")
919} 1040}
920 1041
921pub fn get_git_config_item(git_repo: &Option<&Repo>, item: &str) -> Result<Option<String>> { 1042pub fn get_git_config_item(git_repo: &Option<&Repo>, item: &str) -> Result<Option<String>> {
@@ -1182,19 +1303,20 @@ mod tests {
1182 } 1303 }
1183 } 1304 }
1184 1305
1185 mod extract_sig_from_patch_tags { 1306 mod extract_signature_data_from_tags {
1186 use super::*; 1307 use super::*;
1187 1308
1188 fn test(time: git2::Time) -> Result<()> { 1309 fn test(time: git2::Time) -> Result<()> {
1310 let data = extract_signature_data_from_tags(
1311 &Tags::from_list(vec![nostr::Tag::custom(
1312 nostr::TagKind::Custom("author".to_string().into()),
1313 prep(&time)?,
1314 )]),
1315 "author",
1316 )?;
1317 let sig = data.to_signature()?;
1189 assert_eq!( 1318 assert_eq!(
1190 extract_sig_from_patch_tags( 1319 sig.to_string(),
1191 &Tags::from_list(vec![nostr::Tag::custom(
1192 nostr::TagKind::Custom("author".to_string().into()),
1193 prep(&time)?,
1194 )]),
1195 "author",
1196 )?
1197 .to_string(),
1198 git2::Signature::new(NAME, EMAIL, &time)?.to_string(), 1320 git2::Signature::new(NAME, EMAIL, &time)?.to_string(),
1199 ); 1321 );
1200 Ok(()) 1322 Ok(())