From c0d1866b35d3e8d0bee3760a9f52574fa2ab8491 Mon Sep 17 00:00:00 2001 From: Mike Frysinger Date: Wed, 19 Feb 2020 19:19:18 -0500 Subject: project/sync: move DeleteProject helper to Project Since deleting a source checkout involves a good bit of internal knowledge of .repo/, move the DeleteProject helper out of the sync code and into the Project class itself. This allows us to add git worktree support to it so we can unlock/unlink project checkouts. Change-Id: If9af8bd4a9c7e29743827d8166bc3db81547ca50 Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/256072 Reviewed-by: Jonathan Nieder Tested-by: Mike Frysinger --- project.py | 116 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ subcmds/sync.py | 85 ++--------------------------------------- 2 files changed, 120 insertions(+), 81 deletions(-) diff --git a/project.py b/project.py index 5bead641..a5d35bf3 100644 --- a/project.py +++ b/project.py @@ -1832,6 +1832,122 @@ class Project(object): patch_id, self.bare_git.rev_parse('FETCH_HEAD')) + def DeleteWorktree(self, quiet=False, force=False): + """Delete the source checkout and any other housekeeping tasks. + + This currently leaves behind the internal .repo/ cache state. This helps + when switching branches or manifest changes get reverted as we don't have + to redownload all the git objects. But we should do some GC at some point. + + Args: + quiet: Whether to hide normal messages. + force: Always delete tree even if dirty. + + Returns: + True if the worktree was completely cleaned out. + """ + if self.IsDirty(): + if force: + print('warning: %s: Removing dirty project: uncommitted changes lost.' % + (self.relpath,), file=sys.stderr) + else: + print('error: %s: Cannot remove project: uncommitted changes are ' + 'present.\n' % (self.relpath,), file=sys.stderr) + return False + + if not quiet: + print('%s: Deleting obsolete checkout.' % (self.relpath,)) + + # Unlock and delink from the main worktree. We don't use git's worktree + # remove because it will recursively delete projects -- we handle that + # ourselves below. https://crbug.com/git/48 + if self.use_git_worktrees: + needle = platform_utils.realpath(self.gitdir) + # Find the git worktree commondir under .repo/worktrees/. + output = self.bare_git.worktree('list', '--porcelain').splitlines()[0] + assert output.startswith('worktree '), output + commondir = output[9:] + # Walk each of the git worktrees to see where they point. + configs = os.path.join(commondir, 'worktrees') + for name in os.listdir(configs): + gitdir = os.path.join(configs, name, 'gitdir') + with open(gitdir) as fp: + relpath = fp.read().strip() + # Resolve the checkout path and see if it matches this project. + fullpath = platform_utils.realpath(os.path.join(configs, name, relpath)) + if fullpath == needle: + platform_utils.rmtree(os.path.join(configs, name)) + + # Delete the .git directory first, so we're less likely to have a partially + # working git repository around. There shouldn't be any git projects here, + # so rmtree works. + + # Try to remove plain files first in case of git worktrees. If this fails + # for any reason, we'll fall back to rmtree, and that'll display errors if + # it can't remove things either. + try: + platform_utils.remove(self.gitdir) + except OSError: + pass + try: + platform_utils.rmtree(self.gitdir) + except OSError as e: + if e.errno != errno.ENOENT: + print('error: %s: %s' % (self.gitdir, e), file=sys.stderr) + print('error: %s: Failed to delete obsolete checkout; remove manually, ' + 'then run `repo sync -l`.' % (self.relpath,), file=sys.stderr) + return False + + # Delete everything under the worktree, except for directories that contain + # another git project. + dirs_to_remove = [] + failed = False + for root, dirs, files in platform_utils.walk(self.worktree): + for f in files: + path = os.path.join(root, f) + try: + platform_utils.remove(path) + except OSError as e: + if e.errno != errno.ENOENT: + print('error: %s: Failed to remove: %s' % (path, e), file=sys.stderr) + failed = True + dirs[:] = [d for d in dirs + if not os.path.lexists(os.path.join(root, d, '.git'))] + dirs_to_remove += [os.path.join(root, d) for d in dirs + if os.path.join(root, d) not in dirs_to_remove] + for d in reversed(dirs_to_remove): + if platform_utils.islink(d): + try: + platform_utils.remove(d) + except OSError as e: + if e.errno != errno.ENOENT: + print('error: %s: Failed to remove: %s' % (d, e), file=sys.stderr) + failed = True + elif not platform_utils.listdir(d): + try: + platform_utils.rmdir(d) + except OSError as e: + if e.errno != errno.ENOENT: + print('error: %s: Failed to remove: %s' % (d, e), file=sys.stderr) + failed = True + if failed: + print('error: %s: Failed to delete obsolete checkout.' % (self.relpath,), + file=sys.stderr) + print(' Remove manually, then run `repo sync -l`.', file=sys.stderr) + return False + + # Try deleting parent dirs if they are empty. + path = self.worktree + while path != self.manifest.topdir: + try: + platform_utils.rmdir(path) + except OSError as e: + if e.errno != errno.ENOENT: + break + path = os.path.dirname(path) + + return True + # Branch Management ## def GetHeadPath(self): """Return the full path to the HEAD ref.""" diff --git a/subcmds/sync.py b/subcmds/sync.py index eada76a7..f2af0ec3 100644 --- a/subcmds/sync.py +++ b/subcmds/sync.py @@ -16,7 +16,6 @@ from __future__ import print_function -import errno import json import netrc from optparse import SUPPRESS_HELP @@ -633,74 +632,6 @@ later is required to fix a server side protocol bug. else: self.manifest._Unload() - def _DeleteProject(self, path): - print('Deleting obsolete path %s' % path, file=sys.stderr) - - # Delete the .git directory first, so we're less likely to have a partially - # working git repository around. There shouldn't be any git projects here, - # so rmtree works. - dotgit = os.path.join(path, '.git') - # Try to remove plain files first in case of git worktrees. If this fails - # for any reason, we'll fall back to rmtree, and that'll display errors if - # it can't remove things either. - try: - platform_utils.remove(dotgit) - except OSError: - pass - try: - platform_utils.rmtree(dotgit) - except OSError as e: - if e.errno != errno.ENOENT: - print('error: %s: %s' % (dotgit, str(e)), file=sys.stderr) - print('error: %s: Failed to delete obsolete path; remove manually, then ' - 'run sync again' % (path,), file=sys.stderr) - return 1 - - # Delete everything under the worktree, except for directories that contain - # another git project - dirs_to_remove = [] - failed = False - for root, dirs, files in platform_utils.walk(path): - for f in files: - try: - platform_utils.remove(os.path.join(root, f)) - except OSError as e: - print('Failed to remove %s (%s)' % (os.path.join(root, f), str(e)), file=sys.stderr) - failed = True - dirs[:] = [d for d in dirs - if not os.path.lexists(os.path.join(root, d, '.git'))] - dirs_to_remove += [os.path.join(root, d) for d in dirs - if os.path.join(root, d) not in dirs_to_remove] - for d in reversed(dirs_to_remove): - if platform_utils.islink(d): - try: - platform_utils.remove(d) - except OSError as e: - print('Failed to remove %s (%s)' % (os.path.join(root, d), str(e)), file=sys.stderr) - failed = True - elif len(platform_utils.listdir(d)) == 0: - try: - platform_utils.rmdir(d) - except OSError as e: - print('Failed to remove %s (%s)' % (os.path.join(root, d), str(e)), file=sys.stderr) - failed = True - continue - if failed: - print('error: Failed to delete obsolete path %s' % path, file=sys.stderr) - print(' remove manually, then run sync again', file=sys.stderr) - return 1 - - # Try deleting parent dirs if they are empty - project_dir = path - while project_dir != self.manifest.topdir: - if len(platform_utils.listdir(project_dir)) == 0: - platform_utils.rmdir(project_dir) - else: - break - project_dir = os.path.dirname(project_dir) - - return 0 - def UpdateProjectList(self, opt): new_project_paths = [] for project in self.GetProjects(None, missing_ok=True): @@ -727,23 +658,15 @@ later is required to fix a server side protocol bug. remote=RemoteSpec('origin'), gitdir=gitdir, objdir=gitdir, + use_git_worktrees=os.path.isfile(gitdir), worktree=os.path.join(self.manifest.topdir, path), relpath=path, revisionExpr='HEAD', revisionId=None, groups=None) - - if project.IsDirty() and opt.force_remove_dirty: - print('WARNING: Removing dirty project "%s": uncommitted changes ' - 'erased' % project.relpath, file=sys.stderr) - self._DeleteProject(project.worktree) - elif project.IsDirty(): - print('error: Cannot remove project "%s": uncommitted changes ' - 'are present' % project.relpath, file=sys.stderr) - print(' commit changes, then run sync again', - file=sys.stderr) - return 1 - elif self._DeleteProject(project.worktree): + if not project.DeleteWorktree( + quiet=opt.quiet, + force=opt.force_remove_dirty): return 1 new_project_paths.sort() -- cgit v1.2.3-54-g00ecf