mirror of
https://gerrit.googlesource.com/git-repo
synced 2026-10-20 20:28:25 +00:00
sync: implement reprojectcmd for the local half
Add `repo.reprojectcmd`, the checkout counterpart to `repo.fetchcmd`.
When set, `repo sync` runs this command instead of Git to materialize a
project's index and worktree at the target tree, while `repo` handles
ref updates directly via `git update-ref`.
This replaces Git's tree materialization steps: detaching HEAD,
fast-forwarding, and hard-resetting. Rebasing is not delegated, and the
command is skipped if HEAD is already at the target, if HEAD is ahead of
the target during fast-forward, or for MetaProjects.
The command runs in a subshell with project environment variables (such
as `REPO_TREV`). Before running, `repo` ensures no operation is in
progress and no staged changes exist. Worktree collision detection is
delegated to the command (preserving benign unstaged/untracked edits).
Afterward, `repo` verifies that HEAD was untouched and that the index
matches the target tree.
Like `repo.fetchcmd`, this requires `repo.uselocalgitdirs`. Nested
projects and submodules are unsupported; `repo sync` fails if the
manifest contains any while `repo.reprojectcmd` is enabled.
Verified end-to-end with repo init using local-gitdirs, repo.fetchcmd,
and repo.reprojectcmd ('git -C $REPO_PATH read-tree -m -u $REPO_TREV'):
* Verified detached HEAD checkout and correct reflog generation across
projects.
* Verified benign unstaged edits and untracked files survive checkout.
* Verified conflicting untracked files fail with exit 128 without
clobbering worktree.
* Verified staged changes fail upfront before reprojectcmd is executed.
Bug: 513329573
Change-Id: I964d24d22dccffc05a9b991ad69f3a7e93268c01
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/626281
Tested-by: Gavin Mak <gavinmak@google.com>
Reviewed-by: Brian Gan <brgan@google.com>
Commit-Queue: Gavin Mak <gavinmak@google.com>
This commit is contained in:
1 parent
c2c330ba4a
commit
a468ea7752
6 files changed
+1335
-32
No files matched your search
+767
-11
@@ -20,7 +20,7 @@ from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
@@ -1866,6 +1866,7 @@ def _create_mock_project(
|
||||
tempdir,
|
||||
use_local_gitdirs=False,
|
||||
fetch_cmd=None,
|
||||
reproject_cmd: Optional[str] = None,
|
||||
depth=None,
|
||||
gitdir=None,
|
||||
objdir=None,
|
||||
@@ -1875,6 +1876,7 @@ def _create_mock_project(
|
||||
manifest = mock.MagicMock()
|
||||
manifest.manifestProject.use_local_gitdirs = use_local_gitdirs
|
||||
manifest.manifestProject.fetch_cmd = fetch_cmd
|
||||
manifest.manifestProject.reproject_cmd = reproject_cmd
|
||||
manifest.manifestProject.depth = depth
|
||||
manifest.manifestProject.dissociate = False
|
||||
manifest.manifestProject.clone_filter = None
|
||||
@@ -2839,20 +2841,25 @@ class GetEnvVarsTests(unittest.TestCase):
|
||||
self.assertEqual(env["REPO_LREV"], "")
|
||||
|
||||
|
||||
def _create_manifest_project(tempdir: str) -> project.ManifestProject:
|
||||
"""Return a ManifestProject for a new .repo/ under |tempdir|."""
|
||||
repodir = os.path.join(tempdir, ".repo")
|
||||
manifest_dir = os.path.join(repodir, "manifests")
|
||||
manifest_file = os.path.join(repodir, manifest_xml.MANIFEST_FILE_NAME)
|
||||
os.mkdir(repodir)
|
||||
os.mkdir(manifest_dir)
|
||||
manifest = manifest_xml.XmlManifest(repodir, manifest_file)
|
||||
|
||||
return project.ManifestProject(
|
||||
manifest, "test/manifest", os.path.join(tempdir, ".git"), tempdir
|
||||
)
|
||||
|
||||
|
||||
class FetchCmdTests(unittest.TestCase):
|
||||
"""Tests for fetch_cmd feature."""
|
||||
|
||||
def setUpManifest(self, tempdir):
|
||||
repodir = os.path.join(tempdir, ".repo")
|
||||
manifest_dir = os.path.join(repodir, "manifests")
|
||||
manifest_file = os.path.join(repodir, manifest_xml.MANIFEST_FILE_NAME)
|
||||
os.mkdir(repodir)
|
||||
os.mkdir(manifest_dir)
|
||||
manifest = manifest_xml.XmlManifest(repodir, manifest_file)
|
||||
|
||||
return project.ManifestProject(
|
||||
manifest, "test/manifest", os.path.join(tempdir, ".git"), tempdir
|
||||
)
|
||||
return _create_manifest_project(tempdir)
|
||||
|
||||
def _get_project(self, tempdir):
|
||||
proj = _create_mock_project(
|
||||
@@ -2905,3 +2912,752 @@ class FetchCmdTests(unittest.TestCase):
|
||||
|
||||
result = fakeproj.Sync(use_local_gitdirs=False)
|
||||
self.assertFalse(result)
|
||||
|
||||
|
||||
class ReprojectCmdTests(unittest.TestCase):
|
||||
"""Tests for the repo.reprojectcmd feature."""
|
||||
|
||||
REVID = "1234abcd" * 5
|
||||
HEAD_ID = "5678abcd" * 5
|
||||
HEAD_TREE = "cafe0000" * 5
|
||||
OTHER_ID = "9abcdef0" * 5
|
||||
PUB_ID = "0fedcba9" * 5
|
||||
|
||||
def _get_project(self, tempdir: str) -> project.Project:
|
||||
proj = _create_mock_project(
|
||||
tempdir, use_local_gitdirs=True, reproject_cmd="echo reproject"
|
||||
)
|
||||
proj.manifest.path_prefix = ""
|
||||
proj.GetRevisionId = mock.MagicMock(return_value=self.REVID)
|
||||
proj.IsRebaseInProgress = mock.MagicMock(return_value=False)
|
||||
proj.IsCherryPickInProgress = mock.MagicMock(return_value=False)
|
||||
proj.work_git = mock.MagicMock()
|
||||
proj.work_git.GetHead.return_value = self.HEAD_ID
|
||||
proj.work_git.rev_parse.return_value = self.HEAD_TREE
|
||||
proj.work_git.GetDotgitPath.side_effect = lambda subpath: os.path.join(
|
||||
tempdir, ".git", subpath
|
||||
)
|
||||
return proj
|
||||
|
||||
@staticmethod
|
||||
def _z(*items: str) -> str:
|
||||
"""Return |items| as NUL-delimited Git output."""
|
||||
return "".join(item + "\0" for item in items)
|
||||
|
||||
@staticmethod
|
||||
def _git_command(
|
||||
staged: str = "", diff: str = "", returncode: int = 0
|
||||
) -> Callable[..., mock.MagicMock]:
|
||||
"""Return a GitCommand stand-in answering the reproject queries.
|
||||
|
||||
Args:
|
||||
staged: `git diff-index -z --cached` or `git ls-files -z` output.
|
||||
diff: `git diff-index --cached` postcondition output.
|
||||
returncode: exit code of GitCommand.
|
||||
"""
|
||||
|
||||
def make(
|
||||
project: project.Project, cmdv: List[str], **kwargs: Any
|
||||
) -> mock.MagicMock:
|
||||
cmd = mock.MagicMock()
|
||||
cmd.stderr = ""
|
||||
if cmdv[0] == "diff-index":
|
||||
if any("^{tree}" in arg for arg in cmdv):
|
||||
cmd.stdout = diff
|
||||
else:
|
||||
cmd.stdout = staged
|
||||
elif cmdv[0] == "ls-files":
|
||||
cmd.stdout = staged
|
||||
else:
|
||||
cmd.stdout = ""
|
||||
cmd.Wait.return_value = returncode
|
||||
return cmd
|
||||
|
||||
return make
|
||||
|
||||
def _reproject(
|
||||
self, proj: project.Project, **outputs: Any
|
||||
) -> mock.MagicMock:
|
||||
"""Run _Reproject against mocked Git; return the subprocess mock."""
|
||||
with mock.patch(
|
||||
"project.GitCommand", side_effect=self._git_command(**outputs)
|
||||
), mock.patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = mock.MagicMock(returncode=0, stdout="")
|
||||
proj._Reproject(self.REVID)
|
||||
return mock_run
|
||||
|
||||
def test_reproject_runs_the_command_with_project_env(self) -> None:
|
||||
"""Test the command runs from the client root with REPO_TREV set."""
|
||||
with utils_for_test.TempGitTree() as tempdir:
|
||||
proj = self._get_project(tempdir)
|
||||
with mock.patch(
|
||||
"project.GitCommand", side_effect=self._git_command()
|
||||
), mock.patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = mock.MagicMock(returncode=0, stdout="")
|
||||
proj._Reproject(self.REVID)
|
||||
|
||||
mock_run.assert_called_once()
|
||||
args, kwargs = mock_run.call_args
|
||||
self.assertEqual(args[0], "echo reproject")
|
||||
self.assertTrue(kwargs["shell"])
|
||||
self.assertEqual(kwargs["cwd"], tempdir)
|
||||
self.assertEqual(kwargs["env"]["REPO_TREV"], self.REVID)
|
||||
self.assertEqual(kwargs["env"]["REPO_PATH"], "test-project")
|
||||
|
||||
def test_reproject_rejects_a_staged_change(self) -> None:
|
||||
"""Test a staged change fails before the command runs."""
|
||||
with utils_for_test.TempGitTree() as tempdir:
|
||||
proj = self._get_project(tempdir)
|
||||
with self.assertRaises(project.LocalSyncFail) as e:
|
||||
self._reproject(proj, staged=self._z("lib/a.c"))
|
||||
self.assertIn(
|
||||
"reprojectcmd cannot run with staged changes", str(e.exception)
|
||||
)
|
||||
self.assertIn("lib/a.c", str(e.exception))
|
||||
|
||||
def test_reproject_unborn_head_rejects_a_staged_change(self) -> None:
|
||||
"""Test a staged change on unborn HEAD fails before command runs."""
|
||||
with utils_for_test.TempGitTree() as tempdir:
|
||||
proj = self._get_project(tempdir)
|
||||
proj.work_git.rev_parse.side_effect = error.GitError("unborn")
|
||||
with self.assertRaises(project.LocalSyncFail) as e:
|
||||
self._reproject(proj, staged=self._z("lib/a.c"))
|
||||
self.assertIn(
|
||||
"reprojectcmd cannot run with staged changes", str(e.exception)
|
||||
)
|
||||
self.assertIn("lib/a.c", str(e.exception))
|
||||
|
||||
def test_reproject_unborn_head_runs_clean(self) -> None:
|
||||
"""Test a clean unborn HEAD allows the command to run."""
|
||||
with utils_for_test.TempGitTree() as tempdir:
|
||||
proj = self._get_project(tempdir)
|
||||
proj.work_git.rev_parse.side_effect = error.GitError("unborn")
|
||||
mock_run = self._reproject(proj)
|
||||
mock_run.assert_called_once()
|
||||
|
||||
def test_reproject_rejects_an_operation_in_progress(self) -> None:
|
||||
"""Test an unfinished rebase fails the sync before the command runs."""
|
||||
with utils_for_test.TempGitTree() as tempdir:
|
||||
proj = self._get_project(tempdir)
|
||||
proj.IsRebaseInProgress.return_value = True
|
||||
with mock.patch("subprocess.run") as mock_run:
|
||||
with self.assertRaises(project.LocalSyncFail) as e:
|
||||
proj._Reproject(self.REVID)
|
||||
self.assertIn("rebase in progress", str(e.exception))
|
||||
mock_run.assert_not_called()
|
||||
|
||||
def test_reproject_surfaces_a_failed_command(self) -> None:
|
||||
"""Test a non-zero exit fails the sync with the command's output."""
|
||||
with utils_for_test.TempGitTree() as tempdir:
|
||||
proj = self._get_project(tempdir)
|
||||
with mock.patch(
|
||||
"project.GitCommand", side_effect=self._git_command()
|
||||
), mock.patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = mock.MagicMock(
|
||||
returncode=3, stdout="disk on fire\n"
|
||||
)
|
||||
with self.assertRaises(project.LocalSyncFail) as e:
|
||||
proj._Reproject(self.REVID)
|
||||
self.assertIn("exited with 3", str(e.exception))
|
||||
self.assertIn("disk on fire", str(e.exception))
|
||||
|
||||
def test_reproject_rejects_a_moved_head_ref(self) -> None:
|
||||
"""Test a command that wrote HEAD fails the sync."""
|
||||
with utils_for_test.TempGitTree() as tempdir:
|
||||
proj = self._get_project(tempdir)
|
||||
proj.work_git.GetHead.side_effect = [self.HEAD_ID, self.REVID]
|
||||
with mock.patch(
|
||||
"project.GitCommand", side_effect=self._git_command()
|
||||
), mock.patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = mock.MagicMock(returncode=0, stdout="")
|
||||
with self.assertRaises(project.LocalSyncFail) as e:
|
||||
proj._Reproject(self.REVID)
|
||||
self.assertIn("moved HEAD", str(e.exception))
|
||||
|
||||
def test_reproject_rejects_a_moved_head_oid_on_branch(self) -> None:
|
||||
"""Test a command that moved a branch pointer fails the sync."""
|
||||
with utils_for_test.TempGitTree() as tempdir:
|
||||
proj = self._get_project(tempdir)
|
||||
proj.work_git.GetHead.return_value = "refs/heads/main"
|
||||
proj.work_git.rev_parse.side_effect = [
|
||||
"tree123", # HEAD^{tree}
|
||||
self.HEAD_ID, # HEAD before command
|
||||
self.REVID, # HEAD after command
|
||||
]
|
||||
with mock.patch(
|
||||
"project.GitCommand", side_effect=self._git_command()
|
||||
), mock.patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = mock.MagicMock(returncode=0, stdout="")
|
||||
with self.assertRaises(project.LocalSyncFail) as e:
|
||||
proj._Reproject(self.REVID)
|
||||
self.assertIn("moved HEAD", str(e.exception))
|
||||
|
||||
def test_reproject_rejects_an_index_mismatch(self) -> None:
|
||||
"""Test a command that left the index off the target fails the sync."""
|
||||
with utils_for_test.TempGitTree() as tempdir:
|
||||
proj = self._get_project(tempdir)
|
||||
with mock.patch(
|
||||
"project.GitCommand",
|
||||
side_effect=self._git_command(diff="lib/a.c\n"),
|
||||
), mock.patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = mock.MagicMock(returncode=0, stdout="")
|
||||
with self.assertRaises(project.LocalSyncFail) as e:
|
||||
proj._Reproject(self.REVID)
|
||||
self.assertIn(self.REVID, str(e.exception))
|
||||
self.assertIn("lib/a.c", str(e.exception))
|
||||
|
||||
def _get_synced_project(
|
||||
self,
|
||||
tempdir: str,
|
||||
head: Optional[str],
|
||||
branch: Optional[str] = None,
|
||||
upstream_gain: Sequence[str] = (),
|
||||
local_changes: Sequence[str] = (),
|
||||
) -> project.Project:
|
||||
"""Return a project ready for Sync_LocalHalf with Git mocked out.
|
||||
|
||||
Args:
|
||||
head: The commit HEAD is at, or None for an unborn branch.
|
||||
branch: The name of the checked out branch, or None if detached.
|
||||
upstream_gain: The commits the target has that HEAD lacks.
|
||||
local_changes: The "<sha> <email>" lines HEAD has that the target
|
||||
lacks.
|
||||
"""
|
||||
proj = self._get_project(tempdir)
|
||||
for name in (
|
||||
"_InitWorkTree",
|
||||
"CleanPublishedCache",
|
||||
"_CopyAndLinkFiles",
|
||||
"_Reproject",
|
||||
"_Checkout",
|
||||
"_FastForward",
|
||||
"_ResetHard",
|
||||
"_Rebase",
|
||||
):
|
||||
setattr(proj, name, mock.MagicMock())
|
||||
proj.IsDirty = mock.MagicMock(return_value=False)
|
||||
proj._userident_name = "Me"
|
||||
proj._userident_email = "me@example.com"
|
||||
|
||||
proj.bare_ref = mock.MagicMock()
|
||||
if branch:
|
||||
proj.bare_ref.head = project.R_HEADS + branch
|
||||
proj.bare_ref.all = {project.R_HEADS + branch: head} if head else {}
|
||||
# A branch that does not track upstream; tests that need one
|
||||
# tracking upstream replace this with _tracking_branch().
|
||||
proj.GetBranch = mock.MagicMock(
|
||||
return_value=self._tracking_branch(branch, merge=None)
|
||||
)
|
||||
else:
|
||||
proj.bare_ref.head = head
|
||||
proj.bare_ref.all = {}
|
||||
|
||||
def _revlist(*args: Any, **kwargs: Any) -> List[str]:
|
||||
if kwargs.get("format"):
|
||||
return list(local_changes)
|
||||
if args[0] == project.not_rev(project.HEAD):
|
||||
return list(upstream_gain)
|
||||
if args[1] == self.PUB_ID:
|
||||
return [self.PUB_ID]
|
||||
return []
|
||||
|
||||
proj._revlist = mock.MagicMock(side_effect=_revlist)
|
||||
return proj
|
||||
|
||||
@staticmethod
|
||||
def _tracking_branch(
|
||||
name: str = "topic", merge: Optional[str] = "main"
|
||||
) -> mock.MagicMock:
|
||||
"""Return a branch tracking |merge| upstream, or nothing if None."""
|
||||
branch = mock.MagicMock()
|
||||
branch.name = name
|
||||
branch.merge = merge
|
||||
branch.LocalMerge = f"refs/remotes/origin/{merge}" if merge else None
|
||||
return branch
|
||||
|
||||
def test_sync_local_half_materializes_a_fresh_project(self) -> None:
|
||||
"""Test a project with an unborn HEAD is checked out by the command."""
|
||||
with utils_for_test.TempGitTree() as tempdir:
|
||||
proj = self._get_synced_project(tempdir, head=None, branch="main")
|
||||
syncbuf = project.SyncBuffer(proj.config)
|
||||
proj.Sync_LocalHalf(syncbuf)
|
||||
|
||||
self.assertTrue(syncbuf.Finish())
|
||||
proj._Reproject.assert_called_once_with(self.REVID, verbose=False)
|
||||
proj.work_git.DetachHead.assert_called_once_with(
|
||||
self.REVID,
|
||||
message=f"checkout: moving from main to {self.REVID}",
|
||||
)
|
||||
proj._Checkout.assert_not_called()
|
||||
proj._CopyAndLinkFiles.assert_called_once_with()
|
||||
|
||||
def test_sync_local_half_detached_head_uses_the_command(self) -> None:
|
||||
"""Test a detached HEAD is moved to the target by the command."""
|
||||
with utils_for_test.TempGitTree() as tempdir:
|
||||
proj = self._get_synced_project(tempdir, head=self.HEAD_ID)
|
||||
syncbuf = project.SyncBuffer(proj.config)
|
||||
proj.Sync_LocalHalf(syncbuf)
|
||||
|
||||
self.assertTrue(syncbuf.Finish())
|
||||
proj._Reproject.assert_called_once_with(self.REVID, verbose=False)
|
||||
proj.work_git.DetachHead.assert_called_once_with(
|
||||
self.REVID,
|
||||
message=f"checkout: moving from {self.HEAD_ID} to {self.REVID}",
|
||||
)
|
||||
proj._Checkout.assert_not_called()
|
||||
|
||||
def test_sync_local_half_head_at_target_skips_the_command(self) -> None:
|
||||
"""Test `repo sync -d` at the target only detaches HEAD."""
|
||||
with utils_for_test.TempGitTree() as tempdir:
|
||||
proj = self._get_synced_project(
|
||||
tempdir, head=self.REVID, branch="topic"
|
||||
)
|
||||
syncbuf = project.SyncBuffer(proj.config, detach_head=True)
|
||||
proj.Sync_LocalHalf(syncbuf)
|
||||
|
||||
self.assertTrue(syncbuf.Finish())
|
||||
proj._Reproject.assert_not_called()
|
||||
proj.work_git.DetachHead.assert_called_once()
|
||||
self.assertEqual(
|
||||
proj.work_git.DetachHead.call_args[0][0], self.REVID
|
||||
)
|
||||
|
||||
def test_sync_local_half_command_failure_fails_the_project(self) -> None:
|
||||
"""Test a failed command is reported and leaves the ref alone."""
|
||||
with utils_for_test.TempGitTree() as tempdir:
|
||||
proj = self._get_synced_project(tempdir, head=self.HEAD_ID)
|
||||
proj._Reproject.side_effect = project.LocalSyncFail(
|
||||
"reprojectcmd exited with 1", project=proj.name
|
||||
)
|
||||
syncbuf = project.SyncBuffer(proj.config)
|
||||
proj.Sync_LocalHalf(syncbuf)
|
||||
|
||||
self.assertFalse(syncbuf.Finish())
|
||||
self.assertEqual(len(syncbuf.errors), 1)
|
||||
self.assertIn("exited with 1", str(syncbuf.errors[0]))
|
||||
proj.work_git.DetachHead.assert_not_called()
|
||||
proj._CopyAndLinkFiles.assert_not_called()
|
||||
|
||||
def test_sync_local_half_fast_forward_uses_the_command(self) -> None:
|
||||
"""Test a branch behind the target is fast-forwarded by the command."""
|
||||
with utils_for_test.TempGitTree() as tempdir:
|
||||
proj = self._get_synced_project(
|
||||
tempdir,
|
||||
head=self.HEAD_ID,
|
||||
branch="topic",
|
||||
upstream_gain=[self.REVID],
|
||||
)
|
||||
proj.GetBranch = mock.MagicMock(
|
||||
return_value=self._tracking_branch()
|
||||
)
|
||||
syncbuf = project.SyncBuffer(proj.config)
|
||||
proj.Sync_LocalHalf(syncbuf)
|
||||
|
||||
self.assertTrue(syncbuf.Finish())
|
||||
proj._Reproject.assert_called_once_with(self.REVID, verbose=False)
|
||||
proj.work_git.UpdateRef.assert_called_once_with(
|
||||
project.HEAD,
|
||||
self.REVID,
|
||||
old=self.HEAD_ID,
|
||||
message=f"merge {self.REVID}: Fast-forward",
|
||||
)
|
||||
proj._FastForward.assert_not_called()
|
||||
proj._CopyAndLinkFiles.assert_called_once_with()
|
||||
|
||||
def test_sync_local_half_head_ahead_skips_the_command(self) -> None:
|
||||
"""Test a published branch ahead of the target is left alone."""
|
||||
with utils_for_test.TempGitTree() as tempdir:
|
||||
proj = self._get_synced_project(
|
||||
tempdir, head=self.HEAD_ID, branch="topic"
|
||||
)
|
||||
proj.GetBranch = mock.MagicMock(
|
||||
return_value=self._tracking_branch()
|
||||
)
|
||||
proj.work_git.merge_base.side_effect = error.GitError("no")
|
||||
proj.WasPublished = mock.MagicMock(return_value=self.PUB_ID)
|
||||
syncbuf = project.SyncBuffer(proj.config)
|
||||
proj.Sync_LocalHalf(syncbuf)
|
||||
|
||||
self.assertTrue(syncbuf.Finish())
|
||||
proj._Reproject.assert_not_called()
|
||||
proj.work_git.UpdateRef.assert_not_called()
|
||||
proj._FastForward.assert_not_called()
|
||||
proj._CopyAndLinkFiles.assert_called_once_with()
|
||||
|
||||
def test_sync_local_half_hard_reset_uses_the_command(self) -> None:
|
||||
"""Test a branch whose commits upstream dropped is reset by it."""
|
||||
with utils_for_test.TempGitTree() as tempdir:
|
||||
proj = self._get_synced_project(
|
||||
tempdir,
|
||||
head=self.HEAD_ID,
|
||||
branch="topic",
|
||||
upstream_gain=[self.REVID],
|
||||
local_changes=[f"{self.OTHER_ID} other@example.com"],
|
||||
)
|
||||
proj.GetBranch = mock.MagicMock(
|
||||
return_value=self._tracking_branch()
|
||||
)
|
||||
syncbuf = project.SyncBuffer(proj.config)
|
||||
proj.Sync_LocalHalf(syncbuf)
|
||||
|
||||
self.assertTrue(syncbuf.Finish())
|
||||
proj._Reproject.assert_called_once_with(self.REVID, verbose=False)
|
||||
proj.work_git.UpdateRef.assert_called_once_with(
|
||||
project.HEAD,
|
||||
self.REVID,
|
||||
old=self.HEAD_ID,
|
||||
message=f"reset: moving to {self.REVID}",
|
||||
)
|
||||
proj._ResetHard.assert_not_called()
|
||||
|
||||
def test_sync_local_half_rebase_is_not_delegated(self) -> None:
|
||||
"""Test a branch carrying the user's commits is rebased by Git."""
|
||||
with utils_for_test.TempGitTree() as tempdir:
|
||||
proj = self._get_synced_project(
|
||||
tempdir,
|
||||
head=self.HEAD_ID,
|
||||
branch="topic",
|
||||
upstream_gain=[self.REVID],
|
||||
local_changes=[f"{self.OTHER_ID} me@example.com"],
|
||||
)
|
||||
proj.GetBranch = mock.MagicMock(
|
||||
return_value=self._tracking_branch()
|
||||
)
|
||||
syncbuf = project.SyncBuffer(proj.config)
|
||||
proj.Sync_LocalHalf(syncbuf)
|
||||
|
||||
self.assertTrue(syncbuf.Finish())
|
||||
proj._Rebase.assert_called_once_with(
|
||||
upstream=f"{self.OTHER_ID}^1", onto=self.REVID
|
||||
)
|
||||
proj._Reproject.assert_not_called()
|
||||
proj.work_git.UpdateRef.assert_not_called()
|
||||
|
||||
def test_sync_local_half_rejects_a_nested_project(self) -> None:
|
||||
"""Test a submodule or nested project fails before any checkout."""
|
||||
with utils_for_test.TempGitTree() as tempdir:
|
||||
proj = self._get_synced_project(tempdir, head=self.HEAD_ID)
|
||||
proj.parent = mock.MagicMock()
|
||||
syncbuf = project.SyncBuffer(proj.config)
|
||||
proj.Sync_LocalHalf(syncbuf)
|
||||
|
||||
self.assertFalse(syncbuf.Finish())
|
||||
self.assertIn("nested", str(syncbuf.errors[0]))
|
||||
proj._InitWorkTree.assert_not_called()
|
||||
proj._Reproject.assert_not_called()
|
||||
|
||||
def test_sync_local_half_without_the_command_uses_git(self) -> None:
|
||||
"""Test Git keeps doing the checkout when the command is not set."""
|
||||
with utils_for_test.TempGitTree() as tempdir:
|
||||
proj = self._get_synced_project(tempdir, head=self.HEAD_ID)
|
||||
proj.manifest.manifestProject.reproject_cmd = None
|
||||
syncbuf = project.SyncBuffer(proj.config)
|
||||
proj.Sync_LocalHalf(syncbuf)
|
||||
|
||||
self.assertTrue(syncbuf.Finish())
|
||||
proj._Checkout.assert_called_once_with(
|
||||
self.REVID, force_checkout=False, quiet=True
|
||||
)
|
||||
proj._Reproject.assert_not_called()
|
||||
proj.work_git.DetachHead.assert_not_called()
|
||||
|
||||
def test_metaproject_never_uses_the_command(self) -> None:
|
||||
"""Test .repo/manifests and .repo/repo are checked out by Git."""
|
||||
with utils_for_test.TempGitTree() as tempdir:
|
||||
fakeproj = _create_manifest_project(tempdir)
|
||||
fakeproj.config.SetString("repo.reprojectcmd", "echo hi")
|
||||
fakeproj.config.SetBoolean("repo.uselocalgitdirs", True)
|
||||
self.assertFalse(fakeproj.UseReprojectCmd)
|
||||
|
||||
def test_sync_reproject_cmd_requires_use_local_gitdirs(self) -> None:
|
||||
"""Test that repo.reprojectcmd requires repo.uselocalgitdirs."""
|
||||
with utils_for_test.TempGitTree() as tempdir:
|
||||
fakeproj = _create_manifest_project(tempdir)
|
||||
|
||||
class DummyManifest:
|
||||
is_submanifest = False
|
||||
|
||||
def GetDefaultGroupsStr(
|
||||
self, with_platform: bool = False
|
||||
) -> str:
|
||||
return ""
|
||||
|
||||
fakeproj.manifest = DummyManifest()
|
||||
|
||||
fakeproj.config.SetString("repo.reprojectcmd", "echo hi")
|
||||
fakeproj.config.SetBoolean("repo.uselocalgitdirs", False)
|
||||
|
||||
result = fakeproj.Sync(use_local_gitdirs=False)
|
||||
self.assertFalse(result)
|
||||
|
||||
|
||||
class ReprojectCmdGitTests(unittest.TestCase):
|
||||
"""Tests running the reprojectcmd contract against a real Git checkout."""
|
||||
|
||||
READ_TREE = "git -C $REPO_PATH read-tree -m -u $REPO_TREV"
|
||||
|
||||
@staticmethod
|
||||
def _git(cwd: str, *args: str) -> str:
|
||||
return subprocess.run(
|
||||
["git", "-C", cwd] + list(args),
|
||||
check=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
universal_newlines=True,
|
||||
).stdout.rstrip("\n")
|
||||
|
||||
def _make_client(
|
||||
self, topdir: str, reproject_cmd: Optional[str]
|
||||
) -> Tuple[project.Project, str]:
|
||||
"""Set up a fetched, never checked out project under |topdir|.
|
||||
|
||||
Returns:
|
||||
The project and the commit its manifest revision names.
|
||||
"""
|
||||
# A remote holding the history the project fetches.
|
||||
remote = os.path.join(topdir, "remote")
|
||||
os.mkdir(remote)
|
||||
self._git(remote, "init", "-q")
|
||||
self._git(remote, "symbolic-ref", "HEAD", "refs/heads/main")
|
||||
for msg, files in (
|
||||
("one", {"one.txt": "one\n", "keep.txt": "keep\n"}),
|
||||
("two", {"one.txt": "one, revised\n", "two.txt": "two\n"}),
|
||||
):
|
||||
for name, content in files.items():
|
||||
with open(os.path.join(remote, name), "w") as fp:
|
||||
fp.write(content)
|
||||
self._git(remote, "add", name)
|
||||
self._git(
|
||||
remote,
|
||||
"-c",
|
||||
"user.name=Test",
|
||||
"-c",
|
||||
"user.email=test@example.com",
|
||||
"commit",
|
||||
"-q",
|
||||
"-m",
|
||||
msg,
|
||||
)
|
||||
revid = self._git(remote, "rev-parse", "HEAD")
|
||||
|
||||
# The project as repo.fetchcmd leaves it: objects fetched, HEAD on an
|
||||
# unborn branch, nothing in the index or the worktree.
|
||||
worktree = os.path.join(topdir, "proj")
|
||||
os.mkdir(worktree)
|
||||
self._git(worktree, "init", "-q")
|
||||
self._git(worktree, "symbolic-ref", "HEAD", "refs/heads/main")
|
||||
self._git(worktree, "fetch", "-q", remote, "refs/heads/main")
|
||||
|
||||
manifest = mock.MagicMock()
|
||||
manifest.manifestProject.use_local_gitdirs = True
|
||||
manifest.manifestProject.reproject_cmd = reproject_cmd
|
||||
manifest.UseLocalGitDirs = True
|
||||
manifest.IsMirror = False
|
||||
manifest.is_multimanifest = False
|
||||
manifest.topdir = topdir
|
||||
manifest.path_prefix = ""
|
||||
manifest.globalConfig = None
|
||||
|
||||
remote_spec = mock.MagicMock()
|
||||
remote_spec.name = "origin"
|
||||
remote_spec.url = remote
|
||||
|
||||
proj = project.Project(
|
||||
manifest=manifest,
|
||||
name="proj",
|
||||
remote=remote_spec,
|
||||
gitdir=os.path.join(worktree, ".git"),
|
||||
objdir=os.path.join(worktree, ".git"),
|
||||
worktree=worktree,
|
||||
relpath="proj",
|
||||
revisionExpr="main",
|
||||
revisionId=revid,
|
||||
)
|
||||
proj._Checkout = mock.MagicMock(
|
||||
side_effect=AssertionError("Git must not do the checkout")
|
||||
)
|
||||
return proj, revid
|
||||
|
||||
def _sync(self, proj: project.Project) -> Tuple[bool, List[Any]]:
|
||||
"""Run Sync_LocalHalf; return whether it succeeded and its errors."""
|
||||
syncbuf = project.SyncBuffer(proj.config)
|
||||
proj.Sync_LocalHalf(syncbuf)
|
||||
return syncbuf.Finish(), syncbuf.errors
|
||||
|
||||
def _step_back(self, proj: project.Project, revid: str) -> str:
|
||||
"""Put the project cleanly at the commit before |revid|, with Git."""
|
||||
parent = self._git(proj.worktree, "rev-parse", revid + "~1")
|
||||
self._git(proj.worktree, "update-ref", "--no-deref", "HEAD", parent)
|
||||
self._git(proj.worktree, "read-tree", "-u", "--reset", "HEAD")
|
||||
return parent
|
||||
|
||||
def test_sync_local_half_checks_out_a_fresh_project(self) -> None:
|
||||
"""Test the read-tree command materializes a project like Git would."""
|
||||
with tempfile.TemporaryDirectory(prefix="repo-tests") as topdir:
|
||||
marker = os.path.join(topdir, "ran")
|
||||
proj, revid = self._make_client(
|
||||
topdir, f"{self.READ_TREE} && touch {marker}"
|
||||
)
|
||||
worktree = proj.worktree
|
||||
|
||||
syncbuf = project.SyncBuffer(proj.config)
|
||||
proj.Sync_LocalHalf(syncbuf)
|
||||
self.assertTrue(syncbuf.Finish(), syncbuf.errors)
|
||||
|
||||
self.assertTrue(os.path.exists(marker))
|
||||
self.assertEqual(self._git(worktree, "rev-parse", "HEAD"), revid)
|
||||
with self.assertRaises(subprocess.CalledProcessError):
|
||||
self._git(worktree, "symbolic-ref", "-q", "HEAD")
|
||||
self.assertEqual(self._git(worktree, "status", "--porcelain"), "")
|
||||
with open(os.path.join(worktree, "one.txt")) as fp:
|
||||
self.assertEqual(fp.read(), "one, revised\n")
|
||||
with open(os.path.join(worktree, "two.txt")) as fp:
|
||||
self.assertEqual(fp.read(), "two\n")
|
||||
|
||||
# Syncing again finds HEAD at the target and leaves it alone.
|
||||
os.remove(marker)
|
||||
syncbuf = project.SyncBuffer(proj.config)
|
||||
proj.Sync_LocalHalf(syncbuf)
|
||||
self.assertTrue(syncbuf.Finish(), syncbuf.errors)
|
||||
self.assertFalse(os.path.exists(marker))
|
||||
self.assertEqual(self._git(worktree, "rev-parse", "HEAD"), revid)
|
||||
|
||||
def test_sync_local_half_leaves_an_untracked_file_in_the_way_alone(
|
||||
self,
|
||||
) -> None:
|
||||
"""Test an untracked file the target adds fails without any change."""
|
||||
with tempfile.TemporaryDirectory(prefix="repo-tests") as topdir:
|
||||
proj, revid = self._make_client(topdir, self.READ_TREE)
|
||||
worktree = proj.worktree
|
||||
with open(os.path.join(worktree, "two.txt"), "w") as fp:
|
||||
fp.write("mine\n")
|
||||
|
||||
clean, errors = self._sync(proj)
|
||||
self.assertFalse(clean)
|
||||
self.assertIn("two.txt", str(errors[0]))
|
||||
|
||||
self.assertEqual(
|
||||
self._git(worktree, "symbolic-ref", "HEAD"), "refs/heads/main"
|
||||
)
|
||||
self.assertEqual(sorted(os.listdir(worktree)), [".git", "two.txt"])
|
||||
with open(os.path.join(worktree, "two.txt")) as fp:
|
||||
self.assertEqual(fp.read(), "mine\n")
|
||||
|
||||
def test_sync_local_half_keeps_local_changes_out_of_the_way(self) -> None:
|
||||
"""Test edits and untracked files the target leaves alone survive."""
|
||||
with tempfile.TemporaryDirectory(prefix="repo-tests") as topdir:
|
||||
proj, revid = self._make_client(topdir, self.READ_TREE)
|
||||
worktree = proj.worktree
|
||||
self.assertTrue(self._sync(proj)[0])
|
||||
self._step_back(proj, revid)
|
||||
with open(os.path.join(worktree, "keep.txt"), "w") as fp:
|
||||
fp.write("edited\n")
|
||||
with open(os.path.join(worktree, "junk"), "w") as fp:
|
||||
fp.write("junk\n")
|
||||
|
||||
clean, errors = self._sync(proj)
|
||||
self.assertTrue(clean, errors)
|
||||
|
||||
self.assertEqual(self._git(worktree, "rev-parse", "HEAD"), revid)
|
||||
with open(os.path.join(worktree, "one.txt")) as fp:
|
||||
self.assertEqual(fp.read(), "one, revised\n")
|
||||
with open(os.path.join(worktree, "keep.txt")) as fp:
|
||||
self.assertEqual(fp.read(), "edited\n")
|
||||
self.assertEqual(
|
||||
self._git(worktree, "status", "--porcelain").splitlines(),
|
||||
[" M keep.txt", "?? junk"],
|
||||
)
|
||||
|
||||
def test_sync_local_half_rejects_a_local_change_in_the_way(self) -> None:
|
||||
"""Test an edit to a file the target changes fails without a change."""
|
||||
with tempfile.TemporaryDirectory(prefix="repo-tests") as topdir:
|
||||
proj, revid = self._make_client(topdir, self.READ_TREE)
|
||||
worktree = proj.worktree
|
||||
self.assertTrue(self._sync(proj)[0])
|
||||
parent = self._step_back(proj, revid)
|
||||
with open(os.path.join(worktree, "one.txt"), "w") as fp:
|
||||
fp.write("edited\n")
|
||||
|
||||
clean, errors = self._sync(proj)
|
||||
self.assertFalse(clean)
|
||||
self.assertIn("one.txt", str(errors[0]))
|
||||
|
||||
self.assertEqual(self._git(worktree, "rev-parse", "HEAD"), parent)
|
||||
with open(os.path.join(worktree, "one.txt")) as fp:
|
||||
self.assertEqual(fp.read(), "edited\n")
|
||||
self.assertNotIn("two.txt", os.listdir(worktree))
|
||||
|
||||
def test_sync_local_half_rejects_a_command_that_moves_head(self) -> None:
|
||||
"""Test a command that writes HEAD fails the postcondition."""
|
||||
with tempfile.TemporaryDirectory(prefix="repo-tests") as topdir:
|
||||
proj, revid = self._make_client(
|
||||
topdir,
|
||||
f"{self.READ_TREE} && git -C $REPO_PATH update-ref "
|
||||
"--no-deref HEAD $REPO_TREV",
|
||||
)
|
||||
syncbuf = project.SyncBuffer(proj.config)
|
||||
proj.Sync_LocalHalf(syncbuf)
|
||||
self.assertFalse(syncbuf.Finish())
|
||||
self.assertIn("moved HEAD", str(syncbuf.errors[0]))
|
||||
|
||||
def test_sync_local_half_rejects_staged_changes(self) -> None:
|
||||
"""Test a staged change fails before the command runs."""
|
||||
with tempfile.TemporaryDirectory(prefix="repo-tests") as topdir:
|
||||
proj, revid = self._make_client(topdir, self.READ_TREE)
|
||||
worktree = proj.worktree
|
||||
self.assertTrue(self._sync(proj)[0])
|
||||
self._step_back(proj, revid)
|
||||
with open(os.path.join(worktree, "keep.txt"), "w") as fp:
|
||||
fp.write("staged\n")
|
||||
self._git(worktree, "add", "keep.txt")
|
||||
|
||||
clean, errors = self._sync(proj)
|
||||
self.assertFalse(clean)
|
||||
self.assertIn("staged changes", str(errors[0]))
|
||||
self.assertIn("keep.txt", str(errors[0]))
|
||||
|
||||
def test_sync_local_half_rejects_a_command_that_moves_branch_tip(
|
||||
self,
|
||||
) -> None:
|
||||
"""Test a command that moves the branch commit fails postcondition."""
|
||||
with tempfile.TemporaryDirectory(prefix="repo-tests") as topdir:
|
||||
proj, revid = self._make_client(
|
||||
topdir,
|
||||
f"{self.READ_TREE} && git -C $REPO_PATH update-ref "
|
||||
"refs/heads/main $REPO_TREV",
|
||||
)
|
||||
clean, errors = self._sync(proj)
|
||||
self.assertFalse(clean)
|
||||
self.assertIn("moved HEAD", str(errors[0]))
|
||||
|
||||
def test_sync_local_half_keeps_local_changes_on_tracking_branch(
|
||||
self,
|
||||
) -> None:
|
||||
"""Test benign edits survive fast-forward on a tracking branch."""
|
||||
with tempfile.TemporaryDirectory(prefix="repo-tests") as topdir:
|
||||
proj, revid = self._make_client(topdir, self.READ_TREE)
|
||||
worktree = proj.worktree
|
||||
self.assertTrue(self._sync(proj)[0])
|
||||
parent = self._step_back(proj, revid)
|
||||
self._git(worktree, "checkout", "-q", "-b", "main", parent)
|
||||
self._git(worktree, "config", "branch.main.remote", "origin")
|
||||
self._git(
|
||||
worktree, "config", "branch.main.merge", "refs/heads/main"
|
||||
)
|
||||
with open(os.path.join(worktree, "keep.txt"), "w") as fp:
|
||||
fp.write("edited\n")
|
||||
with open(os.path.join(worktree, "junk"), "w") as fp:
|
||||
fp.write("junk\n")
|
||||
|
||||
clean, errors = self._sync(proj)
|
||||
self.assertTrue(clean, errors)
|
||||
|
||||
self.assertEqual(self._git(worktree, "rev-parse", "HEAD"), revid)
|
||||
with open(os.path.join(worktree, "one.txt")) as fp:
|
||||
self.assertEqual(fp.read(), "one, revised\n")
|
||||
with open(os.path.join(worktree, "keep.txt")) as fp:
|
||||
self.assertEqual(fp.read(), "edited\n")
|
||||
self.assertEqual(
|
||||
self._git(worktree, "status", "--porcelain").splitlines(),
|
||||
[" M keep.txt", "?? junk"],
|
||||
)
|
||||
@@ -13,6 +13,7 @@
|
||||
# limitations under the License.
|
||||
"""Unittests for the subcmds/sync.py module."""
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
import optparse
|
||||
import os
|
||||
@@ -20,7 +21,7 @@ from pathlib import Path
|
||||
import shutil
|
||||
import tempfile
|
||||
import time
|
||||
from typing import Dict, List, Optional
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
@@ -516,6 +517,7 @@ class FakeProject:
|
||||
|
||||
self.use_git_worktrees = False
|
||||
self.UseAlternates = False
|
||||
self.UseReprojectCmd = False
|
||||
self.manifest = mock.MagicMock()
|
||||
self.manifest.GetProjectsWithName.return_value = [self]
|
||||
self.config = mock.MagicMock()
|
||||
@@ -642,6 +644,27 @@ class SafeCheckoutOrder(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
class NestedProjects(unittest.TestCase):
|
||||
def test_flat_manifest(self) -> None:
|
||||
p_foo = FakeProject("foo")
|
||||
p_foo_bar = FakeProject("foo-bar")
|
||||
self.assertEqual(sync._NestedProjects([p_foo, p_foo_bar]), [])
|
||||
|
||||
def test_nested_paths(self) -> None:
|
||||
p_foo = FakeProject("foo")
|
||||
p_foo_bar = FakeProject("foo/bar")
|
||||
p_foo_bar_baz = FakeProject("foo/bar/baz")
|
||||
self.assertEqual(
|
||||
sync._NestedProjects([p_foo_bar_baz, p_foo, p_foo_bar]),
|
||||
[p_foo_bar, p_foo_bar_baz],
|
||||
)
|
||||
|
||||
def test_submodule_of_a_parent(self) -> None:
|
||||
parent = FakeProject("foo")
|
||||
sub = FakeProject("foo/sub", parent=parent, is_derived=True)
|
||||
self.assertEqual(sync._NestedProjects([parent, sub]), [sub])
|
||||
|
||||
|
||||
class ParentFirstBatches(unittest.TestCase):
|
||||
def test_no_submodules(self) -> None:
|
||||
p_a = FakeProject("a")
|
||||
@@ -1083,7 +1106,10 @@ class SyncCommand(unittest.TestCase):
|
||||
self.project = p = mock.MagicMock(
|
||||
use_git_worktrees=False,
|
||||
UseAlternates=False,
|
||||
UseReprojectCmd=False,
|
||||
name="project",
|
||||
relpath="rel_path",
|
||||
parent=None,
|
||||
Sync_NetworkHalf=Sync_NetworkHalf,
|
||||
Sync_LocalHalf=Sync_LocalHalf,
|
||||
RelPath=mock.Mock(return_value="rel_path"),
|
||||
@@ -1142,6 +1168,78 @@ class SyncCommand(unittest.TestCase):
|
||||
_, kwargs = self.cmd.GetProjects.call_args
|
||||
self.assertEqual(kwargs.get("groups"), "my_group")
|
||||
|
||||
def _ExecuteUntilSync(
|
||||
self, args: List[str]
|
||||
) -> Tuple[mock.MagicMock, mock.MagicMock]:
|
||||
"""Run Execute up to the sync itself, returning the sync mocks."""
|
||||
self.opt.mp_update = False
|
||||
with contextlib.ExitStack() as stack:
|
||||
for name in (
|
||||
"_UpdateRepoProject",
|
||||
"_UpdateProjectsRevisionId",
|
||||
"_ValidateOptionsWithManifest",
|
||||
"_RunPostSyncHook",
|
||||
):
|
||||
stack.enter_context(mock.patch.object(self.cmd, name))
|
||||
phased = stack.enter_context(
|
||||
mock.patch.object(self.cmd, "_SyncPhased")
|
||||
)
|
||||
interleaved = stack.enter_context(
|
||||
mock.patch.object(self.cmd, "_SyncInterleaved")
|
||||
)
|
||||
self.cmd.Execute(self.opt, args)
|
||||
return phased, interleaved
|
||||
|
||||
def test_reproject_cmd_allows_a_flat_manifest(self) -> None:
|
||||
"""Ensure repo.reprojectcmd syncs a manifest without nesting."""
|
||||
self.project.UseReprojectCmd = True
|
||||
phased, interleaved = self._ExecuteUntilSync([])
|
||||
self.assertTrue(phased.called or interleaved.called)
|
||||
|
||||
def test_reproject_cmd_rejects_nested_projects(self) -> None:
|
||||
"""Ensure repo.reprojectcmd fails a manifest with nested projects."""
|
||||
p_foo = FakeProject("foo")
|
||||
p_foo_bar = FakeProject("foo/bar")
|
||||
p_foo.UseReprojectCmd = p_foo_bar.UseReprojectCmd = True
|
||||
self.cmd.GetProjects.return_value = [p_foo, p_foo_bar]
|
||||
with self.assertRaises(sync.SyncError) as e:
|
||||
self._ExecuteUntilSync([])
|
||||
self.assertIn("foo/bar", str(e.exception))
|
||||
self.assertNotIn(" - foo\n", str(e.exception))
|
||||
|
||||
def test_reproject_cmd_rejects_a_submodule(self) -> None:
|
||||
"""Ensure repo.reprojectcmd fails a manifest with a submodule."""
|
||||
p_foo = FakeProject("foo")
|
||||
p_sub = FakeProject("foo/sub", parent=p_foo, is_derived=True)
|
||||
p_foo.UseReprojectCmd = p_sub.UseReprojectCmd = True
|
||||
self.cmd.GetProjects.return_value = [p_foo, p_sub]
|
||||
with self.assertRaises(sync.SyncError) as e:
|
||||
self._ExecuteUntilSync([])
|
||||
self.assertIn("foo/sub", str(e.exception))
|
||||
|
||||
def test_reproject_cmd_checks_the_whole_manifest(self) -> None:
|
||||
"""Ensure nesting is checked beyond the projects given as args."""
|
||||
p_foo = FakeProject("foo")
|
||||
p_foo_bar = FakeProject("foo/bar")
|
||||
p_foo.UseReprojectCmd = p_foo_bar.UseReprojectCmd = True
|
||||
self.cmd.GetProjects.side_effect = lambda args, **kwargs: (
|
||||
[p_foo_bar] if args else [p_foo, p_foo_bar]
|
||||
)
|
||||
with self.assertRaises(sync.SyncError):
|
||||
self._ExecuteUntilSync(["foo/bar"])
|
||||
self.assertEqual(self.cmd.GetProjects.call_count, 2)
|
||||
_, kwargs = self.cmd.GetProjects.call_args
|
||||
self.assertEqual(kwargs.get("missing_ok"), True)
|
||||
|
||||
def test_reproject_cmd_off_ignores_nested_projects(self) -> None:
|
||||
"""Ensure nesting is only checked with repo.reprojectcmd in use."""
|
||||
projects = [FakeProject("foo"), FakeProject("foo/bar")]
|
||||
for p in projects:
|
||||
p.Exists = False
|
||||
self.cmd.GetProjects.return_value = projects
|
||||
phased, interleaved = self._ExecuteUntilSync([])
|
||||
self.assertTrue(phased.called or interleaved.called)
|
||||
|
||||
|
||||
class SyncUpdateRepoProject(unittest.TestCase):
|
||||
"""Tests for Sync._UpdateRepoProject."""
|
||||
|
||||
Reference in new issue
Block a user