Skip to content

Commit 15527ef

Browse files
authored
Merge pull request #2175 from gitpython-developers/submodule-deinit-fix
fix: make `submodule.update()` after `submodule.deinit()` work
2 parents f89e18c + b9d82c0 commit 15527ef

2 files changed

Lines changed: 255 additions & 100 deletions

File tree

git/objects/submodule/base.py

Lines changed: 110 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -739,122 +739,132 @@ def update(
739739
mrepo = None
740740
# END init mrepo
741741

742+
def fetch_remotes(module_repo: "Repo") -> None:
743+
rmts = module_repo.remotes
744+
len_rmts = len(rmts)
745+
for i, remote in enumerate(rmts):
746+
op = FETCH
747+
if i == 0:
748+
op |= BEGIN
749+
# END handle start
750+
751+
progress.update(
752+
op,
753+
i,
754+
len_rmts,
755+
prefix + "Fetching remote %s of submodule %r" % (remote, self.name),
756+
)
757+
# ===============================
758+
if not dry_run:
759+
remote.fetch(progress=progress)
760+
# END handle dry-run
761+
# ===============================
762+
if i == len_rmts - 1:
763+
op |= END
764+
# END handle end
765+
progress.update(
766+
op,
767+
i,
768+
len_rmts,
769+
prefix + "Done fetching remote of submodule %r" % self.name,
770+
)
771+
# END fetch new data
772+
742773
try:
743774
# ENSURE REPO IS PRESENT AND UP-TO-DATE
744775
#######################################
745776
try:
746777
mrepo = self.module()
747-
rmts = mrepo.remotes
748-
len_rmts = len(rmts)
749-
for i, remote in enumerate(rmts):
750-
op = FETCH
751-
if i == 0:
752-
op |= BEGIN
753-
# END handle start
754-
755-
progress.update(
756-
op,
757-
i,
758-
len_rmts,
759-
prefix + "Fetching remote %s of submodule %r" % (remote, self.name),
760-
)
761-
# ===============================
762-
if not dry_run:
763-
remote.fetch(progress=progress)
764-
# END handle dry-run
765-
# ===============================
766-
if i == len_rmts - 1:
767-
op |= END
768-
# END handle end
769-
progress.update(
770-
op,
771-
i,
772-
len_rmts,
773-
prefix + "Done fetching remote of submodule %r" % self.name,
774-
)
775-
# END fetch new data
778+
fetch_remotes(mrepo)
776779
except InvalidGitRepositoryError:
777780
mrepo = None
778781
if not init:
779782
return self
780783
# END early abort if init is not allowed
781784

782-
# There is no git-repository yet - but delete empty paths.
783785
checkout_module_abspath = self.abspath
784-
if not dry_run and osp.isdir(checkout_module_abspath):
786+
module_abspath = self._module_abspath(self.repo, self.path, self.name)
787+
788+
# ``git submodule deinit`` leaves the repository in
789+
# ``.git/modules`` and empties the checkout. Reconnect that retained
790+
# repository instead of trying to clone over it.
791+
if not dry_run and osp.isdir(module_abspath):
785792
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)
793+
git.Repo(module_abspath)
794+
except InvalidGitRepositoryError:
795+
pass
806796
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,
797+
if osp.lexists(checkout_module_abspath) and (
798+
osp.islink(checkout_module_abspath)
799+
or not osp.isdir(checkout_module_abspath)
800+
or os.listdir(checkout_module_abspath)
801+
):
802+
raise OSError(
803+
"Module directory at %r does already exist and is non-empty" % checkout_module_abspath
804+
)
805+
os.makedirs(checkout_module_abspath, exist_ok=True)
806+
self._write_git_file_and_module_config(checkout_module_abspath, module_abspath)
807+
mrepo = git.Repo(checkout_module_abspath)
808+
mrepo.head.reset(mrepo.head.commit, index=True, working_tree=True)
809+
fetch_remotes(mrepo)
810+
with self.repo.config_writer() as writer:
811+
writer.set_value(sm_section(self.name), "url", self.url)
812+
813+
if mrepo is None:
814+
# There is no git-repository yet - but delete empty paths.
815+
if not dry_run and osp.isdir(checkout_module_abspath):
816+
try:
817+
os.rmdir(checkout_module_abspath)
818+
except OSError as e:
819+
raise OSError(
820+
"Module directory at %r does already exist and is non-empty" % checkout_module_abspath
821+
) from e
822+
# END handle directory removal
823+
824+
# Don't check it out at first - nonetheless it will create a local
825+
# branch according to the remote-HEAD if possible.
826+
progress.update(
827+
BEGIN | CLONE,
828+
0,
829+
1,
830+
prefix
831+
+ "Cloning url '%s' to '%s' in submodule %r" % (self.url, checkout_module_abspath, self.name),
818832
)
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,
833+
if not dry_run:
834+
if self.url.startswith("."):
835+
url = urllib.parse.urljoin(self.repo.remotes.origin.url + "/", self.url)
836+
else:
837+
url = self.url
838+
mrepo = self._clone_repo(
839+
self.repo,
840+
url,
841+
self.path,
842+
self.name,
843+
n=True,
844+
env=env,
845+
multi_options=clone_multi_options,
846+
allow_unsafe_options=allow_unsafe_options,
847+
allow_unsafe_protocols=allow_unsafe_protocols,
844848
)
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
849+
progress.update(END | CLONE, 0, 1, prefix + "Done cloning to %s" % checkout_module_abspath)
850+
851+
if not dry_run:
852+
# See whether we have a valid branch to check out.
853+
try:
854+
mrepo = cast("Repo", mrepo)
855+
remote_branch = find_first_remote_branch(mrepo.remotes, self.branch_name)
856+
local_branch = mkhead(mrepo, self.branch_path)
857+
local_branch.set_object(Object(mrepo, self.NULL_BIN_SHA))
858+
mrepo.head.set_reference(
859+
local_branch,
860+
logmsg="submodule: attaching head to %s" % local_branch,
861+
)
862+
mrepo.head.reference.set_tracking_branch(remote_branch)
863+
except (IndexError, InvalidGitRepositoryError):
864+
_logger.warning("Failed to checkout tracking branch %s", self.branch_path)
865+
866+
with self.repo.config_writer() as writer:
867+
writer.set_value(sm_section(self.name), "url", self.url)
858868
# END handle initialization
859869

860870
# DETERMINE SHAS TO CHECK OUT

test/test_submodule.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,151 @@ 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_to_latest_revision_after_deinit_fetches_remote(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+
touch(osp.join(source_path, "new-file"))
766+
source_repo.index.add(["new-file"])
767+
source_repo.index.commit("advance remote")
768+
769+
submodule.update(to_latest_revision=True)
770+
771+
assert submodule.module().head.commit == source_repo.head.commit
772+
assert osp.isfile(osp.join(submodule.abspath, "new-file"))
773+
774+
@with_rw_directory
775+
def test_update_after_deinit_fetches_new_gitlink_commit(self, rwdir):
776+
source_path = osp.join(rwdir, "source")
777+
source_repo = git.Repo.init(source_path)
778+
source_repo.git.commit(m="initial commit", allow_empty=True)
779+
780+
parent_repo = git.Repo.init(osp.join(rwdir, "parent"))
781+
submodule = parent_repo.create_submodule("module", "module", source_path)
782+
parent_repo.index.commit("add submodule")
783+
submodule.deinit()
784+
785+
touch(osp.join(source_path, "new-file"))
786+
source_repo.index.add(["new-file"])
787+
source_repo.index.commit("advance remote")
788+
parent_repo.git.update_index(
789+
"--cacheinfo",
790+
f"160000,{source_repo.head.commit.hexsha},{submodule.path}",
791+
)
792+
parent_repo.index.commit("advance submodule")
793+
794+
submodule = parent_repo.submodule(submodule.name)
795+
submodule.update()
796+
797+
assert submodule.module().head.commit == source_repo.head.commit
798+
assert osp.isfile(osp.join(submodule.abspath, "new-file"))
799+
800+
@with_rw_directory
801+
def test_update_after_deinit_refuses_non_empty_checkout(self, rwdir):
802+
source_path = osp.join(rwdir, "source")
803+
source_repo = git.Repo.init(source_path)
804+
tracked_file = osp.join(source_path, "file")
805+
with open(tracked_file, "w") as fp:
806+
fp.write("submodule content")
807+
source_repo.index.add(["file"])
808+
source_repo.index.commit("initial commit")
809+
810+
parent_repo = git.Repo.init(osp.join(rwdir, "parent"))
811+
submodule = parent_repo.create_submodule("module", "module", source_path)
812+
parent_repo.index.commit("add submodule")
813+
submodule.deinit()
814+
815+
checkout_file = osp.join(submodule.abspath, "file")
816+
with open(checkout_file, "w") as fp:
817+
fp.write("user content")
818+
819+
with pytest.raises(OSError, match="does already exist and is non-empty"):
820+
submodule.update()
821+
822+
with open(checkout_file) as fp:
823+
assert fp.read() == "user content"
824+
assert not osp.exists(osp.join(submodule.abspath, ".git"))
825+
826+
@with_rw_directory
827+
def test_update_after_deinit_without_init_does_not_restore_checkout(self, rwdir):
828+
source_path = osp.join(rwdir, "source")
829+
source_repo = git.Repo.init(source_path)
830+
source_repo.git.commit(m="initial commit", allow_empty=True)
831+
832+
parent_repo = git.Repo.init(osp.join(rwdir, "parent"))
833+
submodule = parent_repo.create_submodule("module", "module", source_path)
834+
parent_repo.index.commit("add submodule")
835+
submodule.deinit()
836+
837+
assert submodule.update(init=False) is submodule
838+
assert not submodule.module_exists()
839+
assert not osp.exists(osp.join(submodule.abspath, ".git"))
840+
841+
@with_rw_directory
842+
def test_dry_run_update_after_deinit_does_not_restore_checkout(self, rwdir):
843+
source_path = osp.join(rwdir, "source")
844+
source_repo = git.Repo.init(source_path)
845+
source_repo.git.commit(m="initial commit", allow_empty=True)
846+
847+
parent_repo = git.Repo.init(osp.join(rwdir, "parent"))
848+
submodule = parent_repo.create_submodule("module", "module", source_path)
849+
parent_repo.index.commit("add submodule")
850+
submodule.deinit()
851+
852+
assert submodule.update(dry_run=True) is submodule
853+
assert not submodule.module_exists()
854+
assert not osp.exists(osp.join(submodule.abspath, ".git"))
855+
856+
@with_rw_directory
857+
def test_update_after_deinit_restores_nested_checkout(self, rwdir):
858+
source_path = osp.join(rwdir, "source")
859+
source_repo = git.Repo.init(source_path)
860+
touch(osp.join(source_path, "file"))
861+
source_repo.index.add(["file"])
862+
source_repo.index.commit("initial commit")
863+
864+
parent_repo = git.Repo.init(osp.join(rwdir, "parent"))
865+
submodule = parent_repo.create_submodule("nested/module", "deps/module", source_path)
866+
parent_repo.index.commit("add nested submodule")
867+
submodule.deinit()
868+
869+
submodule.update()
870+
871+
module_repo = submodule.module()
872+
assert module_repo.head.commit == source_repo.head.commit
873+
assert osp.isfile(osp.join(submodule.abspath, "file"))
874+
assert osp.samefile(module_repo.git_dir, osp.join(parent_repo.git_dir, "modules", submodule.name))
875+
731876
@with_rw_repo(k_no_subm_tag, bare=False)
732877
def test_first_submodule(self, rwrepo):
733878
assert len(list(rwrepo.iter_submodules())) == 0

0 commit comments

Comments
 (0)