Commit Graph
3402 Commits
Author SHA1 Message Date
Stefan HallerandClaude Opus 5 503ca22d09 Generalize CanDoFastForwardMerge into IsAncestor
The function asks git whether HEAD is an ancestor of a ref, and its name
says what the one caller wants to know. A later commit asks the same
question about a branch that isn't checked out, so let the caller name
both refs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-27 11:00:36 +02:00
Stefan HallerandClaude Opus 5 3ff0fdbcc1 Dim the divergence of a branch whose upstream was rewritten
When somebody else rebases a branch and force-pushes it, our branch
shows up as diverged, say ↓5↑3, and that looks exactly like a branch
that carries three commits of our own. The two want different treatment.
The first one only has to be moved to its upstream, while the second one
needs a decision about what to do with those commits. The panel doesn't
tell them apart, so users have to look at each branch in turn to find
out which one they are dealing with.

Determine for every diverged branch whether it has commits of its own,
and dim its divergence when it doesn't. This runs on a worker, as it
takes a few git commands per diverged branch, and a branch keeps the
answer from the previous load while a new one is in flight, so that the
display doesn't flicker. A later commit teaches the fast-forward command
to act on such a branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-27 11:00:36 +02:00
Stefan HallerandClaude Opus 5 349bcc3691 Rename the loadBehindCounts parameter to loadExtraInfo
The flag says whether this load is the one that kicks off the values
determined in the background afterwards. A later commit adds a second of
those next to the behind-counts, so name the flag after what it controls
rather than after its only user so far.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-27 11:00:36 +02:00
Stefan HallerandClaude Fable 5.1 9a585bf11e Offer to push the branches stacked below the current one
After rebasing a stack of branches, every branch of the stack has to be
force-pushed, and so far the only way to do that in lazygit was to check
out each branch in turn and push it. For a stack of a dozen branches
that is a lot of work for a routine task.

When the current branch is pushed and there are branches below it in the
stack that have commits to push, show a menu that offers to push them
along with it. The menu lists the branches and how far each of them has
diverged from its remote branch, so that the user can see what is about
to happen. It doesn't show where each branch goes; the branches panel
doesn't show that for a normal push either. Pushing only the current
branch is the second entry. If any of the branches has diverged from its
remote branch, a single confirmation covers force-pushing all of them.

A branch is offered if its tip is a commit of the current branch that
isn't merged yet, it has an upstream that is stored locally, that
upstream is not gone, and it is ahead of its push destination. Branches
without an upstream are left out because a branch that was never pushed
can't be told apart from one that is meant to stay local. A branch that
is only behind is left out because pushing it would move the remote
branch back to an older commit; the lease doesn't catch that when the
remote-tracking branch is up to date.

Each branch is pushed to where `git push` would push it if it were
checked out, using the destination git reports in the %(push) field.
This honors push.default, remote.pushDefault and
branch.<name>.pushRemote without lazygit having to interpret them.
Branches going to the same remote are pushed in one command, with
--force-with-lease when the user confirmed force-pushing; the lease
checks each ref against its remote-tracking branch, so a coworker's
unseen push is still rejected. The current branch joins that command
when its remote-tracking branch is stored locally. Otherwise it needs a
plain push first, for example with --set-upstream, and that option
would apply to every refspec of a combined command; so in that case it
is pushed on its own as before, and the other branches follow in a
second command.

Pushing the current branch on its own still runs a bare `git push`, so
users who rely on push.default or remote.<name>.push for it see no
change. The push is non-atomic, as git defaults to; if one branch's
lease fails, the others still go through and the error names the one to
look at.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-27 08:21:02 +02:00
Stefan HallerandClaude Fable 5.1 bfb8c91b7b Split pushing the current branch into resolving its destination and pushing
Pushing the current branch has three cases: it has an upstream, it has
none but push.default is "current", or the user is prompted for one.
Each case ends in a push, and the first one also checks whether a force
push is needed. Let the three cases call a common callback instead, and
put the check and the push there. This makes it possible to run the same
three cases with a different callback, so that the branches stacked
below the current one can be pushed along with it.

The check returns false for a branch without an upstream, so the other
two cases behave as before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-27 08:21:02 +02:00
Stefan HallerandClaude Fable 5.1 c3b09cebad Add a function to find the branches stacked below a branch
lazygit will offer to push these along with the current branch, so that
a rebased stack can be pushed in one go. The commits panel already marks
the tips of stacked branches, and this uses the same criterion. A branch
is below the given one if its tip is one of the loaded commits of that
branch that isn't merged into a main branch. This needs no git command.
In whole-graph mode the commit list also contains commits of other
branches, but those are marked as merged, so they don't count.

Not used yet.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-27 08:21:02 +02:00
Stefan HallerandClaude Fable 5.1 6378618544 Generalize the push command to take a remote and a list of refspecs
PushOpts can only describe a push of the current branch. It takes the
branch's name and the upstream to push it to, and builds a single
refspec from them. Pushing several branches in one command needs one
refspec per branch, so let callers pass the refspecs themselves. The
sync controller, the only caller so far, builds the same single refspec
that PushCmdObj built before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-27 08:21:02 +02:00
Stefan HallerandClaude Opus 5.5 5196c4fab5 Let users turn off the bold text of the selected line
The text of the selected line is always bold. Some users don't like
this (#2304), but there is no way to turn it off. Setting
selectedLineBgColor doesn't help. Its attributes apply to the text too,
so it can add bold, but it can't take it away.

Add gui.theme.selectedLineFgColor for the text of the selected line, in
focused and unfocused views alike. Its attributes are added to those of
the text, and a color replaces the colors of the text. It is [bold] by
default, so nothing changes unless you set it; 'default' leaves the
text as it is.

Putting bold into the default of selectedLineBgColor instead wouldn't
work well. To turn it off, you would have to replace the whole list, and
lose the color that is computed from the terminal's background.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 08:16:06 +02:00
Stefan HallerandClaude Opus 5.5 ca950181e8 Pick the built-in theme defaults for the terminal's background
Some theme colors can't have one default that suits both dark and light
backgrounds. A gray background for the selected line of an inactive
view has to be lighter than the terminal's background if that is dark,
and darker if it is light.

Give such fields a default for each kind of background. Derive it from
the terminal's background color if the terminal tells us, so that it
keeps the same distance from the background however dark or light that
is, and takes on its tint. Otherwise, assume a black or white
background.

These defaults can't be the defaults of gui.theme. We would then have
to tell whether a value there came from the user or from the built-in
defaults, because only the user's value should win over a background
default. Keep them apart from the user config instead, and leave these
fields empty in the defaults of gui.theme, so that a value there always
comes from the user. Tests ensure that no field has both kinds of
defaults, and that both kinds of background set the same fields.

Start with inactiveViewSelectedLineBgColor: the background mixed with
30% white if it is dark (#4d4d4d on black), or with 15% black if it is
light (#d9d9d9 on white). A gray background shows where the selection
is in a view without the focus more clearly than the bold text we used
so far.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 08:16:06 +02:00
Stefan HallerandClaude Opus 5.5 34c86efc67 Show commits outside the bisect range in faint text
While bisecting, the hashes of the commits outside the bisect range are
black. On a dark gray background this makes them recede, but on a black
one they are invisible, and on a light one they stand out more than any
other hash.

Draw them in faint text instead. The terminal shades faint text toward
its background, whatever that is.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 08:16:06 +02:00
Stefan HallerandClaude Opus 5 679fe58e8e Add a dim text style
A later commit shows the hashes of commits outside the bisect range in a
dimmer variant of the default text color. A darker shade of a terminal
palette color can't be derived, because we don't know what the palette
is, so leave the shading to the terminal and use its faint attribute,
SGR 2. In gookit/color that attribute is called OpFuzzy.

Terminals that don't implement SGR 2 render the text in its normal color.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-27 08:16:06 +02:00
Stefan HallerandClaude Opus 5.5 0ca4fdefba Add gui.darkTheme and gui.lightTheme
A color that reads well on a dark background can be hard to read on a
light one, and the other way round. If you switch your terminal between
dark and light, there is often no single set of theme colors that works
for both (#4366).

Add two overrides of gui.theme, one for each kind of background.
gui.colorScheme, or else what the terminal tells us, decides which one
applies. A field that is set in the override replaces the one in
gui.theme. Author colors and branch color patterns are merged by entry
instead, so that an override doesn't have to repeat the entries it
doesn't change.

When the terminal's background changes, re-apply the theme and
re-render all views, not only the ones that show author colors.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 08:16:06 +02:00
Stefan HallerandClaude Opus 5.5 2d72387b19 Rename setColorScheme to applyTheme
Since gui.colorScheme exists, the name reads as if the function set that
config. That gets more confusing once gui.colorScheme decides which
theme the function applies.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 08:16:06 +02:00
Stefan HallerandClaude Opus 5.5 51002cc473 Move gui.authorColors and gui.branchColorPatterns into gui.theme
We are about to add overrides of gui.theme for dark and light
backgrounds. Author and branch colors need them too, because a color
that reads well on a dark background may be hard to read on a light
one. Move them into gui.theme, so that the overrides cover them without
a mechanism of their own.

The migration of gui.branchColors creates gui.branchColorPatterns, so it
now has to run before the moves.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 08:16:06 +02:00
Stefan HallerandClaude Opus 5.5 8e396228e9 Try branch color patterns in the order they are written
If several patterns in gui.branchColorPatterns match a branch, the color
it gets is picked at random, and it can change from one render to the
next. The patterns are kept in a Go map, and Go randomizes the order in
which a map is iterated.

Keep the patterns in a list instead, in the order in which they are
written, and let the first match win. If a repo's config file has
patterns too, put them in front of those of the global config file,
because they are more specific.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 08:16:06 +02:00
Stefan HallerandClaude Opus 5.5 573cd8a005 Migrate gui.branchColors to gui.branchColorPatterns
gui.branchColors has been deprecated in favor of gui.branchColorPatterns
since 0.44.0. We are about to move gui.branchColorPatterns into
gui.theme, and the deprecated key would have to move along with it.
Migrate it instead, so that we can remove it.

gui.branchColors matched its keys against the part of a branch name
before the first slash. The pattern ^<key>(/|$), with the key escaped,
matches the same branches.

If gui.branchColorPatterns is set, gui.branchColors has no effect; in
that case the migration removes it. This check is done per file. So if
the global config sets gui.branchColorPatterns and a repo config sets
only gui.branchColors, the repo's colors were ignored so far, and now
they apply.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 08:16:06 +02:00
Stefan HallerandClaude Opus 5.5 d38248d851 Tell diff renderers whether the terminal is dark or light
Renderers like delta and difftastic pick their colors for either a dark
or a light background, and they can't find out which one the terminal
has. Lazygit runs them with TERM=dumb, in a pty that doesn't answer
their queries. So the colors come from the config, and when the
terminal switches between dark and light, the diff keeps the ones it
has.

Add {{colorScheme}} to the commands of diff renderers. It is 'dark' or
'light', going by gui.colorScheme, or by the terminal if that is
'auto'. It can be passed to delta as --{{colorScheme}} and to
difftastic as --background={{colorScheme}}; other renderers can choose
between options with a template expression. When the terminal switches
between dark and light, render the diff again.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 08:09:45 +02:00
Stefan HallerandClaude Opus 5.5 2c91bd99d6 Move re-rendering the main view after a diff renderer change to a helper
The next commit needs to render the diff again when the terminal
switches between dark and light, with the same care not to replace
whatever else the main view might show.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 08:09:45 +02:00
Stefan HallerandClaude Opus 5.5 c8a4c8d392 Let the command of a diff renderer be a template
The command of a diff renderer can refer to values like the width it
renders at, as {{width}}. They are filled in by plain replacement, so a
command can't choose between options depending on them. The next commit
adds a value that needs this: whether the terminal is dark or light.
delta takes --dark or --light, but for other renderers the choice has to
be spelled out differently, for example as the name of a syntax theme.

Resolve the command as a Go template instead. The values become its
variables, so that {{if gt .width 160}} --side-by-side{{end}} works too.
To keep the existing commands working, a variable can still be written
without the leading dot.

A mistake in a template, such as a misspelled variable, now makes
resolving the command fail, instead of leaving the placeholder in it.
Check the commands when the config is loaded, by resolving each of them
with made-up values, so that the mistake shows up as an invalid config.
This also rejects a variable that the kind of renderer doesn't have,
such as {{columnWidth}} in the command of an external diff; until now,
it reached the renderer as it was.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 08:09:45 +02:00
Stefan HallerandClaude Opus 5.5 4ce8d77173 Resolve the commands of all kinds of diff renderers in one place
The getters for the stdin filter and the external diff command each
take the values they fill in as parameters of their own, and each
builds the placeholders for them. The next commit checks the commands
when the config is loaded, and for that it needs to resolve a command
whatever its kind. A value that both kinds can use comes after that.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 08:09:45 +02:00
Stefan HallerandClaude Opus 5.5 f51c4aaefc Pick the colors of authors for the terminal's background
The colors that lazygit derives from the names of authors are picked
for a dark background. On a light one, they are too pale to read;
against white, every author has a contrast ratio between 2.2:1 and
3.5:1.

Now that lazygit knows whether the terminal is dark or light, pick them
from a darker range of lightness when it is light. Against white, the
contrast ratio is now between 5.2:1 and 9.2:1, and against the
background of Solarized Light between 4.8:1 and 8.5:1. On a dark
background, nothing changes. If the terminal switches between dark and
light while lazygit is running, draw the commits again in the new
colors.

Add gui.colorScheme for terminals that don't tell us, and for anyone
who wants to override what they tell. It is 'auto' by default; 'dark'
and 'light' ignore what the terminal says.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 16:58:33 +02:00
Stefan HallerandClaude Opus 5.5 7282c21aa5 Derive author colors in a color space where lightness means brightness
If gui.authorColors names no color for an author, lazygit picks one
from a hash of their name, at an HSL lightness between 0.4 and 0.6.
HSL lightness says nothing about how bright a color looks, though. At
0.5 a yellow is glaring and a blue is nearly black, so whether an
author's initials can be read comes down to where their name happens
to hash to. Against a background of #1e1e1e, 45% of four thousand
names fall below a contrast ratio of 4.5:1 and 21% below 3:1, with the
worst at 1.46:1. Several issues have been raised about author colors
being too dark to read.

Use HSLuv instead. Its lightness tracks perceived brightness, so the
range can be narrow now that a number in it means something. Against
#1e1e1e, every author now lands between 4.7:1 and 7.7:1, so no name is
unlucky any more.

This changes the color of every author who isn't named in
gui.authorColors. On a light background, the new colors are uniformly
too pale, where before they were mostly too pale. The next commit gives
a light background a range of its own.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 12:17:21 +02:00
Stefan HallerandClaude Opus 5.5 d4f6e77d94 Add a tool that creates a repository of authors at the color extremes
If gui.authorColors names no color for an author, lazygit derives one
from a hash of their name. Checking whether these colors are readable
means looking at a repository, but the authors of a real one rarely
land near the edges of the range, so the worst cases go unseen.

Add cmd/author_colors_repo. It searches for names at the lowest and
highest lightness and saturation, at twelve hues, and commits once as
each of them. The names depend only on the hashing, so the same
repository shows the colors of any lazygit build and can be kept
around to compare them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 12:17:21 +02:00
Stefan HallerandClaude Opus 5.5 fcabbf517c Split an author's color into where the name lands and the color there
The next commit adds a tool that creates a repository of authors at
the edges of the range their colors are picked from. To find such
names, it needs to know where a name lands in the range, and it
shouldn't keep a copy of the hashing that could drift from this one.
Later commits test the colors, and need the colors themselves for that
rather than the styles made from them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 12:17:21 +02:00
Stefan HallerandClaude Opus 5.5 c06233073a Let author colors follow a change of gui.authorColors
When the config is reloaded, SetCustomAuthors replaces the styles of
authors, but the initials and names that were rendered with the old
styles stay cached. So do the pipes of the commit graph; each of them
carries the style of the author of the commit it starts at.

Drop these whenever the colors of authors change. The graph's cache
finds out by itself, by comparing a version number, so that nothing
has to remember to reset it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 12:17:21 +02:00
Stefan HallerandClaude Opus 5.5 46761ad2e5 Demonstrate that author colors don't follow a change of gui.authorColors
If gui.authorColors changes while lazygit is running, the authors that
are already on screen keep their old colors, both in the author column
and in the commit graph.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 12:17:21 +02:00
Stefan HallerandClaude Opus 5.5 771e75c8ab Keep the configured author styles apart from the derived ones
The styles from gui.authorColors and the ones derived from the names
of the other authors share a map, so the derived ones can't be dropped
without the configured ones. A later commit needs to drop the derived
ones on their own, when the terminal switches between dark and light.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 12:17:21 +02:00
Stefan HallerandClaude Opus 5.5 65efd3dde5 Detect whether the terminal is dark or light
Lazygit doesn't know whether the terminal it runs in has a dark or a
light background. The colors of authors, for example, have to suit one
or the other, and are too pale to read on a light background.

Ask the terminal. Many terminals answer CSI ? 996 n with whether they
are dark or light, and send the same report again whenever that changes
while mode 2031 is on. Terminals differ in what they base this on,
though. kitty goes by its background color, but Ghostty goes by the dark
or light mode of the operating system, even with a theme that doesn't
follow it. So also ask for the background color with OSC 11. If the
terminal answers that, the background decides, and a report only makes
us ask for the background again. Some terminals answer OSC 11 but send
no reports; ask these for the background again whenever the terminal
gains focus.

Turn mode 2031 off whenever lazygit hands the terminal to another
program, so that the program doesn't receive the reports as typed text.
For the same reason, first wait for the answers to any queries that are
still outstanding, but for no longer than half a second. Locally, the
answers take a few milliseconds at most, but over ssh they take a
network round trip, and focusing the terminal and then pressing a key
that starts an editor fits into that.

Leave out the terminals that tcell doesn't send its own queries to,
except for Terminal.app and WezTerm; these are only asked for the
background color.

This doesn't make startup any slower. tcell sends its own queries in
Screen.Init and waits for the terminal to answer them. Ours go out just
before, so their answers arrive before tcell stops waiting, and the
color scheme is known before the first layout.

tcell has no support for any of this, and drops the answers when it
parses its input. Instead of adding support to tcell, wrap the tty that
tcell reads from and watch the input as it goes by.

For now, lazygit only writes the color scheme to the debug log.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 12:17:21 +02:00
Stefan HallerandClaude Opus 5 7a0f475414 Let a render refresh the index again on Windows
Renders were kept from refreshing git's index because a pty-rendered
command on Windows is terminated at an arbitrary instruction when its
task stops, and one landing in the window where git holds index.lock to
write back refreshed stat information leaves that lock behind.

A render no longer runs in a pty there, and nothing kills it any more
either: the pipe to the renderer breaks, git's write fails, and git dies
through its own die path with its lock files cleaned up. So let the
refresh happen, and let renders heal stale stat info the way they do
everywhere else.

The teardown of a pseudoconsole took its reassurance about index.lock
from this, and renders were the reason it held. What still runs in a pty
there is a custom command asking for logWithPty and a command that may
be asked for a credential, so the claim no longer follows; drop it
rather than restate it for clients it was never about.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-25 11:31:59 +02:00
Stefan HallerandClaude Opus 5 087bdcd57f Render a diff without a pty on Windows
ConPTY doesn't carry a diff renderer's output to us as the renderer
wrote it. It parses the output into a screen buffer and re-encodes that
for the terminal side, and a sequence it can't represent there goes out
the moment it is parsed, separately from the text around it. The OSC
1717 records a renderer states its diff lines in therefore arrive
detached from the rows they describe, and the identity layer attributes
rows to the wrong diff line or to none.

Feed the renderer through a pipe there instead, so that its bytes reach
us unaltered. A stdin filter becomes a command of our own, since git
only invokes the one named by GIT_PAGER when it talks to a terminal; an
external diff renderer is git's own business either way and needs
nothing but the pipe.

Unix keeps the pty. A renderer reads the width to lay out to off it, so
taking it away would leave every configuration that doesn't name a
width rendering at whatever the renderer falls back to, and diff
renderers have worked on Unix far too long for that.
LAZYGIT_RENDER_WITHOUT_PTY asks for the piped path anyway, which is how
the integration tests cover it on a platform where they run at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-25 11:31:59 +02:00
Stefan HallerandClaude Fable 5.1 bf948ba782 Remove the LAZYGIT_COLUMNS environment variable
LAZYGIT_COLUMNS told a diff renderer script the width of the view at a
time when a render on Windows ran without a pty, so that the script had
nowhere else to read it from. It was documented for that case only, and
the documentation went when Windows gained a pty; it has not been
mentioned since.

COLUMNS now tells every command rendering into a view the same thing,
in the variable git and the common renderers already read, and the
{{width}} template variable covers a renderer that reads neither. The
value LAZYGIT_COLUMNS carried was also the width before the layout
pass, which is not always the width the view ends up with.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-25 11:31:59 +02:00
Stefan HallerandClaude Opus 5 a51952d826 Lay a diffstat out to the width of the view showing it
Tell a command that renders into a view how wide that view is, through
COLUMNS. git reads it in preference to the size of the terminal it is
talking to, so the diffstat now fills the view whether or not the render
has a terminal to offer.

A diff renderer that can't ask a terminal gets the width from it too;
difftastic, diff-so-fancy and delta all support COLUMNS, so we can stop
running git in a PTY and these will still work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-25 11:31:59 +02:00
Stefan HallerandClaude Fable 5.1 77c9418c89 Rename RunPtyTask to RunDiffRendererTask
The task runs a command with its output shown through the configured
diff renderer. A pty is one way of getting the output to the renderer,
and is about to become one of two, so the task can no longer be named
after it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-25 11:31:59 +02:00
Stefan HallerandClaude Opus 5 118d9e8e1f Separate setting up a render from running it in a pty
A render is about to have a second way of giving the diff renderer the
command's output, so the part that decides how it runs needs to be
apart from the part that does.

Move the setting up to newRenderTask, named for what it does now that a
pty is one of two ways of doing it. Leave the pty with the pair of
functions a task drives it by, and with naming the stdin filter to git
as its pager, since git only runs a pager when it talks to a terminal.
What a way needs to know about the render it runs travels as a
renderSpec.

Running the command plainly, with its output going straight into a
pipe, is a way of its own already, and newCmdTask ends as newRenderTask
does, by creating the task that reads the output into the view. Put the
plain way on the same seam as plainRender, and share the ending as
newTaskForRender.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-25 11:31:59 +02:00
Stefan HallerandClaude Opus 5 ec3427fdcc Let an external diff command be told the width it renders at
Like a stdin filter, an external diff program lays out its rendering to
the width it reads off the terminal, and a render through a pipe leaves
it nothing to read. difftastic in side-by-side mode is the case that
shows it.

Offer the width as the {{width}} template variable, the same name the
stdin filter command takes it by. Since difftastic supports the COLUMNS
variable, which we will set later in this branch, and I don't know of
any other external-diff renderer that doesn't, we don't document this.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-25 11:31:59 +02:00
Stefan HallerandClaude Opus 5 f496f17452 Hand an external diff command to git through the environment
An external diff command rides on the git command as a diff.external
config, which means it is fixed when the command is built. That is
before the layout pass, so the width the renderer is to lay out for
isn't known yet, and the command can't be told about it.

Pass it as GIT_EXTERNAL_DIFF from the render instead, where the width
is known and where a stdin filter is already handed to git the same
way. git ranks the variable exactly as it ranks the config, behind a
per-path diff driver from .gitattributes, so a repository that defines
one still gets it (verified on git 2.22.5 and 2.55). An empty command
means the user wants their own git config to apply, so leave the
variable unset for that; git takes it being set at all as an
instruction.

The renderer command leaves the git arguments, so it also leaves the key
that says which diff a render is of. Cycling between two external diff
renderers now keeps the view's place, the way cycling between two stdin
filters already does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-25 11:31:59 +02:00
Stefan HallerandClaude Opus 5 5a61f658a9 Let the branches refresh wait for the remotes
The pull request icons in the branches panel come from the remotes, so a
branches render that happens before the remotes are in the model shows no icons,
and the render that follows once the remotes land has to add them. The branches
refresh already waits for the worktrees for exactly this reason.

Wait for the remotes as well. They are read from the git config before their
branches are loaded, so the wait is over well before the branch load itself is
done, and the first render of the branch list shows the icons no matter which of
the two finishes first.

Start the remotes scope before the branches scope, so that an early return in
performRefresh can't leave the branches refresh waiting for a scope that was
never started. The worktrees scope goes first for the same reason.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 10:52:35 +02:00
Stefan HallerandClaude Opus 5 1b662a8ec3 Put the remotes in the model before loading their branches
When lazygit starts up in a repo with many remote branches, the pull request
icons in the branches panel show up a good while after the branch list itself,
even though the pull requests come from the cache file and are in the model
before the first render. In the repo where this showed up (5400 remote
branches) the icons were up to half a second late.

The icons are rendered from Model.PullRequestsMap, and that map is built from
the remotes' URLs, because a branch's upstream remote tells us which repo
owner's pull requests to look for. The map therefore stays empty until the
remotes are in the model, and the remotes refresh doesn't put them there until
it has also enumerated and sorted all remote branches. In a big repo that takes
hundreds of milliseconds; reading the remotes themselves takes ten.

Load the two separately, and put the remotes in the model as soon as they have
been read from the git config. The branches refresh then finds them there, and
the pull request icons are part of the first render of the branch list.

Carry over the branches of the remotes we already have in the model in that
first update, so that the remote branches, and the branch counts in the remotes
panel, stay in place until the fresh ones are loaded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 10:48:22 +02:00
Stefan HallerandClaude Fable 5.1 c7f62ea9b6 Keep the topmost directory selected when a compressed directory splits
When a single file is modified inside a nested directory, the file tree
compresses the whole chain of directories into one line, such as
"pkg/gui/controllers/helpers". Selecting that line shows the diff of the
entire working tree. When a second file is then modified in another
subdirectory of pkg/gui, the tree splits the line into "pkg/gui" with
"context" and "controllers/helpers" below it, and the refresh moves the
selection down to "controllers/helpers". Users who keep the top
directory selected to see the diff of everything lose that view and
have to move the cursor back up after every such refresh.

This happens because the selection is re-found by the node's own path,
and a compressed node's path is the deepest directory in its chain. The
node stood for every directory in that chain, though, and the topmost
piece of the split is the one that stays on the same line.

Match a compressed directory node against any new node that stands for
at least one of the same directories. The list is in depth-first order,
so the topmost piece wins and the cursor stays on its line. Files are
never compressed, so their handling doesn't change. The reverse case,
where two directories fold back into one compressed line, already
selected the merged line and still does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-20 16:04:57 +02:00
Stefan HallerandClaude Fable 5.1 7d576c3d30 Expand to the new half of a rename after rebuilding the tree
When the deletion of the selected file is staged and git then reports
it as the old half of a rename whose new half sits in a collapsed
directory, the selection is meant to move to the rename. With
gui.showRootItemInFileTree turned on, the directory stays collapsed and
the selection lands on it. With the option turned off, the directory
expands, but if another file in it sorts before the rename, that file
gets selected.

The loop that expands the directory runs before the tree is rebuilt and
works on the file list instead of on tree nodes. It compares the
rename's previous path, a user-facing path, against the selected node's
internal path, so the two never match while the root item is shown. The
path it hands to ExpandToPath is user-facing as well, so the directory
would stay collapsed either way. And because the loop runs before the
old node list is captured, expanding a directory above the selection
shifts that list, and the search for the new selection starts from
whatever node moved into the selected line.

Rebuild the tree first, capture the old node list before anything
expands, and then look for the rename among the leaves of the new tree.
ExpandToPath gets the leaf's own internal path, so the two kinds of
paths never need converting.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-20 16:03:49 +02:00
Stefan HallerandClaude Fable 5.1 b4b6a993ea Add a test for following a file into a rename in a collapsed directory
When the deletion of the selected file is staged and git then reports
it as the old half of a rename whose new half sits in a collapsed
directory, the selection is meant to move to the rename, expanding the
directory on the way. With gui.showRootItemInFileTree turned on, the
directory stays collapsed and the selection lands on it instead. With
the option turned off, the directory does expand, but if another file
in it sorts before the rename, that file gets selected.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-20 16:03:05 +02:00
Stefan HallerandClaude Fable 5.1 4766ce009e Compare user-facing paths when re-finding the selection after a refresh
When the selected rename splits into its two halves, e.g. because it was
unstaged, the selection is meant to move to the new half. With the
default setting of gui.showRootItemInFileTree, it lands on the file that
follows the rename in the list instead.

findNewSelectedIdx identifies a rename by the names of its two halves.
These are user-facing paths, without the "./" prefix that the root item
adds to every internal path, but they were compared against internal
paths. The comparison never matched while the root item was shown.

Compare user-facing paths throughout findNewSelectedIdx. Node.ID already
identifies list items by their user-facing path.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-20 15:32:33 +02:00
Stefan HallerandClaude Fable 5.1 77cbb532f2 Add a test for the selection after a selected rename splits in two
When a rename is selected in the files panel and then splits into its
two halves, e.g. because it was unstaged, the selection is meant to move
to the new half. This only works with gui.showRootItemInFileTree turned
off. With the default setting, the selection lands on the file that
follows the rename in the list instead.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-20 15:31:38 +02:00
Stefan HallerandClaude Opus 5 1d67763b7e Reserve the width the whole reflog needs for its date column
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>
2026-09-19 10:16:04 +02:00
Stefan HallerandClaude Opus 5 7cf7b51467 Demonstrate that the reflog's date column is as wide as the visible lines need
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>
2026-09-19 10:16:04 +02:00
Stefan HallerandClaude Opus 5 0720a05ae4 Hand the whole reflog to GetReflogCommitListDisplayStrings
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>
2026-09-19 10:16:04 +02:00
Stefan HallerandClaude Opus 5 410d7209b1 Reserve the width that all commits need for a commit list's columns
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>
2026-09-19 10:16:04 +02:00
Stefan HallerandClaude Opus 5 101424322a Demonstrate that a commit list's columns are as wide as the visible lines need
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>
2026-09-19 10:16:04 +02:00
Stefan HallerandClaude Opus 5 56c2ffb8f8 Don't strip leading spaces from a scenario's expected output
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>
2026-09-19 10:16:04 +02:00
Stefan HallerandClaude Opus 5 53e9c73034 Separate the text of a commit's hash, bisect and action cells from its styling
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>
2026-09-19 09:54:36 +02:00