Example #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):
        logging.info(subprocess.check_output(args))

    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.ssh_url = self._gh.ssh_url

        # 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', 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
        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', repo_path, '-c',
            '[email protected]', '-c',
            'user.name=robot-clickhouse'
        ]

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

        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', self.target_branch, self.backport_branch])
        ])
        self._run(git_prefix + ['commit', '-a', '-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=
            '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

    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
Example #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 _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.ssh_url = self._gh.ssh_url

        # 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",
            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
        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",
            repo_path,
            "-c",
            "[email protected]",
            "-c",
            "user.name=robot-clickhouse",
        ]

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

        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=
            "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

    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