ci / rust (push) Successful in 2m40s
Rewrites CLAUDE.md as the current-architecture reference, drops the registry from INSTALL/PRODUCT/DESIGN and the web README, deletes the registry Dockerfile, and marks the dated design specs superseded where they describe the registry, JWT auth, or cross-host attach. The installer loses its configure step — every config field now has a default, so a daemon starts with no config file at all.
119 lines
4.8 KiB
Rust
119 lines
4.8 KiB
Rust
//! Git-toplevel probe for the spawn-form worktree section (#25).
|
|
//!
|
|
//! When the user picks a working directory, the spawn form wants to offer a
|
|
//! "git worktree" toggle that needs only a branch name — the `base_repo` is
|
|
//! *inferred* from the chosen dir rather than typed. This module answers that
|
|
//! one question: "what git repository, if any, encloses this path?"
|
|
//!
|
|
//! [`find_root`] walks up from a path to the nearest ancestor containing a
|
|
//! `.git` entry and returns that ancestor (the repo toplevel). A regular `.git`
|
|
//! directory and a `.git` *file* (the linked-worktree / submodule form) both
|
|
//! count, so a dir that is itself inside a checked-out worktree still resolves
|
|
//! to its worktree toplevel.
|
|
//!
|
|
//! It's a pure filesystem walk with no `git` subprocess — same shape as
|
|
//! [`crate::dirlist`]: unit-tested in isolation, and the HTTP handler /
|
|
//! HTTP layer (#26) just wraps its `Option` for the wire.
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
/// Walk up from `start` to the nearest ancestor that holds a `.git` entry and
|
|
/// return that ancestor — the enclosing repository's toplevel. Returns `None`
|
|
/// when no ancestor is a git repo.
|
|
///
|
|
/// Both a `.git` directory (ordinary checkout) and a `.git` file (linked
|
|
/// worktree or submodule, which stores a `gitdir:` pointer) count as a repo
|
|
/// marker, matching what `git rev-parse --show-toplevel` would resolve. `start`
|
|
/// itself is checked first, so passing a repo root returns that root.
|
|
///
|
|
/// The input is used as-is: callers that accept abbreviated client paths
|
|
/// should resolve them (e.g. via [`crate::dirlist`]'s home-dir expansion)
|
|
/// before probing, so an empty or `~` path doesn't walk up from the process
|
|
/// working directory.
|
|
pub fn find_root(start: &Path) -> Option<PathBuf> {
|
|
// `ancestors()` yields `start`, its parent, grandparent, … up to the root,
|
|
// so a repo toplevel is matched at the first hit (closest enclosing repo).
|
|
start
|
|
.ancestors()
|
|
.find(|dir| is_git_root(dir))
|
|
.map(Path::to_path_buf)
|
|
}
|
|
|
|
/// Whether `dir` holds a `.git` entry — directory or file. A bare `exists()`
|
|
/// check is deliberate: we don't open or parse it, only detect the marker.
|
|
fn is_git_root(dir: &Path) -> bool {
|
|
dir.join(".git").exists()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::fs;
|
|
|
|
/// A temp repo: `<root>/.git/` plus a nested `a/b/c` subtree. Returns the
|
|
/// temp dir (its `Drop` cleans up) so callers keep it alive for the test.
|
|
fn repo_fixture() -> tempfile::TempDir {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let root = tmp.path();
|
|
fs::create_dir(root.join(".git")).unwrap();
|
|
fs::create_dir_all(root.join("a/b/c")).unwrap();
|
|
tmp
|
|
}
|
|
|
|
#[test]
|
|
fn dir_inside_repo_resolves_to_toplevel() {
|
|
let tmp = repo_fixture();
|
|
// The repo root itself resolves to the repo root.
|
|
assert_eq!(find_root(tmp.path()).as_deref(), Some(tmp.path()));
|
|
// An immediate subdir resolves to the toplevel, not itself.
|
|
assert_eq!(
|
|
find_root(&tmp.path().join("a")).as_deref(),
|
|
Some(tmp.path())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn nested_subdir_resolves_to_toplevel() {
|
|
let tmp = repo_fixture();
|
|
let deep = tmp.path().join("a/b/c");
|
|
assert_eq!(find_root(&deep).as_deref(), Some(tmp.path()));
|
|
}
|
|
|
|
#[test]
|
|
fn non_repo_dir_is_none() {
|
|
// A temp dir with no `.git` anywhere up the chain. Use the temp root so
|
|
// the walk can't accidentally hit a real repo above the test's cwd.
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let sub = tmp.path().join("plain/dir");
|
|
fs::create_dir_all(&sub).unwrap();
|
|
assert_eq!(find_root(&sub), None);
|
|
}
|
|
|
|
#[test]
|
|
fn git_file_worktree_marker_counts_as_root() {
|
|
// Linked worktrees and submodules store `.git` as a *file* holding a
|
|
// `gitdir:` pointer rather than a directory. It must still resolve.
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let root = tmp.path();
|
|
fs::write(root.join(".git"), b"gitdir: /somewhere/.git/worktrees/wt\n").unwrap();
|
|
let sub = root.join("src");
|
|
fs::create_dir(&sub).unwrap();
|
|
assert_eq!(find_root(&sub).as_deref(), Some(root));
|
|
}
|
|
|
|
#[test]
|
|
fn nearest_enclosing_repo_wins() {
|
|
// An inner repo nested inside an outer repo resolves to the *inner*
|
|
// toplevel — the walk stops at the first `.git` it meets going up.
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let outer = tmp.path();
|
|
fs::create_dir(outer.join(".git")).unwrap();
|
|
let inner = outer.join("vendored");
|
|
fs::create_dir(&inner).unwrap();
|
|
fs::create_dir(inner.join(".git")).unwrap();
|
|
let inner_sub = inner.join("src");
|
|
fs::create_dir(&inner_sub).unwrap();
|
|
assert_eq!(find_root(&inner_sub).as_deref(), Some(inner.as_path()));
|
|
}
|
|
}
|