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.<manifest-branch>.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 <brgan@google.com>
Tested-by: Gavin Mak <gavinmak@google.com>
Commit-Queue: Gavin Mak <gavinmak@google.com>
This commit is contained in:
Gavin Mak authored and gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com committed 2026-11-13 13:55:47 -08:00
1 parent af53524261
commit 032f2341ec
13 files changed
+387 -50

No files matched your search

+1
View File
@@ -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]' \
+1
View File
@@ -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.\<branch\>.smartsyncbranch | `--smart-sync-branch` | Server branch to query when [Smart Sync](./smart-sync.md)ing `<branch>` |
| repo.submodules | `--submodules` | Sync git submodules |
| repo.superproject | `--use-superproject` | Sync [superproject] |
| repo.worktree | `--worktree` | Use [git worktree] for checkouts |
+33 -8
View File
@@ -205,6 +205,23 @@ Sync fails (e.g. the manifest checkout is not tracking a remote branch,
`<manifest-server>` 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 <url> -b main -s --smart-sync-branch=git_main-warmup
```
This persists `repo.<manifest-branch>.smartsyncbranch` in
`.repo/manifests.git/config`. Because the setting is scoped to the manifest
branch, switching branches with `repo init -b <other>` will not send the
override branch when syncing `<other>`. 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 <sha>` or `-b refs/tags/<tag>`),
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.<manifest-branch>.smartsyncbranch`.
2. `repo.<manifest-branch>.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 <sha>` or
`-b refs/tags/<tag>`), 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
+4 -1
View File
@@ -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
+67 -30
View File
@@ -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 "
"<branch>`"
)
return False
if not manifest_name:
logger.error("fatal: manifest name (-m) is required.")
return False
+10 -2
View File
@@ -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",
+10 -2
View File
@@ -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 "
+15 -4
View File
@@ -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.<branch>.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(
+6
View File
@@ -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
+118 -1
View File
@@ -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'<project name="platform/foo" path="foo" revision="{rev}" />',
fetch=host.url,
),
}
with _manifest_server(handle_v2) as server_url:
m_xml = manifest_xml(
f'<manifest-server url="{server_url}" />',
'<project name="platform/foo" path="foo" />',
)
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
+58
View File
@@ -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.
+23 -1
View File
@@ -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:
+41 -1
View File
@@ -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.<branch>.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."""