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.
.TH REPO "1" "September 2024" "repo init" "Repo Manual"
.TH REPO "1" "April 2026" "repo init" "Repo Manual"
.SH NAME
repo \- repo init - manual page for repo init
.SH SYNOPSIS
@@ -80,6 +80,10 @@ each project. See git archive.
.TP
\fB\-\-worktree\fR
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:
.TP
\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):
return self.manifestProject.use_worktree
@property
def UseLocalGitDirs(self):
return self.manifestProject.use_local_gitdirs
@property
def IsArchive(self):
return self.manifestProject.archive
@@ -2042,15 +2046,21 @@ https://gerrit.googlesource.com/git-repo/+/HEAD/docs/manifest-format.md
else:
namepath = f"{name}.git"
worktree = os.path.join(self.topdir, path).replace("\\", "/")
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)
if self.UseLocalGitDirs:
gitdir = os.path.join(worktree, ".git")
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
def GetProjectsWithName(self, name, all_manifests=False):
+44 -1
View File
@@ -3686,7 +3686,11 @@ class Project:
self._MigrateOldSubmoduleDir()
# 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)
init_dotgit = not os.path.lexists(dotgit)
@@ -3735,6 +3739,11 @@ class Project:
For submodule projects, create a '.git' file using the gitfile
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)
if self.parent:
_lwrite(
@@ -4488,6 +4497,11 @@ class ManifestProject(MetaProject):
"""Whether we use 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
def clone_bundle(self):
"""Whether we use clone_bundle."""
@@ -4642,6 +4656,7 @@ class ManifestProject(MetaProject):
this_manifest_only=False,
outer_manifest=True,
clone_filter_for_depth=None,
use_local_gitdirs=False,
):
"""Sync the manifest and all submanifests.
@@ -4872,6 +4887,34 @@ class ManifestProject(MetaProject):
self.use_git_worktrees = True
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 is_new:
self.config.SetBoolean("repo.archive", archive)
+5
View File
@@ -379,6 +379,11 @@ def InitParser(parser):
action="store_true",
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.
group = parser.add_option_group("Project checkout optimizations")
+1
View File
@@ -169,6 +169,7 @@ to update the working directory files.
depth=opt.depth,
git_event_log=self.git_event_log,
manifest_name=opt.manifest_name,
use_local_gitdirs=opt.use_local_gitdirs,
):
manifest_name = opt.manifest_name
raise UpdateManifestError(
+20
View File
@@ -819,6 +819,26 @@ class TestProjectElement:
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:
"""Check handling of bad path & name attributes."""
+51
View File
@@ -657,6 +657,9 @@ class ManifestPropertiesFetchedCorrectly(unittest.TestCase):
fakeproj.config.SetBoolean("repo.worktree", False)
self.assertFalse(fakeproj.use_worktree)
fakeproj.config.SetBoolean("repo.uselocalgitdirs", False)
self.assertFalse(fakeproj.use_local_gitdirs)
fakeproj.config.SetBoolean("repo.clonebundle", False)
self.assertFalse(fakeproj.clone_bundle)
@@ -691,6 +694,54 @@ class ManifestPropertiesFetchedCorrectly(unittest.TestCase):
fakeproj.config.SetString("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):
"""Tests for stateless sync strategy."""