aboutsummaryrefslogtreecommitdiff
path: root/src/workspace
diff options
context:
space:
mode:
authorLane <150401101+LaneBirmingham@users.noreply.github.com>2026-06-03 05:23:51 +1000
committerGitHub <noreply@github.com>2026-06-02 22:23:51 +0300
commita0b124c3822023655206aaaef5a32c37d3ac18a4 (patch)
tree23a4814e04bb740bb9d9e29bcd5899ff21ad1c3c /src/workspace
parentb144f608d682f3aa518b50deebf1e5b66adfd132 (diff)
fix: resolve git branch names in reftable repos (#423)
* fix: resolve git branch names in reftable repos refs #384 * fix: limit git plumbing to reftable refs refs #384 --------- Co-authored-by: Can Celik <ogulcancelik@gmail.com>
Diffstat (limited to 'src/workspace')
-rw-r--r--src/workspace/git/discovery.rs126
-rw-r--r--src/workspace/git/status.rs72
2 files changed, 194 insertions, 4 deletions
diff --git a/src/workspace/git/discovery.rs b/src/workspace/git/discovery.rs
index de4b090c..7e973bb9 100644
--- a/src/workspace/git/discovery.rs
+++ b/src/workspace/git/discovery.rs
@@ -111,6 +111,11 @@ fn git_common_dir_for_git_dir(git_dir: &Path) -> PathBuf {
pub fn git_branch(cwd: &Path) -> Option<String> {
let repo_root = git_repo_root(cwd)?;
let git_dir = git_dir_for_repo_root(&repo_root)?;
+ let git_common_dir = git_common_dir_for_git_dir(&git_dir);
+ if git_ref_storage_is_reftable(&git_common_dir) {
+ return git_symbolic_head_short(&repo_root);
+ }
+
let head = std::fs::read_to_string(git_dir.join("HEAD")).ok()?;
parse_git_head_branch(&head)
}
@@ -131,11 +136,86 @@ pub(super) fn git_dir_for_repo_root(repo_root: &Path) -> Option<PathBuf> {
})
}
+pub(super) fn git_symbolic_head_full(repo_root: &Path) -> Option<String> {
+ git_trimmed_stdout(repo_root, &["symbolic-ref", "--quiet", "HEAD"])
+}
+
+fn git_symbolic_head_short(repo_root: &Path) -> Option<String> {
+ git_trimmed_stdout(repo_root, &["symbolic-ref", "--quiet", "--short", "HEAD"])
+}
+
+pub(super) fn git_rev_parse_verify(repo_root: &Path, revision: &str) -> Option<String> {
+ git_trimmed_stdout(repo_root, &["rev-parse", "--verify", revision])
+}
+
+pub(super) fn git_ref_storage_is_reftable(git_common_dir: &Path) -> bool {
+ read_git_config_value(&git_common_dir.join("config"), "extensions", "refstorage")
+ .is_some_and(|value| value.eq_ignore_ascii_case("reftable"))
+}
+
fn parse_git_head_branch(head: &str) -> Option<String> {
let branch = head.trim().strip_prefix("ref: refs/heads/")?;
(!branch.is_empty()).then(|| branch.to_string())
}
+fn read_git_config_value(path: &Path, section: &str, key: &str) -> Option<String> {
+ let contents = std::fs::read_to_string(path).ok()?;
+ let mut in_section = false;
+ for raw_line in contents.lines() {
+ let line = raw_line.trim();
+ if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
+ continue;
+ }
+ if let Some(section_name) = simple_git_config_section(line) {
+ in_section = section_name.eq_ignore_ascii_case(section);
+ continue;
+ }
+ if !in_section {
+ continue;
+ }
+ let Some((name, value)) = line.split_once('=') else {
+ continue;
+ };
+ if name.trim().eq_ignore_ascii_case(key) {
+ return Some(strip_git_config_comment(value).trim().to_string());
+ }
+ }
+ None
+}
+
+fn simple_git_config_section(line: &str) -> Option<&str> {
+ let section = line.strip_prefix('[')?.split_once(']')?.0.trim();
+ (!section.contains('"')).then_some(section)
+}
+
+fn strip_git_config_comment(value: &str) -> &str {
+ let value = value.trim();
+ for marker in ['#', ';'] {
+ if let Some((prefix, _)) = value.split_once(marker) {
+ if prefix.chars().next_back().is_some_and(char::is_whitespace) {
+ return prefix;
+ }
+ }
+ }
+ value
+}
+
+fn git_trimmed_stdout(repo_root: &Path, args: &[&str]) -> Option<String> {
+ let output = std::process::Command::new("git")
+ .arg("-C")
+ .arg(repo_root)
+ .args(args)
+ .output()
+ .ok()?;
+ if !output.status.success() {
+ return None;
+ }
+
+ let stdout = String::from_utf8(output.stdout).ok()?;
+ let stdout = stdout.trim();
+ (!stdout.is_empty()).then(|| stdout.to_string())
+}
+
pub(super) fn git_repo_root(start: &Path) -> Option<PathBuf> {
let mut current = if start.is_dir() {
start.to_path_buf()
@@ -187,6 +267,7 @@ mod tests {
use std::time::{SystemTime, UNIX_EPOCH};
use super::*;
+ use crate::workspace::git::test_support::run_git;
fn temp_test_dir(name: &str) -> PathBuf {
let unique = format!(
@@ -239,6 +320,24 @@ mod tests {
}
#[test]
+ fn git_branch_reads_symbolic_head_from_reftable_repo() {
+ let root = temp_test_dir("reftable-branch");
+ let root_arg = root.to_string_lossy().to_string();
+ let output = std::process::Command::new("git")
+ .args(["init", "--ref-format=reftable", "-b", "main", &root_arg])
+ .output()
+ .unwrap();
+ if !output.status.success() {
+ std::fs::remove_dir_all(root).unwrap();
+ return;
+ }
+
+ assert_eq!(git_branch(&root).as_deref(), Some("main"));
+
+ std::fs::remove_dir_all(root).unwrap();
+ }
+
+ #[test]
fn git_repo_root_ignores_invalid_git_marker() {
let base = temp_test_dir("invalid-git-root");
let cwd = base.join("workspace");
@@ -275,4 +374,31 @@ mod tests {
std::fs::remove_dir_all(root).unwrap();
}
+
+ #[test]
+ fn git_rev_parse_verify_reads_reftable_refs() {
+ let root = temp_test_dir("reftable-ref-oid");
+ let root_arg = root.to_string_lossy().to_string();
+ let output = std::process::Command::new("git")
+ .args(["init", "--ref-format=reftable", "-b", "main", &root_arg])
+ .output()
+ .unwrap();
+ if !output.status.success() {
+ std::fs::remove_dir_all(root).unwrap();
+ return;
+ }
+
+ run_git(&root, &["config", "user.email", "herdr@example.invalid"]);
+ run_git(&root, &["config", "user.name", "Herdr Test"]);
+ run_git(&root, &["commit", "--allow-empty", "-m", "initial"]);
+
+ let head_oid = git_rev_parse_verify(&root, "HEAD").unwrap();
+
+ assert_eq!(
+ git_rev_parse_verify(&root, "refs/heads/main").as_deref(),
+ Some(head_oid.as_str())
+ );
+
+ std::fs::remove_dir_all(root).unwrap();
+ }
}
diff --git a/src/workspace/git/status.rs b/src/workspace/git/status.rs
index 24e59e57..024ffdde 100644
--- a/src/workspace/git/status.rs
+++ b/src/workspace/git/status.rs
@@ -5,7 +5,8 @@ use crate::workspace::WorkspaceGitStatusSnapshot;
use super::{
config::{read_branch_config, upstream_full_ref},
discovery::{
- canonicalize_best_effort_path, git_branch, git_space_metadata, git_worktree_info,
+ canonicalize_best_effort_path, git_branch, git_ref_storage_is_reftable,
+ git_rev_parse_verify, git_space_metadata, git_symbolic_head_full, git_worktree_info,
read_ref_oid, GitWorktreeInfo,
},
};
@@ -52,18 +53,18 @@ pub fn git_status_snapshot_for_cwd(
cwd: &Path,
cached: Option<&GitStatusCacheEntry>,
) -> (WorkspaceGitStatusSnapshot, Option<GitStatusCacheEntry>) {
- let branch = git_branch(cwd);
let space = git_space_metadata(cwd);
let Some(fingerprint) = git_status_fingerprint(cwd) else {
return (
WorkspaceGitStatusSnapshot {
- branch,
+ branch: git_branch(cwd),
ahead_behind: None,
space,
},
None,
);
};
+ let branch = fingerprint.branch_name().map(str::to_string);
if let Some(cached) = cached.filter(|entry| entry.fingerprint == fingerprint) {
let snapshot = WorkspaceGitStatusSnapshot {
@@ -115,6 +116,13 @@ pub(super) fn git_status_fingerprint(cwd: &Path) -> Option<GitStatusFingerprint>
}
impl GitStatusFingerprint {
+ fn branch_name(&self) -> Option<&str> {
+ match &self.head {
+ GitHeadIdentity::Branch { short_name, .. } => Some(short_name.as_str()),
+ GitHeadIdentity::Detached { .. } => None,
+ }
+ }
+
fn head_oid(&self) -> Option<&str> {
match &self.head {
GitHeadIdentity::Branch { oid, .. } => oid.as_deref(),
@@ -130,6 +138,28 @@ impl GitStatusFingerprint {
}
fn read_head_identity(info: &GitWorktreeInfo) -> Option<GitHeadIdentity> {
+ if git_ref_storage_is_reftable(&info.git_common_dir) {
+ return read_head_identity_from_git(info);
+ }
+
+ read_head_identity_from_files(info)
+}
+
+fn read_head_identity_from_git(info: &GitWorktreeInfo) -> Option<GitHeadIdentity> {
+ if let Some(full_ref) = git_symbolic_head_full(&info.repo_root) {
+ let short_name = full_ref.strip_prefix("refs/heads/")?.to_string();
+ let oid = git_rev_parse_verify(&info.repo_root, &full_ref);
+ return Some(GitHeadIdentity::Branch {
+ full_ref,
+ short_name,
+ oid,
+ });
+ }
+
+ git_rev_parse_verify(&info.repo_root, "HEAD").map(|oid| GitHeadIdentity::Detached { oid })
+}
+
+fn read_head_identity_from_files(info: &GitWorktreeInfo) -> Option<GitHeadIdentity> {
let head = std::fs::read_to_string(info.git_dir.join("HEAD")).ok()?;
let head = head.trim();
if let Some(full_ref) = head.strip_prefix("ref: ") {
@@ -150,7 +180,11 @@ fn read_head_identity(info: &GitWorktreeInfo) -> Option<GitHeadIdentity> {
fn read_upstream_identity(info: &GitWorktreeInfo, branch: &str) -> Option<GitUpstreamIdentity> {
let config = read_branch_config(info, branch)?;
let full_ref = upstream_full_ref(&config)?;
- let oid = read_ref_oid(&info.git_common_dir, &full_ref);
+ let oid = if git_ref_storage_is_reftable(&info.git_common_dir) {
+ git_rev_parse_verify(&info.repo_root, &full_ref)
+ } else {
+ read_ref_oid(&info.git_common_dir, &full_ref)
+ };
Some(GitUpstreamIdentity {
remote: config.remote,
merge_ref: config.merge_ref,
@@ -357,6 +391,36 @@ mod tests {
}
#[test]
+ fn git_status_fingerprint_reads_reftable_branch_identity() {
+ let root = temp_test_dir("reftable-fingerprint");
+ let root_arg = root.to_string_lossy().to_string();
+ let output = std::process::Command::new("git")
+ .args(["init", "--ref-format=reftable", "-b", "main", &root_arg])
+ .output()
+ .unwrap();
+ if !output.status.success() {
+ std::fs::remove_dir_all(root).unwrap();
+ return;
+ }
+ run_git(&root, &["config", "user.email", "herdr@example.invalid"]);
+ run_git(&root, &["config", "user.name", "Herdr Test"]);
+ run_git(&root, &["commit", "--allow-empty", "-m", "initial"]);
+
+ let fingerprint = git_status_fingerprint(&root).unwrap();
+
+ assert_eq!(
+ fingerprint.head,
+ GitHeadIdentity::Branch {
+ full_ref: "refs/heads/main".into(),
+ short_name: "main".into(),
+ oid: git_rev_parse_verify(&root, "HEAD"),
+ }
+ );
+
+ std::fs::remove_dir_all(root).unwrap();
+ }
+
+ #[test]
fn git_status_recomputes_ahead_behind_when_head_moves() {
let base = temp_test_dir("head-moves");
let remote = base.join("remote.git");