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 04062cbb688a011e0801a77b4094dd7120a14dcc
Parent: 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-cospan
1 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 
cospan · schematic version control on atproto built on AT Protocol