Repository navigation
Description
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:
call()and the deletion decision- unfiltered index walk in
checkDirty() - recursive deletion in
deinit()
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 SUCCESSOther information
The standalone reproducer is included in the accompanying archive:
reproduce.shcopiesreproduce_test.javainto
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/20260909The 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.
Activity
adityaanikam commentedon Sep 12, 2026
Pushed a change for review: https://review.gerrithub.io/c/eclipse-jgit/jgit/+/1249453
N0zoM1z0 commentedon Sep 21, 2026
I tested change 1249453 at revision
672071ea2aa6bcbeeb0bcfad07416f253bada8bawithSubmoduleDeinitTest. All 7 tests pass, including the regression where two submodules share an object ID while only the requested submodule is dirty.The path-filtered index walk addresses the reported issue, and this change looks good to me.