diff --git a/tools/buildman/builder.py b/tools/buildman/builder.py index dbb75b35c1..c93946842a 100644 --- a/tools/buildman/builder.py +++ b/tools/buildman/builder.py @@ -1541,41 +1541,73 @@ class Builder: """Prepare the working directory for a thread. This clones or fetches the repo into the thread's work directory. + Optionally, it can create a linked working tree of the repo in the + thread's work directory instead. Args: thread_num: Thread number (0, 1, ...) - setup_git: True to set up a git repo clone + setup_git: + 'clone' to set up a git clone + 'worktree' to set up a git worktree """ thread_dir = self.GetThreadDir(thread_num) builderthread.Mkdir(thread_dir) git_dir = os.path.join(thread_dir, '.git') - # Clone the repo if it doesn't already exist - # TODO(sjg@chromium): Perhaps some git hackery to symlink instead, so - # we have a private index but uses the origin repo's contents? + # Create a worktree or a git repo clone for this thread if it + # doesn't already exist if setup_git and self.git_dir: src_dir = os.path.abspath(self.git_dir) - if os.path.exists(git_dir): + if os.path.isdir(git_dir): + # This is a clone of the src_dir repo, we can keep using + # it but need to fetch from src_dir. Print('\rFetching repo for thread %d' % thread_num, newline=False) gitutil.Fetch(git_dir, thread_dir) terminal.PrintClear() - else: + elif os.path.isfile(git_dir): + # This is a worktree of the src_dir repo, we don't need to + # create it again or update it in any way. + pass + elif os.path.exists(git_dir): + # Don't know what could trigger this, but we probably + # can't create a git worktree/clone here. + raise ValueError('Git dir %s exists, but is not a file ' + 'or a directory.' % git_dir) + elif setup_git == 'worktree': + Print('\rChecking out worktree for thread %d' % thread_num, + newline=False) + gitutil.AddWorktree(src_dir, thread_dir) + terminal.PrintClear() + elif setup_git == 'clone' or setup_git == True: Print('\rCloning repo for thread %d' % thread_num, newline=False) gitutil.Clone(src_dir, thread_dir) terminal.PrintClear() + else: + raise ValueError("Can't setup git repo with %s." % setup_git) def _PrepareWorkingSpace(self, max_threads, setup_git): """Prepare the working directory for use. - Set up the git repo for each thread. + Set up the git repo for each thread. Creates a linked working tree + if git-worktree is available, or clones the repo if it isn't. Args: max_threads: Maximum number of threads we expect to need. - setup_git: True to set up a git repo clone + setup_git: True to set up a git worktree or a git clone """ builderthread.Mkdir(self._working_dir) + if setup_git and self.git_dir: + src_dir = os.path.abspath(self.git_dir) + if gitutil.CheckWorktreeIsAvailable(src_dir): + setup_git = 'worktree' + # If we previously added a worktree but the directory for it + # got deleted, we need to prune its files from the repo so + # that we can check out another in its place. + gitutil.PruneWorktrees(src_dir) + else: + setup_git = 'clone' for thread in range(max_threads): self._PrepareThread(thread, setup_git) diff --git a/tools/buildman/func_test.py b/tools/buildman/func_test.py index 418677f9cc..3dd2e6ee5b 100644 --- a/tools/buildman/func_test.py +++ b/tools/buildman/func_test.py @@ -319,6 +319,8 @@ class TestFunctional(unittest.TestCase): return command.CommandResult(return_code=0) elif sub_cmd == 'checkout': return command.CommandResult(return_code=0) + elif sub_cmd == 'worktree': + return command.CommandResult(return_code=0) # Not handled, so abort print('git', git_args, sub_cmd, args) diff --git a/tools/patman/gitutil.py b/tools/patman/gitutil.py index 192d8e69b3..27a0a9fbc1 100644 --- a/tools/patman/gitutil.py +++ b/tools/patman/gitutil.py @@ -259,6 +259,48 @@ def Fetch(git_dir=None, work_tree=None): if result.return_code != 0: raise OSError('git fetch: %s' % result.stderr) +def CheckWorktreeIsAvailable(git_dir): + """Check if git-worktree functionality is available + + Args: + git_dir: The repository to test in + + Returns: + True if git-worktree commands will work, False otherwise. + """ + pipe = ['git', '--git-dir', git_dir, 'worktree', 'list'] + result = command.RunPipe([pipe], capture=True, capture_stderr=True, + raise_on_error=False) + return result.return_code == 0 + +def AddWorktree(git_dir, output_dir, commit_hash=None): + """Create and checkout a new git worktree for this build + + Args: + git_dir: The repository to checkout the worktree from + output_dir: Path for the new worktree + commit_hash: Commit hash to checkout + """ + # We need to pass --detach to avoid creating a new branch + pipe = ['git', '--git-dir', git_dir, 'worktree', 'add', '.', '--detach'] + if commit_hash: + pipe.append(commit_hash) + result = command.RunPipe([pipe], capture=True, cwd=output_dir, + capture_stderr=True) + if result.return_code != 0: + raise OSError('git worktree add: %s' % result.stderr) + +def PruneWorktrees(git_dir): + """Remove administrative files for deleted worktrees + + Args: + git_dir: The repository whose deleted worktrees should be pruned + """ + pipe = ['git', '--git-dir', git_dir, 'worktree', 'prune'] + result = command.RunPipe([pipe], capture=True, capture_stderr=True) + if result.return_code != 0: + raise OSError('git worktree prune: %s' % result.stderr) + def CreatePatches(branch, start, count, ignore_binary, series): """Create a series of patches from the top of the current branch.