[PATCH] buildman: Use git worktrees instead of git clones

Alper Nebi Yasak alpernebiyasak at gmail.com
Wed Sep 2 14:35:36 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.

Signed-off-by: Alper Nebi Yasak <alpernebiyasak at gmail.com>
---
The git-worktree is available since about 2015 (git v2.5), but its
manual page mentions this in its BUGS section:

   Multiple checkout in general is still experimental, and the
   support for submodules is incomplete. It is NOT recommended to
   make multiple checkouts of a superproject.

I think it'll be fine since U-Boot doesn't use submodules, but still
wanted to let you know.

 tools/buildman/builder.py   | 36 ++++++++++++++++++++++++------------
 tools/buildman/func_test.py |  2 ++
 tools/patman/gitutil.py     | 28 ++++++++++++++++++++++++++++
 3 files changed, 54 insertions(+), 12 deletions(-)

diff --git a/tools/buildman/builder.py b/tools/buildman/builder.py
index dbb75b35c1..9cb65c15a8 100644
--- a/tools/buildman/builder.py
+++ b/tools/buildman/builder.py
@@ -1540,31 +1540,38 @@ class Builder:
     def _PrepareThread(self, thread_num, setup_git):
         """Prepare the working directory for a thread.
 
-        This clones or fetches the repo into the thread's work directory.
+        This adds a git worktree for the repo into the thread's work
+        directory or fetches the repo into a clone that might already
+        exist there.
 
         Args:
             thread_num: Thread number (0, 1, ...)
-            setup_git: True to set up a git repo clone
+            setup_git: True 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 for this thread if it (or a git repo clone)
+        # 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 not os.path.exists(git_dir):
+                Print('\rChecking out worktree for thread %d' % thread_num,
+                      newline=False)
+                gitutil.AddWorktree(src_dir, thread_dir)
+                terminal.PrintClear()
+            elif os.path.isdir(git_dir):
+                # Older versions cloned the src_dir repo, we can keep using
+                # the clones 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:
-                Print('\rCloning repo for thread %d' % thread_num,
-                      newline=False)
-                gitutil.Clone(src_dir, thread_dir)
-                terminal.PrintClear()
+            elif os.path.isfile(git_dir):
+                # This is a worktree of the src_dir repo, we don't need to
+                # create it again.
+                pass
 
     def _PrepareWorkingSpace(self, max_threads, setup_git):
         """Prepare the working directory for use.
@@ -1573,9 +1580,14 @@ class Builder:
 
         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
         """
         builderthread.Mkdir(self._working_dir)
+        if setup_git and self.git_dir:
+            # 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(os.path.abspath(self.git_dir))
         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..354884a79d 100644
--- a/tools/patman/gitutil.py
+++ b/tools/patman/gitutil.py
@@ -259,6 +259,34 @@ def Fetch(git_dir=None, work_tree=None):
     if result.return_code != 0:
         raise OSError('git fetch: %s' % result.stderr)
 
+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