mirror of
https://gerrit.googlesource.com/git-repo
synced 2026-10-21 12:48:28 +00:00
command: preserve project filters in regex selection
`FindProjects` gets its candidate projects from `GetProjects` and then applies regex matching. However, it only forwards `all_manifests`, so other project filters are lost on regex paths. As a result, `repo forall -r/-i --groups` falls back to the configured group selection instead of using the requested groups. Similarly, `repo list -r --groups` ignores the requested groups, and `repo list -r --all` still excludes projects without a checkout. Pass `groups` and `missing_ok` through `FindProjects`, and forward the existing options from `forall` and `list`. When no group override is given, keep using each manifest's effective groups. Add regression tests for `FindProjects` and both command paths. Change-Id: I5dfc22172fdd81de5312a84433867300a1342aec Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/631281 Reviewed-by: Mike Frysinger <vapier@google.com> Reviewed-by: Gavin Mak <gavinmak@google.com> Commit-Queue: Victor Pushkarev <corvinus.v@gmail.com> Tested-by: Victor Pushkarev <corvinus.v@gmail.com>
This commit is contained in:
1 parent
fb4a91060e
commit
c638b54e19
6 files changed
+230
-6
No files matched your search
+17
-2
@@ -545,7 +545,14 @@ class Command:
|
||||
result.sort(key=_getpath)
|
||||
return result
|
||||
|
||||
def FindProjects(self, args, inverse=False, all_manifests=False):
|
||||
def FindProjects(
|
||||
self,
|
||||
args: List[str],
|
||||
inverse: bool = False,
|
||||
all_manifests: bool = False,
|
||||
groups: Optional[str] = "",
|
||||
missing_ok: Optional[bool] = False,
|
||||
) -> List["Project"]:
|
||||
"""Find projects from command line arguments.
|
||||
|
||||
Args:
|
||||
@@ -555,10 +562,18 @@ class Command:
|
||||
all_manifests: a boolean, if True then all manifests and
|
||||
submanifests are used. If False, then only the local
|
||||
(sub)manifest is used.
|
||||
groups: a string specifying manifest groups. If empty or None, use
|
||||
each manifest's effective groups.
|
||||
missing_ok: a boolean, whether to allow missing projects.
|
||||
"""
|
||||
result = []
|
||||
patterns = [re.compile(r"%s" % a, re.IGNORECASE) for a in args]
|
||||
for project in self.GetProjects("", all_manifests=all_manifests):
|
||||
for project in self.GetProjects(
|
||||
"",
|
||||
groups=groups,
|
||||
missing_ok=missing_ok,
|
||||
all_manifests=all_manifests,
|
||||
):
|
||||
paths = [project.name, project.RelPath(local=not all_manifests)]
|
||||
for pattern in patterns:
|
||||
match = any(pattern.search(x) for x in paths)
|
||||
|
||||
+6
-2
@@ -244,10 +244,14 @@ without iterating through the remaining projects.
|
||||
mirror = self.manifest.IsMirror
|
||||
|
||||
if opt.regex:
|
||||
projects = self.FindProjects(args, all_manifests=all_trees)
|
||||
projects = self.FindProjects(
|
||||
args,
|
||||
groups=opt.groups,
|
||||
all_manifests=all_trees,
|
||||
)
|
||||
elif opt.inverse_regex:
|
||||
projects = self.FindProjects(
|
||||
args, inverse=True, all_manifests=all_trees
|
||||
args, inverse=True, groups=opt.groups, all_manifests=all_trees
|
||||
)
|
||||
else:
|
||||
projects = self.GetProjects(
|
||||
|
||||
+4
-1
@@ -109,7 +109,10 @@ This is similar to running: repo forall -c 'echo "$REPO_PATH : $REPO_PROJECT"'.
|
||||
)
|
||||
else:
|
||||
projects = self.FindProjects(
|
||||
args, all_manifests=not opt.this_manifest_only
|
||||
args,
|
||||
groups=opt.groups,
|
||||
missing_ok=opt.all,
|
||||
all_manifests=not opt.this_manifest_only,
|
||||
)
|
||||
|
||||
def _getpath(x):
|
||||
|
||||
+90
-1
@@ -32,6 +32,7 @@ class FakeProject:
|
||||
gitdir=None,
|
||||
derived_subprojects=None,
|
||||
sync_s=False,
|
||||
exists: bool = True,
|
||||
):
|
||||
self.name = name
|
||||
self.relpath = relpath
|
||||
@@ -39,7 +40,7 @@ class FakeProject:
|
||||
self.manifest = None
|
||||
self.gitdir = gitdir or f"/git/{relpath}"
|
||||
self.sync_s = sync_s
|
||||
self.Exists = True
|
||||
self.Exists = exists
|
||||
self._derived_subprojects = derived_subprojects or []
|
||||
|
||||
def GetDerivedSubprojects(self):
|
||||
@@ -67,6 +68,7 @@ class FakeManifest:
|
||||
list(self.projects) if all_projects is None else list(all_projects)
|
||||
)
|
||||
self._effective_groups = effective_groups
|
||||
self.path_prefix = ""
|
||||
|
||||
# all_projects may include projects owned by child manifests,
|
||||
# so only set this manifest on its direct projects.
|
||||
@@ -258,3 +260,90 @@ def test_get_projects_by_name_uses_groups_from_each_manifest() -> None:
|
||||
"outer/shared",
|
||||
"sub/shared",
|
||||
]
|
||||
|
||||
|
||||
def test_find_projects_uses_groups_from_each_manifest() -> None:
|
||||
"""Use each manifest's effective groups for regex selection."""
|
||||
outer_project = GroupMatchingFakeProject(
|
||||
"match-outer",
|
||||
"outer",
|
||||
matching_groups={"outer-group"},
|
||||
)
|
||||
child_project = GroupMatchingFakeProject(
|
||||
"match-child",
|
||||
"sub/child",
|
||||
matching_groups={"child-group"},
|
||||
)
|
||||
excluded_project = GroupMatchingFakeProject(
|
||||
"match-excluded",
|
||||
"sub/excluded",
|
||||
matching_groups={"other-group"},
|
||||
)
|
||||
|
||||
child_manifest = FakeManifest(
|
||||
[child_project, excluded_project],
|
||||
effective_groups="child-group",
|
||||
)
|
||||
child_manifest.path_prefix = "sub"
|
||||
|
||||
outer_manifest = FakeManifest(
|
||||
[outer_project],
|
||||
all_projects=[
|
||||
outer_project,
|
||||
*child_manifest.projects,
|
||||
],
|
||||
effective_groups="outer-group",
|
||||
)
|
||||
outer_manifest.outer_client = outer_manifest
|
||||
|
||||
cmd = Command(manifest=outer_manifest)
|
||||
|
||||
projects = cmd.FindProjects(["match"], all_manifests=True)
|
||||
|
||||
assert [project.relpath for project in projects] == [
|
||||
"outer",
|
||||
"sub/child",
|
||||
]
|
||||
|
||||
|
||||
def test_find_projects_uses_explicit_groups() -> None:
|
||||
"""Use explicit groups for regex selection."""
|
||||
default_project = GroupMatchingFakeProject(
|
||||
"default",
|
||||
"default",
|
||||
matching_groups={"default-group"},
|
||||
)
|
||||
override_project = GroupMatchingFakeProject(
|
||||
"override",
|
||||
"override",
|
||||
matching_groups={"override-group"},
|
||||
)
|
||||
manifest = FakeManifest(
|
||||
[default_project, override_project],
|
||||
effective_groups="default-group",
|
||||
)
|
||||
cmd = Command(manifest=manifest)
|
||||
|
||||
projects = cmd.FindProjects(
|
||||
["override"],
|
||||
groups="override-group",
|
||||
)
|
||||
|
||||
assert projects == [override_project]
|
||||
|
||||
|
||||
def test_find_projects_allows_missing_projects() -> None:
|
||||
"""Allow regex selection to include projects without a checkout."""
|
||||
project = FakeProject(
|
||||
"missing",
|
||||
"missing",
|
||||
exists=False,
|
||||
)
|
||||
cmd = Command(manifest=FakeManifest([project]))
|
||||
|
||||
projects = cmd.FindProjects(
|
||||
["missing"],
|
||||
missing_ok=True,
|
||||
)
|
||||
|
||||
assert projects == [project]
|
||||
@@ -17,7 +17,9 @@
|
||||
import contextlib
|
||||
import io
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
import utils_for_test
|
||||
|
||||
import manifest_xml
|
||||
@@ -105,3 +107,57 @@ def test_forall_all_projects_called_once(tmp_path: Path) -> None:
|
||||
line_count = sum(1 for x in output.splitlines() if x)
|
||||
# Verify that we didn't get more lines than expected.
|
||||
assert line_count == 8
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("regex_option", "inverse"),
|
||||
[
|
||||
("-r", False),
|
||||
("-i", True),
|
||||
],
|
||||
ids=("regex", "inverse-regex"),
|
||||
)
|
||||
def test_forall_regex_modes_pass_groups_to_find_projects(
|
||||
tmp_path: Path,
|
||||
regex_option: str,
|
||||
inverse: bool,
|
||||
) -> None:
|
||||
"""Pass --groups through in regex modes."""
|
||||
manifest = _create_manifest_with_8_projects(tmp_path)
|
||||
|
||||
cmd = subcmds.forall.Forall()
|
||||
cmd.manifest = manifest
|
||||
|
||||
opts, args = cmd.OptionParser.parse_args(
|
||||
[
|
||||
regex_option,
|
||||
"--groups",
|
||||
"special",
|
||||
"project",
|
||||
"-c",
|
||||
"true",
|
||||
]
|
||||
)
|
||||
|
||||
with mock.patch.object(
|
||||
cmd,
|
||||
"FindProjects",
|
||||
return_value=[],
|
||||
) as find_projects, mock.patch.object(
|
||||
cmd,
|
||||
"ExecuteInParallel",
|
||||
return_value=0,
|
||||
):
|
||||
cmd.Execute(opts, args)
|
||||
|
||||
expected_kwargs = {
|
||||
"groups": "special",
|
||||
"all_manifests": True,
|
||||
}
|
||||
if inverse:
|
||||
expected_kwargs["inverse"] = True
|
||||
|
||||
find_projects.assert_called_once_with(
|
||||
["project"],
|
||||
**expected_kwargs,
|
||||
)
|
||||
@@ -0,0 +1,57 @@
|
||||
# Copyright (C) 2026 The Android Open Source Project
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Unittests for the list subcmd."""
|
||||
|
||||
from typing import List, Optional
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
import subcmds
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("extra_args", "expected_groups", "expected_missing_ok"),
|
||||
[
|
||||
(["--groups", "special"], "special", None),
|
||||
(["--all"], None, True),
|
||||
],
|
||||
ids=("groups", "all"),
|
||||
)
|
||||
def test_list_regex_passes_groups_and_all(
|
||||
extra_args: List[str],
|
||||
expected_groups: Optional[str],
|
||||
expected_missing_ok: Optional[bool],
|
||||
) -> None:
|
||||
"""Pass --groups and --all through in regex mode."""
|
||||
cmd = subcmds.list.List()
|
||||
|
||||
opts, args = cmd.OptionParser.parse_args(
|
||||
["--regex", *extra_args, "project"]
|
||||
)
|
||||
|
||||
with mock.patch.object(
|
||||
cmd,
|
||||
"FindProjects",
|
||||
return_value=[],
|
||||
) as find_projects:
|
||||
cmd.Execute(opts, args)
|
||||
|
||||
find_projects.assert_called_once_with(
|
||||
["project"],
|
||||
groups=expected_groups,
|
||||
missing_ok=expected_missing_ok,
|
||||
all_manifests=True,
|
||||
)
|
||||
Reference in new issue
Block a user