From b38b645f32d41d40c05326ddb2048bd56185e412 Mon Sep 17 00:00:00 2001 From: Shawn Hartsock Date: Sun, 31 May 2026 11:59:20 -0400 Subject: [PATCH] M1 R5: remote_head_sha via gix rev_parse_single MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit src/repo/remote_head_sha.rs — `remote_head_sha(path, remote_ref) -> Result>` resolves a remote-tracking ref (default "origin/main") to its full hex object id via gix `rev_parse_single`, `Ok(None)` if absent. Registered + PyO3 wrapper (with the remote_ref="origin/main" default restored). Parity-tested vs `git rev-parse origin/main` (real bare remote) and the no-remote 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 | 6 +++-- src/repo/mod.rs | 3 +++ src/repo/remote_head_sha.rs | 47 +++++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 src/repo/remote_head_sha.rs diff --git a/src/python.rs b/src/python.rs index 5351b4d..f7fa15b 100644 --- a/src/python.rs +++ b/src/python.rs @@ -75,8 +75,10 @@ fn head_sha(path: String) -> PyResult> { } #[pyfunction] -fn remote_head_sha(_path: String, _remote_ref: String) -> PyResult> { - todo!("repo::remote_head_sha (resolve remote-tracking ref)") +#[pyo3(signature = (path, remote_ref="origin/main".to_string()))] +fn remote_head_sha(path: String, remote_ref: String) -> PyResult> { + // soft-fail: any error -> None (API.md) + Ok(crate::repo::remote_head_sha(std::path::Path::new(&path), &remote_ref).unwrap_or(None)) } #[pyfunction] diff --git a/src/repo/mod.rs b/src/repo/mod.rs index 04f2ae9..0121f86 100644 --- a/src/repo/mod.rs +++ b/src/repo/mod.rs @@ -32,6 +32,9 @@ pub use current_branch::current_branch; mod tracking_branch; pub use tracking_branch::tracking_branch; +mod remote_head_sha; +pub use remote_head_sha::remote_head_sha; + /// 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/remote_head_sha.rs b/src/repo/remote_head_sha.rs new file mode 100644 index 0000000..6735da7 --- /dev/null +++ b/src/repo/remote_head_sha.rs @@ -0,0 +1,47 @@ +use crate::error::GitxtendError; +use crate::repo::Result; +use std::path::Path; + +/// Object id (full hex) that a remote-tracking ref resolves to, e.g. +/// "origin/main"; `Ok(None)` if that ref doesn't exist. Mirrors +/// `git rev-parse `. +pub fn remote_head_sha(path: &Path, remote_ref: &str) -> Result> { + let repo = gix::open(path).map_err(GitxtendError::from_err)?; + match repo.rev_parse_single(remote_ref) { + Ok(id) => Ok(Some(id.detach().to_string())), + Err(_) => Ok(None), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::repo::fixtures; + use tempfile::tempdir; + + #[test] + fn no_such_remote_ref() { + let td = fixtures::repo(); + let p = td.path(); + assert_eq!(remote_head_sha(p, "origin/main").unwrap(), None); + } + + #[test] + fn with_a_remote() { + let remote = tempdir().unwrap(); + fixtures::git(remote.path(), &["init", "--bare", "-q", "-b", "main"]); + let td = fixtures::repo(); + let p = td.path(); + fixtures::git( + p, + &["remote", "add", "origin", &remote.path().to_string_lossy()], + ); + fixtures::write(p, "file.txt", "content"); + fixtures::git(p, &["add", "-A"]); + fixtures::git(p, &["commit", "-m", "initial commit"]); + fixtures::git(p, &["push", "-q", "-u", "origin", "main"]); + + let expected = fixtures::git(p, &["rev-parse", "origin/main"]); + assert_eq!(remote_head_sha(p, "origin/main").unwrap(), Some(expected)); + } +}