summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Chant <achant@google.com>2026-07-17 15:09:06 -0700
committergerrit-scoped@luci-project-accounts.iam.gserviceaccount.com <gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com>2026-07-21 13:42:38 -0700
commit0b82311632d14d3f49675fb6a017c20f7a7dee37 (patch)
tree265e41f241585ae11afd6f4b12dfa0e9a60ef726
parent06c4f9e1cca33a51b5396de2d1d975648e9f2ce4 (diff)
downloadgit-repo-main.tar.gz
git-repo-main.zip
sync: allow syncing groups with repo sync -g groupHEADmain
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>
-rw-r--r--man/repo-smartsync.13
-rw-r--r--man/repo-sync.13
-rw-r--r--subcmds/sync.py11
-rw-r--r--tests/test_subcmds_sync.py129
4 files changed, 146 insertions, 0 deletions
diff --git a/man/repo-smartsync.1 b/man/repo-smartsync.1
index e3fddbc72..f4566dc01 100644
--- a/man/repo-smartsync.1
+++ b/man/repo-smartsync.1
@@ -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
diff --git a/man/repo-sync.1 b/man/repo-sync.1
index 66cd37a9e..754c02927 100644
--- a/man/repo-sync.1
+++ b/man/repo-sync.1
@@ -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
diff --git a/subcmds/sync.py b/subcmds/sync.py
index 3fdb36afa..573c3444b 100644
--- a/subcmds/sync.py
+++ b/subcmds/sync.py
@@ -559,6 +559,13 @@ later is required to fix a server side protocol bug.
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",
help="enable use of /clone.bundle on HTTP/HTTPS",
@@ -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,
diff --git a/tests/test_subcmds_sync.py b/tests/test_subcmds_sync.py
index 36ba13cae..bc0a82759 100644
--- a/tests/test_subcmds_sync.py
+++ b/tests/test_subcmds_sync.py
@@ -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."""