Version
v7.8.0.202609011348-r-10-g87ec21000
Operating System
Linux/Unix
Bug description
SubmoduleDeinitCommand can deinitialize a dirty requested submodule without
force when another earlier submodule entry records the same object ID.
The command is expected to check the requested path and refuse to remove its
worktree when it contains local changes. In the reproducing repository,
submodules a and b both point to the same commit. a is clean and b
contains an untracked file. The call below requests only b and does not call
setForce(true):
new SubmoduleDeinitCommand(db).addPath("b").call();
call() initially walks the requested path, but checkDirty() then opens a
second SubmoduleWalk.forIndex(repo) without a path filter and examines only
the first index entry. Because a appears before b and has the same object
ID, the status of clean submodule a is used for the requested path b.
The command consequently proceeds to deinit("b"), which recursively deletes
the contents of b.
Relevant source at the tested revision:
Actual behavior
The requested dirty submodule is reported as successfully deinitialized, and
its untracked file is deleted even though force was not requested:
OBSERVED deinit requested=b status=SUCCESS dirty_file_exists=false
The control case, with only submodule b present, correctly detects the same
dirty file:
CONTROL deinit only=b status=DIRTY dirty_file_exists=true
Expected behavior
Because b contains local modifications and force was not requested, the
command should return SubmoduleDeinitStatus.DIRTY for b and leave all files
under b intact. This is also the behavior documented by setForce(false).
Relevant log output
OBSERVED deinit requested=b status=SUCCESS dirty_file_exists=false
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS
CONTROL deinit only=b status=DIRTY dirty_file_exists=true
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS
Other information
The standalone reproducer is included in the accompanying archive:
reproduce.sh copies reproduce_test.java into
org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule, runs the two tests,
records the environment and revision, and removes the temporary test source
and generated test artifacts.
- The test source is
org.eclipse.jgit.submodule.SubmoduleDeinitReproductionTest.
- Run it from the repository root with:
findings/submodules/jgit-deinit-unfiltered-dirty-check/reproduce.sh \
findings/submodules/jgit-deinit-unfiltered-dirty-check/evidence/20260909
The effective Maven command uses -Pjavac -pl org.eclipse.jgit.test -am and
the following environment was used:
Git 2.39.5
Java openjdk version "17.0.20.1" 2026-08-18
Maven 3.8.7
The issue causes local data loss: uncommitted, untracked, generated, or other
files in the requested submodule can be removed by a non-forced deinit. The
demonstration requires two submodule entries with the same recorded object ID,
an initialized dirty target submodule, and an application or user invoking
deinit for that target. It does not demonstrate deletion outside the requested
submodule, code execution, privilege escalation, or a remote-only trigger.
The complete reproduction output is in
evidence/20260909/run.log. The implementation contract is documented by
setForce(),
which states that deinitialization should refuse local modifications unless
force is enabled.
Attachments
jgit-deinit-unfiltered-dirty-check.zip
Version
v7.8.0.202609011348-r-10-g87ec21000
Operating System
Linux/Unix
Bug description
SubmoduleDeinitCommandcan deinitialize a dirty requested submodule withoutforce when another earlier submodule entry records the same object ID.
The command is expected to check the requested path and refuse to remove its
worktree when it contains local changes. In the reproducing repository,
submodules
aandbboth point to the same commit.ais clean andbcontains an untracked file. The call below requests only
band does not callsetForce(true):call()initially walks the requested path, butcheckDirty()then opens asecond
SubmoduleWalk.forIndex(repo)without a path filter and examines onlythe first index entry. Because
aappears beforeband has the same objectID, the status of clean submodule
ais used for the requested pathb.The command consequently proceeds to
deinit("b"), which recursively deletesthe contents of
b.Relevant source at the tested revision:
call()and the deletion decisioncheckDirty()deinit()Actual behavior
The requested dirty submodule is reported as successfully deinitialized, and
its untracked file is deleted even though force was not requested:
The control case, with only submodule
bpresent, correctly detects the samedirty file:
Expected behavior
Because
bcontains local modifications and force was not requested, thecommand should return
SubmoduleDeinitStatus.DIRTYforband leave all filesunder
bintact. This is also the behavior documented bysetForce(false).Relevant log output
Other information
The standalone reproducer is included in the accompanying archive:
reproduce.shcopiesreproduce_test.javaintoorg.eclipse.jgit.test/tst/org/eclipse/jgit/submodule, runs the two tests,records the environment and revision, and removes the temporary test source
and generated test artifacts.
org.eclipse.jgit.submodule.SubmoduleDeinitReproductionTest.The effective Maven command uses
-Pjavac -pl org.eclipse.jgit.test -amandthe following environment was used:
The issue causes local data loss: uncommitted, untracked, generated, or other
files in the requested submodule can be removed by a non-forced deinit. The
demonstration requires two submodule entries with the same recorded object ID,
an initialized dirty target submodule, and an application or user invoking
deinit for that target. It does not demonstrate deletion outside the requested
submodule, code execution, privilege escalation, or a remote-only trigger.
The complete reproduction output is in
evidence/20260909/run.log. The implementation contract is documented bysetForce(),which states that deinitialization should refuse local modifications unless
force is enabled.
Attachments
jgit-deinit-unfiltered-dirty-check.zip