diff --git a/project.py b/project.py index 3eb2848ce..4da098e8d 100644 --- a/project.py +++ b/project.py @@ -4490,7 +4490,7 @@ class Project: if tmp_gitdir: platform_utils.rename(tmp_gitdir, self.gitdir) tmp_gitdir = None - if not init_obj_dir: + if not init_obj_dir and self.objdir != self.gitdir: # The project might be shared (obj_dir already initialized), but # such information is not available here. Instead of passing it, # set it as shared, and rely to be unset down the execution diff --git a/subcmds/sync.py b/subcmds/sync.py index bfa3fa645..bc14d3cdd 100644 --- a/subcmds/sync.py +++ b/subcmds/sync.py @@ -1563,22 +1563,20 @@ later is required to fix a server side protocol bug. if len(projects) == 1: return False if len(projects) > 1: - # Objects are potentially shared with another project. + if project.UseAlternates: + return False + # Objects are shared when another project uses the same objdir. # See the logic in Project.Sync_NetworkHalf regarding UseAlternates. - # - When False, shared projects share (via symlink) - # .repo/project-objects/{PROJECT_NAME}.git as the one-and-only - # objects directory. All objects are precious, since there is no - # project with a complete set of refs. + # - When False and projects share .repo/project-objects/{NAME}.git + # as the one-and-only objects directory, all objects are precious + # since no project has a complete set of refs. Projects with + # distinct local .git directories (--use-local-gitdirs) do not + # share objects even when their manifest names match. # - When True, shared projects share (via info/alternates) # .repo/project-objects/{PROJECT_NAME}.git as an alternate object # store, which is written only on the first clone of the project, - # and is not written subsequently. (When Sync_NetworkHalf sees - # that it exists, it makes sure that the alternates file points - # there, and uses a project-local .git/objects directory for all - # syncs going forward. - # We do not support switching between the options. The environment - # variable is present for testing and migration only. - return not project.UseAlternates + # and is not written subsequently. + return sum(1 for p in projects if p.objdir == project.objdir) > 1 return False @@ -1649,6 +1647,18 @@ later is required to fix a server side protocol bug. if not opt.auto_gc: # Just repair preciousObjects state, and return. for project in projects: + if ( + project.manifest.UseLocalGitDirs is True + and not project.parent + and getattr(project, "gitdir", None) == project.objdir + and len( + project.manifest.GetProjectsWithName( + project.name, all_manifests=True + ) + ) + == 1 + ): + continue cls._SetPreciousObjectsState(project, opt) return diff --git a/tests/test_subcmds_sync.py b/tests/test_subcmds_sync.py index 01310e751..67a8ac610 100644 --- a/tests/test_subcmds_sync.py +++ b/tests/test_subcmds_sync.py @@ -1374,6 +1374,20 @@ class GetPreciousObjectsState(unittest.TestCase): self.cmd._GetPreciousObjectsState(self.project, self.opt) ) + def test_duplicate_name_distinct_objdirs(self) -> None: + """Duplicate-name projects with distinct local objdirs aren't shared.""" + other = mock.MagicMock( + use_git_worktrees=False, UseAlternates=False, objdir="b/.git" + ) + self.project.objdir = "a/.git" + self.project.manifest.GetProjectsWithName.return_value = [ + self.project, + other, + ] + self.assertFalse( + self.cmd._GetPreciousObjectsState(self.project, self.opt) + ) + class KeyboardInterruptTest(unittest.TestCase): """Tests for KeyboardInterrupt handling in Sync operations.""" @@ -1626,6 +1640,29 @@ class GCProjectsTest(unittest.TestCase): mock_set_state.assert_called_once_with(self.project, self.opt) self.assertFalse(self.project.bare_git.gc.called) + def test_GCProjects_skip_unshared_local_gitdirs(self) -> None: + """Unshared --use-local-gitdirs projects skip preciousObjects scan.""" + self.opt.auto_gc = False + self.project.parent = None + self.project.gitdir = "work/.git" + self.project.objdir = "work/.git" + self.project.manifest.UseLocalGitDirs = True + self.project.manifest.GetProjectsWithName.return_value = [self.project] + + dup = mock.MagicMock() + dup.name = "dup" + dup.parent = None + dup.gitdir = "dup1/.git" + dup.objdir = "dup1/.git" + dup.manifest.UseLocalGitDirs = True + dup.manifest.GetProjectsWithName.return_value = [dup, mock.MagicMock()] + + with mock.patch.object( + sync.Sync, "_SetPreciousObjectsState" + ) as mock_set_state: + self.cmd._GCProjects([self.project, dup], self.opt, mock.Mock()) + mock_set_state.assert_called_once_with(dup, self.opt) + @mock.patch.object(progress, "Progress") def test_GCProjects_sequential(self, mock_progress): """Test sequential GC (jobs < 2)."""