init: Add --use-local-gitdirs for standard Git layouts

Introduce --use-local-gitdirs to bypass repo's symlink-based layouts in
favor of standard local .git directories.

Bug: 513329573
Bug: 508146070
Change-Id: I53d1602e61be0b86964529bcbea3dc801471f9c9
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/569001
Tested-by: Gavin Mak <gavinmak@google.com>
Commit-Queue: Gavin Mak <gavinmak@google.com>
Reviewed-by: Dan Willemsen <dwillemsen@google.com>
This commit is contained in:
Gavin Mak
2026-04-03 01:11:35 +00:00
committed by gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com
parent 2d54384a5e
commit b8531133de
7 changed files with 144 additions and 10 deletions
+5 -1
View File
@@ -1,5 +1,5 @@
.\" DO NOT MODIFY THIS FILE! It was generated by help2man. .\" DO NOT MODIFY THIS FILE! It was generated by help2man.
.TH REPO "1" "September 2024" "repo init" "Repo Manual" .TH REPO "1" "April 2026" "repo init" "Repo Manual"
.SH NAME .SH NAME
repo \- repo init - manual page for repo init repo \- repo init - manual page for repo init
.SH SYNOPSIS .SH SYNOPSIS
@@ -80,6 +80,10 @@ each project. See git archive.
.TP .TP
\fB\-\-worktree\fR \fB\-\-worktree\fR
use git\-worktree to manage projects use git\-worktree to manage projects
.TP
\fB\-\-use\-local\-gitdirs\fR
bypass .repo/projects/ and use standard Git layout in
working tree
.SS Project checkout optimizations: .SS Project checkout optimizations:
.TP .TP
\fB\-\-reference\fR=\fI\,DIR\/\fR \fB\-\-reference\fR=\fI\,DIR\/\fR
+18 -8
View File
@@ -1055,6 +1055,10 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
def UseGitWorktrees(self): def UseGitWorktrees(self):
return self.manifestProject.use_worktree return self.manifestProject.use_worktree
@property
def UseLocalGitDirs(self):
return self.manifestProject.use_local_gitdirs
@property @property
def IsArchive(self): def IsArchive(self):
return self.manifestProject.archive return self.manifestProject.archive
@@ -2042,15 +2046,21 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
else: else:
namepath = f"{name}.git" namepath = f"{name}.git"
worktree = os.path.join(self.topdir, path).replace("\\", "/") worktree = os.path.join(self.topdir, path).replace("\\", "/")
gitdir = os.path.join(self.subdir, "projects", "%s.git" % path) if self.UseLocalGitDirs:
# We allow people to mix git worktrees & non-git worktrees for now. gitdir = os.path.join(worktree, ".git")
# This allows for in situ migration of repo clients.
if os.path.exists(gitdir) or not self.UseGitWorktrees:
objdir = os.path.join(self.repodir, "project-objects", namepath)
else:
use_git_worktrees = True
gitdir = os.path.join(self.repodir, "worktrees", namepath)
objdir = gitdir objdir = gitdir
else:
gitdir = os.path.join(self.subdir, "projects", "%s.git" % path)
# We allow people to mix git worktrees & non-git worktrees for
# now. This allows for in situ migration of repo clients.
if os.path.exists(gitdir) or not self.UseGitWorktrees:
objdir = os.path.join(
self.repodir, "project-objects", namepath
)
else:
use_git_worktrees = True
gitdir = os.path.join(self.repodir, "worktrees", namepath)
objdir = gitdir
return relpath, worktree, gitdir, objdir, use_git_worktrees return relpath, worktree, gitdir, objdir, use_git_worktrees
def GetProjectsWithName(self, name, all_manifests=False): def GetProjectsWithName(self, name, all_manifests=False):
+44 -1
View File
@@ -3686,7 +3686,11 @@ class Project:
self._MigrateOldSubmoduleDir() self._MigrateOldSubmoduleDir()
# If using an old layout style (a directory), migrate it. # If using an old layout style (a directory), migrate it.
if not platform_utils.islink(dotgit) and platform_utils.isdir(dotgit): if (
not platform_utils.islink(dotgit)
and platform_utils.isdir(dotgit)
and not self.manifest.UseLocalGitDirs
):
self._MigrateOldWorkTreeGitDir(dotgit, project=self.name) self._MigrateOldWorkTreeGitDir(dotgit, project=self.name)
init_dotgit = not os.path.lexists(dotgit) init_dotgit = not os.path.lexists(dotgit)
@@ -3735,6 +3739,11 @@ class Project:
For submodule projects, create a '.git' file using the gitfile For submodule projects, create a '.git' file using the gitfile
mechanism, and for the rest, create a symbolic link. mechanism, and for the rest, create a symbolic link.
""" """
if self.manifest.UseLocalGitDirs and os.path.normpath(
self.gitdir
) == os.path.normpath(dotgit):
return
os.makedirs(self.worktree, exist_ok=True) os.makedirs(self.worktree, exist_ok=True)
if self.parent: if self.parent:
_lwrite( _lwrite(
@@ -4488,6 +4497,11 @@ class ManifestProject(MetaProject):
"""Whether we use worktree.""" """Whether we use worktree."""
return self.config.GetBoolean("repo.worktree") return self.config.GetBoolean("repo.worktree")
@property
def use_local_gitdirs(self):
"""Whether we use local gitdirs."""
return self.config.GetBoolean("repo.uselocalgitdirs")
@property @property
def clone_bundle(self): def clone_bundle(self):
"""Whether we use clone_bundle.""" """Whether we use clone_bundle."""
@@ -4642,6 +4656,7 @@ class ManifestProject(MetaProject):
this_manifest_only=False, this_manifest_only=False,
outer_manifest=True, outer_manifest=True,
clone_filter_for_depth=None, clone_filter_for_depth=None,
use_local_gitdirs=False,
): ):
"""Sync the manifest and all submanifests. """Sync the manifest and all submanifests.
@@ -4872,6 +4887,34 @@ class ManifestProject(MetaProject):
self.use_git_worktrees = True self.use_git_worktrees = True
logger.warning("warning: --worktree is experimental!") logger.warning("warning: --worktree is experimental!")
if use_local_gitdirs:
if mirror:
logger.error(
"fatal: --mirror and --use-local-gitdirs are incompatible"
)
return False
if worktree:
logger.error(
"fatal: --worktree and --use-local-gitdirs are incompatible"
)
return False
if archive:
logger.error(
"fatal: --archive and --use-local-gitdirs are incompatible"
)
return False
if not is_new and not self.use_local_gitdirs:
logger.error(
"fatal: --use-local-gitdirs is only supported when "
"initializing a new workspace."
)
return False
self.config.SetBoolean("repo.uselocalgitdirs", use_local_gitdirs)
if archive: if archive:
if is_new: if is_new:
self.config.SetBoolean("repo.archive", archive) self.config.SetBoolean("repo.archive", archive)
+5
View File
@@ -379,6 +379,11 @@ def InitParser(parser):
action="store_true", action="store_true",
help="use git-worktree to manage projects", help="use git-worktree to manage projects",
) )
group.add_option(
"--use-local-gitdirs",
action="store_true",
help="bypass .repo/projects/ and use standard Git layout in working tree",
)
# These are fundamentally different ways of structuring the checkout. # These are fundamentally different ways of structuring the checkout.
group = parser.add_option_group("Project checkout optimizations") group = parser.add_option_group("Project checkout optimizations")
+1
View File
@@ -169,6 +169,7 @@ to update the working directory files.
depth=opt.depth, depth=opt.depth,
git_event_log=self.git_event_log, git_event_log=self.git_event_log,
manifest_name=opt.manifest_name, manifest_name=opt.manifest_name,
use_local_gitdirs=opt.use_local_gitdirs,
): ):
manifest_name = opt.manifest_name manifest_name = opt.manifest_name
raise UpdateManifestError( raise UpdateManifestError(
+20
View File
@@ -819,6 +819,26 @@ class TestProjectElement:
str(repo_client.topdir), ".repo", "projects", "..git" str(repo_client.topdir), ".repo", "projects", "..git"
) )
def test_get_project_paths_local_gitdirs(
self, repo_client: RepoClient
) -> None:
"""Check GetProjectPaths with UseLocalGitDirs."""
manifest = repo_client.get_xml_manifest(
'<?xml version="1.0" encoding="UTF-8"?><manifest></manifest>'
)
manifest.manifestProject.config.SetBoolean("repo.uselocalgitdirs", True)
relpath, worktree, gitdir, objdir, use_git_worktrees = (
manifest.GetProjectPaths("foo", "bar", "origin")
)
assert os.path.normpath(gitdir) == os.path.normpath(
os.path.join(str(repo_client.topdir), "bar", ".git")
)
assert os.path.normpath(objdir) == os.path.normpath(
os.path.join(str(repo_client.topdir), "bar", ".git")
)
def test_bad_path_name_checks(self, repo_client: RepoClient) -> None: def test_bad_path_name_checks(self, repo_client: RepoClient) -> None:
"""Check handling of bad path & name attributes.""" """Check handling of bad path & name attributes."""
+51
View File
@@ -657,6 +657,9 @@ class ManifestPropertiesFetchedCorrectly(unittest.TestCase):
fakeproj.config.SetBoolean("repo.worktree", False) fakeproj.config.SetBoolean("repo.worktree", False)
self.assertFalse(fakeproj.use_worktree) self.assertFalse(fakeproj.use_worktree)
fakeproj.config.SetBoolean("repo.uselocalgitdirs", False)
self.assertFalse(fakeproj.use_local_gitdirs)
fakeproj.config.SetBoolean("repo.clonebundle", False) fakeproj.config.SetBoolean("repo.clonebundle", False)
self.assertFalse(fakeproj.clone_bundle) self.assertFalse(fakeproj.clone_bundle)
@@ -691,6 +694,54 @@ class ManifestPropertiesFetchedCorrectly(unittest.TestCase):
fakeproj.config.SetString("manifest.platform", "auto") fakeproj.config.SetString("manifest.platform", "auto")
self.assertEqual(fakeproj.manifest_platform, "auto") self.assertEqual(fakeproj.manifest_platform, "auto")
def test_sync_use_local_gitdirs_worktree_conflict(self):
"""Test that --use-local-gitdirs conflicts with --worktree."""
with utils_for_test.TempGitTree() as tempdir:
fakeproj = self.setUpManifest(tempdir)
class DummyManifest:
is_submanifest = False
def GetDefaultGroupsStr(self, with_platform=False):
return ""
fakeproj.manifest = DummyManifest()
result = fakeproj.Sync(use_local_gitdirs=True, worktree=True)
self.assertFalse(result)
def test_sync_use_local_gitdirs_archive_conflict(self):
"""Test that --use-local-gitdirs conflicts with --archive."""
with utils_for_test.TempGitTree() as tempdir:
fakeproj = self.setUpManifest(tempdir)
class DummyManifest:
is_submanifest = False
def GetDefaultGroupsStr(self, with_platform=False):
return ""
fakeproj.manifest = DummyManifest()
result = fakeproj.Sync(use_local_gitdirs=True, archive=True)
self.assertFalse(result)
def test_sync_use_local_gitdirs_mirror_conflict(self):
"""Test that --use-local-gitdirs conflicts with --mirror."""
with utils_for_test.TempGitTree() as tempdir:
fakeproj = self.setUpManifest(tempdir)
class DummyManifest:
is_submanifest = False
def GetDefaultGroupsStr(self, with_platform=False):
return ""
fakeproj.manifest = DummyManifest()
result = fakeproj.Sync(use_local_gitdirs=True, mirror=True)
self.assertFalse(result)
class StatelessSyncTests(unittest.TestCase): class StatelessSyncTests(unittest.TestCase):
"""Tests for stateless sync strategy.""" """Tests for stateless sync strategy."""