feat(maestro): gitroot walk-up probe for worktree base_repo inference (#25)
ci / rust (push) Successful in 5m56s

Add a pure `gitroot::find_root` resolver that walks up from a path to the
nearest ancestor holding a `.git` entry (directory or linked-worktree file)
and returns that repo toplevel, else None. Mirrors the `dirlist` module
shape: filesystem-only, no `git` subprocess, unit-tested in isolation.

Drives the spawn form's worktree section (#21): infer `base_repo` from the
chosen working dir so a worktree spawn needs only a branch. HTTP/wire
exposure lands in #26.

Closes #25

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-20 20:37:17 +00:00
co-authored by Claude Opus 4.8
parent 09093bd2f6
commit 099dfc17b0
2 changed files with 119 additions and 0 deletions
+118
View File
@@ -0,0 +1,118 @@
//! 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 registry-client task /
//! 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()));
}
}
+1
View File
@@ -13,6 +13,7 @@ pub mod cli;
pub mod config;
pub mod configure;
pub mod dirlist;
pub mod gitroot;
pub mod jwt;
pub mod local_api;
pub mod local_events;