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 <jrn@google.com>
Tested-by: Mike Frysinger <vapier@google.com>
diff --git a/project.py b/project.py
index 5bead64..a5d35bf 100644
--- a/project.py
+++ b/project.py
@@ -1832,6 +1832,122 @@
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 eada76a..f2af0ec 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 @@
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 @@
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()