Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion mergify_cli/ci/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,14 @@ def git_refs() -> None:
default=".mergify.yml",
help="Path to YAML config file.",
)
@click.option(
"--base",
help="The base git reference to use to look for changed files",
)
@click.option(
"--head",
help="The head git reference to use to look for changed files",
)
@click.option(
"--write",
"-w",
Expand All @@ -226,9 +234,17 @@ def git_refs() -> None:
def scopes(
config_path: str,
write: str | None = None,
head: str | None = None,
base: str | None = None,
) -> None:
ref = git_refs_detector.detect()
try:
scopes = scopes_cli.detect(config_path=config_path)
scopes = scopes_cli.detect(
config_path=config_path,
base=base or ref.base,
head=head or ref.head,
is_merge_queue=ref.is_merge_queue,
)
except scopes_exc.ScopesError as e:
raise click.ClickException(str(e)) from e

Expand Down
31 changes: 14 additions & 17 deletions mergify_cli/ci/scopes/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import pydantic

from mergify_cli import utils
from mergify_cli.ci.git_refs import detector as git_refs_detector
from mergify_cli.ci.scopes import changed_files
from mergify_cli.ci.scopes import config
from mergify_cli.ci.scopes import exceptions
Expand Down Expand Up @@ -100,8 +99,6 @@ class InvalidDetectedScopeError(exceptions.ScopesError):


class DetectedScope(pydantic.BaseModel):
base_ref: str
head_ref: str
scopes: set[str]

def save_to_file(self, file: str) -> None:
Expand All @@ -117,12 +114,17 @@ def load_from_file(cls, filename: str) -> DetectedScope:
raise InvalidDetectedScopeError(str(e))


def detect(config_path: str) -> DetectedScope:
def detect(
config_path: str,
*,
base: str,
head: str,
is_merge_queue: bool,
) -> DetectedScope:
cfg = config.Config.from_yaml(config_path)
git_refs = git_refs_detector.detect()

click.echo(f"Base: {git_refs.base}")
click.echo(f"Head: {git_refs.head}")
click.echo(f"Base: {base}")
click.echo(f"Head: {head}")

scopes_hit: set[str]
per_scope: dict[str, list[str]]
Expand All @@ -133,7 +135,7 @@ def detect(config_path: str) -> DetectedScope:
scopes_hit = set()
per_scope = {}
elif isinstance(source, config.SourceFiles):
changed = changed_files.git_changed_files(git_refs.base, git_refs.head)
changed = changed_files.git_changed_files(base, head)
click.echo("Changed files detected:")
for file in changed:
click.echo(f"- {file}")
Expand All @@ -148,7 +150,7 @@ def detect(config_path: str) -> DetectedScope:

if cfg.scopes.merge_queue_scope is not None:
all_scopes.add(cfg.scopes.merge_queue_scope)
if git_refs.is_merge_queue:
if is_merge_queue:
scopes_hit.add(cfg.scopes.merge_queue_scope)

if scopes_hit:
Expand All @@ -161,19 +163,14 @@ def detect(config_path: str) -> DetectedScope:
else:
click.echo("No scopes matched.")

git_refs.maybe_write_to_github_outputs()
maybe_write_github_outputs(all_scopes, scopes_hit)
maybe_write_github_step_summary(
git_refs.base,
git_refs.head,
base,
head,
all_scopes,
scopes_hit,
)
return DetectedScope(
base_ref=git_refs.base,
head_ref=git_refs.head,
scopes=scopes_hit,
)
return DetectedScope(scopes=scopes_hit)


async def send_scopes(
Expand Down
69 changes: 24 additions & 45 deletions mergify_cli/tests/ci/scopes/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import respx
import yaml

from mergify_cli.ci.git_refs import detector
from mergify_cli.ci.scopes import cli
from mergify_cli.ci.scopes import config
from mergify_cli.ci.scopes import exceptions
Expand Down Expand Up @@ -337,13 +336,11 @@ def test_maybe_write_github_outputs_no_env(
cli.maybe_write_github_outputs(["backend"], {"backend"})


@mock.patch("mergify_cli.ci.git_refs.detector.detect")
@mock.patch("mergify_cli.ci.scopes.changed_files.git_changed_files")
@mock.patch("mergify_cli.ci.scopes.cli.maybe_write_github_outputs")
def test_detect_with_matches(
mock_github_outputs: mock.Mock,
mock_git_changed: mock.Mock,
mock_detect_base: mock.Mock,
tmp_path: pathlib.Path,
) -> None:
# Setup config file
Expand All @@ -361,19 +358,18 @@ def test_detect_with_matches(
config_file.write_text(yaml.dump(config_data))

# Setup mocks
mock_detect_base.return_value = detector.References(
"old",
"new",
is_merge_queue=True,
)
mock_git_changed.return_value = ["api/models.py", "other.txt"]

# Capture output
with mock.patch("click.echo") as mock_echo:
result = cli.detect(str(config_file))
result = cli.detect(
str(config_file),
base="old",
head="new",
is_merge_queue=True,
)

# Verify calls
mock_detect_base.assert_called_once()
mock_git_changed.assert_called_once_with("old", "new")
mock_github_outputs.assert_called_once()

Expand All @@ -385,16 +381,12 @@ def test_detect_with_matches(
assert "- backend" in calls
assert "- merge-queue" in calls

assert result.base_ref == "old"
assert result.head_ref == "new"
assert result.scopes == {"backend", "merge-queue"}


@mock.patch("mergify_cli.ci.git_refs.detector.detect")
@mock.patch("mergify_cli.ci.scopes.cli.maybe_write_github_outputs")
def test_detect_manual(
_: mock.Mock,
mock_detect_base: mock.Mock,
tmp_path: pathlib.Path,
) -> None:
# Setup config file
Expand All @@ -404,28 +396,24 @@ def test_detect_manual(
config_file = tmp_path / ".mergify-ci.yml"
config_file.write_text(yaml.dump(config_data))

# Setup mocks
mock_detect_base.return_value = detector.References(
"old",
"new",
is_merge_queue=False,
)

# Capture output
with pytest.raises(
exceptions.ScopesError,
match="source `manual` has been set, scopes must be sent with `scopes-send` or API",
):
cli.detect(str(config_file))
cli.detect(
str(config_file),
base="old",
head="new",
is_merge_queue=False,
)


@mock.patch("mergify_cli.ci.git_refs.detector.detect")
@mock.patch("mergify_cli.ci.scopes.changed_files.git_changed_files")
@mock.patch("mergify_cli.ci.scopes.cli.maybe_write_github_outputs")
def test_detect_no_matches(
_: mock.Mock,
mock_git_changed: mock.Mock,
mock_detect_base: mock.Mock,
tmp_path: pathlib.Path,
) -> None:
# Setup config file
Expand All @@ -436,32 +424,28 @@ def test_detect_no_matches(
config_file.write_text(yaml.dump(config_data))

# Setup mocks
mock_detect_base.return_value = detector.References(
"old",
"new",
is_merge_queue=False,
)
mock_git_changed.return_value = ["other.txt"]

# Capture output
with mock.patch("click.echo") as mock_echo:
result = cli.detect(str(config_file))
result = cli.detect(
str(config_file),
base="old",
head="new",
is_merge_queue=False,
)

# Verify output
calls = [call.args[0] for call in mock_echo.call_args_list]
assert "Base: old" in calls
assert "Head: new" in calls
assert "No scopes matched." in calls
assert result.scopes == set()
assert result.base_ref == "old"
assert result.head_ref == "new"


@mock.patch("mergify_cli.ci.git_refs.detector.detect")
@mock.patch("mergify_cli.ci.scopes.changed_files.git_changed_files")
def test_detect_debug_output(
mock_git_changed: mock.Mock,
mock_detect_base: mock.Mock,
tmp_path: pathlib.Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
Expand All @@ -476,23 +460,21 @@ def test_detect_debug_output(
config_file.write_text(yaml.dump(config_data))

# Setup mocks
mock_detect_base.return_value = detector.References(
"main",
"HEAD",
is_merge_queue=False,
)
mock_git_changed.return_value = ["api/models.py", "api/views.py"]

# Capture output
with mock.patch("click.echo") as mock_echo:
result = cli.detect(str(config_file))

result = cli.detect(
str(config_file),
base="old",
head="new",
is_merge_queue=False,
)
# Verify debug output includes file details
calls = [call.args[0] for call in mock_echo.call_args_list]
assert any(" api/models.py" in call for call in calls)
assert any(" api/views.py" in call for call in calls)

assert result.base_ref == "main"
assert result.scopes == {"backend"}


Expand Down Expand Up @@ -535,12 +517,9 @@ async def test_upload_scopes(respx_mock: respx.MockRouter) -> None:
def test_dump(tmp_path: pathlib.Path) -> None:
config_file = tmp_path / "scopes.json"
saved = cli.DetectedScope(
base_ref="base",
head_ref="head",
scopes={"backend", "merge-queue"},
)
saved.save_to_file(str(config_file))

loaded = cli.DetectedScope.load_from_file(str(config_file))
assert loaded.scopes == saved.scopes
assert loaded.base_ref == saved.base_ref