mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-05 15:08:24 +00:00
fix(git): honor replacement ordering in owner repository sync
The public sync_to_owner_repos helper skipped all same-second candidates and selected prior states by timestamp alone. A lower-ID replacement could therefore never reach Git alignment. Reuse latest_authorized_state and require strictly preferred replacement ordering. Exact replays and losing candidates still skip alignment. Authorization and Git object copying are unchanged. No internal callers of this public helper currently exist. Validation: a regression against a temporary bare repository fails before the fix and passes afterward. It covers both ID directions, replay, both prior-state orders and a later timestamp. Focused owner tests, workspace all-target Clippy and formatting pass. Assisted-by: GPT-6
This commit is contained in:
@@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- Honor lower-ID same-second replacements in the public owner-repository sync
|
||||||
|
helper while preserving replay suppression.
|
||||||
|
|
||||||
- Restore relay-owner profiles and relay lists using the lower event ID when
|
- Restore relay-owner profiles and relay lists using the lower event ID when
|
||||||
fetched versions share a timestamp.
|
fetched versions share a timestamp.
|
||||||
|
|
||||||
|
|||||||
+73
-8
@@ -403,16 +403,12 @@ pub fn sync_to_owner_repos(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find the previous latest state for this owner's maintainer set
|
// Find the previous latest state for this owner's maintainer set
|
||||||
let previous_state = db_repo_data
|
let previous_state = latest_authorized_state(maintainers, &db_repo_data.states);
|
||||||
.states
|
|
||||||
.iter()
|
|
||||||
.filter(|s| maintainers.contains(&s.event.pubkey.to_hex()))
|
|
||||||
.max_by_key(|s| s.event.created_at);
|
|
||||||
|
|
||||||
// Only update if this state is newer than any existing state
|
// Apply only a strictly preferred state; replaying the same event is a no-op.
|
||||||
// TODO: in event of a tie, the event with the biggest event id wins
|
|
||||||
if let Some(prev) = previous_state {
|
if let Some(prev) = previous_state {
|
||||||
if state.event.created_at <= prev.event.created_at {
|
if !crate::nostr::events::compare_replacement_events(&state.event, &prev.event).is_gt()
|
||||||
|
{
|
||||||
debug!(
|
debug!(
|
||||||
identifier = %state.identifier,
|
identifier = %state.identifier,
|
||||||
owner = %owner,
|
owner = %owner,
|
||||||
@@ -1843,6 +1839,75 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use nostr_sdk::prelude::Keys;
|
use nostr_sdk::prelude::Keys;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn owner_repo_sync_honors_same_second_replacement_order() {
|
||||||
|
use crate::nostr::events::RepositoryAnnouncement;
|
||||||
|
use nostr_sdk::prelude::{EventBuilder, FinalizeEvent, Kind, Tag, Timestamp};
|
||||||
|
|
||||||
|
let keys = Keys::generate();
|
||||||
|
let announcement = RepositoryAnnouncement::from_event(
|
||||||
|
EventBuilder::new(Kind::GitRepoAnnouncement, "")
|
||||||
|
.tags([Tag::identifier("tie-test")])
|
||||||
|
.finalize(&keys)
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let root = tempfile::tempdir().unwrap();
|
||||||
|
let repo = root.path().join(announcement.repo_path());
|
||||||
|
std::fs::create_dir_all(&repo).unwrap();
|
||||||
|
assert!(Command::new("git")
|
||||||
|
.args(["init", "--bare"])
|
||||||
|
.arg(&repo)
|
||||||
|
.output()
|
||||||
|
.unwrap()
|
||||||
|
.status
|
||||||
|
.success());
|
||||||
|
let build = |content: &str, timestamp| {
|
||||||
|
RepositoryState::from_event(
|
||||||
|
EventBuilder::new(Kind::RepoState, content)
|
||||||
|
.tags([Tag::identifier("tie-test")])
|
||||||
|
.custom_created_at(Timestamp::from_secs(timestamp))
|
||||||
|
.finalize(&keys)
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
let mut pair = [build("first", 1000), build("second", 1000)];
|
||||||
|
pair.sort_by_key(|state| state.event.id);
|
||||||
|
let [winner, loser] = pair;
|
||||||
|
let mut data = RepositoryData {
|
||||||
|
announcements: vec![announcement],
|
||||||
|
states: vec![loser.clone()],
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
sync_to_owner_repos(&repo, &winner, &data, root.path()).repos_synced,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
data.states = vec![winner.clone()];
|
||||||
|
assert_eq!(
|
||||||
|
sync_to_owner_repos(&repo, &loser, &data, root.path()).repos_synced,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
sync_to_owner_repos(&repo, &winner, &data, root.path()).repos_synced,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
for states in [
|
||||||
|
vec![winner.clone(), loser.clone()],
|
||||||
|
vec![loser, winner.clone()],
|
||||||
|
] {
|
||||||
|
data.states = states;
|
||||||
|
assert_eq!(
|
||||||
|
sync_to_owner_repos(&repo, &winner, &data, root.path()).repos_synced,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
sync_to_owner_repos(&repo, &build("later", 1001), &data, root.path()).repos_synced,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_process_result_default() {
|
fn test_process_result_default() {
|
||||||
let result = ProcessResult::default();
|
let result = ProcessResult::default();
|
||||||
|
|||||||
Reference in New Issue
Block a user