import os import re import subprocess import sys from typing import Optional import osc.commandline_git def get_editor() -> str: import shutil editor = os.getenv("EDITOR", None) if editor: candidates = [editor] else: candidates = ["vim", "vi"] editor_path = None for i in candidates: editor_path = shutil.which(i) if editor_path: break if not editor_path: raise RuntimeError(f"Unable to start editor '{candidates[0]}'") return editor_path def run_editor(file_path: str): cmd = [get_editor(), file_path] subprocess.run(cmd) def edit_message(template: Optional[str] = None) -> str: import tempfile with tempfile.NamedTemporaryFile(mode="w+", encoding="utf-8", prefix="git_obs_message_") as f: if template: f.write(template) f.flush() run_editor(f.name) f.seek(0) return f.read() NEW_PULL_REQUEST_TEMPLATE = """ # title {title} # description {description} # # Please enter pull request title and description in the following format: # # <blank line> # <description> # # Lines starting with '#' will be ignored, and an empty message aborts the operation. # # Creating {source_owner}/{source_repo}#{source_branch} -> {target_owner}/{target_repo}#{target_branch} # """.lstrip() class PullRequestCreateCommand(osc.commandline_git.GitObsCommand): """ Create a pull request """ name = "create" parent = "PullRequestCommand" def init_arguments(self): self.add_argument( "--title", metavar="TEXT", help="Pull request title", ) self.add_argument( "--description", metavar="TEXT", help="Pull request description (body)", ) self.add_argument( "--source-owner", metavar="OWNER", help="Owner of the source repo (default: derived from remote URL in local git repo)", ) self.add_argument( "--source-repo", metavar="REPO", help="Name of the source repo (default: derived from remote URL in local git repo)", ) self.add_argument( "--source-branch", metavar="BRANCH", help="Source branch (default: the current branch in local git repo)", ) self.add_argument( "--target-branch", metavar="BRANCH", help="Target branch (default: derived from the current branch in local git repo)", ) def run(self, args): from osc import gitea_api # the source args are optional, but if one of them is used, the others must be used too source_args = (args.source_owner, args.source_repo, args.source_branch) if sum((int(i is not None) for i in source_args)) not in (0, len(source_args)): self.parser.error("All of the following options must be used together: --source-owner, --source-repo, --source-branch") self.print_gitea_settings() use_local_git = args.source_owner is None if use_local_git: # local git repo git = gitea_api.Git(".") local_owner, local_repo = git.get_owner_repo() local_branch = git.current_branch local_rev = git.get_branch_head(local_branch) # remote git repo - source if use_local_git: source_owner = local_owner source_repo = local_repo source_branch = local_branch else: source_owner = args.source_owner source_repo = args.source_repo source_branch = args.source_branch source_repo_data = gitea_api.Repo.get(self.gitea_conn, source_owner, source_repo).json() source_branch_data = gitea_api.Branch.get(self.gitea_conn, source_owner, source_repo, source_branch).json() source_rev = source_branch_data["commit"]["id"] # remote git repo - target target_owner, target_repo = source_repo_data["parent"]["full_name"].split("/") if source_branch.startswith("for/"): # source branch name format: for/<target-branch>/<what-the-branch-name-would-normally-be> target_branch = source_branch.split("/")[1] else: target_branch = source_branch target_branch_data = gitea_api.Branch.get(self.gitea_conn, target_owner, target_repo, target_branch).json() target_rev = target_branch_data["commit"]["id"] print("Creating a pull request ...", file=sys.stderr) if use_local_git: print(f" * Local git: branch: {local_branch}, rev: {local_rev}", file=sys.stderr) print(f" * Source: {source_owner}/{source_repo}, branch: {source_branch}, rev: {source_rev}", file=sys.stderr) print(f" * Target: {target_owner}/{target_repo}, branch: {target_branch}, rev: {target_rev}", file=sys.stderr) if use_local_git and local_rev != source_rev: from osc.output import tty print(f"{tty.colorize('ERROR', 'red,bold')}: Local commit doesn't correspond with the latest commit in the remote source branch") sys.exit(1) if source_rev == target_rev: from osc.output import tty print(f"{tty.colorize('ERROR', 'red,bold')}: Source and target are identical, make and push changes to the remote source repo first") sys.exit(1) title = args.title or "" description = args.description or "" if not title or not description: # TODO: add list of commits and list of changed files to the template; requires local git repo message = edit_message(template=NEW_PULL_REQUEST_TEMPLATE.format(**locals())) # remove comments message = "\n".join([i for i in message.splitlines() if not i.startswith("#")]) # strip leading and trailing spaces message = message.strip() if not message: raise RuntimeError("Aborting operation due to empty title and description.") parts = re.split(r"\n\n", message, 1) if len(parts) == 1: # empty description title = parts[0] description = "" else: title = parts[0] description = parts[1] title = title.strip() description = description.strip() pull = gitea_api.PullRequest.create( self.gitea_conn, target_owner=target_owner, target_repo=target_repo, target_branch=target_branch, source_owner=source_owner, # source_repo is not required because the information lives in Gitea database source_branch=source_branch, title=title, description=description, ).json() print("", file=sys.stderr) print("Pull request created:", file=sys.stderr) print(gitea_api.PullRequest.to_human_readable_string(pull))