pointbreak 0.7.0

Durable terminal code review for changes humans and coding agents collaborate on together
Documentation
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
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
//! The repo/store identity the inspector chrome renders (issue #391).
//!
//! A deliberately lightweight, path-private sibling of [`store_status`]: it reports
//! *which* repository and store the inspector is serving, without the full
//! store-inventory and worktree-sensitivity scans `store_status` runs. Identity is a
//! chrome cue, so it stays cheap.
//!
//! Path-privacy is the load-bearing constraint (issue #391): no field ever carries an
//! absolute or repo-relative filesystem path. Path **basenames** (final components),
//! the opaque family slug, and one-way store/context path hashes cross the boundary —
//! the same label convention the inspector's revision endpoints already follow, plus
//! opaque equality keys that reveal no path (`src/cli/inspect/api.rs`).
//!
//! [`store_status`]: super::store_status::store_status

use std::ffi::OsString;
use std::fmt::Write as _;
use std::path::{Path, PathBuf};

use serde::Serialize;
use sha2::{Digest, Sha256};

use crate::error::{Result, ShoreError};
use crate::git::{git_common_dir, git_worktree_root};
use crate::session::store::resolution::resolve_store;

/// Floor label when no basename can be derived (unusual git layouts). Mirrors the
/// `WORKING_TREE_FLOOR` idea in `src/cli/inspect/api.rs`; kept as a lib-local copy
/// because that constant is private to the binary crate.
const REPOSITORY_FLOOR: &str = "repository";

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct StoreIdentityOptions {
    repo: PathBuf,
}

impl StoreIdentityOptions {
    pub fn new(repo: impl AsRef<Path>) -> Self {
        Self {
            repo: repo.as_ref().to_path_buf(),
        }
    }
}

/// The path-private repo/store identity document the inspector renders.
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct StoreIdentity {
    /// Opaque identity of the resolved store directory. Equality is the contract;
    /// callers must not parse the digest.
    pub store_identity: String,
    /// Opaque identity of the current Git worktree root. Distinguishes contexts
    /// that share one store without exposing either path.
    pub context_identity: String,
    /// Stable repository label: the main-worktree-root basename (path-free).
    pub repository: String,
    /// The current worktree-root basename; present ONLY when it differs from
    /// `repository` (a linked worktree — the common-dir store serves several).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub worktree: Option<String>,
    /// Which store tier is being read.
    pub placement: StorePlacement,
    /// The repository family; present ONLY under the user-level (family) tier.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub family: Option<StoreFamily>,
}

/// The resolved store placement tier.
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct StorePlacement {
    /// Domain-named tier tag: `"clone"` | `"family"` | `"ephemeral"`.
    pub tier: &'static str,
    /// Human label: `"clone store"` | `"family store"` | `"ephemeral store"`.
    pub label: &'static str,
}

/// The repository family a user-level store serves.
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct StoreFamily {
    /// The family slug (`repository_family_ref`) — a non-identity placement label.
    pub id: String,
}

/// Derive the [`StoreIdentity`] for `repo`. Reuses the same store resolver every read
/// surface uses, plus git basenames; performs no store-inventory or sensitivity scan.
pub fn store_identity(options: StoreIdentityOptions) -> Result<StoreIdentity> {
    let worktree_root = git_worktree_root(&options.repo)?;
    let resolution = resolve_store(&options.repo)?;
    let store_identity = opaque_path_identity("store", resolution.store_dir())?;
    let context_identity = opaque_path_identity("context", &worktree_root)?;
    let view = resolution.command_view();
    let placement = placement_for(view.mode);
    let family = view.repository_family_ref.map(|id| StoreFamily { id });

    // `repository` is the stable main-clone basename; `worktree` names the current
    // checkout only when it differs (a linked worktree — the shared store serves
    // several). Both are basenames, never paths.
    let current = basename(&worktree_root).unwrap_or_else(|| REPOSITORY_FLOOR.to_owned());
    let repository = main_worktree_basename(&options.repo).unwrap_or_else(|| current.clone());
    let worktree = (current != repository).then_some(current);

    Ok(StoreIdentity {
        store_identity,
        context_identity,
        repository,
        worktree,
        placement,
        family,
    })
}

/// Map `command_view().mode` to a placement. The single site for this mapping.
fn placement_for(mode: &str) -> StorePlacement {
    match mode {
        "user-level" => StorePlacement {
            tier: "family",
            label: "family store",
        },
        "ephemeral" => StorePlacement {
            tier: "ephemeral",
            label: "ephemeral store",
        },
        // "local" and any unexpected value floor to the clone-local default.
        _ => StorePlacement {
            tier: "clone",
            label: "clone store",
        },
    }
}

/// `basename(parent(git_common_dir(repo)))` — the stable main-clone name. `None` when
/// the common dir has no parent basename (unusual layouts); the caller falls back.
fn main_worktree_basename(repo: &Path) -> Option<String> {
    let common = git_common_dir(repo).ok()?; // <main>/.git (absolute)
    basename(common.parent()?) // <main>
}

/// Final non-empty path component, or `None` when the path has none.
fn basename(path: &Path) -> Option<String> {
    path.file_name()
        .and_then(|name| name.to_str())
        .filter(|name| !name.is_empty())
        .map(str::to_owned)
}

/// Hash a normalized path into the opaque identity shared by `store status` and
/// `/api/identity`. This stays workflow-private: equality, not path recovery or
/// digest parsing, is the public contract.
pub(super) fn opaque_path_identity(namespace: &str, path: &Path) -> Result<String> {
    let normalized = normalize_path_without_requiring_leaf(path)?;
    let digest = Sha256::digest(normalized.as_os_str().as_encoded_bytes());
    let mut hex = String::with_capacity(digest.len() * 2);
    for byte in digest {
        write!(&mut hex, "{byte:02x}").expect("writing to a string cannot fail");
    }
    Ok(format!("{namespace}:sha256:{hex}"))
}

fn normalize_path_without_requiring_leaf(path: &Path) -> Result<PathBuf> {
    let absolute = if path.is_absolute() {
        path.to_path_buf()
    } else {
        std::env::current_dir()
            .map_err(|error| ShoreError::Message(format!("resolve current directory: {error}")))?
            .join(path)
    };
    let mut existing = absolute.as_path();
    let mut missing = Vec::<OsString>::new();

    while !existing.try_exists().map_err(|error| {
        ShoreError::Message(format!(
            "inspect identity path {}: {error}",
            existing.display()
        ))
    })? {
        let name = existing.file_name().ok_or_else(|| {
            ShoreError::Message(format!(
                "cannot find an existing ancestor for identity path {}",
                absolute.display()
            ))
        })?;
        missing.push(name.to_owned());
        existing = existing.parent().ok_or_else(|| {
            ShoreError::Message(format!(
                "cannot find an existing ancestor for identity path {}",
                absolute.display()
            ))
        })?;
    }

    let mut normalized = existing.canonicalize().map_err(|error| {
        ShoreError::Message(format!(
            "canonicalize identity path ancestor {}: {error}",
            existing.display()
        ))
    })?;
    for component in missing.into_iter().rev() {
        normalized.push(component);
    }
    Ok(normalized)
}

#[cfg(test)]
mod tests {
    use std::ffi::{OsStr, OsString};
    use std::path::Path;
    use std::process::Command;

    use tempfile::TempDir;

    use super::*;
    use crate::session::store::store_config::{StoreMode, write_store_config};
    use crate::session::{StoreLinkOptions, link_store_to_family};

    struct TestRepo {
        root: TempDir,
    }

    impl TestRepo {
        fn new() -> Self {
            let root = TempDir::new().expect("create temp git repository directory");
            let repo = Self { root };
            repo.git(["init"]);
            repo.git(["config", "user.name", "Shore Tests"]);
            repo.git(["config", "user.email", "shore-tests@example.com"]);
            repo.git(["config", "commit.gpgsign", "false"]);
            repo
        }
        fn path(&self) -> &Path {
            self.root.path()
        }
        fn write(&self, path: &str, contents: &str) {
            let path = self.root.path().join(path);
            if let Some(parent) = path.parent() {
                std::fs::create_dir_all(parent).unwrap();
            }
            std::fs::write(path, contents).unwrap();
        }
        fn commit_all(&self, message: &str) {
            self.git(["add", "--all"]);
            self.git(["commit", "-m", message]);
        }
        fn git<I, S>(&self, args: I)
        where
            I: IntoIterator<Item = S>,
            S: AsRef<OsStr>,
        {
            run_git(self.root.path(), args);
        }
    }

    fn run_git<I, S>(cwd: &Path, args: I)
    where
        I: IntoIterator<Item = S>,
        S: AsRef<OsStr>,
    {
        let output = Command::new("git")
            .args(args)
            .current_dir(cwd)
            .output()
            .expect("run git");
        assert!(
            output.status.success(),
            "git failed in {}\nstderr:\n{}",
            cwd.display(),
            String::from_utf8_lossy(&output.stderr)
        );
    }

    /// A main clone plus one real linked worktree on a fresh branch, sharing the
    /// common-dir store.
    struct LinkedWorktreeFixture {
        main: TestRepo,
        _parent: TempDir,
        linked_path: PathBuf,
    }

    impl LinkedWorktreeFixture {
        fn new(dir_name: &str, branch: &str) -> Self {
            let main = TestRepo::new();
            main.write("README.md", "base\n");
            main.commit_all("base");

            let parent = TempDir::new().expect("worktree parent");
            let linked_path = parent.path().join(dir_name);
            main.git([
                OsString::from("worktree"),
                OsString::from("add"),
                OsString::from("-b"),
                OsString::from(branch),
                linked_path.clone().into_os_string(),
            ]);

            Self {
                main,
                _parent: parent,
                linked_path,
            }
        }
    }

    /// Set `POINTBREAK_HOME` for the duration of `f`. nextest's process-per-test keeps the
    /// mutation contained (the `keys/home.rs` seam). SAFETY: single-threaded test.
    fn with_pointbreak_home<T>(home: &Path, f: impl FnOnce() -> T) -> T {
        unsafe {
            std::env::set_var("POINTBREAK_HOME", home);
        }
        let out = f();
        unsafe {
            std::env::remove_var("POINTBREAK_HOME");
        }
        out
    }

    #[test]
    fn clone_local_identity_reports_clone_placement_and_no_family() {
        let repo = TestRepo::new();
        repo.write("README.md", "base\n");
        repo.commit_all("base");

        let id = store_identity(StoreIdentityOptions::new(repo.path())).unwrap();

        assert_eq!(id.placement.tier, "clone");
        assert_eq!(id.placement.label, "clone store");
        assert!(id.family.is_none());
        // The main worktree: `worktree` is suppressed (equals `repository`).
        assert!(id.worktree.is_none());
        // `repository` is a basename — never an absolute path.
        assert!(!id.repository.is_empty());
        assert!(!id.repository.contains(std::path::MAIN_SEPARATOR));
        // The basename matches the repo directory name.
        let expected = repo.path().file_name().unwrap().to_str().unwrap();
        assert_eq!(id.repository, expected);
    }

    #[test]
    fn ephemeral_mode_reports_ephemeral_placement() {
        let repo = TestRepo::new();
        repo.write("README.md", "base\n");
        repo.commit_all("base");
        write_store_config(repo.path(), StoreMode::Ephemeral).unwrap();

        let id = store_identity(StoreIdentityOptions::new(repo.path())).unwrap();

        assert_eq!(id.placement.tier, "ephemeral");
        assert_eq!(id.placement.label, "ephemeral store");
        assert!(id.family.is_none());
    }

    #[test]
    fn user_level_identity_reports_family_placement_and_slug() {
        let repo = TestRepo::new();
        repo.write("README.md", "base\n");
        repo.commit_all("base");

        let home = TempDir::new().unwrap();
        let id = with_pointbreak_home(home.path(), || {
            link_store_to_family(StoreLinkOptions::new(
                repo.path(),
                Some("acme-web".to_owned()),
            ))
            .expect("link a clean, non-ephemeral, non-sensitive worktree");
            store_identity(StoreIdentityOptions::new(repo.path()))
        })
        .unwrap();

        assert_eq!(id.placement.tier, "family");
        assert_eq!(id.placement.label, "family store");
        assert_eq!(id.family.as_ref().map(|f| f.id.as_str()), Some("acme-web"));
    }

    #[test]
    fn linked_worktree_surfaces_the_worktree_basename_distinct_from_repository() {
        // A linked worktree shares the main clone's common-dir store, so `repository`
        // is the MAIN clone basename and `worktree` is the linked-worktree basename.
        let fixture = LinkedWorktreeFixture::new("feat-foo", "feat/foo");
        let id = store_identity(StoreIdentityOptions::new(&fixture.linked_path)).unwrap();

        let main_basename = fixture.main.path().file_name().unwrap().to_str().unwrap();
        assert_eq!(id.repository, main_basename);
        assert_eq!(id.worktree.as_deref(), Some("feat-foo"));
        assert_ne!(id.repository, id.worktree.clone().unwrap());
    }

    #[test]
    fn serialized_identity_carries_no_absolute_path() {
        let repo = TestRepo::new();
        repo.write("README.md", "base\n");
        repo.commit_all("base");

        let id = store_identity(StoreIdentityOptions::new(repo.path())).unwrap();
        let json = serde_json::to_string(&id).unwrap();
        let abs = repo.path().to_str().unwrap();
        assert!(
            !json.contains(abs),
            "identity JSON leaked an absolute path: {json}"
        );
        assert!(json.contains("\"placement\""));
        assert!(json.contains("\"tier\":\"clone\""));
    }
}