Commit graph

3353 commits

Author SHA1 Message Date
Stefan Haller 8eebb63b6a [SQUASHED] diff-file-menu 2026-09-14 13:50:03 +02:00
Stefan Haller 3868a9407b Ask git for the branch when the HEAD file doesn't hold it
Reading HEAD is worth keeping for the repos it can answer for, because
the menu opens on a keystroke and asking git costs a process per entry.
So go to git only for the placeholder, and for anything else the read
can't make sense of. That last part also gets the menu an answer for
layouts we don't know about yet, where it used to give up and say
"Branch unknown".

Git needs two commands to cover every repo. `git symbolic-ref` names the
branch even when it has no commit yet; `git rev-parse` resolves a
detached HEAD, and fails on a branch without a commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 10:49:29 +02:00
Stefan Haller 7b5c6f4e12 Demonstrate that the recent repos menu shows ".invalid" for a reftable repo
A repo that keeps its refs in a reftable shows ".invalid" in the branch
column. Git stores the real HEAD in a binary table there and leaves
"ref: refs/heads/.invalid" in the HEAD file, so that anything still
reading the HEAD file fails loudly instead of getting a stale answer.
We read that file and take the placeholder for a branch name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 10:49:29 +02:00
Stefan Haller 93713b5401 Resolve a relative gitdir against the repo it belongs to
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 10:49:29 +02:00
Stefan Haller 00c0c4c061 Demonstrate that the recent repos menu can't read a submodule's branch
The menu shows "Branch unknown" for every submodule, whatever it has
checked out. A submodule has no .git directory; its .git is a file
naming the directory, and for a submodule git writes that name relative
to the submodule. We hand it to os.ReadFile unchanged, so it resolves
against lazygit's own working directory and the read fails. A worktree
created with --relative-paths (or with worktree.useRelativePaths set)
gets a relative name too, and fails the same way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 10:49:29 +02:00
Stefan Haller 9e542ff6e0 Show "HEAD detached at <hash>" in the recent repos menu
A repo with no branch checked out puts a bare short hash in the branch
column, where it reads as a branch name. Spell it out the way the
worktrees panel already does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 10:18:21 +02:00
Stefan Haller a3e940e20e Pull the reading of a repo's HEAD out of getCurrentBranch
The next commits change what the recent repos menu shows for a repo
without a branch, and teach it about layouts whose HEAD it reads wrongly
today. Give the reading a function of its own first, one that reports
what it found rather than what to display. This keeps the later changes
apart from each other, and it lets the tests call the reading directly
instead of going through a ReposHelper.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 10:16:16 +02:00
Stefan Haller 8d0f81cc15 Make the worktrees panel's detached-head text translatable
The recent repos menu is about to show the same text for a repo that has
no branch checked out. Hardcoding the English a second time would leave
translators with one of the two copies, so give the text a translation
key and take it from there in both places.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 10:13:39 +02:00
Stefan Haller 3452d41fe2 Truncate the path column of the recent repos menu too
The path column is the last one, so nothing pushes it aside; instead it
runs off the right edge of the menu itself, and the reader has no way of
telling that there is more to it. Give it a maximum width as well, sized
so that the three columns and the spaces between them fill the menu at
its widest.

A path loses its middle rather than its end, because the directory that
immediately contains the repo says more about where it is than the root
of the tree does. The whole path, with the home directory still
abbreviated, joins the names in the tooltip when it doesn't fit.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 09:23:26 +02:00
Stefan Haller 49c267d0da Give the maximum width of a menu a name
The recent repos menu is about to size its columns to fit into a menu of
that width.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 09:23:26 +02:00
Stefan Haller fd5c703e1e Truncate the repo and branch names in the recent repos menu
Each column of a menu is padded to the width of its widest entry, so a
single long name in the first two columns of the recent repos menu
pushes the path column off the right edge for every entry. Anyone whose
worktree directories are named after their branches hits this: with a
90 column menu and one 42 character name, the path column starts at
column 86 of 88.

Truncate both names to 30 characters, and put the ones that got
truncated into the item's tooltip, so that the full text is still on
screen for the selected entry. Filtering keeps matching the full names
and the full path, which the columns no longer show in their entirety.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 09:23:26 +02:00
Stefan Haller b11651820a Show the containing directory instead of the full path in the recent repos menu
The third column of the recent repos menu spells out the full path of
each repo. That repeats the directory name which the first column
already shows, and it writes out the home directory in full. Both are
wasted width in a menu that is limited to 90 columns; the path column is
the first thing to run off the right edge, and users who don't know that
'L' scrolls the menu horizontally never see it at all.

Show the directory that contains the repo instead, with the home
directory abbreviated to '~'. For a list of 106 recent repos this takes
the column from 111 characters down to 97 at its longest, and from 47
down to 28 in the median.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 09:23:26 +02:00
Stefan Haller b37018a936 Collect the current branches of the recent repos in a slice
Nothing needs to look up the branch of a repo by its path, so a slice
indexed like the list of paths does the job, and its elements are plain
strings instead of the values of type "any" that a sync.Map hands back.
The next commits measure and truncate the branch name, which needs a
string.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 09:23:26 +02:00
Stefan Haller 7253a45c21 Let a popup panel have an odd width
The left and the right edge of a popup panel were each derived by
halving the panel's width, so a panel that asked for an odd width lost a
column and came out one column left of centre. Resizing the window then
moved the panel's right edge only every other column, and left a gap of
one column between the panel and where it should end half of the time.

Derive the right edge from the left one and the width instead, the way
the branch above already does it for a popup that has a parent. This
also means that a panel of an odd width is now as wide as the width its
text was wrapped to.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 09:23:26 +02:00
Stefan Haller d0fa50d4ef Give popup panels their full width whenever the window has room for it
The width of a popup panel was a ratio of the window's width, capped at
the panel's maximum. The ratio never did anything for confirmations and
prompts, whose minimum and maximum widths are both 80; they always came
out at 80, or at the window width if that was narrower. For menus and
the commit message editor it meant that the window had to be 158 columns
wide before either of them reached its maximum width, and that below 140
columns they were 80 columns wide. This is narrower than the window has
room for, and too narrow for the three columns of the recent repos menu.

Give every panel the width it asks for as long as it fits, and shrink it
only when the window leaves no choice. A panel keeps a margin of three
columns on either side while it can afford to, so that it doesn't sit
flush against the sides of the window as soon as the window gets a
little narrow.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 09:23:26 +02:00
Stefan Haller 114d3a5a25 Render the focused main view again while it is being searched
A refresh left the focused main view alone while a search was on, so the
diff on screen stayed as it was however much the working tree had moved
on underneath it. The search could not cope with the content changing
under it, and leaving the content alone was the way around that.

It can cope now. The positions are worked out again from whatever the
view holds, and the status with them, so render it like any other.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-05 15:09:52 +02:00
Stefan Haller 811b3fbdd1 Show the search status of what a re-rendered view now holds
Rendering a view's content again while a search is on leaves the "x of y"
describing the content that has just been replaced. The status is worked
out when the search is typed and again when a key steps through the
matches, and a render is neither. Change the diff context size while
searching the focused main view, and the count stays as it was, however
many matches the wider context brought in or took away.

Run the search again over the new content once the render has finished
putting it there.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-05 15:09:52 +02:00
Stefan Haller 04a7ae3ec8 Read a view that is being searched to the end when it renders again
Opening the search prompt reads the whole of the view's content, so that
the search counts every match in it. Rendering the content again reads
only as much as the scrollbar needs, so the matches below that point are
lost. The "x of y" drops to what the shortened content holds, and grows
again as the user scrolls far enough to load more.

Read to the end while a search is on, the way opening the prompt does.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-05 15:09:52 +02:00
Stefan Haller d2dc38ee87 Drop the highlight fixups the context stack now makes unnecessary
Two places nudged the flags because nothing else would: switching repos,
where the view focused in the repo being left is not the one focused in the
repo being entered, and tabbing from the suggestions list back to the
prompt, which replaces the top of the stack rather than popping it, so the
suggestions context never hears that it lost the focus. Both are just a
context leaving the stack now.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 21:19:04 +02:00
Stefan Haller ec3f681ebf Derive the selection highlight from the context stack
A view drew a selection because something told it to, from four places on
three different schedules: a context being focused, a context losing focus,
a context being activated over another one, and a list being re-rendered.
Whether the flags ended up describing the state of the app depended on
which of those had run last, and the last one to run was often none of
them: a refresh only re-focuses the view that has the focus, so a list
whose contents changed underneath an unfocused panel kept whichever
highlight it happened to have.

Derive both flags instead, in one place, from the two things they mean: a
view shows a selection while its context is on the stack and has something
to select, and the context the user is in shows an active one where the
ones behind it show inactive ones. Nothing else needs to say anything about
highlighting, so nothing else can leave a view saying something untrue
about where the focus is.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 21:19:04 +02:00
Stefan Haller d1707d5dd2 Re-render the main view without re-focusing the panel beneath it
Toggling whitespace needs the panel beneath to render its diff again, which
is what HandleRenderToMain is for; HandleFocus does that and also everything
else that belongs to a panel gaining the focus, which this panel already has
or, when the focus is in the main view, does not want. Re-selecting its
current item is harmless, but re-deriving its highlight as a focused panel's
is not: the selection turns bright while the user is somewhere else.

Changing the context size and switching diff renderers already ask for a
re-render this way.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 21:19:04 +02:00
Stefan Haller 4b391acec0 Ask each context whether it has content to select
Whether a view draws a selection is about to be derived in one place from
the context stack, which needs to ask any context — list or not — whether
there is something for a selection to sit on. Name the existing flag after
that question, and let a list context answer it from its length.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 21:19:04 +02:00
Stefan Haller e0927d4faf Validate the context names in the "context" field of custom commands
If a custom command's "context" field contains a context name that
doesn't exist, lazygit panics when building the keybindings. This could
happen either because of a typo, or because a context is removed or
renamed in a later version. Prevent the panic by validating those names
at config load time, and rejecting the config as invalid there, like we
do for other config errors.

The gui package owns the list, but can't be imported from here, so it is
mirrored and a test over there ensures the copies stay in sync.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 21:07:32 +02:00
Stefan Haller 8aa57264fa Point the note about the menu's essential keys at what it means
There is no `reservedKeys` any more; the list of keys that menu items must
not shadow is `essentialKeys` in the function that creates the menu.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 20:41:22 +02:00
Stefan Haller 4116a15dae Filter the recent repositories menu as you type
Picking a repository out of that list is the other place where the menu is
a list to search rather than a set of commands, and its items have no keys
that typing could clash with.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 20:41:22 +02:00
Stefan Haller bb7e74968b Drop the menu-specific wording for the filter prompt
The only menu that ever asked for it was the keybindings menu, which now
filters as you type and doesn't use the prompt at all. That leaves every
filterable context with the same prompt, so the whole hook can go, and
with it the two implementations that only existed to satisfy it.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 20:41:22 +02:00
Stefan Haller fb761892aa Filter the keybindings menu as you type
Looking up a keybinding is a search, so the menu that lists them is the
one that most wants this. Its items do have keys, but only as a reminder
of what they do outside the menu, so nothing is lost by not binding them.

The prompt in front of the input field says what '@' does. It only ever
showed up while the user was typing in the search prompt, so it could
afford to be wordy; on a row that is on screen for as long as the menu
is, it can't.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 20:41:22 +02:00
Stefan Haller 68355862c1 Add test helpers for a menu's filter row
Its footer, the hint in the menu's subtitle, where the row sits in
relation to the menu and the tooltip, and whether the text cursor is
showing are all things the tests for it need to look at.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 20:41:22 +02:00
Stefan Haller 2048c7f0a6 Keep a filtering menu navigable whatever the keybindings are
The keys for paging through a menu are ',' and '.' by default, and there
is no non-printable alternative for them, so a menu that filters as you
type would lose paging altogether as soon as the user typed anything. The
same goes for confirming and cancelling if those keys are configured as
printable ones.

So bind the physical keys for all of it, on top of whatever is configured,
and only where they aren't the configured keys anyway.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 20:41:22 +02:00
Stefan Haller fe9b990c4c Filter a menu by typing into it
The filter input is where the keyboard points for as long as such a menu
is open, so that the first printable key can go straight into it. The menu
still gets every key the input doesn't take, because the input view is
embedded in the menu view, and the two are drawn as one focused panel.

Which keys the input takes changes once there is a filter: until then
printable keys still drive the menu, so that the configured navigation
keys work as usual, and afterwards they are all filter text. A menu item's
own keys are never bound in such a menu, because typing one has to reach
the filter rather than execute the item.

Escape gives up the filter and leaves the menu open; the next one closes
it. The filter prompt behind '/' is gone from these menus: the row already
does that job, and a second filter would only be confusing.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 20:41:22 +02:00
Stefan Haller 1fb5f87d05 Lay out the filter row of a menu that filters as you type
The row is reserved for as long as such a menu is open, even while it is
still hidden, so that it can appear without moving the menu. That costs
two rows of the popup, which is why the screen has to be a little taller
before a menu is worth showing at all.

The prompt in front of the input field is dropped when the row gets too
narrow to type in, and the keybindings menu says what '@' does when it
still fits.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 20:41:22 +02:00
Stefan Haller c80035d7ee Add the views for a menu's filter row
Nothing shows or positions them yet. The row is two views because the
input field has to start after the "Filter:" prompt, and a gocui view is
a rectangle: the frame view draws the row and the prompt, the field sits
inside it.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 20:41:22 +02:00
Stefan Haller f1b0347111 Allow a list to render its footer elsewhere
The footer is drawn on the bottom border of the list's view, which is not
always a free row: a panel that puts something else below the list shares
that border with it, and has to render the footer there instead.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 20:41:22 +02:00
Stefan Haller a9fe055af4 Extract applying a filter to a context
A filter can come from somewhere other than the search prompt: a menu
that filters as you type has its own input field, and needs to apply what
is typed there without going through the prompt's state.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 20:41:22 +02:00
Stefan Haller 45d68bccc2 Treat the views of a popup panel as a group when clicking
The check was a single set of view names, so it also let a click move
between two different panels, e.g. from the prompt to the commit message.
List the panels instead, and require both views to be in the same one.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 20:41:22 +02:00
Stefan Haller c840013ca3 Remove error return value from functions that always return nil
Originally I thought we'd benefit from this change in this branch; turns
out that we didn't after all, because we changed the approach, but it's
a nice cleanup anyway, so we include it here.
2026-08-31 20:41:22 +02:00
Stefan Haller 3914755c98
Fix visual glitches when moving a rebase todo
This fixes two problems:
1. the list selection updated before the list content did, which caused
a bit of a wobble effect in the list
2. the main view would first update to a different commit's diff and
then back to the one that is being moved, resulting in a very ugly
flicker especially when moving the todo multiple times with auto repeat
2026-08-29 17:25:59 +02:00
Stefan Haller d37f901ac1
Remove PostRefreshUpdateKeepingScrollPosition
Now that we have PostRefreshUpdateWithOptions there's no reason to offer
a bespoke method for setting one particular option.
2026-08-29 17:25:59 +02:00
Stefan Haller fa531dc518
Add PostRefreshUpdateWithOptions 2026-08-29 17:25:59 +02:00
Stefan Haller fd1b229f93
Cleanup: remove CommitSelection option when refreshing rebase todos
This option has no effect, it isn't respected by refreshRebaseCommits,
so it looks misleading to include it here.
2026-08-29 17:25:59 +02:00
Stefan Haller 530d773052 Allow filtering worktrees by branch name in Worktrees pane
Co-authored-by: phanirithvij <phanirithvij2000@gmail.com>
2026-08-29 17:02:44 +02:00
Stefan Haller c3027caf6b Cleanup: use lowercase for function parameters 2026-08-29 17:02:07 +02:00
Stefan Haller 35753fd042 Fix scrolling back when dragging a commit
During auto-scrolling, turn off the automatic
scroll-to-make-the-selected-item-visible functionality of
PostRefreshUpdate.
2026-08-27 08:22:55 +02:00
Stefan Haller d7401559f0 Collapse the paths of a moved directory into the directory itself
A commit that moves an entire package elsewhere renames hundreds of
files, and passing every one of their old paths can push the command past
the length limit the OS imposes (~32k characters on Windows). Their
common parent directory does just as well whenever everything it holds
ends up in the diff anyway.

Deciding that needs to consider every file of the diff, not only those on
display, so the paths are now derived from the model rather than from the
tree; a status filter must not make a directory look emptier than it is.
2026-08-17 09:32:30 +02:00
Stefan Haller bee03d3b98 Show renames when diffing a directory that a file was moved into or out of
Git limits its tree diff by the pathspec before it looks for renames, so
a directory only ever gets one end of a rename whose other end is outside
it. Nothing is left to pair up, and the file turns into an addition or a
deletion that the commit doesn't contain.

Pass the other end along with the directory. This is bounded by the
number of renames that cross the directory's boundary, so it costs
nothing at all for the vast majority of commits.
2026-08-17 09:32:30 +02:00
Stefan Haller 40cb4bb24d Extract a single helper for the paths a node's diff is limited to
The files and commit files panels each had their own copy of this, one of
which used to be missing the previous path of a rename. Growing them
apart again is the last thing we want, since the next commit needs to
teach both of them about renames that cross a directory boundary.

The files panel version only returned paths for the filtered case, and
left it to WorktreeFileDiffCmdObj to derive the rest from the node; now
that all callers pass the paths in, that command doesn't need to know
about renames at all.
2026-08-17 09:32:30 +02:00
Stefan Haller 2c9187acb9 Pass the previous path when diffing a filtered directory in the files panel
Restricting the diff to the files that a filter leaves visible drops the
delete-side entry of a staged rename, so git shows the file as an
addition instead. Its commit files counterpart already passes both paths;
this brings the files panel in line.
2026-08-17 09:32:30 +02:00
Stefan Haller 43b47d16dd Keep showing files whose conflicts have been resolved
When several files have conflicts, resolving one of them makes it vanish
from the files panel as soon as it is auto-staged, and it only comes back
once the last conflict is resolved and the filter turns off again. By
then it sits among all the other changed files of the merge, so it is
hard to find the ones whose resulting diff you still wanted to check.

So remember which files had conflicts while the conflicted-files filter
is on, and keep showing them once they are resolved. This is the general
solution that 39513d244d called for; that commit only helped for the
case of a single conflicted file.

The consequence is that the selection no longer moves on to the next
conflicted file when one is resolved: it stays on the file you just
resolved, which shows you its diff right away.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 16:45:50 +02:00
Stefan Haller c10bc3b697 Collect the paths of the files with conflicts, rather than only counting them
The next commit needs to know which files have conflicts, not just how
many of them there are.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 16:45:50 +02:00
Stefan Haller 486d8536f2 Preallocate arrays where the new linter version would warn about it 2026-08-16 16:35:11 +02:00