From ac4690cc4bc1ff418ac58f4d567f1db75387be46 Mon Sep 17 00:00:00 2001 From: Shawn Hartsock Date: Sun, 31 May 2026 11:49:16 -0400 Subject: [PATCH] M1 R3: current_branch via gix head.referent_name() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit src/repo/current_branch.rs — `current_branch(path) -> Result>` returns the short current-branch name (`Ok(None)` on detached HEAD) via gix `head.referent_name().shorten()`; registered + wired through the soft-fail PyO3 wrapper. Parity-tested vs `git rev-parse --abbrev-ref HEAD` plus a detached-HEAD case. PROVENANCE: implementation + tests written by the local model qwen2.5-coder:32b, driven headlessly through `newt worker` (newt-agent's ACP worker) by the pilot, which applied the module registration + PyO3 wrapper wiring and any clippy fixups. Co-Authored-By: qwen2.5-coder:32b Model: qwen2.5-coder:32b Piloted-by: newt-agent Co-Authored-By: Claude Opus 4.8 (1M context) --- src/python.rs | 5 +++-- src/repo/current_branch.rs | 38 ++++++++++++++++++++++++++++++++++++++ src/repo/mod.rs | 3 +++ 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 src/repo/current_branch.rs diff --git a/src/python.rs b/src/python.rs index ecc15fd..c041bad 100644 --- a/src/python.rs +++ b/src/python.rs @@ -57,8 +57,9 @@ fn is_clean(_path: String) -> PyResult { } #[pyfunction] -fn current_branch(_path: String) -> PyResult> { - todo!("repo::current_branch (None if detached)") +fn current_branch(path: String) -> PyResult> { + // soft-fail: any error -> None (API.md) + Ok(crate::repo::current_branch(std::path::Path::new(&path)).unwrap_or(None)) } #[pyfunction] diff --git a/src/repo/current_branch.rs b/src/repo/current_branch.rs new file mode 100644 index 0000000..09630c8 --- /dev/null +++ b/src/repo/current_branch.rs @@ -0,0 +1,38 @@ +use crate::error::GitxtendError; +use crate::repo::Result; +use std::path::Path; + +/// Short name of the current branch (e.g. "main"); `Ok(None)` when HEAD is +/// detached (mirrors `git rev-parse --abbrev-ref HEAD`, which prints "HEAD" +/// when detached — we return None in that case). +pub fn current_branch(path: &Path) -> Result> { + let repo = gix::open(path).map_err(GitxtendError::from_err)?; + let head = repo.head().map_err(GitxtendError::from_err)?; + Ok(head.referent_name().map(|n| n.shorten().to_string())) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::repo::fixtures; + + #[test] + fn test_current_branch_on_main() { + let td = fixtures::repo(); + let p = td.path(); + assert_eq!(current_branch(p).unwrap(), Some("main".into())); + assert_eq!( + current_branch(p).unwrap(), + Some(fixtures::git(p, &["rev-parse", "--abbrev-ref", "HEAD"])) + ); + } + + #[test] + fn test_current_branch_detached_head() { + let td = fixtures::repo(); + let p = td.path(); + let sha = fixtures::git(p, &["rev-parse", "HEAD"]); + fixtures::git(p, &["checkout", "--detach", &sha]); + assert_eq!(current_branch(p).unwrap(), None); + } +} diff --git a/src/repo/mod.rs b/src/repo/mod.rs index 994d8d4..d14428f 100644 --- a/src/repo/mod.rs +++ b/src/repo/mod.rs @@ -26,6 +26,9 @@ pub use is_git_repo::is_git_repo; mod head_sha; pub use head_sha::head_sha; +mod current_branch; +pub use current_branch::current_branch; + /// Temp-dir git fixtures shared by the per-method parity tests. /// /// Fixtures are built with the real `git` CLI, so each parity test asserts