|
10 | 10 | import os
|
11 | 11 | import sys
|
12 | 12 | import weakref
|
| 13 | +import shutil |
13 | 14 |
|
14 | 15 | __all__ = ("Submodule", "RootModule")
|
15 | 16 |
|
16 | 17 | #{ Utilities
|
17 | 18 |
|
18 |
| -def sm_section(path): |
| 19 | +def sm_section(name): |
19 | 20 | """:return: section title used in .gitmodules configuration file"""
|
20 |
| - return 'submodule "%s"' % path |
| 21 | + return 'submodule "%s"' % name |
21 | 22 |
|
22 | 23 | def sm_name(section):
|
23 | 24 | """: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):
|
223 | 224 | if the remote repository had a master branch, or of the 'branch' option
|
224 | 225 | was specified for this submodule and the branch existed remotely
|
225 | 226 | :note: does nothing in bare repositories
|
| 227 | + :note: method is definitely not atomic if recurisve is True |
226 | 228 | :return: self"""
|
227 | 229 | if self.repo.bare:
|
228 | 230 | return self
|
@@ -329,6 +331,111 @@ def update(self, recursive=False, init=True, to_latest_revision=False):
|
329 | 331 |
|
330 | 332 | return self
|
331 | 333 |
|
| 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 | + |
332 | 439 | def set_parent_commit(self, commit, check=True):
|
333 | 440 | """Set this instance to use the given commit whose tree is supposed to
|
334 | 441 | contain the .gitmodules blob.
|
@@ -410,10 +517,23 @@ def module_exists(self):
|
410 | 517 | try:
|
411 | 518 | self.module()
|
412 | 519 | return True
|
413 |
| - except InvalidGitRepositoryError: |
| 520 | + except Exception: |
414 | 521 | return False
|
415 | 522 | # END handle exception
|
416 | 523 |
|
| 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 | + |
417 | 537 | @property
|
418 | 538 | def branch(self):
|
419 | 539 | """:return: The branch name that we are to checkout"""
|
|
0 commit comments