AutoForwardBranches runs in the Then callback of the refresh after a
fetch. This is on the UI thread, so the git command for updating the
branches blocks the UI while it runs. This is fine for a single
update-ref, but the next commit adds a git status and a git merge for
each branch that is checked out in another worktree.
Keep reading the branches from the model on the UI thread, and move the
git work to a worker.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Lazygit shows local branches that have diverged from their upstream with
a yellow `↓3↑5`. There can be two reasons for diverging, and right now
it's impossible to tell which of these is the case: (1) you rebased the
branch locally (or rewrote its history), in which case you want to
force-push it, or (2) someone else rebased the branch, in which case you
want to pull it. (We'll ignore the case where both of these happened, in
which case you need to manually reconcile the work that each side did,
e.g. through cherry-picking; you want to avoid this situation.)
With this PR, lazygit distinguishes the two scenarios, and for (2) it
shows the `↓3↑5` in a dim yellow. What's more, you don't even have to
check out the branch to pull it; you can press `f` to reset it to its
upstream without checking it out, like you would fast-forward a branch
that is behind its upstream. In fact the command is still called
"Fast-forward", which is technically not quite correct, but it feels the
same to me. Fast-forwarding a diverged branch in this way is very useful
if an entire stack of branches was rebased remotely; you can simply
range-select all those branches and hit `f` to bring them up to date
with their upstreams.
The worktree loader associates a worktree that is mid-rebase or
mid-bisect with the branch involved, even though its HEAD is detached.
Fast-forwarding such a branch therefore ran the fast-forward in that
worktree. This moved the detached HEAD and put the upstream commits
into the rebase in progress, while the branch stayed where it was.
Remember in the worktree model when its branch comes from a rebase or
bisect, and refuse to fast-forward the branch in that case. Updating
only the ref is no option for a rebase; the rebase writes the branch
when it finishes.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
If a branch is being rebased in a worktree, pressing `f` on it moves
the detached HEAD of that worktree instead of the branch. The upstream
commits end up in the rebase in progress, and the branch itself stays
where it was. The same happens for a branch being bisected.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Branches that lazygit forwards to their upstream move by a direct ref
write, so no git command turns up in their reflog to explain it. Until
now the entry had no message either, leaving `git reflog <branch>` with
nothing but a blank line about it. Name lazygit and what it did, so that
a branch which moved on its own can be traced.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
After somebody else rebased and force-pushed a stack of branches, every
branch of the stack has to be brought back to its upstream, and pressing
`f` on them one by one is as tedious as the checking out and pulling it
replaces.
Let `f` work on a range selection. The upstream branches are fetched with
one `git fetch` per remote, and the branches that aren't checked out
anywhere move in a single `git update-ref` call. All the selected
branches are looked at before any of them is moved, so a branch that has
to be refused leaves the others alone rather than updating the stack
halfway.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
When somebody else rebases a branch and force-pushes it, the local
branch is left diverged from it, so `f` refuses to touch it. To get back
in sync, the branch has to be checked out and pulled, and for a stack of
branches that means doing it once per branch.
Such a branch has none of its own work in it, though: the commits it is
ahead by are the old versions of the ones that are now on the remote
branch. So when the branch has diverged and none of the commits it is
ahead by is ours, reset it to its upstream instead of refusing. This is
the same state that checking the branch out and pulling it would produce,
as rebase drops commits that the upstream already has.
A branch that isn't checked out anywhere is reset with the same
`git update-ref` that a fast-forward uses. One that is checked out
somewhere is reset with `git reset --keep`, and only when that worktree
has no changes to tracked files, so that the files under the user's feet
change no more than they have to. `--keep` also refuses to overwrite an
untracked file that the reset would bring in, which `--hard` would do
silently.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fast-forwarding a branch that isn't checked out ran a single
`git fetch <remote> refs/heads/<branch>:<branch>`, so git's own refusal
to move a local branch backwards served as the safety check. A later
commit widens the set of branches the command accepts, and for that
lazygit has to make that decision itself.
Fetch the remote branch on its own, updating only its remote-tracking
branch. Then ask git whether the branch is an ancestor of it, and only
then move the branch: with `git update-ref` when it isn't checked out
anywhere, guarded by the hash we knew it to have, and with
`git merge --ff-only` in its worktree when it is checked out there.
Keeping `git pull --ff-only` for the latter case would fetch a second
time.
Fast-forwarding a branch that isn't checked out in a worktree had no
integration test at all, so add one. The second new test covers a repo
that keeps no reflogs, because a later commit starts reading those and
this case has to keep working without them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Most of the logic of this controller belongs in a helper, and the next
commit reshapes it, so move it over unchanged first to keep that diff
about the change itself.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
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>
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>
A branch that has diverged from its upstream doesn't necessarily hold any
work of its own. If somebody else rewrote the remote branch and
force-pushed it, our branch is still at the commits it had before, and
every one of them was on the remote branch at some point. A later commit
offers to reset such a branch to its upstream, and for that it has to
tell this case apart from a branch that holds commits created here.
The reflog of the remote-tracking branch records the values it had before
it was rewritten, so a commit that was ever on the remote branch is
contained in its current value or in one of those. HasLocalOnlyCommits
asks git for a commit of the branch that none of them contains.
Two details of the reflog are worth knowing. The value the ref had before
its oldest entry is that entry's old value, and <ref>@{<number of
entries>} is the only way to name it. It doesn't exist when the oldest
entry is the one that created the ref, and asking for it then is an
error, not an empty result. Reflogs can also be missing altogether,
because core.logAllRefUpdates defaults to false in a bare repository.
Neither case must stop us from recognizing a branch that is strictly
behind its upstream, so the current value of the remote-tracking branch
is always used; missing previous values only make the answer more
conservative.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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 (i.e. what its
upstream branch's name is or on which remote it is stored); the branches
panel doesn't show that for a normal push either. If any of the branches
has diverged from its remote branch, a single confirmation covers
force-pushing all of them (listing exactly the ones that need
force-pushing).
This only covers branches that already have an upstream configured,
because a branch that was never pushed yet
can't be told apart from one that is meant to stay local. This means
that the very first time you want to push a newly created stack, you
still need to do it manually the old way. We can see if we want to
improve this somehow in the future, but repeatedly pushing a rebased
stack is the more frequent operation, and that's what we improve here.
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.
Some repositories are configured to reject pushes of more than x
branches at once; for those, the command fails with an error. We could
be smarter about this, detect the error, and fall back to pushing each
branch one by one, but I first want to see how many reports we get about
this before investing in the extra logic.
WrapViewLinesToWidth counted the characters of ANSI escape sequences as
if they were visible, so colored text wrapped earlier than the view
does, and the number of lines it reported was too high. In a narrow
terminal the divergence in the menu that offers to push a stack of
branches, colored yellow, ended up on a line of its own even though it
fitted. The tooltip of a disabled menu item, whose prefix is red, gets
its height from the same function.
Skip escape sequences when measuring the width, and never break a line
inside one. gocui wraps parsed cells and never sees escape sequences, so
this brings the two in line; the test's parity check now compares the
wrapped lines with the escape sequences stripped.
Only CSI sequences are recognized. Those are the color and style codes
that lazygit puts into view content; hyperlinks (OSC 8) only occur in
the main view, and gocui wraps that one itself.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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>
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>
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>
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>
To push a branch other than the checked-out one, lazygit has to name the
remote and the remote branch in the push command; a bare `git push` only
works for the current branch. The upstream isn't always the right
destination. In a triangular workflow the branch is pushed to a
different remote than it pulls from, and with push.default set to
"current" it goes to a branch of the same name whatever the upstream is
called.
Read the destination from git instead of working it out from the config.
The %(push) field of for-each-ref names the remote-tracking ref that a
push would update, taking push.default, remote.pushDefault and
branch.<name>.pushRemote into account. It is also the ref that the
push:track counts are computed against, so the force-push detection and
the destination agree. The field is available since git 2.5, well before
the oldest version lazygit supports.
Nothing uses the new fields yet.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Make it possible to override `gui.theme` colors for light or dark
backgrounds, by setting them in `gui.darkTheme` or `gui.lightTheme`,
respectively; this is often necessary when there isn't a single color
that looks good in both.
Use this to provide better defaults for the background color of a
selected line, and to add a color for the background of an inactive
selected line (used in unfocused views), which didn't have one by
default.
Also, provide a way to turn off the "bold" highlighting of the selected
line, which was hard-coded and couldn't be changed; there's a new config
setting `gui.theme.selectedLineFgColor` that is `[bold]` by default (so
existing behavior is unchanged), but can be set to `[default]` to remove
the bold. This addresses #2304.
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>
gocui draws the palette colors 0 to 7 on the selected line in their
bright variants. This was meant for the blue highlight on a dark
background, but in most dark palettes the bright variants are so close
to the normal ones that it's barely visible. Elsewhere it makes the text
harder to read. In many palettes, the bright variants are lighter, and
they don't work on the light highlight of a light background. Solarized
maps most of its bright colors to its grays, so colored text on the
selected line turns gray.
Draw the text of the selected line in the same colors as on the other
lines.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On a light background, the selected line is black text on the palette's
blue. With most palettes, this is hard to read. Light palettes make all
their colors dark enough to read as text on the light background, so
none of them works well as a background for text.
On a light background, mix 25% of #0064ff into the terminal's background
instead. On white, this gives #bfd8ff. The colored text on the selected
line then stays as readable as it is elsewhere.
On a dark background, keep the palette's blue. Dark palettes make it
dark enough to work as a background, and it keeps working on terminals
with only 8 colors. There, a color mixed from a dark background would
turn into black.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The borders of inactive windows are drawn in the terminal's default text
color, so they are as prominent as the text inside the windows. Draw
them in faint text instead. The terminal shades faint text toward its
background, so this works on dark and light backgrounds alike, and also
on backgrounds that are neither black nor white. The titles of inactive
windows use the same color, so they are dimmed too.
Terminals that don't support faint text draw the borders in the default
color, as before.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>
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>
The faint attribute makes text recede on dark and light backgrounds
alike, without having to pick a gray for each. Let users use it in the
theme.
Some theme colors go through both GetTextStyle and GetGocuiStyle, so
both have to know it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Config.md lists strikethrough as a modifier for theme colors, but only
GetTextStyle knows it. GetGocuiStyle turns an unknown name into white,
and white OR-ed with a palette color is white. So if you set a border
color to [red, strikethrough], you get a white border without
strikethrough. The same goes for the parts of the selected line,
options text and default text colors that gocui draws.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>
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>
We are about to add gui.darkTheme and gui.lightTheme, with the same type
as gui.theme. The schema generator stores a struct type as one
definition that all its properties refer to, and setDefaultVals writes
the defaults of each path into that definition. The overrides would
then claim the defaults of gui.theme, both in the schema and in
Config.md.
Give each property whose struct definition is shared a copy of its own.
In Config.md, print only the description of every copy after the first,
so that the fields aren't listed several times.
Nothing in the config shares a struct definition yet, so the generated
files don't change.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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, because lazygit runs them with TERM=dumb, and in a pty that doesn't
answer their queries.
Add `{{colorScheme}}` to the commands of diff renderers. It is 'dark' or
'light', based on what the terminal reports (which can be overridden by
`gui.colorScheme` if the terminal doesn't support the query). It can be
passed to delta as `--{{colorScheme}}` and to difftastic as
`--background={{colorScheme}}`; other renderers can choose between
options with a template expression.
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>
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>
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>
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>
Lazygit automatically picks random colors for the authors in the Commits
log. On a dark terminal theme, many of these were too dark and barely
readable; this can also be seen in the demo videos on the lazygit entry
page. Improving this is not trivial, because a range of colors that
looks good on a dark background comes out too pale on a light
background.
To improve this, detect the terminal's background color (with a config
override for those terminals that don't report it), and pick different
ranges of colors depending on that. In later PRs we will use the same
background detection to allow passing `--dark` or `--light` to a diff
renderer, and to support different `gui.theme` configurations for light
and dark.
While at it, fix the problem that the `gui.authorColors` config for
manually overriding some of the random colors didn't update on a config
reload.
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>
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>
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>
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>
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>
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>
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>