Skip to content

Commit

Permalink
feat: add helper to interactively rebase the stack
Browse files Browse the repository at this point in the history
This adds `mergify stack edit` to interactively rebase the stack.

Add `mergify stack` becomes `mergify stack push`

`mergify stack push` being the default when `mergify` or `mergify stack`
is used.

Change-Id: Ic488198667591d50d117b55e3d4dc2a22bd13b8d
  • Loading branch information
sileht committed Sep 24, 2024
1 parent b5f827b commit 7135ac4
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 48 deletions.
137 changes: 96 additions & 41 deletions mergify_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.

from __future__ import annotations

import argparse
import asyncio
Expand Down Expand Up @@ -109,7 +110,7 @@ def get_slug(url: str) -> tuple[str, str]:
return user, repo


async def do_setup() -> None:
async def stack_setup(_: argparse.Namespace) -> None:
hooks_dir = pathlib.Path(await git("rev-parse", "--git-path", "hooks"))
installed_hook_file = hooks_dir / "commit-msg"

Expand Down Expand Up @@ -498,9 +499,16 @@ def check_local_branch(branch_name: str, branch_prefix: str) -> None:
raise LocalBranchInvalidError(msg)


async def stack_edit(_: argparse.Namespace) -> None:
os.chdir(await git("rev-parse", "--show-toplevel"))
trunk = await get_trunk()
base = await git("merge-base", trunk, "HEAD")
os.execvp("git", ("git", "rebase", "-i", f"{base}^")) # noqa: S606


# TODO(charly): fix code to conform to linter (number of arguments, local
# variables, statements, positional arguments, branches)
async def stack( # noqa: PLR0913, PLR0914, PLR0915, PLR0917, PLR0912
async def stack_push( # noqa: PLR0913, PLR0914, PLR0915, PLR0917, PLR0912
github_server: str,
token: str,
skip_rebase: bool,
Expand All @@ -512,7 +520,7 @@ async def stack( # noqa: PLR0913, PLR0914, PLR0915, PLR0917, PLR0912
keep_pull_request_title_and_body: bool = False,
) -> None:
os.chdir(await git("rev-parse", "--show-toplevel"))
dest_branch = await git("rev-parse", "--abbrev-ref", "HEAD")
dest_branch = await git_get_branch_name()

try:
check_local_branch(branch_name=dest_branch, branch_prefix=branch_prefix)
Expand Down Expand Up @@ -747,12 +755,13 @@ async def get_default_token() -> str:
return token


async def stack_main(args: argparse.Namespace) -> None:
async def _stack_push(args: argparse.Namespace) -> None:
if args.setup:
await do_setup()
# backward compat
await stack_setup(args)
return

await stack(
await stack_push(
args.github_server,
args.token,
args.skip_rebase,
Expand All @@ -765,90 +774,136 @@ async def stack_main(args: argparse.Namespace) -> None:
)


async def parse_args(args: typing.MutableSequence[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument(
"--version",
"-V",
action="version",
version=f"%(prog)s {VERSION}",
help="display version",
)
parser.add_argument("--debug", action="store_true", help="debug mode")
parser.add_argument(
"--token",
default=await get_default_token(),
type=GitHubToken,
help="GitHub personal access token",
def register_stack_setup_parser(
sub_parsers: argparse._SubParsersAction[typing.Any],
) -> None:
parser = sub_parsers.add_parser(
"setup",
description="Configure the git hooks",
help="Initial installation of the required git commit-msg hook",
)
parser.add_argument("--dry-run", "-n", action="store_true")
parser.add_argument(
"--github-server",
action="store_true",
default=await get_default_github_server(),
parser.set_defaults(func=stack_setup)


def register_stack_edit_parser(
sub_parsers: argparse._SubParsersAction[typing.Any],
) -> None:
parser = sub_parsers.add_parser(
"edit",
description="Edit the stack history",
help="Edit the stack history",
)
sub_parsers = parser.add_subparsers(dest="action")
parser.set_defaults(func=stack_edit)

stack_parser = sub_parsers.add_parser(
"stack",
description="Stacked Pull Requests CLI",
help="Create a pull requests stack",

async def register_stack_push_parser(
sub_parsers: argparse._SubParsersAction[typing.Any],
) -> None:
parser = sub_parsers.add_parser(
"push",
description="Push/sync the pull requests stack",
help="Push/sync the pull requests stack",
)
stack_parser.set_defaults(func=stack_main)
stack_parser.add_argument(
parser.set_defaults(func=_stack_push)

# Backward compat
parser.add_argument(
"--setup",
action="store_true",
help="Initial installation of the required git commit-msg hook",
)
stack_parser.add_argument(

parser.add_argument(
"--dry-run",
"-n",
action="store_true",
help="Only show what is going to be done",
)
stack_parser.add_argument(
parser.add_argument(
"--next-only",
"-x",
action="store_true",
help="Only rebase and update the next pull request of the stack",
)
stack_parser.add_argument(
parser.add_argument(
"--skip-rebase",
"-R",
action="store_true",
help="Skip stack rebase",
)
stack_parser.add_argument(
parser.add_argument(
"--draft",
"-d",
action="store_true",
help="Create stacked pull request as draft",
)
stack_parser.add_argument(
parser.add_argument(
"--keep-pull-request-title-and-body",
"-k",
action="store_true",
default=await get_default_keep_pr_title_body(),
help="Don't update the title and body of already opened pull requests. "
"Default fetched from git config if added with `git config --add mergify-cli.stack-keep-pr-title-body true`",
)
stack_parser.add_argument(
parser.add_argument(
"--trunk",
"-t",
type=trunk_type,
default=await get_trunk(),
help="Change the target branch of the stack.",
)
stack_parser.add_argument(
parser.add_argument(
"--branch-prefix",
default=await get_default_branch_prefix(),
help="Branch prefix used to create stacked PR. "
"Default fetched from git config if added with `git config --add mergify-cli.stack-branch-prefix some-prefix`",
)


async def parse_args(args: typing.MutableSequence[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument(
"--version",
"-V",
action="version",
version=f"%(prog)s {VERSION}",
help="display version",
)
parser.add_argument("--debug", action="store_true", help="debug mode")
parser.add_argument(
"--token",
default=await get_default_token(),
type=GitHubToken,
help="GitHub personal access token",
)
parser.add_argument("--dry-run", "-n", action="store_true")
parser.add_argument(
"--github-server",
action="store_true",
default=await get_default_github_server(),
)
sub_parsers = parser.add_subparsers(dest="action")

stack_parser = sub_parsers.add_parser(
"stack",
description="Stacked Pull Requests CLI",
help="Create a pull requests stack",
)
stack_sub_parsers = stack_parser.add_subparsers(dest="stack_action")
await register_stack_push_parser(stack_sub_parsers)
register_stack_edit_parser(stack_sub_parsers)
register_stack_setup_parser(stack_sub_parsers)

known_args, _ = parser.parse_known_args(args)

# Default
if known_args.action is None:
args.insert(1, "stack")
args.insert(0, "stack")

known_args, _ = parser.parse_known_args(args)

if known_args.action == "stack" and known_args.stack_action is None:
args.insert(1, "push")

return parser.parse_args(args)

Expand Down
49 changes: 42 additions & 7 deletions mergify_cli/tests/test_mergify_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@
from mergify_cli.tests import utils as test_utils


@pytest.fixture(autouse=True)
def _unset_github_token(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("GITHUB_TOKEN", "whatever")


@pytest.fixture(autouse=True)
def _change_working_directory(
monkeypatch: pytest.MonkeyPatch,
Expand Down Expand Up @@ -201,7 +208,7 @@ async def test_stack_create(
200,
)

await mergify_cli.stack(
await mergify_cli.stack_push(
github_server="https://api.github.com/",
token="",
skip_rebase=False,
Expand Down Expand Up @@ -289,7 +296,7 @@ async def test_stack_create_single_pull(
)
respx_mock.get("/repos/user/repo/issues/1/comments").respond(200, json=[])

await mergify_cli.stack(
await mergify_cli.stack_push(
github_server="https://api.github.com/",
token="",
skip_rebase=False,
Expand Down Expand Up @@ -367,7 +374,7 @@ async def test_stack_update_no_rebase(
)
respx_mock.patch("/repos/user/repo/issues/comments/456").respond(200)

await mergify_cli.stack(
await mergify_cli.stack_push(
github_server="https://api.github.com/",
token="",
skip_rebase=True,
Expand Down Expand Up @@ -445,7 +452,7 @@ async def test_stack_update(
)
respx_mock.patch("/repos/user/repo/issues/comments/456").respond(200)

await mergify_cli.stack(
await mergify_cli.stack_push(
github_server="https://api.github.com/",
token="",
skip_rebase=False,
Expand Down Expand Up @@ -523,7 +530,7 @@ async def test_stack_update_keep_title_and_body(
)
respx_mock.patch("/repos/user/repo/issues/comments/456").respond(200)

await mergify_cli.stack(
await mergify_cli.stack_push(
github_server="https://api.github.com/",
token="",
skip_rebase=False,
Expand All @@ -550,7 +557,7 @@ async def test_stack_on_destination_branch_raises_an_error(
git_mock.mock("rev-parse", "--abbrev-ref", "HEAD", output="main")

with pytest.raises(SystemExit, match="1"):
await mergify_cli.stack(
await mergify_cli.stack_push(
github_server="https://api.github.com/",
token="",
skip_rebase=False,
Expand All @@ -568,7 +575,7 @@ async def test_stack_without_common_commit_raises_an_error(
git_mock.mock("merge-base", "--fork-point", "origin/main", output="")

with pytest.raises(SystemExit, match="1"):
await mergify_cli.stack(
await mergify_cli.stack_push(
github_server="https://api.github.com/",
token="",
skip_rebase=False,
Expand Down Expand Up @@ -596,3 +603,31 @@ async def test_defaults_config_args_set(
) -> None:
with mock.patch.object(mergify_cli, "_run_command", return_value=config_get_result):
assert (await default_arg_fct()) == expected_default


@pytest.mark.parametrize(
"args",
[
["-R"],
["stack", "-R"],
["stack", "push", "-R"],
],
)
async def test_default(
git_mock: test_utils.GitMock,
args: list[str],
) -> None:
git_mock.default_cli_args()
parsed = await mergify_cli.parse_args(args)
assert parsed.action == "stack"
assert parsed.stack_action == "push"
assert parsed.skip_rebase


async def test_parse_edit(
git_mock: test_utils.GitMock,
) -> None:
git_mock.default_cli_args()
parsed = await mergify_cli.parse_args(["stack", "edit"])
assert parsed.action == "stack"
assert parsed.stack_action == "edit"
8 changes: 8 additions & 0 deletions mergify_cli/tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ async def __call__(self, *args: str) -> str:
msg = f"git_mock called with `{args}`, not mocked!"
raise AssertionError(msg)

def default_cli_args(self) -> None:
self.mock("config", "--get", "mergify-cli.github-server", output="")
self.mock("config", "--get", "mergify-cli.stack-keep-pr-title-body", output="")
self.mock("config", "--get", "branch.current-branch.merge", output="")
self.mock("config", "--get", "branch.current-branch.remote", output="")
self.mock("config", "--get", "mergify-cli.stack-branch-prefix", output="")
self.mock("merge-base", "--fork-point", "origin/main", output="")

def commit(self, commit: Commit) -> None:
self._commits.append(commit)

Expand Down

0 comments on commit 7135ac4

Please sign in to comment.