From d942f59dcd2c8d8783e8aa2600221b7bd7bb46e2 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Fri, 6 Nov 2026 17:18:48 +0800 Subject: [PATCH] fix(vcs): exclude symbolic aliases from containing branches (#1747) --- crates/ide-vcs/src/history.rs | 10 +++---- crates/ide-vcs/src/lib.rs | 4 +-- crates/ide-vcs/src/ops.rs | 6 ++--- crates/ide-vcs/src/refs.rs | 12 ++++++--- crates/ide-vcs/tests/history_ops.rs | 15 +++++++++++ crates/ide-vcs/tests/log_projection.rs | 37 ++++++++++++++++++++++++++ 6 files changed, 70 insertions(+), 14 deletions(-) diff --git a/crates/ide-vcs/src/history.rs b/crates/ide-vcs/src/history.rs index 593dcbc88..37990aeae 100644 --- a/crates/ide-vcs/src/history.rs +++ b/crates/ide-vcs/src/history.rs @@ -948,13 +948,11 @@ pub(crate) fn decorations(repo: &gix::Repository) -> IdeResult Option { } ["refs/heads", "refs/remotes"].into_iter().find_map(|refs| { let out = - run(r.root(), &["for-each-ref", "--points-at", commit, "--format=%(refname:short)", refs], None).ok()?; - let names: Vec = - out.stdout_str().lines().filter(|l| !l.is_empty() && !l.ends_with("/HEAD")).map(str::to_string).collect(); + run(r.root(), &["for-each-ref", "--points-at", commit, crate::refs::DIRECT_BRANCH_FORMAT, refs], None) + .ok()?; + let names: Vec = out.stdout_str().lines().filter(|l| !l.is_empty()).map(str::to_string).collect(); match names.as_slice() { [one] => Some(one.clone()), _ => None, diff --git a/crates/ide-vcs/src/refs.rs b/crates/ide-vcs/src/refs.rs index 1be20430d..24a0db405 100644 --- a/crates/ide-vcs/src/refs.rs +++ b/crates/ide-vcs/src/refs.rs @@ -4,6 +4,9 @@ use crate::repo::{gx, to_string}; use crate::{Branch, BranchInfo, Repo, Stash}; use ide_core::IdeResult; +// Filter symbolic refs before shortening; `refname:short` also strips remote `/HEAD` suffixes. +pub(crate) const DIRECT_BRANCH_FORMAT: &str = "--format=%(if)%(symref)%(then)%(else)%(refname:lstrip=2)%(end)"; + pub(crate) fn branch_info(r: &Repo) -> IdeResult { let repo = r.gix()?; let head_ref = repo.head_name().map_err(gx)?; @@ -93,6 +96,9 @@ pub(crate) fn branches(r: &Repo) -> IdeResult> { for reference in platform.local_branches().map_err(gx)? { let mut reference = reference.map_err(gx)?; + if reference.target().try_name().is_some() { + continue; + } let full = reference.name().to_owned(); let Ok(id) = reference.peel_to_id().map(|id| id.detach()) else { continue }; let current = head_name.as_ref().is_some_and(|h| h == &full); @@ -118,11 +124,11 @@ pub(crate) fn branches(r: &Repo) -> IdeResult> { } for reference in platform.remote_branches().map_err(gx)? { let mut reference = reference.map_err(gx)?; - let full = reference.name().to_owned(); - // Skip `refs/remotes//HEAD` symbolic pointers. - if full.as_bstr().ends_with(b"/HEAD") { + // Skip symbolic aliases such as `refs/remotes//HEAD`. + if reference.target().try_name().is_some() { continue; } + let full = reference.name().to_owned(); let Ok(id) = reference.peel_to_id().map(|id| id.detach()) else { continue }; out.push(Branch { name: to_string(full.as_ref().shorten()), diff --git a/crates/ide-vcs/tests/history_ops.rs b/crates/ide-vcs/tests/history_ops.rs index 79f428492..f2af0663e 100644 --- a/crates/ide-vcs/tests/history_ops.rs +++ b/crates/ide-vcs/tests/history_ops.rs @@ -149,6 +149,21 @@ fn merge_versions_put_the_users_work_on_the_left() { assert_eq!(repo.merge_sides().unwrap(), MergeSides { branch: Some("topic".into()), ..Default::default() }); } +#[test] +fn rebase_conflict_names_the_remote_branch_without_its_symbolic_aliases() { + let fx = diverged(); + let main = fx.git(&["rev-parse", "main"]); + fx.git(&["update-ref", "refs/remotes/origin/main", &main]); + fx.git(&["symbolic-ref", "refs/remotes/origin/HEAD", "refs/remotes/origin/main"]); + fx.git(&["symbolic-ref", "refs/remotes/origin/alias", "refs/remotes/origin/main"]); + fx.git(&["branch", "-D", "main"]); + let repo = fx.repo(); + assert!(repo.rebase("origin/main").is_err()); + assert_eq!(repo.merge_sides().unwrap().other_branch.as_deref(), Some("origin/main")); + let (cancel, out) = quiet(); + repo.abort_operation(&cancel, &out).unwrap(); +} + #[test] fn rebase_conflict_then_abort_and_skip() { let fx = diverged(); diff --git a/crates/ide-vcs/tests/log_projection.rs b/crates/ide-vcs/tests/log_projection.rs index 451613663..9015f793e 100644 --- a/crates/ide-vcs/tests/log_projection.rs +++ b/crates/ide-vcs/tests/log_projection.rs @@ -113,6 +113,43 @@ fn lazy_summaries_seek_and_index_survive_refresh_and_restart() { assert_eq!(repo.log_details(&ids[69]).unwrap().signature, "N"); } +#[test] +fn containing_branches_exclude_symbolic_aliases_but_keep_direct_head_names() { + let tmp = tempfile::tempdir().unwrap(); + let dir = tmp.path(); + git(dir, &["init", "-q"]); + let tip = commit(dir, "a", "tip"); + git(dir, &["branch", "topic/HEAD"]); + for remote in ["origin", "contributor-742", "issue-739", "issue-765"] { + let main = format!("refs/remotes/{remote}/main"); + git(dir, &["update-ref", &main, &tip]); + git(dir, &["symbolic-ref", &format!("refs/remotes/{remote}/HEAD"), &main]); + git(dir, &["symbolic-ref", &format!("refs/remotes/{remote}/alias"), &main]); + } + git(dir, &["update-ref", "refs/remotes/origin/topic/HEAD", &tip]); + git(dir, &["symbolic-ref", "refs/heads/local-alias", "refs/heads/main"]); + let repo = Repo::discover(dir).unwrap().unwrap(); + let expected = [ + "contributor-742/main", + "issue-739/main", + "issue-765/main", + "main", + "origin/main", + "origin/topic/HEAD", + "topic/HEAD", + ]; + let mut branches = repo.log_details(&tip).unwrap().containing_branches; + branches.sort(); + assert_eq!(branches, expected); + let mut listed: Vec<_> = repo.branches().unwrap().into_iter().map(|b| b.name).collect(); + listed.sort(); + assert_eq!(listed, expected); + let log = repo.log(&LogQuery::default()).unwrap(); + let mut labels: Vec<_> = log[0].refs.iter().filter(|r| *r != "HEAD").cloned().collect(); + labels.sort(); + assert_eq!(labels, expected); +} + #[test] fn followed_history_carries_the_filename_at_each_revision() { let tmp = tempfile::tempdir().unwrap();