Commit graph

4905 commits

Author SHA1 Message Date
Stefan Haller 6e74cc0268 Show base branches as bare names in labels
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.
2026-07-03 19:37:37 +02:00
Stefan Haller cee2de60ba Signal ambiguity in the move-commits-to-new-branch label and prompt
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.
2026-07-03 19:37:37 +02:00
Stefan Haller d688fa9319 Signal ambiguity in the view-divergence-from-base-branch label
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'.
2026-07-03 19:37:37 +02:00
Stefan Haller 272235925b Signal ambiguity in the rebase-onto-base-branch label
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.
2026-07-03 19:37:37 +02:00
Stefan Haller 2efaa38124 Document mainBranches and the disambiguation prompt 2026-07-03 19:37:37 +02:00
Stefan Haller df138a4c72 Show ? and ↓? in the branches list when the base is ambiguous
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.
2026-07-03 19:37:37 +02:00
Stefan Haller acc15a6e43 Prompt for base branch when move-commits-to-new-branch is ambiguous
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.
2026-07-03 19:37:37 +02:00
Stefan Haller dcf0532020 Prompt for base branch when viewing divergence is ambiguous
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.
2026-07-03 19:37:37 +02:00
Stefan Haller f59f993d1c Prompt for base branch when rebase-onto-base is ambiguous
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.
2026-07-03 19:37:37 +02:00
Stefan Haller c9eb915279 Add disambiguation menu to BaseBranchHelper
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).
2026-07-03 19:37:37 +02:00
Stefan Haller 98a421091f Route base-branch lookups through the shared resolver
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.
2026-07-03 19:37:37 +02:00
Stefan Haller 672a37031d Introduce BaseBranchHelper around candidate resolution
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.
2026-07-03 19:35:33 +02:00
Stefan Haller 84bd2b6fe1 Return all tied candidates from base-branch detection
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.
2026-07-03 19:35:33 +02:00
Stefan Haller 5a9e8c6af9 Share the multi-candidate selector between fast and legacy paths
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.
2026-07-03 19:35:33 +02:00
Stefan Haller 28cfcaced2 Reshape selection to expose the winning base ref
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.
2026-07-03 19:35:33 +02:00
Stefan Haller dd03884087 Pick base branch by smallest ahead instead of relying on for-each-ref'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.
2026-07-03 19:35:33 +02:00
Stefan Haller a0e51da643 Add test demonstrating wrong base branch on ambiguous merge-base
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.
2026-07-03 19:35:33 +02:00
Stefan Haller a929f34c84 Add gui.shrinkSidePanelsToContent option
Accordion mode expands the focused side panel, but when that panel has
little content (an empty Files panel, a Branches panel with only master)
it just fills the extra height with blank space. The same waste happens
for any panel that gets more height than it has content to show.

When this option is enabled, each side panel is sized to its own content
(plus a blank line, so it's clear there's nothing more below) rather than
to an equal share of the height. The height a small panel gives up flows
to the panels that have more content than fits; those grow up to their
content and then scroll, weighted toward the focused panel in accordion
mode so the two features compose. Only when every panel fits with room to
spare is the leftover shared out equally, regardless of focus: enlarging
the focused panel there would reveal no more content and would only make
the panels jump around as the focus moves.

The option is independent of expandFocusedSidePanel and off by default.
The status panel, and the stash panel when unfocused, keep their fixed
one-line height as before.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 19:25:01 +02:00
Stefan Haller b039d98b09 Scale the side-panel layout height thresholds by panel count
The height thresholds that decide between the proportional layout and
the squashed layout (and, within the squashed layout, between 3-row and
1-row unfocused panels) were hard-coded constants tuned for the fixed
set of five side panels. Now that the panels are configurable, a layout
with fewer panels has less to fit, yet was still forced into the
squashed layout at the same height as five panels would be.

Scale the thresholds down in proportion to the panel count so a smaller
layout keeps using the proportional layout at smaller heights. Only ever
scale down: raising the thresholds for more panels would make them
squash sooner, which works against the reason someone adds panels in the
first place (they want to see them).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 19:25:01 +02:00
Stefan Haller 9a4ef7d1a1 Allow overriding the platform used for default keybindings
A handful of default keybindings differ by platform (e.g. word-wise
cursor movement in text inputs uses alt on macOS but ctrl elsewhere).
Lazygit chooses these based on the OS it runs on, but that's the wrong
signal when the OS isn't where the user is actually typing: someone
running lazygit in a Linux container that they access over ssh from a
Mac gets the Linux bindings, when they'd rather have the Mac ones.
Remapping each binding by hand via config is tedious, so add a single
LAZYGIT_KEYBINDING_PLATFORM override.

An unrecognized value falls back to the real OS rather than to the
non-darwin default bindings, since the latter would be an arbitrary
choice.
2026-07-03 19:08:22 +02:00
Stefan Haller 0aed44c7f3 Offer to delete the branch when removing a worktree
Pressing `d` on a worktree only ever removed the worktree, leaving its branch
behind even though deleting it too is often what you want. Turn the confirmation
into a menu: "Remove worktree", "Remove worktree and delete branch", and "Remove
worktree and delete local and remote branch". The branch-deleting items come
after the plain removal (they do more harm if picked by accident); both are
greyed out for a detached-HEAD worktree, and the local-and-remote one is also
greyed when the branch has no upstream. The plain menu pick is the confirmation,
so the standalone "remove worktree?" prompt is gone (and its now-dead
translation string with it); the dirty-worktree force prompt and the
unmerged-branch warning still appear when relevant.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 19:04:44 +02:00
Stefan Haller 22914da8e5 Allow deleting local+remote of a worktree-checked-out branch at once
Picking "Delete local and remote branch" for a single branch that's checked
out in another worktree used to fail with "Some of the selected branches are
checked out by other worktrees. Select them one by one to delete them." That
message only makes sense for a multi-selection; for a single branch there's no
reason we can't remove the worktree and delete both the local and remote branch
in one go. Route that case through the same worktree menu as the local-only
delete, with labels that spell out that the remote goes too. The multi-select
error stays for actual multi-selections.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 19:04:44 +02:00
Stefan Haller 4f078f5463 Delete the branch when deleting it via its worktree
When you delete a local branch that's checked out in another worktree, the
menu offered to remove or detach the worktree but then stopped there, leaving
the branch you asked to delete still around. Now both actions delete the branch
afterwards, and the labels say so ("Remove worktree and delete branch" /
"Detach worktree and delete branch") to avoid surprises.

Also drop the "Switch to worktree" item: switching abandons the delete the user
asked for, and it's already reachable by checking out the branch or via the
worktrees panel. And drop the now-redundant "remove worktree?" confirmation:
the explicit menu pick is the confirmation (the dirty-worktree force prompt and
the unmerged-branch warning still appear when relevant).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 19:04:44 +02:00
Stefan Haller 9a8244110f Let worktree removal/detach chain follow-up work
Split the actual worktree removal out of the confirmation in Remove into a
non-confirming helper, and give both Remove and Detach an optional `then`
continuation that runs after a successful removal in place of the default
refresh. Upcoming flows need to delete the worktree's branch once the worktree
is out of the way; threading a continuation through (rather than the caller
firing branch deletion independently) keeps it ordered after the git command
that actually frees the branch. No behavior change yet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 19:04:44 +02:00
Stefan Haller d6016d6286 Extract reusable branch-deletion helpers
Pull the merged-check-and-force-warning step and the actual git deletion
out of ConfirmLocalDelete and ConfirmLocalAndRemoteDelete into helpers, so
that the upcoming worktree-aware delete flows can reuse them instead of
duplicating the logic. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 19:04:44 +02:00
Stefan Haller 7a67cea687 Expand a leading ~ in worktree paths to the home directory
Lazygit runs git directly rather than through a shell, so a literal "~"
reaches `git worktree add` unexpanded and git creates a directory named
"~" instead of using the home directory.

Expand the tilde ourselves, both for paths typed into the "Other"
location prompt and for the worktree.defaultPath config value.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 18:53:05 +02:00
Stefan Haller 737fb98967 Move the new-worktree keybinding from worktrees to universal
The command was renamed from "View worktree options" to "New worktree",
but its keybinding config key was still 'worktrees.viewWorktreeOptions'.
That name no longer matches the command, and the 'worktrees' section made
little sense: it held a single binding that isn't even used in the
worktrees panel (that panel uses universal.new), only in the branches,
remotes, tags, commits, and stash panels. Other keybinding sections are
named after the panel they're local to; this one wasn't local to any.

Move it to universal.newWorktree, which describes the action and drops the
spurious section, and migrate existing configs automatically.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 18:53:05 +02:00
Stefan Haller d53a9ea854 Add MoveYamlKey helper to move config keys between sections
RenameYamlKey can only rename a key in place, under the same parent. To
migrate a keybinding from one section to another we need to relocate the
key to a different parent mapping, which is a move, not a rename.

MoveYamlKey creates intermediate maps at the destination as needed and
prunes any maps left empty behind the key, so a section that held only
the moved key doesn't linger as an empty mapping in the user's config.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 18:53:05 +02:00
Stefan Haller 768d9f1a3f Rework the worktrees-panel 'n' into a branch picker
The old 'n' flow opened a "normal vs detached" menu (the same meaningless
gate the 'w' flow used to have), then asked for a base ref, a path typed from
scratch, and a branch name in three separate prompts.

Replace it with a single picker prompt titled "New worktree for branch",
suggesting local branches not already checked out anywhere, plus remote
branches that don't yet have a local branch of the same name. The entered
value is classified on confirm: an existing local branch checks out into a
new worktree, a remote branch creates a new local tracking branch, and
anything else creates a new branch off the current ref. All three then feed
the same location menu the 'w' flow uses, so paths are chosen from candidates
rather than typed blind. Picking a remote or new branch needs no separate
name prompt — the picker value already is the name. Checked-out branches are
filtered from the suggestions, and a verbatim type-in of one is rejected with
an error.

createWorktree now takes the context to switch focus to once the worktree is
created, so 'n' lands back in the worktrees panel while 'w' still lands in
the branches panel.

This deletes the old NewWorktree / NewWorktreeCheckout core and the now-
orphaned i18n (CreateWorktreeFrom, CreateWorktreeFromDetached, NewWorktreeBase,
NewBranchNameLeaveBlank), completing the migration started for 'w'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 18:53:05 +02:00
Stefan Haller 23d01ac1bd Redesign the 'w' worktree-creation flow
The old flow forced an up-front "normal vs detached" menu (meaningless for
commits, tags and stashes), then asked the user to type a worktree path from
scratch — easy to get wrong, and ambiguous about what relative paths resolve
against. It also offered the same two actions everywhere regardless of what
was selected.

Replace it with per-context "Worktree" menus whose items imply the intent
(new branch + worktree, worktree for an existing branch, detached worktree),
each feeding a shared name -> location -> create pipeline. The location menu
offers candidate parent directories as absolute paths instead of a blank
field, and "Worktree for a branch" is disabled (with a reason) when that
branch is already checked out somewhere, rather than failing after the fact.

Each ref/commit panel binds 'w' in its own controller and calls the matching
typed entry point on the worktree helper, so which menu opens is decided
statically by the call site rather than by dispatching on a ref's dynamic
type. The three commit panels share one menu through BasicCommitsController;
there is no longer a shared worktree-options controller.

The worktrees-panel 'n' flow and its old core (NewWorktree /
NewWorktreeCheckout) are left untouched here so nothing is written and then
rewritten; they migrate, and the dead i18n strings get removed, in a
follow-up commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 18:53:05 +02:00
Stefan Haller b02eca451c Add helper to compute candidate worktree parent directories
This is the core of "never type a path from scratch": from the repo root,
the configured default path, and the parents of existing worktrees, derive
the ordered list of directories under which a new worktree could be placed.
Pure and unit-tested here; wired into the creation flow in a later commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 18:53:05 +02:00
Stefan Haller ef73406a96 Add worktree.defaultPath config
The redesigned worktree-creation flow never asks the user to type a path
from scratch; instead it offers candidate parent directories. Until a repo
has any linked worktrees to learn from, there's nothing to offer, so let
users seed that list with a configured default location.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 18:52:09 +02:00
Stefan Haller 2fce9c91b6 Materialize cursor-forward escapes as space runs
ConPTY compresses runs of default-colored spaces into ECH + CUF
(\x1b[NX\x1b[NC) instead of emitting them literally. ECH is still a
no-op for us — our buffer is built sequentially and has nothing to
erase — but CUF has to materialize as N visible space cells so the
gap actually appears, otherwise content the child wrote with leading
indentation slides left against the preceding cell.

The view's cursorForward branch reuses the same machinery as tab
expansion: substitute the trigger byte for a space and let the
repeatCount path emit the cells under the parser-tracked SGR. The
existing notifyCellsWritten plumbing then advances screenCol over
the gap, keeping subsequent CUP targets aligned.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-03 18:47:15 +02:00
Stefan Haller 0c0c50c3f2 Demonstrate that cursor forward escapes collapse runs of spaces
ConPTY compresses runs of default-colored spaces into ECH + CUF
(\x1b[NX\x1b[NC) rather than emitting them literally. Both currently
fall through the parser's swallow path, so the gap they describe
collapses entirely and content that the child wrote with leading
indentation ends up slid left against the previous cell.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-03 18:47:15 +02:00
Stefan Haller 180fe0cd26 Convert forward cursor-positioning escapes into row advances
ConPTY presents its child's output as a screen buffer and uses CUP /
CUD / CNL / VPA to skip over blank rows rather than emitting LFs. The
previous behaviour swallowed all of those and the visible content
collapsed together. Now the escape parser tracks the screen-relative
cursor row, and any CSI that moves the cursor past the current row
emits a cursorDown instruction that the view turns into the matching
number of empty lines.

Column tracking is deliberately omitted: doing it correctly would mean
duplicating the view's grapheme-cluster width math in the parser, and
ConPTY in practice positions to column 1 after a CR-equivalent, which
the existing wx-reset path already handles. ConPTY-internal scrolling
needs no special handling either: it only emits cursor-positioning
escapes within the first, un-scrolled screenful — once its screen
scrolls it switches to plain linefeeds, which the view advances on
directly regardless of the tracked cursor.

Backward cursor moves are silently dropped — the view's buffer is
append-style and can't undo earlier writes. The exception is cursor-home
(CUP to row 1): ConPTY emits it at the start of every screen, so rather
than drop it we re-anchor the row tracking to the current write position.
Without that, a view not rewound in lockstep with ConPTY's screen (the
command log, which streams pty output without a rewind) accumulates
drift, and every later absolute CUP becomes a dropped backward move that
collapses the rows ConPTY positioned with.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-03 18:47:15 +02:00
Stefan Haller 79bc8e0bc6 Demonstrate that cursor positioning escapes collapse blank rows
ConPTY presents its child's stdout as a screen buffer and uses CUP
(`\x1b[<row>;<col>H`) to skip over blank rows rather than emitting LFs
for them. Our escape interpreter swallows CUP via the catch-all
"valid CSI final byte we don't implement" branch, so the blank rows
the child put between non-blank ones disappear and the surrounding
lines collapse together — which is what makes the delta-rendered diff
in the screenshot look like its blank lines and section breaks were
removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-03 18:47:15 +02:00
Stefan Haller 8c035ebe60 Use ConPTY on Windows for pty-backed command execution
The per-platform getCmdHandlerPty split existed because the Unix side
had creack/pty and the Windows side had nothing — so it fell back to a
non-pty handler. Now that oscommands.StartPty provides a pty on both
platforms, the two files collapse into one cross-platform
implementation and the stub is gone.

cmdHandler grows a 'wait' field because the pty path on Windows spawns
via CreateProcess and never runs exec.Cmd.Start — so cmd.Wait wouldn't
work there. Non-pty handlers set wait = cmd.Wait; pty handlers set it
to the wait closure StartPty returns.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-03 18:47:15 +02:00
Stefan Haller 1935117141 Add pty support on Windows via ConPTY
Replace the StartPty stub with a real ConPTY implementation:
CreatePipe + CreatePseudoConsole + PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE
+ CreateProcess. Pagers and external diff tools now get real terminal
behavior instead of being handed pipes.

One Windows-specific quirk worth flagging: ConPTY does not EOF the
output pipe when the child exits; conhost keeps it alive until
ClosePseudoConsole is called explicitly. A background waiter goroutine
calls ClosePseudoConsole as soon as proc.Wait returns, so callers see
EOF on outRead — restoring the Unix master-fd-EOFs-when-slave-closes
semantics they depend on.

The ErrPtyUnsupported sentinel and the no-pty fallback in newPtyTask
are gone now that both platforms have a real implementation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-03 18:47:15 +02:00
Stefan Haller c85f7530bb Abstract pty startup behind a platform-specific primitive
Move the pty master behind a small interface (Read/Write/Close/Resize),
and push the actual startup into a platform-specific StartPty function
in pkg/commands/oscommands. The Unix implementation still uses
creack/pty; the Windows implementation is a stub that returns
ErrPtyUnsupported, at which point newPtyTask falls back to a plain cmd
task — matching the existing Windows behavior.

The primitive lives in oscommands rather than pkg/gui because the
cmd_obj_runner pty handler (also in oscommands) is going to consume it
too, and tasks → oscommands is the existing dependency direction.

Same observable behavior on every platform; this just carves out a seam
for a real ConPTY implementation on Windows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-03 18:47:15 +02:00
Stefan Haller 8ec4283e55 Abstract task command over *exec.Cmd
Windows ConPTY can't attach a child process to a pseudoconsole via
os/exec — Go's stdlib doesn't expose PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE
(golang/go#62708). The ConPTY path has to call CreateProcess directly,
so it can't hand an *exec.Cmd back to the task runner.

Widen NewCmdTask to accept a small Cmd interface satisfied by both
*exec.Cmd (via the ExecCmd adapter) and the Windows ConPTY command type
we're about to add. Change TerminateProcessGracefully to take
*os.Process, which both cmd shapes can provide.

Behavior is unchanged on every platform.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-03 18:47:15 +02:00
Stefan Haller 51c8f9e6ad Always set LAZYGIT_COLUMNS
The env var was previously set only on Windows, where the no-op pty
stub was just running the command without a pty and needed to expose
the width to pager scripts another way. With ConPTY coming to Windows
the rationale disappears there, but the env var is documented in
docs/Custom_Pagers.md for pager scripts that can't query the terminal
width directly. Set it on every platform so those scripts remain
portable, regardless of whether a pty is in play.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-03 18:47:15 +02:00
Stefan Haller 90d8ef499c Auto-dismiss the continue-rebase prompt when it becomes stale
The prompt offering to continue a rebase/merge is opened from a refresh
and then left to sit until the user acts on it. But the operation can
change out from under it: a coding agent (or the user in another
terminal) might continue or abort it, or advance it to a commit with new
conflicts. The prompt then becomes stale — pressing continue fails with
"no rebase in progress" or acts on the wrong state.

Track whether the prompt is showing, and on each refresh dismiss it if
the operation is no longer in the "resolved, ready to continue" state
that the prompt is offering to act on. This runs on the same refreshes
that would open it (including the background poll and the refresh on
window focus), so the prompt disappears on its own shortly after the
operation moves on.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 18:30:02 +02:00
Stefan Haller ad507d67f4 Only prompt to continue a rebase/merge if we started it
When conflicts of an in-progress rebase/merge/cherry-pick/revert are
resolved, lazygit pops up a prompt offering to continue it. This is
helpful when you started the operation in lazygit and resolved the
conflicts in your editor. But it's confusing when the operation was
started outside lazygit — e.g. by a coding agent in another terminal
that resolves the conflicts but hasn't continued yet because it's still
running tests or fixing the build. lazygit would then prompt unbidden.

Track whether the in-progress operation was started from within lazygit,
and only show the prompt in that case. We record this right after running
a merge/rebase step (in CheckMergeOrRebaseWithRefreshOptions, the
subprocess branch of genericMergeCommand, and the custom-command
conflict path), and clear it whenever a refresh observes that no
operation is in progress — which also handles an operation that was
finished or aborted externally.

The conflict-resolution tests start their operation by running git
directly (not through lazygit's UI), so they call the new test helper
Common.PretendMergeOrRebaseStartedInLazygit to have lazygit treat the
operation as its own and still get the prompt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 18:28:07 +02:00
Stefan Haller 873804a37b Make Gui.Update a synchronous FIFO enqueue
Update spawned a goroutine per call that then sent on the user-events
channel, so multiple Update calls from the same goroutine could be
reordered by the scheduler — the doc comment even admitted "the order in
which the user events will be handled is not guaranteed." That
non-determinism is a latent source of flaky rendering: code that queues a
model update and then a render in source order could see them run in the
opposite order.

Send on the channel directly instead, so same-goroutine calls arrive in
source order. The send is non-blocking and panics on a full channel
rather than blocking (a blocked send from the UI goroutine would deadlock
against itself) or silently reordering; the buffer is sized generously so
this is unreachable in normal use. UpdateAsync is now identical to Update
and unused, so it's removed along with the shared updateAsyncAux helper.
2026-07-02 17:32:01 +02:00
Stefan Haller badf398a94 Run moveMainContextPairToTop before queueing main-view tasks
Copy the outgoing view's content into the target view (the flicker-
prevention step) before queuing the render task, rather than after. The
task writes the fresh content from a worker goroutine, so with the old
order the worker write races the UI-thread copy, and the copy can land
last and clobber the fresh content with stale output.

This is only needed while view writes happen concurrently. Once view
writes are serialized on the UI thread and the view write-mutex goes
away, the synchronous copy always precedes the FIFO-queued write
regardless of order, so the reorder becomes unnecessary. No code comment
is added for it, since that comment would be obsoleted by that work and
likely left behind.
2026-07-02 17:32:01 +02:00
Stefan Haller e53c72be52 Make ViewBufferManager.NewTask respect call order
NewTask was incrementing newTaskID and reading taskID inside the
spawned goroutine, so for two NewTask calls in quick succession the
assignment was determined by goroutine scheduling order rather than
call order. When the goroutines reordered, the first NewTask call
could end up with the higher taskID and "win" the staleness check,
superseding the second call's task even though the caller intended
the second to be the latest.

Worse, the staleness check ran after onNewKey, so a goroutine destined
to bail as stale would still reset the view buffer first, potentially
wiping the winning task's already-written output.

Take newTaskID++ synchronously in NewTask so taskIDs follow call order,
and move the first staleness check ahead of onNewKey so a stale task
doesn't side-effect the view before exiting.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-02 17:24:48 +02:00
Stefan Haller 4fcf6ddb5f Revert "Add GlobalArg/GlobalArgIf to GitCommandBuilder"
After the previous commit this is no longer needed.

This reverts commit 94db69f64b.
2026-07-02 16:49:36 +02:00
Stefan Haller ccaa96b29d Suppress optional locks by default again, except foreground refresh
Commit d94f2f05 dropped the GIT_OPTIONAL_LOCKS=0 env var that we used
to set on every git command, and re-added lock suppression only as a
--no-optional-locks flag on the background files refresh. The intent
was sound — a foreground `git status` should persist git's refreshed
stat-cache — but the change was too broad: it stopped suppressing
optional locks for every other command too.

The one that bites is the main-view diff. When a folder containing
submodules is selected, we render `git diff --submodule -- <dir>`, and
`--submodule` makes git run `git status` inside each submodule to
describe its "modified" state. That status now grabs the submodule's
index.lock. It runs as a PTY task on its own goroutine, so it races
any submodule-mutating action the user triggers — e.g. resetting a
submodule runs `git -C <submodule> stash`, which then fails with
"index.lock: File exists". This is what made submodule/reset_folder
flaky. `git status` is in fact the only command that takes the
optional lock, but the env var also covered its use inside `git diff
--submodule`, inside PTY-run commands, and inside git's own submodule
child processes — none of which a per-command flag reaches cleanly.

Invert the polarity to match how it worked before d94f2f05: the git
command builder disables optional locks on every command by default,
and the single command that benefits from taking the lock — the
foreground files refresh — opts back in. This restores the original
contention avoidance (including against the user's terminal git) while
keeping d94f2f05's stat-cache-persistence win for the foreground
refresh.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 16:05:08 +02:00
Stefan Haller 29b70105c2 Reintroduce the gitCmdObjBuilder wrapper
This reverts commit eb988395e6, which removed the wrapper on the
grounds that it no longer had anything to do but delegate. We're about
to give it a job again: hosting a git-specific default environment
variable on every command. Restore the scaffolding first, as a pure
behaviour-preserving step, so the behaviour change that follows is
minimal.
2026-07-02 15:29:57 +02:00
Stefan Haller ed3f4db4f9 Pick both hunks, not the common ancestor, in diff3 conflicts
`b` on a merge conflict is meant to keep both sides. With the diff3
conflict style git additionally renders the common ancestor between the
two sides, and the old ALL selection kept everything between the
outermost markers, dragging that ancestor into the resolved file.

Rename the selection from ALL to BOTH and restrict it to the top and
bottom hunks so the common base is dropped. Without the diff3 style
there is no ancestor section, so the behaviour there is unchanged.

The user-facing keybinding config was already named pickBothHunks; only
the internal enum, handler, translation and log string still said "all".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 09:57:26 +02:00