From b8531133deb053b333a3e90ce3b91a6f448b034e Mon Sep 17 00:00:00 2001 From: Gavin Mak Date: Fri, 3 Apr 2026 01:11:35 +0000 Subject: [PATCH] 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 Commit-Queue: Gavin Mak Reviewed-by: Dan Willemsen --- man/repo-init.1 | 6 ++++- manifest_xml.py | 26 +++++++++++++------ project.py | 45 ++++++++++++++++++++++++++++++++- repo | 5 ++++ subcmds/init.py | 1 + tests/test_manifest_xml.py | 20 +++++++++++++++ tests/test_project.py | 51 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 144 insertions(+), 10 deletions(-) diff --git a/man/repo-init.1 b/man/repo-init.1 index 374117527..51be6510d 100644 --- a/man/repo-init.1 +++ b/man/repo-init.1 @@ -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 diff --git a/manifest_xml.py b/manifest_xml.py index 5dc9d2fe9..8f7294990 100644 --- a/manifest_xml.py +++ b/manifest_xml.py @@ -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): diff --git a/project.py b/project.py index b86367317..6731f8599 100644 --- a/project.py +++ b/project.py @@ -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) diff --git a/repo b/repo index ee3a5b79b..2d7f163f4 100755 --- a/repo +++ b/repo @@ -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") diff --git a/subcmds/init.py b/subcmds/init.py index f5a3892a5..b60ef28bc 100644 --- a/subcmds/init.py +++ b/subcmds/init.py @@ -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( diff --git a/tests/test_manifest_xml.py b/tests/test_manifest_xml.py index c7352f89a..3f3f50a7f 100644 --- a/tests/test_manifest_xml.py +++ b/tests/test_manifest_xml.py @@ -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( + '' + ) + 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.""" diff --git a/tests/test_project.py b/tests/test_project.py index 131dccd9f..ba78efab9 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -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."""