# Copyright 2023 The Servo Project Developers. See the COPYRIGHT # file at the top-level directory of this distribution. # # Licensed under the Apache License, Version 2.0 or the MIT license # , at your # option. This file may not be copied, modified, or distributed # except according to those terms. # pylint: disable=broad-except # pylint: disable=dangerous-default-value # pylint: disable=fixme # pylint: disable=missing-docstring # This allows using types that are defined later in the file. from __future__ import annotations import dataclasses import json import logging import re import shutil import subprocess from typing import Callable, Optional from .common import \ CLOSING_EXISTING_UPSTREAM_PR, \ NO_SYNC_SIGNAL, \ NO_UPSTREAMBLE_CHANGES_COMMENT, \ OPENED_NEW_UPSTREAM_PR, \ UPDATED_EXISTING_UPSTREAM_PR, \ UPDATED_TITLE_IN_EXISTING_UPSTREAM_PR, \ UPSTREAMABLE_PATH, \ wpt_branch_name_from_servo_pr_number from .github import GithubRepository, PullRequest from .step import \ AsyncValue, \ ChangePRStep, \ CommentStep, \ CreateOrUpdateBranchForPRStep, \ MergePRStep, \ OpenPRStep, \ RemoveBranchForPRStep, \ Step class LocalGitRepo: def __init__(self, path: str, sync: WPTSync): self.path = path self.sync = sync # We pass env to subprocess, which may clobber PATH, so we need to find # git in advance and run the subprocess by its absolute path. self.git_path = shutil.which("git") def run_without_encoding(self, *args, env: dict = {}): command_line = [self.git_path] + list(args) logging.info(" → Execution (cwd='%s'): %s", self.path, " ".join(command_line)) env.setdefault("GIT_AUTHOR_EMAIL", self.sync.github_email) env.setdefault("GIT_COMMITTER_EMAIL", self.sync.github_email) env.setdefault("GIT_AUTHOR_NAME", self.sync.github_name) env.setdefault("GIT_COMMITTER_NAME", self.sync.github_name) try: return subprocess.check_output( command_line, cwd=self.path, env=env, stderr=subprocess.STDOUT ) except subprocess.CalledProcessError as exception: logging.warning("Process execution failed with output:\n%s", exception.output.decode("utf-8", errors="surrogateescape")) raise exception def run(self, *args, env: dict = {}): return ( self .run_without_encoding(*args, env=env) .decode("utf-8", errors="surrogateescape") ) @dataclasses.dataclass() class SyncRun: sync: WPTSync servo_pr: PullRequest upstream_pr: AsyncValue[PullRequest] step_callback: Optional[Callable[[Step], None]] steps: list[Step] = dataclasses.field(default_factory=list) def make_comment(self, template: str) -> str: upstream_pr = self.upstream_pr.value() if self.upstream_pr.has_value() else "" return template.format( upstream_pr=upstream_pr, servo_pr=self.servo_pr, ) def add_step(self, step) -> Optional[AsyncValue]: self.steps.append(step) return step.provides() def run(self): # This loop always removes the first step and runs it, because # individual steps can modify the list of steps. For instance, if a # step fails, it might clear the remaining steps and replace them with # steps that report the error to GitHub. while self.steps: step = self.steps.pop(0) step.run(self) if self.step_callback: self.step_callback(step) @staticmethod def clean_up_body_text(body: str) -> str: # Turn all bare or relative issue references into unlinked ones, so that # the PR doesn't inadvertently close or link to issues in the upstream # repository. return ( re.sub( r"(^|\s)(\w*)#([1-9]\d*)", r"\g<1>\g<2>#\g<3>", body, flags=re.MULTILINE, ) .split("\n---")[0] .split("