mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-06 07:28:23 +00:00
fix(recovery): prune tombstoned PR refs during restore
This commit is contained in:
+45
-1
@@ -1771,6 +1771,13 @@ impl Nip34WritePolicy {
|
||||
}
|
||||
|
||||
fn pr_event_identifiers(event: &Event) -> BTreeSet<String> {
|
||||
Self::pr_event_announcement_scopes(event)
|
||||
.into_iter()
|
||||
.map(|(_, identifier)| identifier)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn pr_event_announcement_scopes(event: &Event) -> BTreeSet<(PublicKey, String)> {
|
||||
event
|
||||
.tags
|
||||
.iter()
|
||||
@@ -1785,7 +1792,8 @@ impl Nip34WritePolicy {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(coord[2].to_string())
|
||||
let owner = PublicKey::from_hex(coord[1]).ok()?;
|
||||
Some((owner, coord[2].to_string()))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -1793,6 +1801,13 @@ impl Nip34WritePolicy {
|
||||
async fn pr_target_repo_paths_for_recovery(&self, event: &Event) -> BTreeSet<PathBuf> {
|
||||
let mut paths = BTreeSet::new();
|
||||
|
||||
for (owner, identifier) in Self::pr_event_announcement_scopes(event) {
|
||||
paths.insert(self.repo_path_for_owner_and_identifier(
|
||||
&Self::owner_directory_component(&owner),
|
||||
&identifier,
|
||||
));
|
||||
}
|
||||
|
||||
for identifier in Self::pr_event_identifiers(event) {
|
||||
let Ok(repo_data) =
|
||||
crate::git::authorization::fetch_repository_data_excluding_purgatory(
|
||||
@@ -1876,6 +1891,34 @@ impl Nip34WritePolicy {
|
||||
Ok(restored_any)
|
||||
}
|
||||
|
||||
async fn cleanup_pr_git_refs_for_tombstoned_event(&self, event: &Event) {
|
||||
if !matches!(
|
||||
event.kind,
|
||||
Kind::GitPullRequest | Kind::GitPullRequestUpdate
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let ref_name = format!("refs/nostr/{}", event.id.to_hex());
|
||||
let repo_paths = self.pr_target_repo_paths_for_recovery(event).await;
|
||||
|
||||
for repo_path in repo_paths {
|
||||
if !repo_path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(e) = crate::git::delete_ref(&repo_path, &ref_name) {
|
||||
tracing::debug!(
|
||||
event_id = %event.id.to_hex(),
|
||||
repo = %repo_path.display(),
|
||||
ref_name = %ref_name,
|
||||
error = %e,
|
||||
"Recovery tombstone cleanup: PR git ref not deleted for this repository"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn restore_events_from_holding(
|
||||
&self,
|
||||
records: &[RecoveryMetadataRecord],
|
||||
@@ -1893,6 +1936,7 @@ impl Nip34WritePolicy {
|
||||
match self.ctx.holding.archived_event(&event_id).await {
|
||||
Ok(Some(event)) => {
|
||||
if self.recovery_blocked_by_tombstone(&event).await {
|
||||
self.cleanup_pr_git_refs_for_tombstoned_event(&event).await;
|
||||
tracing::info!(
|
||||
event_id = %event.id.to_hex(),
|
||||
kind = event.kind.as_u16(),
|
||||
|
||||
@@ -536,6 +536,70 @@ async fn purgatory_promotion_recovery_respects_state_tombstones() {
|
||||
fixture.relay.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn purgatory_promotion_tombstoned_pr_events_do_not_restore_pr_refs() {
|
||||
let fixture =
|
||||
setup_parked_announcement_with_failed_ingest_recovery("promotion-recovery-pr-tombstones", false)
|
||||
.await;
|
||||
|
||||
fixture
|
||||
.client
|
||||
.send_event(build_deletion(
|
||||
&fixture.client,
|
||||
&[fixture.pr.id, fixture.pr_update.id],
|
||||
&[],
|
||||
))
|
||||
.await
|
||||
.expect("submit PR tombstone deletion request before promotion");
|
||||
tokio::time::sleep(Duration::from_millis(300)).await;
|
||||
|
||||
std::fs::write(&fixture.archive_path, &fixture.archive_backup)
|
||||
.expect("restore archive bytes before PR tombstone promotion");
|
||||
|
||||
let promoted_state =
|
||||
submit_new_state_and_push_for_promotion(&fixture, "promotion-pr-tombstone").await;
|
||||
|
||||
assert!(fixture
|
||||
.client
|
||||
.is_event_on_relay(fixture.reannouncement.id)
|
||||
.await
|
||||
.expect("query promoted re-announcement"));
|
||||
assert!(fixture
|
||||
.client
|
||||
.is_event_on_relay(promoted_state.id)
|
||||
.await
|
||||
.expect("query promoted new state"));
|
||||
assert!(
|
||||
!fixture
|
||||
.client
|
||||
.is_event_on_relay(fixture.pr.id)
|
||||
.await
|
||||
.expect("query tombstoned pr"),
|
||||
"tombstoned PR must not resurrect during recovery"
|
||||
);
|
||||
assert!(
|
||||
!fixture
|
||||
.client
|
||||
.is_event_on_relay(fixture.pr_update.id)
|
||||
.await
|
||||
.expect("query tombstoned pr update"),
|
||||
"tombstoned PR update must not resurrect during recovery"
|
||||
);
|
||||
|
||||
let pr_ref = format!("refs/nostr/{}", fixture.pr.id.to_hex());
|
||||
let pr_update_ref = format!("refs/nostr/{}", fixture.pr_update.id.to_hex());
|
||||
assert!(
|
||||
bare_ref_value(&fixture.repo_path, &pr_ref).is_none(),
|
||||
"tombstoned PR should have refs/nostr entry cleaned during recovery"
|
||||
);
|
||||
assert!(
|
||||
bare_ref_value(&fixture.repo_path, &pr_update_ref).is_none(),
|
||||
"tombstoned PR update should have refs/nostr entry cleaned during recovery"
|
||||
);
|
||||
|
||||
fixture.relay.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn repeated_purgatory_promotion_attempts_remain_idempotent() {
|
||||
let fixture = setup_parked_announcement_with_failed_ingest_recovery(
|
||||
|
||||
Reference in New Issue
Block a user