diff --git a/project.py b/project.py index d87060929..0ad558446 100644 --- a/project.py +++ b/project.py @@ -1538,6 +1538,7 @@ class Project: # See if we can skip the network fetch entirely. remote_fetched = False + has_shallow = os.path.exists(os.path.join(self.gitdir, "shallow")) if not ( optimized_fetch and IsId(self.revisionExpr) @@ -1545,8 +1546,8 @@ class Project: use_superproject=use_superproject ) and ( - not depth - or os.path.exists(os.path.join(self.gitdir, "shallow")) + has_shallow + or (not depth and not self._SharingProjectHasShallow()) ) ): remote_fetched = True @@ -2643,6 +2644,23 @@ class Project: # There is no such persistent revision. We have to fetch it. return False + def _SharingProjectHasShallow(self) -> bool: + """Check if another project sharing this objdir has a "shallow" file. + + If any project sharing this objdir has a shallow file in its gitdir, + then the shared objdir may be depth-limited, and every other project + sharing this objdir needs its own shallow file so that git knows + where history is truncated. + """ + other_projects = self.manifest.GetProjectsWithName( + self.name, all_manifests=True + ) + for proj in other_projects: + if proj.objdir == self.objdir and proj.gitdir != self.gitdir: + if os.path.exists(os.path.join(proj.gitdir, "shallow")): + return True + return False + def _FetchArchive(self, tarpath, cwd=None): cmd = ["archive", "-v", "-o", tarpath] cmd.append("--remote=%s" % self.remote.url) @@ -2702,11 +2720,14 @@ class Project: tag_name = self.upstream[len(R_TAGS) :] if is_sha1 or tag_name is not None: + has_shallow = os.path.exists( + os.path.join(self.gitdir, "shallow") + ) if self._CheckForImmutableRevision( use_superproject=use_superproject ) and ( - not depth - or os.path.exists(os.path.join(self.gitdir, "shallow")) + has_shallow + or (not depth and not self._SharingProjectHasShallow()) ): if verbose: print( diff --git a/tests/test_project.py b/tests/test_project.py index 7c4be48e6..a824407cf 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -812,6 +812,7 @@ def _create_mock_project( proj.bare_git = mock.MagicMock() proj._LsRemote = mock.MagicMock(return_value="1234abcd\trefs/heads/main\n") + manifest.GetProjectsWithName.return_value = [proj] return proj @@ -957,6 +958,31 @@ class SyncOptimizationTests(unittest.TestCase): proj._InitMRef = mock.MagicMock() return proj + def _create_sharing_project(self, tempdir, proj, share_objdir=True): + """Create another project with the same name but a different gitdir. + + Args: + share_objdir: a boolean, if True - the new project shares the same + objdir, if False - the new project has a different objdir. + """ + if share_objdir: + other_objdir = proj.objdir + else: + other_objdir = os.path.join(tempdir, "other_objdir") + other = project.Project( + manifest=proj.manifest, + name=proj.name, + remote=proj.remote, + gitdir=os.path.join(tempdir, "other_gitdir"), + objdir=other_objdir, + worktree=os.path.join(tempdir, "other_worktree"), + relpath="other-test-project", + revisionExpr=proj.revisionExpr, + revisionId=None, + ) + proj.manifest.GetProjectsWithName.return_value.append(other) + return other + def test_sync_network_half_shallow_missing_fetches(self): """Test Sync_NetworkHalf fetches if shallow file is missing.""" with utils_for_test.TempGitTree() as tempdir: @@ -991,6 +1017,72 @@ class SyncOptimizationTests(unittest.TestCase): self.assertTrue(res.success) proj._RemoteFetch.assert_not_called() + def test_sync_network_half_sharing_project_shallow_missing_fetches( + self, + ): + """Test Sync_NetworkHalf fetches when sharing project has shallow + file but this project does not.""" + with utils_for_test.TempGitTree() as tempdir: + proj = self._get_project(tempdir, depth=1) + os.makedirs(proj.gitdir, exist_ok=True) + os.makedirs(proj.objdir, exist_ok=True) + + other = self._create_sharing_project(tempdir, proj) + os.makedirs(other.gitdir, exist_ok=True) + with open(os.path.join(other.gitdir, "shallow"), "w") as f: + f.write("") + + proj._RemoteFetch = mock.MagicMock(return_value=True) + + res = proj.Sync_NetworkHalf(optimized_fetch=True) + + self.assertTrue(res.success) + proj._RemoteFetch.assert_called_once() + + def test_sync_network_half_different_objdir_shallow_exists_skips(self): + """Test Sync_NetworkHalf skips when same-name project has shallow file + but different objdir (like in a multi-manifest setup).""" + with utils_for_test.TempGitTree() as tempdir: + proj = self._get_project(tempdir, depth=1) + os.makedirs(proj.gitdir, exist_ok=True) + os.makedirs(proj.objdir, exist_ok=True) + + other = self._create_sharing_project( + tempdir, proj, share_objdir=False + ) + os.makedirs(other.gitdir, exist_ok=True) + with open(os.path.join(other.gitdir, "shallow"), "w") as f: + f.write("") + + proj._RemoteFetch = mock.MagicMock() + + res = proj.Sync_NetworkHalf(optimized_fetch=True) + + self.assertTrue(res.success) + proj._RemoteFetch.assert_not_called() + + def test_sync_network_half_sharing_project_both_shallow_skips(self): + """Test Sync_NetworkHalf skips when both this project and the sharing + project have shallow files.""" + with utils_for_test.TempGitTree() as tempdir: + proj = self._get_project(tempdir, depth=1) + os.makedirs(proj.gitdir, exist_ok=True) + os.makedirs(proj.objdir, exist_ok=True) + with open(os.path.join(proj.gitdir, "shallow"), "w") as f: + f.write("") + + other = self._create_sharing_project(tempdir, proj) + os.makedirs(other.gitdir, exist_ok=True) + with open(os.path.join(other.gitdir, "shallow"), "w") as f: + f.write("") + + proj._RemoteFetch = mock.MagicMock() + + res = proj.Sync_NetworkHalf(optimized_fetch=True) + + self.assertTrue(res.success) + proj._RemoteFetch.assert_not_called() + def test_remote_fetch_shallow_missing_fetches(self): """Test _RemoteFetch fetches if shallow file is missing.""" with utils_for_test.TempGitTree() as tempdir: @@ -1032,6 +1124,58 @@ class SyncOptimizationTests(unittest.TestCase): self.assertTrue(res) mock_git_cmd.assert_not_called() + def test_remote_fetch_sharing_project_shallow_missing_fetches(self): + """Test _RemoteFetch fetches when sharing project has shallow file + but this project does not.""" + with utils_for_test.TempGitTree() as tempdir: + proj = self._get_project(tempdir, depth=1) + os.makedirs(proj.gitdir, exist_ok=True) + os.makedirs(proj.objdir, exist_ok=True) + + other = self._create_sharing_project(tempdir, proj) + os.makedirs(other.gitdir, exist_ok=True) + with open(os.path.join(other.gitdir, "shallow"), "w") as f: + f.write("") + + with mock.patch("project.GitCommand") as mock_git_cmd: + mock_cmd_instance = mock.MagicMock() + mock_cmd_instance.Wait.return_value = 0 + mock_git_cmd.return_value = mock_cmd_instance + + res = proj._RemoteFetch( + current_branch_only=True, + depth=1, + use_superproject=False, + ) + + self.assertTrue(res) + mock_git_cmd.assert_called() + + def test_remote_fetch_sharing_project_both_shallow_skips(self): + """Test _RemoteFetch skips when both this project and the sharing + project have shallow files.""" + with utils_for_test.TempGitTree() as tempdir: + proj = self._get_project(tempdir, depth=1) + os.makedirs(proj.gitdir, exist_ok=True) + os.makedirs(proj.objdir, exist_ok=True) + with open(os.path.join(proj.gitdir, "shallow"), "w") as f: + f.write("") + + other = self._create_sharing_project(tempdir, proj) + os.makedirs(other.gitdir, exist_ok=True) + with open(os.path.join(other.gitdir, "shallow"), "w") as f: + f.write("") + + with mock.patch("project.GitCommand") as mock_git_cmd: + res = proj._RemoteFetch( + current_branch_only=True, + depth=1, + use_superproject=False, + ) + + self.assertTrue(res) + mock_git_cmd.assert_not_called() + class GetEnvVarsTests(unittest.TestCase): """Tests for GetEnvVars project environment variable generation."""