fix: negotiate computes need as have minus server objects The handler was walking from server-resolved refs and returning their delta — wrong for push, where the server has none of the incoming refs. For push, the client sends its full local object set as 'have' and expects 'need' to be the subset the server is missing. Loop over have, check store.has(id), include if missing. Drops the want ref-walking entirely: for push it's informational (client follows up with setRef), and for pull the client doesn't use this endpoint.
Author: Aaron Steven White
Commit
04062cbb688a011e0801a77b4094dd7120a14dccParent: 1a79d385f4
Structural diff unavailable
These commits were pushed via plain git push, so no pre-parsed
schemas are available. Install git-remote-cospan and re-push via panproto:// to
see scope-level changes, breaking change detection, and semantic diffs.
brew install panproto/tap/git-remote-cospan1 file changed +17 -39
@@ -1,11 +1,10 @@
1-use std::collections::HashSet; 21 use std::sync::Arc; 32 43 use axum::Json; 54 use axum::extract::State; 65 use serde::Deserialize; 76 8-use panproto_core::vcs::{Object, Store}; 7+use panproto_core::vcs::Store; 98 109 use crate::auth::DidAuth; 1110 use crate::error::NodeError;
@@ -16,9 +15,10 @@ use crate::state::NodeState;
1615 pub struct NegotiateInput { 1716 pub did: String, 1817 pub repo: String, 19- /// Object IDs the client already has. 18+ /// Object IDs the client already has (for push: the full local object set). 2019 pub have: Vec<String>, 21- /// Refs the client wants. 20+ /// Refs the client wants (for push: the ref names it intends to update). 21+ #[serde(default)] 2222 pub want: Vec<String>, 2323 } 2424
@@ -27,6 +27,8 @@ pub async fn negotiate(
2727 _auth: DidAuth, 2828 Json(input): Json<NegotiateInput>, 2929 ) -> Result<Json<serde_json::Value>, NodeError> { 30+ let _ = input.want; // ref names are informational; client follows up with setRef 31+ 3032 let store = state.store.lock().await; 3133 // Auto-init on first push: an authenticated client negotiating against 3234 // a not-yet-existing repo means "I'm about to push you everything".
@@ -34,41 +36,17 @@ pub async fn negotiate(
3436 .open_or_init(&input.did, &input.repo) 3537 .map_err(|e| NodeError::Internal(format!("store error: {e}")))?; 3638 37- // Build set of objects the client already has 38- let have_set: HashSet<String> = input.have.into_iter().collect(); 39- 40- // Resolve wanted refs to object IDs 41- let mut want_ids = Vec::new(); 42- for ref_name in &input.want { 43- if let Ok(Some(target)) = fs_store.get_ref(ref_name) { 44- want_ids.push(target); 45- } 46- } 47- 48- // Walk the commit DAG from each wanted object, collecting all 49- // reachable objects that the client doesn't have. 50- let mut need = Vec::new(); 51- let mut visited = HashSet::new(); 52- let mut queue: Vec<panproto_core::vcs::ObjectId> = want_ids.clone(); 53- 54- while let Some(id) = queue.pop() { 55- let id_str = id.to_string(); 56- if have_set.contains(&id_str) || !visited.insert(id_str.clone()) { 57- continue; 58- } 59- need.push(id_str); 60- 61- // If this object is a commit, follow its parents and schema ref 62- if let Ok(obj) = fs_store.get(&id) 63- && let Object::Commit(commit) = obj 64- { 65- for parent in &commit.parents { 66- queue.push(*parent); 67- } 68- queue.push(commit.schema_id); 69- if let Some(ref mig) = commit.migration_id { 70- queue.push(*mig); 71- } 39+ // For push: the server replies with the subset of `have` it doesn't 40+ // yet have. The client then putObjects each one before calling setRef. 41+ let mut need = Vec::with_capacity(input.have.len()); 42+ for id_str in input.have { 43+ let id = match id_str.parse::<panproto_core::vcs::ObjectId>() { 44+ Ok(id) => id, 45+ Err(_) => continue, 46+ }; 47+ let already_have = fs_store.has(&id); 48+ if !already_have { 49+ need.push(id_str); 7250 } 7351 } 7452