[PATCH v2] buildman: Use git worktrees instead of git clones when possible

Alper Nebi Yasak alpernebiyasak at gmail.com
Thu Sep 3 14:51:03 CEST 2020


This patch makes buildman create linked working trees instead of clones
of the source repository, but keeps updating the older clones of the
repository that might already exist. These worktrees share "everything
except working directory specific files such as HEAD, index, etc." with
the source repository. See the git-worktree(1) manual page for more
information.

If git-worktree isn't available, silently falls back to cloning the
repository.

Signed-off-by: Alper Nebi Yasak <alpernebiyasak at gmail.com>
---
I used 'git worktree list's return code, since 'git worktree' returns
non-zero (prints usage) even if it's available.

This does fall back to git clone on my Ubuntu 14.04 amd64 VM, but all
the builds instantly give an error about the Makefile even before this
patch. The buildman parts are working as far as I can tell.

Changes in v2:
- Fall back to cloning if git-worktree isn't available
- Add a gitutil.CheckWorktreeIsAvailable(git_dir) function
- Refactor the _PrepareThread changes
- Make _PrepeareThread's setup_git argument accept 'clone' or 'worktree'
- Some comment and docstring changes

v1: https://patchwork.ozlabs.org/project/uboot/list/?series=199060

 tools/buildman/builder.py   | 48 ++++++++++++++++++++++++++++++-------
 tools/buildman/func_test.py |  2 ++
 tools/patman/gitutil.py     | 42 ++++++++++++++++++++++++++++++++
 3 files changed, 84 insertions(+), 8 deletions(-)

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 at 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.
 
-- 
2.28.0



More information about the U-Boot mailing list