This commit is contained in:
2026-08-25 17:23:32 +07:00
parent e34008775c
commit e1eef3d107
9 changed files with 1090 additions and 51 deletions
+88
View File
@@ -1245,6 +1245,53 @@ pub fn current_branch(repo: &gix::Repository) -> Result<Option<String>> {
Ok(Some(String::from_utf8_lossy(name.shorten()).into_owned()))
}
/// Branch, tag and HEAD refs of a repository, ready for a NIP-34 kind-30618
/// repository state announcement.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RepoRefState {
/// `(full refname, commit id)` pairs for heads and tags, sorted.
pub refs: Vec<(String, String)>,
/// Short branch name HEAD points to, or `None` when detached.
pub head: Option<String>,
}
/// Collect the refs of `repo`: local branches and tags as
/// `(refname, commit-id)` pairs, plus the branch HEAD points to.
pub fn repo_ref_state(repo: &gix::Repository) -> Result<RepoRefState> {
let mut refs = Vec::new();
for reference in repo.references()?.local_branches()? {
let reference = reference.map_err(|error| anyhow::anyhow!("{error}"))?;
refs.push((
String::from_utf8_lossy(reference.name().as_bstr()).into_owned(),
reference.id().to_string(),
));
}
for reference in repo.references()?.tags()? {
let reference = reference.map_err(|error| anyhow::anyhow!("{error}"))?;
refs.push((
String::from_utf8_lossy(reference.name().as_bstr()).into_owned(),
reference.id().to_string(),
));
}
refs.sort();
let head = match repo.head() {
Ok(head) => head
.referent_name()
.filter(|name| name.as_bstr().starts_with(b"refs/heads/"))
.map(|name| String::from_utf8_lossy(name.shorten()).into_owned()),
Err(_) => None,
};
Ok(RepoRefState { refs, head })
}
/// [`repo_ref_state`] for the repository at `workdir`.
pub fn worktree_ref_state(workdir: &Path) -> Result<RepoRefState> {
repo_ref_state(&open_with_cache(workdir)?)
}
/// Everything the browser needs to refresh after a branch or tag switch.
pub struct WorktreeSnapshot {
/// Relative paths of all worktree entries, directories first.
@@ -1375,6 +1422,47 @@ mod tests {
);
}
#[test]
fn repo_ref_state_lists_branches_tags_and_head() {
let (_dir, repo) = fixture(&[("a.txt", b"hello")]);
commit_all(&repo, "initial");
let workdir = repo.workdir().expect("workdir").to_path_buf();
let state = repo_ref_state(&repo).expect("refs");
let branch = current_branch(&repo).expect("branch").expect("on a branch");
assert_eq!(state.head.as_deref(), Some(branch.as_str()));
assert_eq!(state.refs.len(), 1);
assert_eq!(state.refs[0].0, format!("refs/heads/{branch}"));
assert_eq!(state.refs[0].1.len(), 40);
// Additional branches and tags are listed alongside.
git_run(&workdir, &["branch", "feature"]);
git_run(&workdir, &["tag", "v1.0"]);
let state = repo_ref_state(&repo).expect("refs");
let mut expected: Vec<String> = vec![
format!("refs/heads/{branch}"),
"refs/heads/feature".to_owned(),
"refs/tags/v1.0".to_owned(),
];
expected.sort();
assert_eq!(
state
.refs
.iter()
.map(|(name, _)| name.clone())
.collect::<Vec<_>>(),
expected
);
// A detached HEAD yields no head branch.
git_run(&workdir, &["checkout", "--detach"]);
let state = repo_ref_state(&repo).expect("refs");
assert!(state.head.is_none());
assert_eq!(state.refs.len(), 3);
}
/// Build a throwaway non-bare repository with the given files (rel → bytes).
fn fixture(files: &[(&str, &[u8])]) -> (tempfile::TempDir, gix::Repository) {
let dir = tempfile::tempdir().expect("tempdir");