1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
|
//! Event Processing - Unified logic for processing state and PR events with git data
//!
//! This module provides the core processing logic used when events have git data available.
//! These functions are used in multiple scenarios:
//! - When events arrive with git data already available (policy handlers)
//! - When events are released from purgatory (purgatory sync)
//! - When git pushes trigger purgatory releases (receive-pack handler)
use crate::git;
use crate::git::authorization::{collect_authorized_maintainers, RepositoryData};
use crate::git::sync::{
align_repository_with_state, copy_missing_oids_between_repos,
sync_pr_refs_to_tagged_owner_repos,
};
use crate::nostr::events::RepositoryState;
use nostr_sdk::Event;
use std::path::Path;
/// Result of processing a state event with git data
#[derive(Debug, Default, Clone)]
pub struct ProcessStateResult {
/// Number of repositories synced (OIDs copied + refs aligned)
pub repos_synced: usize,
/// Number of refs created across all repos
pub refs_created: usize,
/// Number of refs updated across all repos
pub refs_updated: usize,
/// Number of refs deleted across all repos
pub refs_deleted: usize,
/// Errors encountered (non-fatal)
pub errors: Vec<String>,
}
/// Result of processing a PR event with git data
#[derive(Debug, Default, Clone)]
pub struct ProcessPrResult {
/// Number of repositories synced
pub repos_synced: usize,
/// Number of refs created across all repos
pub refs_created: usize,
/// Errors encountered (non-fatal)
pub errors: Vec<String>,
}
/// Process a single state event that has git data available.
///
/// This is the core processing logic used when:
/// - A state event arrives with git data already available
/// - A state event is released from purgatory
///
/// Does NOT save to database or notify subscribers - caller handles that.
///
/// # Processing Steps
/// 1. Identify owner repos where state author is an authorized maintainer
/// 2. For each owner repo, check if this state is the latest authorized
/// 3. Copy missing OIDs from source repo to target repo
/// 4. Align refs (branches, tags, HEAD) with the state
///
/// # Arguments
/// * `state` - The state event to process
/// * `source_repo_path` - Path to repo that has the git data
/// * `db_repo_data` - Repository data from database (announcements + states)
/// * `git_data_path` - Base path for git repositories
///
/// # Returns
/// ProcessStateResult with statistics
pub fn process_state_with_git_data(
state: &RepositoryState,
source_repo_path: &Path,
db_repo_data: &RepositoryData,
git_data_path: &Path,
) -> ProcessStateResult {
let mut result = ProcessStateResult::default();
let state_author = state.event.pubkey.to_hex();
// Collect authorized maintainers per owner
let by_owner = collect_authorized_maintainers(&db_repo_data.announcements);
// Step 1: Identify owner repos that the state event author is maintainer for
let authorized_owners: Vec<&String> = by_owner
.iter()
.filter(|(_, maintainers)| maintainers.contains(&state_author))
.map(|(owner, _)| owner)
.collect();
if authorized_owners.is_empty() {
tracing::debug!(
identifier = %state.identifier,
author = %state_author,
"State event author not authorized for any owner"
);
return result;
}
// Process each owner repo that authorizes this state event author
for owner in &authorized_owners {
let maintainers = by_owner.get(*owner).unwrap();
// Step 2: Check if this state event is the latest authorized for this owner
let is_latest = crate::git::sync::is_latest_authorized_state_public(
state,
maintainers,
&db_repo_data.states,
);
if !is_latest {
tracing::debug!(
identifier = %state.identifier,
owner = %owner,
"Skipping owner - newer authorized state exists"
);
continue;
}
// Find the announcement for this owner
let Some(announcement) = db_repo_data
.announcements
.iter()
.find(|a| a.event.pubkey.to_hex() == **owner)
else {
continue;
};
let target_repo_path = git_data_path.join(announcement.repo_path());
// Step 3: Check git repo exists for that owner
if !target_repo_path.exists() {
tracing::debug!(
identifier = %state.identifier,
owner = %owner,
repo_path = %target_repo_path.display(),
"Skipping owner - repository doesn't exist"
);
continue;
}
// Step 4: Copy all required OIDs to that repo (unless it's source_repo_path)
if target_repo_path != source_repo_path {
if let Err(e) =
copy_missing_oids_between_repos(source_repo_path, &target_repo_path, state)
{
tracing::warn!(
identifier = %state.identifier,
source = %source_repo_path.display(),
target = %target_repo_path.display(),
error = %e,
"Failed to copy OIDs between repos"
);
result.errors.push(e);
continue; // Skip this owner repo
}
}
// Step 5: Reset the git state in that repo to match the state event
let align_result = align_repository_with_state(&target_repo_path, state);
result.repos_synced += 1;
result.refs_created += align_result.refs_created;
result.refs_updated += align_result.refs_updated;
result.refs_deleted += align_result.refs_deleted;
tracing::info!(
identifier = %state.identifier,
owner = %owner,
repo_path = %target_repo_path.display(),
refs_created = align_result.refs_created,
refs_updated = align_result.refs_updated,
refs_deleted = align_result.refs_deleted,
head_set = align_result.head_set,
"Aligned repository with state"
);
}
result
}
/// Process a single PR event that has git data available.
///
/// This is the core processing logic used when:
/// - A PR event arrives with git data already available
/// - A PR event is released from purgatory
///
/// Does NOT save to database or notify subscribers - caller handles that.
///
/// # Processing Steps
/// 1. Sync PR commit to owner repos using tagged maintainer logic
/// 2. Create refs/nostr/<event-id> ref in source repo (if missing)
/// 3. Create refs/nostr/<event-id> refs in all synced repos
///
/// # Arguments
/// * `event` - The PR event to process
/// * `commit` - The commit hash from the PR event
/// * `source_repo_path` - Path to repo that has the commit
/// * `db_repo_data` - Repository data from database (announcements + states)
/// * `git_data_path` - Base path for git repositories
/// * `source_owner_pubkey` - Owner pubkey of source repo (to skip)
///
/// # Returns
/// ProcessPrResult with statistics
pub fn process_pr_with_git_data(
event: &Event,
commit: &str,
source_repo_path: &Path,
db_repo_data: &RepositoryData,
git_data_path: &Path,
source_owner_pubkey: &str,
) -> ProcessPrResult {
let mut result = ProcessPrResult::default();
let event_id = event.id.to_hex();
// Sync PR ref to owner repos using tagged maintainer logic
let pr_refs = vec![(event_id.clone(), commit.to_string())];
let pr_events = vec![event.clone()];
let sync_result = sync_pr_refs_to_tagged_owner_repos(
source_repo_path,
&pr_refs,
&pr_events,
db_repo_data,
git_data_path,
source_owner_pubkey,
);
result.repos_synced += sync_result.repos_synced;
result.refs_created += sync_result.refs_created;
result
.errors
.extend(sync_result.errors.into_iter().map(|(_, e)| e));
// Create the ref in the source repo if it doesn't exist
let ref_name = format!("refs/nostr/{}", event_id);
if git::get_ref_commit(source_repo_path, &ref_name).is_none() {
if let Err(e) = git::update_ref(source_repo_path, &ref_name, commit) {
tracing::warn!(
event_id = %event_id,
repo = %source_repo_path.display(),
error = %e,
"Failed to create PR ref in source repo"
);
result.errors.push(e);
} else {
result.refs_created += 1;
tracing::info!(
event_id = %event_id,
commit = %commit,
repo = %source_repo_path.display(),
"Created PR ref in source repo"
);
}
}
result
}
|