If another git process takes index.lock while a rebase is replaying
commits, git stops the rebase and reschedules the todo that failed. The
git command runner then ran the whole "git rebase --interactive" command
again. This failed because the rebase was already in progress, and the
user was left with a rebase that stopped halfway.
Continue the rebase with "git rebase --continue" instead; this runs the
rescheduled todo again. Only do this if the todo files show that git
rescheduled it. In some cases git doesn't, for example if the lock stops
a pick after it has applied the changes but before it has committed
them. Continuing is not safe in that case, so give up and show the
error.
If the lock stops the command before the rebase starts, git cleans up
after itself, and we run the command again as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
If another git process takes index.lock while a rebase is replaying
commits (for example an IDE running "git status" in the background),
git stops the rebase and reschedules the command that failed. Lazygit
then runs the whole "git rebase --interactive" command again. This
fails with "It seems that there is already a rebase-merge directory",
and the user is left with a rebase that stopped halfway.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The next commit needs a "git rebase --continue" command with this setup
that the git command runner runs, rather than one that we run directly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The git command runner runs a command again if it failed because
another git process held a lock. This is wrong for a command that starts
an operation of several steps, such as a rebase or a cherry-pick of
several commits, if the lock stopped it partway through. Running it
again then fails because the operation is already in progress.
Add a hook that such a command can use to tell the runner what to do
instead. The following commits use it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The repository doesn't say how to report a vulnerability privately, and
#6185 was reported in a public issue. Point reporters to GitHub's
private vulnerability reporting.
The repository doesn't say how to report a vulnerability privately,
and #6185 was reported in a public issue. Point reporters to GitHub's
private vulnerability reporting.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In the Commits panel, the "go to bottom" command (`>`) already has a
special case where it jumps to the beginning of the checked-out branch
first, and only then to the end of the list.
In this PR we take this one step further and also make it stop at the
beginning of each individual branch in a stack. This makes it easier to
navigate stacks of long branches.
Also, we make it stop at the beginning of the rebase todos in an
interactive rebase, and at the bad, current and good commits of a
bisect.
The divergence view shows the remote commits in a section above the
local ones. Pressing '>' now goes to the beginning of the remote section
before it goes on to the bottom of the local one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
During a bisect, the commits panel shows the commits known to be bad at
the top, the known good ones at the bottom, and the ones that the bisect
hasn't ruled out yet between them. Pressing '>' now stops at the
bottommost bad commit, then at the commit to test, and then at the
topmost good commit.
It doesn't stop at each commit marked good. The commits between two of
them are all good, and they don't form a section of their own.
During a bisect, it also doesn't stop at the beginnings of the other
sections, such as the beginning of a branch. The commits panel doesn't
color the commits by whether they are merged while bisecting, so these
would look like arbitrary commits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In a stack of branches, pressing '>' in the commits panel went to the
beginning of the bottommost branch right away. Make it go to the
beginning of the branch that the selection is in first, and then to the
beginning of each branch below it. During a rebase, the update-ref todos
separate the branches among the todos, so stop above each of them too.
The branches of the stack are the ones that the commits list marks, and
that pushing and pulling a stack include.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
During a rebase, cherry-pick or revert, the todos are a section of their
own at the top of the commits panel. Pressing '>' now goes to the first
of them, the bottommost one, before it goes on to the beginning of the
branch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The command went to the commit above the first merged one whenever the
commit below the selection wasn't merged. It did this even if the
selection was below that commit already.
Collect the places where the command can stop, and go to the closest one
below the selection, or to the bottom if there is none. This also makes
it easy to add more places to stop at.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
If the history of a branch interleaves merged and unmerged commits, for
example because master was merged into the branch, pressing '>' on a
merged commit that an unmerged one follows selects a commit above it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The local commits and the sub-commits contexts had identical copies of
it. We are going to make it smarter, so make sure the copies don't
drift apart.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
If a branch was merged into a local main branch, but the main branch
wasn't pushed yet, the commits list still marks the head of the branch
as part of a stack. For example, after merging wip into master locally,
the commits of master show a marker at the head of wip.
Pushing and pulling a stack leave such branches out since #6169, and the
markers disappear as soon as the main branch is pushed. Leave them out
of the markers too, so that the markers show the same stack.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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.
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>
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>
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>
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>
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>
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.
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>
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.
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>
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>
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>
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>
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>
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>
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.
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>