Commit Graph
8388 Commits
Author SHA1 Message Date
Stefan HallerandClaude Opus 5 f04c561b2e Keep a resized view's place in the content it wraps
Wrapping the content at another width moves every line of it to a
different view line. The positions into the view are all view lines: the
scroll offset, the cursor, a range's anchor. Each of them is then left
pointing at a line it was never on. Committing the last of the staged
changes widens the main view by half a screen, and that moves a selected
hunk somewhere else entirely.

Carry the positions through the lines of content they were on. A position
always meant a line of content rather than a view line. The line the
cursor is on keeps the row it was drawn on, so it stays in front of the
user rather than the view scrolling under it; a view with no cursor on
screen keeps its own place instead. A range's ends go on the outermost
segments of their lines, since a range covers lines of content and not
the segments those lines are drawn as.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 19:01:01 +02:00
Stefan HallerandClaude Opus 5 9c8c1cb479 Demonstrate that resizing a wrapping view moves its selection
The scroll offset, the cursor and a range's anchor are all view lines, which
count the segments each line of the content is wrapped into. A change of
width wraps the content differently, so every one of them ends up on a
different line than the one it was put on.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 19:01:01 +02:00
Stefan HallerandClaude Opus 5 3b0cf1e182 Report a wrapped selection by the lines of content it covers
A test asks which lines of a view are selected; a wrapping view's cursor and
range anchor answer in view lines, which count the segments each line is
drawn as. Going through the segment-to-line mapping keeps the answer in the
terms the question was asked in, and a line the selection covers several
segments of is reported once.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 19:01:01 +02:00
Stefan HallerandClaude Opus 5 bc7d95b4a3 Demonstrate that a wrapped selection is reported by segment
The lines a selection covers are asked for by view line, which counts the
segments a wrapping view breaks a line into, and then used to index the
content, whose lines are unwrapped. The two agree only for a view whose
content doesn't wrap.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 19:01:01 +02:00
Stefan Haller 576d21868a Improve the rendering of the commit graph for terminals that support it (#6077)
Lazygit uses box drawing characters to render the commit graph. This is
problematic, because not all possible graph topologies can be
represented by them. In the example below, the first merge commit is the
parent of two more commits, a1 and b1, but you can't see this in the
left image; it looks like a1's parent was x1.

Improve this by using kitty's branch drawing characters that were added
to kitty in https://github.com/kovidgoyal/kitty/pull/7681. Ghostty,
Contour and VS Code's builtin terminal support them too; WezTerm
supports them in a nightly build. Lazygit uses them automatically when
it can tell that the terminal supports them, which is the case for
Ghostty and kitty, but not VS Code or Contour; if you are using a
terminal that supports them but isn't recognized (or if you happen to
use a font that contains them), you can opt in by setting the new
`gui.commitGraphStyle` config to "detailed".

The little circles that represent commits look nicer too.

| Before | After |
| ------ | ----- |
| <img width="191" height="114" alt="before"
src="https://github.com/user-attachments/assets/f00e1e91-67f5-4026-81ed-9a5f8b2fdef1"
/> | <img width="191" height="114" alt="after"
src="https://github.com/user-attachments/assets/bf60ac91-b761-458f-a13a-8eb5ea915e14"
/> |
2026-10-04 19:00:35 +02:00
Stefan HallerandClaude Opus 5.5 4780c8bb72 Use the detailed commit graph automatically in terminals that draw it
The detailed commit graph is only drawn for users who set
gui.commitGraphStyle to 'detailed'. Many users of terminals that can
draw it will never find out about it. A terminal can't be asked whether
it draws a given character, but it does tell us its name and version
when tcell asks for them with XTVERSION at startup.

Add an 'auto' value and make it the default. It uses the detailed graph
in kitty from 0.36.2 and in Ghostty from 1.0.0 on. These are the first
versions that draw all of the symbols. Everywhere else it stays with
the classic graph. This includes tmux, because tmux answers XTVERSION
itself. So far, WezTerm draws the symbols only in its nightly builds, so
it isn't detected until a release has them. VS Code isn't detected
either, because it only draws the symbols with GPU acceleration.

The expected output of the integration tests has the classic graph, so
pin it in their config. Otherwise they would fail when run with a
visible UI in one of these terminals.

Log the terminal's name and version at startup, to make it possible to
find out why 'auto' picked what it did.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 18:47:01 +02:00
Stefan HallerandClaude Opus 5.5 da5b9b54eb Add an option to draw the commit graph with branch drawing symbols
If a branch forks off a merge commit, the graph can't show both lines
in the merge commit's row. The line from the branch's first commit ends
in the same cell in which the merge's line to its second parent starts.
The box drawing characters have no symbol for a line from above that
bends to the left combined with a line from the left that bends down,
so one of the two gets lost. Today the cell shows a plain vertical
line, and the branch looks as if it sat on the merge's second parent
(#5497).

The branch drawing symbols in the Unicode Private Use Area (U+F5D0 to
U+F60D) have a symbol for every way in which lines meet in a cell.
kitty introduced them for git graph viewers like vim-flog, and Ghostty,
Contour, the terminal of VS Code and nightly builds of WezTerm render
them too. Other terminals need a font that contains them.

Add the gui.commitGraphStyle config. Its 'detailed' style draws the
graph with these symbols, and the commit symbols of the set connect to
their lines. Commits are drawn as hollow circles and merge commits as
filled ones. The default stays the 'classic' style with the box drawing
characters, because not every terminal can display the others.

A cell has only one colour. Where two lines meet, the symbol takes the
colour of the line from above. With the box drawing characters,
highlighting the lines of the selected commit clears the cells they run
through. The branch drawing symbols keep the other lines. A cell in
which a highlighted line bends is highlighted as a whole. Where a
highlighted line crosses the vertical line of another commit, it is
drawn over that line, so that it reads as one line. In a cell in which
another line bends, the symbol keeps the colour of that line.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 18:47:01 +02:00
Stefan HallerandClaude Opus 5.5 74e7572d7a Extract the choice of a graph cell's box drawing characters
This separates choosing the characters from writing them out with their
styles, so that the next commit can add another set of characters.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 18:47:01 +02:00
Stefan HallerandClaude Opus 5.5 e5c5a62cd9 Record how lines run through the cells of the commit graph
A cell of the commit graph only records which of its edges are touched
by lines. That's all the box drawing characters can show, but it
doesn't say how the lines connect. A vertical line with a horizontal
line passing behind it touches all four edges. So does a cell in which
the line from above bends to the left, the line below comes in from the
left, and a horizontal line passes through.

For the lines at the top and bottom edges, record whether they run
straight on or bend to the left or right, and record whether a line
passes through horizontally. Nothing reads this yet. It prepares for
drawing the graph with symbols that can show these differences.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 18:47:01 +02:00
Stefan HallerandClaude Opus 5.5 91378b1e8a Stop drawing a line below root commits in the commit graph
If a root commit isn't the last one in the list, the graph draws a
vertical line below it in every row that follows. A root commit's pipe
to the empty tree never terminates, because no commit comes after it,
so it is carried on from row to row. The line also takes up its column,
and an unrelated commit after the root commit is placed to the right of
it.

Drop that pipe after the root commit's row. In its own row, it still
gives the commit symbol the style of the commit. Also, place a commit
that no line leads to next to the lines that go on into its row, not
next to the lines that ended in the row before. Their columns are free
again.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 18:47:01 +02:00
Stefan HallerandClaude Opus 5.5 cee602c029 Demonstrate that the graph continues the line below a root commit
If a root commit isn't the last one in the list, the graph draws a
vertical line below it in every row that follows, although nothing
comes after a root commit. This happens in repos with more than one
root commit, for example when showing all branches of a repo with a
gh-pages branch, or after merging an unrelated history.

Also, a commit that no line leads to is placed to the right of the
lines that ended in the row before it, although their columns are free
again.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 18:47:01 +02:00
Stefan Haller 4dc619c081 Fix occasionally failing integration test (#6088)
The integration test filter_and_search/search_a_long_diff fails now and
then on CI with "Expected search prompt to be focused". Pressing "/" in
the focused main view reads the rest of the diff with ReadToEnd. Its
callback opens the prompt by enqueueing the work onto the UI thread.
ReadToEnd marks its task as done before calling the callback, so for a
moment no task is busy. If the idle wait of the test driver wakes up in
that moment, the test checks for the prompt before it is open. Adding a
sleep between the two calls makes the test fail every time.

The same gap exists when going to the bottom of a view, since that
callback enqueues its work onto the UI thread too.

Call the callback first, and mark the task as done after it returns. The
work that the callback enqueues holds a task of its own from the moment
it is enqueued, so lazygit no longer counts as idle in between.
2026-10-04 16:50:49 +02:00
Stefan HallerandClaude Opus 5.5 8588cd0b4e Keep ReadToEnd's task busy until its callback has returned
The integration test filter_and_search/search_a_long_diff fails now and
then on CI with "Expected search prompt to be focused". Pressing "/" in
the focused main view reads the rest of the diff with ReadToEnd. Its
callback opens the prompt by enqueueing the work onto the UI thread.
ReadToEnd marks its task as done before calling the callback, so for a
moment no task is busy. If the idle wait of the test driver wakes up in
that moment, the test checks for the prompt before it is open. Adding a
sleep between the two calls makes the test fail every time.

The same gap exists when going to the bottom of a view, since that
callback enqueues its work onto the UI thread too.

Call the callback first, and mark the task as done after it returns. The
work that the callback enqueues holds a task of its own from the moment
it is enqueued, so lazygit no longer counts as idle in between.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 16:24:53 +02:00
Stefan HallerandClaude Opus 5.5 b34691777d Add a test for the task that ReadToEnd holds while it calls back
ReadToEnd marks its task as done before it calls then. Its callers
enqueue their follow-up work onto the UI thread from then, so for a
moment in between, no task is busy.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 16:21:28 +02:00
Stefan Haller ae37d44bce Fix drawing git's diff stat after switching screen modes (#6087)
Changing the screen mode renders the main view again, and so does
anything else that changes its size along with what it shows. With git's
own diff, that render laid a commit's diffstat out to the width the view
had before. The graph then wrapped onto rows of its own in a view that
got narrower, and stopped short in one that got wider.

A diff renderer's render is created after the layout pass, since only
the layout settles the view's size, and takes its width there. git's own
diff took its width straight away instead, when the render was asked
for. Create its task after the layout as well.

This bug is going to be a little more prominent with the upcoming PR
series that moves hunk staging to the main view; in that case, when you
stage the first hunk into a custom patch, the main view can get split
side-by-side, and without this fix the left half of the main view would
render its diff stat to the old width of the view.
2026-10-04 15:42:37 +02:00
Stefan HallerandClaude Opus 5.5 0afb94e97b Lay git's own diff out to the width the layout gives the view
Changing the screen mode renders the main view again, and so does
anything else that changes its size along with what it shows. With git's
own diff, that render laid a commit's diffstat out to the width the view
had before. The graph then wrapped onto rows of its own in a view that
got narrower, and stopped short in one that got wider.

A diff renderer's render is created after the layout pass, since only
the layout settles the view's size, and takes its width there. git's own
diff took its width straight away instead, when the render was asked
for. Create its task after the layout as well.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 16:50:48 +02:00
Stefan HallerandClaude Opus 5.5 6b38e8fcde Demonstrate that git's diffstat keeps the width from before a screen mode change
Pressing + in the commits panel moves to half-screen mode and renders
the commit's diff again in the narrower main view. With git's own diff,
the diffstat graph comes out as wide as the view was before, and wraps
onto rows of its own.

ContainsViewLines asserts on the rows the view draws, after wrapping.
The existing line assertions see the lines of the content and can't tell
a wrapped line from one that fits.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 16:42:10 +02:00
Stefan Haller ff375b124d Revert the change of InactiveBorderColor to "dim" (#6080)
This was an attempt to make lazygit's overall UI look more "elegant",
but it has too many problems. The biggest is that we have panels that
combine two views (e.g. a prompt with suggestions below, or the commit
message editor with subject and body), and showing the frame of the
inactive half of these as "dim" looks wrong; we'd have to add a new
concept of a view that doesn't have the focus but otherwise still looks
active.

Also, some terminals don't do a good job of showing lines made of dim
box drawing characters (e.g. Zed's builtin terminal). This could
probably be improved by using a grey hex color instead of dim, but then
we'd have to pick one that looks good with all possible terminal color
scheme.

That's too many questions, and it's not important enough to spend time
on this, so just revert the change.
2026-09-30 08:44:09 +02:00
Stefan Haller f2a1e178ba Revert the change of InactiveBorderColor to "dim"
This was an attempt to make lazygit's overall UI look more "elegant",
but it has too many problems. The biggest is that we have panels that
combine two views (e.g. a prompt with suggestions below, or the commit
message editor with subject and body), and showing the frame of the
inactive half of these as "dim" looks wrong; we'd have to add a new
concept of a view that doesn't have the focus but otherwise still looks
active.

Also, some terminals don't do a good job of showing lines made of dim
box drawing characters (e.g. Zed's builtin terminal). This could
probably be improved by using a grey hex color instead of dim, but then
we'd have to pick one that looks good with all possible terminal color
scheme.

That's too many questions, and it's not important enough to spend time
on this, so just revert the change.
2026-09-30 08:38:27 +02:00
Stefan Haller a3fae72578 Make shift-backspace do the same as backspace in editors (#6078)
This can be useful when typing an all-caps word; if you mistype one
letter, you can delete it without having to let go of shift.

Closes #6076.
2026-09-28 22:52:10 +02:00
Stefan Haller 050803827e Make shift-backspace do the same as backspace in editors
This can be useful when typing an all-caps word; if you mistype one
letter, you can delete it without having to let go of shift.
2026-09-28 22:22:22 +02:00
Stefan Haller cfbbf18656 Delete or push multiple tags at once (#6071)
Make it possible to range-select multiple tags and delete or push them
all at once.
2026-09-27 11:24:08 +02:00
Stefan HallerandClaude Opus 5.5 09b53e9a85 Allow pushing a range selection of tags
Tags can only be pushed one at a time. Let the push command of the tags
panel work on a range selection of tags too. It asks for one remote and
pushes all selected tags to it with a single git command.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 11:19:25 +02:00
Stefan HallerandClaude Opus 5.5 3080747fba Let the command for pushing tags take several tags
This lets pushing a range selection of tags push them all with a single
git command.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 11:19:25 +02:00
Stefan HallerandClaude Opus 5.5 1b4757be89 Allow deleting a range selection of tags
Tags can only be deleted one at a time. If many of them have piled up,
for example backup tags made before rewriting a branch, deleting them
takes a lot of keypresses.

Let the delete menu work on a range selection of tags. It deletes the
selected tags with one git command for the local tags and one for the
remote tags, and asks for a single remote for all of them. After
deleting local tags, collapse the range selection to its first line;
otherwise it would select the tags that moved up into the place of the
deleted ones.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 11:19:25 +02:00
Stefan HallerandClaude Opus 5.5 2ca2be6025 Extract the remote prompt and confirmation of deleting a remote tag
Deleting a remote tag and deleting a local and remote tag each have
their own copy of the code that asks for the remote and for a
confirmation. Deleting a range selection of tags needs to change it in
the same way for both.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 11:19:25 +02:00
Stefan HallerandClaude Opus 5.5 80446461c1 Let the commands for deleting tags take several tags
This lets deleting a range selection of tags delete them all with a
single git command, the same way it works for branches.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 11:19:25 +02:00
Stefan HallerandClaude Opus 5.5 fc6ed07747 Generalize showing an inline status on several branches to any items
Deleting and pushing a range selection of tags will need to show their
operation on all the selected tags. Move the code from BranchesHelper
into a generic function that takes the items and the key of the context
that shows them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 11:19:25 +02:00
Stefan Haller 02e103c873 Offer to pull all branches in a stack when pulling the topmost one (#6070)
When somebody else rebases a stack of branches and force-pushes it,
pulling the topmost branch leaves the branches below it diverged. Since
#6067 they can be updated manually with `f`, but pushing the stack
offers to push the branches
below along with the current one, so pulling should be symmetric, and
this PR adds that.
2026-09-27 11:19:22 +02:00
Stefan HallerandClaude Opus 5.5 26ad23a8d2 Offer to update the branches stacked below the current one when pulling
When somebody else rebases a stack and force-pushes it, pulling the
topmost branch leaves the branches below it diverged. They can be
updated with `f`, but pushing the stack offers to push the branches
below along with the current one, and pulling should be symmetric.

When pulling, find the branches stacked below the current one that can
be updated without losing anything. These are those that are behind
their upstream branches, and those that diverged from them only because
they were rewritten. If there are any, show a menu that lists them and
offers to update them in addition to pulling the current branch, or to
pull only the current branch. The current branch is pulled as before.

The decision is based on the last fetch, not on a new one, so that the
menu appears right away. If the remote has changed since then, a branch
might not be offered although it could be updated; it then shows as
diverged after the pull, and pulling again offers it. The update itself
fetches the upstream branches and checks them again, so it never loses
commits.

Branches that are checked out in another worktree are not offered,
since updating them would change the files there.

The branches below are updated before the current branch is pulled. If
one of them pointed into the commits that a rebasing pull rebases, the
pull would move it when rebase.updateRefs is set, and updating it
afterwards would fail. If updating them fails, the current branch isn't
pulled.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 11:13:15 +02:00
Stefan HallerandClaude Opus 5.5 6919a2aa47 Extract showing an inline status on several branches
Pushing a stack and fast-forwarding a range of branches each show their
operation on all the branches involved, with their own copy of the same
code. Pulling a stack will need it too, so move it to BranchesHelper.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 11:13:15 +02:00
Stefan HallerandClaude Opus 5.5 b7395b1637 Split fast-forwarding branches into preparing and running it
FastForwardBranches looks up the worktrees of the branches in the model
on the UI thread, and then does the rest on a worker, with the branches
shown as being fast-forwarded. The next commit needs to update branches
as part of pulling the current one, in the pull's own task.

Move everything except the inline status into PrepareFastForward, which
returns the part to run on a worker.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 11:13:15 +02:00
Stefan Haller 9b5b49840b Auto-forward branches even if checked out in another worktree (#6069)
Lazygit can auto-forward main branches after fetching, which is useful
to always keep your `main` branch up to date. However, checking out
`main` in another worktree stopped this from happening, on the
assumption that we don't want to touch branches that are checked out
somewhere. For a main branch this is not an issue (as long as the
worktree doesn't have modified files); forwarding it in that case too is
useful for the setup where you always keep main checked out in the main
worktree, and work on feature branches in linked worktrees.

Pressing `f` on such a branch already supported fast-forwarding it even
when checked out in a worktree, so now auto-forwarding after fetching
does the same.
2026-09-27 11:13:12 +02:00
Stefan HallerandClaude Opus 5.5 0a99f65be3 Auto-forward branches that are checked out in other worktrees
If a main branch is checked out in another worktree, auto-forwarding
after a fetch skips it. People who keep the main branch checked out in
their main worktree, and work on feature branches in linked worktrees,
therefore never get it forwarded.

Fast-forward such a branch in the worktree it is checked out in, as `f`
does. Skip the worktree if it has changes to tracked files, since
nobody asked for its files to change. Also skip it if it is mid-rebase
or mid-bisect, because its HEAD is then detached from the branch. A
branch in the current worktree is never forwarded. This covers the case
of the current worktree being mid-rebase on a main branch, whose ref
would otherwise be forwarded under the rebase.

Auto-forwarding now writes the same reflog message as `f`
("lazygit: update to upstream branch").

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 11:05:47 +02:00
Stefan HallerandClaude Opus 5.5 1b4fc02040 Keep fast-forwarding the other branches when one of them fails
If `f` is used on several branches that are checked out in other
worktrees, the first worktree in which the fast-forward fails stops the
whole operation. For example, an untracked file that the fast-forward
would overwrite leaves the branches after it untouched, even though
nothing is wrong with them.

Try all the branches, and report the errors of the failed ones
together. The checks that refuse to fast-forward any of the branches
still run before anything is moved.

The next commit uses forwardBranches for auto-forwarding branches in
other worktrees, where stopping at the first failure is worse. A
worktree that fails every time would prevent the ones after it from
ever being forwarded. For this, forwardBranches also takes the git
commands to use, since a background worker doesn't block switching
repos.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 11:05:47 +02:00
Stefan HallerandClaude Opus 5.5 245a613762 Auto-forward branches on a worker
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>
2026-09-27 11:05:47 +02:00
Stefan Haller acc58a2d12 Use f to "fast-forward" branches whose upstream branch was rewritten (#6067)
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.
2026-09-27 11:05:44 +02:00
Stefan HallerandClaude Opus 5.5 ef00e18247 Refuse to fast-forward a branch being rebased or bisected in a worktree
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>
2026-09-27 11:00:36 +02:00
Stefan HallerandClaude Opus 5.5 5990946a23 Add a test for fast-forwarding a branch being rebased in a worktree
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>
2026-09-27 11:00:36 +02:00
Stefan HallerandClaude Opus 5 9183269ee8 Write a reflog message when moving branch refs
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>
2026-09-27 11:00:36 +02:00
Stefan HallerandClaude Opus 5 83bff569a0 Allow fast-forwarding a range of branches
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>
2026-09-27 11:00:36 +02:00
Stefan HallerandClaude Opus 5 78dc65cbeb Fast-forward branches whose upstream branch was rewritten
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>
2026-09-27 11:00:36 +02:00
Stefan HallerandClaude Opus 5 667df9e784 Split fast-forwarding a branch into fetching and updating it
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>
2026-09-27 11:00:36 +02:00
Stefan HallerandClaude Opus 5 0d8b18cfb7 Move fast-forwarding a branch into BranchesHelper
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>
2026-09-27 11:00:36 +02:00
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 Opus 5 173001c3e7 Add a function to tell whether a branch has commits of its own
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>
2026-09-27 11:00:36 +02:00
Stefan Haller 93fb28a864 Push all branches of a rebased branch stack at once (#6045)
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.
2026-09-27 11:00:32 +02:00
Stefan HallerandClaude Fable 5.1 149c5f8578 Ignore escape sequences when wrapping text for a view
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>
2026-09-27 08:21:02 +02:00