async def test_gitter(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("LANG", "C") git = gitter.Gitter(mock.Mock()) try: await git.init() await git.configure() await git.add_cred("foo", "bar", "https://github.com") with pytest.raises(gitter.GitError) as exc_info: await git("add", "toto") assert exc_info.value.returncode == 128 assert (exc_info.value.output == "fatal: pathspec 'toto' did not match any files\n") if git.repository is None: pytest.fail("No tmp dir") with open(git.repository + "/.mergify.yml", "w") as f: f.write("pull_request_rules:") await git("add", ".mergify.yml") await git("commit", "-m", "Intial commit", "-a", "--no-edit") assert os.path.exists(f"{git.repository}/.git") finally: await git.cleanup() assert not os.path.exists(f"{git.repository}/.git")
async def _do_rebase( ctxt: context.Context, user: user_tokens.UserTokensUser, committer: typing.Optional[user_tokens.UserTokensUser], ) -> None: # NOTE(sileht): # $ curl https://api.github.com/repos/sileht/repotest/pulls/2 | jq .commits # 2 # $ git clone https://[email protected]/sileht-tester/repotest \ # --depth=$((2 + 1)) -b sileht/testpr # $ cd repotest # $ git remote add upstream https://[email protected]/sileht/repotest.git # $ git log | grep Date | tail -1 # Date: Fri Mar 30 21:30:26 2018 (10 days ago) # $ git fetch upstream master --shallow-since="Fri Mar 30 21:30:26 2018" # $ git rebase upstream/master # $ git push origin sileht/testpr:sileht/testpr if ctxt.pull["head"]["repo"] is None: raise BranchUpdateFailure("The head repository does not exist anymore") head_branch = ctxt.pull["head"]["ref"] base_branch = ctxt.pull["base"]["ref"] git = gitter.Gitter(ctxt.log) try: await git.init() await git.configure(committer) await git.setup_remote( "origin", ctxt.pull["head"]["repo"], user["oauth_access_token"], "" ) await git.setup_remote( "upstream", ctxt.pull["base"]["repo"], user["oauth_access_token"], "" ) await git("fetch", "--quiet", "origin", head_branch) await git("checkout", "-q", "-b", head_branch, f"origin/{head_branch}") await git("fetch", "--quiet", "upstream", base_branch) await git("rebase", f"upstream/{base_branch}") await git("push", "--verbose", "origin", head_branch, "--force-with-lease") expected_sha = (await git("log", "-1", "--format=%H")).strip() # NOTE(sileht): We store this for dismissal action await ctxt.redis.cache.setex( f"branch-update-{expected_sha}", 60 * 60, expected_sha ) except gitter.GitMergifyNamespaceConflict as e: raise BranchUpdateFailure( "`Mergify uses `mergify/...` namespace for creating temporary branches. " "A branch of your repository is conflicting with this namespace\n" f"```\n{e.output}\n```\n" ) except gitter.GitAuthenticationFailure: raise except gitter.GitErrorRetriable as e: raise BranchUpdateNeedRetry( f"Git reported the following error:\n```\n{e.output}\n```\n" ) except gitter.GitFatalError as e: raise BranchUpdateFailure( f"Git reported the following error:\n```\n{e.output}\n```\n" ) except gitter.GitError as e: for message, out_exception in GIT_MESSAGE_TO_EXCEPTION.items(): if message in e.output: raise out_exception( f"Git reported the following error:\n```\n{e.output}\n```\n" ) ctxt.log.error( "update branch failed", output=e.output, returncode=e.returncode, exc_info=True, ) raise BranchUpdateFailure() except Exception: # pragma: no cover ctxt.log.error("update branch failed", exc_info=True) raise BranchUpdateFailure() finally: await git.cleanup()
async def duplicate( ctxt: context.Context, branch_name: github_types.GitHubRefType, *, title_template: str, body_template: str, bot_account: typing.Optional[str] = None, labels: typing.Optional[List[str]] = None, label_conflicts: typing.Optional[str] = None, ignore_conflicts: bool = False, assignees: typing.Optional[List[str]] = None, kind: KindT = "backport", branch_prefix: str = "bp", ) -> typing.Optional[github_types.GitHubPullRequest]: """Duplicate a pull request. :param pull: The pull request. :type pull: py:class:mergify_engine.context.Context :param title_template: The pull request title template. :param body_template: The pull request body template. :param branch: The branch to copy to. :param labels: The list of labels to add to the created PR. :param label_conflicts: The label to add to the created PR when cherry-pick failed. :param ignore_conflicts: Whether to commit the result if the cherry-pick fails. :param assignees: The list of users to be assigned to the created PR. :param kind: is a backport or a copy :param branch_prefix: the prefix of the temporary created branch """ repo_full_name = ctxt.pull["base"]["repo"]["full_name"] bp_branch = get_destination_branch_name(ctxt.pull["number"], branch_name, branch_prefix) cherry_pick_error: str = "" repo_info = await ctxt.client.item(f"/repos/{repo_full_name}") if repo_info["size"] > config.NOSUB_MAX_REPO_SIZE_KB: if not ctxt.subscription.has_feature( subscription.Features.LARGE_REPOSITORY): ctxt.log.warning( "repository too big and no subscription active, refusing to %s", kind, size=repo_info["size"], ) raise DuplicateFailed( f"{kind} fail: repository is too big and no subscription is active" ) ctxt.log.info("running %s on large repository", kind) bot_account_user: typing.Optional[UserTokensUser] = None if bot_account is not None: user_tokens = await ctxt.repository.installation.get_user_tokens() bot_account_user = user_tokens.get_token_for(bot_account) if not bot_account_user: raise DuplicateFailed( f"{kind} fail: user `{bot_account}` is unknown. " f"Please make sure `{bot_account}` has logged in Mergify dashboard." ) # TODO(sileht): This can be done with the Github API only I think: # An example: # https://github.com/shiqiyang-okta/ghpick/blob/master/ghpick/cherry.py git = gitter.Gitter(ctxt.log) try: await git.init() if bot_account_user is None: token = ctxt.client.auth.get_access_token() await git.configure() await git.add_cred("x-access-token", token, repo_full_name) else: await git.configure( bot_account_user["name"] or bot_account_user["login"], bot_account_user["email"], ) await git.add_cred(bot_account_user["oauth_access_token"], "", repo_full_name) await git("remote", "add", "origin", f"{config.GITHUB_URL}/{repo_full_name}") await git("fetch", "--quiet", "origin", f"pull/{ctxt.pull['number']}/head") await git("fetch", "--quiet", "origin", ctxt.pull["base"]["ref"]) await git("fetch", "--quiet", "origin", branch_name) await git("checkout", "--quiet", "-b", bp_branch, f"origin/{branch_name}") merge_commit = await ctxt.client.item( f"{ctxt.base_url}/commits/{ctxt.pull['merge_commit_sha']}") for commit in await _get_commits_to_cherrypick(ctxt, merge_commit): # FIXME(sileht): Github does not allow to fetch only one commit # So we have to fetch the branch since the commit date ... # git("fetch", "origin", "%s:refs/remotes/origin/%s-commit" % # (commit["sha"], commit["sha"]) # ) # last_commit_date = commit["commit"]["committer"]["date"] # git("fetch", "origin", ctxt.pull["base"]["ref"], # "--shallow-since='%s'" % last_commit_date) try: await git("cherry-pick", "-x", commit["sha"]) except gitter.GitError as e: # pragma: no cover ctxt.log.info("fail to cherry-pick %s: %s", commit["sha"], e.output) output = await git("status") cherry_pick_error += f"Cherry-pick of {commit['sha']} has failed:\n```\n{output}```\n\n\n" if not ignore_conflicts: raise DuplicateFailed(cherry_pick_error) await git("add", "*") await git("commit", "-a", "--no-edit", "--allow-empty") await git("push", "origin", bp_branch) except gitter.GitError as in_exception: # pragma: no cover if in_exception.output == "": raise DuplicateNeedRetry("git process got sigkill") for message, out_exception in GIT_MESSAGE_TO_EXCEPTION.items(): if message in in_exception.output: raise out_exception("Git reported the following error:\n" f"```\n{in_exception.output}\n```\n") else: raise DuplicateUnexpectedError(in_exception.output) finally: await git.cleanup() if cherry_pick_error: cherry_pick_error += ( "To fix up this pull request, you can check it out locally. " "See documentation: " "https://help.github.com/articles/" "checking-out-pull-requests-locally/") try: title = await ctxt.pull_request.render_template( title_template, extra_variables={"destination_branch": branch_name}, ) except context.RenderTemplateFailure as rmf: raise DuplicateFailed(f"Invalid title message: {rmf}") try: body = await ctxt.pull_request.render_template( body_template, extra_variables={ "destination_branch": branch_name, "cherry_pick_error": cherry_pick_error, }, ) except context.RenderTemplateFailure as rmf: raise DuplicateFailed(f"Invalid title message: {rmf}") try: duplicate_pr = typing.cast( github_types.GitHubPullRequest, (await ctxt.client.post( f"{ctxt.base_url}/pulls", json={ "title": title, "body": body + "\n\n---\n\n" + constants.MERGIFY_PULL_REQUEST_DOC, "base": branch_name, "head": bp_branch, }, oauth_token=bot_account_user["oauth_access_token"] if bot_account_user else None, )).json(), ) except http.HTTPClientSideError as e: if e.status_code == 422 and "No commits between" in e.message: if cherry_pick_error: raise DuplicateFailed(cherry_pick_error) else: raise DuplicateNotNeeded(e.message) raise effective_labels = [] if labels is not None: effective_labels.extend(labels) if cherry_pick_error and label_conflicts is not None: effective_labels.append(label_conflicts) if len(effective_labels) > 0: await ctxt.client.post( f"{ctxt.base_url}/issues/{duplicate_pr['number']}/labels", json={"labels": effective_labels}, ) if assignees is not None and len(assignees) > 0: # NOTE(sileht): we don't have to deal with invalid assignees as GitHub # just ignore them and always return 201 await ctxt.client.post( f"{ctxt.base_url}/issues/{duplicate_pr['number']}/assignees", json={"assignees": assignees}, ) return duplicate_pr
async def _do_rebase(ctxt: context.Context, token: str) -> None: # NOTE(sileht): # $ curl https://api.github.com/repos/sileht/repotest/pulls/2 | jq .commits # 2 # $ git clone https://[email protected]/sileht-tester/repotest \ # --depth=$((2 + 1)) -b sileht/testpr # $ cd repotest # $ git remote add upstream https://[email protected]/sileht/repotest.git # $ git log | grep Date | tail -1 # Date: Fri Mar 30 21:30:26 2018 (10 days ago) # $ git fetch upstream master --shallow-since="Fri Mar 30 21:30:26 2018" # $ git rebase upstream/master # $ git push origin sileht/testpr:sileht/testpr head_repo = (ctxt.pull["head"]["repo"]["owner"]["login"] + "/" + ctxt.pull["head"]["repo"]["name"]) base_repo = (ctxt.pull["base"]["repo"]["owner"]["login"] + "/" + ctxt.pull["base"]["repo"]["name"]) head_branch = ctxt.pull["head"]["ref"] base_branch = ctxt.pull["base"]["ref"] git = gitter.Gitter(ctxt.log) try: await git.init() await git.configure() await git.add_cred(token, "", head_repo) await git.add_cred(token, "", base_repo) await git("remote", "add", "origin", f"{config.GITHUB_URL}/{head_repo}") await git("remote", "add", "upstream", f"{config.GITHUB_URL}/{base_repo}") depth = len(await ctxt.commits) + 1 await git("fetch", "--quiet", f"--depth={depth}", "origin", head_branch) await git("checkout", "-q", "-b", head_branch, f"origin/{head_branch}") output = await git("log", "--format=%cI") last_commit_date = [d for d in output.split("\n") if d.strip()][-1] await git( "fetch", "--quiet", "upstream", base_branch, f"--shallow-since='{last_commit_date}'", ) # Try to find the merge base, but don't fetch more that 1000 commits. for _ in range(20): await git("repack", "-d") try: await git( "merge-base", f"upstream/{base_branch}", f"origin/{head_branch}", ) except gitter.GitError as e: # pragma: no cover if e.returncode == 1: # We need more commits await git("fetch", "-q", "--deepen=50", "upstream", base_branch) continue raise else: break try: await git("rebase", f"upstream/{base_branch}") await git("push", "--verbose", "origin", head_branch, "-f") except gitter.GitError as e: # pragma: no cover for message in GIT_MESSAGE_TO_UNSHALLOW: if message in e.output: ctxt.log.info("Complete history cloned") # NOTE(sileht): We currently assume we have only one parent # commit in common. Since Git is a graph, in some case this # graph can be more complicated. # So, retrying with the whole git history for now await git("fetch", "--unshallow") await git("fetch", "--quiet", "origin", head_branch) await git("fetch", "--quiet", "upstream", base_branch) await git("rebase", f"upstream/{base_branch}") await git("push", "--verbose", "origin", head_branch, "-f") break else: raise expected_sha = (await git("log", "-1", "--format=%H")).strip() # NOTE(sileht): We store this for dismissal action await ctxt.redis.setex(f"branch-update-{expected_sha}", 60 * 60, expected_sha) except gitter.GitError as in_exception: # pragma: no cover if in_exception.output == "": # SIGKILL... raise BranchUpdateNeedRetry("Git process got killed") for message, out_exception in GIT_MESSAGE_TO_EXCEPTION.items(): if message in in_exception.output: raise out_exception("Git reported the following error:\n" f"```\n{in_exception.output}\n```\n") else: ctxt.log.error( "update branch failed: %s", in_exception.output, exc_info=True, ) raise BranchUpdateFailure() except Exception: # pragma: no cover ctxt.log.error("update branch failed", exc_info=True) raise BranchUpdateFailure() finally: await git.cleanup()
async def duplicate( ctxt: context.Context, branch_name: str, label_conflicts: typing.Optional[str] = None, ignore_conflicts: bool = False, kind: KindT = "backport", ) -> typing.Optional[github_types.GitHubPullRequest]: """Duplicate a pull request. :param pull: The pull request. :type pull: py:class:mergify_engine.context.Context :param branch: The branch to copy to. :param label_conflicts: The label to add to the created PR when cherry-pick failed. :param ignore_conflicts: Whether to commit the result if the cherry-pick fails. :param kind: is a backport or a copy """ repo_full_name = ctxt.pull["base"]["repo"]["full_name"] bp_branch = get_destination_branch_name(ctxt.pull["number"], branch_name, kind) cherry_pick_fail = False body = "" repo_info = await ctxt.client.item(f"/repos/{repo_full_name}") if repo_info["size"] > config.NOSUB_MAX_REPO_SIZE_KB: if not ctxt.subscription.has_feature( subscription.Features.LARGE_REPOSITORY): ctxt.log.warning( "repository too big and no subscription active, refusing to %s", kind, size=repo_info["size"], ) raise DuplicateFailed( f"{kind} fail: repository is too big and no subscription is active" ) ctxt.log.info("running %s on large repository", kind) # TODO(sileht): This can be done with the Github API only I think: # An example: # https://github.com/shiqiyang-okta/ghpick/blob/master/ghpick/cherry.py git = gitter.Gitter(ctxt.log) try: token = ctxt.client.auth.get_access_token() await git.init() await git.configure() await git.add_cred("x-access-token", token, repo_full_name) await git("remote", "add", "origin", f"{config.GITHUB_URL}/{repo_full_name}") await git("fetch", "--quiet", "origin", f"pull/{ctxt.pull['number']}/head") await git("fetch", "--quiet", "origin", ctxt.pull["base"]["ref"]) await git("fetch", "--quiet", "origin", branch_name) await git("checkout", "--quiet", "-b", bp_branch, f"origin/{branch_name}") merge_commit = await ctxt.client.item( f"{ctxt.base_url}/commits/{ctxt.pull['merge_commit_sha']}") for commit in await _get_commits_to_cherrypick(ctxt, merge_commit): # FIXME(sileht): Github does not allow to fetch only one commit # So we have to fetch the branch since the commit date ... # git("fetch", "origin", "%s:refs/remotes/origin/%s-commit" % # (commit["sha"], commit["sha"]) # ) # last_commit_date = commit["commit"]["committer"]["date"] # git("fetch", "origin", ctxt.pull["base"]["ref"], # "--shallow-since='%s'" % last_commit_date) try: await git("cherry-pick", "-x", commit["sha"]) except gitter.GitError as e: # pragma: no cover ctxt.log.info("fail to cherry-pick %s: %s", commit["sha"], e.output) output = await git("status") body += f"\n\nCherry-pick of {commit['sha']} has failed:\n```\n{output}```\n\n" if not ignore_conflicts: raise DuplicateFailed(body) cherry_pick_fail = True await git("add", "*") await git("commit", "-a", "--no-edit", "--allow-empty") await git("push", "origin", bp_branch) except gitter.GitError as in_exception: # pragma: no cover if in_exception.output == "": raise DuplicateNeedRetry("git process got sigkill") for message, out_exception in GIT_MESSAGE_TO_EXCEPTION.items(): if message in in_exception.output: raise out_exception("Git reported the following error:\n" f"```\n{in_exception.output}\n```\n") else: raise DuplicateUnexpectedError(in_exception.output) finally: await git.cleanup() body = ( f"This is an automatic {kind} of pull request #{ctxt.pull['number']} done by [Mergify](https://mergify.io)." + body) if cherry_pick_fail: body += ("To fixup this pull request, you can check out it locally. " "See documentation: " "https://help.github.com/articles/" "checking-out-pull-requests-locally/") try: duplicate_pr = typing.cast( github_types.GitHubPullRequest, (await ctxt.client.post( f"{ctxt.base_url}/pulls", json={ "title": f"{ctxt.pull['title']} ({BRANCH_PREFIX_MAP[kind]} #{ctxt.pull['number']})", "body": body + "\n\n---\n\n" + doc.MERGIFY_PULL_REQUEST_DOC, "base": branch_name, "head": bp_branch, }, )).json(), ) except http.HTTPClientSideError as e: if e.status_code == 422 and "No commits between" in e.message: raise DuplicateNotNeeded(e.message) raise if cherry_pick_fail and label_conflicts is not None: await ctxt.client.post( f"{ctxt.base_url}/issues/{duplicate_pr['number']}/labels", json={"labels": [label_conflicts]}, ) return duplicate_pr
async def _do_squash(ctxt: context.Context, user: user_tokens.UserTokensUser, squash_message: str) -> None: head_branch = ctxt.pull["head"]["ref"] base_branch = ctxt.pull["base"]["ref"] tmp_branch = "squashed-head-branch" git = gitter.Gitter(ctxt.log) if ctxt.pull["head"]["repo"] is None: raise SquashFailure( f"The head repository of {ctxt.pull['base']['label']} has been deleted." ) try: await git.init() if ctxt.subscription.has_feature(Features.BOT_ACCOUNT): await git.configure(user) else: await git.configure() await git.setup_remote("origin", ctxt.pull["head"]["repo"], user["oauth_access_token"], "") await git.setup_remote("upstream", ctxt.pull["base"]["repo"], user["oauth_access_token"], "") await git("fetch", "--quiet", "origin", head_branch) await git("fetch", "--quiet", "upstream", base_branch) await git("checkout", "-q", "-b", tmp_branch, f"upstream/{base_branch}") await git("merge", "--squash", "--no-edit", f"origin/{head_branch}") await git("commit", "-m", squash_message) await git( "push", "--verbose", "origin", f"{tmp_branch}:{head_branch}", "--force-with-lease", ) expected_sha = (await git("log", "-1", "--format=%H")).strip() # NOTE(sileht): We store this for dismissal action # FIXME(sileht): use a more generic name for the key await ctxt.redis.cache.setex(f"branch-update-{expected_sha}", 60 * 60, expected_sha) except gitter.GitMergifyNamespaceConflict as e: raise SquashFailure( "`Mergify uses `mergify/...` namespace for creating temporary branches. " "A branch of your repository is conflicting with this namespace\n" f"```\n{e.output}\n```\n") except gitter.GitAuthenticationFailure: raise except gitter.GitErrorRetriable as e: raise SquashNeedRetry( f"Git reported the following error:\n```\n{e.output}\n```\n") except gitter.GitFatalError as e: raise SquashFailure( f"Git reported the following error:\n```\n{e.output}\n```\n") except gitter.GitError as e: ctxt.log.error( "squash failed", output=e.output, returncode=e.returncode, exc_info=True, ) raise SquashFailure("") except Exception: # pragma: no cover ctxt.log.error("squash failed", exc_info=True) raise SquashFailure("") finally: await git.cleanup()
async def duplicate( ctxt: context.Context, branch_name: github_types.GitHubRefType, *, title_template: str, body_template: str, bot_account: typing.Optional[github_types.GitHubLogin] = None, labels: typing.Optional[List[str]] = None, label_conflicts: typing.Optional[str] = None, ignore_conflicts: bool = False, assignees: typing.Optional[List[str]] = None, branch_prefix: str = "bp", ) -> typing.Optional[github_types.GitHubPullRequest]: """Duplicate a pull request. :param pull: The pull request. :type pull: py:class:mergify_engine.context.Context :param title_template: The pull request title template. :param body_template: The pull request body template. :param branch: The branch to copy to. :param labels: The list of labels to add to the created PR. :param label_conflicts: The label to add to the created PR when cherry-pick failed. :param ignore_conflicts: Whether to commit the result if the cherry-pick fails. :param assignees: The list of users to be assigned to the created PR. :param branch_prefix: the prefix of the temporary created branch """ bp_branch = get_destination_branch_name( ctxt.pull["number"], branch_name, branch_prefix ) cherry_pick_error: str = "" bot_account_user: typing.Optional[UserTokensUser] = None if bot_account is not None: user_tokens = await ctxt.repository.installation.get_user_tokens() bot_account_user = user_tokens.get_token_for(bot_account) if not bot_account_user: raise DuplicateFailed( f"User `{bot_account}` is unknown. " f"Please make sure `{bot_account}` has logged in Mergify dashboard." ) # TODO(sileht): This can be done with the Github API only I think: # An example: # https://github.com/shiqiyang-okta/ghpick/blob/master/ghpick/cherry.py git = gitter.Gitter(ctxt.log) try: await git.init() if bot_account_user is None: token = await ctxt.client.get_access_token() await git.configure() username = "******" password = token else: await git.configure(bot_account_user) username = bot_account_user["oauth_access_token"] password = "" # nosec await git.setup_remote("origin", ctxt.pull["base"]["repo"], username, password) await git("fetch", "--quiet", "origin", f"pull/{ctxt.pull['number']}/head") await git("fetch", "--quiet", "origin", ctxt.pull["base"]["ref"]) await git("fetch", "--quiet", "origin", branch_name) await git("checkout", "--quiet", "-b", bp_branch, f"origin/{branch_name}") merge_commit = github_types.to_cached_github_branch_commit( typing.cast( github_types.GitHubBranchCommit, await ctxt.client.item( f"{ctxt.base_url}/commits/{ctxt.pull['merge_commit_sha']}" ), ) ) for commit in await _get_commits_to_cherrypick(ctxt, merge_commit): # FIXME(sileht): Github does not allow to fetch only one commit # So we have to fetch the branch since the commit date ... # git("fetch", "origin", "%s:refs/remotes/origin/%s-commit" % # (commit["sha"], commit["sha"]) # ) # last_commit_date = commit["commit"]["committer"]["date"] # git("fetch", "origin", ctxt.pull["base"]["ref"], # "--shallow-since='%s'" % last_commit_date) try: await git("cherry-pick", "-x", commit["sha"]) except ( gitter.GitAuthenticationFailure, gitter.GitErrorRetriable, gitter.GitFatalError, ): raise except gitter.GitError as e: # pragma: no cover for message in GIT_MESSAGE_TO_EXCEPTION.keys(): if message in e.output: raise ctxt.log.info("fail to cherry-pick %s: %s", commit["sha"], e.output) output = await git("status") cherry_pick_error += f"Cherry-pick of {commit['sha']} has failed:\n```\n{output}```\n\n\n" if not ignore_conflicts: raise DuplicateFailed(cherry_pick_error) await git("add", "*", _env={"GIT_NOGLOB_PATHSPECS": "0"}) await git("commit", "-a", "--no-edit", "--allow-empty") await git("push", "origin", bp_branch) except gitter.GitMergifyNamespaceConflict as e: raise DuplicateUnexpectedError( "`Mergify uses `mergify/...` namespace for creating temporary branches. " "A branch of your repository is conflicting with this namespace\n" f"```\n{e.output}\n```\n" ) except gitter.GitAuthenticationFailure as e: if bot_account_user is None: # Need to get a new token raise DuplicateNeedRetry( f"Git reported the following error:\n```\n{e.output}\n```\n" ) else: raise DuplicateUnexpectedError( f"Git reported the following error:\n```\n{e.output}\n```\n" ) except gitter.GitErrorRetriable as e: raise DuplicateNeedRetry( f"Git reported the following error:\n```\n{e.output}\n```\n" ) except gitter.GitFatalError as e: raise DuplicateUnexpectedError( f"Git reported the following error:\n```\n{e.output}\n```\n" ) except gitter.GitError as e: # pragma: no cover for message, out_exception in GIT_MESSAGE_TO_EXCEPTION.items(): if message in e.output: raise out_exception( f"Git reported the following error:\n```\n{e.output}\n```\n" ) ctxt.log.error( "duplicate pull failed", output=e.output, returncode=e.returncode, exc_info=True, ) raise DuplicateUnexpectedError(e.output) finally: await git.cleanup() if cherry_pick_error: cherry_pick_error += ( "To fix up this pull request, you can check it out locally. " "See documentation: " "https://docs.github.com/en/github/" "collaborating-with-pull-requests/reviewing-changes-in-pull-requests/checking-out-pull-requests-locally" ) try: title = await ctxt.pull_request.render_template( title_template, extra_variables={"destination_branch": branch_name}, ) except context.RenderTemplateFailure as rmf: raise DuplicateFailed(f"Invalid title message: {rmf}") try: body = await ctxt.pull_request.render_template( body_template, extra_variables={ "destination_branch": branch_name, "cherry_pick_error": cherry_pick_error, }, ) except context.RenderTemplateFailure as rmf: raise DuplicateFailed(f"Invalid title message: {rmf}") try: duplicate_pr = typing.cast( github_types.GitHubPullRequest, ( await ctxt.client.post( f"{ctxt.base_url}/pulls", json={ "title": title, "body": body + "\n\n---\n\n" + constants.MERGIFY_PULL_REQUEST_DOC, "base": branch_name, "head": bp_branch, }, oauth_token=bot_account_user["oauth_access_token"] if bot_account_user else None, ) ).json(), ) except http.HTTPClientSideError as e: if e.status_code == 422: if "No commits between" in e.message: if cherry_pick_error: raise DuplicateFailed(cherry_pick_error) else: raise DuplicateNotNeeded(e.message) elif "A pull request already exists" in e.message: raise DuplicateAlreadyExists(e.message) raise effective_labels = [] if labels is not None: effective_labels.extend(labels) if cherry_pick_error and label_conflicts is not None: effective_labels.append(label_conflicts) if len(effective_labels) > 0: await ctxt.client.post( f"{ctxt.base_url}/issues/{duplicate_pr['number']}/labels", json={"labels": effective_labels}, ) if assignees is not None and len(assignees) > 0: # NOTE(sileht): we don't have to deal with invalid assignees as GitHub # just ignore them and always return 201 await ctxt.client.post( f"{ctxt.base_url}/issues/{duplicate_pr['number']}/assignees", json={"assignees": assignees}, ) return duplicate_pr