From 2f1a23c33a5ea0e555f2494349117f794092795e Mon Sep 17 00:00:00 2001 From: Shawn Hartsock Date: Sun, 31 May 2026 11:55:10 -0400 Subject: [PATCH] M1 R4: tracking_branch via git config (branch..remote/.merge) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit src/repo/tracking_branch.rs — `tracking_branch(path) -> Result>` resolves the current branch's configured upstream to a short "remote/branch" name (e.g. "origin/main") from `branch..remote` + `.merge` via gix `config_snapshot().string()`; `Ok(None)` when no upstream or detached. Registered + soft-fail PyO3 wrapper. Parity-tested vs `git rev-parse --abbrev-ref @{upstream}` (with a real bare remote) and the no-upstream 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/mod.rs | 3 ++ src/repo/tracking_branch.rs | 62 +++++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 src/repo/tracking_branch.rs diff --git a/src/python.rs b/src/python.rs index c041bad..5351b4d 100644 --- a/src/python.rs +++ b/src/python.rs @@ -63,8 +63,9 @@ fn current_branch(path: String) -> PyResult> { } #[pyfunction] -fn tracking_branch(_path: String) -> PyResult> { - todo!("repo::tracking_branch (configured upstream)") +fn tracking_branch(path: String) -> PyResult> { + // soft-fail: any error -> None (API.md) + Ok(crate::repo::tracking_branch(std::path::Path::new(&path)).unwrap_or(None)) } #[pyfunction] diff --git a/src/repo/mod.rs b/src/repo/mod.rs index d14428f..04f2ae9 100644 --- a/src/repo/mod.rs +++ b/src/repo/mod.rs @@ -29,6 +29,9 @@ pub use head_sha::head_sha; mod current_branch; pub use current_branch::current_branch; +mod tracking_branch; +pub use tracking_branch::tracking_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 diff --git a/src/repo/tracking_branch.rs b/src/repo/tracking_branch.rs new file mode 100644 index 0000000..f74c510 --- /dev/null +++ b/src/repo/tracking_branch.rs @@ -0,0 +1,62 @@ +use crate::error::GitxtendError; +use crate::repo::Result; +use std::path::Path; + +/// The configured upstream of the current branch as a short "remote/branch" +/// name (e.g. "origin/main"); `Ok(None)` if there is no upstream or HEAD is +/// detached. Mirrors `git rev-parse --abbrev-ref @{upstream}`. +pub fn tracking_branch(path: &Path) -> Result> { + let repo = gix::open(path).map_err(GitxtendError::from_err)?; + let head = repo.head().map_err(GitxtendError::from_err)?; + let branch = match head.referent_name() { + Some(n) => n.shorten().to_string(), // e.g. "main" + None => return Ok(None), // detached + }; + let cfg = repo.config_snapshot(); + // branch..remote -> e.g. "origin" ; branch..merge -> "refs/heads/main" + let remote_key = format!("branch.{branch}.remote"); + let merge_key = format!("branch.{branch}.merge"); + let remote = cfg.string(remote_key.as_str()).map(|v| v.to_string()); + let merge = cfg.string(merge_key.as_str()).map(|v| v.to_string()); + match (remote, merge) { + (Some(r), Some(m)) => { + let short = m.strip_prefix("refs/heads/").unwrap_or(&m); + Ok(Some(format!("{r}/{short}"))) + } + _ => Ok(None), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::repo::fixtures::{git, repo}; + + #[test] + fn no_upstream() { + let td = repo(); + assert_eq!(tracking_branch(td.path()).unwrap(), None); + } + + #[test] + fn with_upstream() { + let td = repo(); + let remote = tempfile::tempdir().unwrap(); + git(remote.path(), &["init", "--bare", "-q", "-b", "main"]); + git( + td.path(), + &["remote", "add", "origin", &remote.path().to_string_lossy()], + ); + git(td.path(), &["push", "-q", "-u", "origin", "main"]); + + let expected = Some("origin/main".into()); + assert_eq!(tracking_branch(td.path()).unwrap(), expected); + assert_eq!( + tracking_branch(td.path()).unwrap(), + Some(git( + td.path(), + &["rev-parse", "--abbrev-ref", "@{upstream}"] + )) + ); + } +}