Delta and difftastic both rendered the preview normally with the trees a
line ending apart, so only the warning was ever visible. The setting
behind it was core.autocrlf in the system config Git for Windows
installs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The lint failure at PR 6's tip came out of the 2026-09-19 replay, and the
fixups for it sit on two PRs. Write down why the wrapper leaves PR 6 and
comes back in PR 7, and that the PR 7 commit between the two fixups is
meant to stay unbuildable until they are folded in.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The click names its own line, so it needs none of the gate the edit
keybinding needs, and for a modify/delete conflict the file it opens is
in the working tree.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The branch's opening commit belongs in PR 5, and moving it there proved
much cheaper than the note in the plan had assumed. Four more findings
needed code, two of them in gocui.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A fixup on "Show git's own diff when the renderer's can't be acted on"
would have folded two decisions into one commit: which diff to render,
and whether the content can be pointed at.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two problems from testing PR 9: wrapLinesInDiffView governed everything
the two main panes showed, and the hint for a conflict that has to be
resolved by picking a side had a selection over it. Both are answered by
having a render say whether it holds the panel's diff.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two bugs reported from using the stack and a third found while reproducing the
first (PR 2 round 1, PR 5 round 5, PR 8 round 1), then two more rounds as each
fix in turn was tested and found short: the pane answered "is there anything to
select here?" one screenful too early (round 6), and then took the answer over
from the commit before while it still couldn't tell (round 7).
Round 6 carries the measurements from the reported commit, which say what each
fix is worth. §8 gains the diffstat row the user has since decided to keep, and
loses the row round 7 closes.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
PR 10 landed independently of the explorer removal and gained visible feedback for non-suspending editors. Capture its actual dependency, six-commit shape, pending terminal matrix, and the separately deferred click-selection behavior.
Co-authored-by: GitHub Copilot <noreply@github.com>
Four problems, the one root cause behind three of them, the prep refactor the
focus rule needed, and the two gaps left open. Also corrects a claim made
while the round was being scoped, that a diff renderer is re-laid-out
whenever the view width changes.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two defects and their fixes, the testing setup that reproduces them, and
the rebase traps the round turned up.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Record the four review comments the interactive pass came with, what was
decided for each, and where their fixups landed — three of them belong to
PRs 5 and 6 rather than to PR 7.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The staged-side branch grew out of it, PR 7 was rebuilt on top, and the
selection-visibility rule became a question about content rather than about
which kind of task rendered it.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
It sits below PR 7, which is rebased onto it and simpler for it: which side a
pane shows is now a property of the pane, and the focus-follow rule is
symmetric.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Seventeen deviations, of which the ones that bind later work are the split
seam (PR 8's panels satisfy the extension interface), the stale timing fact
about what a refresh has done by the time it returns, and the restore's new
Done hook.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two things bind later work: a restore keeps the task reading to the end of
its input, because a rendering that has to be parsed as a diff can only be
parsed whole, and identities match through a key in which every kind of
content line collapses together - which is what lets a change that becomes
a context line still count as the same place, and a hunk header not.
The interactive pass on PR 5 found nothing to fix; refining the behaviour
of a few special cases can wait until the remaining PRs are written.
Toggling "ignore whitespace" re-renders the diff just like a context-size
change or a renderer switch does, so it belongs in PR 6 as a third
consumer. It is the first one whose anchor can vanish along with its hunk
or its whole file, which invalidates the candidate walk's reason for
stopping at the first change line; the walk goes unbounded instead, so the
restore lands on the nearest surviving line wherever in the diff that is.
And once staging happens in the main view, master's refusal to ignore
whitespace while staging no longer catches it - parked as an open question
for PR 7.
The selection commit is split in four, comments no longer point at the
staging view, and the selection commands are described and enabled only
where they apply.
The two findings from the interactive pass were folded back, which took the
loading-indicator gate to before the off-screen render — where the blanking
would otherwise arrive — and made the amend! for the origin reset
unnecessary, since the flag it needs now already exists by then.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Deviation 6 still described the TaskOpts field as the shape PR 6 should
build on while deviation 8 said it was gone, so a reader hitting 6 first
would follow a design that no longer exists. Deviation 6 now describes the
final shape and what it means for PR 6, and PR 6's own commit 1 says the
same.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The regression it found retires the PR 6 note from deviation 6, and the
race fix establishes where unrelated bugs found during this work go.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two of them constrain later work: TaskOpts.ResetOrigin replaces the planned
LinesToRead.ResetOrigin that PR 6 was going to extend, and the screenColMax
gap PR 1 had to fix is still live on the prototype branch that later PRs
transcribe from.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The plan assumed git would never emit the records, so PR 4 skipped the
advertisement for rawGit renderers and PR 7 listed "how should rawGit
entries decide the raw fallback" as undecided, with a static args-present
rule or a well-formedness gate as the candidates. git can emit them now,
which answers that question better than either candidate: probe a rawGit
entry like any other renderer and the fallback collapses to a single rule
for every renderer type.
Record the two traps as well, since neither is visible from the outside --
the advertisement has to precede newPtyTask's no-pty early return, and the
probe's cache signature has to include the args.
Say explicitly that none of this waits on git upstream. The probe asks the
installed git what it can do, so a stock git degrades to exactly the
behaviour of a non-conforming renderer, and a fork covers the users who
want the feature if the patch is never accepted.
Probe git for the handshake like the other renderers, not for a record.
git announces itself for exactly the formats it describes, so asked with
the renderer's own arguments the handshake already answers the question,
and on empty input at that. Looking for a record was over-specified and
made git the one renderer probed differently for no reason.
The two probes that now both stand a diff up from two empty temp files
share that setup.
A rawGit diff renderer needs no pty -- git renders the diff itself, and
only a pager needs a terminal to be spawned at all -- so newPtyTask hands
that case straight to newCmdTask. But the OSC1717 advertisement was set
forty lines further down, past that early return, so git was never asked
to annotate its output and the word-diff renderer we just started
trusting emitted no records.
Set it before the branch, next to LAZYGIT_COLUMNS, which is there for the
same reason. Nothing else changes: a renderer that doesn't know the
variable ignores it, and git says nothing for the formats it doesn't
annotate, which is every format a rawGit renderer without word-diff
arguments produces.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
git now emits the diff line metadata records itself, for the word-diff
formats -- the ones whose output can't be read back from its own text,
which is the same reason we need records out of delta and difftastic. So
a rawGit renderer configured with --color-words is no longer a diff we
have to give up on: it names every row it shows, as a patched pager does.
Two things stood in the way. The probe only knew how to ask a stdin
filter or an external diff driver and reported false for anything else,
so git's own records were never looked for; and the focused main view
treated a rawGit renderer with arguments as unresolvable by definition,
re-rendering it raw whatever the probe said.
So probe git the way we will run it, with the renderer's own arguments,
and look for an actual record rather than the handshake the other probes
settle for. git annotates only some of its formats, so an installed git
that doesn't speak the protocol and arguments that select no word diff
both leave us without records, and looking for a record answers both at
once. Arguments that aren't a word diff (-U10, --stat) therefore keep
rendering raw when focused, as they did before.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
We stopped updating these notes once the real work moved to
focused-main-view-notes.md and the production plan, but two things in them
have since become actively misleading rather than merely stale: they say in
five places that the protocol is content-lines-only, and they use the env
var name from two renames ago.
Mark the document historical, name the spec as the authority, and correct
§11's outcome banner in place rather than rewriting it, so the reasoning
that led to dropping `f`/`h` is still on record next to the reason it was
reversed -- which is the very cost that banner judged acceptable.
Don't touch the `EMIT_OSC1717_METADATA` spellings further down: they are
what those prototypes were built with, and the status block now says so.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The prototype was rebased onto master past #5854 (gocui mouse gestures)
and #5870 (diff-renderer config rework), so the SHAs quoted in the plan
now resolve only on the pre-rebase copy at
fold-staging-functionality-into-main-view-plan; note that in §1 and in a
dated log in §10.
PR 3 is done: #5870 landed the rename with a bigger config restructure
than planned (per-entry 'type' field, unified 'command', 'rawGit' type,
DiffRendererConfigManager). Rewrite the PR 3 section as a summary of
what landed and its consequences, key the affected decisions in PRs 4-7
off GetDiffRendererType(), add rawGit as a new case for PR 7's raw
fallback, and resolve the open question about the 'pager:' field name.
PR 10 commit 3 is superseded: the rebase dropped the press-only
modifier fix, and #5854's gesture model makes press-only insufficient
anyway; the re-implemented prototype commit carries the press-time
modifiers through the whole gesture, and the plan now points at that
shape.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A click is reported on the button press, but the press never set the
event's modifier, so a modified click (alt/shift/ctrl+click) reached
handlers stripped of its modifier, indistinguishable from a plain
click. An earlier fix for this ("Carry the keyboard modifier on mouse
click events") was lost while rebasing onto master's mouse gesture
handling (#5854) — and that work also changes what the right shape is:
it isn't enough for the modifier to ride the press alone.
Drag events and the release are delivered to the view that owns the
gesture, and bindings match modifiers exactly. If the press carried alt
but the drags carried plain ModMotion, an alt-press that no binding
consumed would start matching unmodified drag bindings mid-gesture
(drag-select), and the release of a modified gesture would look like a
plain one. So snapshot the modifiers at press time and stamp them on
every event of the gesture: the press, each drag event (combined with
ModMotion), and the release; modifier changes while the button is held
don't alter the gesture. TestMouseReleaseDoesNotKeepPressModifiers
asserted the opposite for the release and is inverted accordingly.
As before, this means a modified click that nothing binds is a no-op
rather than silently acting as a plain click.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review clarified that "deferred" in the gap table meant "not addressed
while prototyping", not "optional". Five items are mandatory:
- IsSingleHunkForWholeFile (PR 5 commit 3): regression vs master
otherwise. Single-file-ness comes from the side panel selection; the
single-block computation's lean is patch-space over the raw diff
fetched at focus, with ReadToEnd-on-focus as the accepted fallback.
- Nav targets beyond the loaded content (PR 5 commit 6): ReadToEnd and
re-scan like openSearch; free if the above reads to end on focus.
Covers the jump-to-file menu too.
- Copy (PR 5 commit 9): reframed — copy the corresponding raw diff
lines, never the renderer's output. Dissolves the can't-tell-if-the-
renderer-preserves-the-prefix-column problem instead of solving it.
- Deleted-file MD-vs-D (new PR 7 commit 5): staging a deleted file's
entire content stages the file deletion itself.
- Renderer switch shifting the inclusion checkmarks mid-build (new
PR 8 commit 10): recompute from the new buffer at render completion.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A parallel session committed the zero-width OSC-record carrier
(fe8022827) and diagnosed the secondary patch pane's removal as broken
under difftastic: the ordinal bridge assumes displayed change lines
match the patch's change lines in order and multiplicity (difftastic
reorders d/a per hunk and collapses modification rows), and an external
diff tool over the temp trees emits the literal a/-b/ paths in its
records, so the patch-builder file lookup no-ops. PR 8 commit 7 now
prescribes the identity bridge (all payloads per row, matched against
identities from the raw temp-tree diff, k-th match -> included[k]) and
records the path-leak as an open sub-item to decide with the user; PR 4
commit 1 references the now-committed carrier, which the bridge relies
on for the d half of collapsed modification rows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A rendered row can carry several diff-metadata records back-to-back with
nothing painted in between: difftastic's per-hunk banner emits the
file's f immediately followed by the hunk's h, and a modification row
collapsed to a single column emits its d immediately followed by its a
(spec 6.1/6.2). The escape interpreter kept only one accumulating
payload and reset it when the next record started, so every record but
the last was silently dropped -- the banner resolved as a bare hunk
header, and staging a collapsed modification row staged only the
addition half.
Orphan an unconsumed payload instead of dropping it: when a new record
starts (or the line ends) before any cell consumed the current payload,
hand it to the write loop, which materializes it as a content-less
zero-width carrier cell -- the same trick finishLine already used for
delta's metadata-only blank lines, now generalized. Carrier cells hold
their place in left-to-right payload order and are invisible: drawing
paints a transient space that the next cell immediately overwrites at
the same x, and their zero width keeps wrap accounting unchanged.
DiffLineMetadataPayloads thus reports every record of such a row, so
ChangeLinesInViewRange stages both halves of a collapsed modification
row; DiffLineMetadataInLine reports the first (the f of a banner, the d
of a collapsed row), matching the two-column convention.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two corrections from review:
- "Pager" is retired in favor of "diff renderer" throughout (the spec
already made this change); a new mini PR 3 renames the pagers config
to diffRenderers with migration and docs, and the remaining PRs are
renumbered accordingly.
- The locked-decision list wrongly said concurrency stays mutex-based;
the main-thread-mutation rework has landed on master and the
prototype is rebased on top of it. The plan now states the landed
threading contract that all PRs must honor, and PR 1 gained a
re-validate-against-master preamble. Its reset-after-CopyContent
commit was verified still needed (master's CopyContent still copies
the source origin); the two locking fixes were reshaped onto the
viewLines readers master actually has, since the prototype's
HyperLinkInLine guard belongs to the dropped hyperlink backend.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The prototype is complete and signed off; this is the plan for
re-implementing it as a stack of clean PRs off master. It divides the
work into ten PRs (grouped for release-notes value as much as for
technical cohesion), outlines the commits of each, records the scope
decisions made in the planning session (panels removed, enter/dive
gesture dropped, stacked PRs within one release, both extras in scope,
nav/preserve as early standalone PRs), lists everything from the
prototype that must NOT be ported, and carries the sign-off matrix,
separate-lists compatibility seams, and known-gap dispositions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The spec regained file-header (f) and hunk-header (h) records: f never
carries a line number, h always does (the first line of the hunk it
heads). Accept them in the OSC metadata backend, so a conforming pager's
header rows resolve to the same DiffLineFileHeader/DiffLineHunkHeader
identities the buffer parser already reports for raw diffs.
With header rows located, next/previous file navigation and the
jump-to-file menu land on a file's header row, and header rows become
usable scroll-restore anchors. The consumers need a few adjustments:
- File navigation used to reach a file's top by backing up over the
untagged rows above its first located row. With tagged headers that
overshoots onto the blank separator row above the file header, so drop
the back-up (backUpOverHeader) and land on the first located row
itself: the header for any conforming source, or the first content
line under a pager that leaves its headers untagged — an accepted
degradation for non-conforming pagers, now that the spec makes f/h
mandatory.
- SamePatchLine now requires headers to match headers of the same kind.
A hunk header shares its line number with the hunk's first content
line (and a file header shares "0" with a deleted file's hunk header),
so without this a position restore aiming at one could land on the
other. This also applied to raw diffs before, but headers used to be
unlikely restore targets; now that navigation deliberately lands on
them, the ambiguity would bite.
- Editing a file-header row opens the file without jumping to a line,
like pressing edit on a whole file in a side panel. (It used to open
at line 1 for raw diffs, where headers resolved already.)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
n / N step through the files of a multi-file diff one at a time, which
is tedious when the diff spans many files. Add `f`, which pops up a menu
listing every file in the diff — in the order they appear, as
repo-relative paths — so you can jump straight to one.
Picking a file reuses the exact landing logic n / N use (navigate to the
file's first row), computed from the same backUpOverHeader machinery
AdjacentFile uses, so the menu and the step keys agree on where each
file begins and land identically.
The menu title and the keybinding description are hard-coded English for
now; this is a prototype.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The focused main view now plays the role the staging panel used to,
so the working-tree commands the staging panel offered — commit (and
its variants) and find-base-commit-for-fixup — need to be reachable
there as well.
Gate them to when the focused main view actually shows the working-tree
diff (DiffMainViewTypeStaging), which is exactly the role the staging
panel filled. Over a commit's or stash's diff these commands would
operate on the working tree, unrelated to what's on screen, so the keys
are a no-op there and the bindings don't clutter its keybinding menu.
The gate is re-checked on each press rather than captured at
registration time, since one keybinding set serves the main view over
every panel. That requires reading the panel beneath the main view from
GetKeybindings (to decide whether to show the descriptions), which runs
for off-stack contexts too — at startup and during cheatsheet
generation — so a panic-safe IsInStack guards the NextInStack lookup.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Records the two rename-handling gaps the rebase onto f84ada494 leaves
for productionization: the previousPath="" conflict resolutions in the
focused-main-view patch code, and the failing renamed_file_whole e2e
(a view-rendering regression in the prototype's patch package, not a
build regression and not the previousPath issue).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pager authors reviewing the protocol asked to drop the EMIT_ prefix: the
OSC number already names the protocol, so EMIT_ was redundant. Rename the
handshake variable to OSC1717_METADATA in the spec and in the two places
lazygit advertises it (the pager PTY and the metadata probe).
The inclusion gutter (the ✓ markers on the commit diff showing which lines are
in the custom patch) is painted on the Normal pane but is an affordance of the
whole focused-main-view pair. It was gated on the Normal pane specifically being
current, so tabbing to the secondary (custom-patch) pane hid it — even though
both panes are visible and you're still building the patch.
Show it whenever either pane of the focused main view holds focus, finding the
side panel beneath whichever pane is current. GetOnFocusLost now re-evaluates the
gutter (rather than unconditionally hiding) so it persists across a pane switch
but still hides when focus leaves the pair; the new context is already current by
then, so it decides correctly and doesn't flicker.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Staging a hunk from the focused main view advances the selection to the next
hunk, because staging removes the acted-on lines from the diff so the preserved
change-line ordinal lands on the next change. Toggling a hunk into a custom
patch left the selection sitting on the just-toggled hunk instead — the toggle
doesn't change the diff (only the inclusion set), so the same ordinal lands back
where it was, and you had to navigate by hand to build a patch hunk by hunk.
Give the toggle the same feel by advancing the reveal ordinal past the toggled
change lines (RevealSelectionAfterStaging gains an advanceBy arg, fed the toggled
change-line count). Staging and removal still pass 0 (their lines are consumed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Use osCommand.GetTempDir() (lazygit's own per-session temp dir, which it
creates and cleans up) as the parent for the custom-patch diff trees, instead
of the OS default — so it honors the configured temp dir and is cleaned up with
the rest of lazygit's temp files.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The secondary pane showed the custom patch with a bespoke in-memory render
(PatchBuilder.RenderAggregatedPatch / FormatView). That had two problems: it
could never be fed to a pager the way the main view's diff is (a stdin pager
might have worked, but an external diff tool like difftastic, which diffs two
files rather than a unified diff, could not), and its hand-rolled hunk/context
handling differed subtly from git's.
Materialize the patch instead as two real file trees under a temp dir — a/ holds
each patched file's "from"-side content, b/ that content with the patch applied —
and render it with `git diff --no-index`, reusing the exact pager wiring the main
view uses (stdin pager, external diff, or git's own colour as the raw fallback).
`--no-index` honors both GIT_PAGER and --ext-diff, so every pager type now renders
the custom patch like any other diff, and git computes the context, fixing the
quirks. Because the secondary is now an async diff task, the post-removal
selection reveal (which rides a task's restore) finally takes effect.
The trees are named a/b so that with --no-prefix the diff shows the real
repo-relative paths; added files are seeded empty in a/ so they pair up and show
their real paths rather than git's directory-comparison "added in b" form. The
patch builder owns the temp dir's lifetime (created on Start, removed on Reset)
and bumps a generation counter on every change, so the trees are rebuilt only
when the patch actually changes — covering the focused-main view and the old
explorer alike, without rebuilding on mere navigation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The focused-main-view rework made the secondary pane actionable, but the old
patch-building explorer's secondary was inert, so its actions were never thought
through. Pressing space there routed through the same toggle handler as the main
pane, resolving the selection against the secondary's diff and mapping it to
patch-builder indices by line number. But the secondary shows the *aggregated*
custom patch, which renumbers included additions whenever an earlier addition in
the same hunk is excluded (Transform recomputes each hunk's +start). So the
shifted number resolved to the wrong line in the original diff — often adding an
unrelated line instead of removing the selected one.
Resolve the secondary selection by its *ordinal* among the change lines shown
instead: the custom-patch view renders exactly the included change lines in
order, so the k-th change line of a file is that file's k-th included change line
(PatchBuilder.IncludedChangeLineIndices), independent of the renumbering. Space
in the secondary now only ever removes, mirroring how space in the staging view's
staged pane unstages.
Discarding from the commit (the remove key) makes no sense in the custom-patch
preview — it would act on lines shown only as the patch, and space already
removes them — so it's disabled there.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Discarding lines from a commit via the focused main view installed its own
revealSelectionAfterPrimaryAction before the rebase. That rebase rewrites
the commit, so the selection-preserve net now re-establishes the selection
as the diff re-renders — with the same anchor (the selection's first line),
making this install redundant. Files-discard keeps its own reveal: its diff
command is stable, so the net never fires there, and it has staging's
focus-follow to the secondary pane besides.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dropping a hunk with `d` re-establishes the focused main view's selection
on the next surviving change, which feels great. But other operations that
rewrite the commit under the focused main view — moving a custom patch out
into the index, undoing right after a discard or a patch move — don't run
through the focused-main-view action handlers, so nothing was preserving
the selection. The stale gocui selection was left painted over the new
content, often as a large, now-meaningless range.
Rather than teach every such command to capture and restore the selection
(move-patch, undo, redo, and any future one), the focused main view now
preserves it itself, by its change-line ordinal, as the diff re-renders —
the command-agnostic counterpart of revealSelectionAfterPrimaryAction. The
diff side panels call it from their render-to-main before triggering the
render, so the restore rides the re-render.
It stands down unless the focused main view is current and shows a
selection, no precise restore is already pending (escape / post-stage
reveal / context-size place the selection more precisely), and the diff
command is actually changing. That last gate matters: a plain background
refresh re-renders the same commit's diff unchanged, and there the
selection — range and all — must be left alone; only a command change
(e.g. a rebase rewriting the commit's hash) means the content moved.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SHAs go stale on every history rewrite of this throwaway branch; describe
commits by what they did instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The driver fix is a no-op for the old staging panel, not a tightening: that
panel anchors its drag at the click on mouse-down, so it never exposed the
bug.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Click-and-drag in the focused main view turns a hunk selection into a range,
but it was anchored at the change block's far end — where selecting a hunk
leaves the range anchor — rather than the clicked line. And on a context
line, where the click leaves no range anchor at all, dragging just moved the
single selected line instead of opening a range. Remember the line each
mouse-down lands on (the click can show a whole hunk, so it can't be read
back from the view) and, as the drag proceeds, anchor the range there while
the cursor end follows the mouse as gocui already moves it. Works for a click
that focuses the view and for one while it's already focused, on change and
context lines alike.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When the diff re-renders for a new pager (or a changed context size),
PreserveDiffPositionOnRerender restored the cursor by patch identity but left
the selection's other end — the range anchor — pinned to its old view line. A
pager that restructures the diff (delta side-by-side is the clearest case)
then left the selection spanning the wrong range of patch lines. Remember the
far end by patch identity too and put it back the same way, so the selection
covers the same lines however the new pager lays them out. One mechanism now
restores both ends, covering hunk and range selections alike; a single-line
selection still needs only the cursor.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Clicking the staged/unstaged pane you weren't focused on selected a single
line the first time, even in hunk mode, because that pane's select mode was
still its default until it had been focused once (tabbing to it and back was
the workaround). Seed the clicked pane's mode from the one we're leaving so
the very first click there behaves like every later one.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Once in hunk mode, clicking another change line reset the selection to a
single line, so you had to press `a` again for each block you wanted to
stage (a behaviour the main view inherited from the staging and patch-
building panels). Preserve hunk mode across clicks instead: a click on a
change line re-selects that whole block, while a click on context — or any
click when we weren't in hunk mode — drops to a single line, where the click
points precisely (e.g. to edit it).
The two click handlers shared an identical selection body, so unify them
into one helper and make the change in a single place.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Focusing the main view by keyboard already selects a whole change block in
hunk mode; a click, though, only ever placed a single-line selection, so the
common "click the diff to stage this block" gesture still needed a follow-up
`a`. Now a click on a change line selects that line's block too. A click on
context still selects just that line, since the click points at it precisely
(e.g. to edit it with `e`).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
§10 now lists diff-so-fancy alongside delta and difftastic: the same #2 case as
delta's default (strips +/- markers, conveys side by color) but a line-oriented
Perl filter, unified-only, and -- the one wrinkle worth stating in the spec --
it strips terminal escapes from its content, so the record is prepended to the
line rather than embedded. Fix the now-stale "all three"/"both pagers" counts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Record the third pager emitter (diff-so-fancy, commit c397cd6 on its
prototype-osc-metadata branch): the simplest of the three -- a line-oriented
Perl filter, unified single-column only, the same #2 category as delta's
default. Captures the diff-so-fancy-specific findings: sanitize_display strips
OSC so the record is prepended not embedded; the path is derived from
$file_1/$file_2 (not $last_file_seen, which is empty for a noprefix deletion);
combined diffs skipped; counters are file-scope globals so a chunk-spanning hunk
keeps counting; classification mirrors strip_leading_indicators so the
no-newline marker is correctly skipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add §4.4: a conforming pager emits a version-only OSC 1717 record (no further
fields) as its first output, so a host can probe it on an empty diff — which
emits no per-line records — and tell "speaks the protocol" apart from
"unsupported pager", content-independently. Used by the focused main view's
raw-diff fallback to decide whether a pager's output is actionable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A metadata-aware pager emits a version-only OSC 1717 record (no fields) as its
first output, to announce it speaks the diff-line-metadata protocol so lazygit
can probe for it. It isn't per-line metadata, so on a real render it must be
swallowed whole rather than lingering in the accumulator and attaching to the
following diff-header line (or, if newline-terminated, producing a phantom blank
line). Drop any OSC 1717 payload with no fields at its terminator; per-line
payloads always have fields, so they're kept.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire the remaining diff panels — local commits, sub-commits, commit files,
stash, and reflog — into the focused main view's raw-diff fallback, the same way
the files panel already is: each computes DiffMainViewShouldRenderRaw and renders
its diff raw (no pager) when focused under a pager whose output we can't resolve,
so its selection stays toggleable into a custom patch (or, for reflog, navigable
and copyable).
The commit-diff panels build their diff through the diff helper, which can't
reach the staging helper, so the panel computes renderRaw and passes it in.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Prep for extending the focused main view's raw-diff fallback beyond the files
panel. ShowCmdObj, ShowFileDiffCmdObj, ShowStashEntryCmdObj and DiffCmdObj gain
the ignoreExternalDiff arg WorktreeFileDiffCmdObj already has — forcing git's own
coloured diff regardless of a configured external diff command. All callers pass
false, so behaviour is unchanged.
Also lift the pty-vs-command task choice out of the files controller into
types.NewMainViewDiffTask(WithPrefix), so the commit/stash/patch-building panels
(whose diffs are built in the diff helper, which can't reach the staging helper)
can select the same raw-fallback task.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The focused main view is the staging surface, so it has to resolve the diff it
shows to patch-space. A pager that restructures the diff without emitting our
metadata (stock delta-default, plain difftastic, `cat -n`) produces output the
buffer parser can't read, which would leave its diff unstageable.
When such a diff is focused, re-render it raw — git's own colour, no pager — the
same content a no-pager setup shows, which the buffer parser handles. Browsing
keeps the pretty pager output; only focusing to act switches to raw.
Whether the pager is usable is decided by probing it: run it on empty input and
look for the version-only OSC 1717 handshake a metadata-aware pager emits first.
This is a pager-level, content-independent fact (a binary file, which has no
change lines under any pager, can't mislead it) and it's known before we render,
so we never render pretty only to discover mid-flight that we should have rendered
raw. The verdict is cached per pager (reset when the pager changes). No PTY is
needed — git needs a terminal to decide to invoke a pager, but the pager emits the
handshake regardless. A git-config external diff driver (useExternalDiffGitConfig)
is chosen per file via .gitattributes and a single diff can mix drivers, so
there's no one pager to probe; it's treated as unsupported (always raw).
Bypassing the pager needs two things, since a pager reaches the diff by two
routes: an external diff command (suppressed in the cmd via the new
ignoreExternalDiff arg, keeping git's colour, unlike plain) and a stdin pager
(GIT_PAGER, applied by the pty task — so the raw render uses a plain command
task instead).
This wires the files panel; the commit/stash/patch-building panels follow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`ctrl+o` in the focused main view copies the selection to the clipboard,
mirroring the staging view's copy. Unlike stage / discard, copying is the same
for every diff panel — it just reads the text shown — so it's a direct
MainViewController command rather than a FocusedMainViewActions method. That
keeps it free of per-panel duplication and lets it work over panels that have
no actions, notably the reflog.
The +/- prefix handling is metadata-aware where it matters: with no pager the
main view shows the raw diff, so a homogeneous selection has its +/-/space
column stripped (dropDiffPrefix) to ease pasting into code. With a pager
configured the rendered lines carry no such column, so stripping a leading
character would eat real content — copy verbatim instead. (Refining the raw
case to classify header lines via the diff-line metadata, fixing the
'--- a/file' edge case noted in 159bbb0825, is left for later.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The focused main view always showed a selection while it held focus, even when
its content was a placeholder ("No changed files", a merge-conflict message)
rather than a diff — so pressing a navigation key conjured a highlighted line
over "No changed files", and discarding the last change (or changes vanishing
outside lazygit) left a stale selection behind.
A selection should only show when there are change lines to act on. Two
moments establish or change the content:
- Focus: showInitialDiffSelection now leaves the selection off when the view
has no change lines (ViewHasChangeLines), rather than highlighting a stray
line at the top.
- Render: the side panel's render-to-main is where it decides between a diff
and a placeholder, so that's where the selection's visibility is set —
updateFocusedMainViewSelectionVisibility shows it only on the focused pane
and only when a diff is being rendered. This covers the refresh cases that
focus can't: discarding the last change, and changes disappearing or
reappearing outside lazygit, all hide or restore the selection on the next
refresh. (Focusing reuses the already-rendered content rather than
re-rendering, which is why focus needs its own check.)
Wired into the files panel, where placeholders occur; the commit panels always
render a diff. Adds a SelectionIsShown/SelectionIsHidden test assertion.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire the focused main view's `d` binding to a DiscardSelection action on the
side panel beneath, the discard counterpart of the primary action. As with
stage/toggle, what discard means is the panel's business:
- the files panel discards from the working tree, mirroring the staging
view's `d` — reverse-apply not cached on the unstaged side (destructive, so
confirmed), reverse-apply cached on the staged side (i.e. unstage);
- the commit panels (commit files, and the whole-commit diff of the local
commits / sub-commits / stash panels) remove the selected lines from the
commit via a rebase, mirroring the patch builder's "discard lines from
commit" — building a one-off patch from the selection and deleting it.
The commit-discard backend and its guard are shared free functions in
patch_building_from_main_view.go, paralleling the patch toggle. Discarding
from a commit is only possible on a local branch, so DiscardSelection grows a
disabled-reason companion on the FocusedMainViewActions interface: the `d`
binding is greyed with a reason on stash and other-branch sub-commits (never
rebaseable) and while a rebase is in progress, exactly as the patch builder
greys it. The files panel is always available (a zero-context diff is the one
error it reports inline, matching the staging view).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The focused main view's discard command (coming next) needs to read either
side's diff and apply a patch with an arbitrary direction and target: discard
from the working tree reads the unstaged diff but applies a reverse patch that
is not cached. stageDiffLines couldn't express that — it used a single reverse
flag both to pick which diff to read and to reverse the apply, and hardcoded
Cached: true.
The read side and the apply direction are independent (they coincide only for
staging and unstaging), so split them: applyDiffLines now takes sourceCached
(which diff to read) and a git_commands.ApplyPatchOpts (how to apply). Renamed
from stageDiffLines since it now covers stage, unstage, and discard. Staging
passes the same values as before, preserving behavior.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Each command a side panel handles in the focused main view needed its own
delegation channel: a HasKeybindings getter, an IBaseContext Add method, a
BaseContext field + getter, a baseController nil default, and an attach.go
registration — roughly five touch points per command. With three commands
(click, stage, toggle-patch) that was already a lot of boilerplate, and it
doesn't scale to the discard / copy commands coming next.
Replace the three channels with one: a side panel exposes a single
FocusedMainViewActions interface via GetFocusedMainViewActions (nil when its
diff offers no actions), and the controllers implement that interface
directly. The stage and toggle-patch handlers, already unified to a plain
error return, become one PrimaryAction method whose meaning is the panel's
business (stage for the files panel, patch toggle for the commit panels);
the click handler becomes OnClick. MainViewController is now a thin
dispatcher: fetch the actions from the panel beneath, call the method.
Adding a command is now one interface method, an implementation in the two
or three controllers, and a keybinding — no plumbing.
The toggle-patch channel did double duty as the "this panel builds a custom
patch" signal for the inclusion gutter (shown only beneath a patch-building
panel, not the staging files panel). Collapsing the channels removes that
proxy, so make the classification explicit on DiffMainViewContext, whose
marker method now returns a DiffMainViewType (none / staging / patch-building)
instead of being a bare marker. The gutter shows only beneath a panel whose
type is patch-building (commit files, local commits, sub-commits, stash).
Behavior-preserving.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The focused main view's space binding delegates to the side panel beneath
it, but the two handlers were asymmetric: the patch-toggle handler did its
own re-render and re-established the selection itself, while the staging
handler returned a focusViewName so the dispatcher (stageRange) could do the
reveal-and-focus dance on its behalf. That split blocks collapsing the
per-command handler channels into one, since the two have different return
types.
Make staging match the toggle: GetOnStageFocusedMainView returns plain
error, and the FilesController handler does the post-staging reveal and pane
focus itself. The reveal/select-mode logic the two handlers shared (collapse
a range to a line, preserve the change-line ordinal across the re-render,
re-expand a hunk) is extracted into revealSelectionAfterPrimaryAction, with
mainContextForViewName resolving a main view name to its context. The
dispatcher is now uniform: read the selected range, hand it to whichever
handler the panel registered.
Behavior-preserving.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Until now space toggled lines into a custom patch only from the commit files
main view (the per-file diff). Register the same toggle handler on the commits,
sub-commits and stash panels, so a patch can be built straight from the
whole-commit (multi-file) diff their main view shows, without first diving into
the commit files panel.
The handler lives on SwitchToDiffFilesController, which is already bound to
exactly those three panels and already knows how to derive the patch target
(from/to/reverse/canRebase) for the selected ref — pulled out of enter() into a
shared canRebase helper and the reused FromAndToForDiff. The post-toggle refresh
is the cheap one: these panels have no per-file patch indicator to update, so we
just re-render their own main + secondary views rather than reloading the whole
commit list on every keystroke.
Sub-commits and stash didn't render the secondary patch view at all; give them
the same secondaryPatchPanelUpdateOpts the commits panel uses, and recompute the
inclusion gutter as their diff (re-)renders, so the cumulative patch and the
gutter track the toggle.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The next commit adds patch building from the commits / sub-commits / stash
main views, which toggle the same way as the commit files main view; only the
patch target and the post-toggle refresh differ. Pull the panel-agnostic back
end (the toggle skeleton, the line toggle, the post-toggle reveal, the patch
filename mapping) into shared free functions so both controllers reuse it.
The skeleton now takes the patch target (from/to/reverse/canRebase) and a
refresh callback as parameters rather than reading them from the commit files
context and hardcoding Refresh({COMMIT_FILES}). The commit files handler passes
exactly what it did before, so behaviour is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Records the visibility model (gutter visible iff patch-active AND main
focused), the selection re-select after the split re-render, the
all-wrapped-segments marker, and the deferred pager-switch bug.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Records the fix for the §21.20 live-update gap: the patch toggle now
refreshes (updating the browser indicators + secondary patch summary +
the split), scroll/selection are preserved automatically because the
re-render uses the same diff command, and the gutter rides the same
re-render via the pipeline's own content-equality test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Records what landed, the confirmed sync (no re-render) insight, what
needs interactive sign-off, the known limitations, and the 6c/step-7
next steps.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pressing space in the commit-files focused main view now toggles the
selected line(s)/hunk into or out of the custom patch, instead of being a
no-op. It's the patch-building counterpart of working-tree staging from
the main view: the selection is resolved to change-line identities the
same way (ChangeLinesInViewRange), then mapped to the patch builder's
per-file line indices and added or removed.
The crux is that the commit's diff is unchanged by a toggle — only the
inclusion set changes — so unlike staging there's no async re-render to
ride. Membership is shown by an on-demand inclusion gutter: a reserved
left column painted with a marker on every change line currently in the
patch, recomputed synchronously after each toggle and on focusing the
main view, over the existing pager output. This is what lets patch
building work over a restructuring pager at all, where the old green
first-char overlay (which rewrites the rendered bytes) can't.
space routes to staging or patch building based on which handler the
panel beneath registers, via a new onTogglePatchFocusedMainView channel
(kept separate from staging because the post-action differs: sync gutter
repaint vs async reveal). Scoped to the commit-files panel for now;
commits/sub-commits/stash and the whole-commit multi-file diff follow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Prep for routing space to custom-patch building when the panel beneath
the focused main view is a commits/commit-files panel. stageSelectedLine
becomes the router; the staging-specific body (async re-render + reveal)
moves to stageRange. Behavior-preserving.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The patch builder works in patch-line-index space, which the patch
explorer can supply directly from its parsed-patch state. The focused
main view can't: it only knows a selection as diff-line metadata
identities (file line number + deletion?), and those indices differ
between the raw diff and however a pager renders it.
Add the conversion at the boundary so the main view stays in identity
space: PatchLineIndicesForLines maps identities to indices (for
toggling) and IncludedLineIdentities reports the included change lines
as identities (for the inclusion gutter to match rendered rows against).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A reserved left-hand column that draws a per-line marker glyph and shifts
the content right past it, for the custom-patch inclusion overlay (which
needs to show which change lines are in the patch over arbitrary pager
output, where it can't touch the rendered bytes).
It's pure draw-time decoration: the content buffer, and so the diff-line
metadata, click resolution and wrapping inputs, are untouched. The marker
is drawn on a line's first wrapped segment only, and the gutter narrows
the content wrap width while shown. Off by default; SetInclusionGutter
turns it on per view.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Records the plan for patch-building from the main view: the sync (no
re-render) vs async (staging) split, the included-set mapping, the
user's gutter decisions (on-demand, inclusion-only, checkmark), the
draw-time gocui rendering approach, and the 6a/6b/6c decomposition.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The reveal advanced the selection by capturing the next/previous change as a
patch identity and finding it again after the re-render. No line-number
identity is stable and unique across a stage, though: a deletion shares its
new-file number with the rest of its block (so staging one deletion of a block
landed back on the block's first line), and in the staged pane the new-file
number is the index, which staging/unstaging shifts (so unstaging a
modification's deletion missed its replacement line and fell back to an earlier
block). The previous worktree-line and adjacent-change-line matching each fixed
one case and left another.
Do instead what the staging view does: preserve the selection's ordinal among
change lines. Read the acted-on line's ordinal from the pane it was in before
the op; after the re-render, select the change line at that ordinal in the
target pane (clamped to the last). Since the op removes the acted-on change
line(s), that ordinal then holds the next surviving change — the next line of
the same block, or the next block when a whole block was staged. This needs no
reasoning about which side's line numbers are stable, so it handles deletions
and the staged pane uniformly.
The matching machinery (matchByWorktreeChange, AdjacentChangeLine) is gone; the
identity-matched restore (escape, -U preserve) and the new positional restore
now share installDiffLineRestore.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The post-stage reveal picked its primary candidate with AdjacentChangeBlock,
which skips the rest of the acted-on line's block. That's right in hunk mode,
where the whole block is staged, but wrong in line mode: staging the first
line of a multi-line block left the rest of the block in place, yet the
selection jumped past it to the next block instead of landing on the next
(now first) line of the same block.
Use AdjacentChangeLine — the first change line strictly after/before the
selection — for the next/previous candidates. It unifies both modes: after a
whole block (whose last line is followed by context) the next change line is
the next block's, so hunk mode is unchanged; after a single line the next
change line is the next line of the same block. place stays mode-aware
(hunk-expand vs single line), so nothing else changes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This makes it possible to see what lines delta renders as red or green.
It's not pretty, but makes testing easier for now. Needs a better
solution eventually.
The reveal that advances the focused main view's selection after staging
captured the next/previous change block as a patch identity and found it
again in the re-rendered diff via SamePatchLine. SamePatchLine keys a
deletion on its old-file (index-side) line number, which staging shifts:
staging a hunk that changes the line count moves the index-side numbers of
every hunk below it. So a deletion-led "next hunk" candidate no longer
matched, and the reveal fell back to a worse candidate — often colliding
with a header or context row (SamePatchLine doesn't require a change line)
and, in hunk mode, snapping to the first change block. The visible result
was the selection jumping to an *earlier* hunk after staging.
Match the reveal's candidates by their worktree (new-file) line number,
which staging never moves, and require a change line of the same side. This
is safe here because the reveal only ever targets change blocks and
selectHunkAround expands to the whole block, so the new-file number's
ambiguity between two consecutive deletions (the reason SamePatchLine uses
the old-file number) doesn't matter.
The escape restore and the -U context-size preserve keep SamePatchLine:
they re-render the same staged/unstaged state, so no index-side shift, and
the latter deliberately anchors on context lines.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
delta, fed a colored diff, renders some blank changed lines (an empty
deleted/added line) as just the OSC 1717 metadata followed by an empty
line — either with no fill at all, or as a background plus an ESC[0K that
the buffer now turns into a draw-time trailing fill rather than content
cells. Either way the line ends up with no cells, so its metadata had
nowhere to live and the line resolved to "not a change".
In the focused main view that broke change-block detection: a blank
deleted line in the middle of a hunk split it in two, so pressing 0
selected only the lines above the blank and the down arrow jumped to the
rest below it.
Restore the content-less sentinel cell finishLine used to add, but only
when there is pending OSC metadata to carry — so an empty line stays
resolvable as the change it is, without reintroducing the unconditional
sentinel (and its prevFgColor coupling) that was removed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
After staging or unstaging from the focused main view, focus stayed on the
pane the user acted in even when the side they were acting on moved to the
other pane. Two cases got it wrong:
- Unstaging the first hunk of an only-staged file splits the diff: the main
half flips to show the just-unstaged change, and the staged remainder jumps
to the secondary half — but focus stayed on the main half, away from the
staged content the user was working through.
- Unstaging the last staged hunk from the secondary half empties the staged
side and collapses the split, hiding the secondary half — leaving focus
stranded on a hidden pane.
The rule is the same in both: focus the staged side while it survives. The
stage handler now reports which focused-main pane should hold focus —
the secondary half when unstaging leaves the file split, the main half
otherwise — and the controller re-selects the revealed change in that pane
and focuses it. The split is read from the model, which Refresh has already
updated synchronously by the time the handler returns; the re-render it
queues is what the reveal rides.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Staging or unstaging can move the acted-on side to the other half of the
focused main view — unstaging the first hunk of an only-staged file splits
it, pushing the staged remainder to the secondary pane. The reveal that
re-selects the nearest surviving change after the op then has to ride the
re-render of that other pane, not the one the user acted in.
Split RevealSelectionAfterStaging's single view into a source (where the
candidate lines are read) and a target (where they're found again and
re-selected), and get-or-create the target's buffer manager so the restore
can be installed on a pane that hasn't rendered yet. Behavior-preserving:
the sole caller passes the same view for both, as before.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Staging or unstaging a selection re-renders the focused main view's diff (the
staged lines move to the other side), which left the selection at a now-stale
view position — often off the new, shorter content, so it looked like there was
no selection at all.
Install a restore before the re-render that lands the selection on the change
nearest the one just acted on, the same way the staging view advances: the
change block after the selection, else the one before it, else the acted-on line
itself (which only survives when the whole side was staged and the view flips to
the other side). It re-selects in the current mode, so hunk staging walks hunk to
hunk. A range selection collapses to a single line first, since staging consumes
it. This rides the existing restore-by-identity machinery (as the escape restore
does), so it works over any conforming rendering.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A side-by-side rendering (patched delta in side-by-side mode) carries more than
one diff-line record per rendered row: the deletion on the left, the addition
replacing it on the right. DiffLineContent.Metadata keeps only the first payload
— enough to identify a single-column row, but it drops the right side — so a
range staged from such a view would miss half the changes.
Add a gocui accessor for all the distinct metadata payloads a row's cells carry,
and have the range collector resolve every one of them, so staging a
side-by-side row includes both sides. You can't stage just one side of a
side-by-side row; that's an accepted restriction (switch to a single-column
rendering to do it). Rows without metadata (no pager, or the buffer-parse /
hyperlink backends) keep falling back to their single resolved record, so
single-column staging is unchanged.
The full delta-side-by-side chain can't run in the integration harness (no real
pager), so it's covered by a gocui unit test for the multi-payload accessor;
end-to-end behaviour needs interactive verification with the patched delta.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The focused main view of a directory node shows a multi-file diff, so a range
selected in it can span more than one file. Group the selected change lines by
file and apply one patch per file, logging the action and refreshing once around
the whole batch. The stage/unstage direction is uniform — the whole diff is
rendered on one side — so it's decided once for the batch.
Each change line is mapped back to its file by the path its diff-line metadata
carries, which also lets a single-file selection go through the same path
instead of relying on the selected node being the file; staging from the focused
main view no longer bails on a directory selection.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
With hunk mode the default (gui.useHunkModeInStagingView, on by default),
focusing the main view now selects the whole first visible change block rather
than a single line, matching what entering the staging view does and what the
just-added hunk staging expects. In line mode it still anchors on a single line,
as before.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Step 1 made the focused main view a staging surface for a single line. This
adds the range and hunk selection the staging view has, so you can stage (or
unstage) a range or a whole hunk in place — `v` starts a range, shift-up/down
extend one, `a` toggles hunk selection — without diving into the staging view.
The selection mode (line / range / hunk, plus sticky-range and "the user turned
hunk mode on themselves") lives on MainContext rather than the controller,
because three places need a pane's mode: the main view controller that drives
it, the focus controller that resets it when you focus the view, and togglePanel
which sets it on the *other* pane. The selected line and the range anchor stay
in the gocui view itself (its cursor and rangeSelectStartY), so the existing
native range-select rendering draws the highlight — no new highlight machinery.
Hunk bounds come from the diff-line metadata (the isChange run around the
cursor, via ChangeBlockBounds), not from a parsed patch, so they work over any
conforming rendering. The up/down keys are mode-aware: hunk mode steps hunk to
hunk, a non-sticky range collapses on a plain move, a sticky range extends.
GetOnStageFocusedMainView now takes the selected view-line range instead of one
line; the files handler collects the change lines in the range and applies a
single patch. It identifies each change line's patch line by scanning the parsed
patch and matching (file line number, deletion?) identities, rather than looking
each number up with PatchLineForLineNumber. That lookup can't tell an addition at
the very start of a hunk from the deletion above it, nor an addition from the
deletion it replaces on a modified line — both of which a range routinely spans.
As a side effect this also fixes staging a change on the first line of a file.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Record §21.10: step 1 is committed (always-show selection + single-line
staging), step 3's prep (the ViewSelectionController fold) is done, and the
step-3 feature design is fully settled — mode state mirrored from the staging
machine in view-line space, gocui's native range-select for rendering, ↑/↓ made
hunk-aware, config-driven hunk default, and range-aware staging. Captured so the
work resumes cleanly in a new session.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ViewSelectionController owned the focused main view's line navigation (up/down
move the selection or scroll, page up/down, goto top/bottom). It was extracted
long ago in case other scrollable views (e.g. a focused command log) would reuse
it, but none ever did — it's only attached to Normal and NormalSecondary, the
two focused main views MainViewController already controls.
Keeping the navigation in a separate controller is now in the way: the next
change makes up/down hunk-aware (move by hunk when hunk-select mode is on),
which needs the selection mode that lives in MainViewController. Rather than
teach the generic-looking ViewSelectionController about that mode, fold its
bindings and handlers into MainViewController and delete it. Behavior-preserving.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Record the session-11 reframe (use the diff-line-metadata primitive to fold
staging and custom-patch-building into the focused main view, dropping the
separate explorer views) with its feasibility analysis and an 8-step plan, plus
the session-12 findings from implementing step 1 (always-show selection +
single-line staging): the DiffMainViewContext predicate, the first-visible-change
anchor, the tab-selection fix, and two carried-forward concerns — the
per-command handler-channel won't scale, and delta's background-conveyed side is
hidden under the selection highlight.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The focused main view already lets you point at a diff line and act on it
(dive into staging, edit it, open it in a PR). This makes it a staging surface
in its own right: press space to stage — or unstage — the selected line without
diving into the separate staging view.
To make that usable, the selection is now shown automatically whenever the main
view holds a diff, anchored on the first change line already visible (so the
view doesn't jump), rather than being toggled on demand from the middle of the
view. That frees space for staging and means there's always a line to act on.
Which contexts show a diff is marked by a new types.DiffMainViewContext, because
"shows a diff" is the right signal, not "is stageable": reflog shows a diff (its
selection drives edit/PR/navigation) but can't be staged, while a branch's log
or the status dashboard show no diff and get no selection at all.
Staging is delegated to the side panel beneath via a GetOnStageFocusedMainView
handler mirroring GetOnClickFocusedMainView, so what "stage" means stays the
panel's concern (the working tree stages/unstages; commits will later add to a
custom patch). The files handler resolves the line's patch identity from the
diff-line metadata, maps it to a patch line, and applies a one-line patch,
choosing stage vs unstage from whether the shown diff is the unstaged or staged
side (diffSplitState).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The files diff renderer decides, from the selected file's staged/unstaged
status and the splitDiff config, whether the focused main view is split into
unstaged/staged halves and — when not split — whether the single view shows the
staged diff. A second consumer is about to need the same decision (staging a
line directly from the focused main view must know whether the shown diff is
staged or unstaged, to pick apply vs apply --reverse). Pull it into a method so
the two can't drift from each other.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Updates §20.5 with the pinned cause (the restore scan ran after the swap, so
the new content was briefly shown at the stale scroll) and the fix, plus
productionization notes: the resolve-then-swap ordering is an invariant of the
restore mechanism, the irreducible post-swap mapping window, and that fast
renders unmask latent transients slowness was hiding.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On a position-preserving re-render the first paint swapped the off-screen
content in and only then ran Apply, which for the buffer-parse backend (no
pager) scans the whole diff to locate the line to land on. That scan takes
tens of milliseconds on a large diff, during which the new content was already
displayed at the *old* scroll position — a layout draw landing in that window
showed a frame at the stale (and now out-of-range) scroll, a pronounced
flicker when changing context size while scrolled down. The metadata/hyperlink
backends didn't show it because they resolve the target during the load, so
their Apply is instant.
Let Apply own the swap: it locates the target against the still-off-screen
(and, at end of input, complete) buffer first, then calls swapIn and settles
the scroll. The scan now runs while the previous content is still displayed, so
the new content is revealed already at the right position — matching what the
early-resolving backends already did.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Records the scan-cost diagnosis and the batch-resolver fix (§20), and the
context-change flicker the speedup unmasked (§20.5), still to be pinned.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Changing the -U context size or switching pagers preserves the scroll
position by scanning the re-rendered diff for the line to land on. Each scan
resolved every buffer line through the per-line resolver, whose buffer-parse
backend re-parses that line's whole file section on every call — so a scan was
O(n2) in the diff length. On a 9600-line diff, changing context took ~33s with
no pager (worse with delta); the file/hunk navigation scans had the same cost.
Route the whole-buffer scans (the position restore's nearbyDiffLines and
end-of-load resolution, and the file/hunk navigation) through a new
resolveDiffLines, which parses each file section once for the whole buffer and
applies the metadata/buffer/hyperlink precedence on top — O(n). The incremental
restore scan now resolves only the rows that loaded since it last looked, using
the per-row backends (metadata/hyperlink) that don't need surrounding context;
the buffer-parse case still resolves once the diff is complete. The single-line
resolver (clicks) is unchanged.
Measured on a synthetic single-file diff, the whole-buffer scan drops from
1.7s to 1ms at 1700 lines and from ~107s to 15ms at 6800 lines.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The restore scan polls the off-screen render after each line read to find its
target. OffscreenDiffLineContents rebuilds a snapshot of every loaded row each
call, so polling per line is O(n2) on a large diff. Add
OffscreenDiffLineContentsFrom, which returns only the rows from a given index
onward, so a scan that remembers how far it has read can process just the new
lines.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The diff-line buffer-parse backend (mechanism #1) resolves one line at a
time, re-parsing that line's whole file section on every call. The position-
restore and navigation scans resolve every line of the buffer, so they pay
that whole-section parse once per line — O(n2) on a large single-file diff.
Extract the per-section parse (parseFileSection) and the section-bounds
search (fileSectionBounds) out of parseDiffLineFromBuffer, and add
parseAllDiffLinesFromBuffer, which walks the file sections once and resolves
every line in a single O(n) pass. Behavior-preserving: parseDiffLineFromBuffer
now delegates to the same per-section parser, so a single-line lookup is
unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When {/} change the -U context size with no selection showing, we re-anchor
the re-rendered diff on the top visible line. Anchor on the middle visible
line instead, so the diff appears to pivot around the line you're most
likely looking at rather than around its top edge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pressing space in the focused main view starts the selection at the middle
row of the viewport. When the diff is shorter than the viewport that row is
empty space below the content, so the selection clamps onto the last line
instead of landing somewhere useful. Anchor on the middle of the visible
content instead; once the content fills the view this is the same row as
before.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Re-verified empirically: a pure deletion's new-line is exact at default context
(difftastic emits d;4;4) and only drifts at --context 0 (d;1;4, low by the elided
count); old-line is exact either way. lazygit renders at default context, so the
drift isn't reachable in normal use, and even at zero context the only effect is
that 'e' on a deleted line opens the new file a few lines off — inherently
approximate, since the line isn't in the new file. Too marginal to keep as a v1
limitation, so §8 collapses to the single token-vs-line note. Re-verification
recorded in the design notes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Incorporate the round of feedback before circulating to pager developers. The
substantive decisions:
- Make f and h (file-header / hunk-header) mandatory v1 record types and drop the
speculative o; a header carries new-line = its hunk's first line so e lands near
what the user is looking at, and a multi-row header block (delta's box) re-emits
the record on every row, mirroring the wrapped-row rule. This replaces the old
reserved-and-unused framing and the host-side backUpOverHeader guessing.
- A host must ignore an unrecognized type rather than reject the record, so the
type set can grow without a version bump.
- Drop sections that were redundant or over-stated: §5.4 (the side is already
motivated in §1), §8.1 (c/a old-line is a host concern with no consumer), §8.4
(word-diff), and the at-most-two-records and trailing-fields paragraphs.
- Tighten the difftastic token-vs-line note (now §8.1): the c-on-a-pure-addition
old line only happens in difftastic's AST mode and the record faithfully matches
what difftastic renders (dim context); its line/Text fallback emits d/a normally.
- Document the rename rule for the file field, and reframe §7 as the host access
model the carrier is designed for rather than a description of the prototype.
Also record in the design notes the one user-visible bite of the token-vs-line
model: cross-pager selection preservation (delta d vs difftastic-AST c) — a host
concern kept out of the spec.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Record session 9 (§19): the modifier-click interaction, the per-terminal
probe findings that drove the alt+shift choice, the two gocui pieces, and
a planning hint that it lands as a separate PR at the very end of the
productionization stack.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
delta's clickable line-number hyperlinks let you jump to the editor from
the diff, but only on the line-number gutter (a small, fiddly target,
present only on added/context lines and costing horizontal space). Add a
modifier-click that opens whatever diff line is under the cursor — the
whole line is the target, deletions included, and the gutter is no longer
needed.
Both alt- and shift-click are bound because no single modifier survives
every terminal's mouse handling: Ghostty forwards alt (and keeps shift
for text selection), iTerm2 forwards only shift, and VS Code forwards
both. Whichever a terminal delivers triggers the edit; the one it keeps
for itself never reaches us. Right-click and ctrl-click were ruled out —
terminals variously claim them for context menus, promote ctrl-click to a
secondary click, or strip the modifier.
Unlike the `e` keybinding it doesn't require focusing the main view or
holding a selection, and being registered with HandleWhenPopupPanelFocused
it works while a popup covers the view — so you can jump to code shown
behind the commit-message panel, just like the hyperlinks did.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `e` keybinding resolves the file/line of the selected diff line and
opens it in the editor. A forthcoming right-click handler needs the same
resolve-and-edit step for the clicked line rather than the selected one,
so split it out behind a view-line-index argument. No behavior change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mouse clicks on a view other than the focused popup panel are normally
swallowed by the ShouldHandleMouseEvent gate, so a registered click
handler can't run while a modal is up. Hyperlink clicks already dodge
this by being handled in an earlier phase; generalize that to ordinary
mouse bindings via a HandleWhenPopupPanelFocused flag, dispatched before
the gate. No binding sets it yet, so behavior is unchanged.
This is what lets a click on the main view stay live behind a popup
(e.g. opening a diff line in the editor while the commit-message panel is
in front), the way the wheel already scrolls it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The terminal-allocation audit settled the protocol's OSC number on 1717 (unused
by every surveyed terminal — see diff-line-metadata-osc-spec.md), retiring the
456 placeholder. Rename the host side to match: the gocui carrier that accumulates
and reads back the sequence, the parser, and the handshake env var the pager
subprocess is given (now EMIT_OSC1717_METADATA). Flip the design notes and spec
from 'rename pending' to done.
The delta and difftastic emitters are renamed in their own repos.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve the OSC number from the 456 placeholder to 1717. There is no central
registry for OSC numbers, so the audit reduces to: pick a high, distinctive
number that no real terminal *acts on* — an unknown OSC is skipped harmlessly,
but a recognized one can fire a visible side-effect (OSC 555 flashes foot, OSC
777 raises a desktop notification), and the metadata flows through real
terminals whenever the pager runs outside a host. Audited the live OSC
allocations of xterm, VTE, kitty, foot, WezTerm, iTerm2, Windows Terminal,
Ghostty, VS Code, ConEmu and urxvt; 1717 collides with none and sits in the
empty 1400-5000 band (only iTerm2's 1337 is nearby).
Write the spec as a standalone draft to circulate to pager developers for
feedback — separate from the internal session notes, with motivation, the v1
wire format, the env handshake, semantics, emit rules (including the per-row
wrapping correction and the side-by-side two-records-per-row case), and the
known v2 candidates (both-numbers-always, the difftastic token-vs-line
mismatch). The prototype code still emits the 456 placeholder; the 456->1717
rename across delta/difftastic/gocui/lazygit and the EMIT_OSC<n>_METADATA env
var is a tracked follow-up, not part of this commit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Record session 8 — preserving scroll/selection when cycling pagers, as
the seventh consumer of the identity-based restore. Captures why both the
plain-pager (GIT_PAGER, line-number kept) and externalDiffCommand
(command changes, reset to top) cases needed it, the "main"-window-only
scope, the graceful no-op fallback, and that the side-by-side<->inline
structural win awaits the §17.4 row+column resolver.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cycling pagers re-renders the diff into the main view. Until now that
either lost the scroll position outright — an entry with its own
externalDiffCommand changes the actual git command, so the re-render
reset the view to the top — or kept it only by raw line number, which a
plain pager swap got for free because the git command was unchanged.
Raw line number is the wrong anchor: two pagers can structure the same
diff very differently (side-by-side vs inline), so the same screen line
means something different afterwards. Reuse the identity-based restore
(PreserveDiffPositionOnRerender, already driving the -U context-size
consumer) to re-anchor on the same patch line instead. That both keeps
the position meaningful when the structure changes and covers the
externalDiffCommand case the line-number approach couldn't.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The same pager-level wrapping gap found in difftastic (§10.8) was fixed in delta
too. Delta only wraps in side-by-side mode, so the bug was SxS-only; each wrapped
continuation row now re-emits the record of the primary line it continues,
without advancing delta's line-number counters (so subsequent lines keep correct
numbers). Update §9.3, §10.8, and the §17.1 correction note accordingly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Testing the difftastic prototype in lazygit exposed that the "wrapped
continuation rows carry no attachment" convention is a bug whenever the pager
itself wraps a long line (difftastic side-by-side, delta with wrap-max-lines):
each wrapped row is a distinct host buffer line, so e/enter and hunk navigation
break on the un-tagged continuation rows. The fix -- emit the line's record on
every output row, not just the first -- was applied to difftastic; delta needs
the same. Record the underlying distinction (terminal/host wrapping vs
pager wrapping) and state the spec rule positively (§10.8).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
difftastic is now prototyped as an OSC-metadata emitter in both its modes
(side-by-side and inline), the one pager mechanism #1 categorically cannot
serve. The notes capture what was built and, more importantly, what it reveals
about the v1 format: it holds, but difftastic's token-granular structural model
surfaces a model mismatch the unified-diff pagers hid -- an aligned row whose
old side has no novel tokens is faithfully a context cell plus an addition cell,
with no deletion record for the old side. Also resolves the "how many regions
per row" open question (two, not N) and amplifies the §17.3 "context/added carry
no old-line" limitation, since difftastic's old/new line numbers diverge with no
constant offset.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The OSC-metadata prototype now extends to difftastic (the categorical #2-only
pager), checked out at /Users/stk/Stk/Dev/Builds/difftastic.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A parallel session added per-line OSC metadata to delta's side-by-side mode
(in the delta repo, not lazygit) and recorded its findings here as §17, with
the §15 roadmap updated to mark step 3 done.
The headline, which feeds the OSC spec draft: side-by-side needs no change to
the v1 wire format — `type` already implies the column (a→right/new, d→left/
old) and context is symmetric, so no side discriminator is warranted. This
corrects §15's earlier guess that side-by-side would likely add payload, and
leaves the OSC number as the spec's only remaining open item. Also flagged for
productionization: the host reader becomes row+column→identity (a changed row
carries two records), which the §16 consumers haven't yet been exercised
against.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hunk/file navigation (#4) and -U-context-change scroll preservation (#5) are
built; document the design, decisions (main-view-only scope, offset-preserving
placement, file nav landing on the file top, the visibility guard) and the
pending interactive sign-off.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add file and change-block ("hunk") navigation to the focused main view,
mirroring the staging view's hunk keys: `<left>`/`<right>` jump to the
previous/next hunk and `n`/`N` to the next/previous file. A "hunk" here is
lazygit's notion — a run of consecutive added/deleted lines separated by
context, not a git `@@` section — matching what the staging view jumps
between.
This is a consumer of the diff-line primitive in its forward direction:
resolve each rendered row's patch identity, then scan for the next/previous
change block (by the line type) or file boundary (by the path changing). The
file scan lands on the top of the neighbouring file even when a restructuring
pager leaves the header rows untagged, by backing up over them from the
file's first identifiable row — which is impossible without the per-line
metadata once the pager stops emitting a parseable unified diff. The
boundary arithmetic is pulled out into pure functions and unit-tested.
The anchor is the selected line if a selection is showing, else the top
visible line. With a selection we move it to the target and scroll it into
view, like the staging view; with none we stay in scroll mode, bringing the
target to the top without creating a selection.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Increasing or decreasing the diff context size (the `{`/`}` keybindings)
re-renders the diff with a different `git diff -U<n>` command. Because the
command key changes, the render reset the main view to the top — losing the
spot the user was reading, which is exactly the spot the context change is
about.
Preserve it instead, reusing the identity-based restore built for the escape
path. This is its sibling consumer: capture the lines around the anchor (the
selection, or the top visible line when there's none) as restore candidates,
and after the re-render land on the nearest one that survived, put back at the
same screen row. Prefer the anchor line itself, falling back outward only when
it didn't survive: a context line vanishes when the context size shrinks,
whereas additions and deletions always survive, so expansion stops at the
first change line in each direction and the candidate list always contains a
survivor. Landing on the nearest survivor keeps scrolling to a minimum, and a
context line that is still in the patch stays put (or stays selected).
This generalizes the shared restore helper to take a prioritized candidate
list (the escape path passes a single candidate). It covers the focused main
view and every side panel's diff, since they all render into the same "main"
view through the same path. A showing selection is re-established on the landed
line; otherwise the view stays in scroll mode.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The escape restore's mechanism — set a RenderRestore that scans the
re-rendering content for a target patch identity and, once it loads,
positions the view on the matched row — is about to gain a second caller:
preserving a diff view's scroll/selection when its -U context size changes
re-renders it (the sibling consumer of the diff-line primitive). That caller
positions the row differently (put it back where it was, rather than scroll to
and select it), so split the positioning out behind a `place` callback and
keep the scan/swap machinery shared.
Behaviour-preserving: RestoreFocusedMainViewOnEscape passes the same
FocusPoint-and-select closure the inline Apply used.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When a main view re-renders content different from what it last showed, the
scroll resets to the top. That reset fired synchronously when the task started —
but with the off-screen render the previous content stays displayed until the
swap, so resetting the origin up front scrolled that still-visible content to the
top before the new content replaced it: a distracting jump when switching commits
(or any item) while scrolled down.
Defer the reset to the task's first paint (the swap), alongside the restore that
already runs there: the previous content stays at its scroll until the new content
takes its place, then the new content appears at the top. A same-content re-render
keeps its scroll (no reset); a restore places the scroll itself. The "loading..."
indicator path also resets the origin now, since it clears the previous content to
show the message and must put it at the top.
The reset moves out of NewTask (it no longer needs the task key or the pending
restore for this) into the read loop, driven by LinesToRead.ResetOrigin, which the
cmd/pty wrappers set from the key comparison the reset used to do. The manager's
onNewKey callback is renamed resetOrigin to match its now-decoupled timing.
onMouseMove (and findHyperlinkAt, which it calls) read v.viewLines without
holding writeMutex, unlike every other reader. They run on the event-handling
goroutine, so a re-render on the task goroutine can shrink or rebuild viewLines
between onMouseMove's bounds check and findHyperlinkAt's indexing, causing an
out-of-range panic (observed: "index out of range [60] with length 0" while
hovering during a diff re-render).
Take writeMutex for the duration, like the other viewLines readers do, so the
check and the access see the same slice. Pre-existing, but the off-screen
re-render rebuilds viewLines on the task goroutine more often, widening the window.
§14: the escape restore now anchors on a patch identity scanned from the loading
re-render (items 1+3 of the part-3 plan, which collapsed into one mechanism), the
partial §12.2 routing fix, and the analysis for the deferred pieces (the (b)
no-clobber lever's interaction with the entry origin reset, NormalSecondary
routing, the hyperlink-backend match limitation, the O(n^2) scan). Records that
interactive sign-off — including the still-pending session-5 scrollbar/stopped-task
checks — remains, since the agent couldn't drive the TUI.
Diving into staging from a focused main view records a snapshot so escape can
return there. But it was recorded only on the half we entered (unstaged), and
staging the last unstaged hunk moves the selection to the staged half
(RefreshStagingPanel pushes StagingSecondary when the unstaged state goes empty).
Escaping from there found no snapshot and fell back to the files panel, instead
of returning to the focused main view.
Record the snapshot on both staging halves at entry, and clear both on escape.
Escaping from either half now returns to the focused main view, and the
identity-based restore lands on the line the explorer ended up selecting — which,
with the last unstaged hunk gone, is shown in the main view (the file now has only
staged changes, so no split). Routing the split-and-tabbed-to-staged case to the
secondary focused main view is left for follow-up; see focused-main-view-notes.md
§14.3.
Escaping a patch explorer (staging / patch building) back to the focused main
view it was entered from used to replay a numeric scroll position and selection
index captured on the way in. But the reason to escape after staging or dropping
a hunk is that the content changed, so a saved index points at the wrong line —
and the host auto-advances the explorer's selection to a still-valid line anyway,
which is the line the user actually cares about returning to.
Restore by *patch identity* instead. On escape, read the (file, type, source
line) the explorer currently has selected, then have the main view's re-render
land on the row that matches it: scan the incoming content as it loads (the
inverse of the diff-line primitive), and once the matching row plus a screenful
below it have loaded, swap the off-screen render in and scroll to / select that
row in one step. FocusPoint with scrollIntoView centres the row only if it's
off-screen, so the common unchanged-content escape — where the row is already
where it was — doesn't move at all. If the line is gone (the content really
changed), nothing is forced.
This generalizes the scroll restore from a fixed origin to a predicate
(RenderRestore: FirstPaintReady decides when the saved position is reachable,
Apply re-establishes it), folding the separate selection restore into the same
first paint — so it no longer rides a post-load callback that could fire early.
The restore also now survives task replacement, which the numeric version did
not: a periodic refresh can stop the escape's re-render before it first-paints.
The pending restore is held on the buffer manager and is *not* cleared when a
task starts, so the replacement task picks it up. It is not gated on the command
key — staging the last unstaged hunk re-renders `git diff` as `git diff --cached`,
a different command, yet the line to land on is still in the new content — but
validates itself: the scan finds the target line only when the content still
contains it, so applying it to a different item is a harmless no-op. A task
clears it once it has applied it (found or not), so it lives for exactly one
re-render. Because the restore is anchored on content identity and is idempotent,
"survive replacement" and "restore by identity" are one mechanism, not two.
With the identity in hand the snapshot no longer needs the captured scroll/index;
they're derived from the explorer's live selection.
The escape restore (next commit) finds the row in a re-rendering focused main
view that matches a target patch identity, and it has to do so while the content
is still loading — i.e. against the off-screen buffer, before it is swapped in,
since the displayed buffer still shows the previous render.
Add the two primitives that scan needs:
- OffscreenDiffLineContents exposes the per-line diff material (text, metadata,
hyperlink) of the rows read so far into the off-screen render, so the resolver
built last commit can run against the incoming content.
- ViewLineForBufferLine maps a matched (unwrapped) buffer line back to the first
view line that renders it — the inverse of BufferLineForViewLine — so the
restore can scroll to and select that line once the render is swapped in.
The diff-line primitive (recover a rendered row's patch-space identity) is about
to gain a second, inverse consumer: the escape restore scans a focused main
view's rows as it re-renders, looking for the row that matches a target patch
identity. That scan runs over the *loading* off-screen buffer, not the displayed
view, so the resolver can't be tied to the displayed view's per-view-line readers.
Pull the three backends (OSC metadata, buffer parse, lazygit-edit hyperlink) onto
a single buffer-agnostic resolver that takes a snapshot of a diff's per-line
content — text, metadata and hyperlink per unwrapped buffer line — and the line
to resolve. The forward consumers (click/enter/edit/PR) feed it a snapshot of the
displayed buffer (gocui.DiffLineContents) after mapping the wrapped view line to
its buffer line; the upcoming scan will feed it the off-screen buffer's loaded
rows. Behavior is unchanged.
When a task is stopped to make way for a newer one, stopping closes
opts.Stop, and the scanner goroutine then closes lineChan. The read loop's
select between those two channels is therefore non-deterministic: it can
land on the closed lineChan (ok == false) instead of the opts.Stop case,
sending a stopped task into the end-of-input branch.
There it runs the full finalize — swapping its half-read off-screen buffer
in, applying the saved scroll, clamping the origin to the truncated
content, and clearing the loading flag — all of which corrupt what the
incoming task is about to render. The most visible symptom is a brief frame
of truncated content with the scroll yanked to the top, seen when re-renders
overlap rapidly (e.g. the periodic background refresh re-rendering a main
view faster than it can load, very easy to hit under LAZYGIT_SLOW_RENDER).
The underlying bug predates the off-screen render (the EOF branch always
clamped the origin via onEndOfInput), but that change made it far worse by
also swapping a truncated buffer into the display. Fix it at the source: in
the EOF branch, check whether we were stopped and, if so, bail out like the
explicit stop case, leaving the view entirely to the task that replaces us.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A cmd/pty re-render used to overwrite the displayed buffer from the top
down as lines arrived, relying on keeping the previous render's view-line
tail to avoid a blank frame. That left the view showing a mixture of old
and new content while loading, and any reader (draw, the diff-line
mapping, clicks) could observe a half-written buffer at the wrong scroll —
the §11 Race A flicker and the §8 stale-tail mapping both came from this.
Instead, build the new content in a second, off-screen viewBuffer: until
the task has read enough to paint, writes go there and the displayed
buffer — and so everything every reader sees — is left untouched. Once the
task reaches its first-paint point (InitialRefreshAfter, or EOF for short
content) it swaps the off-screen buffer in atomically and applies the
saved scroll in the same step, so the view jumps straight from the
previous render to the new one with no intermediate frame. Subsequent
lines append to the now-displayed buffer.
Swapping at the first-paint point means the displayed buffer is only a
viewport tall when it appears and then grows as the rest streams in toward
the count needed for an accurate scrollbar. The scrollbar is sized from the
displayed buffer's height, so left to itself the thumb would shrink and
snap back during that growth (most visibly: the files panel's periodic
refresh making the thumb jump while scrolled down). The total height the
scrollbar needs is a strictly later quantity than the viewport-fill paint,
so no single early swap can have both right. FreezeScrollbarHeight therefore
records the view's height when a load begins and the scrollbar is held there
— growing only if the new content turns out taller — until the load ends; a
synchronous render superseding the load releases it. This mirrors the layout
clamp, which already ignores the partial content height while a view loads.
With the swap doing a wholesale replace, refreshViewLinesIfNeeded can
truncate the view lines to the current buffer: there is no longer a
half-loaded shorter buffer whose tail we must keep showing, so the stale
tail (§8) never forms. clear()/Reset() abandon any in-progress off-screen
render so a synchronous SetContent after a stopped task writes to the
display.
This replaces the holdViewLines and freshViewLineCount patches reverted in
the previous two commits with one mechanism. The swap holds writeMutex for
now; it could later move to the main thread. Flicker behaviour still needs
interactive verification (LAZYGIT_SLOW_RENDER + a real pager).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This reverts the freshViewLineCount stale-tail guard (§8), restoring the
demonstrate-the-bug test state. The upcoming off-screen render rebuilds
the displayed buffer wholesale on swap and lets refreshViewLinesIfNeeded
truncate, so the stale tail never forms — a cleaner fix than tracking a
fresh-count. Removing the guard on its own keeps that change focused; the
bug it guarded against is re-fixed by the truncation in the next commit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This reverts the holdViewLines flicker patch (Race A). It is about to be
superseded by rendering a re-render into an off-screen buffer and swapping
it in atomically, which keeps the displayed buffer (and so every reader)
untouched until the new content is ready — a cleaner mechanism than
suppressing the view-line rebuild. Removing it on its own keeps that
upcoming change focused.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
write, writeCells, makeWriteable, parseInput and
autoRenderHyperlinksInCurrentLine produced cells into v.buf; move them onto
viewBuffer so they can write into any buffer, not just the displayed one.
The display-side effects that don't belong to content production —
tainting, clearing hover, updating search positions — stay behind in the
View.write wrapper, which delegates the actual writing to v.buf.write(v).
Render config the writer needs (Editable, colors, width, tab width,
hyperlink auto-render) is read from the passed View. Behaviour-preserving:
the wrapper still always targets v.buf.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The fields that make up a view's content and the act of writing to it —
the cell buffer (lines), the write cursor (wx/wy), the escape-sequence
decoder (ei) and the held-newline flag (pendingNewline) — were loose
fields on View. Bundle them into a viewBuffer struct that View holds by
pointer. This is a behaviour-preserving prep refactor: every access just
goes through v.buf now. It sets up rendering into a second, off-screen
viewBuffer that can be swapped in atomically, so an async re-render never
exposes a half-written buffer to readers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When escaping to a focused main view scrolled down, the re-render keeps
the previous content as a placeholder and the task scrolls to the saved
position at its first paint. But the task's writes mark the view tainted,
so any layout pass landing between the first write and the first paint
rebuilds the view lines from the half-loaded buffer and draws them at the
placeholder's scroll — a brief frame of the wrong content at the wrong
scroll before it snaps into place. Intermittent, and only visible when the
load is slow enough for a layout pass to fall in that window.
Give the view a hold: while set, refreshViewLinesIfNeeded keeps the
current view lines instead of rebuilding from the buffer, so the view goes
on drawing the coherent placeholder. The re-render task sets it only when
restoring a scroll position (so the normal load-from-top case is
untouched) and releases it at its first paint, which applies the saved
scroll in the same step — so the loaded content appears at the restored
scroll with no intermediate frame. While held, the displayed view lines
need not match the loading buffer, so the view-line→buffer-line mapping
reports no result.
This needs interactive verification (LAZYGIT_SLOW_RENDER + a real pager);
see focused-main-view-notes.md §13.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Escaping to a focused main view restored the selection by scheduling, on
the next UI tick, a ReadToEnd whose callback re-selected the saved line.
But ReadToEnd fires its callback synchronously when the manager has no
live read channel, and the re-render task triggered by the push creates
that channel later, inside its own goroutine (after stopping the previous
task). If the UI tick won that race, the restore ran before any content
was loaded, FocusPoint no-oped against the unloaded line, and the
selection was silently dropped — intermittently, and more often under
load.
Thread the restore through the task instead: a thenForNextTask hook on
the buffer manager, folded into the next cmd/pty task's initial-read Then,
mirroring scrollToOriginYForNextTask. It runs once the task has read
enough to place the selection, and can't fire before the task exists. The
scroll restore already applies at the task's first paint, which precedes
the initial read's end, so the origin is in place when the selection is
restored.
This needs interactive verification (LAZYGIT_SLOW_RENDER + a real pager);
see focused-main-view-notes.md §13.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pin the two escape-restore races to concrete mechanisms: Race A (layout
draws reveal partial content at the placeholder scroll before the task's
first paint) and Race B (the selection restore can fire before the
re-render task's readLines channel is live). Classify both as bounded
interleavings, sitting over the one fundamental constraint (no flicker-free
first paint of the target scroll without buffering that screenful), and
record the prescribed fixes and their implications for part 3. Documents
why faithful repro is the interactive app, not the headless harness.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Track how many leading viewLines entries the most recent refresh built
from the current buffer (freshViewLineCount) and bound the view-line→
buffer-line mapping on it. The entries past that count are the stale tail
refreshViewLinesIfNeeded leaves in place for flicker-avoidance; they
belong to a previous, longer render and must not be mapped. The old
in-range guard was insufficient: with wrapping a stale entry's buffer
index can still be in range of the shrunk buffer. Within the fresh range
every entry was just built from the current buffer, so its index is
guaranteed in range and the guard is no longer needed.
This is the §8 correctness fix the identity-based escape restore depends
on, since that read scans the view buffer while it is still loading.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refreshViewLinesIfNeeded keeps the previous render's view lines in the
tail when the new render is shorter, so the view can go on showing old
content without flicker until the new content catches up. The mapping
readers guard only against a buffer index that has gone out of range of
the shrunk buffer — but with wrapping, a stale tail entry's index can
still be in range, so the guard passes and a view line that no longer
exists is mapped onto the wrong buffer line. See diff-line-metadata-notes.md §8.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
HyperLinkInLine, DiffLineMetadataInLine and BufferLineForViewLine each
repeated the same preamble: take the lock, refresh the view lines, range-
check the view line, and guard against a stale viewLines entry pointing
past a shrunk buffer. They all need that mapping to stay consistent with
the buffer they then read, so the logic belongs in one place. Extract it
into bufferLineForViewLine and have all three call it. Behavior-preserving.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Capture a design discussion (no code yet; implementation is a future session):
- The escape-from-staging restore should anchor on the explorer view's current
patch identity at escape time, not a saved numeric scroll/index, since staging
or dropping hunks changes the content. This is the inverse direction of the
diff-line-metadata primitive (identity -> rendered row) and the same operation
the -U scroll-preservation consumer needs; record it as consumer #6 and split
the consumer list into forward (1-4) and inverse (5-6) directions.
- Record the escape-routing cases the current prototype gets wrong (staging the
last hunk should land in the staged half; <tab> between staged/unstaged; the
empty-view and custom-patch-builder cases).
- Decide to solve the new restore mechanism, the §11 timing races, and the
BufferLineForViewLine staleness trap together in the prototype rather than
defer them to productionization (you can't plan around unsolved entangled
mechanisms), with a dependency-first attack order (§8 fix, characterize the
races, then the predicate-scroll restore).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The emitter (delta), carrier (gocui per-cell attachment), and consumer (the
GetDiffLineInfo metadata backend + env-var handshake) are now built for the
normal unified case and verified end-to-end — including in the running app with
delta's default mode, where clicking/enter/e/G resolve via #2 and deletions get
the correct side. Update §9 from "in progress" to a "built & verified" record
(what was built, how it was verified) mirroring §8 for #1, and mark the build
order (§7) accordingly. The remaining step-5 deliverables — finalizing/publishing
the spec and the production plan — and side-by-side/difftastic are still open.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add mechanism #2 to GetDiffLineInfo: when a patched pager annotated each diff
line with OSC 456 metadata, read it back as the first backend, ahead of the
buffer parser (#1) and the lazygit-edit hyperlink. It is strictly higher
fidelity -- it carries the side explicitly, so it serves the renderings #1
cannot parse (delta's default mode, --line-numbers, diff-so-fancy) and conveys
deletions, which the hyperlink can't.
The host advertises the protocol versions it understands by setting
EMIT_OSC456_METADATA on the pager subprocess; a pager that doesn't understand
it ignores the variable, so this is safe to set unconditionally.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A patched pager (delta) prefixes each diff line with an OSC 456 sequence
carrying that line's patch-space identity (see diff-line-metadata-notes.md),
so the host can map a rendered row back to (file, type, new-line, old-line)
without re-parsing -- the only way to recover the side for renderings that
drop the +/- markers (delta's default mode).
Recognize it in the escape interpreter and stamp the payload onto each cell,
mirroring how OSC-8 hyperlinks are handled, exposing it via
DiffLineMetadataInLine. To do so, generalize the OSC dispatch to accumulate
the (possibly multi-digit) OSC number before branching, rather than matching
the single character '8'; the OSC-8 path is unchanged for well-formed input.
Unlike a hyperlink, the metadata sequence is never closed -- the pager
re-emits one per line -- so clear it at each line boundary to keep it from
bleeding onto a following line that has none (e.g. a hunk header).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mechanism #2's emitter side is now prototyped on this branch (delta, normal
unified mode only) and the bytes are verified. Capture what that settled, since
the spec is meant to be published for pager-developer feedback:
- The single per-line emit point in delta and which fields are reachable there
(the gotcha: delta only maintains its line-number counters with --line-numbers
on, so the patch tracks its own), and why a dedicated additive emitter beats
reusing LineNumbersData.
- The pinned v1 wire format (positional, file last so it may contain ';',
empty old-line unless deleted) and the EMIT_OSC456_METADATA env-var handshake.
- Deferred items: the OSC-number terminal audit, wrapped continuation rows, and
header-row attachments.
Also resolves the §6 wire-format open question accordingly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Record what the #1 prototype settled. The two open questions #1 touches are now
answered (the deleted-line new-line convention is patch.LineNumberOfLine's, and a
deletion carries both line numbers; multi-file diffs split on "diff --git" and
the section parses 1:1 with patch.Parse). Add a §8 capturing what landed, how it
was verified, and the implications for #2.
Two coverage corrections came out of verifying against real pager output, both
worth pinning before the spec is written: delta --color-only qualifies for #1
only *without* line numbers (the gutter pushes the +/- marker off column 0, so a
naive parse is confidently wrong — handled by an integrity check that falls back,
not by teaching the host delta's gutter), and diff-so-fancy strips the +/- markers
entirely, so it's a #2 case, not #1.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The focused main view's click/enter/e/G handlers all need the same thing: given
a rendered diff row, the patch-space line it corresponds to. Until now that came
solely from delta's lazygit-edit:// hyperlinks, which only carry a path and a
single line number — no side. That's lossy: for a deletion the number is the old
line, but the consumers fed it into new-file lookups, and two consecutive
deletions (which share a new-file line number) couldn't be told apart at all.
Replace GetFileAndLineForClickedDiffLine with GetDiffLineInfo, returning the
fuller (file, type, new-line, old-line) record from diff-line-metadata-notes.md.
This is mechanism #1: parse the decolorized view buffer — walk up to the file's
"diff --git" section, reuse patch.Parse on it (splitting multi-file commit diffs
on the "diff --git" boundaries), and read the type and line numbers off the patch
arithmetic. It serves the structure-preserving renderings — no pager, git diff
--color, and delta --color-only without line numbers — with no external
dependency.
To avoid trusting a mis-parse, the parser bails when a hunk's body no longer
matches its header (Patch.IsWellFormed). That's what happens when a pager keeps
the diff/hunk headers but restructures the body: delta's line-number gutters push
the +/- marker off the start of each line, so every body line reads as context.
Such renderings fall through to the next backend rather than yielding a confident
wrong answer. (diff-so-fancy goes further and rewrites the headers too, so it
fails even earlier, on the missing "diff --git".)
GetDiffLineInfo is a seam with swappable backends: the buffer parser first, then
the old hyperlink reader as a fallback for renderings the parser can't handle
(delta's default mode, or delta with line-number gutters). The future #2 OSC
per-cell metadata reader plugs in ahead of both, behind the same record shape.
Wire the consumers to the record per that doc's field mapping:
- dive into staging/patch building lands on the exact patch line, looking a
deletion up by its old-file line number (PatchLineForOldLineNumber) so the
two-deletions case resolves correctly;
- `e` edits at the new-file line;
- `G` anchors the PR link on the left (old) side for a deletion, the right (new)
side otherwise.
The hyperlink fallback can't convey the side, so it reports DiffLineOther, which
the consumers treat as a non-deletion — i.e. exactly today's behavior.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The diff-line parser needs to walk the unwrapped diff buffer upward from a
clicked/selected row, but the row index it's handed is a view line index (which
counts wrapped lines). Expose the existing internal mapping (viewLines[y].linesY)
so callers can translate, with the same lock and stale-tail guard that
HyperLinkInLine uses against a concurrent shorter re-render.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The patch package can already map a patch line index to its new-file line
number and back (LineNumberOfLine / PatchLineForLineNumber). The diff-line
parser being built on top of this needs the old-file equivalents: a deletion
has no distinct new-file position (two consecutive deletions share one), so to
land on the exact deletion when diving into staging we have to look it up by
old-file line number.
Add OldLineNumberOfLine and PatchLineForOldLineNumber as direct mirrors of the
new-file functions, counting DELETION+CONTEXT lines instead of ADDITION+CONTEXT.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Captures a design discussion about the remaining big problem behind the
focused-main-view feature: recovering a diff row's patch-space identity
(file, type, source line) when the rendering came from a pager. Records
the two complementary mechanisms (a host-side parser for
structure-preserving renderings, and a pager-emitted OSC protocol for
ones that restructure), the per-cell carrier with its keyboard/mouse
access rules, the type + old?/new-line payload and why each field is
load-bearing, and the version-negotiation handshake. Design only; no
implementation yet.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diving into staging or patch building from a focused main view (by
double-clicking a line, or pressing enter on the selected line) always
landed on a single-line selection. Entering the same views through the
side panel honours the UseHunkModeInStagingView config and selects the
whole hunk by default. The two ways in should agree, so that diving in
from the main view feels like the established flow.
A non-negative line index in NewState was overloaded for two intents:
clicking directly on the patch explorer view (where a single-line range
is the start of a drag) and diving in from the main view (where we want
the default select mode). Distinguish them with SelectLineInDefaultMode
on OnFocusOpts: the main-view entry points set it; the click-to-drag
path does not.
In hunk mode the selection covers the block of changes around the
clicked line. A context line has no surrounding changes, so we snap to
the next change line (as toggling hunk mode does); the clicked context
line itself is then not part of the selection.
This is a separate commit only because the branch is a throwaway
prototype; in a real history it would be folded into the commit that
introduces the focused-main-view enter behavior rather than landing on
top of it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Records session 3: the cmd/pty scroll-restore mechanism, the refreshMainViews
reset reorder, the corrected onNewKey understanding, and the remaining
timing-race investigation to do before productionizing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EscapeFromPatchExplorer re-renders the side panel's content back into the main
view and wants to land at the scroll position and selection the user had before
diving into staging/patch building. The previous version set the origin on the
next UI tick, after the placeholder had already been painted at the wrong
position, so the restore was visible as a jump.
Instead, ask the re-render itself to restore the scroll (via
ScrollToOriginYForNextTask), so the saved position is applied in the first
paint that shows the real content. The selection still needs the diff loaded
down to the selected line, so restore it via ReadToEnd once the content is
fully read; the scroll is no longer touched there.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When re-rendering content the user was already scrolled into, we want the
saved scroll position applied exactly when the real content first paints — not
before. Setting the origin up front instead paints it onto whatever placeholder
is currently in the view (e.g. the shorter buffer CopyContent left there),
which flickers: either a blank frame past the placeholder's end, or a jump to
the top when the task resets the origin at startup.
Add ViewBufferManager.ScrollToOriginYForNextTask: the next cmd/pty task then
(a) does not reset the view to the top at startup even though the command key
changed, so the placeholder stays put, (b) sizes its initial read to the saved
position so enough content is loaded to fill the view there, and (c) scrolls to
it as part of the first refresh, in the same paint that shows the real content.
This is the cmd/pty analogue of RenderStringWithScrollTask.
No caller sets it yet, so this is behaviour-preserving on its own.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refreshMainViews reset the scroll position of every other main view at the
very top, before moveMainContextPairToTop runs its CopyContent. CopyContent
copies the previously-shown view's content into the now-visible one to avoid a
blank frame during the async re-render — but because the reset ran first, it
had already zeroed the origin of that soon-to-be-copied source view. The
placeholder therefore always appeared scrolled to the top, jumping away from
wherever the screen actually was, on every cross-pair transition.
Move the reset to after the copy. The end state is unchanged (each other main
view still ends at origin 0, and the destination always re-renders), but the
brief placeholder now stays at the source view's real scroll position until
the real content paints.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Several methods assigned v.ox and v.oy directly: SetOrigin, CopyContent,
the wrap/autoscroll branches in draw, FocusPoint, and
Scroll{Up,Down,Left,Right}. Funnelling them all through SetOriginX and
SetOriginY gives a single place to observe (or set a breakpoint on)
every change to a view's scroll position, which makes debugging scroll
behaviour much easier.
This means those call sites now also get the setters' `< 0` clamps, but
that is behaviour-preserving in every case: each assigned value is
already >= 0. calculateNewOrigin never returns a negative number;
CopyContent copies origins that are themselves always >= 0; and the draw
and scroll writes are all guarded (or fed only non-negative amounts) so
the result can't go below zero.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Records session 2's findings: the escape flicker was caused by the layout's
scroll-up-to-fill clamp running against partially-loaded async content (not the
"only a screenful loaded" mechanism session 1 guessed); the full origin-reset
chain (onNewKey / CopyContent / layout clamp) and how each is handled; the three
standalone bug fixes that landed; and the precise remaining task (apply the
saved origin at the pty task's first repaint, i.e. a cmd-task RenderWithScroll).
Also captures the reusable debug tooling that was stripped from the tree.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A task's read loop processes one LinesToRead request at a time. The initial
request has a large line count and no Then callback; if the content is shorter
than that, the loop hits EOF on the initial request and breaks out, abandoning
any further requests still sitting in the readLines channel. So a ReadToEnd
call that races a still-loading-but-shorter-than-its-initial-read view has its
Then silently dropped: it isn't fired immediately (the channel was non-nil at
call time) and it's never dequeued.
On EOF, drain the queued requests and fire their Then callbacks before
breaking out, since reaching EOF trivially satisfies any "read more" request.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The layout scrolls a view up if its origin is past the bottom of its
content, to avoid showing blank space (e.g. after a resize). But it measures
content height by the lines loaded so far, and command/pty tasks load
asynchronously. So when a view is re-rendered while scrolled down, the layout
would yank it to the top because only a fraction of the content has been read
yet, then leave it there once loading finished.
Track whether a command task is actively reading (set synchronously when the
task is created, so a layout pass in between sees it; cleared at EOF, but not
when stopped, since that means a newer task is taking over) and skip the
scroll-up clamp for such views. onEndOfInput already re-clamps once loading
completes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
HyperLinkInLine read v.lines/v.viewLines without holding writeMutex, so it
could race a concurrent re-render rebuilding the buffer. It also indexed
v.lines by viewLines[y].linesY after only checking y against len(viewLines);
since refreshViewLinesIfNeeded overwrites viewLines in place without
truncating, the tail can hold stale entries pointing past a shrunk v.lines,
giving an out-of-range panic while a shorter diff is still loading.
Take writeMutex (as the sibling view methods do) and bounds-check linesY
against len(v.lines), returning "no link" rather than panicking.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Instead of a user config that always shows a selection on focus, the
focused main view starts without a selection (matching master). Pressing
<space> shows a selection in the middle of the view (no scrolling);
pressing <esc> hides it again before falling through to exiting the view.
This was already possible, but only when a file was selected, and it woudln't
always land on the right line when a pager was used. Now it's also possible to
do this for directories, and it jumps to the right line.
At the moment this is a hack that relies on delta's hyperlinks, so it only works
on lines that have hyperlinks (added and context).
The implementation is very hacky for other reasons too (e.g. the addition of the
weirdly named ClickedViewRealLineIdx to OnFocusOpts).
Re-rendering a diff into a main view is asynchronous and lazy: the read
loop fills the view a screenful at a time and refreshes as it goes. When
debugging scroll-restore and flicker behaviour, the individual frames go by
too fast to see. Setting LAZYGIT_SLOW_RENDER=<milliseconds> sleeps that long
after each line is written, stretching the load out so the frames become
visible. It has no effect when unset, so it's safe to leave in.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
If the `conflict-marker-size` git attribute is used to set the marker
size to a non-default value (!= 7), lazygit's handling of conflicted
files was totally broken. Stopping at a commit with conflicts in a
rebase would show the `UU` files for a moment, and then, a few seconds
later, would stage all conflicted files and offer to continue the rebase
(with the conflicts baked into the resulting commits if you confirmed).
Even if you cancelled the continue prompt, it wasn't possible to use
git's conflict panel to resolve the conflicts; it would only show the
regular diff for those files, not its conflicts editor.
Fix this by querying the `conflict-marker-size` git attribute for all
conflicting files and use that to match the conflict markers.
Fixes#4367.
Git only writes the space after a marker when there is a label to write
after it, and the label can be empty: `git checkout -m` with the diff3
conflict style, for instance, has no name for the common ancestor, so it
writes a bare "|||||||" line.
Ask git for the attribute of every conflicted file whenever we load the
file status, so that we recognize the markers it actually wrote. Files
that are set up this way are precisely the ones whose regular content
tends to contain marker-looking lines, so matching a run of at least
seven characters instead is not an option: we'd take the file's own
content for markers and then never consider its conflicts resolved.
One `git check-attr` call covers all conflicted files at once; asking per
file would take seconds when hundreds of files are conflicted, and it
would hurt worst on Windows, where spawning a process is expensive.
Because the lookup rides along with the file status, it costs nothing
when there are no conflicts, and editing .gitattributes during a merge
takes effect on the next refresh.
When a file's conflict markers aren't seven characters long we don't
recognize them at all. Two things go wrong: we consider the file's
conflicts resolved, so we stage it and offer to continue the merge a
moment after stopping at it; and pressing enter on it shows its diff
instead of the merge conflicts view, leaving no way to resolve it in
lazygit.
Git doesn't always write conflict markers of seven characters: the
conflict-marker-size gitattribute overrides that per file, and it is set
for good reasons — for file types whose regular content tends to contain
marker-looking lines, such as documentation about merging, or test
scripts. We hard-code seven characters everywhere we look for markers,
so none of that works.
Prepare for honoring the attribute by threading the marker size through
everything that recognizes a marker, carried on the file model. Nothing
fills it in yet, so we still use git's default size of seven everywhere,
and matching is unchanged: a marker consists of exactly that many marker
characters, and all but the "=======" one are followed by a space and a
label.
This fixes a regression in 0.64.0: before that version, creating or
popping a stash would happen synchronously on the UI thread (including
the refresh), blocking the UI until everything changed, including the
panel focus. Blocking the UI was not nice of course, but at least the UI
update was clean. With 0.64.0 this changed to a background refresh, so
that the update to the two panels and the focus change all happened out
of sync, which looks rather ugly. Fix this by using Refresh's mechanism
to batch UI updates, and switch the panel focus in the Refresh's Then so
that it updates at the same time.
While we're at it, use a waiting status spinner for these operations;
they are usually fast when only few files are involved, but when
stashing a large number of files in a larger repo it can be noticeable,
and it looks ugly if the confirmation prompt stays on the screen while
it is running.
Creating and applying a stash both touch every changed file, so in a
large repo they can take long enough to be noticeable — and running them
on the UI thread meant the confirmation popup stayed on screen, frozen,
for the whole operation. Run them on a worker instead, with a spinner,
and keep blocking input for their duration so that the type-ahead
guarantee the refresh used to provide still holds.
Dropping stays on the UI thread: it only rewrites the stash reflog, so
it's fast no matter how big the stashes are.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Collapsing the range before kicking off the refresh paints the new
selection against the list as it was before the drop, so for a frame the
entries that were just dropped are still on screen (and, with
gui.shrinkSidePanelsToContent, the panel is still at its old size).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pushing the files context right after kicking off the refresh moves the
focus (and, with gui.shrinkSidePanelsToContent, resizes the panels) a
frame before the refreshed stash and files lists arrive. Doing it from
Then puts it in the same frame as the data it belongs to.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Stashing and popping change both the stash list and the files list.
With each scope updating the UI as soon as its own refresh is done, the
two panels visibly change at different times; with
gui.shrinkSidePanelsToContent that also means their sizes change at
different times than their contents.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Lazygit assumes a repo can be found again from its working directory: it
chdirs there and lets git rediscover the git dir from `<worktree>/.git`.
That holds for an ordinary repo and breaks for every setup where the git
dir lives somewhere else, which is where these bugs come from. Opening
such a repo worked at all only when `--git-dir` happened to leave
`GIT_DIR` in the environment for every command to inherit — which is
also why entering a submodule, which has to clear it, broke the way back
out.
Three reported problems:
- **`core.worktree` (#5895).** A repo whose work tree is elsewhere
panicked on startup with `fatal: not a git repository`: we chdir'd into
a work tree with no `.git` in it and every command after that was lost.
We now work out at startup whether git can find the repo from its work
tree, and when it can't we put `GIT_DIR`/`GIT_WORK_TREE` on every
command the repo's command builder produces — as well as in the process
environment, for subprocesses that don't come through the builder.
Nothing is set for the repos git can find on its own, which is nearly
all of them.
- **Escaping a submodule of a dotfile repo (#1118).** The repo-path
stack we push the superproject onto only held its path, and for a repo
opened with `--git-dir`/`--work-tree` the path leads nowhere. Escaping
failed with `not a git repository`, or, if some unrelated repo happened
to lie above the work tree, quietly switched to that one instead. The
stack now carries the environment as well, taken from the repo paths
rather than from the process env, so it also covers a repo whose
location lazygit worked out itself.
- **Opening a directory that holds a bare repo (#5469, #5681).** `git
rev-parse --show-toplevel` is fatal when there's no work tree, so we
never got an answer at all for a bare repo: `IsBareRepo()` could never
come out true, and lazygit either died with a stack trace or decided we
weren't in a repository. We now ask again without `--show-toplevel` when
the first query fails, and the existing "open most recent repo?" prompt
does its job.
Some related things that turned up on the way:
- **A submodule no longer looks like a linked worktree.** `git worktree
list` reports the main worktree as the common git dir with a trailing
`/.git` removed, which is not the working tree when the git dir doesn't
live inside it. Comparing that against the working tree path matched
nothing, so inside a submodule the status bar claimed we were in a
linked worktree named after the submodule, the worktrees panel listed it
as not current, and its branch got a "checked out elsewhere" marker.
Worktrees are now identified by their git dir, which names them
unambiguously.
- **Commands aimed at another repo no longer resolve against ours.**
With `GIT_DIR` set, `git -C mysub log -1` reports the *superproject's*
commit, silently. So opening lazygit with `--git-dir`/`--work-tree`
quietly broke resolving submodule conflicts, stashing and resetting a
submodule, and detaching another worktree.
- **Starting lazygit in a repo's `.git` dir opens the repo.** It used to
tell you that you were in a bare repo, which you weren't — the work tree
was one directory up. git's own convention is that a git dir called
`.git` belongs to the directory holding it, so we look there. (A linked
worktree's or a submodule's git dir isn't called `.git`, and nothing we
look at says where their work tree is, so those still get the prompt.)
- **`RepoPath()`** is documented to be the work tree when we're in the
main worktree, but was derived from the git dir's location, which is
only the same thing when the git dir is inside the work tree. This fixes
the repo name shown in the status panel for split setups.
Fixes#1118Fixes#5469Fixes#5681Fixes#5736Fixes#5895
Running lazygit in a .git dir got you told you were in a bare repo,
which you weren't: the worktree was sitting right there, one directory
up. git's own convention is that a git dir called .git belongs to the
directory holding it — that's how `git worktree list` names the main
worktree — so ask that directory, and if it is a worktree, open the repo
we were really being asked about.
The git dirs that aren't called .git keep the answer they had. A linked
worktree's and a submodule's do have a worktree, but nothing we look at
says where, so we would be guessing; a bare repo's has none to find.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entering a submodule clears GIT_DIR and GIT_WORK_TREE, as it must: they
say where the superproject is. But the stack we push the superproject
onto so that escape brings us back only held its path, and for a repo
opened with --git-dir/--work-tree the path leads nowhere — git can't
find a repo there. Escaping out of a submodule of a dotfile repo failed
with "not a git repository", or, if some unrelated repo happened to lie
above the work tree, quietly switched to that one instead.
Push the environment onto the stack along with the path, taken from the
repo paths rather than from the process env, so that it also covers a
repo we worked the location out for ourselves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
git finds a repo by looking for a .git in the directory a command runs
in. Lazygit runs its commands in the work tree, so that normally works —
but not when the git dir lives somewhere else entirely, which is what
core.worktree and --work-tree are for. Lazygit chdir'd into such a work
tree and then ran commands that couldn't see any repo from there, so
opening a repo with core.worktree set panicked on startup. It only
worked with --git-dir because that leaves GIT_DIR in the environment for
every command to inherit.
Work out at startup whether git can find the repo from its work tree,
and when it can't, put GIT_DIR and GIT_WORK_TREE on every command the
repo's builder produces. As with the working directory the builder pins
(527124d0e0), these also go into the process env — subprocesses don't
come through the builder — but the commands don't read them from there,
because the process env belongs to whichever repo we have switched to
since.
Working out whether git can find the repo means asking git, rather than
reading the .git file, whose contents can spell the same directory
differently than git does. The extra query is skipped for a repo whose
git dir is simply its .git directory, which is nearly all of them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
GIT_DIR and GIT_WORK_TREE tell git where our repo is, and every command
we run inherits them — including the ones we point at a submodule or
another worktree. git resolves those against our repo instead, and says
nothing about it: with GIT_DIR set, `git -C mysub log -1` reports the
superproject's commit. So opening lazygit with --git-dir/--work-tree
quietly broke resolving submodule conflicts, stashing and resetting a
submodule, and detaching another worktree; the worktree list came back
claiming every worktree shared our git dir.
Drop the two variables from the commands that address another repo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reset told git to change directory with -C while runInParentModule does
it by setting the command's working directory, but they were computing
the same directory for the same reason. Use the helper, so that there is
one place that knows what running in a nested submodule's parent means.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Its working directory resolves against the process rather than against
the repo the command builder pins commands to, which is only safe
because nothing but foreground commands come through here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
git makes `rev-parse --show-toplevel` fatal when there's no work tree,
so asking for it together with everything else meant we never got an
answer at all for a bare repo: GetRepoPaths returned an error, nobody
ever saw IsBareRepo() == true, and lazygit either died with a stack
trace or decided we weren't in a repository. That's what you got for
opening it in a directory holding a bare repo and a .git file pointing
at it, which is a normal way to keep a repo and its worktrees together.
Ask again without --show-toplevel when the first query fails: the other
queries work fine without a work tree, so if they now succeed we know
we're in a bare repo, and the existing prompt offering to open a recent
repo does its job. If they fail too we're not in a repo at all, and the
first error already says so.
--is-bare-repository is gone from the query: a work tree implies
core.bare is false, so it could only ever come back false there, and
what matters to us is whether there is a work tree to show, which is
what we now go by.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"does not yet support" reads as a promise that it will, but it's quite
likely that it never will.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
onNewRepo also runs when switching repos, and a failure there leaves us
in the repo we came from — with a nil GitCommand, which nothing else is
prepared for. Only assign once we have one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
RepoPath() is meant to be the same as WorktreePath() when we're in the
main worktree, but we derived it from the git dir's location instead.
That is only the same thing when the git dir lives inside the work tree.
With core.worktree, --work-tree, or a .git file pointing at a repo dir
that isn't called .git, it lands on a directory that isn't a worktree at
all, and the repo name we show follows it there.
A worktree that has the repo's common git dir to itself is the main
worktree, so use its path. That subsumes the submodule case, whose git
dir lives under the superproject's .git/modules but is still the
submodule's own common dir; --show-superproject-working-tree is now only
needed for a linked worktree of a submodule.
The existing bare repo test asserted a git output that can't occur (a
work tree and --is-bare-repository=true at once), but the rest of it is
the shape of a repo opened with --git-dir/--work-tree, where the new
repo path is the correct one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
When the work tree lives somewhere else entirely — set up with
core.worktree or --work-tree — we're still in the main worktree, so
RepoPath() should be the work tree, as its own doc comment says. Instead
we derive it from the git dir's location, which lands somewhere that
isn't a worktree at all, and the repo name follows it.
The ACTUAL lines are indented as they will be once the EXPECTED ones
replace them, rather than as gofumpt wants them while the comment
markers are still splitting the struct's alignment. That leaves this one
file not gofumpt-clean until the next commit, in exchange for a diff
there that shows only the lines that actually change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`git worktree list` reports the main worktree as the common git dir with
a trailing "/.git" removed, which equals the working tree only when the
git dir sits inside it. In a submodule, a bare repo, or a repo using
core.worktree it doesn't, so comparing the reported path against the
working tree path matches nothing: no worktree is recognized as current
or as main. Most visibly, inside a submodule lazygit claimed we were in
a linked worktree named after the submodule, and offered to remove that
"worktree".
Comparing git dirs identifies a worktree unambiguously, so use that.
A worktree whose directory is gone has no git dir to compare, and there
we still have nothing better than its path.
The submodule tests were asserting the linked-worktree suffix in the
status view; it is gone now, and the repo name still says which
submodule we're in.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A submodule's git dir doesn't live inside its working tree, and `git
worktree list` reports it by its git dir. Lazygit compares that against
the working tree path, so it recognizes neither the current nor the main
worktree, and the UI ends up claiming we're in a linked worktree.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The scenarios describe their repo by its paths but leave the git dirs
empty, which no repo has. Unused for now; the loader is about to want
them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Our minimum required git version is 2.32.0, which is the first version
that does support the GIT_CONFIG_GLOBAL env var, so we don't need the
~/.gitconfig dance any more.
Our minimum required git version is 2.32.0, which is the first version
that does support the GIT_CONFIG_GLOBAL env var, so we don't need the
~/.gitconfig dance any more.
### PR Description
Refresh the Nix flake inputs - see #5882.
Direct input revisions:
- `flake-parts`: `758cf7296b` → `427bf4bd94`
- `nixpkgs`: `c9b6fb7985` → `e72e4f2994`
- `treefmt-nix`: `5eda4ee812` → `d1187f8bc7`
- `systems`: unchanged at `da67096a3b`
- `flake-compat`: unchanged at `ff81ac966b`; its immutable URL metadata
was normalized by the lock refresh
The refresh also updates the transitive `nixpkgs-lib` and
`treefmt-nix/nixpkgs` inputs.
The newer nixpkgs revision deprecates `pkgs.nixfmt-rfc-style` because it
now aliases `pkgs.nixfmt`. Uses the current attribute directly,
preserving
formatter behavior and removing the two evaluation warnings.
### Testing
- `nix flake check`
- `nix develop --ignore-environment -c just format` with a writable
`GOCACHE`
- `nix develop --ignore-environment -c just build` with a writable
`GOCACHE`
- `nix develop --ignore-environment --keep HOME -c just unit-test` with
a writable `GOCACHE`
- `nix develop --ignore-environment --keep HOME -c just lint` with
managed proxy variables and task-local caches
- `nix-shell --run 'just --version'`
- `git diff --check`
### Please check if the PR fulfills these requirements
- [x] Cheatsheets are up-to-date — no integration-test or keybinding
changes
- [x] Code has been formatted
- [x] Tests have been added/updated — no test changes needed; unit suite
passes
- [x] Text is internationalised — no user-facing strings changed
- [x] UserConfig hot reload is unaffected — no UserConfig changes
- [x] Docs have been updated if necessary — no documentation changes
needed
- [x] You've read through your own file changes for silly mistakes etc
<!--
Be sure to name your PR with an imperative e.g. 'Add worktrees view',
and make sure the title
is suitable to be included as a bullet point in release notes (i.e.
phrased from a user's point
of view).
see https://github.com/jesseduffield/lazygit/releases/tag/v0.40.0 for
examples
-->
flake.lock pins the exact revision of everything the Nix build pulls in
(the package set it takes git, Go and the formatters from, plus a few
helper libraries), much like go.sum does for Go modules. Nothing
refreshes it automatically and no CI job exercises it, so the pins had
quietly drifted the better part of a year behind.
The flake-compat entry also grows a "?rev=...&revCount=..." suffix on
its download URL. That is not a version change: the server hosting the
archive advertises those two values as part of the canonical URL now,
and they only restate the rev and revCount that the same entry already
lists as separate fields. The archive fetched is byte-for-byte identical
either way.
The nixpkgs package set renamed the Nix code formatter: what used to be
called nixfmt-rfc-style is now simply nixfmt, and the old name lives on
only as an alias. Both names resolve to the identical program, so this
doesn't change how anything gets formatted.
The rename comes first because the dependency update in the next commit
makes that alias start printing a deprecation warning, so doing it in
this order means no commit in the history evaluates with one.
PipeCommands ran every command in its own goroutine, each doing
Start/read-stderr/Wait, with nothing ordering one goroutine's Start
against another's Wait. That ordering matters: StdoutPipe registers the
parent's read end in cmd.parentIOPipes, and Cmd.Wait closes those
descriptors when it returns. The next command's Stdin is that very
*os.File, and exec passes a user-supplied *os.File through untouched, so
Start hands the child whatever the fd happens to be at that moment. If
the producer finished and got reaped before the consumer's goroutine
reached Start, that fd was already closed, File.Fd() returned -1, and
the child was started with fd 0 closed -- reading nothing at all.
The only caller is the pre-2.35 fallback in SaveStagedChanges, which
pipes `git stash show -p` into `git apply -R`. Losing that race left git
apply with an empty patch, so it failed with "unrecognized input", the
following `git stash drop` never ran, and the user was left with a stray
stash entry. This turned up as a flaky stash/stash_staged on the git
2.32.0 CI job; the newer-git jobs take the `git stash push --staged`
path and never reach this code.
Starting every command up front removes the race, and collecting stderr
into buffers lets exec's own copying goroutines do the work. That also
fixes two lesser problems in the same function: finalErrors was appended
to from several goroutines without synchronization, and a failed Start
was only logged, so a pipeline that never ran reported success.
PipeCommands ran every command in its own goroutine, each doing
Start/read-stderr/Wait, with nothing ordering one goroutine's Start
against another's Wait. That ordering matters: StdoutPipe registers the
parent's read end in cmd.parentIOPipes, and Cmd.Wait closes those
descriptors when it returns. The next command's Stdin is that very
*os.File, and exec passes a user-supplied *os.File through untouched, so
Start hands the child whatever the fd happens to be at that moment. If
the producer finished and got reaped before the consumer's goroutine
reached Start, that fd was already closed, File.Fd() returned -1, and
the child was started with fd 0 closed -- reading nothing at all.
The only caller is the pre-2.35 fallback in SaveStagedChanges, which
pipes `git stash show -p` into `git apply -R`. Losing that race left
git apply with an empty patch, so it failed with "unrecognized input",
the following `git stash drop` never ran, and the user was left with a
stray stash entry. This turned up as a flaky stash/stash_staged on the
git 2.32.0 CI job; the newer-git jobs take the `git stash push --staged`
path and never reach this code.
Starting every command up front removes the race, and collecting stderr
into buffers lets exec's own copying goroutines do the work. That also
fixes two lesser problems in the same function: finalErrors was appended
to from several goroutines without synchronization, and a failed Start
was only logged, so a pipeline that never ran reported success.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entering or leaving filtering mode switched the screen mode and the
focused panel immediately, then reloaded the commit list in the
background. The result was an unfiltered list presented in the layout
that says "you are filtering", with nothing to say that anything was
still happening — and in a big repo that state can last seconds.
This is a regression in 0.64.0 (more specifically, from #5790): before
that we stopped blocking the UI thread on refreshes, the reload happened
before any of it, so the two always agreed; the price was a frozen UI
for the duration.
Do neither: reload on a worker, so the UI stays live, and hold back
everything the user can see of the change until the new lists are ready,
so they still land together in one frame. A waiting status says what is
going on in the meantime, and blocking input means the keys pressed
while it runs arrive after the change rather than acting on a list that
is about to be replaced.
The commit list a filtering mode change leaves behind has nothing to do
with the one that was showing, so the scroll position it inherits says
nothing about where the selection ended up, and the selection can land
anywhere off screen. PostRefreshUpdate only moves the cursor within the
existing scroll position, so ask for the scroll separately, the way the
commits refresh does when it moves the selection itself.
Exiting filtering mode looked like it worked, but only by accident: the
commits refresh recognizes the commit that was selected before it ran,
selects it again at its new index, and scrolls because the index moved.
That does nothing for the case where the commit is gone from the list, or
for entering filtering mode, where we select the first commit ourselves.
Entering or leaving filtering mode switched the screen mode and the
focused panel immediately, then reloaded the commit list in the
background. The result was an unfiltered list presented in the layout
that says "you are filtering", with nothing to say that anything was
still happening — and in a big repo that state can last seconds.
Before we stopped blocking the UI thread on refreshes, the reload
happened before any of it, so the two always agreed; the price was a
frozen UI for the duration.
Do neither: reload on a worker, so the UI stays live, and hold back
everything the user can see of the change until the new lists are ready,
so they still land together in one frame. A waiting status says what is
going on in the meantime, and blocking input means the keys pressed while
it runs arrive after the change rather than acting on a list that is
about to be replaced.
Setting a filter and clearing it are the same transition in opposite
directions: mutate the mode, bring the screen mode in line with it,
reload the views that depend on the filter, and put the selection
somewhere sensible in the reloaded commit list. They were implemented
twice, once in the filtering menu and once in ModeHelper, which is how
the two came to repaint the commit list in different ways.
Derive the screen mode and the panel switch from whether a filter is
active after the change, so both directions fall out of the same code,
and give ModeHelper the entry points for both. The filtering menu is
left with nothing but the menu.
Blocking keyboard input and hiding the working tree state mode are two
separate concerns; they were fused into one helper because every caller
so far wanted both. A caller that blocks input for something other than a
rebase would then hide the "Rebasing" indicator for the duration of its
operation, which has nothing to do with it.
Make it an explicit option instead, so blocking input on its own doesn't
imply anything about the modes on display.
The mode it suppresses is active for any working tree state, not just a
rebase: merging, cherry-picking and reverting show through the same
indicator. Name it after what it hides.
Whether a graph can be drawn was read from the filtering mode, while the
graph itself is drawn over the commit list in the model. Those two only
agree once the list has been reloaded for the new mode, and a filtering
mode change reloads the list in the background, so in between we can be
asked to draw a graph over a list the graph makes no sense for.
That is not just cosmetic. Commits in a filtered list are almost never
each other's parents, so no pipe ever terminates: the pipe set grows by
one per row and every continuing pipe rescans it, which is cubic in the
length of the list. Escaping out of filtering mode with a filtered list
of 13000 commits — as you get once the 300 commit limit has been lifted,
which happens for good as soon as the selection passes COMMIT_THRESHOLD
— wedges the UI thread for around twenty minutes.
Record whether the list was loaded with a filter, right where the list
itself is stored, and decide from that. The graph now also stays up while
the pre-change list is still on display, rather than vanishing a moment
before the list it belongs to.
This fixes spurious test failures caused by git's auto maintenance
running concurrently with a test's fixture setup; see the first commit's
message for details.
In addition, we change the test harness so that such a failure in a
setup method doesn't take down the whole test binary.
A failing setup step called Shell.fail, which panicked. Tests run as
parallel subtests, so that panic aborted the entire test binary: one bad
fixture cost us the results of all ~500 tests, and the failure was
reported as a stack trace rather than against the test that caused it.
Keep panicking to skip the remaining setup steps -- they would only
produce follow-on failures -- but recover in createFixture and return the
message as that test's error. All three clients already propagate an
error from a test, so they report it the way they report any other
failure.
Every `git commit` forks `git maintenance run --auto --quiet --detach`,
and git 2.54 changed what that runs from the `gc` task to the
"geometric" strategy. The geometric repack's auto condition passes its
threshold of 100 to too_many_loose_objects(), which estimates the loose
object count from the objects/17 fanout directory times 256, so the real
trigger is two objects in that one directory -- where the old gc task
needed 27. Fixture repos reach two easily: every CreateNCommits(n>=6)
repo already stores the blob for file06.txt there, so a single commit
object hashing into 17 (about 4% of fixtures with 10 commits, 15% with
40) tips it over, and from then on every commit in that repo forks a
detached `git repack -d`, which prunes loose objects while the next
fixture command -- or lazygit under test -- is still working in the same
repo.
That is where the CI panics during fixture setup come from:
panic: error running command: [git commit -m commit-10]
error: invalid object 100644 50d5612... for 'file09.txt'
error: Error building trees
The reported hashes are exactly the fixture blobs, so `git add` staged
them correctly; they were unlinked underneath the commit. Only the
"git latest" jobs saw this, since the pinned 2.32/2.38/2.44 jobs predate
the strategy change.
git's own test suite guards against the same thing by exporting
GIT_TEST_MAINT_AUTO_DETACH=false ("Ensure that tests cannot race with
background maintenance by default"). Turning maintenance off outright is
stronger: no test repo needs it, and it also spares us a forked git
process per commit. maintenance.auto has been honored since git 2.29, so
it covers every version in the CI matrix.
Measured on a 40-commit fixture: a background repack fired in 4 of 25
runs before, 0 of 25 after.
### Description
Add `just` to the default Nix development shell so contributors can use
the repository's `justfile` without installing `just` separately. Keep
GNU Make available as the existing fallback.
no `flake.lock` update required. `just` comes from the already-pinned
`nixpkgs` input, and no flake input changes.
Closes#5881.
### Testing
- `nix-instantiate --parse flake.nix`
- `nix flake check`
- `nix develop --ignore-environment -c just --version`
- `nix develop --ignore-environment -c go version`
- `nix develop --ignore-environment -c just format`
- `nix develop --ignore-environment -c just build`
- `nix develop --ignore-environment -c just lint`
- `nix develop --ignore-environment -c just unit-test`
- `nix-shell --run 'just --version'`
### Please check if the PR fulfills these requirements
* [x] Cheatsheets are up-to-date (run `go generate ./...`)
* [x] Code has been formatted (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting))
* [x] Tests have been added/updated (see
[here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
for the integration test guide) — no test-code change needed; validated
with the commands above
* [x] Text is internationalised (see
[here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation))
— no user-facing strings changed
* [x] If a new UserConfig entry was added, make sure it can be
hot-reloaded (see
[here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig))
— no UserConfig entry added
* [x] Docs have been updated if necessary — no documentation change
needed
* [x] You've read through your own file changes for silly mistakes etc
Fixes#5879: with an external diff command configured, quickly
navigating between commits on Windows accumulates orphaned
`git.exe`/`difft.exe`/`conhost.exe` processes that keep computing their
diffs in the background and persist after lazygit exits.
Stopping a pty task on Windows relied on `ClosePseudoConsole`, whose
`CTRL_CLOSE_EVENT` only reaches clients *attached to the pseudoconsole
at that moment*. Attachment happens asynchronously during child startup,
so a task stopped within the first few milliseconds of its life — which
is exactly what rapid navigation produces — misses the event entirely
and survives, together with its whole process tree (git for Windows
spawns through a two-level git.exe wrapper, so a single task has several
attach windows).
The fix puts the child into a job object before it executes its first
instruction (created suspended → assigned → resumed), so every
descendant is in the job from the start. The pty teardown still closes
the pseudoconsole first and gives clients that received the close event
a moment (500ms) to exit through their own handlers — git's cleans up
lock files — and then terminates whatever is left in the job.
`JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` doubles as a safety net: if lazygit
exits without running the teardown, the OS closes the job handle and
reaps the tree.
Validated with a harness that mimics lazygit's stop path with randomized
0–120ms stop delays: before the fix, 3 of 30 process trees survived as
permanent orphans per run; after it, three runs of 30 all came back with
zero orphans, with the job kill catching exactly the children that
missed the close event (6 of 90) while the rest still exited gracefully
through the event as before.
The pty teardown in Close runs on a background goroutine that doesn't
get to finish when lazygit is quitting: the process exits milliseconds
after the view buffer managers are closed. The job objects still cover
the clients -- KILL_ON_JOB_CLOSE reaps them when the process's handles
are rundown at exit -- but nothing reaps the conhost, so on Windows
builds whose conhost fails to run down on its own, quitting leaks one
conhost per live pty.
This is not a rare timing window: a diff longer than what has been
read keeps its git process (and thus its pty and conhost) running for
the entire time it is displayed, so that scrolling can read more.
Quitting while looking at a long diff is therefore the common case,
and with an external differ configured it leaks a conhost on affected
builds on almost every quit.
Fix this by having the gui's shutdown path wait synchronously for the
in-flight teardowns after closing the view buffer managers. A quit
signal makes the teardowns skip the conhost rundown wait -- the
conhost serves nothing once its clients are dead, and the exit must
not stall for its sake -- so the wait normally completes in
milliseconds, keeping quit as fast as before; a 2-second cap protects
the exit path even if a teardown wedges.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A pseudoconsole's conhost.exe is spawned by CreatePseudoConsole as a
child of lazygit, so it is not part of the job object that the pty
teardown kills. That is normally fine: a healthy conhost runs itself
down once the reference handle is closed and its clients are gone. But
conhost builds before the ConPTY overhaul that shipped with Windows 11
24H2 (confirmed on 23H2, build 22631) fail to complete the rundown
when a client attached after the close event was delivered and was
then killed -- the fate of exactly the clients the job kill exists for
-- and such a conhost lingers forever with no clients, at a rate of
about one per five fast commit navigations. These builds remain
widespread: all of Windows 10 (whose ESU tail runs into 2028, and
whose hardware often cannot run Windows 11 at all) plus pre-24H2
Windows 11 fleets.
Since Windows offers no way to obtain the conhost's pid or handle from
the HPCON, identify it by diffing lazygit's direct conhost children
around the CreatePseudoConsole call, serialized by a mutex so that two
concurrently starting ptys can't confuse each other's diff, and open a
handle immediately so that pid reuse is harmless. The teardown then
gives conhost a second to exit on its own before terminating it; on
healthy builds the wait succeeds and the reap never fires. If the
conhost can't be identified unambiguously, we simply don't reap, which
is no worse than before.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stopping a pty task on Windows relies on ClosePseudoConsole, which
delivers CTRL_CLOSE_EVENT to the console's attached clients. But only
to those attached at that moment: when the user flicks quickly through
commits, a task is often stopped within the first few milliseconds of
its life, before the child has attached to the pseudoconsole. Such a
child misses the event and survives, running the entire diff to
completion in the background (spawning one external differ per changed
file) and keeping its conhost.exe alive; rapid navigation accumulates
these git/difft/conhost trees, and they outlive lazygit. Grandchildren
are affected too: git for Windows runs commands through a two-level
git.exe wrapper, so a single task has several attach windows, and a
grandchild spawned while the console is going down is orphaned even
when its parent got the event and exited.
Fix this by putting the child into a job object before it runs its
first instruction (created suspended, assigned, then resumed), so that
every descendant is in the job from the start; the teardown in Close
terminates the job right after initiating the pseudoconsole close.
There is no point in a grace period between the two: the close event
is not a graceful signal -- git and the common diff tools leave it to
the default handler, which calls ExitProcess at an arbitrary point --
so clients that received it are already dying, and the kill exists for
those that missed it. Killing at an arbitrary point cannot leak a
stale index.lock, because pty-rendered commands no longer take that
lock (see withPtyGitConfig in pkg/gui/pty.go).
The pseudoconsole close runs on its own goroutine because the kill
must not wait for it: on builds where ClosePseudoConsole blocks until
the console host exits (pre-24H2), the host keeps running as long as a
surviving client does, and that client only goes away through the job
kill; sequencing the kill after a blocking close would deadlock in
exactly the case the kill exists for.
KILL_ON_JOB_CLOSE doubles as a safety net: if lazygit exits without
running the teardown, the OS closes the job handle and reaps the tree.
In a harness that mimicked the stop path with randomized 0-120ms stop
delays, 3 of 30 process trees survived as orphans before this change;
none survive with it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The task stop path terminates the still-running command by pulling its
*os.Process out of the Cmd interface and applying one global strategy
(TerminateProcessGracefully) to it. That shape can't accommodate the
upcoming fix for orphaned process trees on Windows: there, stopping a
pty task requires terminating the entire process tree via a job object
whose handle lives with the pty, not with the process. And the two Cmd
implementations genuinely need different strategies anyway: a
process-group kill (the likely future fix for #5675 on Unix) is only
safe for pty children, which run as session leaders, while plain
commands share lazygit's own process group.
So let each Cmd implementation decide how to terminate itself, and drop
GetProcess, which had no other callers. No change in behavior.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
At the end of a diff against the worktree, git re-reads and refreshes
the index and writes it back if it found stale stat information
(diff.autoRefreshIndex, on by default). It holds index.lock for the
whole refresh; GIT_OPTIONAL_LOCKS does not cover this lock, and the
window scales with the size of the repository (~150ms for a 6k-file
repository with a warm stat cache).
On Windows, a pty task that is stopped because the user moved on
terminates its git process at an arbitrary point: tearing down the
pseudoconsole delivers CTRL_CLOSE_EVENT, which git leaves to the default
handler, which simply calls ExitProcess. If that lands inside the
refresh, a stale index.lock is left behind and the next git command
chokes on it. This is the same problem that 98801da106 fixed by no
longer killing git processes; the ConPTY support added in 0.63
reintroduced it through the close event.
Disable the automatic refresh for pty-rendered commands. They can afford
it: the refresh only persists refreshed stat information, and lazygit's
foreground git status refreshes -- which never run in a pty and are
never killed -- already write that back on every user action and on
terminal focus-in. The cost is that while the on-disk stat cache is
stale, an external differ is invoked even for files whose stat
information changed but whose content didn't, showing them as empty
diffs; this heals with the next foreground refresh, which also
re-renders the view.
Unix keeps the refresh: a stopped pty child gets SIGTERM there, and
git's signal handlers remove its lock files, so the lock window is
harmless. The rawGit renderer keeps it too: its tasks don't run in a pty
and are never killed on Windows -- they either run to completion or die
on a broken pipe mid-output, before the refresh begins.
At the end of a diff against the worktree, git re-reads and refreshes
the index and writes it back if it found stale stat information
(diff.autoRefreshIndex, on by default). It holds index.lock for the
whole refresh; GIT_OPTIONAL_LOCKS does not cover this lock, and the
window scales with the size of the repository (~150ms for a 6k-file
repository with a warm stat cache).
On Windows, a pty task that is stopped because the user moved on
terminates its git process at an arbitrary point: tearing down the
pseudoconsole delivers CTRL_CLOSE_EVENT, which git leaves to the
default handler, which simply calls ExitProcess. If that lands inside
the refresh, a stale index.lock is left behind and the next git
command chokes on it. This is the same problem that 98801da106 fixed
by no longer killing git processes; the ConPTY support added in 0.63
reintroduced it through the close event.
Disable the automatic refresh for pty-rendered commands. They can
afford it: the refresh only persists refreshed stat information, and
lazygit's foreground git status refreshes -- which never run in a pty
and are never killed -- already write that back on every user action
and on terminal focus-in. The cost is that while the on-disk stat
cache is stale, an external differ is invoked even for files whose
stat information changed but whose content didn't, showing them as
empty diffs; this heals with the next foreground refresh, which also
re-renders the view.
Unix keeps the refresh: a stopped pty child gets SIGTERM there, and
git's signal handlers remove its lock files, so the lock window is
harmless. The rawGit renderer keeps it too: its tasks don't run in a
pty and are never killed on Windows -- they either run to completion
or die on a broken pipe mid-output, before the refresh begins.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
We already excluded the most commonly used commit trailers from being
auto-wrapped when typing or rewording a commit message, but this was
limited to two hard-coded ones ("Signed-off-by:" and "Co-authored-by:").
Extend this mechanism to use a heuristic to prevent more trailers from
wrapping; the heuristic kicks in for any "Key: Value" line if Key
contains a dash, or the value looks like a URL (so that it also catches
things like "Bug: https://my-bug-tracker/345").
Fixes https://github.com/jesseduffield/lazygit/discussions/5869.
We already excluded the most commonly used commit trailers from being
auto-wrapped when typing or rewording a commit message, but this was
limited to two hard-coded ones ("Signed-off-by:" and "Co-authored-by:").
Extend this mechanism to use a heuristic to prevent more trailers from
wrapping; the heuristic kicks in for any "Key: Value" line if Key
contains a dash, or the value looks like a URL (so that it also catches
things like "Bug: https://my-bug-tracker/345").
To avoid mistaking a "Key: Value"-looking line in the message body for a
trailer, only apply the heuristic in the last paragraph of the message,
i.e. the block of lines at the end that is separated from the body by a
blank line. Each line there is judged on its own, so a line that isn't
recognized as a trailer still wraps without affecting the real trailers
next to it.
Having the cache in state.yml causes this file to be rewritten every
60s, which is annoying if you have a lazygit running in the background
somewhere without even realizing it, and it keeps overwriting the
foreground lazygit's newer command shell history and recent repos list
with its stale data. State.yml should only contain things that change in
response to user actions, not periodically.
Having the cache in state.yml causes this file to be rewritten every
60s, which is annoying if you have a lazygit running in the background
somewhere without even realizing it, and it keeps overwriting the
foreground lazygit's newer command shell history and recent repos list
with its stale data. State.yml should only contain things that change in
response to user actions, not periodically.
For a while now we have been showing Github icons in the branches panel
to indicate which branches have an associated pull request. This PR
changes this so that open PRs (which used to show a green Github icon)
now show a colored symbol indicating the combined state of the Github
checks (if any are configured for this repo):
<img width="1046" height="418" alt="image"
src="https://github.com/user-attachments/assets/3e9074a6-0ae4-423a-90d9-306863daa46c"
/>
This lets you see at a glance which of your PRs are ready to merge.
(Note however that this feature knows nothing about which of the checks
are _required_ for merging, so a branch showing a failure icon may still
be ready to merge if the failing checks are not required ones.)
It may be a little confusing for new users to see two green checkmarks
for a branch, one to the left of the name and one to the right; they
mean very different things. Still, seeing the Github checks state in the
branches list is so useful that I think it's a good compromise;
hopefully users will learn quickly enough what all the icons mean.
When selecting a branch that has a pull request, the main view already
showed some information about the PR (its title, pr number, and the
state (open/closed/merged). This PR adds an aggregated pull request
checks field to this, indicating the overall checks state
(passed/failed/pending/...). It has a hyperlink taking you directly to
the checks tab on Github.
<img width="1538" height="246" alt="CleanShot 2026-07-30 at 12 32 25@2x"
src="https://github.com/user-attachments/assets/68aa24da-d78a-45d7-8812-db8b509bbbc8"
/>
The update frequency of the checks badge is tied to the auto-fetch
cadence for now (once every minute by default); you can fetch manually
to force an update, and some user actions (including the focus-in
refresh) update the PR states too, but anything more fancy (e.g.
fetching just the checks state of the selected branch) would have been
more work, and I want to see if this is good enough.
In the branches list we show the checks icon (✓, ✗ etc) instead of the
gihub icon for branches that are open and have a state. It is a little
confusing, because the ✓ in front of the name means something very
different than the ✓ after it, but the checks status is just too useful
to see in the list.
In the main view we show it as a compact status before the PR title,
with a hyperlink that takes you directly to the checks tab in Github.
The branch controller should decide which pull request to show, not how its
header is styled and linked. Move the existing formatter and state badge next
to the branch presentation helpers so subsequent header changes stay in one
layer.
GitHub exposes a combined status for the head commit without requiring
individual check contexts. Include that rollup in the existing request
and startup cache so every consumer sees the same state without making a
second network request.
The fetch currently combines transport, JSON decoding, and model
conversion, which makes response changes difficult to verify without
exercising the network. Put the deterministic work behind a small parser
so later payload changes can be covered with raw GraphQL fixtures.
For a long time lazygit has used the term "custom pager" to refer to
what's really a "diff renderer". A pager is a program that allows you to
view output page by page (hence the name), e.g. less; lazygit's custom
diff renderers are not pagers. It used the term only because the feature
is implemented using git's `GIT_PAGER` env var, but that's an
implementation detail.
Rename the 'git.pagers' config to 'git.diffRenderers', and restructure
its elements while we're at it to make things clearer:
- Add a 'type' field to explicitly specify which type of diff renderer
it is (the two fundamentally different ones are 'stdinFilter' and
'extDiff').
- Add a third type, 'rawGit', which has an 'args' field that makes it
easy to use 'git diff --color-words' as a custom renderer
- Unify the old 'pager' and 'externalDiffCommand' fields to a single
'command' field for both types
Existing config files are migrated automatically.
For a long time lazygit has used the term "custom pager" to refer to
what's really a "diff renderer". A pager is a program that allows you to
view output page by page (hence the name), e.g. less; lazygit's custom
diff renderers are not pagers. It used the term only because the feature
is implemented using git's GIT_PAGER env var, but that's an
implementation detail.
Rename the 'git.pagers' config to 'git.diffRenderers', and restructure
its elements while we're at it to make things clearer:
- Add a 'type' field to explicitly specify which type of diff renderer
it is (the two fundamentally different ones are 'stdinFilter' and
'extDiff').
- Add a third type, 'rawGit', which has an 'args' field that makes it
easy to use 'git --color-words' as a custom renderer
- Unify the old 'pager' and 'externalDiffCommand' fields to a single
'command' field for both types
Existing config files are migrated automatically.
It doesn't matter for what this test used to test (that nothing changes
if the 'pagers' array exists), but it will be relevant once we further
migrate the 'pagers' array from there.
We don't want callers to need any additional logic, so pass in the
translation set so that the function can decide what static text to
return. This allows us to get rid of the CurrentPagerUsesGitConfigDiff
method which is in the way for the refactoring we're about to do.
It has served its purpose when config migration was initially
implemented, but nobody runs this benchmark nowadays, and the example
config has run out of date with reality. Some PRs have still updated it
when they made changes to the config, but others didn't, and it's
unclear what the rules are; so let's just remove it.
Pressing the left button on the current selection now starts a drag that
moves the selected commits, both in the normal commits view and for
todos during an interactive rebase. A press on an unselected row still
creates a range selection.
While dragging, a "drop here" indicator shows where the commits will be
inserted.
Moving commits with the mouse is useful for the case that you want to
move them a longer distance, because that's slow when doing it one by
one with the keyboard, and also you don't want to resolve conflicts at
every step. The standard workaround for that is to enter an interactive
rebase first and then continue it afterwards, but that's a bit
cumbersome; dragging solves that nicely.
Closes#5842.
Moving commits runs a rebase, which can take a while. Instead of
letting the drop indicator vanish the moment the button is released,
keep it in place and turn it into a "moving commits here" spinner once
the move takes longer than a short grace period, so that quick moves
stay free of flicker. The indicator is cleared when the post-move
refresh lands.
While a commit drag is in progress, escape now aborts it: the drag
state and the drop indicator are discarded and the mouse capture is
released, so nothing happens when the button is eventually released.
Otherwise escape keeps its normal meaning.
Reuse the drag autoscroller for commit drags. Scrolling stops once the
insertion point reaches the end of the allowed range in the scroll
direction, so during a rebase the view doesn't keep scrolling once the
last insertion position among the todos has been reached.
Pressing the left button on the current selection now starts a drag
that moves the selected commits, both in the normal commits view and
for todos during an interactive rebase. A press anywhere else falls
through to the usual click handling, so dragging from an unselected
line still creates a range selection, and releasing without having
moved collapses the selection to the pressed commit like a plain click
would.
While dragging, the insertion point follows the pointer: rows below
the dragged block insert after the pointed-at commit, rows above it
insert before it, and during a rebase the destination is limited to
the contiguous block of movable todos around the selection. gocui
moves the view cursor along with the pointer, so each drag event moves
it back to keep the original selection highlighted.
The move happens on release. The model may have been refreshed during
the drag, so the dragged commits are located again by their identity
(hash, subject, todo action); if they no longer form a unique
contiguous block, the drop is ignored rather than guessing.
Render the insertion point of a commit drag as a non-model item in the
commits list. It must be inserted at the right position relative to
the section headers, because the list renderer assumes non-model items
are ordered by their model index.
Not used yet, we'll hook it up to the drag gesture in the next commit.
Let the todo-move primitives take a distance instead of hardcoding a
single row, by iterating the one-row move in memory. Dropping a commit
several rows away thus rewrites the todo file once and, outside of an
interactive rebase, runs a single rebase rather than one per row.
Merge the up/down variants of the move commands into one
direction-parameterized implementation. Dragging commits is about to
need moves over arbitrary distances, which we don't want to build twice.
Like in the staging view, drag with the mouse in a list view to create a
non-sticky range selection (including auto-scrolling as the mouse
reaches the view edge).
Give the list views the same edge autoscroll during drag selection
that the staging view already has; the new mouse-release binding stops
the autoscroll when the drag ends.
Dragging with the left button held now extends the selection from the
pressed line, exactly like moving with shift+up/down does. We use the
non-sticky flavor so that the range collapses on the next plain cursor
movement, again matching the keyboard behavior.
The binding is only registered for contexts that support range selection
in the first place; dragging in other lists continues to do nothing.
The staging view (and custom patch building view) already has the
capability to create a range selection by dragging with the mouse;
however, a longer range couldn't be selected this way because the
dragging stopped at the view edge. Add auto-scrolling so that the view
scrolls as the mouse reaches the view edge; slowly on the innermost edge
row, faster on the outermost row and beyond. Scrolling starts after a
short delay so that a drag merely passing near the edge doesn't scroll.
When the pointer reaches the edge of the view during a drag (or leaves
the view entirely, which mouse capture makes possible), keep scrolling
and extending the selection: slowly on the innermost edge row, faster
on the outermost row, and very fast beyond. Scrolling starts after a
short delay so that a drag merely passing near the edge doesn't scroll.
When the view loses focus mid-drag (e.g. because a popup appeared),
cancel the autoscroll and the mouse capture.
Add press/move/release primitives next to the existing Click. The test
driver remembers the last reported position so a release doesn't have
to repeat the coordinates, and RepeatMouseMove lets a test verify that
a held-button motion event within the same cell has no effect.
Route all mouse events to the view that was under the pointer when the
left button was pressed, until the button is released. Previously each
event went to whatever view was under the pointer at the time, so a
drag that left the view's bounds started acting on neighboring views.
Since events can now carry positions outside the view, clamp the view
cursor to the view's bounds in that case (handlers still receive the
unclamped position), and require an actual click for tab activation so
that a captured drag crossing the tab row doesn't switch tabs.
Releasing a mouse button was delivered as a plain mouse-move (hover)
event: the release processing resets dragState to NOT_DRAGGING, after
which the event fell into the NOT_DRAGGING branch. Views therefore had
no way of telling that a drag gesture ended, which the upcoming
drag-based features (range selection, commit reordering) need.
Deliver the release as a real mouse event with the MouseRelease key
and normalize its modifiers to ModNone, so release bindings also match
modified drags. Make recordClickInfo ignore it: a release is the end of
a click, not a click of its own, and must not break double-click
detection.
Add a test pinning down that a press/release/press sequence at the
same position is detected as a double click. An upcoming commit starts
delivering the release as a real mouse event to the click-recording
code, which must not mistake it for a click of its own.
When the left button is pressed and the pointer then moves, the event
that made the MAYBE_DRAGGING -> DRAGGING transition fell through the
switch without being assigned a key or modifier, so the first cell of
every drag arrived at handlers as a MouseRelease event without the
motion modifier and was effectively lost. Give it the same
MouseLeft/ModMotion identity as all subsequent drag events.
Held-button motion events that stay within the pressed cell carry no
information at all; swallow them instead of letting them through as
further release-shaped events (which used to clobber the double-click
state when the pointer jittered within a cell between two clicks).
I'm not a skilled UI designer, so I suspect there may be even better
options, but it's definitely already better than the raw ASCII "---" we
had before.
Put the line only at the beginning because it looks bad if the line
after the label is misaligned when labels don't have the same width
(e.g. "Remote" vs. "Local" in the divergence view).
When selecting sibling directories sharing a common prefix, trying to
add them to a custom patch or discarding them from the commit would only
add one, not both. We had the same bug in the files panel and fixed it
in #3599, but forgot to make the equivalent change in the commit files
panel.
Fixes#5866.
The root item's path is ".", and the path of a file at top level is
"./file". When using GetPath, this gives us "." and "file",
respectively, and isDescendentOfSelectedCommitFileNodes would return
false for these.
Working with the internal paths (i.e. without stripping the leading
"./") fixes this.
There is no known breakage that is caused by this, that's why I'm not
adding an integration test that demonstrates a bug.
Equivalent to the change that was made to isDescendentOfSelectedNodes in
files_controller.go in 302b621b68.
It never changes inside this function, so there's no need to recompute
it with every loop iteration.
Equivalent to the change that was made to isDescendentOfSelectedNodes in
files_controller.go in d0c6e27fee.
When lazygit is running in a VS Code window that doesn't have the focus
(e.g. a split tab) and you click in the commits panel to focus it and
select a commit at the same time, it would briefly select the commit you
clicked but then flash back to the commit that was previously selected.
This PR fixes that so that the clicked commit stays selected.
This was only a problem with VS Code; in Zed's builtin terminal it
worked, apparently because it first dispatches the click and then the
focus-in event. Ghostty and iTerm2 were not affected because they don't
dispatch clicks in inactive windows or tabs at all.
Labelling as ignore-for-release because it fixes a regression that was
introduced since the last release.
This fixes the problem described in the previous commit; we no longer
capture the selection at the start of the refresh. There's no reason to
do that (we don't do it for branches either). It is enough to capture
the selection in the final bounce, before we assign the new model slice.
When clicking in the commits view of lazygit running in an unfocused VS
Code window, VS Code first sends us the focus-in event and then the
mouse-click. The focus-in refresh captures the selection when it starts,
then we handle the mouse click and you briefly see the clicked row
getting selected, but then the selection flashes back to the original
row as the refresh restores it when done.
CreatePseudoConsole and ResizePseudoConsole reject zero dimensions with
E_INVALIDARG, but we legitimately request them: the pty is sized after
the main view, and that view is zero-sized while hidden, e.g. in
full-screen mode with a side panel focused. Entering that mode while a
custom pager is configured therefore made StartPty fail (degrading to
unpaged output now that the fallback works), and resizing a live pty
from onResize would fail layout. The Unix pty accepts zero sizes, so
the clamp lives in the Windows implementation only.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CreatePseudoConsole rejects zero dimensions with E_INVALIDARG, so
starting a pty sized after a hidden (and thus zero-sized) view fails.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
NewCmdTask feeds the reader returned by its start func into a
bufio.Scanner, and Scanner.Scan panics with a nil pointer dereference
when that reader is nil. Two start funcs could produce one:
- newPtyTask's fallback for a failed StartPty returned a literal nil
reader, alongside an ExecCmd that was never started, so the intended
"fall back to a plain cmd task" never worked. This crashed lazygit on
Windows when using a custom pager with the main view zero-sized, e.g.
after pressing + twice to enter full-screen mode with a side panel
focused: ConPTY rejects zero dimensions, making StartPty fail.
- startCmdWithPipe returned nil when the pipe couldn't be created,
which the Unix pty fallback path can trigger, since a failed pty
start can leave the tty assigned to the command's stdout.
Make startCmdWithPipe never return a nil reader: when the pipe can't be
created, don't start the command at all and return an empty reader so
the task shuts down cleanly with the error in the log. Then route
newPtyTask's fallback through it, so a StartPty failure degrades to
running the command without a pty: the pager is lost, but the command's
output still renders.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
NewCmdTask feeds the reader returned by its start func straight into a
bufio.Scanner, whose Scan panics on a nil reader with a nil pointer
dereference. startCmdWithPipe returns exactly that when the pipe cannot
be created.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The fallback path in newPtyTask (taken when StartPty fails) needs the
same start-the-command-with-a-pipe logic that newCmdTask uses, so pull
it out into a helper that both can share. No behavior change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A refresh from the UI thread returns immediately and applies its model
and view updates as queued UI-thread callbacks. A key pressed before
those have run is handled against the stale, pre-refresh state. For most
keys that's harmless, but some handlers turn that state into git
commands: pressing space twice in quick succession in the staging panel
builds the second patch from the already-applied diff and fails with
'patch does not apply', because the refresh after the first press is
what moves the selection to the next stageable hunk.
Notably, this is not just a regression of the recent change that made
UI-thread refreshes non-blocking; the window was merely much narrower
before. A blocking refresh parked the UI thread while the scopes'
bounces were queued, and the event loop drains pending keyboard input
with priority over queued user events, so a key pressed during the
blocked window still beat the queued state updates. The guarantee that
the next keypress sees post-refresh state had already ended when the
scopes' state updates moved from worker-side mutex-guarded writes to
UI-thread bounces.
Fix it with the input-blocking mechanism we already use for commit
surgery, exposed as a new RefreshBlockingInput entry point: it begins
blocking events synchronously in the calling handler, and ends the block
from a callback that the finishing step queues behind the refresh's own
updates. Keys pressed while the refresh is in flight are buffered and
replayed, in order, against the fully refreshed state; since a replayed
key's handler re-enters this same path, a burst of keypresses applies
sequentially, each one seeing the previous one's refresh. Unlike the old
blocking refreshes, this doesn't freeze the UI thread: rendering,
spinners, resizing, and mouse scrolling keep working while input is
withheld.
Blocking input is opt-in per call site rather than the default for all
UI-thread refreshes, because most refreshes (the focus-in and startup
refreshes, say) don't produce state that the next keypress depends on,
and blocking on them would delay typing for no reason. It should also be
limited to quick, narrow-scoped refreshes: a full refresh, or any scope
that pulls in COMMITS, can take very long in large repos and should
usually not hold up input.
Use the new function for the staging panel's stage/discard/edit-hunk
refreshes, for moving rebase todos, and for stash operations.
Labelling as ignore-for-release because it fixes regressions introduced
after the last release.
On startup we don't want to block input during the initial refresh (it
should be possible to press, say, `4` to jump to the commits panel right
after startup without a delay), and we also want panels to show their
contents as soon as possible; it doesn't matter so much that it's not in
sync, we go from empty to populated here. However, when switching repos
it can be confusing that some panels that are slow to update still show
the old repo's data while others already show the new one's data, so
update the UI only when everything is ready, and also block input to
prevent accidentally trying to act on the old, stale data.
Popping or dropping a stash shifts the indices of the entries below it,
and renaming re-creates the stash at the top, shifting all the others.
The stash model is only rebuilt by the refresh, which finishes in the
background, so acting on the next entry in quick succession — pressing
the key, confirming the popup, and pressing again right away — reads the
stale pre-operation indices and targets the wrong stash. Note that the
confirmation popup is no protection here: the race starts when the
confirm handler runs, and the next keypress can easily beat the refresh.
Use RefreshBlockingInput so a quick follow-up keypress is buffered and
replayed once the refreshed stash list is in place.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Moving a todo rewrites the todo file and advances the selection
synchronously, but the commits model is only rebuilt by the refresh. A
second press arriving before that grabs the swapped-with todo from the
stale model at the advanced index and moves it back, so holding the key
to move a todo several slots misbehaved. Use RefreshBlockingInput so the
second press is buffered and replayed once the moved todo list is in
place.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Moving a todo up or down rewrites the todo file and advances the
selection synchronously, but the commits model is only rebuilt by the
refresh, which finishes in the background. A second keypress arriving
before that reads the pre-move model at the advanced selection index —
that's the todo the first move swapped with, so the second press moves
that one back instead of moving the selected todo further. Two rapid
presses (e.g. from holding the key down) thus amount to a net no-op.
The two presses also spawn two racing refreshes whose model updates can
land in either order, so the todo list can even end up disagreeing with
the todo file. That's why the test continues the rebase and asserts the
resulting commit order instead of the displayed list: the rebase replays
the file, which is deterministic.
The test documents this currently broken behavior; the fix comes next.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A refresh from the UI thread returns immediately and applies its model
and view updates as queued UI-thread callbacks. A key pressed before
those have run is handled against the stale, pre-refresh state. For most
keys that's harmless, but some handlers turn that state into git
commands: pressing space twice in quick succession in the staging panel
builds the second patch from the already-applied diff and fails with
'patch does not apply', because the refresh after the first press is
what moves the selection to the next stageable hunk.
Notably, this is not just a regression of the recent change that made
UI-thread refreshes non-blocking; the window was merely much narrower
before. A blocking refresh parked the UI thread while the scopes'
bounces were queued, and the event loop drains pending keyboard input
with priority over queued user events, so a key pressed during the
blocked window still beat the queued state updates. The guarantee that
the next keypress sees post-refresh state had already ended when the
scopes' state updates moved from worker-side mutex-guarded writes to
UI-thread bounces.
Fix it with the input-blocking mechanism we already use for commit
surgery, exposed as a new RefreshBlockingInput entry point: it begins
blocking events synchronously in the calling handler, and ends the
block from a callback that the finishing step queues behind the
refresh's own updates. Keys pressed while the refresh is in flight are
buffered and replayed, in order, against the fully refreshed state;
since a replayed key's handler re-enters this same path, a burst of
keypresses applies sequentially, each one seeing the previous one's
refresh. Unlike the old blocking refreshes, this doesn't freeze the UI
thread: rendering, spinners, resizing, and mouse scrolling keep working
while input is withheld.
Blocking input is opt-in per call site rather than the default for all
UI-thread refreshes, because most refreshes (the focus-in and startup
refreshes, say) don't produce state that the next keypress depends on,
and blocking on them would delay typing for no reason. It should also be
limited to quick, narrow-scoped refreshes: a full refresh, or any scope
that pulls in COMMITS, can take very long in large repos and should
usually not hold up input.
The staging panel's stage/discard/edit-hunk refreshes use it now.
Pressing space twice in quick succession in the staging panel is supposed
to stage two hunks: the refresh triggered by the first press rebuilds the
panel's diff and moves the selection to the next stageable hunk, and the
second press stages that.
Since we made UI-thread refreshes non-blocking, the second press is
handled as soon as it arrives, while that refresh is still in flight. It
then reads the stale pre-refresh diff, builds the first hunk's patch
again, and git apply fails with 'patch does not apply' because those
lines are already in the index.
The test documents this currently broken behavior; the fix comes next.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The test driver waits for lazygit to become idle after every keypress, so
tests could never exercise what happens when a key arrives while the
previous key's processing is still in flight — for example while the
refresh triggered by the previous key hasn't updated the model yet. Real
users type faster than that all the time.
PressRapidly injects all its keys back to back and waits for idle only
once at the end, so the second and later keys are queued before the first
one's processing has finished. The next commit uses this to demonstrate a
bug in exactly that scenario.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A few comments still reasoned in terms of SYNC vs ASYNC refreshes, a
distinction that no longer exists: sync vs async is now derived from the
calling thread. Restate them in terms of the current mechanisms
(RefreshFromWorker blocking its worker, model updates being enqueued on
the UI thread) without changing any behavior.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Checking out a remote branch that has no local counterpart creates the
local branch, refreshes, and then checks it out. The refresh exists so
that CheckoutRef finds the new branch in the model and attaches an inline
status to the branch item instead of showing a global waiting status. But
since UI-thread refreshes stopped blocking, the checkout started before
the refreshed branches had landed in the model, so the lookup failed and
we always got the waiting status. Run the checkout from the refresh's
Then, which is queued behind the model update.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Make the background fetch and the refresh that runs after it more safe
against racing with a concurrent foreground repo switch (i.e. switching
worktrees, repos, or submodules). This fixes a bunch of different
problems; see the individual commit messages for details.
gui.git, gui.helpers and gui.State are all replaced on a repo switch,
which runs on the UI thread. The background fetch and the external-
change poller read them from their own goroutines, racing the
reassignment. This race can't show up in the integration suite, which
doesn't enable the background routines, so no -race run will ever flag
it; it can only bite real users who switch repos while a background
fetch or poll is in flight.
Capture the objects a routine iteration needs in a single blocking
UI-thread hop before using them, the same pattern the refresh's input
capture uses. For the fetch this has two welcome side effects: the
fetch, the post-fetch refresh's generation baseline, and the recorded
fetch time now all refer to the same repo (the old comment documented
the timestamp's mismatch as a known, unguarded race), and the git
instance the fetch runs through is pinned to that repo's directory, so
a switch mid-fetch can no longer direct in-flight work at the new repo.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PostFetchRefresh's refresh is the only background refresh carrying a
Then callback, and Then callbacks are not generation-guarded: when the
background fetch's refresh crossed a repo switch, the callback still
ran — in the new repo — and auto-forwarded the new repo's branches
because the old repo's fetch had completed. That was harmless in
practice (the update-ref call compares against the expected old value,
and it only does what the next fetch's auto-forward would do anyway),
but mutating refs in a repo whose fetch never happened is not an action
the user took. Skip the auto-forward when the repo generation changed
since the fetch started.
The generation is captured by the fetch's callers before the fetch
runs, not by PostFetchRefresh itself: the background fetch doesn't
block repo switching and is a network call, so by the time
PostFetchRefresh runs a switch may already have happened — a capture
there (or the one the refresh itself takes) would compare against the
new repo's generation and let the auto-forward through. For the manual
fetch the capture point makes no difference, since a foreground
operation blocks repo switching for its entire duration.
This deliberately guards only this call site rather than making Then
callbacks generation-guarded in general: a Then is an arbitrary
callback, and whether it is safe to skip on a repo switch is a decision
for the author of the call site.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The cached git config runs its `git config` reads through raw
exec.Command calls, outside the pinned git command builder, so they
followed the process working directory. A cache miss on a stale
instance — one still in use by a refresh that crossed a repo switch —
would therefore read the new repo's local config while computing data
for the old one. Give the cache a directory, set once by NewGitCommand
right after it determines the repo paths (the object is created fresh
for every repo switch, so no cross-repo cache invalidation is needed),
and run every config command there.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The refresh workers read a few files at paths relative to the process
working directory: the submodule config read of .gitmodules, the files
refresh's check for conflict markers, and the submodule stash's
existence check. Git commands are pinned to the repo their instance was
created for, but these Go file reads still followed the cwd, so a
background refresh crossing a repo switch would read the new repo's
files while computing data for the old one. Join them with the worktree
root of the instance they belong to. (Most git-state file reads —
working tree state, rebase todos, bisect info — already resolve
against RepoPaths and need no change.)
This also fixes the submodule stash's existence check for nested
submodules: it stat'ed submodule.Path, which is relative to the parent
module, against the repo root — now it uses the submodule's full path,
matching the stash command right below it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The refreshes on focus-in, right after a repo switch, and after
returning from a subprocess are full foreground refreshes, so their
tasks kept Busy() true for as long as the slowest scope took — and any
switch attempt in that window was refused with the "can't switch"
toast. The focus-in one is particularly annoying: focusing lazygit is
often precisely what the user does in order to switch repos, and right
after regaining focus is when a refresh takes longest.
Blocking the switch bought nothing there. The refusal exists for user
operations, whose follow-up work (e.g. a Then callback reading the
model) isn't covered by the switch-safety guards; but these refreshes
merely reload state, and a refresh by itself is now switch-safe: its
git commands run against the repo it was started for, and the
generation guard drops its updates when the repo changed.
We can't just mark them Background, because that flag also decides
whether the files refresh lets git take optional locks to persist its
refreshed stat cache — worth doing for an attended refresh, and the
focus-in refresh (typically running right after external changes) is
the case that profits most. So split the two meanings: a new
DontBlockRepoSwitch option dispatches the refresh's tasks as background
tasks (excluded from Busy()) while keeping the attended optional-locks
behavior. Combining it with Then panics, since Then is not
generation-guarded.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An error returned from a gocui worker is shown to the user in an error
popup. For the branch loader's behind-counts worker that used to be the
"no such ref" popup when a background refresh crossed a repo switch:
the old repo's main branch didn't exist in the new repo. The previous
commits fix that scenario properly — the command now runs against the
repo the refresh was started for — but a stale worker can still fail
legitimately, most plausibly because that repo was deleted after
switching away from it (e.g. removing a worktree). Its results are
dropped anyway, so log the error instead of alarming the user about a
repo they already left.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A background refresh's model writes are dropped by the generation guard
when the repo is switched mid-flight, but its git commands kept running
— and because the refresh read the live git instance at each step, any
command issued after the switch ran against the new repo. Now that git
commands are pinned to the directory of the instance they were built
from, capture the instance once when the refresh starts and run every
scope's git work through it, so a switch-crossing refresh keeps
addressing the repo it was started for.
The instance is captured together with the repo generation, on the UI
thread (where repo switches run), so the pair can't straddle a switch:
an old instance paired with the new generation would compute data from
the old repo and write it into the new repo's model unguarded.
This also removes the refresh workers' unsynchronized reads of the live
instance pointer, which raced its reassignment on the UI thread when a
background refresh crossed a repo switch (foreground refreshes can't
cross one: they keep Busy() true, which refuses the switch).
Two reads keyed app-state by the live instance's repo path on a worker
and now use the captured instance, fixing which repo they file under
when crossing a switch: the pull-request cache, and the "user dismissed
the base-remote prompt" flag. The base-remote menu's handlers keep
reading the live instance: a switch dismisses any open popup, so they
can't run against the wrong repo (and the OnPress body runs under a
foreground task, which blocks switching anyway).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Lazygit changes the process working directory when switching repos, but
work that is still in flight for the previous repo can keep spawning
git commands after the switch — most notably a background refresh. Its
model writes are already dropped by the repo generation guard, but its
git commands would now run against the new repo. That is wasted work at
best; at worst it surfaces spurious error popups (the behind-base-
branch computation failing with "no such ref" when the old repo's main
branch doesn't exist in the new one) and pollutes caches belonging to
the old repo's reusable state (e.g. MainBranches' existing-branches
cache), which the user sees when switching back.
Give the git command builder the directory of the repo it was created
for, and pin every command it produces to that directory. The pinned
directory and the process cwd are identical until a switch happens
(NewGitCommand chdirs to the worktree path right before creating the
builder), so nothing changes in the steady state; the pin only takes
effect for commands built through a previous repo's GitCommand instance
after a switch, which now keep addressing the repo they were built for.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When the user picks a base remote in the "select remote repository"
prompt, we called setGithubPullRequests directly, bypassing the refresh
machinery — which meant hand-rolling the refresh env that call needs
(with a comment explaining why), and fetching against the branches
captured when the prompt was created. Issue a PULL_REQUESTS-scoped
refresh instead: it re-reads branches and remotes (both fast even in
large repos), fetches against those fresh values, and gets the refresh
machinery's guarantees without any special-casing. The config write is
re-read by the refresh from git config, so it is guaranteed to be
picked up.
The waiting status now covers the config write and the branches/remotes
reload, while the GitHub request itself continues as a background task
— which is how every other pull-request fetch behaves.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deleting a nested submodule (and updating its URL) chdir'd the whole
process into the parent module, ran its git commands there, and chdir'd
back. Only those commands need to run there, and a process-wide chdir
leaks the parent module's directory into any command another goroutine
spawns during that window (e.g. a background refresh's). Set the
directory on the commands themselves instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
To spot slow or anomalous tests across CI runs, record each test's run
duration when LAZYGIT_TEST_TIMING is set (to a file path);
run_integration_tests.sh prints them at the end, sorted by slowest
first. CI sets it for all integration jobs.
The harness appends to a file rather than writing to stdout/stderr
because `go test` captures those and only surfaces them with -v, which
would drown the signal in every test's verbose logs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The watchdog only log.Fatal'd with a message, so a hung test told us
that it timed out but not where it was stuck -- useless for diagnosing
an intermittent deadlock under the race detector. Dump all goroutine
stacks to stderr first (the harness surfaces this process's stderr on
failure), turning a bare timeout into an actionable stack trace.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add one extra integration-tests job that runs the whole suite under the
race detector. A `race` matrix dimension (default false) plus an include
entry adds a single git-latest job with LAZYGIT_RACE_DETECTOR set; races
live in lazygit's own Go code rather than in git, so one git version is
enough, and using latest skips the git-build steps.
The race job skips coverage collection: it's redundant with the non-race
latest job and would only slow the -race build down further.
Go's default 10-minute timeout was enough for running integration tests
normally (both locally and on CI), but with race detection turned on
they can take much longer to run. Increase the timeout unconditionally
to 30 minutes; we don't bother making a distinction between race vs.
normal, because a longer timeout doesn't hurt (I can't recall having hit
the global timeout ever; and we still have the per-test watchdog that
kills an individual test after 40s).
The integration test watchdog fails a test if its recording takes longer
than 40 seconds. Under the race detector everything runs several times
slower, so legitimately slow tests (e.g. a conflicting interactive
rebase) blow that budget and fail even though nothing is actually stuck.
Key the timeout off a build-tag constant: the `race` tag is set
automatically when the binary is built with -race, so a race build gets
a 5x-longer budget while a normal build is unchanged, and the two can't
drift apart the way a runtime flag would. The base 40s stays in one
place; only the multiplier varies by build.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When lazygit exits but leaves behind a subprocess that inherited its
stderr pipe and has detached from the pty, cmd.Wait() blocks in
awaitGoroutines waiting for that pipe to reach EOF -- which never
happens while the straggler is alive. With no WaitDelay set, that wait
is unbounded, so a single leaked process hangs the whole test binary
until the 10-minute global timeout fires and panics. Worse, the timeout
discards whatever lazygit wrote to stderr before exiting (a panic, a
-race report), which is exactly the output needed to diagnose the
failure.
This surfaces under -race, where lazygit runs slow enough to widen the
window for a spawned command to still be alive when lazygit quits, and
it's a blocker for enabling the race detector on CI.
Bound the wait with cmd.WaitDelay so Wait force-closes the pipe and
returns ErrWaitDelay instead of hanging, surface the captured stderr as
the error (falling back to the wait error when nothing was printed), and
kill the child's process group on failure so a straggler can't linger
into a later test or pile up across a run.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When pressing ctrl+z to suspend lazygit, and then `fg` to bring it to
the foreground again, sometimes it wouldn't come to the foreground,
stalling in the background with one core using 100% CPU.
Fixes#5309.
The previous two commits stop gocui from flushing while suspended, but
that guard cannot be fully airtight from gocui's side: it is a
check-then-act on the suspended flag, so a flush racing the suspend
itself (e.g. from a spinner goroutine) could still reach the screen
just as it disengages, and tcell's disengageFinish mutates the cell
buffer without holding the screen lock. Upstream now closes this at
the source (gdamore/tcell#1139): draw() returns immediately on a
disengaged screen, and the draw scan loop can no longer stall on the
width-0 cells that a released cell buffer reports (#5309).
The delta over the previously pinned snapshot is these two fixes, a
CSI R input decode fix, a wasm packaging chore, and dependency bumps.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Until now the repaint after fg was accidental: it only happened because
the suspend keybinding handler still had a flush pending on the UI
thread, and only if that flush happened to run after the SIGCONT
handler had re-engaged the screen. Now that flushes are skipped while
suspended, losing that race would leave the screen blank until the next
input event arrives, so schedule a redraw explicitly (#5309).
When suspending with ctrl+z, the suspend keybinding handler disengages
the screen and then sends SIGSTOP to the process group, so the UI
thread freezes at the return from kill(2) with the handler's follow-up
flush still pending. When fg continues the process, that pending flush
races the SIGCONT handler's Resume. If the flush wins, Show() draws
against the disengaged screen, whose cell buffer tcell has released to
0x0 while its width/height still hold the old size; drawCell() then
reports width 0 for the out-of-range cell, the draw loop's
'x += width - 1' never advances, and the UI thread spins forever while
holding the tcell screen lock. Resume in turn blocks forever on that
lock, so the screen never re-engages and no input is ever read again:
the hard stall of #5309, only recoverable by killing the process.
Guard both flush paths with the suspended flag. For the flag to
guarantee that the screen is engaged whenever it is false, Resume must
clear it only after re-engaging (it used to clear it before); Suspend
already sets it before disengaging. This also covers the pre-existing
unsynchronized suspended check in draw(), which is subsumed by the
guards and can go.
The regression test cannot use the demonstrate-then-fix pattern: on
unfixed code the flush goroutine spins holding the screen lock, which
deadlocks any subsequent screen call including the test cleanup's
Close().
When lazygit is suspended with ctrl+z and brought back with fg, nothing
deliberately triggers a redraw. The screen only repaints because the UI
thread happens to have a flush pending from the suspend keybinding, and
that flush races the SIGCONT handler's resume; when it loses in the
right way, the terminal shows a blank screen until the next input event
arrives (#5309).
Grep-based navigation needs manual filtering for the many colliding
method names in this codebase, while gopls answers reference and
implementation questions type-aware and exactly. Scope the guidance to
the symbol tools and keep grep for textual searches: gopls' own MCP
instructions prescribe running vulncheck at session start and
go_file_context after every file read, which costs more than it helps
here. The server is registered per user and machine, so sessions
without it must just fall back to grep rather than try to set it up.
The command log is supposed to show only commands initiated by the user;
these are commands that we run to get information for rendering, so they
pollute the log and are confusing.
2026-07-19 19:19:47 +02:00
373 changed files with 25541 additions and 3738 deletions
@@ -30,5 +30,6 @@ There are other forms of contributions to a project besides source code that are
- File feature requests for new functionality that you want to see in lazygit. I have a lot of ideas for future improvement myself, but I have also implemented a lot of feature ideas that weren't mine, and I'm grateful for those ideas. (Of course, there are also lots of feature requests that I don't implement, so don't be disappointed if I don't jump on yours.)
- Help make other people's bug reports reproducible. Sometimes people report bugs that they have only seen once, and in such a case it can be helpful to come up with reproducible scenarios.
- Help complete or improve the translation into other languages; join https://crowdin.com/project/lazygit for that.
- Run a master build! This is probably the most valuable way to help me. Test the latest master not just by occasionally trying it, but by actually using it for your daily work; report any issues that you find. This will help prevent having to release hotfix updates for regressions that are only noticed by users updating to a new release.
Importantly, if you file issues (whether bug reports or feature requests), stay around to answer questions and discuss your issue. There are few things that I find more annoying than spending time on responding to someone's issue (sometimes even making a PR that addresses it), and to then never hear from the OP again. So please set up your Github notifications so that you see when there's activity on your issue, and continue to participate.
Custom diff renderers are useful for showing a better rendering of a diff than git's builtin raw diff, and using one is strongly recommended (I personally prefer delta myself, but that's a matter of personal preference). There are three types of diff renderers that lazygit supports:
- **stdin filters**, e.g. [delta](#delta) and [diff-so-fancy](#diff-so-fancy). They take git's raw output as stdin and produce something nicer on stdout, and they are hooked up using git's GIT_PAGER mechanism. (These used to be called "custom pagers" in earlier lazygit versions.)
- **external diff programs**, e.g. difftastic; these are called using git's `--ext-diff` flag, and they take over diff generation from git completely rather than post-processing git's output.
- **git's raw output using custom arguments**; mainly useful for `--color-words` (or `--word-diff` if you are color blind).
Diff renderers are configured with the `diffRenderers` array in the `git` section of lazygit's config file; it is an array because you can have multiple entries that you can cycle through with the `|` key. This can be useful if you usually prefer a particular diff renderer, but want to use a different one for certain kinds of diffs.
Fields that are shared by all renderer types:
- **type** The type of diff renderer; choices are `stdinFilter`, `extDiff`, or `rawGit`. `stdinFilter` is the default, because it's the most common one; so you can omit this if you use delta.
- **name** A name that is shown in the status bar toast when cycling renderers; defaults to the first word of the renderer command, but can be useful e.g. to distinguish "delta" from "delta side-by-side" if you have entries for both.
Fields only for `stdinFilter`:
- **command** The command line to use for `GIT_PAGER`.
- **colorArg** whether you want the `--color=always` arg in your `git diff` command. Some diff renderers want it set to `always`, others want it set to `never`. The default is `always`, since that's what most renderers need.
Fields only for `extDiff`:
- **command** The command line to use for the `diff.external` git config. If left empty, it uses the global value of git's `diff.external` config; this can be useful if you also want to use it for diffs on the command line, and it also has the advantage that you can configure it per file type in `.gitattributes`; see https://git-scm.com/docs/gitattributes#_defining_an_external_diff_driver.
You can include the `{{diffContext}}` template variable to pass lazygit's current diff context size (the value controlled by the `{`/`}` keybindings) to the diff tool.
Fields only for `rawGit`:
- **args** The additional arguments to use in the `git diff` or `git show` call (e.g. `--color-words`)
A cool feature of delta is --hyperlinks, which renders clickable links for the line numbers in the left margin, and lazygit supports these. To use them, set the `command:` field to `delta --dark --paging=never --line-numbers --hyperlinks --hyperlinks-file-link-format="lazygit-edit://{path}:{line}"`; this allows you to click on an underlined line number in the diff to jump right to that same line in your editor.
Note that delta's `--navigate` option doesn't work in lazygit, for technical reasons.
## Diff-so-fancy
```yaml
git:
diffRenderers:
- command:diff-so-fancy
```

## ydiff
```yaml
gui:
sidePanelWidth:0.2# gives you more space to show things side-by-side
Lazygit supports custom pagers, [configured](/docs/Config.md) in the config.yml file (which can be opened by pressing `e` in the Status panel).
Multiple pagers are supported; you can cycle through them with the `|` key. This can be useful if you usually prefer a particular pager, but want to use a different one for certain kinds of diffs.
Pagers are configured with the `pagers` array in the git section; here's an example for a multi-pager setup (use an empty object `{}` for the default builtin diff display that doesn't use a pager):
The `colorArg` key is for whether you want the `--color=always` arg in your `git diff` command. Some pagers want it set to `always`, others want it set to `never`. The default is `always`, since that's what most pagers need.
## Delta:
```yaml
git:
pagers:
- pager:delta --dark --paging=never
```

A cool feature of delta is --hyperlinks, which renders clickable links for the line numbers in the left margin, and lazygit supports these. To use them, set the `pager:` config to `delta --dark --paging=never --line-numbers --hyperlinks --hyperlinks-file-link-format="lazygit-edit://{path}:{line}"`; this allows you to click on an underlined line number in the diff to jump right to that same line in your editor.
Note that delta's `--navigate` option doesn't work in lazygit, for technical reasons.
## Diff-so-fancy
```yaml
git:
pagers:
- pager:diff-so-fancy
```

## ydiff
```yaml
gui:
sidePanelWidth:0.2# gives you more space to show things side-by-side
Be careful with this one, I think the homebrew and pip versions are behind master. I needed to directly download the ydiff script to get the no-pager functionality working.
## Using external diff commands
Some diff tools can't work as a simple pager like the ones above do, because they need access to the entire diff, so just post-processing git's diff is not enough for them. The most notable example is probably [difftastic](https://difftastic.wilfred.me.uk).
These can be used in lazygit by using the `externalDiffCommand` config; in the case of difftastic, that could be
```yaml
git:
pagers:
- externalDiffCommand:difft --color=always
```
The `colorArg` option is not used in this case.
You can add whatever extra arguments you prefer for your difftool; for instance
This can also be used for normal git diffs with custom parameters, such as `--color-words` or `--word-diff` which some people find useful. To do that, save a script like this to, say, `~/bin/color-words.sh`:
Instead of setting this command in lazygit's `externalDiffCommand` config, you can also tell lazygit to use the external diff command that is configured in git itself (`diff.external`), by using
```yaml
git:
pagers:
- useExternalDiffGitConfig:true
```
This can be useful if you also want to use it for diffs on the command line, and it also has the advantage that you can configure it per file type in `.gitattributes`; see https://git-scm.com/docs/gitattributes#_defining_an_external_diff_driver.
`pager`, `externalDiffCommand`, and `useExternalDiffGitConfig` are alternative ways of producing the diff, so a pager entry may use at most one of them.
@@ -22,8 +22,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` R `` | Refresh | Refresh the git state (i.e. run `git status`, `git branch`, etc in background to update the contents of panels). This does not run `git fetch`. |
@@ -22,8 +22,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` R `` | 새로고침 | Refresh the git state (i.e. run `git status`, `git branch`, etc in background to update the contents of panels). This does not run `git fetch`. |
| `` + `` | 다음 스크린 모드 (normal/half/fullscreen) | |
| `` _ `` | 이전 스크린 모드 | |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
| `` \| `` | Cycle diff renderers | Choose the next renderer in the list of configured diff renderers. |
| `` \ `` | Cycle diff renderers (reverse) | Choose the previous renderer in the list of configured diff renderers. |
| `` <esc> `` | 취소 | |
| `` ? `` | 매뉴 열기 | |
| `` <ctrl+s> `` | View filter-by-path options | View options for filtering the commit log, so that only commits matching the filter are shown. |
@@ -85,6 +85,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|-----|--------|-------------|
| `` <tab> `` | 패널 전환 | Switch to other view (staged/unstaged changes). |
| `` <esc> `` | Exit back to side panel | |
| `` <space> `` | Staged 전환 | 선택한 행을 staged / unstaged |
| `` d `` | 변경을 삭제 (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. |
| `` <ctrl+o> `` | 선택한 텍스트를 클립보드에 복사 | |
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
| `` v `` | 드래그 선택 전환 | |
| `` <left>, h `` | 이전 hunk를 선택 | |
| `` <right>, l `` | 다음 hunk를 선택 | |
| `` N `` | Go to previous file | |
| `` n `` | Go to next file | |
| `` / `` | 검색 시작 | |
## Stash
@@ -163,6 +172,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <mouse wheel up> (fn+down) `` | 위로 스크롤 | |
| `` <tab> `` | 패널 전환 | Switch to other view (staged/unstaged changes). |
| `` <esc> `` | Exit back to side panel | |
| `` <space> `` | Staged 전환 | 선택한 행을 staged / unstaged |
| `` d `` | 변경을 삭제 (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. |
| `` <ctrl+o> `` | 선택한 텍스트를 클립보드에 복사 | |
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
| `` P `` | Push | Push de huidige branch naar de bijbehorende upstream-branch. Als er geen upstream is geconfigureerd wordt er gevraagd om een upstream-branch te configureren. |
| `` p `` | Pull | Pull wijzigingen van de remote voor de huidige branch. Als er geen upstream is geconfigureerd wordt er gevraagd om een upstream-branch te configureren. |
| `` ) `` | Increase rename similarity threshold | Increase the similarity threshold for a deletion and addition pair to be treated as a rename.<br><br>The default can be changed in the config file with the key 'git.renameSimilarityThreshold'. |
| `` ( `` | Decrease rename similarity threshold | Decrease the similarity threshold for a deletion and addition pair to be treated as a rename.<br><br>The default can be changed in the config file with the key 'git.renameSimilarityThreshold'. |
| `` } `` | Increase diff context size | Increase the amount of the context shown around changes in the diff view.<br><br>The default can be changed in the config file with the key 'git.diffContextSize'. |
| `` { `` | Decrease diff context size | Decrease the amount of the context shown around changes in the diff view.<br><br>The default can be changed in the config file with the key 'git.diffContextSize'. |
| `` : `` | Execute shellcommand | Bring up a prompt where you can enter a shell command to execute. |
| `` : `` | Voer shellcommando uit | Bring up a prompt where you can enter a shell command to execute. |
| `` m `` | Bekijk merge/rebase opties | Toon abort/continue/skip opties voor huidige merge/rebase. |
| `` R `` | Verversen | Refresh the git state (i.e. run `git status`, `git branch`, etc in background to update the contents of panels). This does not run `git fetch`. |
| `` + `` | Volgende scherm modus (normaal/half/groot) | |
| `` _ `` | Vorige scherm modus | |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
| `` \| `` | Cycle diff renderers | Choose the next renderer in the list of configured diff renderers. |
| `` \ `` | Cycle diff renderers (reverse) | Choose the previous renderer in the list of configured diff renderers. |
| `` <esc> `` | Annuleren | |
| `` ? `` | Open menu | |
| `` <ctrl+s> `` | Bekijk scoping opties | View options for filtering the commit log, so that only commits matching the filter are shown. |
| `` W, <ctrl+e> `` | Open diff menu | View options relating to diffing two refs e.g. diffing against selected ref, entering ref to diff against, and reversing the diff direction. |
| `` q, <ctrl+c> `` | Afsluiten | |
| `` <ctrl+z> `` | Pauzeer de applicatie | |
| `` <ctrl+w> `` | Toggle whitespace | Toggle whether or not whitespace changes are shown in the diff view.<br><br>The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. |
| `` <ctrl+w> `` | Witruimte weergeven in-/uitschakelen | Toggle whether or not whitespace changes are shown in the diff view.<br><br>The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. |
| `` <alt+shift+c> `` | Verander config bestand | Open bestand in externe editor. |
| `` z `` | Ongedaan maken (via reflog) (experimenteel) | The reflog will be used to determine what git command to run to undo the last git command. This does not include changes to the working tree; only commits are taken into consideration. |
| `` Z `` | Redo (via reflog) (experimenteel) | The reflog will be used to determine what git command to run to redo the last git command. This does not include changes to the working tree; only commits are taken into consideration. |
| `` Z `` | Redo (via reflog) (experimenteel) | Het reflog wordt gebruikt om te bepalen welk git commando moet worden gebruikt om het laatste git commando te herhalen. Wijzigingen aan de working tree worden niet meegenomen, alleen command's zijn kandidaten. |
## Lijstpaneel navigatie
@@ -161,25 +161,25 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` s `` | Squash | Squash the selected commit into the commit below it. The selected commit's message will be appended to the commit below it. |
| `` f `` | Fixup | Meld the selected commit into the commit below it. Similar to squash, but the selected commit's message will be discarded. |
| `` c `` | Set fixup message | Set the message option for the fixup commit. The -C option means to use this commit's message instead of the target commit's message. |
| `` r `` | Hernoem commit | Reword the selected commit's message. |
| `` r `` | Hernoem commit | Herschrijf de commit message van de geselecteerde commit. |
| `` R `` | Hernoem commit met editor | |
| `` d `` | Verwijder commit | Drop the selected commit. This will remove the commit from the branch via a rebase. If the commit makes changes that later commits depend on, you may need to resolve merge conflicts. |
| `` i `` | Start interactive rebase | Start an interactive rebase for the commits on your branch. This will include all commits from the HEAD commit down to the first merge commit or main branch commit.<br>If you would instead like to start an interactive rebase from the selected commit, press `e`. |
| `` i `` | Start interactieve rebase | Start an interactive rebase for the commits on your branch. This will include all commits from the HEAD commit down to the first merge commit or main branch commit.<br>If you would instead like to start an interactive rebase from the selected commit, press `e`. |
| `` p `` | Pick | Kies commit (wanneer midden in rebase) |
| `` <ctrl+k>, <alt+up> `` | Verplaats commit 1 naar boven | |
| `` V `` | Plak commits (cherry-pick) | |
| `` B `` | Mark as base commit for rebase | Select a base commit for the next rebase. When you rebase onto a branch, only commits above the base commit will be brought across. This uses the `git rebase --onto` command. |
| `` B `` | Markeer als basiscommit voor rebase | Selecteer een basiscommit voor de volgende rebase. Als je rebased op een branch worden alleen commits boven de basiscommit meegenomen. Hiervoor wordt het `git rebase --onto` commando gebruikt. |
| `` A `` | Amend | Wijzig commit met staged veranderingen |
| `` a `` | Amend commit attribute | Set/Reset commit author or set co-author. |
| `` t `` | Revert | Create a revert commit for the selected commit, which applies the selected commit's changes in reverse. |
| `` T `` | Tag commit | Create a new tag pointing at the selected commit. You'll be prompted to enter a tag name and optional description. |
| `` <ctrl+l> `` | View log options | View options for commit log e.g. changing sort order, hiding the git graph, showing the whole git graph. |
| `` t `` | Revert | Maak een revert commit voor de geselecteerde commit, die de wijzigingen in deze commit terugdraait. |
| `` T `` | Tag commit | Maak een nieuwe tag die naar de geselecteerde commit wijst. Je wordt gevraagd om een tag naam en optionele omschrijving. |
| `` <ctrl+l> `` | Log opties weergeven | View options for commit log e.g. changing sort order, hiding the git graph, showing the whole git graph. |
| `` G `` | Open pull request in browser | |
| `` <space> `` | Uitchecken | Checkout the selected commit as a detached HEAD. |
| `` <space> `` | Uitchecken | Check de geselecteerde branch uit als een detached HEAD. |
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | Open commit in browser | |
| `` n `` | Creëer nieuwe branch van commit | |
@@ -232,6 +232,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` d `` | Verwijdert change (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. |
| `` <ctrl+o> `` | Copy selected text to clipboard | |
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
| `` v `` | Toggle drag selecteer | |
| `` <left>, h `` | Selecteer de vorige hunk | |
| `` <right>, l `` | Selecteer de volgende hunk | |
| `` N `` | Go to previous file | |
| `` n `` | Go to next file | |
| `` / `` | Start met zoeken | |
## Patch bouwen
@@ -241,7 +250,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <left>, h `` | Selecteer de vorige hunk | |
| `` <right>, l `` | Selecteer de volgende hunk | |
| `` v `` | Toggle drag selecteer | |
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
| `` a `` | Wissel tussen hunk selectie aan of uit | Wissel tussen regel-voor-regel of hunk selectie modus. |
| `` <ctrl+o> `` | Copy selected text to clipboard | |
| `` o `` | Open bestand | Open bestand in standaardapplicatie. |
| `` e `` | Verander bestand | Open bestand in externe editor. |
@@ -255,7 +264,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` d `` | Verwijdert change (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. |
| `` <ctrl+o> `` | Copy selected text to clipboard | |
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
| `` v `` | Toggle drag selecteer | |
| `` <left>, h `` | Selecteer de vorige hunk | |
| `` <right>, l `` | Selecteer de volgende hunk | |
| `` N `` | Go to previous file | |
| `` n `` | Go to next file | |
| `` / `` | Start met zoeken | |
## Staging
@@ -316,7 +334,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <left>, h `` | Selecteer de vorige hunk | |
| `` <right>, l `` | Selecteer de volgende hunk | |
| `` v `` | Toggle drag selecteer | |
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
| `` a `` | Wissel tussen hunk selectie aan of uit | Wissel tussen regel-voor-regel of hunk selectie modus. |
| `` <ctrl+o> `` | Copy selected text to clipboard | |
| `` d `` | Verwijdert change (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. |
@@ -361,7 +379,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
@@ -22,8 +22,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` R `` | Odśwież | Odśwież stan git (tj. uruchom `git status`, `git branch`, itp. w tle, aby zaktualizować zawartość paneli). To nie uruchamia `git fetch`. |
| `` d `` | Odrzuć | Gdy zaznaczona jest niezatwierdzona zmiana, odrzuć ją używając `git reset`. Gdy zaznaczona jest zatwierdzona zmiana, cofnij zatwierdzenie. |
| `` <ctrl+o> `` | Kopiuj zaznaczony tekst do schowka | |
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
| `` v `` | Przełącz zaznaczenie zakresu | |
| `` <left>, h `` | Idź do poprzedniego fragmentu | |
| `` <right>, l `` | Idź do następnego fragmentu | |
| `` N `` | Go to previous file | |
| `` n `` | Go to next file | |
| `` / `` | Szukaj w bieżącym widoku po tekście | |
## Drzewa pracy
@@ -202,6 +211,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` d `` | Odrzuć | Gdy zaznaczona jest niezatwierdzona zmiana, odrzuć ją używając `git reset`. Gdy zaznaczona jest zatwierdzona zmiana, cofnij zatwierdzenie. |
| `` <ctrl+o> `` | Kopiuj zaznaczony tekst do schowka | |
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
| `` v `` | Przełącz zaznaczenie zakresu | |
| `` <left>, h `` | Idź do poprzedniego fragmentu | |
| `` <right>, l `` | Idź do następnego fragmentu | |
| `` N `` | Go to previous file | |
| `` n `` | Go to next file | |
| `` / `` | Szukaj w bieżącym widoku po tekście | |
@@ -22,8 +22,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` R `` | Atualizar | Atualize o estado do git (ou seja, execute `git status`, `git branch`, etc em segundo plano para atualizar o conteúdo de painéis). Isso não executa `git fetch`. |
| `` + `` | Modo de tela seguinte (normal/metade/tela cheia) | |
| `` _ `` | Modo de tela anterior | |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
| `` \| `` | Cycle diff renderers | Choose the next renderer in the list of configured diff renderers. |
| `` \ `` | Cycle diff renderers (reverse) | Choose the previous renderer in the list of configured diff renderers. |
| `` <esc> `` | Cancelar | |
| `` ? `` | Abrir o menu de atalhos do teclado | |
| `` <ctrl+s> `` | Ver opções de filtro | View options for filtering the commit log, so that only commits matching the filter are shown. |
@@ -236,6 +236,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` d `` | Descartar | Quando a mudança não desejada for selecionada, descarte a mudança usando `git reset`. Quando a mudança em fase é selecionada, despare a mudança. |
| `` <ctrl+o> `` | Copiar texto selecionado para área de transferência | |
| `` a `` | Toggle hunk selection | Ativa/desativa modo linha por linha vs. modo de seleção por partes. |
| `` v `` | Toggle range select | |
| `` <left>, h `` | Ir para o local anterior | |
| `` <right>, l `` | Ir para o próximo trecho | |
| `` N `` | Go to previous file | |
| `` n `` | Go to next file | |
| `` / `` | Pesquisar na visualização atual por texto | |
## Painel Principal (preparação)
@@ -338,6 +347,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|-----|--------|-------------|
| `` <tab> `` | Mudar de visão | Alternar para outra visão (staged/não processadas alterações). |
| `` d `` | Descartar | Quando a mudança não desejada for selecionada, descarte a mudança usando `git reset`. Quando a mudança em fase é selecionada, despare a mudança. |
| `` <ctrl+o> `` | Copiar texto selecionado para área de transferência | |
| `` a `` | Toggle hunk selection | Ativa/desativa modo linha por linha vs. modo de seleção por partes. |
| `` v `` | Toggle range select | |
| `` <left>, h `` | Ir para o local anterior | |
| `` <right>, l `` | Ir para o próximo trecho | |
| `` N `` | Go to previous file | |
| `` n `` | Go to next file | |
| `` / `` | Pesquisar na visualização atual por texto | |
@@ -22,8 +22,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` R `` | Обновить | Refresh the git state (i.e. run `git status`, `git branch`, etc in background to update the contents of panels). This does not run `git fetch`. |
| `` + `` | Следующий режим экрана (нормальный/полуэкранный/полноэкранный) | |
| `` _ `` | Предыдущий режим экрана | |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
| `` \| `` | Cycle diff renderers | Choose the next renderer in the list of configured diff renderers. |
| `` \ `` | Cycle diff renderers (reverse) | Choose the previous renderer in the list of configured diff renderers. |
| `` <esc> `` | Отменить | |
| `` ? `` | Открыть меню | |
| `` <ctrl+s> `` | Просмотреть параметры фильтрации по пути | View options for filtering the commit log, so that only commits matching the filter are shown. |
@@ -75,6 +75,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|-----|--------|-------------|
| `` <tab> `` | Переключиться на другую панель (проиндексированные/непроиндексированные изменения) | Switch to other view (staged/unstaged changes). |
| `` <esc> `` | Exit back to side panel | |
| `` <space> `` | Переключить индекс | Переключить строку в проиндексированные / непроиндексированные |
| `` d `` | Отменить изменение (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. |
| `` <ctrl+o> `` | Скопировать выделенный текст в буфер обмена | |
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
| `` v `` | Переключить выборку перетаскивания | |
| `` <left>, h `` | Выбрать предыдущую часть | |
| `` <right>, l `` | Выбрать следующую часть | |
| `` N `` | Go to previous file | |
| `` n `` | Go to next file | |
| `` / `` | Найти | |
## Главная панель (Индексирование)
@@ -107,6 +116,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <tab> `` | Переключиться на другую панель (проиндексированные/непроиндексированные изменения) | Switch to other view (staged/unstaged changes). |
| `` <esc> `` | Exit back to side panel | |
| `` <space> `` | Переключить индекс | Переключить строку в проиндексированные / непроиндексированные |
| `` d `` | Отменить изменение (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. |
| `` <ctrl+o> `` | Скопировать выделенный текст в буфер обмена | |
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
| `` v `` | Переключить выборку перетаскивания | |
@@ -22,8 +22,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` R `` | 重新整理 | Refresh the git state (i.e. run `git status`, `git branch`, etc in background to update the contents of panels). This does not run `git fetch`. |
| `` + `` | 下一個螢幕模式(常規/半螢幕/全螢幕) | |
| `` _ `` | 上一個螢幕模式 | |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
| `` \| `` | Cycle diff renderers | Choose the next renderer in the list of configured diff renderers. |
| `` \ `` | Cycle diff renderers (reverse) | Choose the previous renderer in the list of configured diff renderers. |
| `` <esc> `` | 取消 | |
| `` ? `` | 開啟選單 | |
| `` <ctrl+s> `` | 檢視篩選路徑選項 | View options for filtering the commit log, so that only commits matching the filter are shown. |
@@ -83,6 +83,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <mouse wheel up> (fn+down) `` | 向上捲動 | |
| `` <tab> `` | 切換至另一個面板 (已預存/未預存更改) | Switch to other view (staged/unstaged changes). |
| `` <esc> `` | Exit back to side panel | |
| `` <space> `` | 切換預存 | 切換現有行的狀態 (已預存/未預存) |
| `` d `` | 刪除變更 (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. |
| `` <ctrl+o> `` | 複製所選文本至剪貼簿 | |
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
| `` v `` | 切換拖曳選擇 | |
| `` <left>, h `` | 選擇上一段 | |
| `` <right>, l `` | 選擇下一段 | |
| `` N `` | Go to previous file | |
| `` n `` | Go to next file | |
| `` / `` | 搜尋 | |
## 主面板(合併)
@@ -364,6 +373,15 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|-----|--------|-------------|
| `` <tab> `` | 切換至另一個面板 (已預存/未預存更改) | Switch to other view (staged/unstaged changes). |
| `` <esc> `` | Exit back to side panel | |
| `` <space> `` | 切換預存 | 切換現有行的狀態 (已預存/未預存) |
| `` d `` | 刪除變更 (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. |
| `` <ctrl+o> `` | 複製所選文本至剪貼簿 | |
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
Custom diff renderers are useful for showing a better rendering of a diff than git's builtin raw diff, and using one is strongly recommended (I personally prefer delta myself, but that's a matter of personal preference). There are three types of diff renderers that lazygit supports:
- **stdin filters**, e.g. [delta](#delta) and [diff-so-fancy](#diff-so-fancy). They take git's raw output as stdin and produce something nicer on stdout, and they are hooked up using git's GIT_PAGER mechanism. (These used to be called "custom pagers" in earlier lazygit versions.)
- **external diff programs**, e.g. difftastic; these are called using git's `--ext-diff` flag, and they take over diff generation from git completely rather than post-processing git's output.
- **git's raw output using custom arguments**; mainly useful for `--color-words` (or `--word-diff` if you are color blind).
Diff renderers are configured with the `diffRenderers` array in the `git` section of lazygit's config file; it is an array because you can have multiple entries that you can cycle through with the `|` key. This can be useful if you usually prefer a particular diff renderer, but want to use a different one for certain kinds of diffs.
Fields that are shared by all renderer types:
- **type** The type of diff renderer; choices are `stdinFilter`, `extDiff`, or `rawGit`. `stdinFilter` is the default, because it's the most common one; so you can omit this if you use delta.
- **name** A name that is shown in the status bar toast when cycling renderers; defaults to the first word of the renderer command, but can be useful e.g. to distinguish "delta" from "delta side-by-side" if you have entries for both.
Fields only for `stdinFilter`:
- **command** The command line to use for `GIT_PAGER`.
- **colorArg** whether you want the `--color=always` arg in your `git diff` command. Some diff renderers want it set to `always`, others want it set to `never`. The default is `always`, since that's what most renderers need.
Fields only for `extDiff`:
- **command** The command line to use for the `diff.external` git config. If left empty, it uses the global value of git's `diff.external` config; this can be useful if you also want to use it for diffs on the command line, and it also has the advantage that you can configure it per file type in `.gitattributes`; see https://git-scm.com/docs/gitattributes#_defining_an_external_diff_driver.
You can include the `{{diffContext}}` template variable to pass lazygit's current diff context size (the value controlled by the `{`/`}` keybindings) to the diff tool.
Fields only for `rawGit`:
- **args** The additional arguments to use in the `git diff` or `git show` call (e.g. `--color-words`)
A cool feature of delta is --hyperlinks, which renders clickable links for the line numbers in the left margin, and lazygit supports these. To use them, set the `command:` field to `delta --dark --paging=never --line-numbers --hyperlinks --hyperlinks-file-link-format="lazygit-edit://{path}:{line}"`; this allows you to click on an underlined line number in the diff to jump right to that same line in your editor.
Note that delta's `--navigate` option doesn't work in lazygit, for technical reasons.
## Diff-so-fancy
```yaml
git:
diffRenderers:
- command: diff-so-fancy
```

## ydiff
```yaml
gui:
sidePanelWidth: 0.2 # gives you more space to show things side-by-side
Lazygit supports custom pagers, [configured](/docs/Config.md) in the config.yml file (which can be opened by pressing `e` in the Status panel).
Multiple pagers are supported; you can cycle through them with the `|` key. This can be useful if you usually prefer a particular pager, but want to use a different one for certain kinds of diffs.
Pagers are configured with the `pagers` array in the git section; here's an example for a multi-pager setup (use an empty object `{}` for the default builtin diff display that doesn't use a pager):
The `colorArg` key is for whether you want the `--color=always` arg in your `git diff` command. Some pagers want it set to `always`, others want it set to `never`. The default is `always`, since that's what most pagers need.
## Delta:
```yaml
git:
pagers:
- pager: delta --dark --paging=never
```

A cool feature of delta is --hyperlinks, which renders clickable links for the line numbers in the left margin, and lazygit supports these. To use them, set the `pager:` config to `delta --dark --paging=never --line-numbers --hyperlinks --hyperlinks-file-link-format="lazygit-edit://{path}:{line}"`; this allows you to click on an underlined line number in the diff to jump right to that same line in your editor.
Note that delta's `--navigate` option doesn't work in lazygit, for technical reasons.
## Diff-so-fancy
```yaml
git:
pagers:
- pager: diff-so-fancy
```

## ydiff
```yaml
gui:
sidePanelWidth: 0.2 # gives you more space to show things side-by-side
Be careful with this one, I think the homebrew and pip versions are behind master. I needed to directly download the ydiff script to get the no-pager functionality working.
## Using external diff commands
Some diff tools can't work as a simple pager like the ones above do, because they need access to the entire diff, so just post-processing git's diff is not enough for them. The most notable example is probably [difftastic](https://difftastic.wilfred.me.uk).
These can be used in lazygit by using the `externalDiffCommand` config; in the case of difftastic, that could be
```yaml
git:
pagers:
- externalDiffCommand: difft --color=always
```
The `colorArg` option is not used in this case.
You can add whatever extra arguments you prefer for your difftool; for instance
This can also be used for normal git diffs with custom parameters, such as `--color-words` or `--word-diff` which some people find useful. To do that, save a script like this to, say, `~/bin/color-words.sh`:
Instead of setting this command in lazygit's `externalDiffCommand` config, you can also tell lazygit to use the external diff command that is configured in git itself (`diff.external`), by using
```yaml
git:
pagers:
- useExternalDiffGitConfig: true
```
This can be useful if you also want to use it for diffs on the command line, and it also has the advantage that you can configure it per file type in `.gitattributes`; see https://git-scm.com/docs/gitattributes#_defining_an_external_diff_driver.
`pager`, `externalDiffCommand`, and `useExternalDiffGitConfig` are alternative ways of producing the diff, so a pager entry may use at most one of them.
@@ -22,8 +22,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` R `` | Refresh | Refresh the git state (i.e. run `git status`, `git branch`, etc in background to update the contents of panels). This does not run `git fetch`. |
@@ -22,8 +22,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` R `` | 새로고침 | Refresh the git state (i.e. run `git status`, `git branch`, etc in background to update the contents of panels). This does not run `git fetch`. |
| `` + `` | 다음 스크린 모드 (normal/half/fullscreen) | |
| `` _ `` | 이전 스크린 모드 | |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
| `` \| `` | Cycle diff renderers | Choose the next renderer in the list of configured diff renderers. |
| `` \ `` | Cycle diff renderers (reverse) | Choose the previous renderer in the list of configured diff renderers. |
| `` <esc> `` | 취소 | |
| `` ? `` | 매뉴 열기 | |
| `` <ctrl+s> `` | View filter-by-path options | View options for filtering the commit log, so that only commits matching the filter are shown. |
| `` P `` | Push | Push de huidige branch naar de bijbehorende upstream-branch. Als er geen upstream is geconfigureerd wordt er gevraagd om een upstream-branch te configureren. |
| `` p `` | Pull | Pull wijzigingen van de remote voor de huidige branch. Als er geen upstream is geconfigureerd wordt er gevraagd om een upstream-branch te configureren. |
| `` ) `` | Increase rename similarity threshold | Increase the similarity threshold for a deletion and addition pair to be treated as a rename.<br><br>The default can be changed in the config file with the key 'git.renameSimilarityThreshold'. |
| `` ( `` | Decrease rename similarity threshold | Decrease the similarity threshold for a deletion and addition pair to be treated as a rename.<br><br>The default can be changed in the config file with the key 'git.renameSimilarityThreshold'. |
| `` } `` | Increase diff context size | Increase the amount of the context shown around changes in the diff view.<br><br>The default can be changed in the config file with the key 'git.diffContextSize'. |
| `` { `` | Decrease diff context size | Decrease the amount of the context shown around changes in the diff view.<br><br>The default can be changed in the config file with the key 'git.diffContextSize'. |
| `` : `` | Execute shellcommand | Bring up a prompt where you can enter a shell command to execute. |
| `` : `` | Voer shellcommando uit | Bring up a prompt where you can enter a shell command to execute. |
| `` m `` | Bekijk merge/rebase opties | Toon abort/continue/skip opties voor huidige merge/rebase. |
| `` R `` | Verversen | Refresh the git state (i.e. run `git status`, `git branch`, etc in background to update the contents of panels). This does not run `git fetch`. |
| `` + `` | Volgende scherm modus (normaal/half/groot) | |
| `` _ `` | Vorige scherm modus | |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
| `` \| `` | Cycle diff renderers | Choose the next renderer in the list of configured diff renderers. |
| `` \ `` | Cycle diff renderers (reverse) | Choose the previous renderer in the list of configured diff renderers. |
| `` <esc> `` | Annuleren | |
| `` ? `` | Open menu | |
| `` <ctrl+s> `` | Bekijk scoping opties | View options for filtering the commit log, so that only commits matching the filter are shown. |
| `` W, <ctrl+e> `` | Open diff menu | View options relating to diffing two refs e.g. diffing against selected ref, entering ref to diff against, and reversing the diff direction. |
| `` q, <ctrl+c> `` | Afsluiten | |
| `` <ctrl+z> `` | Pauzeer de applicatie | |
| `` <ctrl+w> `` | Toggle whitespace | Toggle whether or not whitespace changes are shown in the diff view.<br><br>The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. |
| `` <ctrl+w> `` | Witruimte weergeven in-/uitschakelen | Toggle whether or not whitespace changes are shown in the diff view.<br><br>The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. |
| `` <alt+shift+c> `` | Verander config bestand | Open bestand in externe editor. |
| `` z `` | Ongedaan maken (via reflog) (experimenteel) | The reflog will be used to determine what git command to run to undo the last git command. This does not include changes to the working tree; only commits are taken into consideration. |
| `` Z `` | Redo (via reflog) (experimenteel) | The reflog will be used to determine what git command to run to redo the last git command. This does not include changes to the working tree; only commits are taken into consideration. |
| `` Z `` | Redo (via reflog) (experimenteel) | Het reflog wordt gebruikt om te bepalen welk git commando moet worden gebruikt om het laatste git commando te herhalen. Wijzigingen aan de working tree worden niet meegenomen, alleen command's zijn kandidaten. |
## Lijstpaneel navigatie
@@ -161,25 +161,25 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` s `` | Squash | Squash the selected commit into the commit below it. The selected commit's message will be appended to the commit below it. |
| `` f `` | Fixup | Meld the selected commit into the commit below it. Similar to squash, but the selected commit's message will be discarded. |
| `` c `` | Set fixup message | Set the message option for the fixup commit. The -C option means to use this commit's message instead of the target commit's message. |
| `` r `` | Hernoem commit | Reword the selected commit's message. |
| `` r `` | Hernoem commit | Herschrijf de commit message van de geselecteerde commit. |
| `` R `` | Hernoem commit met editor | |
| `` d `` | Verwijder commit | Drop the selected commit. This will remove the commit from the branch via a rebase. If the commit makes changes that later commits depend on, you may need to resolve merge conflicts. |
| `` i `` | Start interactive rebase | Start an interactive rebase for the commits on your branch. This will include all commits from the HEAD commit down to the first merge commit or main branch commit.<br>If you would instead like to start an interactive rebase from the selected commit, press `e`. |
| `` i `` | Start interactieve rebase | Start an interactive rebase for the commits on your branch. This will include all commits from the HEAD commit down to the first merge commit or main branch commit.<br>If you would instead like to start an interactive rebase from the selected commit, press `e`. |
| `` p `` | Pick | Kies commit (wanneer midden in rebase) |
| `` <ctrl+k>, <alt+up> `` | Verplaats commit 1 naar boven | |
| `` V `` | Plak commits (cherry-pick) | |
| `` B `` | Mark as base commit for rebase | Select a base commit for the next rebase. When you rebase onto a branch, only commits above the base commit will be brought across. This uses the `git rebase --onto` command. |
| `` B `` | Markeer als basiscommit voor rebase | Selecteer een basiscommit voor de volgende rebase. Als je rebased op een branch worden alleen commits boven de basiscommit meegenomen. Hiervoor wordt het `git rebase --onto` commando gebruikt. |
| `` A `` | Amend | Wijzig commit met staged veranderingen |
| `` a `` | Amend commit attribute | Set/Reset commit author or set co-author. |
| `` t `` | Revert | Create a revert commit for the selected commit, which applies the selected commit's changes in reverse. |
| `` T `` | Tag commit | Create a new tag pointing at the selected commit. You'll be prompted to enter a tag name and optional description. |
| `` <ctrl+l> `` | View log options | View options for commit log e.g. changing sort order, hiding the git graph, showing the whole git graph. |
| `` t `` | Revert | Maak een revert commit voor de geselecteerde commit, die de wijzigingen in deze commit terugdraait. |
| `` T `` | Tag commit | Maak een nieuwe tag die naar de geselecteerde commit wijst. Je wordt gevraagd om een tag naam en optionele omschrijving. |
| `` <ctrl+l> `` | Log opties weergeven | View options for commit log e.g. changing sort order, hiding the git graph, showing the whole git graph. |
| `` G `` | Open pull request in browser | |
| `` <space> `` | Uitchecken | Checkout the selected commit as a detached HEAD. |
| `` <space> `` | Uitchecken | Check de geselecteerde branch uit als een detached HEAD. |
| `` y `` | Copy commit attribute to clipboard | Copy commit attribute to clipboard (e.g. hash, URL, diff, message, author). |
| `` o `` | Open commit in browser | |
| `` n `` | Creëer nieuwe branch van commit | |
@@ -241,7 +241,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` <left>, h `` | Selecteer de vorige hunk | |
| `` <right>, l `` | Selecteer de volgende hunk | |
| `` v `` | Toggle drag selecteer | |
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
| `` a `` | Wissel tussen hunk selectie aan of uit | Wissel tussen regel-voor-regel of hunk selectie modus. |
| `` <ctrl+o> `` | Copy selected text to clipboard | |
| `` o `` | Open bestand | Open bestand in standaardapplicatie. |
| `` e `` | Verander bestand | Open bestand in externe editor. |
@@ -255,7 +255,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` d `` | Verwijdert change (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. |
@@ -361,7 +361,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
@@ -22,8 +22,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` R `` | Odśwież | Odśwież stan git (tj. uruchom `git status`, `git branch`, itp. w tle, aby zaktualizować zawartość paneli). To nie uruchamia `git fetch`. |
@@ -22,8 +22,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` R `` | Atualizar | Atualize o estado do git (ou seja, execute `git status`, `git branch`, etc em segundo plano para atualizar o conteúdo de painéis). Isso não executa `git fetch`. |
| `` + `` | Modo de tela seguinte (normal/metade/tela cheia) | |
| `` _ `` | Modo de tela anterior | |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
| `` \| `` | Cycle diff renderers | Choose the next renderer in the list of configured diff renderers. |
| `` \ `` | Cycle diff renderers (reverse) | Choose the previous renderer in the list of configured diff renderers. |
| `` <esc> `` | Cancelar | |
| `` ? `` | Abrir o menu de atalhos do teclado | |
| `` <ctrl+s> `` | Ver opções de filtro | View options for filtering the commit log, so that only commits matching the filter are shown. |
@@ -22,8 +22,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` R `` | Обновить | Refresh the git state (i.e. run `git status`, `git branch`, etc in background to update the contents of panels). This does not run `git fetch`. |
| `` + `` | Следующий режим экрана (нормальный/полуэкранный/полноэкранный) | |
| `` _ `` | Предыдущий режим экрана | |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
| `` \| `` | Cycle diff renderers | Choose the next renderer in the list of configured diff renderers. |
| `` \ `` | Cycle diff renderers (reverse) | Choose the previous renderer in the list of configured diff renderers. |
| `` <esc> `` | Отменить | |
| `` ? `` | Открыть меню | |
| `` <ctrl+s> `` | Просмотреть параметры фильтрации по пути | View options for filtering the commit log, so that only commits matching the filter are shown. |
@@ -22,8 +22,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
| `` R `` | 重新整理 | Refresh the git state (i.e. run `git status`, `git branch`, etc in background to update the contents of panels). This does not run `git fetch`. |
| `` + `` | 下一個螢幕模式(常規/半螢幕/全螢幕) | |
| `` _ `` | 上一個螢幕模式 | |
| `` \| `` | Cycle pagers | Choose the next pager in the list of configured pagers. |
| `` \ `` | Cycle pagers (reverse) | Choose the previous pager in the list of configured pagers. |
| `` \| `` | Cycle diff renderers | Choose the next renderer in the list of configured diff renderers. |
| `` \ `` | Cycle diff renderers (reverse) | Choose the previous renderer in the list of configured diff renderers. |
| `` <esc> `` | 取消 | |
| `` ? `` | 開啟選單 | |
| `` <ctrl+s> `` | 檢視篩選路徑選項 | View options for filtering the commit log, so that only commits matching the filter are shown. |
// the price of having a convenient interface where we can say .New(...).Run() is that our builder now depends on our runner, so when we want to wrap the default builder/runner in new functionality we need to jump through some hoops. We could avoid the use of a decorator function here by just exporting the runner field on the default builder but that would be misleading because we don't want anybody using that to run commands (i.e. we want there to be a single API used across the codebase)
// [dev] This documentation is duplicated in the GitConfig struct. If you make changes here, make them there too.
typePagingConfigstruct{
// A name for the pager, shown in the notification when cycling pagers. If not set, the name is derived from the first word of the pager command (or of the external diff command).
typeDiffRendererConfigstruct{
// The type of diff renderer. One of: 'stdinFilter' (default) | 'extDiff' | 'rawGit'
// A name for the diff renderer, shown in the notification when cycling renderers. If not set, the name is derived from the first word of the renderer command.
Namestring`yaml:"name"`
// Value of the --color arg in the git diff command. Some pagers want this to be set to 'always' and some want it set to 'never'
// If true, selected diff lines are highlighted only with a narrow bar at the left edge of the view, rather than across the whole width. Useful for pagers whose own line background colors should remain visible.
// Value of the --color arg in the git diff command. Only used for type 'stdinFilter'. Some renderers want this to be set to 'always' and some want it set to 'never'.
// The command to use for rendering diffs. This is either a stdinFilter or an external diff command, depending on the type field; not applicable if the type is 'rawGit'.
// If true, Lazygit will use git's `diff.external` config for paging. The advantage over `externalDiffCommand` is that this can be configured per file type in .gitattributes; see https://git-scm.com/docs/gitattributes#_defining_an_external_diff_driver.
// validatePagers rejects pager entries that combine more than one diff
// mechanism. A pager (GIT_PAGER) formats the diff that git produces, whereas
// externalDiffCommand and useExternalDiffGitConfig change how git produces the
// diff in the first place; piping one through the other almost always yields
// garbled output, so we treat the three as mutually exclusive.
funcvalidatePagers(pagers[]PagingConfig)error{
fori,pager:=rangepagers{
count:=0
ifpager.Pager!=""{
count++
}
ifpager.ExternalDiffCommand!=""{
count++
}
ifpager.UseExternalDiffGitConfig{
count++
}
ifcount>1{
returnfmt.Errorf("git.pagers[%d]: at most one of 'pager', 'externalDiffCommand', and 'useExternalDiffGitConfig' may be set; they are mutually exclusive",i)
// SetGitLocationEnvVars sets the location variables from "NAME=value" entries,
// clearing both first so that only what is given remains. Passing nothing is
// how you say the repo is to be found from the working directory.
funcSetGitLocationEnvVars(envVars[]string){
UnsetGitLocationEnvVars()
for_,envVar:=rangeenvVars{
ifname,value,ok:=strings.Cut(envVar,"=");ok{
os.Setenv(name,value)
}
}
}
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.