summaryrefslogtreecommitdiff
path: root/lib/git/objects/submodule.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/git/objects/submodule.py')
-rw-r--r--lib/git/objects/submodule.py126
1 files changed, 123 insertions, 3 deletions
diff --git a/lib/git/objects/submodule.py b/lib/git/objects/submodule.py
index 116c53f1..9e8abbd4 100644
--- a/lib/git/objects/submodule.py
+++ b/lib/git/objects/submodule.py
@@ -10,14 +10,15 @@ import stat
import os
import sys
import weakref
+import shutil
__all__ = ("Submodule", "RootModule")
#{ Utilities
-def sm_section(path):
+def sm_section(name):
""":return: section title used in .gitmodules configuration file"""
- return 'submodule "%s"' % path
+ return 'submodule "%s"' % name
def sm_name(section):
""":return: name of the submodule as parsed from the section name"""
@@ -223,6 +224,7 @@ class Submodule(base.IndexObject, Iterable, Traversable):
if the remote repository had a master branch, or of the 'branch' option
was specified for this submodule and the branch existed remotely
:note: does nothing in bare repositories
+ :note: method is definitely not atomic if recurisve is True
:return: self"""
if self.repo.bare:
return self
@@ -329,6 +331,111 @@ class Submodule(base.IndexObject, Iterable, Traversable):
return self
+ def remove(self, module=True, force=False, configuration=True, dry_run=False):
+ """Remove this submodule from the repository. This will remove our entry
+ from the .gitmodules file and the entry in the .git/config file.
+ :param module: If True, the module we point to will be deleted
+ as well. If the module is currently on a commit which is not part
+ of any branch in the remote, if the currently checked out branch
+ is ahead of its tracking branch, if you have modifications in the
+ working tree, or untracked files,
+ In case the removal of the repository fails for these reasons, the
+ submodule status will not have been altered.
+ If this submodule has child-modules on its own, these will be deleted
+ prior to touching the own module.
+ :param force: Enforces the deletion of the module even though it contains
+ modifications. This basically enforces a brute-force file system based
+ deletion.
+ :param configuration: if True, the submodule is deleted from the configuration,
+ otherwise it isn't. Although this should be enabled most of the times,
+ this flag enables you to safely delete the repository of your submodule.
+ :param dry_run: if True, we will not actually do anything, but throw the errors
+ we would usually throw
+ :note: doesn't work in bare repositories
+ :raise InvalidGitRepositoryError: thrown if the repository cannot be deleted
+ :raise OSError: if directories or files could not be removed"""
+ if self.repo.bare:
+ raise InvalidGitRepositoryError("Cannot delete a submodule in bare repository")
+ # END handle bare mode
+
+ if not (module + configuration):
+ raise ValueError("Need to specify to delete at least the module, or the configuration")
+ # END handle params
+
+ # DELETE MODULE REPOSITORY
+ ##########################
+ if module and self.module_exists():
+ if force:
+ # take the fast lane and just delete everything in our module path
+ # TODO: If we run into permission problems, we have a highly inconsistent
+ # state. Delete the .git folders last, start with the submodules first
+ mp = self.module_path()
+ method = None
+ if os.path.islink(mp):
+ method = os.remove
+ elif os.path.isdir(mp):
+ method = shutil.rmtree
+ elif os.path.exists(mp):
+ raise AssertionError("Cannot forcibly delete repository as it was neither a link, nor a directory")
+ #END handle brutal deletion
+ if not dry_run:
+ assert method
+ method(mp)
+ #END apply deletion method
+ else:
+ # verify we may delete our module
+ mod = self.module()
+ if mod.is_dirty(untracked_files=True):
+ raise InvalidGitRepositoryError("Cannot delete module at %s with any modifications, unless force is specified" % mod.working_tree_dir)
+ # END check for dirt
+
+ # figure out whether we have new commits compared to the remotes
+ # NOTE: If the user pulled all the time, the remote heads might
+ # not have been updated, so commits coming from the remote look
+ # as if they come from us. But we stay strictly read-only and
+ # don't fetch beforhand.
+ for remote in mod.remotes:
+ num_branches_with_new_commits = 0
+ rrefs = remote.refs
+ for rref in rrefs:
+ num_branches_with_new_commits = len(mod.git.cherry(rref)) != 0
+ # END for each remote ref
+ # not a single remote branch contained all our commits
+ if num_branches_with_new_commits == len(rrefs):
+ raise InvalidGitRepositoryError("Cannot delete module at %s as there are new commits" % mod.working_tree_dir)
+ # END handle new commits
+ # END for each remote
+
+ # gently remove all submodule repositories
+ for sm in self.children():
+ sm.remove(module=True, force=False, configuration=False, dry_run=dry_run)
+ # END for each child-submodule
+
+ # finally delete our own submodule
+ if not dry_run:
+ shutil.rmtree(mod.working_tree_dir)
+ # END delete tree if possible
+ # END handle force
+ # END handle module deletion
+
+ # DELETE CONFIGURATION
+ ######################
+ if configuration and not dry_run:
+ # first the index-entry
+ index = self.repo.index
+ try:
+ del(index.entries[index.entry_key(self.path, 0)])
+ except KeyError:
+ pass
+ #END delete entry
+ index.write()
+
+ # now git config - need the config intact, otherwise we can't query
+ # inforamtion anymore
+ self.repo.config_writer().remove_section(sm_section(self.name))
+ self.config_writer().remove_section()
+ # END delete configuration
+
def set_parent_commit(self, commit, check=True):
"""Set this instance to use the given commit whose tree is supposed to
contain the .gitmodules blob.
@@ -410,10 +517,23 @@ class Submodule(base.IndexObject, Iterable, Traversable):
try:
self.module()
return True
- except InvalidGitRepositoryError:
+ except Exception:
return False
# END handle exception
+ def exists(self):
+ """:return: True if the submodule exists, False otherwise. Please note that
+ a submodule may exist (in the .gitmodules file) even though its module
+ doesn't exist"""
+ self._clear_cache()
+ try:
+ self.path
+ return True
+ except Exception:
+ # we raise if the path cannot be restored from configuration
+ return False
+ # END handle exceptions
+
@property
def branch(self):
""":return: The branch name that we are to checkout"""