From 45ff546229075dd346e470e5e17da4e769ce123d Mon Sep 17 00:00:00 2001 From: JulianMaurin Date: Fri, 10 Apr 2026 14:15:28 +0200 Subject: [PATCH] fix(stack): normalize --branch by stripping prefix and change ID suffix Allow `stack checkout --branch` to accept a full head ref name (e.g. `devs/user/MRGFY-6797/Ibb431d...`) by automatically stripping the branch prefix and change ID suffix before building the search query. Co-Authored-By: Claude Opus 4.6 (1M context) Change-Id: Ibd331d1dd37808970e75cbcda26f36ff64a51b32 --- mergify_cli/stack/changes.py | 4 +- mergify_cli/stack/checkout.py | 7 +++ mergify_cli/stack/push.py | 3 +- mergify_cli/tests/stack/test_checkout.py | 71 ++++++++++++++++++++++++ 4 files changed, 82 insertions(+), 3 deletions(-) diff --git a/mergify_cli/stack/changes.py b/mergify_cli/stack/changes.py index 884d3219..c3979528 100644 --- a/mergify_cli/stack/changes.py +++ b/mergify_cli/stack/changes.py @@ -29,7 +29,9 @@ import httpx -CHANGEID_RE = re.compile(r"Change-Id: (I[0-9a-z]{40})") +_CHANGEID_PATTERN = r"I[0-9a-z]{40}" +CHANGEID_RE = re.compile(rf"Change-Id: ({_CHANGEID_PATTERN})") +CHANGEID_SUFFIX_RE = re.compile(rf"/{_CHANGEID_PATTERN}$") def is_change_id_prefix(prefix: str) -> bool: diff --git a/mergify_cli/stack/checkout.py b/mergify_cli/stack/checkout.py index 913e5ccc..18afae5d 100644 --- a/mergify_cli/stack/checkout.py +++ b/mergify_cli/stack/checkout.py @@ -39,6 +39,13 @@ async def stack_checkout( if branch_prefix is None: branch_prefix = await utils.get_default_branch_prefix(author) + # Strip change ID suffix if present (e.g. /Ibb431d523fb75f48f387a3964d2936ada933cffe) + branch = changes.CHANGEID_SUFFIX_RE.sub("", branch) + + # Strip branch prefix from branch if already included + if branch_prefix and branch.startswith(f"{branch_prefix}/"): + branch = branch.removeprefix(f"{branch_prefix}/") + stack_branch = f"{branch_prefix}/{branch}" if branch_prefix else branch async with utils.get_github_http_client(github_server, token) as client: diff --git a/mergify_cli/stack/push.py b/mergify_cli/stack/push.py index a79228f2..dec8d5ac 100644 --- a/mergify_cli/stack/push.py +++ b/mergify_cli/stack/push.py @@ -43,8 +43,7 @@ class LocalBranchInvalidError(Exception): def check_local_branch(branch_name: str, branch_prefix: str) -> None: - if branch_name.startswith(branch_prefix) and re.search( - r"I[0-9a-z]{40}$", + if branch_name.startswith(branch_prefix) and changes.CHANGEID_SUFFIX_RE.search( branch_name, ): msg = "Local branch is a branch generated by Mergify CLI" diff --git a/mergify_cli/tests/stack/test_checkout.py b/mergify_cli/tests/stack/test_checkout.py index b65ae430..8c094d20 100644 --- a/mergify_cli/tests/stack/test_checkout.py +++ b/mergify_cli/tests/stack/test_checkout.py @@ -156,3 +156,74 @@ async def test_stack_checkout_repository_explicit( "--get", "remote.origin.url", ) + + +@pytest.mark.respx(base_url="https://api.github.com/") +@pytest.mark.parametrize( + ("branch_input", "expected_branch"), + [ + # Full head ref with prefix and change ID suffix → stripped to plain branch + ( + "devs/JulianMaurin/MRGFY-6797/Ibb431d523fb75f48f387a3964d2936ada933cffe", + "MRGFY-6797", + ), + # Change ID suffix only → stripped + ( + "MRGFY-6797/Ibb431d523fb75f48f387a3964d2936ada933cffe", + "MRGFY-6797", + ), + # Prefix only → stripped + ( + "devs/JulianMaurin/MRGFY-6797", + "MRGFY-6797", + ), + # Already plain → unchanged + ( + "MRGFY-6797", + "MRGFY-6797", + ), + ], + ids=[ + "full-head-ref", + "with-suffix-no-prefix", + "with-prefix-no-suffix", + "plain-branch", + ], +) +async def test_stack_checkout_branch_normalization( + git_mock: test_utils.GitMock, + respx_mock: respx.MockRouter, + branch_input: str, + expected_branch: str, +) -> None: + """Test that checkout normalizes --branch by stripping prefix and change ID suffix.""" + git_mock.mock( + "config", + "--get", + "mergify-cli.stack-branch-prefix", + output="devs/JulianMaurin", + ) + + search_mock = respx_mock.get("/search/issues").respond( + 200, + json={"items": []}, + ) + + with pytest.raises(SystemExit, match="0"): + await stack_checkout_mod.stack_checkout( + github_server="https://api.github.com/", + token="", + user="user", + repo="repo", + branch_prefix=None, + branch=branch_input, + author="author", + trunk=("origin", "main"), + dry_run=True, + ) + + # Verify the search query: stack_branch = prefix/normalized_branch + expected_stack_branch = f"devs/JulianMaurin/{expected_branch}" + assert len(search_mock.calls) == 1 + query = str(search_mock.calls[0].request.url) + assert f"head%3A{expected_stack_branch.replace('/', '%2F')}%2F" in query