parseAheadBehindForEachRefOutput dropped fields it couldn't parse, which
left the remaining ones of that line pointing at the wrong bases. The
caller that picks the closest base doesn't care, but sorting refs by
ancestry has to know which base a pair of numbers belongs to. Return an
entry per base and mark the ones that were malformed, as the function's
comment has claimed all along.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The function always asked about every ref under refs/heads. Sorting refs
by ancestry needs the ahead-behind values of a handful of named refs, and
for remote branches those live under refs/remotes, so take the patterns
as an argument. The bases can be commit hashes as well as ref names.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Building and parsing `git for-each-ref --format=%(ahead-behind:<base>)`
is not specific to loading local branches; the commits below use it to
sort refs by ancestry, from the remote branch loader too. Give these
helpers and their tests a file of their own.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pad the date of every line to the width that the oldest entry in the
reflog asks for, so that the column keeps its width as the user scrolls.
This is the same treatment the commits panel's date column gets, with the
same limits; getReservedColumnWidths spells them out.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The reflog panel renders only the lines that are on screen too, so the
expanded panel's date column is only as wide as gui.shortTimeFormat while
nothing but entries from today is visible, and the message of every entry
jumps to the left. A reflog reaches back in time, so scrolling crosses
that boundary soon enough.
GetReflogCommitListDisplayStrings had no tests at all, so cover the two
shapes it can return along the way.
This file is deliberately left un-gofumpt'd: gofumpt indents the body of
the commented-out block by a tab, which would fill the diff of the commit
that swaps the two blocks with whitespace-only changes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The reflog commits context slices the visible lines out itself and passes
only those, so the function has no way to look at the rest of the list. A
following commit needs the oldest entry to work out how much width the
date column needs. Take the whole list along with the range to render, the
way GetCommitListDisplayStrings already does.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
During an interactive rebase, scrolling down far enough that the pending
todos leave the screen makes the author and subject of every commit jump
to the left; scrolling back up makes them jump back. During a bisect the
column that holds "<-- current" and "?" comes and goes the same way, and
in the expanded commits panel the date column is only as wide as
gui.shortTimeFormat while nothing but today's commits is on screen.
A column is as wide as the widest string in it, and a column whose strings
are all empty is dropped altogether. The commits and sub-commits panels
render only the lines that are on screen, so those widths come from the
visible lines alone and follow the scroll position.
Pad the hash, bisect, action and date cell of every line to the width that
all the commits in the list need. A padded cell is no longer empty, so its
column is never dropped, and the column is already as wide as the whole
list needs, so its width no longer depends on what is visible.
The date column is the exception. Formatting the date of every commit on
every render costs too much, so it measures the oldest commit only. That
covers the conventional time formats; getReservedColumnWidths says what it
misses, and why it can never reserve width that no commit asks for.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The commits panel renders only the lines that are on screen, so the width
of a column follows what happens to be visible, and a column all of whose
visible lines are empty disappears altogether. Scrolling past the pending
rebase todos takes the action column away with them, and everything to its
right jumps to the left.
Cover the same problem for two more columns: the hash column, which goes
away while only hashless todos such as "update-ref" are on screen, and the
date column in the expanded commits panel, which is as narrow as the time
format while only commits from today are on screen.
This file is deliberately left un-gofumpt'd: gofumpt indents the body of
the commented-out block by a tab, which would fill the diff of the commit
that swaps the two blocks with whitespace-only changes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
formatExpected runs the expected output through strings.TrimSpace to get
rid of the newlines that the raw string literal starts and ends with, but
that also eats the indentation of the first line. A scenario whose first
line starts with an empty column can't express what it expects, and a
later commit adds two of those. Trim the newlines only.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A caller that asks for a padding of zero gets its string back unchanged,
but only after WithPadding has measured it, and measuring means a
Decolorise lookup and a width scan over the result. A later commit pads
four cells of every commit in the list, and asks for a padding of zero
for all four whenever no rebase or bisect is in progress and the date
column is hidden, so make that case cost nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
These three cells are built and styled in one step, so their width can
only be measured by stripping the styling off again. A later commit needs
those widths to reserve space for the columns they go into. Pull the text
out into getHashText, getActionText and a getBisectStatusText that no
longer styles what it returns, and apply the styling at the call site.
A commit with no hash, such as a "break" or "update-ref" todo, now gets an
empty hash cell instead of one holding nothing but colour codes. Nothing
can see the difference: both render as the same number of blank columns.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Decolorise stores its result in a cache keyed by the string it was given,
but the lookup treats an empty result as a miss, so a string that
decolorises to nothing is recomputed on every call. Recomputing it means
compiling two regexes, and that costs 4534ns against 24.7ns for a string
the cache does answer for.
Empty cells are everywhere in the list panels, and RenderDisplayStrings
measures every cell twice, once to work out the column widths and once to
pad it. A commits panel showing 50 lines with one empty column spends
about 0.45ms of every render on this.
Read the cache with the two-value form, so that an empty result counts as
an answer, and compile the two regexes once at package level.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
The recent repos helper is about to run a git command against each repo
in the menu, and it needs the same treatment of GIT_DIR and GIT_WORK_TREE
that everything else pointed at another repo gets.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
TruncateWithEllipsis cuts off the end of a string, which is the wrong
end for a path: what distinguishes two paths is often the last segment,
and it is the one thing the reader wants to see. Keep both ends and put
the ellipsis between them.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
Running the tests in an exported source tarball fails with "must run in
lazy project folder or child folder". GetLazyRootDirectory searches the
working directory and its parents for a .git directory, and a tarball
doesn't have one. This has always affected the integration tests; since
34da956f5d a unit test calls the function too, so now even
`go test ./... -short` fails.
Search for the go.mod file that declares lazygit's module instead. It
ships in tarballs, and there is exactly one of it per source tree.
Put the function in our own pkg/utils rather than change lazycore's; the
criterion is specific to lazygit, and I don't feel like making a change
to lazycore.
Return an error rather than call log.Fatal, and report it from the two
callers that run under `go test`. In a test binary, log.Fatal exits
without attributing the failure to any test. That is the failure mode
34da956f5d set out to remove. The remaining callers are development
tools that have nothing useful to do without the root directory; they
keep exiting, now through MustFindLazygitRootDirectory.
Also stop the search at the root of the file system rather than at "/".
On Windows the old loop walks up to "C:\" and then spins there forever.
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>
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>
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>
A view's search positions were worked out again from every write, and
each of those walks the whole view. Content arrives a line at a time, so
rendering into a searched view costs a walk per line. Streaming 2000
lines takes 565ms, where the same render into an unsearched view takes
about 10ms.
Mark the positions stale on a write instead, and work them out where they
are read: when the view is drawn, when a key steps through the matches,
when the status is asked for. That is at most once a frame, and the same
2000 lines now take 8ms.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Search a view, step to the last match, then have the view re-rendered
with fewer matches in it, and the status reads "3 of 1". The positions
are worked out again whenever the content changes, but the index into
them stays where it was. Stepping on from there indexes the positions
out of range and panics.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ReadToEnd reads the rest of a view's content on the render task's own
goroutine, and calls back once it has. Nothing held a task for that, so
lazygit counted as idle from the moment the caller returned until the
callback ran. The search prompt in the focused main view opens from such
a callback, so an integration test takes the idle report as its cue to
carry on, and presses its next key while the prompt is not open yet.
Hold the task in ReadToEnd rather than in the caller, so that every
caller is covered (see docs/dev/Busy.md).
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
A caller of ReadLines or ReadToEnd is told that the content it asked for
has been read by the request's Then being called. A task that reaches the
end of its input answers the requests still queued behind the one it was
serving, but a task that is stopped drops them, and their callers wait
for a callback that never comes. Pressing "/" in the focused main view
opens the search prompt from such a callback, so if a re-render replaces
the task at that moment the prompt never opens.
Answering them as the read loop ends would leave a request handed over
after that point unanswered, and there is a window for one. A caller
reads the channel to send on, and can reach the send itself only once the
loop has gone. So hand requests over through a queue instead. Asking
whether a task is there and giving it the request are one step, as are
taking the task away and handing back what it never answered; a request
made in between goes back to the caller to answer.
The queue is unbounded rather than a fixed-size channel, for the reasons
gocui's userEventQueue is. Requests are handed over from the UI thread,
where a blocking send would deadlock against the task waiting to be let
go, and a fixed channel that fills up leaves only blocking, dropping,
reordering or panicking to choose between.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ask a view buffer manager to read to the end of its content twice over,
then stop the task before it has served either request, as a re-render
replacing it does. Only the request it had already picked up is answered;
the one still queued behind it is dropped, and its caller waits for a
callback that never comes.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
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>
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>
Toggling whitespace re-focuses the side panel to re-render the diff, which
also re-derives that panel's highlight — as though the panel had the focus,
which it doesn't.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
A refresh only re-derives the highlight of the view that has the focus, so
a list whose contents change while the user is somewhere else keeps the
selection it had: none for a list that just got its first item, and one
over nothing for a list that just lost its last.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
Nothing said that a context leaving the stack takes its selection with it,
which the work coming up is about to make the rule for every view. Two
places already depend on it and are held together by hand: 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.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The selected line of a view says nothing about whether a selection is drawn
over it, or which of the two ways it is drawn in, and those are what the
tests coming up are about.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>