mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-05 15:08:24 +00:00
fix(grasp06): recover placeholder scope from a surviving ref
A crash can leave a /prs/ ref installed without its purgatory checkpoint. Recover the scope from the signed event's service-local clone URL, signer, identifier, and exact event-id ref and commit. This lets git-first PR events complete after restart without relaxing URL or commit validation. Assume the signed local URL names the intended contributor repository. Standard endpoint recovery and object staging are separate changes. Validation: nix develop -c cargo test --test grasp06_pr_hosting pr_event_after_crash_recovers_prs_placeholder_scope passed. Assisted-by: GPT-6
This commit is contained in:
@@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- Accept a GRASP-06 `/prs/` PR event whose pushed ref survived a crash that
|
||||
lost its purgatory placeholder, matching it by the event's service-local
|
||||
clone URL and exact `refs/nostr/<event-id>` ref.
|
||||
|
||||
- Remove abandoned normal-endpoint PR refs when their pending event expires,
|
||||
retaining exact repository scopes across restarts and retrying failed cleanup.
|
||||
|
||||
|
||||
@@ -65,16 +65,49 @@ impl PrEventPolicy {
|
||||
}
|
||||
};
|
||||
|
||||
// Check for placeholder first (git-data-first scenario)
|
||||
if let Some(placeholder_commit) = self.ctx.purgatory.find_pr_placeholder(&event_id) {
|
||||
// Read the full entry so we can inspect any GRASP-06 scope
|
||||
// recorded when the placeholder was created from a /prs/ push.
|
||||
let prs_scope = self
|
||||
.ctx
|
||||
.purgatory
|
||||
.find_pr(&event_id)
|
||||
.and_then(|entry| entry.prs_scope);
|
||||
|
||||
// A crash may happen after Git installs the ref but before the
|
||||
// purgatory checkpoint. Recover the /prs/ scope only from a signed,
|
||||
// service-local clone URL for this signer and an exact event-id/OID ref.
|
||||
let mut placeholder = self
|
||||
.ctx
|
||||
.purgatory
|
||||
.find_pr_placeholder(&event_id)
|
||||
.map(|commit| {
|
||||
let scope = self
|
||||
.ctx
|
||||
.purgatory
|
||||
.find_pr(&event_id)
|
||||
.and_then(|entry| entry.prs_scope);
|
||||
(commit, scope)
|
||||
});
|
||||
if placeholder.is_none() && self.ctx.config.grasp06_enable {
|
||||
for identifier in crate::grasp06::policy::prs_identifiers_named_by_event_clone_tags(
|
||||
event,
|
||||
&self.ctx.config.service_address(),
|
||||
) {
|
||||
if !git::validate_repository_identifier(&identifier) {
|
||||
continue;
|
||||
}
|
||||
let path = crate::grasp06::paths::prs_repo_path(
|
||||
&self.ctx.git_data_path,
|
||||
&event.pubkey.to_hex(),
|
||||
&identifier,
|
||||
);
|
||||
if git::get_ref_commit(&path, &format!("refs/nostr/{event_id}")).as_deref()
|
||||
== Some(commit.as_str())
|
||||
{
|
||||
placeholder = Some((
|
||||
commit.clone(),
|
||||
Some(crate::purgatory::PrsPlaceholderScope {
|
||||
submitter: event.pubkey,
|
||||
identifier,
|
||||
}),
|
||||
));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((placeholder_commit, prs_scope)) = placeholder {
|
||||
if let Some(scope) = prs_scope {
|
||||
// The placeholder was created by a /prs/<submitter>/<id>.git
|
||||
// push (06.md line 12). The arriving event MUST be signed by
|
||||
|
||||
@@ -451,6 +451,24 @@ impl TestRelay {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Persistent GRASP-06 storage for crash/restart tests.
|
||||
pub async fn start_with_grasp_06_paths(
|
||||
git_data_path: PathBuf,
|
||||
relay_data_path: PathBuf,
|
||||
) -> Self {
|
||||
Self::start_internal(
|
||||
port::reserve_port(),
|
||||
RelayOptions {
|
||||
grasp06_enable: true,
|
||||
lmdb_backend: true,
|
||||
git_data_path: Some(git_data_path),
|
||||
relay_data_path: Some(relay_data_path),
|
||||
..RelayOptions::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Start a relay on a port that the caller has already reserved.
|
||||
///
|
||||
/// Use this when the test needs the port number *before* the relay
|
||||
|
||||
@@ -369,3 +369,54 @@ isolated_test_with_grasp_06!(
|
||||
test_commit_mismatch_deletes_ref_and_blocks_promotion_with_grasp_06,
|
||||
PushValidationTests::test_commit_mismatch_deletes_ref_and_blocks_promotion
|
||||
);
|
||||
|
||||
/// A crash can lose the purgatory checkpoint after a `/prs/` push installed
|
||||
/// its ref. The arriving PR event must still be matched to that push, using
|
||||
/// only its signed, service-local clone URL and the exact event-id ref.
|
||||
#[tokio::test]
|
||||
async fn pr_event_after_crash_recovers_prs_placeholder_scope() {
|
||||
use nostr_sdk::prelude::*;
|
||||
let persistent = tempfile::tempdir().unwrap();
|
||||
let relay = TestRelay::start_with_grasp_06_paths(
|
||||
persistent.path().join("git"),
|
||||
persistent.path().join("relay"),
|
||||
)
|
||||
.await;
|
||||
let client = AuditClient::new(relay.url(), AuditConfig::isolated())
|
||||
.await
|
||||
.unwrap();
|
||||
let keys = client.keys().clone();
|
||||
let npub = keys.public_key().to_bech32().unwrap();
|
||||
let identifier = "crash-recovered-pr";
|
||||
let local = tempfile::tempdir().unwrap();
|
||||
let commit =
|
||||
common::create_test_repo_with_commit(local.path(), common::CommitVariant::PrTest).unwrap();
|
||||
let clone_url = format!("http://{}/prs/{npub}/{identifier}.git", relay.domain());
|
||||
let event = common::create_pr_event_with_clone(
|
||||
&keys,
|
||||
&format!("30617:{}:{identifier}", keys.public_key().to_hex()),
|
||||
&commit,
|
||||
"Crash-recovered PR",
|
||||
&[&clone_url],
|
||||
)
|
||||
.unwrap();
|
||||
common::push_ref_to_relay(
|
||||
local.path(),
|
||||
&relay.domain(),
|
||||
&format!("prs/{npub}"),
|
||||
identifier,
|
||||
&commit,
|
||||
&format!("refs/nostr/{}", event.id),
|
||||
)
|
||||
.unwrap();
|
||||
// Killing the relay loses any placeholder not yet checkpointed.
|
||||
let relay = relay.restart().await;
|
||||
let client = AuditClient::new(relay.url(), AuditConfig::isolated())
|
||||
.await
|
||||
.unwrap();
|
||||
client.send_event(event.clone()).await.unwrap();
|
||||
common::wait_for_event_served(relay.url(), &event.id, std::time::Duration::from_secs(10))
|
||||
.await
|
||||
.unwrap();
|
||||
relay.stop().await;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user