1433 Commits
Author SHA1 Message Date
Stefan HallerandClaude Opus 5.5 c3a051dcca Use Sprint for text that isn't a format string
Two places pass plain text as the format string of TextStyle.Sprintf
without further arguments: the prefix of a disabled menu item's
tooltip, and the key binding entries of the options bar. If the text
contains a "%", for example in a translation or in a key label, it is
mangled into something like "%!(NOVERB)".

go vet doesn't report these calls yet. It will once go.mod says go
1.26, because vet then also finds printf wrappers through interface
methods. TextStyle.Sprintf forwards to the Sprintf method of the
Sprinter interface.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 07:53:03 +02:00
Stefan HallerandClaude Opus 5.5 a75b0457c1 Ask before discarding a nested repository if that loses anything
Discarding a nested repository deletes its directory like that of any
untracked file. If the repository keeps its git dir in its directory,
this deletes all of its history, and nothing said so. The directory of
a removed submodule keeps its git dir elsewhere, but it may have
changes that aren't committed.

Check for both before discarding. If anything would be lost, list it
and ask for confirmation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 20:51:34 +02:00
Stefan HallerandClaude Opus 5.5 9009083f55 Don't stage nested repositories when committing with nothing staged
If nothing is staged, committing offers to stage all files first, or
stages them without asking if gui.skipNoStagedFilesWarning is set. Like
staging all files in the files panel, this adds a nested repository as
an embedded repository.

Show why the nested repository can't be staged instead, as staging all
files in the files panel does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 20:51:34 +02:00
Stefan HallerandClaude Opus 5.5 a2cf915f8d Don't stage nested repositories
If you staged the directory of a removed submodule, a nested repository
or a worktree inside the repo, git added it as an embedded repository: a
gitlink without an entry in .gitmodules. For a removed submodule, this
brings it back in a broken state. git warns about it, but lazygit didn't
show the warning. Staging all files did the same to every such
directory.

Disable staging a selection that contains one of these, and disable
staging all files if that would stage one. The disabled reason tells you
how to get rid of it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 20:51:34 +02:00
Stefan HallerandClaude Opus 5.5 d2675b7dea Extract stageAllScope from toggleStagedAllWithLock
Whether stage-all may be used is going to depend on what it stages.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 20:51:34 +02:00
Stefan HallerandClaude Opus 5.5 35909f65d8 Explain the directories that git status reports as a whole
Selecting one of these in the files panel showed the error "Could not
access 'modules/sub/null'". The main view showed the entry's diff
against /dev/null with --no-index, as for an untracked file. When git
compares a file with a directory that way, it looks for a file of the
same name inside the directory.

Show what the directory is and what to do about it instead. Below that,
show the status of the repository that it is, or the files in it if it
is a plain directory. The status tells you whether discarding the
directory would lose anything.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 20:51:34 +02:00
Stefan HallerandClaude Opus 5.5 d8d11434a4 Extract wrapForReadability from conflictResolutionHint
More messages for the main view are going to need it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 20:51:34 +02:00
Stefan HallerandClaude Opus 5.5 e2a29a6d77 Remove a worktree inside the repo properly when discarding it
If a linked worktree lives inside the repo's working tree, it shows up
in the files panel. Discarding it there deleted its directory like that
of any untracked file. git still had the worktree on record, so the
worktrees panel listed it as missing, and its branch couldn't be checked
out anywhere else until the worktree was pruned.

Show the same menu for removing the worktree as the worktrees panel
does. Like a submodule, a worktree can't be discarded together with
other entries, because it has a menu of its own.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 20:51:34 +02:00
Stefan HallerandClaude Opus 5.5 3ffcc0c7b0 Extract worktreeOfFile from markWorktreeFiles
The files panel is going to need to find the worktree of an entry too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 20:51:34 +02:00
Stefan HallerandClaude Opus 5.5 875641557b Move the menu for removing a worktree to BranchesHelper
The files panel is going to show it too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 20:51:34 +02:00
Stefan HallerandClaude Opus 5.5 8a2d9758e0 Move gitDirOfRepo to git_commands
The file loader is going to need it too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 09:13:12 +02:00
Stefan HallerandClaude Opus 5.5 2ddeebca72 Show a directory that git status reports as a whole as a single entry
git status doesn't descend into a nested repository, such as the
directory that a removed submodule leaves behind. It reports the
directory as a whole, with a trailing slash. The file tree splits paths
at slashes, so it showed a directory with a file without a name in it.
Discarding the directory removed it, but then the cleanup of emptied
directories took the parent of "modules/sub/" to be "modules/sub"
itself, and failed to read it.

Take the slash off when loading the files. This also covers any
untracked directory if status.showUntrackedFiles is "normal", and linked
worktrees inside the repo. Their slash was taken off when they were
marked as worktrees; this is no longer needed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 09:13:12 +02:00
Stefan HallerandClaude Opus 5.5 4b61c4b650 Quit with an error message when the repo or worktree is deleted
If the repo or linked worktree that lazygit has open is deleted while
lazygit is running, every git command fails. lazygit no longer panics
since the previous commit, but it shows empty lists, and there's
nothing useful left to do in it.

Check at the start of every refresh whether the worktree and its git
dir still exist. If one of them is gone, quit and print its path in
the terminal. The background files refresh does this check too, so
lazygit quits within ten seconds even if nobody is using it. To quit
from a refresh, return an error that wraps gocui.ErrQuit; this keeps
it out of the error popup.

Switching to another worktree or to a recent repo would be an
alternative, but it would be more surprising than quitting. Also,
there isn't always a main worktree to switch to.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 08:35:00 +02:00
Stefan HallerandClaude Opus 5.5 787a309b3a Begin blocking input after capturing the refresh's baseline
The next commit makes performRefresh return early right after the
capture, and it must not leave the input blocked when it does.

This doesn't let any keypress slip through before the block begins.
Only refreshes on the UI thread block input, and there the capture
runs synchronously.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-15 08:35:00 +02:00
Stefan HallerandClaude Opus 5.5 7a9e4bee23 Restore the terminal if a timer or errgroup function panics
time.AfterFunc and errgroup.Group.Go run the function they are given
on a goroutine of their own, so a panic in it leaves the terminal in
raw mode, like a panic in any other goroutine that doesn't use
utils.Safe.

Add utils.SafeFunc and utils.SafeFuncWithError, which return a copy
of a function that runs through utils.Safe or utils.SafeWithError, and
use them for the functions given to time.AfterFunc and errgroup.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 20:28:53 +02:00
Stefan HallerandClaude Opus 5.5 73fac4a554 Restore the terminal if a goroutine panics
Most goroutines run their function through utils.Safe, which closes
the screen if the function panics. Without it, the panic is printed to
a terminal that is still in raw mode and on the alternate screen, and
the terminal needs a reset afterwards. Go has no process-wide hook for
panics, so every goroutine has to do this itself.

Use utils.Safe for the goroutines that don't use it yet.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 20:28:53 +02:00
Stefan HallerandClaude Opus 5.5 dc4fee296c Leave out the text of the rows of a rendering that states its lines
Under a diff renderer that states which diff line each row shows, such
as delta, the queries about the lines of the main view read the rows by
those records alone. Still, they took the text of every row as well,
and for a diff of 300,000 lines this was most of the time each of them
took. Moving the selection by change block runs two of them for every
key press.

Take the records of the rows first, and their text only if no row
states a diff line.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 08:41:50 +02:00
Stefan HallerandClaude Opus 5.5 cbfa22cfbe Make the path of a diff's lines absolute once per file
Every query about the lines of the main view, such as moving the
selection to the next change block, finds out which line of which file
each row shows. For every row, this joined the path that the diff
states with the path of the worktree. In a diff of 300,000 lines, this
took more than half of the time of the query.

The lines of a diff come file by file, so remember the last path that
was made absolute and reuse it while the path stays the same. Do the
same in the other places that go through every line of a diff. A query
over that diff now takes about 27 milliseconds instead of 63.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 08:41:50 +02:00
Stefan HallerandClaude Opus 5.5 80dff3c2b9 Number the lines of a diff in one pass when parsing it
With no diff renderer configured, opening the jump-to-file menu for the
diff of a commit that adds or deletes a file of 100,000 lines takes
more than 20 seconds, and so does picking a file from it. In such a
diff, moving the selection by change block takes as long for each key
press. All this time the UI is frozen.

To find out which line of which file each row of the main view shows,
lazygit parses the view's content as a diff. For every line of a file's
diff it asks the parsed patch for the line's number in the new and in
the old file, and each of these calls counts the lines of the hunk
before it. So parsing takes time quadratic in the length of a hunk, and
the diff of a new or deleted file is a single hunk. For a diff of
200,000 lines, one parse took about 10 seconds, and opening the menu
parses it twice. A diff renderer that states which line each row shows,
such as delta, doesn't go through this parse.

Number all lines of the patch in one pass instead. Parsing that diff
now takes about 40 milliseconds.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 08:41:50 +02:00
Stefan HallerandClaude Opus 5.5 56b6c5af31 Stop at the end of the diff when jumping to a file from a panel
With gui.scrollPastBottom set to false, picking the last file of a long
diff from the jump-to-file menu of the commits panel scrolls that file
to the top of the view. This leaves blank space below the end of the
diff.

A jump in a pane that isn't focused puts the file at the top of the view
and leaves it to the layout to stop at the end of the diff. But the
layout doesn't do this while the view is loading. Picking an item from
the menu closes the menu first, and the commits panel starts a re-render
of the diff as it gets the focus back. So the view is loading when the
jump happens, and the layout corrects the position only in a later pass
after the re-render has finished. For a long diff the re-render never
finishes; a later commit fixes this. Clicking a file in the diffstat
doesn't close a menu, so it doesn't have the problem.

Stop the jump at the last screenful of the diff, as a jump in the
focused pane does. Like that one, it stops there whatever
gui.scrollPastBottom is set to.

The diff in click_a_file_in_a_diff_stat_that_comes_first fits on
screen, so the jump doesn't scroll it any more. Make it longer than the
view.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 16:26:38 +02:00
Stefan HallerandClaude Opus 5.5 d198ce56f1 Extract how far a jump can scroll its target to the top of the view
A jump in a pane that isn't focused is going to stop at the last
screenful of the diff as well.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 16:23:36 +02:00
Stefan HallerandClaude Opus 5.5 3742ac6066 Show an error when editing a file of a diff that doesn't exist
If you press `e` on a file of a commit, and a later commit deleted that
file, the editor is opened on a path that doesn't exist. Most editors
then create it as a new, empty file. This is confusing. The same
happens with `o`, and with any other file that the worktree it is
opened from doesn't have. An example is a file that is added in a branch
that isn't checked out anywhere, since it is opened from the current
worktree.

Show an error instead of opening the editor. For a range selection in
the commit files panel, show it only if none of the selected files
exist. Otherwise open the existing ones and leave out the others
silently; an error shown only after returning from the editor would
come too late to be useful.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 13:58:05 +02:00
Stefan HallerandClaude Opus 5.5 6cd6e4532d Copy the absolute path of a branch's file from the worktree that has it
If the commit files panel was entered from a branch that another
worktree has checked out, "Copy absolute path" in its copy menu gives
the file's path in the current worktree. Make it give the path in the
worktree that has the branch checked out. This is the file that `e` and
`o` open, and the one users most likely want the path of.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 13:58:05 +02:00
Stefan HallerandClaude Opus 5.5 690509a8cc Open files with o from the worktree that has the branch checked out
Do the same for `o` in the commit files panel as the previous commits
do for `e`. If the panel was entered from a branch that another worktree
has checked out, open the file from that worktree.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 13:58:05 +02:00
Stefan HallerandClaude Opus 5.5 51e91fbb85 Open files of a branch's diff from the worktree that has it checked out
If you enter a branch in the branches panel and press `e` on a line of
a commit's diff, or on a file in the commit files panel entered from
there, lazygit opens the file in the current worktree. If another
worktree has the branch checked out, this is the wrong copy of the
file. The current worktree may have it in a different state, or not
have it at all. Clicking a lazygit-edit:// hyperlink of a diff renderer
in such a diff has the same problem.

Open the file from the worktree that has the branch checked out
instead, and carry the line number forward to the file as that worktree
has it.

If no other worktree has the branch checked out, the file is still
opened from the current worktree.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 13:58:04 +02:00
Stefan HallerandClaude Opus 5.5 64fd7d8686 Route the editing of a diff's files through DiffHelper
Three places open a file of a diff in the editor: `e` and alt-click in
the main view, the lazygit-edit:// hyperlinks of a diff renderer, and
`e` in the commit files panel. The first two also each carry a line of
the diff forward to the file's current line before opening it.

Give them one pair of methods to call. The next commit changes where
these files are opened from, and can then do so in one place.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 13:58:04 +02:00
Stefan HallerandClaude Opus 5.5 75083c179e Scroll the focused diff sideways with H and L
If gui.wrapLinesInDiffView is false, long lines of a diff run off the
right edge of the main view, and there is no way to see the rest of
them. This makes it hard to see what has changed in such a line. The
old staging and patch-building views scrolled sideways with H and L, but
the focused main view that replaced them has no binding for these keys.

Bind them in the focused main view, as in the side panels. Leaving the
view scrolls it back to the left edge, as it did for the staging view.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 13:44:43 +02:00
Stefan HallerandClaude Opus 5.5 2abe4848f3 Scroll back to the left edge when jumping to another file of a diff
Going to the next or previous file of a diff, or picking one from the
jump-to-file menu or the diffstat, brings that file to the top of the
view. The user goes there to read the file from its start, so scroll the
view back to the left edge as well. Jumps between the hunks of a diff
keep the horizontal scroll position, as moving the selection does.

Nothing scrolls the main view horizontally yet, so this changes nothing
on its own. It prepares for scrolling the focused main view sideways
with H and L.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 13:44:43 +02:00
Stefan HallerandClaude Opus 5.5 4f678b7d0d Keep the horizontal scroll position when a diff is rendered again
Two places that position the main view after a re-render of the same
diff call SetOrigin(0, y), although they only mean to move the view
vertically. One is the restore after a change of the context size, of
whitespace handling or of the diff renderer. The other is the clamp for
content that ends above the scroll position, for example after staging a
large part of the diff. Use SetOriginY in both, so that they keep the
horizontal scroll position.

Nothing scrolls the main view horizontally yet, so this changes nothing
on its own. It prepares for scrolling the focused main view sideways
with H and L.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 13:44:43 +02:00
Stefan HallerandClaude Opus 5.5 2a3883f502 Show an error if the commit to discard lines from can't be found
Discarding lines from a commit's diff rewrites the commit with a
rebase. For this we look up the commit among the commits of the
current branch. If it isn't there, pressing the key does nothing. The
disabled reason only allows discarding in a diff of the commits panel,
so this shouldn't happen. If it does, the user should see it.

Show an error that names the commit instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 13:04:41 +02:00
Stefan HallerandClaude Opus 5.5 56e234e94c Show an error if the selected diff lines can't be found in the diff
When we stage, discard or edit lines of the working tree's diff, we
read the file's diff again and look for the selected lines in it. If
none of them are there, nothing happens. The same goes for adding
lines of a commit's diff to a custom patch, or taking lines out of
it. This can happen if the file changed since its diff was shown, or
if we read the line numbers out of the diff on screen wrongly.

Show an error that names the file instead. As with a file that can't
be found, don't act on any file if the lines of one are missing.

Unlike the file check, this one happens after a new custom patch has
been started. If the error leaves that patch without any lines, reset
it, as we do when the last line is taken out of a patch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 13:04:41 +02:00
Stefan HallerandClaude Opus 5.5 15ac3c28c9 Show an error if the file of the selected diff lines can't be found
If the path in the diff header of a selected line doesn't name a file
of the files panel, staging or discarding the line does nothing. The
command log shows "Apply patch", but nothing else happens, so the user
has nothing to go on when reporting it. Adding a line of a commit's
diff to a custom patch fails in the same silent way if the path
doesn't name a file of that diff. A diff renderer that names files
differently can cause this, and so can a bug in how we read the path,
like the one we had with diff.mnemonicPrefix.

Show an error that names the path instead, and do nothing else. If a
selection spans several files, don't act on the files we did find,
because that would do only part of what the user asked for. Do the
same for opening a line of a diff in its pull request.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 13:04:41 +02:00
Stefan HallerandClaude Opus 5.5 a29c70f1f3 Build the patches of all selected files before applying any of them
When staging, discarding or taking lines out of a custom patch spans
several files, we read the diff of a file and apply its patch before
we look at the next file. The next commits report an error if the
selected lines of a file can't be found. Without this change, the
files before it would already be acted on by then.

So find the lines in every file first, and only then apply anything.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 13:04:41 +02:00
Stefan HallerandClaude Opus 5.5 e5eb4086c5 Don't ignore a git error when listing the files of a commit's diff
If git fails to list the files of the diff that a custom patch is
built from, we carry on as if the diff had no files. Then we don't
know which files were renamed, added or deleted, and build the patch
from the wrong lines or without the file's creation or deletion.

Return the error instead, so that the user sees it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 13:04:41 +02:00
Stefan HallerandClaude Opus 5.5 6be2bd9031 Resolve the files of the selected diff lines before acting on them
The focused diff actions look up the file of a selected line in
different places. In the files panel, staging and discarding do it
per file while applying the patches, and editing a hunk does it for
the first line. For a commit's diff, adding lines to a custom patch or
discarding them from the commit does it only after the confirmation
prompt and after a new patch has been started. The next commits
report a line whose file can't be found as an error. For that, the
lookup has to happen once, before we log an action, ask for
confirmation or start a patch.

So let diffLineSelection group the selected lines by their file, in
the order the diff shows the files, and do the same for a commit's
diff in selectionByFile. A line whose file can't be found is still
skipped, as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 13:04:41 +02:00
Stefan HallerandClaude Opus 5.5 243ede84d7 Fix staging lines in the main view when diff.mnemonicPrefix is set
If diff.mnemonicPrefix is set, pressing space on a line in the focused
main view does nothing. The command log shows "Apply patch", but the
line is not staged. Other commands that read a path out of the working
tree's diff go wrong as well, such as jumping to a file. Building a
custom patch from a commit's diff still works, because git uses
mnemonic prefixes only when one side of the diff is the index or the
working tree.

With this config, the paths in the working tree's diff start with i/
and w/ instead of a/ and b/. We only strip a/ and b/, so the path we
read out of the diff header is not a file of the files panel, and we
skip the line without a message.

Users who set the config want to see these prefixes, so keep them.
Strip the first component of the path instead, whatever it is. `git
apply` does the same by default, and ParseableDiffPrefixes makes sure
there is such a component.

A section without +++ and --- lines needs more than that, because we
find the new path on its "diff --git" line by searching for " b/".
Take the path from the "rename to" or "copy to" line instead where
there is one. Otherwise the two paths on the "diff --git" line are the
same. Split the line where the two halves match after stripping their
prefixes, like `git apply` does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 13:57:25 +02:00
Stefan HallerandClaude Opus 5.5 d7ffcd02b4 Extract the decoding of a path in a diff header into unquoteDiffPath
The "rename to" and "copy to" lines of a diff header spell a path the
same way as the other header lines, but without a prefix. The next
commit reads paths from these lines too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 13:57:00 +02:00
Stefan HallerandClaude Opus 5.5 576017c00b Lay out the rule below a pull request's header to the main view's width
The rule was as wide as the main view was when the render was asked
for. If the layout then changed the width of the main view, the rule
came out too long or too short.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 16:38:13 +02:00
Stefan HallerandClaude Opus 5.5 5235083bd2 Let the prefix of a render be laid out to the width it renders at
The rule below the header of a branch's pull request is drawn as wide as
the main view. Its width is read when the render is asked for, before
the layout has settled it. If the layout then changes the width of the
main view, for example because it unsplits the view for the content
that comes next, the rule comes out as wide as the view used to be.

Make a prefix a function that is called with the width the render is
laid out to, once the layout has run. The function it returns produces
the text on the render's own goroutine before the command starts. This
way a prefix that takes a while to produce, such as one that runs
commands of its own, holds up only the render and not the UI.

All prefixes are still static here; the next commit lays out the rule
of the pull request header to the width.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 16:38:13 +02:00
Stefan HallerandClaude Opus 5 07e18b4085 Link the file names in a diffstat to where each file's diff begins
A diff opens with a diffstat naming every file in it, right above the
diff of each of them. Selecting a commit puts that list in front of you,
naming the same files the menu of the diff's files offers, and the menu
is still the only way to any of them.

Make each of those names a link that goes to where that file's diff
begins, as picking it from the menu does. The panel keeps the focus, so
a file of the commit being read is a click away and the selection stays
on the commit.

The names are found in the output as it is written to the pane, for next
to nothing. The diffstat is git's own text whichever renderer the diff
goes through — delta, diff-so-fancy and difftastic all pass it on
untouched — and it comes first, so the scan for it ends with it and
nothing below is looked at.

The link states the name as the diffstat does, and which file that names
is worked out on the click, against the files the diff turned out to
hold. That is the point at which a name the diffstat cut off behind
"..." or compacted to the "{old => new}" form of a rename can be
recognized at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-05 12:44:17 +02:00
Stefan HallerandClaude Opus 5 e2eab7c956 Add a menu of the diff's files to jump to one directly
n and N step through the files of a multi-file diff one at a time, which
is a long way to the far end of a commit touching a hundred files.

Bind ctrl+g to a menu of the diff's files, in the order the diff shows
them and by the paths the repo knows them by; picking one goes to where
its diff begins, exactly where stepping to it with n would have left
you. The menu filters as you type, so the file you have in mind is a few
characters away however many the commit touches.

The key works in the panel the diff belongs to as well as in the diff
itself. A commit is read from the commits panel, so being able to jump
from there saves focusing the diff and leaving it again for the next
commit; the diff scrolls to the file and the focus stays in the panel.
It is ctrl+g rather than f because a key that works in every panel has
to be free in all of them, and f is fetch in the files panel and fixup
in the commits panel.

The command applies only while the main view is showing a diff, and is
left out of the keybindings menu where it isn't: over a branch's commit
log, or while a conflicted file has given the main section over to the
merge conflicts view.

The diff is read to the end before the menu is built, as searching it
does: a file below the part that has been read is in neither the list
nor the view. Each item names its file rather than the row that file
begins at, so that a diff re-rendered while the menu is up is jumped
into at the row the file begins at now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-05 12:14:02 +02:00
Stefan HallerandClaude Opus 5 0e0236ad7b Move where a jump lands into the diff line helper
Where a focus or a click puts the focused main view's selection is
worked out in the diff line helper; where a jump puts it is worked out
on the main view controller, though it is the same question about the
same pane. A jump asked for from anywhere else — from a panel below the
pane, or from a click on a link in the diff — has no way to reach that
answer.

Move it over. The controller keeps placeNavigationTarget as the short
way to say it for the jumps it makes itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-05 12:14:02 +02:00
Stefan HallerandClaude Opus 5 4fe993442c Ask a diff where each of its files begins
File navigation works out where the neighbouring file begins by walking
the rows itself, forwards or, more laboriously, backwards. A menu of the
diff's files needs the same rows, all of them at once.

Extract fileStarts, which answers that for the whole diff, and have
navigation pick its neighbour out of the answer. The two then agree on
where a file begins by construction, and the walk backwards over a file
goes away.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-05 12:14:02 +02:00
Stefan HallerandClaude Opus 5 89b2697936 Name the file a diff row belongs to in the repo's terms
The identity of a row is reported in the repo's terms already: the pane
previewing the custom patch shows a diff of the two trees the patch was
materialized into, and the paths of those trees are mapped back to the
repo's files before anything sees them. Asking which file a row belongs
to, which file navigation does, was the one query that skipped that step
and answered with the tree's path.

Pull the mapping out of inRepoTerms so that it can be applied to a bare
path, and put filePaths through it. A menu listing the files of the diff
will want to show them by name; where a renderer states the path of each
side of a change, this also has both halves belong to one file rather
than to the two trees.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-05 12:14:02 +02:00
Stefan HallerandClaude Opus 5 ef71bcf9b7 Open the selected diff line in the branch's pull request
Reading a change in lazygit and saying something about it on GitHub
means finding the line again in the browser: open the pull request, find
the commit, find the file, scroll to the line. The line is already under
the cursor here.

Bind G in the focused main view, the key the commits panel opens the
pull request with, to open it at the line the selection is on. The URL
names the commits whose diff is on screen, so that the line numbers of
the diff are the ones the page shows, the file by the SHA-256 of its
repo-relative path, and the line by the side of the diff it is on: R for
the new version of the file, L for the old one, where a deleted line is.
GitHub documents none of that; the form was read off the URLs its own
pages carry.

One commit is named by its hash. A range of them is named by the commit
the range starts after and the commit it ends at, the form the chooser
above a pull request's files uses. The commit a range starts after is
the parent of its oldest commit; where the range starts where the pull
request itself does, that parent is none of the pull request's own
commits, and the keyword BASE stands for it.

Which branch's pull request that is depends on the panel beneath. The
commits panel lists the commits of the checked-out branch, the
sub-commits panel those of the branch drilled into, and the commit files
panel shows the files of a commit from either. In a stack of branches,
each with a pull request of its own, those lists include the commits of
the branches below, and each of those commits is in the pull request of
its own branch. So the command looks upwards from the commit for the
nearest head of a branch with a pull request, and takes the listed
branch if it finds none. Panels showing a diff that no pull request has
a view of don't answer, and the command isn't offered over their diffs
at all.

Neither is it offered over a diff that is not the commit's own, where
the line numbers on screen are not the ones the page shows: a diff
against another ref in diffing mode, and the custom patch, whose lines
sit at the numbers the patch gives them.

A pull request holds only the commits of its branch that are pushed, and
its pages say they can't find any other commit. So the command refuses
where a commit of the diff is not one of the pull request's. Amend a
commit in the middle of the branch, and the diffs of the commits below
it still open; the ones above it sit on hashes the remote doesn't have.
A commit from before the branch, in a main branch already, is refused
too, and so is a range of commits that reaches across the head of a
branch in a stack, since its commits are in two pull requests. Whether a
commit is pushed is known only for the upstream of the listed branch.
For a branch lower in a stack, that is right as long as the branches of
the stack are pushed together.

Only GitHub pull requests are known, since that is where the pull
request data comes from. The whole path can't be exercised headlessly:
no pull request reaches the model without a GitHub token, so the test
covers where the command is offered and the three reasons it refuses.
The URL is unit-tested instead, both the anchor of a line and the way
the commits are named, and so is the choice of a branch in a stack.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 12:05:39 +02:00
Stefan HallerandClaude Opus 5 d8cb217bad Ask for the panel beneath the focused main view in one place
Two questions the pane answers from the panel beneath it each reach for
it themselves, guard included. Opening a line in a pull request needs it
twice more, for the branch and for the commit.

Extract sidePanelBeneath, which is also where the guard against asking
for the panel beneath an off-stack pane now belongs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-05 12:05:39 +02:00
Stefan HallerandClaude Opus 5 2c5629aac9 Name a diff line's file in the repo's terms in one place
A diff line carries the absolute path of its file, and both panels
acting on such a line turn it into the repo-relative one git speaks
themselves. Opening a line in a pull request needs that path too, to
name the file to GitHub by it.

Extract repoRelativePath, and have both panels use it. The files panel
gains the check for a path outside the repo that the other one had; a
path it used to pass on matches no file of the working tree either.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-05 12:05:39 +02:00
Stefan HallerandClaude Opus 5 1228076501 Ask one place whether a branch has a pull request
Two panels offer a branch's pull request today, and each looks it up in
the model's map itself and builds the same disabled reason from the same
string. The focused main view is about to offer a line of the diff in
that pull request, which would make three.

Put the lookup and the disabled reason on the host helper, beside the
pull request URL it already builds, and have both panels ask it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-05 12:05:39 +02:00
Stefan Haller 150dfadf9b Acknowledge editor clicks in the diff
Non-suspending graphical editors can take a moment to reach the
foreground, leaving a modified click with no visible response. Briefly
reverse the clicked row's selection bar after launching the editor so
the registered action is apparent.

Arm the flash only for a resolved diff row, keep newer clicks safe from
stale timers, and rely on suspension to clear the transient state before
terminal editors take over.
2026-10-05 11:55:14 +02:00
Stefan Haller 6990c06880 Open a clicked diff line in the editor
Clickable renderer gutters are small and unavailable on some diff rows.
Make the whole row an editor target without changing focus or selection,
including while a popup is focused.

Use both Alt and Shift because terminal mouse protocols do not deliver
either modifier consistently across Ghostty, iTerm2, and VS Code.
2026-10-05 11:55:14 +02:00