def update(pull, token, method="merge"): # 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 = pull.g_pull.head.repo.full_name base_repo = pull.g_pull.base.repo.full_name head_branch = pull.g_pull.head.ref base_branch = pull.g_pull.base.ref git = utils.Gitter() try: git("init") git.configure() git.add_cred(token, "", head_repo) git.add_cred(token, "", base_repo) git("remote", "add", "origin", "https://%s/%s" % (config.GITHUB_DOMAIN, head_repo)) git("remote", "add", "upstream", "https://%s/%s" % (config.GITHUB_DOMAIN, base_repo)) depth = int(pull.g_pull.commits) + 1 git("fetch", "--quiet", "--depth=%d" % depth, "origin", head_branch) git("checkout", "-q", "-b", head_branch, "origin/%s" % head_branch) out = git("log", "--format=%cI") last_commit_date = [ d for d in out.decode("utf8").split("\n") if d.strip() ][-1] git("fetch", "--quiet", "upstream", pull.g_pull.base.ref, "--shallow-since='%s'" % last_commit_date) if method == "merge": git("merge", "--quiet", "upstream/%s" % base_branch, "-m", "Merge branch '%s' into '%s'" % (base_branch, head_branch)) git("push", "--quiet", "origin", head_branch) elif method == "rebase": git("rebase", "upstream/%s" % base_branch) git("push", "--quiet", "origin", head_branch, "-f") else: raise RuntimeError("Invalid branch update method") return git("log", "-1", "--format=%H").decode().strip() except Exception: # pragma: no cover LOG.error("update branch fail", pull_request=pull, exc_info=True) finally: git.cleanup()
def update_branch(self, token, merge=True): # 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 git = utils.Gitter() try: git("clone", "--depth=%d" % (int(self.commits) + 1), "-b", self.head.ref, "https://%[email protected]/%s/" % (token, self.head.repo.full_name), ".") git("remote", "add", "upstream", "https://%[email protected]/%s.git" % (token, self.base.repo.full_name)) git("config", "user.name", "%s-bot" % config.CONTEXT) git("config", "user.email", "*****@*****.**") out = git("log", "--pretty='format:%cI'") last_commit_date = out.decode("utf8").split("\n")[-1] git("fetch", "upstream", self.base.ref, "--shallow-since='%s'" % last_commit_date) if merge: git("merge", "upstream/%s" % self.base.ref, "-m", "Merge branch '%s' into '%s'" % (self.base.ref, self.head.ref)) else: # TODO(sileht): This will removes approvals, we need to add them # back git("rebase", "upstream/%s" % self.base.ref) git("push", "origin", self.head.ref) except Exception: LOG.exception("git rebase fail") return False finally: git.cleanup() return True
def _do_update(pull, token, method="merge"): # 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 = pull.g_pull.head.repo.full_name base_repo = pull.g_pull.base.repo.full_name head_branch = pull.g_pull.head.ref base_branch = pull.g_pull.base.ref git = utils.Gitter() try: git("init") git.configure() git.add_cred(token, "", head_repo) git.add_cred(token, "", base_repo) git("remote", "add", "origin", "https://%s/%s" % (config.GITHUB_DOMAIN, head_repo)) git("remote", "add", "upstream", "https://%s/%s" % (config.GITHUB_DOMAIN, base_repo)) depth = int(pull.g_pull.commits) + 1 git("fetch", "--quiet", "--depth=%d" % depth, "origin", head_branch) git("checkout", "-q", "-b", head_branch, "origin/%s" % head_branch) out = git("log", "--format=%cI") last_commit_date = [ d for d in out.decode("utf8").split("\n") if d.strip() ][-1] git("fetch", "--quiet", "upstream", base_branch, "--shallow-since='%s'" % last_commit_date) try: _do_update_branch(git, method, base_branch, head_branch) except subprocess.CalledProcessError as e: # pragma: no cover if b"unrelated histories" in e.output: LOG.debug("Complete history cloned", pull_request=pull) # 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 git("fetch", "--quiet", "origin", head_branch) git("fetch", "--quiet", "upstream", base_branch) _do_update_branch(git, method, base_branch, head_branch) else: raise return git("log", "-1", "--format=%H").decode().strip() except subprocess.CalledProcessError as e: # pragma: no cover for message in AUTHENTICATION_FAILURE_MESSAGES: if message in e.output: raise AuthentificationFailure(e.output) else: LOG.error("update branch failed: %s", e.output, pull_request=pull, exc_info=True) except Exception: # pragma: no cover LOG.error("update branch failed", pull_request=pull, exc_info=True) finally: git.cleanup()
def duplicate(pull, branch, installation_token, kind=BACKPORT): """Duplicate a pull request. :param repo: The repository. :param pull: The pull request. :type pull: py:class:mergify_engine.mergify_pull.MergifyPull :param branch_name: The branch name to copy to. :param installation_token: The installation token. :param kind: is a backport or a copy """ repo = pull.g_pull.base.repo bp_branch = get_destination_branch_name(pull, branch, kind) cherry_pick_fail = False body = "This is an automated %s of pull request #%d done " "by Mergify.io" % ( kind, pull.g_pull.number, ) git = utils.Gitter() # 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 try: git("init") git.configure() git.add_cred("x-access-token", installation_token, repo.full_name) git( "remote", "add", "origin", "https://%s/%s" % (config.GITHUB_DOMAIN, repo.full_name), ) git("fetch", "--quiet", "origin", "pull/%s/head" % pull.g_pull.number) git("fetch", "--quiet", "origin", pull.g_pull.base.ref) git("fetch", "--quiet", "origin", branch.name) git("checkout", "--quiet", "-b", bp_branch, "origin/%s" % branch.name) merge_commit = repo.get_commit(pull.g_pull.merge_commit_sha) for commit in _get_commits_to_cherrypick(pull, 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", pull.base.ref, # "--shallow-since='%s'" % last_commit_date) try: git("cherry-pick", "-x", commit.sha) except subprocess.CalledProcessError as e: # pragma: no cover pull.log.debug("fail to cherry-pick %s: %s", commit.sha, e.output) cherry_pick_fail = True status = git("status").decode("utf8") git("add", "*") git("commit", "-a", "--no-edit", "--allow-empty") body += "\n\nCherry-pick of %s has failed:\n```\n%s```\n\n" % ( commit.sha, status, ) git("push", "origin", bp_branch) except subprocess.CalledProcessError as in_exception: # pragma: no cover for message, out_exception in GIT_MESSAGE_TO_EXCEPTION.items(): if message in in_exception.output: if out_exception is None: return else: raise out_exception(in_exception.output.decode()) else: pull.log.error( "duplicate failed: %s", in_exception.output.decode(), branch=branch.name, kind=kind, exc_info=True, ) return except Exception: # pragma: no cover pull.log.error( "duplicate failed", pull_request=pull, branch=branch.name, kind=kind, exc_info=True, ) return finally: git.cleanup() 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: return repo.create_pull( title="{} ({} #{})".format(pull.g_pull.title, BRANCH_PREFIX_MAP[kind], pull.g_pull.number), body=body + "\n---\n\n" + doc.MERGIFY_PULL_REQUEST_DOC, base=branch.name, head=bp_branch, ) except github.GithubException as e: if e.status == 422 and "No commits between" in e.data["message"]: return raise
def duplicate(ctxt, branch_name, label_conflicts=None, ignore_conflicts=False, kind=BACKPORT): """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 = "" git = utils.Gitter(ctxt.log) repo_info = 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 try: token = ctxt.client.auth.get_access_token() git("init") git.configure() git.add_cred("x-access-token", token, repo_full_name) git("remote", "add", "origin", f"{config.GITHUB_URL}/{repo_full_name}") git("fetch", "--quiet", "origin", "pull/%s/head" % ctxt.pull["number"]) git("fetch", "--quiet", "origin", ctxt.pull["base"]["ref"]) git("fetch", "--quiet", "origin", branch_name) git("checkout", "--quiet", "-b", bp_branch, "origin/%s" % branch_name) merge_commit = ctxt.client.item( f"{ctxt.base_url}/commits/{ctxt.pull['merge_commit_sha']}") for commit in _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: git("cherry-pick", "-x", commit["sha"]) except subprocess.CalledProcessError as e: # pragma: no cover ctxt.log.info("fail to cherry-pick %s: %s", commit["sha"], e.output) git_status = git("status").decode("utf8") body += f"\n\nCherry-pick of {commit['sha']} has failed:\n```\n{git_status}```\n\n" if not ignore_conflicts: raise DuplicateFailed(body) cherry_pick_fail = True git("add", "*") git("commit", "-a", "--no-edit", "--allow-empty") git("push", "origin", bp_branch) except subprocess.CalledProcessError as in_exception: # pragma: no cover for message, out_exception in GIT_MESSAGE_TO_EXCEPTION.items(): if message in in_exception.output: if out_exception is None: return else: raise out_exception(in_exception.output.decode()) else: ctxt.log.error( "duplicate failed: %s", in_exception.output.decode(), branch=branch_name, kind=kind, exc_info=True, ) return finally: git.cleanup() body = ( f"This is an automated {kind} of pull request #{ctxt.pull['number']} done by Mergify" + 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 = ctxt.client.post( f"{ctxt.base_url}/pulls", json={ "title": "{} ({} #{})".format(ctxt.pull["title"], BRANCH_PREFIX_MAP[kind], ctxt.pull["number"]), "body": body + "\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: return raise if cherry_pick_fail and label_conflicts is not None: ctxt.client.post( f"{ctxt.base_url}/issues/{duplicate_pr['number']}/labels", json={"labels": [label_conflicts]}, ) return duplicate_pr
def _do_update(ctxt, token, method="merge"): # 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 = utils.Gitter(ctxt.log) try: git("init") git.configure() git.add_cred(token, "", head_repo) git.add_cred(token, "", base_repo) git("remote", "add", "origin", f"{config.GITHUB_URL}/{head_repo}") git("remote", "add", "upstream", f"{config.GITHUB_URL}/{base_repo}") depth = len(ctxt.commits) + 1 git("fetch", "--quiet", "--depth=%d" % depth, "origin", head_branch) git("checkout", "-q", "-b", head_branch, "origin/%s" % head_branch) out = git("log", "--format=%cI") last_commit_date = [ d for d in out.decode("utf8").split("\n") if d.strip() ][-1] git( "fetch", "--quiet", "upstream", base_branch, "--shallow-since='%s'" % last_commit_date, ) # Try to find the merge base, but don't fetch more that 1000 commits. for _ in range(20): git("repack", "-d") if git("merge-base", f"upstream/{base_branch}", f"origin/{head_branch}"): break git("fetch", "-q", "--deepen=50", "upsteam", base_branch) try: _do_update_branch(git, method, base_branch, head_branch) except subprocess.CalledProcessError 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 git("fetch", "--unshallow") git("fetch", "--quiet", "origin", head_branch) git("fetch", "--quiet", "upstream", base_branch) _do_update_branch(git, method, base_branch, head_branch) break else: raise expected_sha = git("log", "-1", "--format=%H").decode().strip() # NOTE(sileht): We store this for dismissal action with utils.get_redis_for_cache() as redis: redis.setex("branch-update-%s" % expected_sha, 60 * 60, expected_sha) except subprocess.CalledProcessError as in_exception: # pragma: no cover 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.decode()}\n```\n") else: ctxt.log.error( "update branch failed: %s", in_exception.output.decode(), exc_info=True, ) raise BranchUpdateFailure() except Exception: # pragma: no cover ctxt.log.error("update branch failed", exc_info=True) raise BranchUpdateFailure() finally: git.cleanup()
def _do_update(pull, token, method="merge"): # 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 = pull.head_repo_owner_login + "/" + pull.head_repo_name base_repo = pull.base_repo_owner_login + "/" + pull.base_repo_name head_branch = pull.head_ref base_branch = pull.base_ref git = utils.Gitter() try: git("init") git.configure() git.add_cred(token, "", head_repo) git.add_cred(token, "", base_repo) git( "remote", "add", "origin", "https://%s/%s" % (config.GITHUB_DOMAIN, head_repo), ) git( "remote", "add", "upstream", "https://%s/%s" % (config.GITHUB_DOMAIN, base_repo), ) depth = len(pull.commits) + 1 git("fetch", "--quiet", "--depth=%d" % depth, "origin", head_branch) git("checkout", "-q", "-b", head_branch, "origin/%s" % head_branch) out = git("log", "--format=%cI") last_commit_date = [ d for d in out.decode("utf8").split("\n") if d.strip() ][-1] git( "fetch", "--quiet", "upstream", base_branch, "--shallow-since='%s'" % last_commit_date, ) try: _do_update_branch(git, method, base_branch, head_branch) except subprocess.CalledProcessError as e: # pragma: no cover for message in GIT_MESSAGE_TO_UNSHALLOW: if message in e.output: pull.log.debug("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 git("fetch", "--unshallow") git("fetch", "--quiet", "origin", head_branch) git("fetch", "--quiet", "upstream", base_branch) _do_update_branch(git, method, base_branch, head_branch) break else: raise expected_sha = git("log", "-1", "--format=%H").decode().strip() # NOTE(sileht): We store this for dismissal action redis = utils.get_redis_for_cache() redis.setex("branch-update-%s" % expected_sha, 60 * 60, expected_sha) except subprocess.CalledProcessError as in_exception: # pragma: no cover for message, out_exception in GIT_MESSAGE_TO_EXCEPTION.items(): if message in in_exception.output: raise out_exception(in_exception.output.decode()) else: pull.log.error( "update branch failed: %s", in_exception.output.decode(), exc_info=True, ) raise BranchUpdateFailure() except Exception: # pragma: no cover pull.log.error("update branch failed", pull_request=pull, exc_info=True) raise BranchUpdateFailure() finally: git.cleanup()
def backport(pull, branch, installation_token): """Backport a pull request. :param repo: The repository. :param pull: The pull request. :type pull: py:class:mergify_engine.mergify_pull.MergifyPull :param branch_name: The branch name to backport to. :param installation_token: The installation token. """ repo = pull.g_pull.base.repo bp_branch = "mergify/bp/%s/pr-%s" % (branch.name, pull.g_pull.number) cherry_pick_fail = False body = ("This is an automated backport of pull request #%d done " "by Mergify.io" % pull.g_pull.number) git = utils.Gitter() # 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 try: git("init") git.configure() git.add_cred("x-access-token", installation_token, repo.full_name) git("remote", "add", "origin", "https://%s/%s" % (config.GITHUB_DOMAIN, repo.full_name)) git("fetch", "--quiet", "origin", "pull/%s/head" % pull.g_pull.number) git("fetch", "--quiet", "origin", pull.g_pull.base.ref) git("fetch", "--quiet", "origin", branch.name) git("checkout", "--quiet", "-b", bp_branch, "origin/%s" % branch.name) merge_commit = repo.get_commit(pull.g_pull.merge_commit_sha) for commit in _get_commits_to_cherrypick(pull, 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", pull.base.ref, # "--shallow-since='%s'" % last_commit_date) try: git("cherry-pick", "-x", commit.sha) except subprocess.CalledProcessError: cherry_pick_fail = True status = git("status").decode("utf8") git("add", "*") git("commit", "-a", "--no-edit", "--allow-empty") body += ("\n\nCherry-pick of %s have failed:\n```\n%s```\n\n" % (commit.sha, status)) git("push", "origin", bp_branch) except Exception: # pragma: no cover LOG.error("backport failed", pull_request=pull, branch=branch.name, exc_info=True) return finally: git.cleanup() 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/") return repo.create_pull( title="Automatic backport of pull request #%d" % pull.g_pull.number, body=body, base=branch.name, head=bp_branch, )
def _backport(repo, pull, branch_name, installation_token): try: branch = repo.get_branch(branch_name) except github.GithubException as e: # NOTE(sileht): PyGitHub is buggy here it should # UnknownObjectException. but because the message is "Branch not # found", instead of "Not found", we got the generic exception. if e.status != 404: # pragma: no cover raise LOG.info("%s doesn't exist for repo %s", branch_name, repo.full_name) return bp_branch = "mergify/bp/%s/pr-%s" % (branch_name, pull.number) cherry_pick_fail = False body = ("This is an automated backport of pull request #%d done " "by Mergify.io" % pull.number) git = utils.Gitter() # 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 try: git( "clone", "-b", branch_name, "https://*****:*****@github.com/%s/" % (installation_token, repo.full_name), ".") git("branch", "-M", bp_branch) git("config", "user.name", "%s-bot" % config.CONTEXT) git("config", "user.email", config.GIT_EMAIL) merge_commit = repo.get_commit(pull.merge_commit_sha) for commit in _get_commits_to_cherrypick(pull, 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", pull.base.ref, # "--shallow-since='%s'" % last_commit_date) try: git("cherry-pick", "-x", commit.sha) except subprocess.CalledProcessError: cherry_pick_fail = True status = git("status").decode("utf8") git("add", "*") git("commit", "-a", "--no-edit") body += ("\n\nCherry-pick of %s have failed:\n```\n%s```\n\n" % (commit.sha, status)) git("push", "origin", bp_branch) except Exception: LOG.error("%s: backport to branch %s fail", pull.pretty(), branch.name, exc_info=True) return finally: git.cleanup() 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/") repo.create_pull( title="Automatic backport of pull request #%d" % pull.number, body=body, base=branch.name, head=bp_branch, )