Commit Graph
8751 Commits
Author SHA1 Message Date
Stefan HallerandClaude Opus 5.5 c84a181de9 Extract finding the commits merged into a local main branch
The commits list will need this to decide which branches to mark. It
renders the list again whenever the selection changes, so stop the walk
at merged commits. Their parents are merged too, and a local main branch
is usually merged itself, so there is mostly nothing left to walk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 12:38:05 +01:00
Stefan HallerandClaude Opus 5.5 4ba8f7c175 Move BranchesBelowInStack to a new branchstack package
The commits list will need it to mark the branches of a stack, and so
will the "go to bottom" command of the commits panel to find their
beginnings. Neither the presentation nor the context package can import
helpers.

Rename it to BranchesBelow, since the name of the package says that it
is about stacks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 12:36:41 +01:00
Stefan Haller d3c0ca6ecf Merge v0.66.3 to master (#6190) 2026-11-08 08:45:02 +01:00
Stefan Haller 5a8adefd53 Merge v0.66.3 into master 2026-11-08 08:30:13 +01:00
Stefan Haller e0fbfd50c3 Ignore submodules whose name or path in .gitmodules leads outside the repository (#6187)
If `.gitmodules` contains a submodule name like `../../../victim`,
removing that submodule in lazygit deletes the directory `victim` next
to the repository, along with everything in it. With enough `../`
components the path becomes `/`, and lazygit then deletes all files that
the user has permission to delete. An attacker only needs to commit such
a `.gitmodules` file to a repository. A user of that repository then
only has to remove the submodule in lazygit and confirm the prompt.

This happens because lazygit parses `.gitmodules` itself and uses the
names and paths in it without checking them. It deletes a submodule's
git dir with `os.RemoveAll`, joining `.git/modules` with the name, and
`filepath.Join` resolves the `..` components. Paths have a similar
problem. Lazygit runs some commands in the submodule's directory, so
with `path = ../other_repo` (or a symlink that points there), nuking the
working tree stashes the changes and untracked files of that other
repository. Git itself ignores such names (it prints "ignoring
suspicious submodule name"), and it only uses a path from `.gitmodules`
if its index has a submodule at that path.

This PR:
- Ignores submodules whose name fails git's own check in
`check_submodule_name()`: an empty name, or `..` as a component, with
both `/` and `\` counting as separators on every platform.
- Ignores submodules whose path isn't a directory inside the repository,
or whose path goes through a symlink.
- Refuses to remove a submodule whose git dir isn't below
`.git/modules`, before running any git command. This also catches the
name `.`, which git accepts. Removing a submodule with that name used to
delete the git dirs of all submodules.
- Anchors the regexes that parse `.gitmodules` at the start of a line.
This is unrelated to the security problem. A url containing `path=` was
taken for the submodule's path, and a commented-out section showed up as
a submodule.

Ignored entries no longer appear in the submodules list. Git doesn't
treat them as submodules either.

Fixes #6185.
v0.66.3
2026-11-08 00:16:11 +01:00
Stefan HallerandClaude Opus 5.5 bb4c681330 Only recognize .gitmodules keys and section headers at the start of a line
If a submodule's url contains "path=", for example
"https://example.com/repo?path=other", lazygit takes "other" for the
submodule's path and loses the url. Also, a commented-out section in
.gitmodules shows up as a submodule.

The regexes that parse .gitmodules aren't anchored, so they find a key
or a section header anywhere in a line. Anchor them at the start of the
line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 23:59:32 +01:00
Stefan HallerandClaude Opus 5.5 aecd5e5bb4 Add a test showing that .gitmodules keys are found in the middle of a line
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 23:59:32 +01:00
Stefan HallerandClaude Opus 5.5 93556c1588 Ignore submodules with a suspicious path in .gitmodules
If .gitmodules contains a submodule path like "../other_repo", nuking
the working tree in lazygit stashes all changes and untracked files of
that other repository. The same happens if the path is a symlink that
points to it. The changes can be recovered with "git stash pop" in that
repository, but the user has no reason to look there.

This happens because lazygit runs some commands in the submodule's
directory, and takes that directory from .gitmodules as it is. Git
itself only uses a path from .gitmodules if its index has a submodule
at that path, and such a path can't lead out of the repository.

Ignore a submodule unless its path is a directory below the directory
that contains the .gitmodules file, without any symlinks in between.
This also keeps GetConfigs from reading the same .gitmodules file again
and again for a path of ".".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 23:59:32 +01:00
Stefan HallerandClaude Opus 5.5 a159e24919 Add a test showing that nuking the working tree can stash changes in other repositories
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 23:59:32 +01:00
Stefan HallerandClaude Opus 5.5 bcbc5ebd6d Refuse to remove a submodule whose git dir isn't below .git/modules
Git accepts "." as a submodule name, and so does GetConfigs. Removing
such a submodule in lazygit deletes .git/modules, together with the git
dirs of all other submodules. Any unpushed commits in those submodules
are lost.

Check the path of the git dir before running any git command, and
refuse to remove the submodule if the path isn't below .git/modules.
This also protects against names with ".." components, in case one ever
gets past GetConfigs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 23:59:32 +01:00
Stefan HallerandClaude Opus 5.5 e67bdba1d5 Add a test showing that removing a submodule can delete more than its git dir
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 23:59:32 +01:00
Stefan HallerandClaude Opus 5.5 82de59b3ff Ignore submodules with a suspicious name in .gitmodules
If .gitmodules contains a submodule name like "../../../victim",
removing that submodule in lazygit deletes the directory "victim"
outside the repository, along with everything in it. With enough "../"
components the path becomes "/", and lazygit then deletes all files
that the user has permission to delete. An attacker only needs to
commit such a .gitmodules file to a repository. If a user of that
repository removes the submodule in lazygit, their files are gone.

This happens because lazygit deletes the submodule's git dir with
os.RemoveAll, and builds its path by joining .git/modules with the name
from .gitmodules. filepath.Join resolves the ".." components.

Git refuses to use such names. It prints "ignoring suspicious submodule
name" and treats the entry as if it didn't exist. Do the same in
GetConfigs, using the same rule as git's check_submodule_name().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 23:59:32 +01:00
Stefan HallerandClaude Opus 5.5 0c46c6261f Add a test showing that removing a submodule can delete a directory outside the repository
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 23:59:32 +01:00
Stefan HallerandClaude Opus 5.5 aa034b1b5e Parse .gitmodules completely before loading nested submodules
So far, GetConfigs loaded the nested submodules of an entry as soon as
it saw the entry's path line. Parse the whole file first, and load the
nested submodules in a second loop. This prepares for checking the name
and path of each entry before using it.

This only makes a difference for malformed entries. If the path value
is empty, we no longer read the same .gitmodules file over and over.
If an entry has several path lines, we no longer load its nested
submodules several times.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 23:59:32 +01:00
Stefan Haller 2f18399841 Fix removing a submodule in a linked worktree breaking the submodule in the main worktree (#6186)
Removing a submodule in a linked worktree deletes the git dir of that
submodule in the main worktree. This breaks the submodule there, and any
unpushed commits, local branches and stashes in it are lost. The git dir
of the submodule in the linked worktree stays behind.

Git keeps the git dirs of submodules separately for each worktree. For a
linked worktree, they are in `.git/worktrees/<worktree>/modules`.
Lazygit used the worktree's git dir until #3341 switched it to the
repo's git dir, so this has been broken since v0.41.0. This PR goes back
to using the worktree's git dir.
2026-11-07 23:59:05 +01:00
Stefan HallerandClaude Opus 5.5 98ca24de23 Delete the right git dir when removing a submodule in a linked worktree
Removing a submodule in a linked worktree deletes the git dir of that
submodule in the main worktree. This breaks the submodule there, and
any unpushed commits, local branches and stashes in it are lost. The
git dir of the submodule in the linked worktree stays behind.

Git keeps the git dirs of submodules separately for each worktree. For
a linked worktree, they are in .git/worktrees/<worktree>/modules.
Lazygit used the worktree's git dir until 3b723282cb switched it to the
repo's git dir, so this has been broken since v0.41.0. Use the
worktree's git dir again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 23:46:56 +01:00
Stefan HallerandClaude Opus 5.5 58fcd14a99 Add a test showing that removing a submodule in a linked worktree deletes the wrong git dir
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 23:44:58 +01:00
Stefan Haller 4faea0e25a Scroll the commits panel sideways to a search match that was off screen (#6184)
If a search match is in a commit below or above the visible area of the
commits panel, going to it with /, n or N doesn't scroll the panel
sideways to show it. The panel keeps the horizontal scroll position of
the previous match instead. Since this depends on whether the commit was
on screen before, it seems to happen only some of the time.

The commits panel renders only the lines on screen. So the search knows
where in a commit's line its match is only once the line is on screen,
and that happens after the search has scrolled the panel to it.

Scroll the panel sideways once more after rendering the lines that a
search has scrolled to. The same applies to the commits of a branch or
tag.
2026-11-07 20:44:30 +01:00
Stefan HallerandClaude Opus 5.5 92525a2347 Scroll the commits panel sideways to a search match that was off screen
If a search match is in a commit below or above the visible area of the
commits panel, going to it with /, n or N doesn't scroll the panel
sideways to show it. The panel keeps the horizontal scroll position of
the previous match instead. Since this depends on whether the commit
was on screen before, it seems to happen only some of the time.

The commits panel renders only the lines on screen. So the search knows
where in a commit's line its match is only once the line is on screen,
and that happens after the search has scrolled the panel to it.

Scroll the panel sideways once more after rendering the lines that a
search has scrolled to. The same applies to the commits of a branch
or tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 20:14:38 +01:00
Stefan HallerandClaude Opus 5.5 a73278d246 Extract View.scrollSidewaysToSearchResult
A view that renders only the lines on screen is going to need it once
it has rendered the line of a match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 20:14:38 +01:00
Stefan HallerandClaude Opus 5.5 74569cf535 Add a test showing that commits search doesn't scroll to an off-screen match
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 20:14:38 +01:00
Stefan Haller 84a7967e49 Fix highlighting the current search match (#6182)
Fixes several minor issues with how search results are highlighted. See
commit messages for details.
2026-11-07 20:13:51 +01:00
Stefan HallerandClaude Opus 5.5 8fb62f85b6 Keep the current search match when it is on the selected line
If the selected line has several search matches and n has gone to the
second one, a refresh makes the first match on the line the current one
again. The "x of y" status changes although the user did nothing, and
the next n goes to the second match on the same line once more. Focusing
the view again does the same.

If the current match is on the selected line, keep it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 19:54:00 +01:00
Stefan HallerandClaude Opus 5.5 029f939252 Add a test showing that a refresh loses the second match on a line
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 19:54:00 +01:00
Stefan HallerandClaude Opus 5.5 4aed79b954 Keep the current search match when a view without a selection refreshes
If a view doesn't show a selection, for example a branch's commit log
in the focused main view, n and N step through the matches without
moving a cursor. A refresh then resets the search to the first match,
so the "x of y" status goes back to "1 of y", and the next n starts
over from the top.

Only reset the current match when a new search starts, and keep it when
the search runs again over refreshed content.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 19:54:00 +01:00
Stefan HallerandClaude Opus 5.5 5f5d6a1dd6 Add a test showing that a refresh resets the search in a commit log
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 19:54:00 +01:00
Stefan HallerandClaude Opus 5.5 c732aaf163 Choose the current search match after a refresh as after moving the selection
If the selection is between two search matches, moving it makes the
match above it the current one, but a refresh makes the match below it
the current one. So a background refresh changes the "x of y" status
and the highlighted match although the user did nothing. If the
selection is below the last match, a refresh makes the first match the
current one; n then skips that match and goes to the second one.

After a refresh, choose the current match in the same way as after
moving the selection. This makes the first match on the selected line
the current one, or else the last match above it. The same applies
when the focused main view renders its diff again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 19:54:00 +01:00
Stefan HallerandClaude Opus 5.5 6878890b96 Give View.Search its own choice of the match to start at
A new search goes to the first match after the cursor. A search that
runs again because the content has changed uses the same rule, since
Search shares it with UpdateSearchResults. Choose the match in Search
itself, so that UpdateSearchResults can choose differently.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 19:54:00 +01:00
Stefan HallerandClaude Opus 5.5 a92f739dc1 Add a test showing that a refresh changes the current search match
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 19:54:00 +01:00
Stefan Haller e056b8c79e Bump github.com/kr/logfmt to latest main (#6181)
The only change is that this adds a LICENSE file, which may make things
easier for package maintainers.
2026-11-07 19:47:58 +01:00
Stefan Haller eb11365ba0 Bump github.com/kr/logfmt to latest main
The only change is that this adds a LICENSE file, which may make things
easier for package maintainers.
2026-11-07 19:25:17 +01:00
Stefan Haller bbca3df2cd Avoid conflicts and duplicate lines when moving a patch to an earlier commit (#6167)
When moving a custom patch to an earlier commit, or into a new commit
before the original one, lazygit applies the patch to the earlier commit
and then replayed the original commit unchanged. It relied on git
noticing during the replay that the patch's changes are already there.
This doesn't always work:

- If the patch contains only some of several adjacent added lines,
replaying the original commit stops with a conflict.
- If git can place the patch's added lines in two positions because they
repeat the line after them, the replay may put them in the other
position than the patch did. Git then keeps both copies, and the file
ends up containing the patch's lines twice. There's no conflict, so this
goes unnoticed.

Fix this by making the rebase pick a copy of the source commit instead.
The copy has the same tree, author and message, but its parent is the
original parent with the patch applied. Picking it only replays the
source commit's changes minus the patch, so git no longer has to
recognize the patch as already applied.
2026-11-06 17:28:57 +01:00
Stefan HallerandClaude Opus 5.5 fabc14d7e5 Keep the conflicts popup open after moving a patch into a new commit
If moving a custom patch into a new commit stops at a conflict,
CheckMergeOrRebase shows the "Conflicts!" popup. The handler then
focused the commits view anyway, and this closed the popup.

Only focus the commits view if the operation finished.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 19:25:48 +01:00
Stefan HallerandClaude Opus 5.5 3864ddc52c Unify the handlers for moving a patch into a new commit
The handlers for moving the patch into a new commit before or after the
original one differ only in the git command they call. The next commit
fixes a bug that both have.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 19:25:48 +01:00
Stefan HallerandClaude Opus 5.5 16f51df6e3 Avoid conflicts and duplicate lines when moving a patch into a new commit before
Moving a custom patch into a new commit before the original one has the
same problem as moving it to an earlier commit. After committing the
patch, the rebase picks the original commit unchanged and relies on git
noticing that the patch is already there.

Make the rebase pick a copy of the original commit whose parent already
contains the patch, as we do when moving to an earlier commit. Here,
the copy's parent normally has the same tree as the new commit, so
picking the copy doesn't conflict. Only a commit hook that changes the
new commit's files could make it conflict.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 19:25:48 +01:00
Stefan HallerandClaude Opus 5.5 507b65065f Avoid conflicts and duplicate lines when moving a patch to an earlier commit
When moving a custom patch to an earlier commit, we apply the patch to
the earlier commit and then let the rebase pick the commit that the
patch came from, unchanged. This relies on git noticing during the pick
that the patch's changes are already there. If the patch contains only
some of several adjacent added lines, git doesn't notice, and the pick
stops with a conflict. If git can place the patch's added lines in more
than one position, it may place them differently for the pick than for
the patch. It then adds them a second time, without any conflict.

Make the rebase pick a copy of the source commit instead. The copy has
the same tree, author and message, but its parent is the original
parent with the patch applied. Picking it only replays the source
commit's changes minus the patch, so git no longer has to recognize
the patch as already applied. Create the copy with plumbing commands
and a temporary index; this doesn't touch the working tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 19:25:48 +01:00
Stefan HallerandClaude Opus 5.5 09068b0360 Add a function to change which commit a rebase todo picks
The next commit uses this to replay a modified copy of a commit in
place of the original one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 19:25:48 +01:00
Stefan HallerandClaude Opus 5.5 e8261e06d1 Disable moving a patch between a merge commit and another commit
If the patch comes from a merge commit, moving it to a later commit
leaves the user in a rebase that is stuck before its first todo.
Moving it to an earlier commit, or into a new commit before the merge,
finishes, but with surprising results. The rebase doesn't replay a
merge commit; it recreates it from its parents. If the patch contains
changes from the merged branch, the commits on that branch that made
them become empty. And a new commit "before" the merge ends up at the
tip of the merged branch.

Moving a patch to a later merge commit gets stuck in the same way.
Moving a patch to a merge commit isn't useful in either direction, so
disable both.

The other patch commands amend the merge commit itself and work fine,
so they stay enabled. They are useful for removing changes that were
added to a merge commit by accident.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 19:25:48 +01:00
Stefan HallerandClaude Opus 5.5 9383172a6c Add a test demonstrating that the conflicts popup disappears
If moving a custom patch into a new commit before the original one
results in a conflict, lazygit shows the "Conflicts!" popup, but then
immediately focuses the commits view. This closes the popup, and the
user is left in a rebase that is stopped at a conflict without being
told about it.

The test uses a pre-commit hook that changes a file of the new commit;
this makes picking the original commit conflict.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 19:25:48 +01:00
Stefan HallerandClaude Opus 5.5 7bf398db1d Add tests demonstrating bugs when moving a patch to an earlier commit
When moving a custom patch to an earlier commit, or into a new commit
before the original one, lazygit applies the patch to the earlier
commit and then replays the original commit unchanged. It relies on
git noticing during the replay that the patch's changes are already
there. This doesn't always work:

- If the patch contains only some of several adjacent added lines,
  replaying the original commit stops with a conflict.
- If git can place the patch's added lines in two positions because
  they repeat the line after them, the replay may put them in the
  other position than the patch did. Git then keeps both copies, and
  the file ends up containing the patch's lines twice. There's no
  conflict, so this goes unnoticed.

When moving into a new commit before the original one, the conflict of
the first case doesn't show the usual "Conflicts!" popup; lazygit
focuses the commits panel instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 19:25:48 +01:00
Stefan Haller 0002c03547 Don't offer to push or pull branches merged into a local main branch (#6169)
When pushing master, lazygit offers to push other branches along with it
if they were merged into master locally and have unpushed commits.
Pulling master offers to update such branches in the same way. These
branches aren't part of a stack, so the menu only adds an extra step to
an ordinary push or pull.

This happens because a branch counts as stacked below the current one if
its tip is one of the current branch's commits that aren't merged into a
main branch yet. A commit's merged status only looks at the remote
branch of each main branch. So until master is pushed, the commits that
were merged into it locally count as unmerged. The same goes for pushing
or pulling a feature branch on top of such a master.

Treat the commits that a local main branch contains as merged too, so
that the search for stacked branches stops at a main branch.

Fixes #6144.
2026-11-03 19:24:53 +01:00
Stefan HallerandClaude Opus 5.5 fb1b6ed1c1 Don't offer to push or pull branches merged into a local main branch
When pushing master, lazygit offers to push other branches along with
it if they were merged into master locally and have unpushed commits.
Pulling master offers to update such branches in the same way. These
branches aren't part of a stack, so the menu only adds an extra step to
an ordinary push or pull.

This happens because a branch counts as stacked below the current one
if its tip is one of the current branch's commits that aren't merged
into a main branch yet. A commit's merged status only looks at the
remote branch of each main branch. So until master is pushed, the
commits that were merged into it locally count as unmerged. The same
goes for pushing or pulling a feature branch on top of such a master.

Treat the commits that a local main branch contains as merged too, so
that the search for stacked branches stops at a main branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:12:36 +01:00
Stefan Haller c9c0db40bd Improve bisecting (#6168)
This PR makes bisecting easier in several ways:

- **Start a bisect from more views.** The bisect menu (`b`) is now also
available in the local branches, remote branches, tags, sub-commits and
reflog views. This makes it possible to mark a branch or tag as good or
bad without checking it out first. Once git knows both a good and a bad
commit, lazygit focuses the commits panel, where the bisect continues.
- **Select the commit to test.** Whenever git picks a new commit to
test, lazygit selects it. Before, this happened only when you marked the
commit to test in lazygit while it was selected. Now it also happens
when lazygit starts during a bisect, when you run `git bisect` commands
in a terminal, and when the commit is further down than the commits that
lazygit loads at first.
- **Select the result.** When the bisect is done, lazygit selects the
first bad commit, so that you can look at it right away. Pressing `b`
afterwards shows the result again and offers to reset the bisect.
Before, the menu offered to mark the commit that you had just marked.
- **Show the log of HEAD until both a good and a bad commit are known.**
If you check out another branch to find a commit to mark, the commits
panel shows that branch.
- **Focus the commits panel at startup** if a bisect is in progress.
- **Mark the remaining commits correctly.** The "?" marks for the
commits that git still has to consider now come from git. They were
missing while git tests the merge base of a good and a bad commit on
different branches, and early in a long bisect.
- **Fix labels.** Before git has picked a commit to test, the bisect
menu names the selected commit instead of calling it the "current
commit". Once the bisect is done, the last tested commit shows its mark
instead of "current".

Closes #2346.
Closes #2941.
Closes #5530.
Closes #6103.
2026-10-31 15:12:18 +01:00
Stefan HallerandClaude Opus 5.5 0b87c9e610 Ask git which commits a bisect hasn't ruled out yet
The commits view marks the commits that a bisect hasn't ruled out yet
with a "?". To find them, it looks for the bad commit and the first good
commit in the list, and marks the commits between them. If there's no
good commit in the list, it marks none. This is the case while git
tests the merge base of a good and a bad commit on different branches.
It's also the case early in a long bisect, when the good commit is
further down than the commits that we load. With merges, the commits
between the two can also include some that the bisect has ruled out.

Ask git instead, with "git rev-list <bad> --not <good>...", when
refreshing the commits while git knows both a good and a bad commit.
This takes about as long as the git log that loads the commits, or
less, and it gets faster as the bisect goes on. Keep only the commits
that the commits view has loaded, because git may list many more.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 12:17:43 +01:00
Stefan HallerandClaude Opus 5.5 fd7178a3a9 Add tests showing that some commits to bisect lack the "?" mark
The commits view marks the commits between the bad commit and the first
good commit in the list with a "?". If there's no good commit in the
list, it doesn't mark any. This is the case while git tests the merge
base of a good and a bad commit on different branches. It's also the
case early in a long bisect, when the good commit is further down than
the commits that we load.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 12:15:16 +01:00
Stefan HallerandClaude Opus 5.5 368633f69c Show the result of a finished bisect instead of the bisect menu
Once git has nothing left to test, the bisect menu offers marking or
skipping the current commit, although the user has marked it already. If
git found the first bad commit without checking out anything to test,
the menu acts on the selected commit instead, as if git didn't know both
a good and a bad commit yet. At this point, the user wants to see the
result and reset the bisect.

Show the "Bisect complete" dialog again instead of the menu. It names
the first bad commit and asks whether to reset the bisect. This also
lets the user see the result again after declining the reset when the
bisect finished.

If git stopped without finding the first bad commit, because the merge
base of the good and the bad commit is bad, show the confirmation for
resetting the bisect. Git said why it stopped when it did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:27:58 +01:00
Stefan HallerandClaude Opus 5.5 5284818424 Add tests showing that the bisect menu offers marks once a bisect is done
Once git has nothing left to test, the bisect menu still offers marking
or skipping the current commit, although the user has marked it
already. This also happens if git stopped because the merge base of the
good and the bad commit is bad. If git found the first bad commit
without checking out anything to test, the menu acts on the selected
commit, as if git didn't know both a good and a bad commit yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:27:58 +01:00
Stefan HallerandClaude Opus 5.5 ba05949879 Show the mark of the last tested commit once a bisect is done
Once git has nothing left to test, the commit that the user marked last
stays the current commit. The commits view labels it "<-- current",
although git doesn't want the user to test it any more. Show its mark
instead, like for the other marked commits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:27:58 +01:00
Stefan HallerandClaude Opus 5.5 21569fa96f Add a test showing that a finished bisect labels the last commit "current"
Once git has nothing left to test, the commit that the user marked last
stays the current commit. The commits view labels it "<-- current",
although it is marked good or bad now, and git doesn't want the user to
test it any more.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:27:58 +01:00
Stefan HallerandClaude Opus 5.5 32d07f2b38 Select the first bad commit once a bisect is done
When the user marks the last commit to test, the commits view keeps it
selected, and the user has to look for the result. The same happens
when lazygit starts after a bisect has finished.

After a mark, git checks out the next commit to test, unless there's
nothing left to test. In that case, the commit that the user marked
last is still the current commit, and the commit marked bad is the
first bad commit. So once the current commit is marked, select the
commit marked bad instead. Do the same if git never checked out a
commit to test, because the good commit is a parent of the bad one.

If only skipped commits are left to test, the first bad commit is
either one of them or the commit marked bad. Select the commit marked
bad then too; it's the newest of the candidates.

The commits view loads the commits down to the commit that it selects,
so this one is always loaded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:27:58 +01:00