upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/lib/git/mod.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-02-18 21:04:59 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-02-18 21:04:59 +0000
commit73d829b916d87626f33ea2adead0c48f1d9d737d (patch)
treeb3adc223df90184e430ac6cb153b9a6e986f8097 /src/lib/git/mod.rs
parent061589cd88d0480dc7cb0b9eb19a3910293ceb56 (diff)
fix: harden apply_patch_chain when optional patch tags absent
- Replace .unwrap() calls in filter closure with pattern-let guards so a missing/invalid mbox commit id conservatively includes the patch - Use the OID returned by create_commit_from_patch as branch tip instead of the tag commit id, which may differ for GPG-signed commits - Add module-level doc comment to mbox_parser explaining design rationale and known limitations around the mbox envelope SHA1
Diffstat (limited to 'src/lib/git/mod.rs')
-rw-r--r--src/lib/git/mod.rs72
1 files changed, 51 insertions, 21 deletions
diff --git a/src/lib/git/mod.rs b/src/lib/git/mod.rs
index 57e8403..9f86b5f 100644
--- a/src/lib/git/mod.rs
+++ b/src/lib/git/mod.rs
@@ -576,15 +576,23 @@ impl RepoActions for Repo {
576 let mut patches_to_apply: Vec<nostr::Event> = patch_and_ancestors 576 let mut patches_to_apply: Vec<nostr::Event> = patch_and_ancestors
577 .into_iter() 577 .into_iter()
578 .filter(|e| { 578 .filter(|e| {
579 let commit_id = get_commit_id_from_patch(e).unwrap(); 579 // When the commit tag is absent, the commit id from the mbox envelope
580 if let Ok(branch_tip) = branch_tip_result { 580 // may not match the reconstructed commit's OID (e.g. GPG-signed commits).
581 !branch_tip.to_string().eq(&commit_id) 581 // In that case we conservatively include the patch for application —
582 && !self 582 // create_commit_from_patch handles idempotency via content-addressed storage.
583 .ancestor_of(&branch_tip, &str_to_sha1(&commit_id).unwrap()) 583 let Ok(commit_id) = get_commit_id_from_patch(e) else {
584 .unwrap() 584 return true;
585 } else { 585 };
586 true 586 let Ok(branch_tip) = branch_tip_result else {
587 } 587 return true;
588 };
589 let Ok(commit_sha1) = str_to_sha1(&commit_id) else {
590 // Commit id is not a valid SHA1 (e.g. placeholder from mbox envelope).
591 // Include conservatively.
592 return true;
593 };
594 !branch_tip.to_string().eq(&commit_id)
595 && !self.ancestor_of(&branch_tip, &commit_sha1).unwrap_or(false)
588 }) 596 })
589 .collect(); 597 .collect();
590 598
@@ -612,12 +620,26 @@ impl RepoActions for Repo {
612 patches_to_apply.reverse(); 620 patches_to_apply.reverse();
613 621
614 for patch in &patches_to_apply { 622 for patch in &patches_to_apply {
615 let commit_id = get_commit_id_from_patch(patch)?; 623 // The commit id from the tag (or mbox envelope) is the authoritative id
616 // only create new commits - otherwise make them the tip 624 // when the optional `commit` nostr tag is present. When it is absent the
617 if !self.does_commit_exist(&commit_id)? { 625 // mbox envelope SHA1 is used as a best-effort value — it will often differ
618 self.create_commit_from_patch(patch, None)?; 626 // from the reconstructed commit's actual OID (e.g. GPG-signed commits).
619 } 627 // We therefore always use the OID returned by create_commit_from_patch as
620 self.create_branch_at_commit(branch_name, &commit_id)?; 628 // the branch tip, falling back to the tag commit id only when the commit
629 // already exists in the repo (meaning it was previously applied correctly).
630 let tag_commit_id = get_commit_id_from_patch(patch).ok();
631 let applied_oid = if let Some(ref id) = tag_commit_id {
632 if self.does_commit_exist(id)? {
633 // Commit already exists (e.g. previously fetched), use it directly.
634 id.clone()
635 } else {
636 self.create_commit_from_patch(patch, None)?.to_string()
637 }
638 } else {
639 // No commit id available at all — apply and use the resulting OID.
640 self.create_commit_from_patch(patch, None)?.to_string()
641 };
642 self.create_branch_at_commit(branch_name, &applied_oid)?;
621 self.checkout(branch_name)?; 643 self.checkout(branch_name)?;
622 } 644 }
623 Ok(patches_to_apply) 645 Ok(patches_to_apply)
@@ -641,8 +663,10 @@ impl RepoActions for Repo {
641 } else { 663 } else {
642 let metadata = crate::mbox_parser::parse_mbox_patch(&patch.content) 664 let metadata = crate::mbox_parser::parse_mbox_patch(&patch.content)
643 .context("failed to parse patch for timestamp")?; 665 .context("failed to parse patch for timestamp")?;
644 let timestamp = metadata.committer_timestamp.unwrap_or(metadata.author_timestamp); 666 let timestamp = metadata
645 667 .committer_timestamp
668 .unwrap_or(metadata.author_timestamp);
669
646 let best_guess = self 670 let best_guess = self
647 .find_best_guess_parent_commit(timestamp) 671 .find_best_guess_parent_commit(timestamp)
648 .context("failed to find best guess parent commit")?; 672 .context("failed to find best guess parent commit")?;
@@ -684,8 +708,10 @@ impl RepoActions for Repo {
684 None 708 None
685 }; 709 };
686 710
687 let author_data = extract_signature_data_with_fallback(&patch.tags, "author", &patch.content)?; 711 let author_data =
688 let committer_data = extract_signature_data_with_fallback(&patch.tags, "committer", &patch.content)?; 712 extract_signature_data_with_fallback(&patch.tags, "author", &patch.content)?;
713 let committer_data =
714 extract_signature_data_with_fallback(&patch.tags, "committer", &patch.content)?;
689 let author_sig = author_data.to_signature()?; 715 let author_sig = author_data.to_signature()?;
690 let committer_sig = committer_data.to_signature()?; 716 let committer_sig = committer_data.to_signature()?;
691 717
@@ -983,7 +1009,9 @@ fn extract_signature_data_from_tags(tags: &Tags, tag_name: &str) -> Result<Signa
983 name: v[1].clone(), 1009 name: v[1].clone(),
984 email: v[2].clone(), 1010 email: v[2].clone(),
985 timestamp: v[3].parse().context("tag time is incorrectly formatted")?, 1011 timestamp: v[3].parse().context("tag time is incorrectly formatted")?,
986 offset_minutes: v[4].parse().context("tag time offset is incorrectly formatted")?, 1012 offset_minutes: v[4]
1013 .parse()
1014 .context("tag time offset is incorrectly formatted")?,
987 }) 1015 })
988} 1016}
989 1017
@@ -1007,7 +1035,9 @@ fn extract_signature_data_with_fallback(
1007 offset_minutes: metadata.author_offset_minutes, 1035 offset_minutes: metadata.author_offset_minutes,
1008 }) 1036 })
1009 } else if tag_name == "committer" { 1037 } else if tag_name == "committer" {
1010 let timestamp = metadata.committer_timestamp.unwrap_or(metadata.author_timestamp); 1038 let timestamp = metadata
1039 .committer_timestamp
1040 .unwrap_or(metadata.author_timestamp);
1011 Ok(SignatureData { 1041 Ok(SignatureData {
1012 name: metadata.author_name, 1042 name: metadata.author_name,
1013 email: metadata.author_email, 1043 email: metadata.author_email,