mirror of
https://gerrit.googlesource.com/git-repo
synced 2026-10-04 20:47:07 +00:00
`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>
350 lines
9.7 KiB
Python
350 lines
9.7 KiB
Python
# 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 command.py module."""
|
|
|
|
from typing import Iterable, List, Optional
|
|
|
|
import pytest
|
|
|
|
from command import Command
|
|
|
|
|
|
class FakeProject:
|
|
"""Minimal project double for Command.GetProjects tests."""
|
|
|
|
def __init__(
|
|
self,
|
|
name,
|
|
relpath,
|
|
*,
|
|
gitdir=None,
|
|
derived_subprojects=None,
|
|
sync_s=False,
|
|
exists: bool = True,
|
|
):
|
|
self.name = name
|
|
self.relpath = relpath
|
|
self.worktree = f"/work/{relpath}"
|
|
self.manifest = None
|
|
self.gitdir = gitdir or f"/git/{relpath}"
|
|
self.sync_s = sync_s
|
|
self.Exists = exists
|
|
self._derived_subprojects = derived_subprojects or []
|
|
|
|
def GetDerivedSubprojects(self):
|
|
return list(self._derived_subprojects)
|
|
|
|
def MatchesGroups(self, _groups):
|
|
return True
|
|
|
|
def RelPath(self, local=True):
|
|
return self.relpath
|
|
|
|
|
|
class FakeManifest:
|
|
"""Minimal manifest double for Command.GetProjects tests."""
|
|
|
|
def __init__(
|
|
self,
|
|
projects: Iterable[FakeProject],
|
|
*,
|
|
all_projects: Optional[Iterable[FakeProject]] = None,
|
|
effective_groups: str = "default",
|
|
):
|
|
self.projects = list(projects)
|
|
self.all_projects = (
|
|
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.
|
|
for project in self.projects:
|
|
self._set_project_manifest(project)
|
|
|
|
def _set_project_manifest(self, project: FakeProject) -> None:
|
|
project.manifest = self
|
|
for subproject in project.GetDerivedSubprojects():
|
|
self._set_project_manifest(subproject)
|
|
|
|
def GetManifestGroupsStr(self):
|
|
return self._effective_groups
|
|
|
|
def GetProjectsWithName(
|
|
self, name: str, all_manifests: bool = False
|
|
) -> List[FakeProject]:
|
|
projects = self.all_projects if all_manifests else self.projects
|
|
return [project for project in projects if project.name == name]
|
|
|
|
|
|
class GroupMatchingFakeProject(FakeProject):
|
|
"""Fake project with predictable group matches for GetProjects tests.
|
|
|
|
This lets the tests check which groups GetProjects uses without
|
|
reimplementing Project.MatchesGroups.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
name: str,
|
|
relpath: str,
|
|
*,
|
|
matching_groups: Iterable[str],
|
|
):
|
|
super().__init__(name, relpath)
|
|
self._matching_groups = set(matching_groups)
|
|
|
|
def MatchesGroups(self, groups: Iterable[str]) -> bool:
|
|
return bool(self._matching_groups.intersection(groups))
|
|
|
|
|
|
def test_get_projects_keeps_derived_subprojects_for_repeated_repo():
|
|
"""Derived subprojects are keyed by checkout path, not repo identity."""
|
|
submodule_a = FakeProject(
|
|
"submodule",
|
|
"src/one/submodule",
|
|
gitdir="/shared/modules/submodule.git",
|
|
)
|
|
submodule_b = FakeProject(
|
|
"submodule",
|
|
"src/two/submodule",
|
|
gitdir="/shared/modules/submodule.git",
|
|
)
|
|
project_a = FakeProject(
|
|
"project",
|
|
"src/one",
|
|
derived_subprojects=[submodule_a],
|
|
sync_s=True,
|
|
)
|
|
project_b = FakeProject(
|
|
"project",
|
|
"src/two",
|
|
derived_subprojects=[submodule_b],
|
|
sync_s=True,
|
|
)
|
|
manifest = FakeManifest([project_a, project_b])
|
|
cmd = Command(manifest=manifest)
|
|
|
|
projects = cmd.GetProjects([])
|
|
|
|
assert set(projects) == {project_a, project_b, submodule_a, submodule_b}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"submodules_ok, sync_s, includes_submodule",
|
|
[
|
|
(None, False, False),
|
|
(None, True, True),
|
|
(True, False, True),
|
|
(True, True, True),
|
|
(False, False, False),
|
|
(False, True, False),
|
|
],
|
|
)
|
|
def test_get_projects_submodule_override(
|
|
submodules_ok, sync_s, includes_submodule
|
|
):
|
|
"""The CLI override takes precedence over a project's sync-s setting."""
|
|
submodule = FakeProject("submodule", "project/submodule")
|
|
project = FakeProject(
|
|
"project",
|
|
"project",
|
|
derived_subprojects=[submodule],
|
|
sync_s=sync_s,
|
|
)
|
|
cmd = Command(manifest=FakeManifest([project]))
|
|
|
|
projects = cmd.GetProjects([], submodules_ok=submodules_ok)
|
|
|
|
assert (submodule in projects) is includes_submodule
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("groups", "expected_relpaths"),
|
|
[
|
|
(None, ["outer", "sub/child"]),
|
|
("", ["outer", "sub/child"]),
|
|
("override-group", ["sub/override"]),
|
|
],
|
|
ids=("groups-omitted", "groups-empty", "explicit-override"),
|
|
)
|
|
def test_get_projects_uses_groups_from_each_manifest_unless_overridden(
|
|
groups: Optional[str],
|
|
expected_relpaths: List[str],
|
|
) -> None:
|
|
"""Use each manifest's effective groups unless the caller overrides them."""
|
|
outer_project = GroupMatchingFakeProject(
|
|
"outer",
|
|
"outer",
|
|
matching_groups={"outer-group"},
|
|
)
|
|
|
|
# Both child projects also match "outer". Reusing the outer manifest's
|
|
# groups would therefore select both child projects.
|
|
child_project = GroupMatchingFakeProject(
|
|
"child",
|
|
"sub/child",
|
|
matching_groups={"outer-group", "child-group"},
|
|
)
|
|
override_project = GroupMatchingFakeProject(
|
|
"override",
|
|
"sub/override",
|
|
matching_groups={"outer-group", "override-group"},
|
|
)
|
|
|
|
child_manifest = FakeManifest(
|
|
[child_project, override_project],
|
|
effective_groups="child-group",
|
|
)
|
|
outer_manifest = FakeManifest(
|
|
[outer_project],
|
|
all_projects=[outer_project, *child_manifest.projects],
|
|
effective_groups="outer-group",
|
|
)
|
|
cmd = Command(manifest=outer_manifest)
|
|
|
|
projects = cmd.GetProjects(
|
|
[],
|
|
manifest=outer_manifest,
|
|
groups=groups,
|
|
all_manifests=True,
|
|
)
|
|
|
|
assert [project.relpath for project in projects] == expected_relpaths
|
|
|
|
|
|
def test_get_projects_by_name_uses_groups_from_each_manifest() -> None:
|
|
"""Name matches use the groups from each project's owning manifest."""
|
|
outer_project = GroupMatchingFakeProject(
|
|
"shared",
|
|
"outer/shared",
|
|
matching_groups={"outer-group"},
|
|
)
|
|
child_project = GroupMatchingFakeProject(
|
|
"shared",
|
|
"sub/shared",
|
|
matching_groups={"child-group"},
|
|
)
|
|
|
|
child_manifest = FakeManifest(
|
|
[child_project],
|
|
effective_groups="child-group",
|
|
)
|
|
outer_manifest = FakeManifest(
|
|
[outer_project],
|
|
all_projects=[outer_project, *child_manifest.projects],
|
|
effective_groups="outer-group",
|
|
)
|
|
cmd = Command(manifest=outer_manifest)
|
|
|
|
projects = cmd.GetProjects(
|
|
["shared"],
|
|
manifest=outer_manifest,
|
|
all_manifests=True,
|
|
)
|
|
|
|
assert [project.relpath for project in projects] == [
|
|
"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]
|