feat(maestro): gitroot walk-up probe for worktree base_repo inference (#25)
ci / rust (push) Successful in 5m56s
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:
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user