Commit Graph
5389 Commits
Author SHA1 Message Date
Stefan HallerandClaude Opus 5 9fa207e8e2 Ask diff renderers for OSC 1717 metadata
A renderer that speaks the protocol emits nothing unless it is asked to,
so that its output stays plain wherever it is used outside lazygit. The
variable names the versions we understand.

git is one of the renderers we ask. It has no pager to spawn, and so no
terminal to spawn one in, but it still renders the diff itself — for the
word-diff formats, whose markup nothing else could resolve — so the
request has to be made before we decide a pty isn't needed.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 19:01:01 +02:00
Stefan HallerandClaude Opus 5 3cb9038ea9 Take a diff renderer at its word about a diff line
Where a renderer states which line of which file it is rendering, take
that as the line's identity. The renderer knows, and for a rendering
that no longer looks like a diff nothing else does. Reading the rendered
text stays the way for the renderings that keep a diff's structure, and
for the renderers that say nothing at all.

Which of the two is used is settled for the rendering as a whole, not
row by row. A rendering with records is not parsed at all, not even for
the rows the renderer says nothing about. Its text is no unified diff,
and a row of it can read like one. Under delta, a commit that adds a
test whose input is a diff shows the test's "diff --git a/img.png
b/img.png" line as a row of its own; parsed, that row opens a file
section that runs to the end of the rendering, and every row delta puts
between hunks and files comes out as a header of a file called img.png.
So the untagged rows of such a rendering stay unresolved, as the
protocol has it (spec section 6.4).

Header records are accepted too. This lets a renderer point at a file
with no content lines, such as a pure rename, a mode change or a binary
file: for those files the header is the only row in the diff.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 19:01:01 +02:00
Stefan HallerandClaude Opus 5 b9b82cf9e5 Rename parsedDiffLine.RelPath to Path
A diff line's identity is about to become recoverable from a second
source, a diff renderer's own records, and a renderer states the path
however it likes — absolute paths included. The field can't promise
repo-relative any more.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 19:01:01 +02:00
Stefan HallerandClaude Opus 5 7bd4978f67 Keep the OSC 1717 records that cover no cell
Wherever two diff lines end up on one rendered line, a renderer emits
their records back to back: the deletion and the addition of a
modification collapsed into a single column, or a banner announcing a
file and its first hunk at once. A changed line that is empty is
rendered as its record alone. Attaching a record only to the cells it
precedes loses all of these — the last record of a run wins, and an
empty changed line becomes a line we can say nothing about at all.

Give such a record a cell of its own instead. It renders nothing, so the
diff looks the same, but the line keeps every record it was given.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 19:01:01 +02:00
Stefan HallerandClaude Opus 5 42ae6d4ea6 Swallow a diff renderer's protocol handshake
Before the diff, a conforming renderer emits one OSC 1717 record that
carries only the version — its way of announcing that it speaks the
protocol at all, without a host having to inspect what it renders. It
describes no line, so keeping it would give the first line of the diff a
record that says nothing about it.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 19:01:01 +02:00
Stefan HallerandClaude Opus 5 d1a9850531 Read the OSC 1717 records a diff renderer emits
A diff renderer that restructures the diff — into columns, or with the
+/- markers replaced by colour — leaves us no way to tell which line of
which file a rendered row came from, which is what acting on the row
requires. The OSC 1717 protocol has the renderer say so directly: it
prefixes each line it renders with a record naming the file and the
line's position in the old and new versions of it.

Attach each record to the cells it precedes, so that a row's records
survive wrapping and the columns of a side-by-side rendering, and hand
them to readers together with the row's text: the two have to describe
the same buffer, and a re-render can rebuild it between two reads.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 19:01:01 +02:00
Stefan HallerandClaude Opus 5 6faa95ce7e Recognize OSC numbers with more than one digit
The OSC parser dispatched on a single character, so only the
single-digit OSC 8 could ever be recognized; the diff-line metadata
protocol we are about to read uses OSC 1717.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 19:01:01 +02:00
Stefan HallerandClaude Opus 5 8cd0a0e9ad Decode the path of a diff header the way git writes it
git doesn't just print the path: it terminates it with a tab when the
path contains a space, and C-quotes the whole field when the path
contains anything it won't print raw — with core.quotePath, which is on
by default, that means any non-ASCII byte, so a file called café shows
up as "b/caf\303\251".

Taking the field verbatim therefore gives a path that doesn't exist, for
a whole class of perfectly ordinary file names. The quoting is Go's own
string syntax, octal escapes and all, so decoding it is a call to
strconv.Unquote.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 19:01:01 +02:00
Stefan HallerandClaude Opus 5 2a190fd1e4 Recover the identity of a diff line from the rendered diff
To act on the line the user is pointing at in a diff view — to stage it,
to open it in an editor, to keep the cursor on it while the diff is
regenerated — we need to know which file it belongs to and where it sits
in that file. Only the diff the view was rendered from knows that, and
by the time it's on screen all we have is text.

So parse it back: the view's contents are (usually) a unified diff, and
running them through the patch parser recovers each row's file, kind and
line numbers. "Usually" is why this goes behind a seam, and why it
answers "I don't know" rather than guessing: a diff renderer is free to
restructure what it prints, and a wrong answer here means acting on the
wrong line of the wrong file.

Parsing a whole buffer is a separate entry point from parsing a single
line, because a caller resolving every row of a large diff must not
re-parse a file's section once per line of it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 19:01:01 +02:00
Stefan HallerandClaude Fable 5.1 d5c19a83f3 Keep a view's lines as they were written, beside their cells
A reader that parses a view's content rather than showing it wants the
text the writer wrote, and the cells don't always spell it. A tab is
expanded into the spaces it fills, so a line read back from the cells
ends in one to four spaces where the writer put a tab. A carriage
return moves the write cursor back to the start of the line, so the
text written after it overwrites what came before.

The parser of the main view's diff, which the next commit adds, meets
the first case in every header of a file whose path contains a space.
git terminates the path field of a "---" or "+++" line with a tab
then, and a parser reading the cells takes the spaces the tab became
for part of the path. That path names a file that doesn't exist, so
the file's lines can't be acted on.

Keep the text as written per line, from the first character on that
the cells spell differently, so that a line without a tab or a
carriage return costs nothing. LinesAsWritten hands it out the way
BufferLines hands out the cells' text.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 19:01:01 +02:00
Stefan HallerandClaude Fable 5.1 caa20f20a0 Clear a pending newline when overwriting lines in place
OverwriteLines is asked for a line and writes the one below it when the
write before it ended in a newline. The view holds such a newline back
until more content arrives, so that it doesn't end in an empty line,
and OverwriteLines moved the write cursor without letting go of it, so
the write that followed advanced to the next line first.

Move the cursor through SetWritePos, which drops the pending newline
along with it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 19:01:01 +02:00
Stefan HallerandClaude Fable 5.1 d317ff1824 Demonstrate that overwriting lines after a pending newline lands a line low
A view holds back the newline that ends a write until more content
arrives, so that it doesn't end in an empty line. OverwriteLines moves
the write cursor to the line it is given without letting go of that
pending newline, so the write that follows advances first and lands on
the line below. Nothing in lazygit overwrites lines right after such a
write today.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-04 19:01:01 +02:00
Stefan HallerandClaude Opus 5 26e5f773cb Let callers map between view lines and buffer lines
Everything about a diff's content — which file and line a row belongs
to, whether it's a change — is a property of the unwrapped buffer line,
while the cursor, clicks and the range selection all speak in view
lines, which count wrapped segments. Reading the content under the
cursor therefore needs the mapping in both directions, and doing it
outside gocui isn't possible: the wrapping is internal, and the caller
couldn't take the view's lock across the lookup and the read.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 19:01:01 +02:00
Stefan HallerandClaude Opus 5 db6a04b8a5 Add a well-formedness check for a parsed patch
Parse is lenient: it takes any text and reads a diff out of it, which is
what we want when we hand it a diff, but it has no way to say "this
isn't one". We're about to parse the *rendered* contents of a diff view,
which a diff renderer is free to restructure — putting the line numbers
in a gutter, say, shifts the +/- marker off the start of each body line,
so every line reads as context and the parse silently lies about which
lines are changes.

Comparing each hunk's body against the lengths its header declares
catches exactly that, without teaching us anything about any particular
renderer's layout: a faithful unified diff agrees with its headers, a
restructured one doesn't.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 19:01:01 +02:00
Stefan HallerandClaude Opus 5 43a4ba3666 Add an old-file counterpart to Patch.LineNumberOfLine
Identifying a change line of a diff by its file line number needs both
sides: two consecutive deletions sit at the same new-file position, so
only their old-file line numbers tell them apart. LineNumberOfLine only
answers for the new file, which leaves deletions ambiguous.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 19:01:01 +02:00
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 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 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 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 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 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 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 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 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 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