Skip to content

Fix unstaging a submodule with dirty content#5666

Merged
stefanhaller merged 6 commits into
masterfrom
submodule-staging
Jun 4, 2026
Merged

Fix unstaging a submodule with dirty content#5666
stefanhaller merged 6 commits into
masterfrom
submodule-staging

Conversation

@stefanhaller

@stefanhaller stefanhaller commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

Staging and unstaging submodules didn't work properly when the submodule had uncommitted changes of its own (modified or untracked files inside it). This PR fixes a few related problems:

  • Unstaging a submodule that has both a new commit and uncommitted changes now works. Previously, staging such a submodule left it half-staged, and from there the stage key would only ever try to re-stage it — there was no way to unstage it again. Now the stage key toggles it back to unstaged as expected.

  • Trying to stage a submodule that has nothing stageable now explains why. When a submodule's only changes are uncommitted content inside it (with no new commit), there's nothing the parent repository can stage. Instead of the keypress silently doing nothing (except briefly flashing the status to staged and then back to unstaged), lazygit now shows an error explaining that you need to commit inside the submodule first.

  • The stage key (space) and the stage-all key (a) now behave consistently. All of the above applies equally whether you act on the submodule directly or use "stage all", and "stage all" no longer gets stuck or behaves differently from the stage key just because a submodule with uncommitted changes is present in the list.

Fixes #3641.

@stefanhaller stefanhaller added the bug Something isn't working label Jun 2, 2026
@stefanhaller stefanhaller changed the title Submodule staging Fix unstaging a submodule with dirty content Jun 2, 2026
stefanhaller and others added 6 commits June 4, 2026 09:12
When a submodule has both a new commit (which the parent repo can stage)
and dirty working-tree content (which it can't), staging it lands on a
"MM" status. Pressing space again should unstage it, but instead it tries
to stage the dirty content over and over, so you can never get back to an
unstaged state.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pressWithLock (acting on the selection) and toggleStagedAllWithLock (acting
on the whole tree) each independently decided whether to stage or unstage,
ran the optimistic update, and logged the action. That duplicated decision
has already drifted: the tracked-files filter was added to press months
before it was applied to stage-all, and fixes to one have repeatedly had to
be chased into the other.

Extract that shared decision into toggleStaged, leaving each caller to
supply only the git commands it runs (per-path for the selection, bulk
add -A / reset for the whole tree — the latter is required because the tree
root node has an empty path, so a per-path stage wouldn't work). This is a
pure refactor: the two callers' decisions were already equivalent, so
behavior is unchanged. It exists so the next change to the staging logic
only has to be made once.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The stage/unstage toggle decides what to do based on whether a node has
unstaged changes: if it does, it stages; otherwise it unstages. For a
submodule this breaks down, because dirty or untracked content inside the
submodule always reports as an unstaged change in the parent repo but can
never be staged from there. Once such a submodule's commit pointer is
staged it sits at "MM", and every subsequent press keeps trying to stage
the unstageable dirty content, so it can never be unstaged.

Treat a submodule's unstaged change as stageable only when its commit
isn't already staged, so that a staged submodule unstages on the next
press regardless of leftover dirty content. Because the decision is now
shared by press and stage-all, this fixes both at once.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Before the staging decision was unified, the stage (space) and stage-all
(a) keybindings each made their own decision, so a fix to one wouldn't
reach the other. Extend the test to drive the submodule through stage-all
as well, guarding against that asymmetry coming back.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This map only feeds the optimistic rendering that makes staging feel
instant; it doesn't affect the eventual status, which git reports after
the refresh. The "MM" entry can never be reached for a regular file: a
file at "MM" has stageable unstaged changes, so pressing space stages it
rather than unstaging, and the unstage path is where this map is used. The
only thing that reaches the unstage path at "MM" is a submodule whose
commit is staged on top of dirty content, so this entry exists purely to
update that submodule instantly instead of waiting for the next git
status.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A submodule that only has dirty or untracked content (no new commit) can't
be staged from the parent repo, but it still shows up as having unstaged
changes. Pressing stage on it therefore briefly flashed as staged and then
reverted, without explaining why nothing was staged.

Detect this case (via `git submodule status`, where a '+' prefix marks a
stageable commit change) in the shared stage/unstage decision: if the only
thing that looks stageable is such a submodule, don't try to stage it.
Instead unstage if there's anything staged to unstage, so the toggle stays
symmetric; otherwise show an error explaining that there's nothing to stage.

Because the decision is shared, this covers both the stage (space) and
stage-all (a) keybindings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@stefanhaller stefanhaller enabled auto-merge June 4, 2026 07:14
@stefanhaller stefanhaller merged commit 68f3bcf into master Jun 4, 2026
13 checks passed
@stefanhaller stefanhaller deleted the submodule-staging branch June 4, 2026 07:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unable to unstage submodule changes when there are uncommitted changes within the submodule

1 participant