From 032f2341ecb3b4ed6e4574923a91a3c3fd28cb3b Mon Sep 17 00:00:00 2001 From: Gavin Mak Date: Fri, 6 Nov 2026 21:57:42 +0000 Subject: [PATCH] init, sync: support per-branch smart sync branch config Add `repo init --smart-sync-branch=BRANCH` to persist a server-side Smart Sync branch override in `repo..smartsyncbranch` inside .repo/manifests.git/config (with an empty string clearing it). During Smart Sync, non-empty SYNC_BRANCH still takes top precedence, while SYNC_BRANCH="" forces the raw manifest branch over checkout config. Bug: 543101585 Bug: 585378823 Change-Id: I933a48d03ed5ebd83aa5aba9ddde761dfac23243 Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/655063 Reviewed-by: Brian Gan Tested-by: Gavin Mak Commit-Queue: Gavin Mak --- completion.zsh | 1 + docs/internal-fs-layout.md | 1 + docs/smart-sync.md | 41 ++++++++++--- man/repo-init.1 | 5 +- project.py | 97 ++++++++++++++++++++--------- repo | 12 +++- subcmds/init.py | 12 +++- subcmds/sync.py | 19 ++++-- tests/e2e/harness.py | 6 ++ tests/e2e/test_init_sync.py | 119 +++++++++++++++++++++++++++++++++++- tests/test_project.py | 58 ++++++++++++++++++ tests/test_subcmds_init.py | 24 +++++++- tests/test_subcmds_sync.py | 42 ++++++++++++- 13 files changed, 387 insertions(+), 50 deletions(-) diff --git a/completion.zsh b/completion.zsh index 857646ca3..71066d353 100644 --- a/completion.zsh +++ b/completion.zsh @@ -249,6 +249,7 @@ _repo() { '(-s --smart-sync)'{-s,--smart-sync}'[Default to smart sync]' \ '--no-smart-sync[Do not default to smart sync]' \ '--head[Do not default to smart sync]' \ + '--smart-sync-branch=[Smart sync branch]:branch:' \ '--clone-bundle[Use clone bundle]' \ '--no-clone-bundle[Do not use clone bundle]' \ '--git-lfs[Use git lfs]' \ diff --git a/docs/internal-fs-layout.md b/docs/internal-fs-layout.md index bd1bb3844..4e60f85cd 100644 --- a/docs/internal-fs-layout.md +++ b/docs/internal-fs-layout.md @@ -180,6 +180,7 @@ User controlled settings are initialized when running `repo init`. | repo.partialcloneexclude | `--partial-clone-exclude` | Comma-delimited list of project names (not paths) to exclude while using [partial git clones] | | repo.reference | `--reference` | Reference repo client checkout | | repo.smartsync | `--smart-sync` | Default to [Smart Sync](./smart-sync.md) when syncing | +| repo.\.smartsyncbranch | `--smart-sync-branch` | Server branch to query when [Smart Sync](./smart-sync.md)ing `` | | repo.submodules | `--submodules` | Sync git submodules | | repo.superproject | `--use-superproject` | Sync [superproject] | | repo.worktree | `--worktree` | Use [git worktree] for checkouts | diff --git a/docs/smart-sync.md b/docs/smart-sync.md index 16af8ce94..279595cdf 100644 --- a/docs/smart-sync.md +++ b/docs/smart-sync.md @@ -205,6 +205,23 @@ Sync fails (e.g. the manifest checkout is not tracking a remote branch, `` is not defined, or the server cannot provide a usable manifest). +### `repo init --smart-sync-branch=BRANCH` + +Users can configure a server-side Smart Sync branch override for the initialized +manifest branch by passing `--smart-sync-branch=BRANCH` to `repo init`: + +```bash +repo init -u -b main -s --smart-sync-branch=git_main-warmup +``` + +This persists `repo..smartsyncbranch` in +`.repo/manifests.git/config`. Because the setting is scoped to the manifest +branch, switching branches with `repo init -b ` will not send the +override branch when syncing ``. Passing `--smart-sync-branch=""` clears +the override for the target manifest branch. Setting a non-empty override +requires the manifest checkout to track a remote branch (not a tag or commit +SHA). + ### sync-smartsync (Manifest Attribute) The manifest can enable Smart Sync by default for all `repo sync` invocations @@ -238,14 +255,22 @@ Explicitly enables Smart Sync. Repo will call `GetApprovedManifestV2({branch[, target]})`, or `GetApprovedManifest(branch[, target])` if the server does not implement it. -The `branch` is determined by the current manifest branch as specified by -`--manifest-branch=BRANCH` when running `repo init`. If `${SYNC_BRANCH}` is -defined and non-empty, its value is sent instead. This allows selecting a -branch other than the one the checkout tracks, e.g. a server-side (CI) branch -name that builds from the same manifest branch. If `${SYNC_BRANCH}` is unset or -empty and the manifest checkout is not tracking a remote branch (e.g. it is on a -detached HEAD, or was initialized with `-b ` or `-b refs/tags/`), -Smart Sync fails without contacting the manifest server. +The `branch` sent to the manifest server is resolved in the following order: + +1. `${SYNC_BRANCH}`: If defined and non-empty, its value is sent directly. + If defined as an empty string (`SYNC_BRANCH=""`), repo sends the checkout's + raw manifest branch, bypassing `repo..smartsyncbranch`. +2. `repo..smartsyncbranch`: If configured via + `repo init --smart-sync-branch=BRANCH`, its value is sent for that manifest + branch. +3. The current manifest branch (as selected by `--manifest-branch=BRANCH` when + running `repo init`). + +Overriding the branch allows selecting a server-side (CI) branch name that +builds or warms caches from the same manifest branch. If `${SYNC_BRANCH}` is +unset or empty and the manifest checkout is not tracking a remote branch (e.g. +it is on a detached HEAD, or was initialized with `-b ` or +`-b refs/tags/`), Smart Sync fails without contacting the manifest server. The `target` is defined by environment variables in the order below. If none of them match, then `target` is omitted. These variables were decided as they diff --git a/man/repo-init.1 b/man/repo-init.1 index 32b4c33dd..39c09a0cb 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" "June 2026" "repo init" "Repo Manual" +.TH REPO "1" "November 2026" "repo init" "Repo Manual" .SH NAME repo \- repo init - manual page for repo init .SH SYNOPSIS @@ -109,6 +109,9 @@ default to smart sync when syncing projects \fB\-\-no\-smart\-sync\fR, \fB\-\-head\fR do not default to smart sync when syncing projects .TP +\fB\-\-smart\-sync\-branch\fR=\fI\,BRANCH\/\fR +server branch to query when smart syncing the manifest branch (empty string to clear) +.TP \fB\-\-clone\-bundle\fR enable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS (default if not \fB\-\-partial\-clone\fR) .TP diff --git a/project.py b/project.py index f9852a75e..cdc6a4b9b 100644 --- a/project.py +++ b/project.py @@ -29,7 +29,7 @@ import sys import tarfile import tempfile import time -from typing import Any, Dict, List, NamedTuple, Optional, Tuple +from typing import Any, Dict, List, NamedTuple, Optional, Tuple, Union import urllib.parse import color @@ -6081,6 +6081,27 @@ class ManifestProject(MetaProject): """Whether we use smart sync by default.""" return self.config.GetBoolean("repo.smartsync") + @staticmethod + def _SmartSyncBranchKey(branch: str) -> str: + """Return the git config key for a branch's smart sync branch.""" + return f"repo.{branch}.smartsyncbranch" + + def GetSmartSyncBranch(self, branch: Optional[str] = None) -> Optional[str]: + """Return the configured smart sync branch for |branch|.""" + if branch is None: + branch = self.GetMergeBranch(heads_only=True) + if not branch: + return None + return self.config.GetString(self._SmartSyncBranchKey(branch)) + + def SetSmartSyncBranch( + self, branch: str, smart_sync_branch: Optional[str] + ) -> None: + """Set or clear the smart sync branch for |branch|.""" + self.config.SetString( + self._SmartSyncBranchKey(branch), smart_sync_branch or None + ) + @property def partial_clone(self): """Whether this is a partial clone.""" @@ -6189,36 +6210,37 @@ class ManifestProject(MetaProject): def Sync( self, - _kwargs_only=(), - manifest_url="", - manifest_branch=None, - standalone_manifest=False, - groups="", - mirror=False, - reference="", - dissociate=False, - worktree=False, - submodules=False, - archive=False, - partial_clone=None, - depth=None, - clone_filter="blob:none", - partial_clone_exclude=None, - clone_bundle=None, - git_lfs=None, - use_superproject=None, + _kwargs_only: Tuple[Any, ...] = (), + manifest_url: Optional[str] = "", + manifest_branch: Optional[str] = None, + standalone_manifest: Union[bool, str, None] = False, + groups: Optional[str] = "", + mirror: Optional[bool] = False, + reference: Optional[str] = "", + dissociate: Optional[bool] = False, + worktree: Optional[bool] = False, + submodules: Optional[bool] = False, + archive: Optional[bool] = False, + partial_clone: Optional[bool] = None, + depth: Optional[int] = None, + clone_filter: Optional[str] = "blob:none", + partial_clone_exclude: Optional[str] = None, + clone_bundle: Optional[bool] = None, + git_lfs: Optional[bool] = None, + use_superproject: Optional[bool] = None, smart_sync: Optional[bool] = None, - verbose=False, - current_branch_only=False, - git_event_log=None, - platform="", - manifest_name="default.xml", - tags="", - this_manifest_only=False, - outer_manifest=True, - clone_filter_for_depth=None, - use_local_gitdirs=False, - ): + smart_sync_branch: Optional[str] = None, + verbose: Optional[bool] = False, + current_branch_only: Optional[bool] = False, + git_event_log: Optional[git_trace2_event_log.EventLog] = None, + platform: Optional[str] = "", + manifest_name: Optional[str] = "default.xml", + tags: Union[bool, str, None] = "", + this_manifest_only: Optional[bool] = False, + outer_manifest: bool = True, + clone_filter_for_depth: Optional[str] = None, + use_local_gitdirs: Optional[bool] = False, + ) -> bool: """Sync the manifest and all submanifests. Args: @@ -6250,6 +6272,8 @@ class ManifestProject(MetaProject): use_superproject: a boolean, whether to use the manifest superproject to sync projects. smart_sync: a boolean, whether to default to smart sync. + smart_sync_branch: a string, server branch to query when smart + syncing the manifest branch (empty string to clear). verbose: a boolean, whether to show all output, rather than only errors. current_branch_only: a boolean, whether to only fetch the current @@ -6298,6 +6322,7 @@ class ManifestProject(MetaProject): git_lfs=git_lfs, use_superproject=use_superproject, smart_sync=smart_sync, + smart_sync_branch=smart_sync_branch, verbose=verbose, current_branch_only=current_branch_only, tags=tags, @@ -6619,6 +6644,18 @@ class ManifestProject(MetaProject): ) return False + if smart_sync_branch is not None: + target_branch = self.GetMergeBranch(heads_only=True) + if target_branch: + self.SetSmartSyncBranch(target_branch, smart_sync_branch) + elif smart_sync_branch: + logger.error( + "fatal: --smart-sync-branch requires the manifest " + "checkout to track a branch; run `repo init -b " + "`" + ) + return False + if not manifest_name: logger.error("fatal: manifest name (-m) is required.") return False diff --git a/repo b/repo index cbe9519a1..02e246bae 100755 --- a/repo +++ b/repo @@ -127,7 +127,7 @@ REPO_REV = os.environ.get("REPO_REV") or "stable" BUG_URL = "https://issues.gerritcodereview.com/issues/new?component=1370071" # increment this whenever we make important changes to this script -VERSION = (2, 71) +VERSION = (2, 72) # increment this if the MAINTAINER_KEYS block is modified KEYRING_VERSION = (2, 3) @@ -249,7 +249,7 @@ def GetParser(): return parser -def InitParser(parser): +def InitParser(parser: optparse.OptionParser) -> optparse.OptionParser: """Setup the CLI parser.""" # NB: Keep in sync with command.py:_CommonOptions(). @@ -455,6 +455,14 @@ def InitParser(parser): dest="smart_sync", help="do not default to smart sync when syncing projects", ) + group.add_option( + "--smart-sync-branch", + action="store", + default=None, + metavar="BRANCH", + help="server branch to query when smart syncing the manifest branch " + "(empty string to clear)", + ) group.add_option( "--clone-bundle", action="store_true", diff --git a/subcmds/init.py b/subcmds/init.py index 0eb2444e8..cc6f96ac8 100644 --- a/subcmds/init.py +++ b/subcmds/init.py @@ -12,8 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +import optparse import os import sys +from typing import List import color import command @@ -128,7 +130,7 @@ to update the working directory files. "REPO_GIT_LFS": "git_lfs", } - def _SyncManifest(self, opt): + def _SyncManifest(self, opt: optparse.Values) -> None: """Call manifestProject.Sync with arguments from opt. Args: @@ -162,6 +164,7 @@ to update the working directory files. git_lfs=opt.git_lfs, use_superproject=opt.use_superproject, smart_sync=opt.smart_sync, + smart_sync_branch=opt.smart_sync_branch, verbose=opt.verbose, current_branch_only=opt.current_branch_only, tags=opt.tags, @@ -294,7 +297,7 @@ to update the working directory files. print(" rm -r %s" % os.path.join(self.manifest.topdir, ".repo")) print("and try again.") - def ValidateOptions(self, opt, args): + def ValidateOptions(self, opt: optparse.Values, args: List[str]) -> None: if opt.reference: opt.reference = os.path.expanduser(opt.reference) @@ -323,6 +326,11 @@ to update the working directory files. " be used with --standalone-manifest." ) + if opt.standalone_manifest and opt.smart_sync_branch is not None: + self.OptionParser.error( + "--smart-sync-branch cannot be used with --standalone-manifest." + ) + if opt.manifest_upstream_branch and opt.manifest_branch is None: self.OptionParser.error( "--manifest-upstream-branch cannot be used without " diff --git a/subcmds/sync.py b/subcmds/sync.py index 3ca6ad3b9..18c15d61d 100644 --- a/subcmds/sync.py +++ b/subcmds/sync.py @@ -2309,10 +2309,21 @@ later is required to fix a server side protocol bug. if opt.smart_sync: # SYNC_BRANCH overrides the branch sent to the manifest server # (e.g. to select a specific CI branch that builds the same - # manifest branch). An empty value is treated as unset. - branch = os.environ.get("SYNC_BRANCH") or ( - manifest.manifestProject.GetMergeBranch(heads_only=True) - ) + # manifest branch). Setting SYNC_BRANCH="" forces the raw + # manifest branch, bypassing repo..smartsyncbranch. + if "SYNC_BRANCH" in os.environ: + branch = os.environ["SYNC_BRANCH"] or ( + manifest.manifestProject.GetMergeBranch(heads_only=True) + ) + else: + manifest_branch = manifest.manifestProject.GetMergeBranch( + heads_only=True + ) + if manifest_branch: + cfg_branch = manifest.manifestProject.GetSmartSyncBranch( + manifest_branch + ) + branch = cfg_branch or manifest_branch # Fail before running the helper or contacting the server. if not branch: raise SmartSyncError( diff --git a/tests/e2e/harness.py b/tests/e2e/harness.py index f295ce02c..36d9c3bd5 100644 --- a/tests/e2e/harness.py +++ b/tests/e2e/harness.py @@ -166,6 +166,12 @@ def clean_environ() -> Dict[str, str]: "pythonwarnings", # LUCI's git wrapper replaces HOME with this. "infra_git_wrapper_home", + # Smart sync reads these (e.g. set by Android's envsetup/lunch). + "sync_branch", + "sync_target", + "target_product", + "target_release", + "target_build_variant", ): continue env[key] = value diff --git a/tests/e2e/test_init_sync.py b/tests/e2e/test_init_sync.py index 67c3d53af..16e082f65 100644 --- a/tests/e2e/test_init_sync.py +++ b/tests/e2e/test_init_sync.py @@ -14,8 +14,11 @@ """E2E tests for the core `repo init` & `repo sync` options.""" +import contextlib from pathlib import Path -from typing import List, Set +import threading +from typing import Any, Callable, Dict, Iterator, List, Set +import xmlrpc.server from harness import GIT_DEFAULTS_PARAMS from harness import GitHost @@ -353,3 +356,117 @@ def test_manifest_depth(host: GitHost, client: RepoClient) -> None: assert client.is_shallow(manifests) assert not client.is_shallow("platform/foo") assert client.git("platform/foo", "rev-list", "--count", "HEAD") == "2" + + +@contextlib.contextmanager +def _manifest_server( + handler: Callable[[Any], Dict[str, Any]], +) -> Iterator[str]: + """Run a local XML-RPC manifest server exposing GetApprovedManifestV2.""" + server = xmlrpc.server.SimpleXMLRPCServer( + ("127.0.0.1", 0), logRequests=False + ) + server.register_function(handler, "GetApprovedManifestV2") + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + addr, port = server.server_address[:2] + yield f"http://{addr}:{port}/" + finally: + server.shutdown() + server.server_close() + thread.join() + + +@pytest.mark.parametrize("git_defaults", GIT_DEFAULTS_PARAMS) +def test_smart_sync_branch(host: GitHost, client: RepoClient) -> None: + """`init --smart-sync-branch` persists per-manifest-branch server branch.""" + foo = host.create_repo("platform/foo", {"f": "v1"}) + v1 = foo.rev_parse("main") + warmup = foo.commit({"f": "warmup"}) + foo.commit({"f": "tot"}) + r16 = foo.commit({"f": "r16"}, branch="release/16") + + requests: List[Dict[str, Any]] = [] + by_branch = { + "main": v1, + "git_main-dev-warmup-abfs": warmup, + "release/16": r16, + } + + def handle_v2(req: Dict[str, Any]) -> Dict[str, Any]: + requests.append(req) + rev = by_branch[req["branch"]] + return { + "success": True, + "manifest_xml": manifest_xml( + f'', + fetch=host.url, + ), + } + + with _manifest_server(handle_v2) as server_url: + m_xml = manifest_xml( + f'', + '', + ) + manifest = host.create_manifest(m_xml) + manifest.commit({"default.xml": m_xml}, branch="release/16") + + client.init( + host.manifest_url, + "-s", + "--smart-sync-branch=git_main-dev-warmup-abfs", + ) + assert ( + client.config(".repo/manifests", "repo.main.smartsyncbranch") + == "git_main-dev-warmup-abfs" + ) + + client.sync() + assert requests[-1]["branch"] == "git_main-dev-warmup-abfs" + assert client.head("foo") == warmup + + # SYNC_BRANCH="" forces the raw manifest branch over checkout config. + saved_env = dict(client.env) + client.env["SYNC_BRANCH"] = "" + try: + client.sync() + finally: + client.env = saved_env + assert requests[-1]["branch"] == "main" + assert client.head("foo") == v1 + + # Switching manifest branches does not leak main's override. + client.init(host.manifest_url, "-b", "release/16") + client.sync() + assert requests[-1]["branch"] == "release/16" + assert client.head("foo") == r16 + + # Switching back to main restores main's configured override. + client.init(host.manifest_url, "-b", "main") + client.sync() + assert requests[-1]["branch"] == "git_main-dev-warmup-abfs" + assert client.head("foo") == warmup + + # Passing an empty --smart-sync-branch clears the override. + client.init(host.manifest_url, "--smart-sync-branch=") + assert ( + client.config(".repo/manifests", "repo.main.smartsyncbranch") + is None + ) + client.sync() + assert requests[-1]["branch"] == "main" + assert client.head("foo") == v1 + + # Non-empty --smart-sync-branch requires tracking a remote branch. + manifest_sha = manifest.rev_parse("main") + res = client.init( + host.manifest_url, + "-b", + manifest_sha, + "--smart-sync-branch=git_main-dev-warmup-abfs", + check=False, + ) + assert res.returncode != 0 + assert "requires the manifest checkout to track a branch" in res.stderr diff --git a/tests/test_project.py b/tests/test_project.py index 750f7815a..0c4fbdfc7 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -2000,6 +2000,64 @@ class ManifestPropertiesFetchedCorrectly(unittest.TestCase): fakeproj.config.SetString("manifest.platform", "auto") self.assertEqual(fakeproj.manifest_platform, "auto") + def test_smart_sync_branch_config(self) -> None: + """Test branch-scoped smart sync branch getters and setters.""" + with utils_for_test.TempGitTree() as tempdir: + fakeproj = self.setUpManifest(tempdir) + + for branch, target in ( + ("main", "git_main-dev-warmup-abfs"), + ("Main", "git_Main-warmup"), + ("release/16", "git_r16-warmup"), + ("16-release", "git_16-warmup"), + ): + fakeproj.SetSmartSyncBranch(branch, target) + + self.assertEqual( + fakeproj.GetSmartSyncBranch("main"), + "git_main-dev-warmup-abfs", + ) + self.assertEqual( + fakeproj.GetSmartSyncBranch("Main"), "git_Main-warmup" + ) + self.assertEqual( + fakeproj.GetSmartSyncBranch("release/16"), "git_r16-warmup" + ) + self.assertEqual( + fakeproj.GetSmartSyncBranch("16-release"), "git_16-warmup" + ) + self.assertIsNone(fakeproj.GetSmartSyncBranch("other")) + self.assertEqual( + fakeproj.config.GetString("repo.release/16.smartsyncbranch"), + "git_r16-warmup", + ) + + # Default lookup resolves the tracked manifest branch. + fakeproj.work_git.checkout("-q", "-b", "default") + b = fakeproj.GetBranch("default") + b.merge = "refs/heads/release/16" + b.Save() + self.assertEqual( + fakeproj.GetMergeBranch(heads_only=True), "release/16" + ) + self.assertEqual(fakeproj.GetSmartSyncBranch(), "git_r16-warmup") + + # Empty string or None clears only that branch's setting. + fakeproj.SetSmartSyncBranch("release/16", "") + self.assertIsNone(fakeproj.GetSmartSyncBranch("release/16")) + self.assertIsNone(fakeproj.GetSmartSyncBranch()) + self.assertEqual( + fakeproj.GetSmartSyncBranch("main"), + "git_main-dev-warmup-abfs", + ) + fakeproj.SetSmartSyncBranch("main", None) + self.assertIsNone(fakeproj.GetSmartSyncBranch("main")) + + # Non-branch refs (e.g. tags) are ignored by default lookup. + b.merge = "refs/tags/v1.0" + b.Save() + self.assertIsNone(fakeproj.GetSmartSyncBranch()) + def test_check_immutable_revision_metaproject_skips_manifest_load(self): """MetaProjects must not parse manifest.xml during immutable check. diff --git a/tests/test_subcmds_init.py b/tests/test_subcmds_init.py index 369f20dfc..89879c199 100644 --- a/tests/test_subcmds_init.py +++ b/tests/test_subcmds_init.py @@ -14,7 +14,7 @@ """Unittests for the subcmds/init.py module.""" -from typing import List +from typing import List, Optional import pytest @@ -29,6 +29,8 @@ from subcmds import init ["--smart-sync"], ["--no-smart-sync"], ["--head"], + ["--smart-sync-branch=git_main-warmup"], + ["--smart-sync-branch="], ), ) def test_cli_parser_good(argv: List[str]) -> None: @@ -38,6 +40,24 @@ def test_cli_parser_good(argv: List[str]) -> None: cmd.ValidateOptions(opts, args) +@pytest.mark.parametrize( + "argv,expected", + ( + ([], None), + (["--smart-sync-branch=git_main-warmup"], "git_main-warmup"), + (["--smart-sync-branch="], ""), + ), +) +def test_cli_smart_sync_branch_value( + argv: List[str], expected: Optional[str] +) -> None: + """Check --smart-sync-branch parsed values.""" + cmd = init.Init() + opts, args = cmd.OptionParser.parse_args(argv) + cmd.ValidateOptions(opts, args) + assert opts.smart_sync_branch == expected + + @pytest.mark.parametrize( "argv", ( @@ -45,6 +65,8 @@ def test_cli_parser_good(argv: List[str]) -> None: ["url", "asdf"], # Conflicting options. ["--mirror", "--archive"], + ["--standalone-manifest", "--smart-sync-branch=git_main-warmup"], + ["--standalone-manifest", "--smart-sync-branch="], ), ) def test_cli_parser_bad(argv: List[str]) -> None: diff --git a/tests/test_subcmds_sync.py b/tests/test_subcmds_sync.py index 830d1c774..9473f7572 100644 --- a/tests/test_subcmds_sync.py +++ b/tests/test_subcmds_sync.py @@ -3936,6 +3936,7 @@ class TestSmartSyncSetupRemoteHelper(unittest.TestCase): self.opt.smart_sync = True self.manifest = mock.MagicMock() self.manifest.manifestProject.GetMergeBranch.return_value = "main" + self.manifest.manifestProject.GetSmartSyncBranch.return_value = None self.smart_sync_manifest_path = "/fake/path/to/manifest.xml" @mock.patch.object(shutil, "which") @@ -4198,6 +4199,7 @@ class TestSmartSyncSetupBranch: ) self.manifest.manifest_server_helper = None self.manifest.manifestProject.GetMergeBranch.return_value = "main" + self.manifest.manifestProject.GetSmartSyncBranch.return_value = None self.smart_sync_manifest_path = "/fake/path/to/manifest.xml" self.mock_server = mock.MagicMock() self.mock_server.GetApprovedManifestV2.return_value = { @@ -4265,13 +4267,16 @@ class TestSmartSyncSetupBranch: def test_no_branch(self, env: Dict[str, str]) -> None: """Without any branch, fail before printing or contacting the server.""" self.opt.quiet = False - self.manifest.manifestProject.GetMergeBranch.return_value = None + mp = self.manifest.manifestProject + mp.GetMergeBranch.return_value = None + mp.GetSmartSyncBranch.return_value = "git_main-warmup" with mock.patch("builtins.print") as mock_print: with pytest.raises( sync.SmartSyncError, match="not tracking a remote branch" ): self._RunSetup(env) mock_print.assert_not_called() + mp.GetSmartSyncBranch.assert_not_called() self.cmd._ResolveManifestServerTransport.assert_not_called() self.mock_server.GetApprovedManifestV2.assert_not_called() @@ -4289,6 +4294,41 @@ class TestSmartSyncSetupBranch: mp.GetMergeBranch.assert_not_called() self.mock_server.GetManifest.assert_called_once_with("build-1234") + def test_config_smart_sync_branch(self) -> None: + """Configured repo..smartsyncbranch overrides manifest + branch.""" + mp = self.manifest.manifestProject + mp.GetSmartSyncBranch.return_value = "git_main-warmup" + self._RunSetup({}) + mp.GetMergeBranch.assert_called_once_with(heads_only=True) + mp.GetSmartSyncBranch.assert_called_once_with("main") + self.mock_server.GetApprovedManifestV2.assert_called_once_with( + {"branch": "git_main-warmup"} + ) + + def test_sync_branch_overrides_config(self) -> None: + """Non-empty SYNC_BRANCH takes precedence over checkout config.""" + mp = self.manifest.manifestProject + mp.GetSmartSyncBranch.return_value = "git_main-warmup" + self._RunSetup({"SYNC_BRANCH": "git_main-env"}) + mp.GetMergeBranch.assert_not_called() + mp.GetSmartSyncBranch.assert_not_called() + self.mock_server.GetApprovedManifestV2.assert_called_once_with( + {"branch": "git_main-env"} + ) + + def test_empty_sync_branch_bypasses_config(self) -> None: + """SYNC_BRANCH='' forces the raw manifest branch over checkout + config.""" + mp = self.manifest.manifestProject + mp.GetSmartSyncBranch.return_value = "git_main-warmup" + self._RunSetup({"SYNC_BRANCH": ""}) + mp.GetMergeBranch.assert_called_once_with(heads_only=True) + mp.GetSmartSyncBranch.assert_not_called() + self.mock_server.GetApprovedManifestV2.assert_called_once_with( + {"branch": "main"} + ) + class GetApprovedManifestTests(unittest.TestCase): """Tests for _GetApprovedManifest."""