Skip to content

Commit f7bbe10

Browse files
hartsockqwen2.5-coder:32bclaude
authored
M1 R4: tracking_branch via git config (branch.<n>.remote/.merge) (#6)
src/repo/tracking_branch.rs — `tracking_branch(path) -> Result<Option<String>>` resolves the current branch's configured upstream to a short "remote/branch" name (e.g. "origin/main") from `branch.<name>.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. Model: qwen2.5-coder:32b Piloted-by: newt-agent Co-authored-by: Shawn Hartsock <hartsock@users.noreply.github.com> Co-authored-by: qwen2.5-coder:32b <model@newt.local> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 89ebb73 commit f7bbe10

3 files changed

Lines changed: 68 additions & 2 deletions

File tree

src/python.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,9 @@ fn current_branch(path: String) -> PyResult<Option<String>> {
6363
}
6464

6565
#[pyfunction]
66-
fn tracking_branch(_path: String) -> PyResult<Option<String>> {
67-
todo!("repo::tracking_branch (configured upstream)")
66+
fn tracking_branch(path: String) -> PyResult<Option<String>> {
67+
// soft-fail: any error -> None (API.md)
68+
Ok(crate::repo::tracking_branch(std::path::Path::new(&path)).unwrap_or(None))
6869
}
6970

7071
#[pyfunction]

src/repo/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ pub use head_sha::head_sha;
2929
mod current_branch;
3030
pub use current_branch::current_branch;
3131

32+
mod tracking_branch;
33+
pub use tracking_branch::tracking_branch;
34+
3235
/// Temp-dir git fixtures shared by the per-method parity tests.
3336
///
3437
/// Fixtures are built with the real `git` CLI, so each parity test asserts

src/repo/tracking_branch.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
use crate::error::GitxtendError;
2+
use crate::repo::Result;
3+
use std::path::Path;
4+
5+
/// The configured upstream of the current branch as a short "remote/branch"
6+
/// name (e.g. "origin/main"); `Ok(None)` if there is no upstream or HEAD is
7+
/// detached. Mirrors `git rev-parse --abbrev-ref @{upstream}`.
8+
pub fn tracking_branch(path: &Path) -> Result<Option<String>> {
9+
let repo = gix::open(path).map_err(GitxtendError::from_err)?;
10+
let head = repo.head().map_err(GitxtendError::from_err)?;
11+
let branch = match head.referent_name() {
12+
Some(n) => n.shorten().to_string(), // e.g. "main"
13+
None => return Ok(None), // detached
14+
};
15+
let cfg = repo.config_snapshot();
16+
// branch.<name>.remote -> e.g. "origin" ; branch.<name>.merge -> "refs/heads/main"
17+
let remote_key = format!("branch.{branch}.remote");
18+
let merge_key = format!("branch.{branch}.merge");
19+
let remote = cfg.string(remote_key.as_str()).map(|v| v.to_string());
20+
let merge = cfg.string(merge_key.as_str()).map(|v| v.to_string());
21+
match (remote, merge) {
22+
(Some(r), Some(m)) => {
23+
let short = m.strip_prefix("refs/heads/").unwrap_or(&m);
24+
Ok(Some(format!("{r}/{short}")))
25+
}
26+
_ => Ok(None),
27+
}
28+
}
29+
30+
#[cfg(test)]
31+
mod tests {
32+
use super::*;
33+
use crate::repo::fixtures::{git, repo};
34+
35+
#[test]
36+
fn no_upstream() {
37+
let td = repo();
38+
assert_eq!(tracking_branch(td.path()).unwrap(), None);
39+
}
40+
41+
#[test]
42+
fn with_upstream() {
43+
let td = repo();
44+
let remote = tempfile::tempdir().unwrap();
45+
git(remote.path(), &["init", "--bare", "-q", "-b", "main"]);
46+
git(
47+
td.path(),
48+
&["remote", "add", "origin", &remote.path().to_string_lossy()],
49+
);
50+
git(td.path(), &["push", "-q", "-u", "origin", "main"]);
51+
52+
let expected = Some("origin/main".into());
53+
assert_eq!(tracking_branch(td.path()).unwrap(), expected);
54+
assert_eq!(
55+
tracking_branch(td.path()).unwrap(),
56+
Some(git(
57+
td.path(),
58+
&["rev-parse", "--abbrev-ref", "@{upstream}"]
59+
))
60+
);
61+
}
62+
}

0 commit comments

Comments
 (0)