git_superproject: avoid re-initing bare repo

Running sync with reftable on a files-backed workspace fails to re-init
the superproject dir with:
```
fatal: could not open
'.../.repo/exp-superproject/<hash>-superproject.git/refs/heads' for writing:
Is a directory
```

Bug: 476209856
Change-Id: Ie8473d66069aafefa5661bd3ea8e73b2b27c6a38
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/550981
Commit-Queue: Gavin Mak <gavinmak@google.com>
Reviewed-by: Mike Frysinger <vapier@google.com>
Tested-by: Gavin Mak <gavinmak@google.com>
This commit is contained in:
Gavin Mak
2026-02-07 00:53:31 +00:00
committed by LUCI
parent a0abfd7339
commit 83b8ebdbbe
2 changed files with 109 additions and 17 deletions
+53 -17
View File
@@ -23,9 +23,11 @@ Examples:
""" """
import functools import functools
import glob
import hashlib import hashlib
import os import os
import sys import sys
import tempfile
import time import time
from typing import NamedTuple from typing import NamedTuple
import urllib.parse import urllib.parse
@@ -34,6 +36,7 @@ from git_command import git_require
from git_command import GitCommand from git_command import GitCommand
from git_config import RepoConfig from git_config import RepoConfig
from git_refs import GitRefs from git_refs import GitRefs
import platform_utils
_SUPERPROJECT_GIT_NAME = "superproject.git" _SUPERPROJECT_GIT_NAME = "superproject.git"
@@ -215,30 +218,63 @@ class Superproject:
""" """
if not os.path.exists(self._superproject_path): if not os.path.exists(self._superproject_path):
os.mkdir(self._superproject_path) os.mkdir(self._superproject_path)
if not self._quiet and not os.path.exists(self._work_git):
if os.path.exists(self._work_git):
return True
if not self._quiet:
print( print(
"%s: Performing initial setup for superproject; this might " "%s: Performing initial setup for superproject; this might "
"take several minutes." % self._work_git "take several minutes." % self._work_git
) )
cmd = ["init", "--bare", self._work_git_name]
p = GitCommand( tmp_gitdir_prefix = ".tmp-superproject-initgitdir-"
None, tmp_gitdir = tempfile.mkdtemp(
cmd, prefix=tmp_gitdir_prefix,
cwd=self._superproject_path, dir=self._superproject_path,
capture_stdout=True,
capture_stderr=True,
) )
retval = p.Wait() tmp_git_name = os.path.basename(tmp_gitdir)
if retval:
self._LogWarning( try:
"git init call failed, command: git {}, " cmd = ["init", "--bare", tmp_git_name]
"return code: {}, stderr: {}", p = GitCommand(
None,
cmd, cmd,
retval, cwd=self._superproject_path,
p.stderr, capture_stdout=True,
capture_stderr=True,
) )
return False retval = p.Wait()
return True if retval:
self._LogWarning(
"git init call failed, command: git {}, "
"return code: {}, stderr: {}",
cmd,
retval,
p.stderr,
)
return False
platform_utils.rename(tmp_gitdir, self._work_git)
tmp_gitdir = None
return True
finally:
# Clean up the temporary directory created during the process,
# as well as any stale ones left over from previous attempts.
if tmp_gitdir and os.path.exists(tmp_gitdir):
platform_utils.rmtree(tmp_gitdir)
age_threshold = 60 * 60 * 24 # 1 day in seconds
now = time.time()
for tmp_dir in glob.glob(
os.path.join(self._superproject_path, f"{tmp_gitdir_prefix}*")
):
try:
mtime = os.path.getmtime(tmp_dir)
if now - mtime > age_threshold:
platform_utils.rmtree(tmp_dir)
except OSError:
pass
def _Fetch(self): def _Fetch(self):
"""Fetches a superproject for the manifest based on |_remote_url|. """Fetches a superproject for the manifest based on |_remote_url|.
+56
View File
@@ -461,6 +461,62 @@ class SuperprojectTestCase(unittest.TestCase):
"</manifest>", "</manifest>",
) )
def test_Init_success(self):
"""Test _Init succeeds and creates the work git dir."""
self.assertFalse(os.path.exists(self._superproject._work_git))
with mock.patch(
"git_superproject.GitCommand", autospec=True
) as mock_git_command:
instance = mock_git_command.return_value
instance.Wait.return_value = 0
self.assertTrue(self._superproject._Init())
mock_git_command.assert_called_once()
args, kwargs = mock_git_command.call_args
self.assertEqual(args[1][:2], ["init", "--bare"])
tmp_git_name = args[1][2]
self.assertTrue(
tmp_git_name.startswith(".tmp-superproject-initgitdir-")
)
self.assertTrue(os.path.exists(self._superproject._work_git))
tmp_git_path = os.path.join(
self._superproject._superproject_path, tmp_git_name
)
self.assertFalse(os.path.exists(tmp_git_path))
def test_Init_already_exists(self):
"""Test _Init returns early if the work git dir already exists."""
os.mkdir(self._superproject._superproject_path)
os.mkdir(self._superproject._work_git)
with mock.patch(
"git_superproject.GitCommand", autospec=True
) as mock_git_command:
self.assertTrue(self._superproject._Init())
mock_git_command.assert_not_called()
def test_Init_failure_cleans_up(self):
"""Test _Init cleans up the temporary directory if 'git init' fails."""
with mock.patch(
"git_superproject.GitCommand", autospec=True
) as mock_git_command:
instance = mock_git_command.return_value
instance.Wait.return_value = 1
instance.stderr = "mock git init failure"
self.assertFalse(self._superproject._Init())
mock_git_command.assert_called_once()
args, kwargs = mock_git_command.call_args
tmp_git_name = args[1][2]
tmp_git_path = os.path.join(
self._superproject._superproject_path, tmp_git_name
)
self.assertFalse(os.path.exists(tmp_git_path))
self.assertFalse(os.path.exists(self._superproject._work_git))
def test_Fetch(self): def test_Fetch(self):
manifest = self.getXmlManifest( manifest = self.getXmlManifest(
""" """