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>
This commit is contained in:
Mike Frysinger 2020-02-19 19:19:18 -05:00
parent f81c72ed77
commit c0d1866b35
2 changed files with 120 additions and 81 deletions

View File

@ -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."""

View File

@ -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()