Exemple #1
0
class CherryPick:
    class Status(Enum):
        DISCARDED = "discarded"
        NOT_INITIATED = "not started"
        FIRST_MERGEABLE = "waiting for 1st stage"
        FIRST_CONFLICTS = "conflicts on 1st stage"
        SECOND_MERGEABLE = "waiting for 2nd stage"
        SECOND_CONFLICTS = "conflicts on 2nd stage"
        MERGED = "backported"

    def _run(self, args):
        out = subprocess.check_output(args).rstrip()
        logging.debug(out)
        return out

    def __init__(self, token, owner, name, team, pr_number, target_branch):
        self._gh = RemoteRepo(token, owner=owner, name=name, team=team)
        self._pr = self._gh.get_pull_request(pr_number)
        self.target_branch = target_branch

        self.ssh_url = self._gh.ssh_url

        # TODO: check if pull-request is merged.
        self.update_pr_branch(self._pr, self.target_branch)

    def update_pr_branch(self, pr_data, target_branch):
        """The method is here to avoid unnecessary creation of new objects"""
        self._pr = pr_data
        self.target_branch = target_branch
        self.merge_commit_oid = self._pr["mergeCommit"]["oid"]

        self.backport_branch = f"backport/{target_branch}/{pr_data['number']}"
        self.cherrypick_branch = f"cherrypick/{target_branch}/{self.merge_commit_oid}"

    def getCherryPickPullRequest(self):
        return self._gh.find_pull_request(base=self.backport_branch,
                                          head=self.cherrypick_branch)

    def createCherryPickPullRequest(self, repo_path):
        DESCRIPTION = (
            "This pull-request is a first step of an automated backporting.\n"
            "It contains changes like after calling a local command `git cherry-pick`.\n"
            "If you intend to continue backporting this changes, then resolve all conflicts if any.\n"
            "Otherwise, if you do not want to backport them, then just close this pull-request.\n"
            "\n"
            "The check results does not matter at this step - you can safely ignore them.\n"
            "Also this pull-request will be merged automatically as it reaches the mergeable state, but you always can merge it manually.\n"
        )

        # FIXME: replace with something better than os.system()
        git_prefix = [
            "git",
            "-C",
            repo_path,
            "-c",
            "[email protected]",
            "-c",
            "user.name=robot-clickhouse",
        ]
        base_commit_oid = self._pr["mergeCommit"]["parents"]["nodes"][0]["oid"]

        # Create separate branch for backporting, and make it look like real cherry-pick.
        self._run(git_prefix + ["checkout", "-f", self.target_branch])
        self._run(git_prefix + ["checkout", "-B", self.backport_branch])
        self._run(git_prefix +
                  ["merge", "-s", "ours", "--no-edit", base_commit_oid])

        # Create secondary branch to allow pull request with cherry-picked commit.
        self._run(
            git_prefix +
            ["branch", "-f", self.cherrypick_branch, self.merge_commit_oid])

        self._run(git_prefix + [
            "push",
            "-f",
            "origin",
            "{branch}:{branch}".format(branch=self.backport_branch),
        ])
        self._run(git_prefix + [
            "push",
            "-f",
            "origin",
            "{branch}:{branch}".format(branch=self.cherrypick_branch),
        ])

        # Create pull-request like a local cherry-pick
        title = self._pr["title"].replace('"', r"\"")
        pr = self._gh.create_pull_request(
            source=self.cherrypick_branch,
            target=self.backport_branch,
            title=(f'Cherry pick #{self._pr["number"]} '
                   f"to {self.target_branch}: "
                   f"{title}"),
            description=
            f'Original pull-request #{self._pr["number"]}\n\n{DESCRIPTION}',
        )

        # FIXME: use `team` to leave a single eligible assignee.
        self._gh.add_assignee(pr, self._pr["author"])
        self._gh.add_assignee(pr, self._pr["mergedBy"])

        self._gh.set_label(pr, "do not test")
        self._gh.set_label(pr, "pr-cherrypick")

        return pr

    def mergeCherryPickPullRequest(self, cherrypick_pr):
        return self._gh.merge_pull_request(cherrypick_pr["id"])

    def getBackportPullRequest(self):
        return self._gh.find_pull_request(base=self.target_branch,
                                          head=self.backport_branch)

    def createBackportPullRequest(self, cherrypick_pr, repo_path):
        DESCRIPTION = (
            "This pull-request is a last step of an automated backporting.\n"
            "Treat it as a standard pull-request: look at the checks and resolve conflicts.\n"
            "Merge it only if you intend to backport changes to the target branch, otherwise just close it.\n"
        )

        git_prefix = [
            "git",
            "-C",
            repo_path,
            "-c",
            "[email protected]",
            "-c",
            "user.name=robot-clickhouse",
        ]

        title = self._pr["title"].replace('"', r"\"")
        pr_title = f"Backport #{self._pr['number']} to {self.target_branch}: {title}"

        self._run(git_prefix + ["checkout", "-f", self.backport_branch])
        self._run(git_prefix +
                  ["pull", "--ff-only", "origin", self.backport_branch])
        self._run(git_prefix + [
            "reset",
            "--soft",
            self._run(git_prefix + [
                "merge-base",
                "origin/" + self.target_branch,
                self.backport_branch,
            ]),
        ])
        self._run(git_prefix +
                  ["commit", "-a", "--allow-empty", "-m", pr_title])
        self._run(git_prefix + [
            "push",
            "-f",
            "origin",
            "{branch}:{branch}".format(branch=self.backport_branch),
        ])

        pr = self._gh.create_pull_request(
            source=self.backport_branch,
            target=self.target_branch,
            title=pr_title,
            description=f"Original pull-request #{self._pr['number']}\n"
            f"Cherry-pick pull-request #{cherrypick_pr['number']}\n\n{DESCRIPTION}",
        )

        # FIXME: use `team` to leave a single eligible assignee.
        self._gh.add_assignee(pr, self._pr["author"])
        self._gh.add_assignee(pr, self._pr["mergedBy"])

        self._gh.set_label(pr, "pr-backport")

        return pr

    def execute(self, repo_path, dry_run=False):
        pr1 = self.getCherryPickPullRequest()
        if not pr1:
            if not dry_run:
                pr1 = self.createCherryPickPullRequest(repo_path)
                logging.debug(
                    "Created PR with cherry-pick of %s to %s: %s",
                    self._pr["number"],
                    self.target_branch,
                    pr1["url"],
                )
            else:
                return CherryPick.Status.NOT_INITIATED
        else:
            logging.debug(
                "Found PR with cherry-pick of %s to %s: %s",
                self._pr["number"],
                self.target_branch,
                pr1["url"],
            )

        if not pr1["merged"] and pr1[
                "mergeable"] == "MERGEABLE" and not pr1["closed"]:
            if not dry_run:
                pr1 = self.mergeCherryPickPullRequest(pr1)
                logging.debug(
                    "Merged PR with cherry-pick of %s to %s: %s",
                    self._pr["number"],
                    self.target_branch,
                    pr1["url"],
                )

        if not pr1["merged"]:
            logging.debug(
                "Waiting for PR with cherry-pick of %s to %s: %s",
                self._pr["number"],
                self.target_branch,
                pr1["url"],
            )

            if pr1["closed"]:
                return CherryPick.Status.DISCARDED
            elif pr1["mergeable"] == "CONFLICTING":
                return CherryPick.Status.FIRST_CONFLICTS
            else:
                return CherryPick.Status.FIRST_MERGEABLE

        pr2 = self.getBackportPullRequest()
        if not pr2:
            if not dry_run:
                pr2 = self.createBackportPullRequest(pr1, repo_path)
                logging.debug(
                    "Created PR with backport of %s to %s: %s",
                    self._pr["number"],
                    self.target_branch,
                    pr2["url"],
                )
            else:
                return CherryPick.Status.FIRST_MERGEABLE
        else:
            logging.debug(
                "Found PR with backport of %s to %s: %s",
                self._pr["number"],
                self.target_branch,
                pr2["url"],
            )

        if pr2["merged"]:
            return CherryPick.Status.MERGED
        elif pr2["closed"]:
            return CherryPick.Status.DISCARDED
        elif pr2["mergeable"] == "CONFLICTING":
            return CherryPick.Status.SECOND_CONFLICTS
        else:
            return CherryPick.Status.SECOND_MERGEABLE
Exemple #2
0
class CherryPick:
    class Status(Enum):
        DISCARDED = 'discarded'
        NOT_INITIATED = 'not started'
        FIRST_MERGEABLE = 'waiting for 1st stage'
        FIRST_CONFLICTS = 'conflicts on 1st stage'
        SECOND_MERGEABLE = 'waiting for 2nd stage'
        SECOND_CONFLICTS = 'conflicts on 2nd stage'
        MERGED = 'backported'

    def __init__(self, token, owner, name, team, pr_number, target_branch):
        self._gh = RemoteRepo(token, owner=owner, name=name, team=team)
        self._pr = self._gh.get_pull_request(pr_number)

        # TODO: check if pull-request is merged.

        self.merge_commit_oid = self._pr['mergeCommit']['oid']

        self.target_branch = target_branch
        self.backport_branch = 'backport/{branch}/{pr}'.format(
            branch=target_branch, pr=pr_number)
        self.cherrypick_branch = 'cherrypick/{branch}/{oid}'.format(
            branch=target_branch, oid=self.merge_commit_oid)

    def getCherryPickPullRequest(self):
        return self._gh.find_pull_request(base=self.backport_branch,
                                          head=self.cherrypick_branch)

    def createCherryPickPullRequest(self, repo_path):
        DESCRIPTION = (
            'This pull-request is a first step of an automated backporting.\n'
            'It contains changes like after calling a local command `git cherry-pick`.\n'
            'If you intend to continue backporting this changes, then resolve all conflicts if any.\n'
            'Otherwise, if you do not want to backport them, then just close this pull-request.\n'
            '\n'
            'The check results does not matter at this step - you can safely ignore them.\n'
            'Also this pull-request will be merged automatically as it reaches the mergeable state, but you always can merge it manually.\n'
        )

        # FIXME: replace with something better than os.system()
        git_prefix = 'git -C {} -c "[email protected]" -c "user.name=robot-clickhouse" '.format(
            repo_path)
        base_commit_oid = self._pr['mergeCommit']['parents']['nodes'][0]['oid']

        # Create separate branch for backporting, and make it look like real cherry-pick.
        os.system(git_prefix + 'checkout -f ' + self.target_branch)
        os.system(git_prefix + 'checkout -B ' + self.backport_branch)
        os.system(git_prefix + 'merge -s ours --no-edit ' + base_commit_oid)

        # Create secondary branch to allow pull request with cherry-picked commit.
        os.system(git_prefix + 'branch -f {} {}'.format(
            self.cherrypick_branch, self.merge_commit_oid))

        os.system(git_prefix + 'push -f origin {branch}:{branch}'.format(
            branch=self.backport_branch))
        os.system(git_prefix + 'push -f origin {branch}:{branch}'.format(
            branch=self.cherrypick_branch))

        # Create pull-request like a local cherry-pick
        pr = self._gh.create_pull_request(
            source=self.cherrypick_branch,
            target=self.backport_branch,
            title='Cherry pick #{number} to {target}: {title}'.format(
                number=self._pr['number'],
                target=self.target_branch,
                title=self._pr['title'].replace('"', '\\"')),
            description='Original pull-request #{}\n\n{}'.format(
                self._pr['number'], DESCRIPTION))

        # FIXME: use `team` to leave a single eligible assignee.
        self._gh.add_assignee(pr, self._pr['author'])
        self._gh.add_assignee(pr, self._pr['mergedBy'])

        self._gh.set_label(pr, "do not test")
        self._gh.set_label(pr, "pr-cherrypick")

        return pr

    def mergeCherryPickPullRequest(self, cherrypick_pr):
        return self._gh.merge_pull_request(cherrypick_pr['id'])

    def getBackportPullRequest(self):
        return self._gh.find_pull_request(base=self.target_branch,
                                          head=self.backport_branch)

    def createBackportPullRequest(self, cherrypick_pr, repo_path):
        DESCRIPTION = (
            'This pull-request is a last step of an automated backporting.\n'
            'Treat it as a standard pull-request: look at the checks and resolve conflicts.\n'
            'Merge it only if you intend to backport changes to the target branch, otherwise just close it.\n'
        )

        git_prefix = 'git -C {} -c "[email protected]" -c "user.name=robot-clickhouse" '.format(
            repo_path)

        os.system(git_prefix + 'checkout -f ' + self.backport_branch)
        os.system(git_prefix + 'pull --ff-only origin ' + self.backport_branch)
        os.system(git_prefix +
                  'reset --soft `{git} merge-base {target} {backport}`'.format(
                      git=git_prefix,
                      target=self.target_branch,
                      backport=self.backport_branch))
        os.system(git_prefix + 'commit -a -m "Squash backport branch"')
        os.system(git_prefix + 'push -f origin {branch}:{branch}'.format(
            branch=self.backport_branch))

        pr = self._gh.create_pull_request(
            source=self.backport_branch,
            target=self.target_branch,
            title='Backport #{number} to {target}: {title}'.format(
                number=self._pr['number'],
                target=self.target_branch,
                title=self._pr['title'].replace('"', '\\"')),
            description=
            'Original pull-request #{}\nCherry-pick pull-request #{}\n\n{}'.
            format(self._pr['number'], cherrypick_pr['number'], DESCRIPTION))

        # FIXME: use `team` to leave a single eligible assignee.
        self._gh.add_assignee(pr, self._pr['author'])
        self._gh.add_assignee(pr, self._pr['mergedBy'])

        self._gh.set_label(pr, "pr-backport")

        return pr