Skip to content

Improve base branch detection#5636

Open
stefanhaller wants to merge 17 commits into
masterfrom
improve-base-branch-detection
Open

Improve base branch detection#5636
stefanhaller wants to merge 17 commits into
masterfrom
improve-base-branch-detection

Conversation

@stefanhaller
Copy link
Copy Markdown
Collaborator

Lazygit's promise was that you can configure several main branches (e.g. main, next, and develop) and it will always pick the right one depending on where your feature branch forked off. Unfortunately this turns out not to be true; it can sometimes happen that you start off with a feature branch forked off main, and then rebasing it onto its base branch rebases it onto develop (because main was merged into develop meanwhile).

This PR improves the situation by guessing the base branch a bit better in certain cases, and by prompting the user to pick the base branch in cases where it still can't tell.

See #5635 for lots of detail about this.

Closes #5635.

GetBaseBranch was determining its candidates purely by "this main
branch contains the merge-base" — and then returning the first one
without further discrimination. That equivalence class is too loose:
multiple main branches can contain the merge-base even when one is
clearly closer to the feature branch than another. The new test
exercises that case: with main and develop both containing the
branch's merge-base, the current code returns whichever
git for-each-ref happens to list first, ignoring how close each
candidate actually is.
…'s order

GetBaseBranch was treating "contains the merge-base" as the equivalence
class for "is the closest base," which is too loose — multiple main
branches can contain the merge-base when one is dramatically closer
to the feature branch than another. The candidate it returned was
then whichever ref git for-each-ref happened to list first.

For example, a branch forked off "develop" can have its combined
merge-base with [main, develop] land on a commit reachable from both
(via develop's own branch-off from main). Both main and develop end
up in the candidate set, even though by any reasonable measure of
closeness the branch differs from develop by a small ahead count and
from main by a much larger one.

Discriminate within the candidate set using ahead values: for each
configured main branch that contains the merge-base, compute the
ahead count from branch to base, and pick the candidate with the
smallest ahead — the closest base. When more than one candidate is
tied at the minimum, return that tied set unchanged so callers can
flag the case as genuinely ambiguous instead of silently collapsing
it; subsequent commits build the disambiguation prompt on top.
The fast path's selectBehindForBranch returned only the behind value
of the closest base, throwing away which base actually won. We need
that information so subsequent commits can present a disambiguation
prompt when more than one main branch is the closest base. Rename to
selectBaseForBranch and have it return (winner, behind, candidates),
where candidates is the full set of refs tied at the minimum ahead
(in config order) so callers can recognise ambiguity via
len(candidates) > 1.

To make that possible, parseAheadBehindForEachRefOutput now preserves
malformed entries with a valid=false flag instead of silently dropping
them — without this the slice would drift out of alignment with
mainRefs and the index→ref mapping would be unreliable.
Have GetBaseBranch reuse selectBaseForBranch to disambiguate the
multi-candidate case, so the fast path (for-each-ref %(ahead-behind))
and the legacy path (rev-list --left-right --count) agree on the
same rule for which base is "closest" and the same config-order
tiebreak when ahead values are equal. The bespoke loop in
GetBaseBranch goes away.
The four callers of GetBaseBranch all ultimately want a single ref,
but the disambiguation prompt we're about to add needs to know when
multiple main branches are genuinely tied at the closest position so
it can ask the user. Change the function to return all min-ahead refs
in config order and rename to GetBaseBranchCandidates; each caller
now takes candidates[0] as a placeholder until the prompt wiring
lands.
GUI call sites that need a base branch all share the same logic: ask
GetBaseBranchCandidates, take candidates[0] as the config-order
tiebreak, and surface the candidate list when the user needs to
disambiguate. Extracting that into a helper keeps the upcoming prompt
and rebase wiring focused on UX concerns. Not yet routed through —
subsequent commits replace the direct GetBaseBranchCandidates calls
with ResolveBaseBranch.
The four call sites that need a single base branch (legacy
behind-base loader, view-divergence menu, rebase-onto-base action,
move-commits-to-new-branch) now go through BaseBranchHelper.ResolveBaseBranch
in the GUI sites, and directly take candidates[0] in the loader. They
all use the same config-order tiebreak. No prompt yet — the helper
still returns the config-order first for ambiguous cases; subsequent
commits add the disambiguation menu.

MergeAndRebaseHelper and RefsHelper take BaseBranchHelper at
construction since helpers don't have access to Helpers() the way
controllers do.
When ResolveBaseBranch reports a tie, callers need a way to ask the
user which of the configured main branches to use as the base.
ShowPicker takes the candidate list and a continuation, presents a
menu of short branch names, and runs the continuation with the
user's selection. Subsequent commits wire this into the three GUI
actions that care (rebase-onto-base, view-divergence, move-commits).
When the checked-out branch's fork point is contained in more than one
configured main branch, pressing 'b' on the rebase menu now opens a small
disambiguation menu first; the selection is recorded and then the rebase runs
against the chosen base. The unambiguous case is unchanged.
Pressing 'b' on the branches view's divergence menu now shows the disambiguation
menu when the selected branch's fork point is reachable from more than one
configured main branch. The user's selection drives the sub-commits view and
gets recorded so subsequent actions on the branch skip the prompt.
The "off of <base>" item in the move-commits-to-new-branch menu now
shows the disambiguation picker first when the base is ambiguous,
then continues into the existing new-branch-name prompt with the
chosen base. The "stacked on current branch" path doesn't use the
base, so it's unaffected.
When a branch's fork point is reachable from more than one configured
main branch and the candidates disagree on the behind count, the
branches column was silently showing the config-order first candidate's
number — confidently asserting something we don't actually know. Worse,
"nothing" in the column means "up to date with the base", which may or
may not be true under ambiguity.

Compute behind values for every candidate (the fast path already had
them; the legacy path now does too via baseBranchCandidatesAndBehinds),
then classify:

  - all candidates agree → show that number (or nothing if 0)
  - some candidates 0, others not → "?" (we can't say if up to date)
  - all non-zero but differing → "↓?" (definitely behind, unknown amount)

Two sentinel constants on the Branch model (BehindBaseAmbiguousMaybeUpToDate
and BehindBaseAmbiguousDefinitelyBehind) encode these states in the
existing atomic.Int32 field via negative values; the renderer switches
on them.
The label currently looks identical in the unambiguous case
("Rebase onto base branch (develop)") and the ambiguous case where
develop is just the config-order tiebreak; pressing 'b' would then
surprise the user with a picker. Show "pick: main, develop" in the
parenthetical when the resolver reports the base is ambiguous so the
upcoming prompt is no longer a surprise.

The new PickBaseBranchLabel i18n string lives next to the existing
PickBaseBranchTitle/Prompt so the disambiguation UI is grouped in
one place.
Same idea as the rebase menu's label: when the resolver reports the
selected branch's base is ambiguous, show "pick: main, develop" in
the parenthetical instead of just the config-order tiebreak, so the
user knows the prompt will appear before they press 'b'.
Same idea as the rebase and view-divergence labels: when the base is
ambiguous, the menu prompt and the "New branch from base branch (...)"
item both substitute "pick: main, develop" for the parenthetical so
the user knows the disambiguation picker will appear before they pick
the "from base" option.
ShortBranchName previously turned "refs/remotes/origin/main" into
"origin/main", leaking the resolved-ref shape into UI labels that
the user thinks of as plain "main" — the name they put in their
mainBranches config. With the ambiguous-base label now potentially
listing several branches ("pick: origin/main, origin/13"), the noise
is even more pronounced. Strip the remote name along with the
"refs/remotes/" prefix so the short name matches what the user
configured.

Rename the helper to BaseBranchDisplayName to make the constraint
explicit at every call site: dropping the remote is a sensible
choice only for base-branch display, not for refs in general. All
current callers happen to be base-branch related, so the rename is
just a scope-tightening.

Existing integration test updated to expect the bare "master" form.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Improve base-branch detection for branches with ambiguous fork points

1 participant