Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 3d061a1

Browse filesBrowse files
committed
Implemented deletion of submodules including proper tests
1 parent 78d2cd6 commit 3d061a1
Copy full SHA for 3d061a1

File tree

2 files changed

+173
-3
lines changed
Filter options

2 files changed

+173
-3
lines changed

‎lib/git/objects/submodule.py

Copy file name to clipboardExpand all lines: lib/git/objects/submodule.py
+123-3Lines changed: 123 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@
1010
import os
1111
import sys
1212
import weakref
13+
import shutil
1314

1415
__all__ = ("Submodule", "RootModule")
1516

1617
#{ Utilities
1718

18-
def sm_section(path):
19+
def sm_section(name):
1920
""":return: section title used in .gitmodules configuration file"""
20-
return 'submodule "%s"' % path
21+
return 'submodule "%s"' % name
2122

2223
def sm_name(section):
2324
""":return: name of the submodule as parsed from the section name"""
@@ -223,6 +224,7 @@ def update(self, recursive=False, init=True, to_latest_revision=False):
223224
if the remote repository had a master branch, or of the 'branch' option
224225
was specified for this submodule and the branch existed remotely
225226
:note: does nothing in bare repositories
227+
:note: method is definitely not atomic if recurisve is True
226228
:return: self"""
227229
if self.repo.bare:
228230
return self
@@ -329,6 +331,111 @@ def update(self, recursive=False, init=True, to_latest_revision=False):
329331

330332
return self
331333

334+
def remove(self, module=True, force=False, configuration=True, dry_run=False):
335+
"""Remove this submodule from the repository. This will remove our entry
336+
from the .gitmodules file and the entry in the .git/config file.
337+
:param module: If True, the module we point to will be deleted
338+
as well. If the module is currently on a commit which is not part
339+
of any branch in the remote, if the currently checked out branch
340+
is ahead of its tracking branch, if you have modifications in the
341+
working tree, or untracked files,
342+
In case the removal of the repository fails for these reasons, the
343+
submodule status will not have been altered.
344+
If this submodule has child-modules on its own, these will be deleted
345+
prior to touching the own module.
346+
:param force: Enforces the deletion of the module even though it contains
347+
modifications. This basically enforces a brute-force file system based
348+
deletion.
349+
:param configuration: if True, the submodule is deleted from the configuration,
350+
otherwise it isn't. Although this should be enabled most of the times,
351+
this flag enables you to safely delete the repository of your submodule.
352+
:param dry_run: if True, we will not actually do anything, but throw the errors
353+
we would usually throw
354+
:note: doesn't work in bare repositories
355+
:raise InvalidGitRepositoryError: thrown if the repository cannot be deleted
356+
:raise OSError: if directories or files could not be removed"""
357+
if self.repo.bare:
358+
raise InvalidGitRepositoryError("Cannot delete a submodule in bare repository")
359+
# END handle bare mode
360+
361+
if not (module + configuration):
362+
raise ValueError("Need to specify to delete at least the module, or the configuration")
363+
# END handle params
364+
365+
# DELETE MODULE REPOSITORY
366+
##########################
367+
if module and self.module_exists():
368+
if force:
369+
# take the fast lane and just delete everything in our module path
370+
# TODO: If we run into permission problems, we have a highly inconsistent
371+
# state. Delete the .git folders last, start with the submodules first
372+
mp = self.module_path()
373+
method = None
374+
if os.path.islink(mp):
375+
method = os.remove
376+
elif os.path.isdir(mp):
377+
method = shutil.rmtree
378+
elif os.path.exists(mp):
379+
raise AssertionError("Cannot forcibly delete repository as it was neither a link, nor a directory")
380+
#END handle brutal deletion
381+
if not dry_run:
382+
assert method
383+
method(mp)
384+
#END apply deletion method
385+
else:
386+
# verify we may delete our module
387+
mod = self.module()
388+
if mod.is_dirty(untracked_files=True):
389+
raise InvalidGitRepositoryError("Cannot delete module at %s with any modifications, unless force is specified" % mod.working_tree_dir)
390+
# END check for dirt
391+
392+
# figure out whether we have new commits compared to the remotes
393+
# NOTE: If the user pulled all the time, the remote heads might
394+
# not have been updated, so commits coming from the remote look
395+
# as if they come from us. But we stay strictly read-only and
396+
# don't fetch beforhand.
397+
for remote in mod.remotes:
398+
num_branches_with_new_commits = 0
399+
rrefs = remote.refs
400+
for rref in rrefs:
401+
num_branches_with_new_commits = len(mod.git.cherry(rref)) != 0
402+
# END for each remote ref
403+
# not a single remote branch contained all our commits
404+
if num_branches_with_new_commits == len(rrefs):
405+
raise InvalidGitRepositoryError("Cannot delete module at %s as there are new commits" % mod.working_tree_dir)
406+
# END handle new commits
407+
# END for each remote
408+
409+
# gently remove all submodule repositories
410+
for sm in self.children():
411+
sm.remove(module=True, force=False, configuration=False, dry_run=dry_run)
412+
# END for each child-submodule
413+
414+
# finally delete our own submodule
415+
if not dry_run:
416+
shutil.rmtree(mod.working_tree_dir)
417+
# END delete tree if possible
418+
# END handle force
419+
# END handle module deletion
420+
421+
# DELETE CONFIGURATION
422+
######################
423+
if configuration and not dry_run:
424+
# first the index-entry
425+
index = self.repo.index
426+
try:
427+
del(index.entries[index.entry_key(self.path, 0)])
428+
except KeyError:
429+
pass
430+
#END delete entry
431+
index.write()
432+
433+
# now git config - need the config intact, otherwise we can't query
434+
# inforamtion anymore
435+
self.repo.config_writer().remove_section(sm_section(self.name))
436+
self.config_writer().remove_section()
437+
# END delete configuration
438+
332439
def set_parent_commit(self, commit, check=True):
333440
"""Set this instance to use the given commit whose tree is supposed to
334441
contain the .gitmodules blob.
@@ -410,10 +517,23 @@ def module_exists(self):
410517
try:
411518
self.module()
412519
return True
413-
except InvalidGitRepositoryError:
520+
except Exception:
414521
return False
415522
# END handle exception
416523

524+
def exists(self):
525+
""":return: True if the submodule exists, False otherwise. Please note that
526+
a submodule may exist (in the .gitmodules file) even though its module
527+
doesn't exist"""
528+
self._clear_cache()
529+
try:
530+
self.path
531+
return True
532+
except Exception:
533+
# we raise if the path cannot be restored from configuration
534+
return False
535+
# END handle exceptions
536+
417537
@property
418538
def branch(self):
419539
""":return: The branch name that we are to checkout"""

‎test/git/test_submodule.py

Copy file name to clipboardExpand all lines: test/git/test_submodule.py
+50Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ def _do_base_tests(self, rwrepo):
8787
# module retrieval is not always possible
8888
if rwrepo.bare:
8989
self.failUnlessRaises(InvalidGitRepositoryError, sm.module)
90+
self.failUnlessRaises(InvalidGitRepositoryError, sm.remove)
9091
else:
9192
# its not checked out in our case
9293
self.failUnlessRaises(InvalidGitRepositoryError, sm.module)
@@ -155,6 +156,55 @@ def _do_base_tests(self, rwrepo):
155156
# undo the changes
156157
sm.module().head.ref = smref
157158
csm.module().head.ref.set_tracking_branch(csm_tracking_branch)
159+
160+
# REMOVAL OF REPOSITOTRY
161+
########################
162+
# must delete something
163+
self.failUnlessRaises(ValueError, csm.remove, module=False, configuration=False)
164+
# We have modified the configuration, hence the index is dirty, and the
165+
# deletion will fail
166+
# NOTE: As we did a few updates in the meanwhile, the indices where reset
167+
# Hence we restore some changes
168+
sm.config_writer().set_value("somekey", "somevalue")
169+
csm.config_writer().set_value("okey", "ovalue")
170+
self.failUnlessRaises(InvalidGitRepositoryError, sm.remove)
171+
# if we remove the dirty index, it would work
172+
sm.module().index.reset()
173+
# still, we have the file modified
174+
self.failUnlessRaises(InvalidGitRepositoryError, sm.remove, dry_run=True)
175+
sm.module().index.reset(working_tree=True)
176+
177+
# this would work
178+
sm.remove(dry_run=True)
179+
assert sm.module_exists()
180+
sm.remove(force=True, dry_run=True)
181+
assert sm.module_exists()
182+
183+
# but ... we have untracked files in the child submodule
184+
fn = join_path_native(csm.module().working_tree_dir, "newfile")
185+
open(fn, 'w').write("hi")
186+
self.failUnlessRaises(InvalidGitRepositoryError, sm.remove)
187+
188+
# forcibly delete the child repository
189+
csm.remove(force=True)
190+
assert not csm.exists()
191+
assert not csm.module_exists()
192+
assert len(sm.children()) == 0
193+
# now we have a changed index, as configuration was altered.
194+
# fix this
195+
sm.module().index.reset(working_tree=True)
196+
197+
# now delete only the module of the main submodule
198+
assert sm.module_exists()
199+
sm.remove(configuration=False)
200+
assert sm.exists()
201+
assert not sm.module_exists()
202+
assert sm.config_reader().get_value('url')
203+
204+
# delete the rest
205+
sm.remove()
206+
assert not sm.exists()
207+
assert not sm.module_exists()
158208
# END handle bare mode
159209

160210

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.