Beispiel #1
0
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()
Beispiel #3
0
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
Beispiel #4
0
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
Beispiel #6
0
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()
Beispiel #7
0
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