diff --git a/docs/internal-fs-layout.md b/docs/internal-fs-layout.md index 8e62cde..f474029 100644 --- a/docs/internal-fs-layout.md +++ b/docs/internal-fs-layout.md @@ -102,6 +102,11 @@ support, see the [manifest-format.md] file. respective servers ... * `subprojects/`: Like `projects/`, but for git submodules. * `subproject-objects/`: Like `project-objects/`, but for git submodules. +* `worktrees/`: Bare checkouts of every project synced by the manifest. The + filesystem layout matches the `worktree path. + setting = fp.read() + assert setting.startswith('gitdir:') + git_worktree_path = setting.split(':', 1)[1].strip() + # Use relative path from checkout->worktree. + with open(dotgit, 'w') as fp: + print('gitdir:', os.path.relpath(git_worktree_path, self.worktree), + file=fp) + # Use relative path from worktree->checkout. + with open(os.path.join(git_worktree_path, 'gitdir'), 'w') as fp: + print(os.path.relpath(dotgit, git_worktree_path), file=fp) + def _InitWorkTree(self, force_sync=False, submodules=False): realdotgit = os.path.join(self.worktree, '.git') tmpdotgit = realdotgit + '.tmp' init_dotgit = not os.path.exists(realdotgit) if init_dotgit: + if self.use_git_worktrees: + self._InitGitWorktree() + self._CopyAndLinkFiles() + return + dotgit = tmpdotgit platform_utils.rmtree(tmpdotgit, ignore_errors=True) os.makedirs(tmpdotgit) diff --git a/repo b/repo index 77e7fe9..743c28b 100755 --- a/repo +++ b/repo @@ -302,6 +302,8 @@ def GetParser(gitc_init=False): group.add_option('--clone-filter', action='store', default='blob:none', help='filter for use with --partial-clone ' '[default: %default]') + group.add_option('--worktree', action='store_true', + help=optparse.SUPPRESS_HELP) group.add_option('--archive', action='store_true', help='checkout an archive instead of a git repository for ' 'each project. See git archive.') diff --git a/subcmds/init.py b/subcmds/init.py index 3c68c2c..8a29321 100644 --- a/subcmds/init.py +++ b/subcmds/init.py @@ -15,6 +15,8 @@ # limitations under the License. from __future__ import print_function + +import optparse import os import platform import re @@ -128,6 +130,10 @@ to update the working directory files. g.add_option('--clone-filter', action='store', default='blob:none', dest='clone_filter', help='filter for use with --partial-clone [default: %default]') + # TODO(vapier): Expose option with real help text once this has been in the + # wild for a while w/out significant bug reports. Goal is by ~Sep 2020. + g.add_option('--worktree', action='store_true', + help=optparse.SUPPRESS_HELP) g.add_option('--archive', dest='archive', action='store_true', help='checkout an archive instead of a git repository for ' @@ -246,6 +252,20 @@ to update the working directory files. if opt.dissociate: m.config.SetString('repo.dissociate', 'true') + if opt.worktree: + if opt.mirror: + print('fatal: --mirror and --worktree are incompatible', + file=sys.stderr) + sys.exit(1) + if opt.submodules: + print('fatal: --submodules and --worktree are incompatible', + file=sys.stderr) + sys.exit(1) + m.config.SetString('repo.worktree', 'true') + if is_new: + m.use_git_worktrees = True + print('warning: --worktree is experimental!', file=sys.stderr) + if opt.archive: if is_new: m.config.SetString('repo.archive', 'true') @@ -459,6 +479,10 @@ to update the working directory files. % ('.'.join(str(x) for x in MIN_GIT_VERSION_SOFT),), file=sys.stderr) + if opt.worktree: + # Older versions of git supported worktree, but had dangerous gc bugs. + git_require((2, 15, 0), fail=True, msg='git gc worktree corruption') + self._SyncManifest(opt) self._LinkManifest(opt.manifest_name) diff --git a/subcmds/sync.py b/subcmds/sync.py index 0ac308e..49867a9 100644 --- a/subcmds/sync.py +++ b/subcmds/sync.py @@ -15,6 +15,8 @@ # limitations under the License. from __future__ import print_function + +import errno import json import netrc from optparse import SUPPRESS_HELP @@ -569,7 +571,8 @@ later is required to fix a server side protocol bug. gc_gitdirs = {} for project in projects: # Make sure pruning never kicks in with shared projects. - if len(project.manifest.GetProjectsWithName(project.name)) > 1: + if (not project.use_git_worktrees and + len(project.manifest.GetProjectsWithName(project.name)) > 1): print('%s: Shared project %s found, disabling pruning.' % (project.relpath, project.name)) if git_require((2, 7, 0)): @@ -637,13 +640,22 @@ later is required to fix a server side protocol bug. # 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.rmtree(os.path.join(path, '.git')) + platform_utils.remove(dotgit) + except OSError: + pass + try: + platform_utils.rmtree(dotgit) except OSError as e: - print('Failed to remove %s (%s)' % (os.path.join(path, '.git'), str(e)), file=sys.stderr) - print('error: Failed to delete obsolete path %s' % path, file=sys.stderr) - print(' remove manually, then run sync again', file=sys.stderr) - return 1 + 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