diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-18 21:04:59 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-18 21:04:59 +0000 |
| commit | 73d829b916d87626f33ea2adead0c48f1d9d737d (patch) | |
| tree | b3adc223df90184e430ac6cb153b9a6e986f8097 /src/lib/git | |
| parent | 061589cd88d0480dc7cb0b9eb19a3910293ceb56 (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')
| -rw-r--r-- | src/lib/git/mod.rs | 72 |
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, |