use std::path::{Path, PathBuf};
use std::process::Command;
pub fn find_repo_root_from_cwd() -> Option<PathBuf> {
if let Ok(dir) = std::env::var("CLAUDE_PROJECT_DIR") {
let candidate = PathBuf::from(&dir);
if candidate.is_dir() {
return Some(candidate);
}
}
if let Ok(output) = Command::new("git")
.args(["rev-parse", "--show-toplevel"])
.output()
{
if output.status.success() {
let raw = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !raw.is_empty() {
let candidate = PathBuf::from(raw);
if candidate.is_dir() {
return Some(candidate);
}
}
}
}
std::env::current_dir().ok()
}
pub fn compute_base_ref(cwd: &Path) -> String {
const CANDIDATES: &[&str] = &["@{upstream}", "origin/main", "origin/master"];
for candidate in CANDIDATES {
if let Some(sha) = git_merge_base(cwd, candidate, "HEAD") {
return sha;
}
}
"HEAD~1".to_string()
}
fn git_merge_base(cwd: &Path, a: &str, b: &str) -> Option<String> {
let output = Command::new("git")
.args(["merge-base", a, b])
.current_dir(cwd)
.output()
.ok()?;
if !output.status.success() {
return None;
}
let raw = String::from_utf8_lossy(&output.stdout).trim().to_string();
if raw.is_empty() {
None
} else {
Some(raw)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::process::Command;
use tempfile::TempDir;
fn init_repo_with_commits(dir: &Path, commits: usize) {
run(dir, &["init", "--initial-branch=main"]);
run(dir, &["config", "user.email", "klasp-test@example.com"]);
run(dir, &["config", "user.name", "klasp-test"]);
run(dir, &["config", "commit.gpgsign", "false"]);
for i in 0..commits {
std::fs::write(dir.join(format!("f{i}.txt")), format!("commit {i}"))
.expect("write fixture file");
run(dir, &["add", "."]);
run(dir, &["commit", "-m", &format!("c{i}")]);
}
}
fn run(cwd: &Path, args: &[&str]) {
let status = Command::new("git")
.args(args)
.current_dir(cwd)
.status()
.expect("spawn git");
assert!(status.success(), "git {args:?} failed");
}
#[test]
fn compute_base_ref_falls_back_to_head_tilde_one_without_remote() {
let tmp = TempDir::new().unwrap();
init_repo_with_commits(tmp.path(), 2);
assert_eq!(compute_base_ref(tmp.path()), "HEAD~1");
}
#[test]
fn compute_base_ref_uses_upstream_when_available() {
let upstream_tmp = TempDir::new().unwrap();
let local_tmp = TempDir::new().unwrap();
init_repo_with_commits(upstream_tmp.path(), 1);
run(
local_tmp.path(),
&[
"clone",
upstream_tmp.path().to_str().unwrap(),
local_tmp.path().to_str().unwrap(),
],
);
run(
local_tmp.path(),
&["config", "user.email", "klasp-test@example.com"],
);
run(local_tmp.path(), &["config", "user.name", "klasp-test"]);
run(local_tmp.path(), &["config", "commit.gpgsign", "false"]);
let expected = String::from_utf8_lossy(
&Command::new("git")
.args(["rev-parse", "origin/main"])
.current_dir(local_tmp.path())
.output()
.expect("rev-parse origin/main")
.stdout,
)
.trim()
.to_string();
std::fs::write(local_tmp.path().join("local.txt"), "local").unwrap();
run(local_tmp.path(), &["add", "."]);
run(local_tmp.path(), &["commit", "-m", "local divergence"]);
let got = compute_base_ref(local_tmp.path());
assert_eq!(got, expected, "merge-base @{{u}} should match origin/main");
}
}