From 979d5bdc3ebe45998a76dbbaff46c33d4e59683b Mon Sep 17 00:00:00 2001 From: Mike Frysinger Date: Sun, 9 Feb 2020 02:28:34 -0500 Subject: add experimental git worktree support This provides initial support for using git worktrees internally instead of our own ad-hoc symlink tree. It's been lightly tested which is why it's not currently exposed via --help. When people opt-in to worktrees in an existing repo client checkout, no projects are migrated. Instead, only new projects will use the worktree method. This allows for limited testing/opting in without having to completely blow things away or get a second checkout. Bug: https://crbug.com/gerrit/11486 Change-Id: Ic3ff891b30940a6ba497b406b2a387e0a8517ed8 Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/254075 Tested-by: Mike Frysinger Reviewed-by: Mike Frysinger --- project.py | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 59 insertions(+), 11 deletions(-) (limited to 'project.py') diff --git a/project.py b/project.py index 86c9ef00..3a7ac9e8 100644 --- a/project.py +++ b/project.py @@ -866,6 +866,7 @@ class Project(object): clone_depth=None, upstream=None, parent=None, + use_git_worktrees=False, is_derived=False, dest_branch=None, optimized_fetch=False, @@ -889,6 +890,7 @@ class Project(object): sync_tags: The `sync-tags` attribute of manifest.xml's project element. upstream: The `upstream` attribute of manifest.xml's project element. parent: The parent Project object. + use_git_worktrees: Whether to use `git worktree` for this project. is_derived: False if the project was explicitly defined in the manifest; True if the project is a discovered submodule. dest_branch: The branch to which to push changes for review by default. @@ -923,6 +925,10 @@ class Project(object): self.clone_depth = clone_depth self.upstream = upstream self.parent = parent + # NB: Do not use this setting in __init__ to change behavior so that the + # manifest.git checkout can inspect & change it after instantiating. See + # the XmlManifest init code for more info. + self.use_git_worktrees = use_git_worktrees self.is_derived = is_derived self.optimized_fetch = optimized_fetch self.subprojects = [] @@ -1872,15 +1878,19 @@ class Project(object): except KeyError: head = None if revid and head and revid == head: - ref = os.path.join(self.gitdir, R_HEADS + name) - try: - os.makedirs(os.path.dirname(ref)) - except OSError: - pass - _lwrite(ref, '%s\n' % revid) - _lwrite(self.GetHeadPath(), 'ref: %s%s\n' % (R_HEADS, name)) - branch.Save() - return True + if self.use_git_worktrees: + self.work_git.update_ref(HEAD, revid) + branch.Save() + else: + ref = os.path.join(self.gitdir, R_HEADS + name) + try: + os.makedirs(os.path.dirname(ref)) + except OSError: + pass + _lwrite(ref, '%s\n' % revid) + _lwrite(self.GetHeadPath(), 'ref: %s%s\n' % (R_HEADS, name)) + branch.Save() + return True if GitCommand(self, ['checkout', '-b', branch.name, revid], @@ -2617,6 +2627,11 @@ class Project(object): os.makedirs(self.objdir) self.bare_objdir.init() + # Enable per-worktree config file support if possible. This is more a + # nice-to-have feature for users rather than a hard requirement. + if self.use_git_worktrees and git_require((2, 19, 0)): + self.config.SetString('extensions.worktreeConfig', 'true') + # If we have a separate directory to hold refs, initialize it as well. if self.objdir != self.gitdir: if init_git_dir: @@ -2651,13 +2666,15 @@ class Project(object): mirror_git = os.path.join(ref_dir, self.name + '.git') repo_git = os.path.join(ref_dir, '.repo', 'projects', self.relpath + '.git') + worktrees_git = os.path.join(ref_dir, '.repo', 'worktrees', + self.name + '.git') if os.path.exists(mirror_git): ref_dir = mirror_git - elif os.path.exists(repo_git): ref_dir = repo_git - + elif os.path.exists(worktrees_git): + ref_dir = worktrees_git else: ref_dir = None @@ -2765,6 +2782,10 @@ class Project(object): self.bare_git.symbolic_ref('-m', msg, ref, dst) def _CheckDirReference(self, srcdir, destdir, share_refs): + # Git worktrees don't use symlinks to share at all. + if self.use_git_worktrees: + return + symlink_files = self.shareable_files[:] symlink_dirs = self.shareable_dirs[:] if share_refs: @@ -2864,11 +2885,38 @@ class Project(object): else: raise + def _InitGitWorktree(self): + """Init the project using git worktrees.""" + self.bare_git.worktree('prune') + self.bare_git.worktree('add', '-ff', '--checkout', '--detach', '--lock', + self.worktree, self.GetRevisionId()) + + # Rewrite the internal state files to use relative paths between the + # checkouts & worktrees. + dotgit = os.path.join(self.worktree, '.git') + with open(dotgit, 'r') as fp: + # Figure out the checkout->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) -- cgit v1.2.3-54-g00ecf