diff options
Diffstat (limited to 'src/bin/git_remote_nostr/list.rs')
| -rw-r--r-- | src/bin/git_remote_nostr/list.rs | 82 |
1 files changed, 64 insertions, 18 deletions
diff --git a/src/bin/git_remote_nostr/list.rs b/src/bin/git_remote_nostr/list.rs index a32ed67..23b3b98 100644 --- a/src/bin/git_remote_nostr/list.rs +++ b/src/bin/git_remote_nostr/list.rs | |||
| @@ -32,13 +32,14 @@ pub async fn run_list( | |||
| 32 | if is_verbose() { | 32 | if is_verbose() { |
| 33 | term.write_line("git servers: listing refs...")?; | 33 | term.write_line("git servers: listing refs...")?; |
| 34 | } | 34 | } |
| 35 | let nostr_git_url = repo_ref.to_nostr_git_url(&None); | ||
| 35 | // nostr_state is passed to list_from_remotes only for the sync-status | 36 | // nostr_state is passed to list_from_remotes only for the sync-status |
| 36 | // display; the actual ref state we advertise is determined below. | 37 | // display; the actual ref state we advertise is determined below. |
| 37 | let remote_states = list_from_remotes( | 38 | let remote_states = list_from_remotes( |
| 38 | &term, | 39 | &term, |
| 39 | git_repo, | 40 | git_repo, |
| 40 | &repo_ref.git_server, | 41 | &repo_ref.git_server, |
| 41 | &repo_ref.to_nostr_git_url(&None), | 42 | &nostr_git_url, |
| 42 | nostr_state.as_ref(), | 43 | nostr_state.as_ref(), |
| 43 | ) | 44 | ) |
| 44 | .await; | 45 | .await; |
| @@ -134,6 +135,7 @@ pub async fn run_list( | |||
| 134 | 135 | ||
| 135 | /// fetches branches and tags from git servers so patch parent commits can be | 136 | /// fetches branches and tags from git servers so patch parent commits can be |
| 136 | /// used to build patches with correct commit ids | 137 | /// used to build patches with correct commit ids |
| 138 | #[allow(clippy::too_many_lines)] | ||
| 137 | async fn get_open_and_draft_proposals_state( | 139 | async fn get_open_and_draft_proposals_state( |
| 138 | term: &console::Term, | 140 | term: &console::Term, |
| 139 | git_repo: &Repo, | 141 | git_repo: &Repo, |
| @@ -156,8 +158,6 @@ async fn get_open_and_draft_proposals_state( | |||
| 156 | .filter(|v| !v.starts_with("ref: ")) | 158 | .filter(|v| !v.starts_with("ref: ")) |
| 157 | .cloned() | 159 | .cloned() |
| 158 | .collect::<Vec<String>>(), | 160 | .collect::<Vec<String>>(), |
| 159 | // TODO we could fetch the oids of Pull Requests and Pull Request Updates to prevent | ||
| 160 | // having repeat fetching during the git remote helper fetch phase | ||
| 161 | git_server_url, | 161 | git_server_url, |
| 162 | &repo_ref.to_nostr_git_url(&None), | 162 | &repo_ref.to_nostr_git_url(&None), |
| 163 | term, | 163 | term, |
| @@ -169,8 +169,62 @@ async fn get_open_and_draft_proposals_state( | |||
| 169 | } | 169 | } |
| 170 | } | 170 | } |
| 171 | 171 | ||
| 172 | let mut state = HashMap::new(); | ||
| 173 | let open_and_draft_proposals = get_open_or_draft_proposals(git_repo, repo_ref).await?; | 172 | let open_and_draft_proposals = get_open_or_draft_proposals(git_repo, repo_ref).await?; |
| 173 | |||
| 174 | // Collect PR/PR-update tip OIDs that are still missing after the bulk prefetch. | ||
| 175 | // We borrow proposals here so we can move them in the state-building loop | ||
| 176 | // below. | ||
| 177 | let mut missing_pr_oids: Vec<String> = open_and_draft_proposals | ||
| 178 | .values() | ||
| 179 | .filter_map(|(_, events)| { | ||
| 180 | events | ||
| 181 | .iter() | ||
| 182 | .find(|e| e.kind.eq(&KIND_PULL_REQUEST) || e.kind.eq(&KIND_PULL_REQUEST_UPDATE)) | ||
| 183 | .and_then(|e| tag_value(e, "c").ok()) | ||
| 184 | }) | ||
| 185 | .filter(|tip| !git_repo.does_commit_exist(tip).unwrap_or(false)) | ||
| 186 | .collect(); | ||
| 187 | |||
| 188 | // For each repo git server, batch-fetch the PR tip OIDs it carries that are | ||
| 189 | // still missing locally. Only OIDs the server has advertised are included in | ||
| 190 | // each batch (avoids all-or-nothing batch-poisoning). We mop up across servers | ||
| 191 | // until all missing OIDs are satisfied or all servers are exhausted. | ||
| 192 | // | ||
| 193 | // NOTE: we intentionally restrict mop-up to the repo's declared git servers | ||
| 194 | // (remote_states) and do NOT try the git-server URL carried in the PR event's | ||
| 195 | // `clone` tag. A PR submitter could include an arbitrary server URL there; | ||
| 196 | // fetching from it unconditionally would let a malicious or slow server | ||
| 197 | // delay every clone/fetch. If we later want to support PR-supplied servers, | ||
| 198 | // it should be opt-in (e.g. an explicit `--include-pr-servers` flag) so | ||
| 199 | // users consciously accept the trust/performance trade-off. PRs whose tip | ||
| 200 | // OID isn't carried by any repo git server will simply not be advertised as | ||
| 201 | // `refs/heads/pr/*` refs; they are still accessible via their patch events. | ||
| 202 | if !missing_pr_oids.is_empty() { | ||
| 203 | for (server_url, (server_state, is_grasp)) in remote_states { | ||
| 204 | let batch: Vec<String> = missing_pr_oids | ||
| 205 | .iter() | ||
| 206 | .filter(|oid| server_state.values().any(|v| v == *oid)) | ||
| 207 | .cloned() | ||
| 208 | .collect(); | ||
| 209 | if batch.is_empty() { | ||
| 210 | continue; | ||
| 211 | } | ||
| 212 | let _ = fetch_from_git_server( | ||
| 213 | git_repo, | ||
| 214 | &batch, | ||
| 215 | server_url, | ||
| 216 | &repo_ref.to_nostr_git_url(&None), | ||
| 217 | term, | ||
| 218 | *is_grasp, | ||
| 219 | ); | ||
| 220 | missing_pr_oids.retain(|oid| !git_repo.does_commit_exist(oid).unwrap_or(false)); | ||
| 221 | if missing_pr_oids.is_empty() { | ||
| 222 | break; | ||
| 223 | } | ||
| 224 | } | ||
| 225 | } | ||
| 226 | |||
| 227 | let mut state = HashMap::new(); | ||
| 174 | let current_user = get_curent_user(git_repo)?; | 228 | let current_user = get_curent_user(git_repo)?; |
| 175 | for (_, (proposal, events_to_apply)) in open_and_draft_proposals { | 229 | for (_, (proposal, events_to_apply)) in open_and_draft_proposals { |
| 176 | if let Ok(cl) = event_to_cover_letter(&proposal) { | 230 | if let Ok(cl) = event_to_cover_letter(&proposal) { |
| @@ -192,17 +246,9 @@ async fn get_open_and_draft_proposals_state( | |||
| 192 | { | 246 | { |
| 193 | match tag_value(pr_or_pr_update, "c") { | 247 | match tag_value(pr_or_pr_update, "c") { |
| 194 | Ok(tip) => { | 248 | Ok(tip) => { |
| 195 | // only list Pull Requests as refs/heads/pr/* if data is commit is | 249 | // Only advertise once confirmed locally available — this |
| 196 | // advertised as tip of a ref on a repo git server or | 250 | // guarantees the subsequent fetch phase can serve the object. |
| 197 | // available locally. Otherwise the standard cmd: | 251 | if git_repo.does_commit_exist(&tip).is_ok_and(|r| r) { |
| 198 | // `git clone nostr://` will fail as it assumes all /refs/heads | ||
| 199 | // returned by list are accessable | ||
| 200 | let tip_oid_is_on_a_repo_git_server = | ||
| 201 | remote_states.iter().any(|(_url, (state, _is_grasp))| { | ||
| 202 | state.iter().any(|(_, oid)| tip == *oid) | ||
| 203 | }) || git_repo.does_commit_exist(&tip).is_ok_and(|r| r); | ||
| 204 | |||
| 205 | if tip_oid_is_on_a_repo_git_server { | ||
| 206 | state.insert(format!("refs/heads/{branch_name}"), tip); | 252 | state.insert(format!("refs/heads/{branch_name}"), tip); |
| 207 | } | 253 | } |
| 208 | } | 254 | } |
| @@ -254,8 +300,8 @@ async fn get_all_proposals_state( | |||
| 254 | let mut state = HashMap::new(); | 300 | let mut state = HashMap::new(); |
| 255 | let all_proposals = get_all_proposals(git_repo, repo_ref).await?; | 301 | let all_proposals = get_all_proposals(git_repo, repo_ref).await?; |
| 256 | let current_user = get_curent_user(git_repo)?; | 302 | let current_user = get_curent_user(git_repo)?; |
| 257 | for (_, (proposal, events_to_apply)) in all_proposals { | 303 | for (proposal, events_to_apply) in all_proposals.values() { |
| 258 | if let Ok(cl) = event_to_cover_letter(&proposal) { | 304 | if let Ok(cl) = event_to_cover_letter(proposal) { |
| 259 | if let Ok(mut branch_name) = cl.get_branch_name_with_pr_prefix_and_shorthand_id() { | 305 | if let Ok(mut branch_name) = cl.get_branch_name_with_pr_prefix_and_shorthand_id() { |
| 260 | branch_name = if let Some(public_key) = current_user { | 306 | branch_name = if let Some(public_key) = current_user { |
| 261 | if proposal.pubkey.eq(&public_key) { | 307 | if proposal.pubkey.eq(&public_key) { |
| @@ -275,7 +321,7 @@ async fn get_all_proposals_state( | |||
| 275 | state.insert(format!("refs/pr/{}/head", proposal.id), tip); | 321 | state.insert(format!("refs/pr/{}/head", proposal.id), tip); |
| 276 | } | 322 | } |
| 277 | } else if let Ok(tip) = | 323 | } else if let Ok(tip) = |
| 278 | make_commits_for_proposal(git_repo, repo_ref, &events_to_apply) | 324 | make_commits_for_proposal(git_repo, repo_ref, events_to_apply) |
| 279 | { | 325 | { |
| 280 | state.insert(format!("refs/{branch_name}"), tip.clone()); | 326 | state.insert(format!("refs/{branch_name}"), tip.clone()); |
| 281 | state.insert(format!("refs/pr/{}/head", proposal.id), tip); | 327 | state.insert(format!("refs/pr/{}/head", proposal.id), tip); |