diff --git a/.ci/mypy.sh b/.ci/mypy.sh index 792529f3af748be96dc1d0feb548cfa11e5f533f..dc01fac92bfd2351b55380ee10cb63914c04ee3f 100755 --- a/.ci/mypy.sh +++ b/.ci/mypy.sh @@ -4,7 +4,7 @@ if [ "$(id -u)" = 0 ]; then set -x - apk -q add py3-argcomplete py3-mypy + apk -q add py3-argcomplete py3-gitlab py3-mypy exec su "${TESTUSER:-build}" -c "sh -e $0" fi diff --git a/mrhlpr/frontend.py b/mrhlpr/frontend.py index c3b47fd84e869ce6b5914ee72b4aab70d5237d64..20b7d488ea186baf5573aa1984a1453e9dcdd38f 100644 --- a/mrhlpr/frontend.py +++ b/mrhlpr/frontend.py @@ -170,19 +170,8 @@ def print_status(mr_id: Optional[int], no_cache: bool = False) -> None: if commits_follow_format is None: print("* Manually check if the commit subjects are correct") - origin = gitlab.parse_git_origin() - - remote_local = status.source.split("/", 1)[0] - if remote_local == origin.project: - remote_local = "origin" - print("* Pretty 'git log -" + str(len(commits)) + " --pretty'?" + " (consider copying MR desc)") - print( - f"* Push your changes ('git push --force {remote_local} HEAD:" f"{status.source_branch}')" - ) - print("* Web UI: comment about your reviewing and testing") - print("* Web UI: approve MR") - print("* Web UI: do (automatic) merge") + print("* Finish the MR: push, approve, merge, comment ('mrhlpr finish')") def parse_args() -> argparse.Namespace: @@ -241,6 +230,16 @@ def parse_args() -> argparse.Namespace: help=f"add message to last commit: {ci_labels['skip_vercheck']}", ) + # finish + finish = sub.add_parser("finish", help="help finishing the MR (push, approve, merge...)") + finish.add_argument( + "-c", + "--comment", + type=str, + nargs="?", + help="comment to post before merging to thank author", + ) + if "argcomplete" in sys.modules: argcomplete.autocomplete(parser, always_complete_options="long") return parser.parse_args() @@ -260,3 +259,11 @@ def main(): mr_id = mr.checked_out() mr.fixmsg(mr_id, args.skip_build, args.ignore_count, args.skip_vercheck) print_status(mr_id) + elif args.action == "finish": + mr_id = mr.checked_out() + # TODO: Check that everything passed + comment = args.comment + if not comment: + comment = input("Provide a thank you comment (discouraged, but can be empty): ") + mr.finish(mr_id, comment, args.no_cache) + print_status(mr_id) diff --git a/mrhlpr/git.py b/mrhlpr/git.py index 40a206463f7a67edd0dc8f70fe796d9b3d5cf83f..c77a6f51ced732d39c7336bc0ee457432c3ee186 100644 --- a/mrhlpr/git.py +++ b/mrhlpr/git.py @@ -115,3 +115,21 @@ def topdir() -> str: msg = "Something went wrong when getting current branch name" raise RuntimeError(msg) return ret + + +def sha() -> str: + """:returns: current SHA""" + ret = run(["rev-parse"]) + if ret is None: + msg = "Something went wrong when getting current commit SHA" + raise RuntimeError(msg) + return ret + + +def push_head(remote: str, branch: str) -> None: + """Push HEAD to branch in remote""" + ret = run(["push", "--force-with-lease", remote, "HEAD:" + branch]) + if ret is None: + msg = f"Something went wrong when pushing HEAD to branch '{branch}' in remote '{remote}'" + raise RuntimeError(msg) + print(ret) diff --git a/mrhlpr/mr.py b/mrhlpr/mr.py index a5d645eecc75201484308b3ac4de91ba56e3c6f6..54a08b0af685caee4ef8a2d712a6e32218368cf6 100644 --- a/mrhlpr/mr.py +++ b/mrhlpr/mr.py @@ -8,9 +8,15 @@ import os import re import subprocess import sys +import time from dataclasses import dataclass from typing import Any, Optional +try: + import gitlab as gitlab_api +except ImportError: + pass + from . import ci_labels # type: ignore[attr-defined] from . import git from . import gitlab @@ -436,3 +442,41 @@ def fixmsg( if skip_vercheck: print("Appending [ci:skip-vercheck] to last commit...") run_git_filter_branch("msg_filter_add_label.py", "HEAD~1", label="skip_vercheck") + + +def finish(mr_id: int, comment: str | None, no_cache: bool = False): + if "gitlab" not in sys.modules: + logging.error("python-gitlab is needed to run 'finish'") + exit(1) + + print("Pushing changes...") + status = get_status(mr_id, no_cache) + origin = gitlab.parse_git_origin() + remote_local = status.source.split("/", 1)[0] + if remote_local == origin.project: + remote_local = "origin" + git.push_head(remote_local, status.source_branch) + + gl = gitlab_api.Gitlab(url="https://gitlab.com", private_token=os.environ["GITLAB_TOKEN"]) + project = gl.projects.get(origin.project_id) + mr = project.mergerequests.get(mr_id) + print("Approving MR...") + try: + mr.approve(git.sha()) + # For some reason, approve fails with auth error if already approved... + except gitlab_api.GitlabAuthenticationError as e: + approved_by_ids = [ap["id"] for ap in mr.approval_state.get().rules[0]["approved_by"]] + gl.auth() + if gl.user is None: + raise e + if gl.user.id not in approved_by_ids: + raise RuntimeError("Approving failed, and MR not yet approved") from e + + if comment: + print("Adding the comment...") + mr.notes.create({"body": comment}) + + # Give gitlab some time to process the push, it sometimes struggles + time.sleep(5) + print("Setting to auto-merge...") + mr.merge(merge_when_pipeline_succeeds=True) diff --git a/pyproject.toml b/pyproject.toml index 6fb5ae726ce694fe12ec16272290cd5449adf1aa..cbabc8921e30ad9dbe8638a559e3d082b9662f7b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ mrtest = "mrtest.frontend:main" [project.optional-dependencies] completion = ["argcomplete"] +api = ["python-gitlab>=4.0.0"] [project.urls] Homepage = "https://www.postmarketos.org"