sync: allow syncing groups with repo sync -g group

Similar to how repo init -g can restrict repo syncs to
a subset of the manifest globally, allow "repo sync -g" to only sync a
subset of projects from the manifest when running that specific
sync command.

Change-Id: I4929aad109de05c73a7db42bb27fd8d47eea32fc
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/609481
Reviewed-by: Mike Frysinger <vapier@google.com>
Commit-Queue: Andrew Chant <achant@google.com>
Reviewed-by: Gavin Mak <gavinmak@google.com>
Tested-by: Andrew Chant <achant@google.com>
This commit is contained in:
Andrew Chant
2026-07-17 15:09:06 -07:00
committed by gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com
parent 06c4f9e1cc
commit 0b82311632
4 changed files with 146 additions and 0 deletions
+3
View File
@@ -68,6 +68,9 @@ fetch all branches from server
\fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml
temporary manifest to use for this sync
.TP
\fB\-g\fR GROUP, \fB\-\-groups\fR=\fI\,GROUP\/\fR
sync projects matching the specific groups. Not persistent unlike when used on init
.TP
\fB\-\-clone\-bundle\fR
enable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS
.TP
+3
View File
@@ -68,6 +68,9 @@ fetch all branches from server
\fB\-m\fR NAME.xml, \fB\-\-manifest\-name\fR=\fI\,NAME\/\fR.xml
temporary manifest to use for this sync
.TP
\fB\-g\fR GROUP, \fB\-\-groups\fR=\fI\,GROUP\/\fR
sync projects matching the specific groups. Not persistent unlike when used on init
.TP
\fB\-\-clone\-bundle\fR
enable use of \fI\,/clone.bundle\/\fP on HTTP/HTTPS
.TP
+11
View File
@@ -558,6 +558,13 @@ later is required to fix a server side protocol bug.
help="temporary manifest to use for this sync",
metavar="NAME.xml",
)
p.add_option(
"-g",
"--groups",
help="sync projects matching the specific groups. Not persistent "
"unlike when used on init",
metavar="GROUP",
)
p.add_option(
"--clone-bundle",
action="store_true",
@@ -774,6 +781,7 @@ later is required to fix a server side protocol bug.
all_projects = self.GetProjects(
args,
groups=opt.groups,
missing_ok=True,
submodules_ok=opt.recurse_submodules,
manifest=manifest,
@@ -1103,6 +1111,7 @@ later is required to fix a server side protocol bug.
self._ReloadManifest(None, manifest)
all_projects = self.GetProjects(
args,
groups=opt.groups,
missing_ok=True,
submodules_ok=opt.recurse_submodules,
manifest=manifest,
@@ -2350,6 +2359,7 @@ later is required to fix a server side protocol bug.
all_projects = self.GetProjects(
args,
groups=opt.groups,
missing_ok=True,
submodules_ok=opt.recurse_submodules,
manifest=manifest,
@@ -3013,6 +3023,7 @@ later is required to fix a server side protocol bug.
self._ReloadManifest(None, manifest)
project_list = self.GetProjects(
args,
groups=opt.groups,
missing_ok=True,
submodules_ok=opt.recurse_submodules,
manifest=manifest,
+129
View File
@@ -15,6 +15,7 @@
import json
import os
from pathlib import Path
import shutil
import tempfile
import time
@@ -26,6 +27,7 @@ import pytest
import command
from error import GitError
from error import RepoExitError
import manifest_xml
from project import SyncNetworkHalfResult
from subcmds import sync
@@ -96,6 +98,120 @@ def test_get_current_branch_only(use_superproject, cli_args, result):
assert cmd._GetCurrentBranchOnly(opts, cmd.manifest) == result
@pytest.mark.parametrize(
"cli_args, expected_groups",
[
([], None),
(["-g", "groupA"], "groupA"),
(["--groups=groupB,groupC"], "groupB,groupC"),
],
)
def test_groups_option_parsing(cli_args, expected_groups):
"""Test --groups / -g option parsing."""
cmd = sync.Sync()
opts, _ = cmd.OptionParser.parse_args(cli_args)
assert opts.groups == expected_groups
def _create_manifest_with_groups(topdir: Path) -> manifest_xml.XmlManifest:
"""Create a test XmlManifest with projects assigned to various groups."""
repodir = topdir / ".repo"
manifest_dir = repodir / "manifests"
manifest_file = repodir / manifest_xml.MANIFEST_FILE_NAME
repodir.mkdir(exist_ok=True)
manifest_dir.mkdir(exist_ok=True)
gitdir = repodir / "manifests.git"
gitdir.mkdir(exist_ok=True)
(gitdir / "config").write_text(
"""[remote "origin"]
url = https://localhost:0/manifest
""",
encoding="utf-8",
)
manifest_file.write_text(
"""
<manifest>
<remote name="origin" fetch="http://localhost" />
<default remote="origin" revision="refs/heads/main" />
<project name="proj_g1" path="path_g1" groups="group1" />
<project name="proj_g2" path="path_g2" groups="group2" />
<project name="proj_g1_g2" path="path_g1_g2"
groups="group1,group2" />
<project name="proj_default" path="path_default" />
<project name="proj_notdefault" path="path_notdefault"
groups="notdefault" />
</manifest>
""",
encoding="utf-8",
)
for p in [
"proj_g1",
"proj_g2",
"proj_g1_g2",
"proj_default",
"proj_notdefault",
]:
(repodir / "projects" / f"{p}.git").mkdir(parents=True, exist_ok=True)
return manifest_xml.XmlManifest(str(repodir), str(manifest_file))
@pytest.mark.parametrize(
"cli_args, expected_projects",
[
(["-g", "group1"], ["proj_g1", "proj_g1_g2"]),
(["-g", "group2"], ["proj_g2", "proj_g1_g2"]),
(["-g", "group1,group2"], ["proj_g1", "proj_g1_g2", "proj_g2"]),
(["-g", "default,-group1"], ["proj_default", "proj_g2"]),
([], ["proj_default", "proj_g1", "proj_g1_g2", "proj_g2"]),
],
)
def test_sync_groups_manifest_filtering(
tmp_path: Path, cli_args, expected_projects
):
"""Test that repo sync -g selects only matching projects."""
manifest = _create_manifest_with_groups(tmp_path)
cmd = sync.Sync()
cmd.manifest = manifest
opts, args = cmd.OptionParser.parse_args(cli_args)
projects = cmd.GetProjects(args, groups=opts.groups, missing_ok=True)
project_names = sorted([p.name for p in projects])
assert project_names == sorted(expected_projects)
def test_sync_update_projects_revision_id_respects_groups(tmp_path: Path):
"""Test that _UpdateProjectsRevisionId filters projects using opt.groups."""
manifest = _create_manifest_with_groups(tmp_path)
cmd = sync.Sync()
cmd.manifest = manifest
superproject = mock.MagicMock()
superproject.UpdateProjectsRevisionId.return_value = mock.MagicMock(
manifest_path=None
)
manifest._superproject = superproject
opts, args = cmd.OptionParser.parse_args(["-g", "group1"])
opts.verbose = False
opts.fetch_submodules = False
opts.this_manifest_only = True
opts.local_only = False
with mock.patch.object(
cmd, "GetProjects", wraps=cmd.GetProjects
) as spy_get_projects:
with mock.patch.object(cmd, "ManifestList", return_value=[manifest]):
cmd._UpdateProjectsRevisionId(opts, args, {}, manifest)
spy_get_projects.assert_called_once()
_, kwargs = spy_get_projects.call_args
assert kwargs.get("groups") == "group1"
# Used to patch os.cpu_count() for reliable results.
OS_CPU_COUNT = 24
@@ -832,6 +948,19 @@ class SyncCommand(unittest.TestCase):
self.assertIn(self.sync_local_half_error, e.aggregate_errors)
self.assertIn(self.sync_network_half_error, e.aggregate_errors)
def test_groups_passed_to_get_projects(self):
"""Ensure Execute passes opt.groups to GetProjects."""
self.opt.groups = "my_group"
self.opt.mp_update = False
with mock.patch.object(self.cmd, "_UpdateRepoProject"):
with mock.patch.object(self.cmd, "_ValidateOptionsWithManifest"):
with mock.patch.object(self.cmd, "_SyncInterleaved"):
with mock.patch.object(self.cmd, "_RunPostSyncHook"):
self.cmd.Execute(self.opt, [])
self.cmd.GetProjects.assert_called()
_, kwargs = self.cmd.GetProjects.call_args
self.assertEqual(kwargs.get("groups"), "my_group")
class SyncUpdateRepoProject(unittest.TestCase):
"""Tests for Sync._UpdateRepoProject."""