Skip to content

Commit 72895f4

Browse files
codexByron
andcommitted
fix: make submodule.update() after submodule.deinit() work
Git's `submodule deinit` command removes the submodule checkout but retains its repository under the parent repository's `.git/modules` directory. Submodule.update() previously treated the missing checkout as a completely uninitialized submodule and attempted to clone it again. The clone could not reuse the existing module repository, preventing a deinitialized submodule from being initialized again through GitPython. Detect a valid retained repository before entering the clone path. Restore the checkout's `.git` file and the module repository's worktree configuration, reset the retained repository to recreate its index and working tree, and restore the submodule URL in the parent configuration. Continue using the existing clone behavior when no valid retained repository is available. Co-authored-by: Sebastian Thiel <sebastian.thiel@icloud.com>
1 parent e87854b commit 72895f4

2 files changed

Lines changed: 142 additions & 71 deletions

File tree

git/objects/submodule/base.py

Lines changed: 69 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -779,82 +779,80 @@ def update(
779779
return self
780780
# END early abort if init is not allowed
781781

782-
# There is no git-repository yet - but delete empty paths.
783782
checkout_module_abspath = self.abspath
784-
if not dry_run and osp.isdir(checkout_module_abspath):
783+
module_abspath = self._module_abspath(self.repo, self.path, self.name)
784+
785+
# ``git submodule deinit`` leaves the repository in
786+
# ``.git/modules`` and empties the checkout. Reconnect that retained
787+
# repository instead of trying to clone over it.
788+
if not dry_run and osp.isdir(module_abspath):
785789
try:
786-
os.rmdir(checkout_module_abspath)
787-
except OSError as e:
788-
raise OSError(
789-
"Module directory at %r does already exist and is non-empty" % checkout_module_abspath
790-
) from e
791-
# END handle OSError
792-
# END handle directory removal
793-
794-
# Don't check it out at first - nonetheless it will create a local
795-
# branch according to the remote-HEAD if possible.
796-
progress.update(
797-
BEGIN | CLONE,
798-
0,
799-
1,
800-
prefix
801-
+ "Cloning url '%s' to '%s' in submodule %r" % (self.url, checkout_module_abspath, self.name),
802-
)
803-
if not dry_run:
804-
if self.url.startswith("."):
805-
url = urllib.parse.urljoin(self.repo.remotes.origin.url + "/", self.url)
790+
git.Repo(module_abspath)
791+
except InvalidGitRepositoryError:
792+
pass
806793
else:
807-
url = self.url
808-
mrepo = self._clone_repo(
809-
self.repo,
810-
url,
811-
self.path,
812-
self.name,
813-
n=True,
814-
env=env,
815-
multi_options=clone_multi_options,
816-
allow_unsafe_options=allow_unsafe_options,
817-
allow_unsafe_protocols=allow_unsafe_protocols,
794+
os.makedirs(checkout_module_abspath, exist_ok=True)
795+
self._write_git_file_and_module_config(checkout_module_abspath, module_abspath)
796+
mrepo = git.Repo(checkout_module_abspath)
797+
mrepo.head.reset(mrepo.head.commit, index=True, working_tree=True)
798+
with self.repo.config_writer() as writer:
799+
writer.set_value(sm_section(self.name), "url", self.url)
800+
801+
if mrepo is None:
802+
# There is no git-repository yet - but delete empty paths.
803+
if not dry_run and osp.isdir(checkout_module_abspath):
804+
try:
805+
os.rmdir(checkout_module_abspath)
806+
except OSError as e:
807+
raise OSError(
808+
"Module directory at %r does already exist and is non-empty" % checkout_module_abspath
809+
) from e
810+
# END handle directory removal
811+
812+
# Don't check it out at first - nonetheless it will create a local
813+
# branch according to the remote-HEAD if possible.
814+
progress.update(
815+
BEGIN | CLONE,
816+
0,
817+
1,
818+
prefix
819+
+ "Cloning url '%s' to '%s' in submodule %r" % (self.url, checkout_module_abspath, self.name),
818820
)
819-
# END handle dry-run
820-
progress.update(
821-
END | CLONE,
822-
0,
823-
1,
824-
prefix + "Done cloning to %s" % checkout_module_abspath,
825-
)
826-
827-
if not dry_run:
828-
# See whether we have a valid branch to check out.
829-
try:
830-
mrepo = cast("Repo", mrepo)
831-
# Find a remote which has our branch - we try to be flexible.
832-
remote_branch = find_first_remote_branch(mrepo.remotes, self.branch_name)
833-
local_branch = mkhead(mrepo, self.branch_path)
834-
835-
# Have a valid branch, but no checkout - make sure we can figure
836-
# that out by marking the commit with a null_sha.
837-
local_branch.set_object(Object(mrepo, self.NULL_BIN_SHA))
838-
# END initial checkout + branch creation
839-
840-
# Make sure HEAD is not detached.
841-
mrepo.head.set_reference(
842-
local_branch,
843-
logmsg="submodule: attaching head to %s" % local_branch,
821+
if not dry_run:
822+
if self.url.startswith("."):
823+
url = urllib.parse.urljoin(self.repo.remotes.origin.url + "/", self.url)
824+
else:
825+
url = self.url
826+
mrepo = self._clone_repo(
827+
self.repo,
828+
url,
829+
self.path,
830+
self.name,
831+
n=True,
832+
env=env,
833+
multi_options=clone_multi_options,
834+
allow_unsafe_options=allow_unsafe_options,
835+
allow_unsafe_protocols=allow_unsafe_protocols,
844836
)
845-
mrepo.head.reference.set_tracking_branch(remote_branch)
846-
except (IndexError, InvalidGitRepositoryError):
847-
_logger.warning("Failed to checkout tracking branch %s", self.branch_path)
848-
# END handle tracking branch
849-
850-
# NOTE: Have to write the repo config file as well, otherwise the
851-
# default implementation will be offended and not update the
852-
# repository. Maybe this is a good way to ensure it doesn't get into
853-
# our way, but we want to stay backwards compatible too... It's so
854-
# redundant!
855-
with self.repo.config_writer() as writer:
856-
writer.set_value(sm_section(self.name), "url", self.url)
857-
# END handle dry_run
837+
progress.update(END | CLONE, 0, 1, prefix + "Done cloning to %s" % checkout_module_abspath)
838+
839+
if not dry_run:
840+
# See whether we have a valid branch to check out.
841+
try:
842+
mrepo = cast("Repo", mrepo)
843+
remote_branch = find_first_remote_branch(mrepo.remotes, self.branch_name)
844+
local_branch = mkhead(mrepo, self.branch_path)
845+
local_branch.set_object(Object(mrepo, self.NULL_BIN_SHA))
846+
mrepo.head.set_reference(
847+
local_branch,
848+
logmsg="submodule: attaching head to %s" % local_branch,
849+
)
850+
mrepo.head.reference.set_tracking_branch(remote_branch)
851+
except (IndexError, InvalidGitRepositoryError):
852+
_logger.warning("Failed to checkout tracking branch %s", self.branch_path)
853+
854+
with self.repo.config_writer() as writer:
855+
writer.set_value(sm_section(self.name), "url", self.url)
858856
# END handle initialization
859857

860858
# DETERMINE SHAS TO CHECK OUT

test/test_submodule.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,79 @@ def test_deinit_calls_git_submodule(self, rwdir):
728728

729729
git_submodule.assert_called_once_with("deinit", "--force", "--", submodule.path)
730730

731+
@with_rw_directory
732+
def test_update_after_deinit(self, rwdir):
733+
source_path = osp.join(rwdir, "source")
734+
source_repo = git.Repo.init(source_path)
735+
touch(osp.join(source_path, "file"))
736+
source_repo.index.add(["file"])
737+
source_repo.index.commit("initial commit")
738+
739+
parent_path = osp.join(rwdir, "parent")
740+
parent_repo = git.Repo.init(parent_path)
741+
submodule = parent_repo.create_submodule("module", "module", source_path)
742+
parent_repo.index.commit("add submodule")
743+
744+
submodule.deinit()
745+
assert not submodule.module_exists()
746+
assert osp.isdir(osp.join(parent_repo.git_dir, "modules", submodule.name))
747+
748+
submodule.update()
749+
750+
assert submodule.module_exists()
751+
assert submodule.module().head.commit == source_repo.head.commit
752+
assert osp.isfile(osp.join(submodule.abspath, "file"))
753+
754+
@with_rw_directory
755+
def test_update_after_deinit_without_init_does_not_restore_checkout(self, rwdir):
756+
source_path = osp.join(rwdir, "source")
757+
source_repo = git.Repo.init(source_path)
758+
source_repo.git.commit(m="initial commit", allow_empty=True)
759+
760+
parent_repo = git.Repo.init(osp.join(rwdir, "parent"))
761+
submodule = parent_repo.create_submodule("module", "module", source_path)
762+
parent_repo.index.commit("add submodule")
763+
submodule.deinit()
764+
765+
assert submodule.update(init=False) is submodule
766+
assert not submodule.module_exists()
767+
assert not osp.exists(osp.join(submodule.abspath, ".git"))
768+
769+
@with_rw_directory
770+
def test_dry_run_update_after_deinit_does_not_restore_checkout(self, rwdir):
771+
source_path = osp.join(rwdir, "source")
772+
source_repo = git.Repo.init(source_path)
773+
source_repo.git.commit(m="initial commit", allow_empty=True)
774+
775+
parent_repo = git.Repo.init(osp.join(rwdir, "parent"))
776+
submodule = parent_repo.create_submodule("module", "module", source_path)
777+
parent_repo.index.commit("add submodule")
778+
submodule.deinit()
779+
780+
assert submodule.update(dry_run=True) is submodule
781+
assert not submodule.module_exists()
782+
assert not osp.exists(osp.join(submodule.abspath, ".git"))
783+
784+
@with_rw_directory
785+
def test_update_after_deinit_restores_nested_checkout(self, rwdir):
786+
source_path = osp.join(rwdir, "source")
787+
source_repo = git.Repo.init(source_path)
788+
touch(osp.join(source_path, "file"))
789+
source_repo.index.add(["file"])
790+
source_repo.index.commit("initial commit")
791+
792+
parent_repo = git.Repo.init(osp.join(rwdir, "parent"))
793+
submodule = parent_repo.create_submodule("nested/module", "deps/module", source_path)
794+
parent_repo.index.commit("add nested submodule")
795+
submodule.deinit()
796+
797+
submodule.update()
798+
799+
module_repo = submodule.module()
800+
assert module_repo.head.commit == source_repo.head.commit
801+
assert osp.isfile(osp.join(submodule.abspath, "file"))
802+
assert osp.samefile(module_repo.git_dir, osp.join(parent_repo.git_dir, "modules", submodule.name))
803+
731804
@with_rw_repo(k_no_subm_tag, bare=False)
732805
def test_first_submodule(self, rwrepo):
733806
assert len(list(rwrepo.iter_submodules())) == 0

0 commit comments

Comments
 (0)