def remote(self) -> str: if not self._remote: # lines of "origin [email protected]:ClickHouse/ClickHouse.git (fetch)" remotes = git_runner("git remote -v").split("\n") # We need the first word from the first matching result self._remote = tuple( remote.split(maxsplit=1)[0] for remote in remotes if f"github.com/{self._repo_name}" in remote # https or f"github.com:{self._repo_name}" in remote # ssh )[0] git_runner(f"git fetch {self._remote}") ReleaseBranch.REMOTE = self._remote return self._remote
def commit_push_staged(pr_info: PRInfo): # It works ONLY for PRs, and only over ssh, so either # ROBOT_CLICKHOUSE_SSH_KEY should be set or ssh-agent should work assert pr_info.number if not pr_info.head_name == pr_info.base_name: # We can't push to forks, sorry folks return git_staged = git_runner("git diff --cached --name-only") if not git_staged: return remote_url = pr_info.event["pull_request"]["base"]["repo"]["ssh_url"] git_prefix = ( # All commits to remote are done as robot-clickhouse "git -c [email protected] " "-c user.name=robot-clickhouse -c commit.gpgsign=false " "-c core.sshCommand=" "'ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'" ) git_runner(f"{git_prefix} commit -m 'Automatic style fix'") push_cmd = ( f"{git_prefix} push {remote_url} head-{pr_info.head_ref}:{pr_info.head_ref}" ) if os.getenv("ROBOT_CLICKHOUSE_SSH_KEY", ""): with SSHKey("ROBOT_CLICKHOUSE_SSH_KEY"): git_runner(push_cmd) else: git_runner(push_cmd)
def clear_repo(): orig_ref = git_runner("git branch --show-current") or git_runner( "git rev-parse HEAD") try: yield except (Exception, KeyboardInterrupt): git_runner(f"git checkout -f {orig_ref}") raise else: git_runner(f"git checkout -f {orig_ref}")
def stash(): need_stash = bool(git_runner("git diff HEAD")) if need_stash: git_runner( "git stash push --no-keep-index -m 'running cherry_pick.py'") try: with clear_repo(): yield except (Exception, KeyboardInterrupt): if need_stash: git_runner("git stash pop") raise else: if need_stash: git_runner("git stash pop")
def receive_prs_for_backport(self): # The commit is the oldest open release branch's merge-base since_commit = git_runner( f"git merge-base {self.remote}/{self.release_branches[0]} " f"{self.remote}/{self.default_branch}") since_date = date.fromisoformat( git_runner.run(f"git log -1 --format=format:%cs {since_commit}")) # To not have a possible TZ issues tomorrow = date.today() + timedelta(days=1) logging.info("Receive PRs suppose to be backported") self.prs_for_backport = self.gh.get_pulls_from_search( query=f"{self._query} -label:pr-backported", label=",".join(self.labels_to_backport + [Labels.LABEL_MUST_BACKPORT]), merged=[since_date, tomorrow], ) logging.info( "PRs to be backported:\n %s", "\n ".join([pr.html_url for pr in self.prs_for_backport]), )
def pre_check(self): branch_updated = git_runner( f"git branch -a --contains={self.pr.merge_commit_sha} " f"{self.REMOTE}/{self.name}") if branch_updated: self._backported = True
def create_backport(self): # Checkout the backport branch from the remote and make all changes to # apply like they are only one cherry-pick commit on top of release git_runner(f"{self.git_prefix} checkout -f {self.backport_branch}") git_runner( f"{self.git_prefix} pull --ff-only {self.REMOTE} {self.backport_branch}" ) merge_base = git_runner( f"{self.git_prefix} merge-base " f"{self.REMOTE}/{self.name} {self.backport_branch}") git_runner(f"{self.git_prefix} reset --soft {merge_base}") title = f"Backport #{self.pr.number} to {self.name}: {self.pr.title}" git_runner(f"{self.git_prefix} commit -a --allow-empty -F -", input=title) # Push with force, create the backport PR, lable and assign it git_runner(f"{self.git_prefix} push -f {self.REMOTE} " f"{self.backport_branch}:{self.backport_branch}") self.backport_pr = self.pr.base.repo.create_pull( title=title, body=f"Original pull-request #{self.pr.number}\n" f"Cherry-pick pull-request #{self.cherrypick_pr.number}\n\n" f"{self.BACKPORT_DESCRIPTION}", base=self.name, head=self.backport_branch, ) self.backport_pr.add_to_labels(Labels.LABEL_BACKPORT) if self.pr.assignee is not None: self.cherrypick_pr.add_to_assignees(self.pr.assignee) self.backport_pr.add_to_assignees(self.pr.user)
def create_cherrypick(self): # First, create backport branch: # Checkout release branch with discarding every change git_runner(f"{self.git_prefix} checkout -f {self.name}") # Create or reset backport branch git_runner(f"{self.git_prefix} checkout -B {self.backport_branch}") # Merge all changes from PR's the first parent commit w/o applying anything # It will allow to create a merge commit like it would be a cherry-pick first_parent = git_runner( f"git rev-parse {self.pr.merge_commit_sha}^1") git_runner(f"{self.git_prefix} merge -s ours --no-edit {first_parent}") # Second step, create cherrypick branch git_runner(f"{self.git_prefix} branch -f " f"{self.cherrypick_branch} {self.pr.merge_commit_sha}") # Check if there actually any changes between branches. If no, then no # other actions are required. It's possible when changes are backported # manually to the release branch already try: output = git_runner( f"{self.git_prefix} merge --no-commit --no-ff {self.cherrypick_branch}" ) # 'up-to-date', 'up to date', who knows what else (╯°v°)╯ ^┻━┻ if output.startswith("Already up") and output.endswith("date."): # The changes are already in the release branch, we are done here logging.info( "Release branch %s already contain changes from %s", self.name, self.pr.number, ) self._backported = True return except CalledProcessError: # There are most probably conflicts, they'll be resolved in PR git_runner(f"{self.git_prefix} reset --merge") else: # There are changes to apply, so continue git_runner(f"{self.git_prefix} reset --merge") # Push, create the cherrypick PR, lable and assign it for branch in [self.cherrypick_branch, self.backport_branch]: git_runner( f"{self.git_prefix} push -f {self.REMOTE} {branch}:{branch}") self.cherrypick_pr = self.pr.base.repo.create_pull( title= f"Cherry pick #{self.pr.number} to {self.name}: {self.pr.title}", body=f"Original pull-request #{self.pr.number}\n\n" f"{self.CHERRYPICK_DESCRIPTION}", base=self.backport_branch, head=self.cherrypick_branch, ) self.cherrypick_pr.add_to_labels(Labels.LABEL_CHERRYPICK) self.cherrypick_pr.add_to_labels(Labels.LABEL_DO_NOT_TEST) if self.pr.assignee is not None: self.cherrypick_pr.add_to_assignees(self.pr.assignee) self.cherrypick_pr.add_to_assignees(self.pr.user)