Beispiel #1
0
    def request(self, method: str, credentials: Dict, url: str, json: Dict):
        try:
            response = requests.request(
                method,
                url,
                headers={
                    "Authorization": "token {}".format(credentials["token"]),
                },
                json=json,
            )
        except OSError as error:
            report_error(cause="request")
            raise RepositoryException(0, str(error))
        self.add_response_breadcrumb(response)
        try:
            data = response.json()
        except JSONDecodeError as error:
            report_error(cause="request json decoding")
            response.raise_for_status()
            raise RepositoryException(0, str(error))

        # Log and parse all errors.
        error_message = ""
        if "message" in data:
            error_message = data["message"]
            self.log(data["message"], level=logging.INFO)

        return data, error_message
Beispiel #2
0
    def create_pull_request(self, credentials: Dict, origin_branch: str,
                            fork_remote: str, fork_branch: str):
        """Create pull request.

        Use to merge branch in forked repository into branch of remote repository.
        """
        if credentials["owner"]:
            pr_list_url = "{url}/{owner}/{slug}/pull-requests".format(
                **credentials)
            pr_create_url = "{url}/{owner}/{slug}/pull-request/new".format(
                **credentials)
        else:
            pr_list_url = "{url}/{slug}/pull-requests".format(**credentials)
            pr_create_url = "{url}/{slug}/pull-request/new".format(
                **credentials)

        # List existing pull requests
        response, error_message = self.request(
            "get",
            credentials,
            pr_list_url,
            params={"author": credentials["username"]})
        if error_message:
            raise RepositoryException(
                0, f"Pull request listing failed: {error_message}")

        if response["total_requests"] > 0:
            # Open pull request from us is already there
            return

        title, description = self.get_merge_message()
        request = {
            "branch_from": fork_branch,
            "branch_to": origin_branch,
            "title": title,
            "initial_comment": description,
        }
        if fork_remote != "origin":
            request["repo_from"] = credentials["slug"]
            request["repo_from_username"] = credentials["username"]

        response, error_message = self.request("post",
                                               credentials,
                                               pr_create_url,
                                               data=request)

        if "id" not in response:
            raise RepositoryException(0,
                                      f"Pull request failed: {error_message}")
Beispiel #3
0
    def disable_fork_features(self, forked_url):
        """Disable features in fork.

        Gitlab initializes a lot of the features in the fork
        that are not desirable, such as merge requests, issues, etc.
        This function is intended to disable all such features by
        editing the forked repo.
        """
        access_level_dict = {
            "issues_access_level": "disabled",
            "forking_access_level": "disabled",
            "builds_access_level": "disabled",
            "wiki_access_level": "disabled",
            "snippets_access_level": "disabled",
            "pages_access_level": "disabled",
        }
        r = requests.put(
            forked_url,
            headers={
                "Authorization": "Bearer {}".format(settings.GITLAB_TOKEN)
            },
            json=access_level_dict,
        )
        if "web_url" not in r.json():
            raise RepositoryException(0, r.json()["error"])
Beispiel #4
0
    def create_pull_request(self, credentials: Dict, origin_branch: str,
                            fork_remote: str, fork_branch: str):
        """Create pull request.

        Use to merge branch in forked repository into branch of remote repository.
        """
        target_project_id = None
        pr_url = "{}/merge_requests".format(credentials["url"])
        if fork_remote != "origin":
            # Gitlab MR works a little different from Github. The MR needs
            # to be sent with the fork's API URL along with a parameter mentioning
            # the target project id
            target_project_id = self.get_target_project_id(credentials)
            pr_url = "{}/merge_requests".format(
                self.get_forked_url(credentials))

        title, description = self.get_merge_message()
        request = {
            "source_branch": fork_branch,
            "target_branch": origin_branch,
            "title": title,
            "description": description,
            "target_project_id": target_project_id,
        }
        response, error = self.request("post", credentials, pr_url, request)

        if ("web_url" not in response
                and "open merge request already exists" not in error):
            raise RepositoryException(-1, error
                                      or "Failed to create pull request")
Beispiel #5
0
    def get_credentials(self) -> Dict:
        url, owner, slug = self.get_api_url()
        hostname = urllib.parse.urlparse(url).hostname.lower()

        credentials = getattr(settings,
                              f"{self.identifier.upper()}_CREDENTIALS")
        if hostname in credentials:
            username = credentials[hostname]["username"]
            token = credentials[hostname]["token"]
        else:
            username = getattr(settings, f"{self.identifier.upper()}_USERNAME")
            token = getattr(settings, f"{self.identifier.upper()}_TOKEN")
            if not username or not token:
                raise RepositoryException(
                    0,
                    f"{self.name} API access for {hostname} is not configured")

        return {
            "url": url,
            "owner": owner,
            "slug": slug,
            "hostname": hostname,
            "username": username,
            "token": token,
        }
Beispiel #6
0
    def create_fork(self, credentials: Dict):
        fork_url = "{}/fork".format(credentials["url"])

        base_params = {
            "repo": credentials["slug"],
            "wait": True,
        }

        if credentials["owner"]:
            # We have no information whether the URL part is namespace
            # or username, try both
            params = [
                {
                    "namespace": credentials["owner"]
                },
                {
                    "username": credentials["owner"]
                },
            ]
        else:
            params = [{}]

        for param in params:
            param.update(base_params)
            response, error = self.request("post", credentials, fork_url,
                                           param)
            if '" cloned to "' in error or "already exists" in error:
                break

        if '" cloned to "' not in error and "already exists" not in error:
            raise RepositoryException(0, error or "Failed to create fork")

        url = "ssh://git@{hostname}/forks/{username}/{slug}.git".format(
            **credentials)
        self.configure_fork_remote(url, credentials["username"])
Beispiel #7
0
    def create_pull_request(self, credentials: Dict, origin_branch: str,
                            fork_remote: str, fork_branch: str):
        """Create pull request.

        Use to merge branch in forked repository into branch of remote repository.
        """
        if fork_remote == "origin":
            head = fork_branch
        else:
            head = "{0}:{1}".format(fork_remote, fork_branch)
        pr_url = "{}/pulls".format(credentials["url"])
        title, description = self.get_merge_message()
        request = {
            "head": head,
            "base": origin_branch,
            "title": title,
            "body": description,
        }
        response, error_message = self.request("post", credentials, pr_url,
                                               request)

        # Check for an error. If the error has a message saying A pull request already
        # exists, then we ignore that, else raise an error. Currently, since the API
        # doesn't return any other separate indication for a pull request existing
        # compared to other errors, checking message seems to be the only option
        if "url" not in response:
            # Gracefully handle pull request already exists or nothing to merge cases
            if ("A pull request already exists" in error_message
                    or "No commits between " in error_message):
                return

            raise RepositoryException(0, error_message
                                      or "Pull request failed")
Beispiel #8
0
    def create_pull_request(self, credentials: Dict, origin_branch: str,
                            fork_remote: str, fork_branch: str):
        """Create pull request.

        Use to merge branch in forked repository into branch of remote repository.
        """
        if fork_remote == "origin":
            if credentials["owner"]:
                pr_url = "{url}/{owner}/{slug}/pull-request/new".format(
                    **credentials)
            else:
                pr_url = "{url}/{slug}/pull-request/new".format(**credentials)
        else:
            pr_url = "{url}/fork/{username}/{slug}/pull-request/new".format(
                **credentials)
        title, description = self.get_merge_message()
        request = {
            "branch_from": fork_branch,
            "branch_to": origin_branch,
            "title": title,
            "initial_comment": description,
        }
        response, error_message = self.request("post", credentials, pr_url,
                                               request)

        if "id" not in response:
            raise RepositoryException(0, error_message
                                      or "Pull request failed")
Beispiel #9
0
    def request(self, method: str, credentials: Dict, url: str, json: Dict):
        response = requests.request(
            method,
            url,
            headers={
                "Accept": "application/vnd.github.v3+json",
                "Authorization": "token {}".format(credentials["token"]),
            },
            json=json,
        )
        try:
            data = response.json()
        except JSONDecodeError as error:
            response.raise_for_status()
            raise RepositoryException(0, str(error))

        # Log and parase all errors. Sometimes GitHub returns the error
        # messages in an errors list instead of the message. Sometimes, there
        # is no errors list. Hence the different logics
        error_message = ""
        if "message" in data:
            error_message = data["message"]
            self.log(data["message"], level=logging.INFO)
        if "errors" in data:
            messages = []
            for error in data["errors"]:
                line = error.get("message", str(error))
                messages.append(line)
                self.log(line, level=logging.WARNING)
            if error_message:
                error_message += ": "
            error_message += ", ".join(messages)

        return data, error_message
Beispiel #10
0
    def create_pull_request(
        self, credentials: Dict, origin_branch: str, fork_remote: str, fork_branch: str
    ):
        """Create pull request.

        Use to merge branch in forked repository into branch of remote repository.
        """
        if fork_remote == "origin":
            head = fork_branch
        else:
            head = "{0}:{1}".format(fork_remote, fork_branch)
        pr_url = "{}/pulls".format(credentials["url"])
        title, description = self.get_merge_message()
        request = {
            "head": head,
            "base": origin_branch,
            "title": title,
            "body": description,
        }
        response = requests.post(
            pr_url,
            headers={
                "Accept": "application/vnd.github.v3+json",
                "Authorization": "token {}".format(credentials["token"]),
            },
            json=request,
        ).json()

        # Log all errors
        if "message" in response:
            self.log(response["message"], level=logging.INFO)
        if "errors" in response:
            for error in response["errors"]:
                self.log(error.get("message", str(error)), level=logging.WARNING)

        # Check for an error. If the error has a message saying A pull request already
        # exists, then we ignore that, else raise an error. Currently, since the API
        # doesn't return any other separate indication for a pull request existing
        # compared to other errors, checking message seems to be the only option
        if "url" not in response:
            # Gracefully handle pull request already exists case
            if (
                "errors" in response
                and "A pull request already exists" in response["errors"][0]["message"]
            ):
                return

            # Sometimes GitHub returns the error messages in an errors list
            # instead of the message. Sometimes, there is no errors list.
            # Hence the different logics
            error_message = "Pull request failed"
            if "errors" in response:
                error_message = "{}: {}".format(
                    response["message"], response["errors"][0]["message"]
                )
            elif "message" in response:
                error_message = response["message"]
            raise RepositoryException(0, error_message)
Beispiel #11
0
    def create_fork(self, credentials: Dict):
        fork_url = "{}/forks".format(credentials["url"])

        # GitHub API returns the entire data of the fork, in case the fork
        # already exists. Hence this is perfectly handled, if the fork already
        # exists in the remote side.
        response, error = self.request("post", credentials, fork_url, {})
        if "ssh_url" not in response:
            raise RepositoryException(0, f"Fork creation failed: {error}")
        self.configure_fork_remote(response["ssh_url"], credentials["username"])
Beispiel #12
0
    def create_fork(self, credentials: Dict):
        fork_url = "{}/forks".format(credentials["url"])

        response, error = self.request("post", credentials, fork_url, {})
        if "message" in response and "repository is already forked by user" in error:
            # we have to get the repository again if it is already forked
            response, error = self.request("get", credentials, credentials["url"], {})
        if "ssh_url" not in response:
            raise RepositoryException(0, f"Fork creation failed: {error}")
        self.configure_fork_remote(response["ssh_url"], credentials["username"])
Beispiel #13
0
    def get_remote_branch(cls, repo: str):
        if not repo:
            return super().get_remote_branch(repo)
        result = cls._popen(["ls-remote", "--symref", repo, "HEAD"])
        for line in result.splitlines():
            if not line.startswith("ref: "):
                continue
            # Parses 'ref: refs/heads/master\tHEAD'
            return line.split("\t")[0].split("refs/heads/")[1]

        raise RepositoryException(0, "Failed to figure out remote branch")
Beispiel #14
0
    def create_pull_request(
        self,
        credentials: Dict,
        origin_branch: str,
        fork_remote: str,
        fork_branch: str,
        retry_fork: bool = True,
    ):
        """Create pull request.

        Use to merge branch in forked repository into branch of remote repository.
        """
        if fork_remote == "origin":
            head = fork_branch
        else:
            head = f"{fork_remote}:{fork_branch}"
        pr_url = "{}/pulls".format(credentials["url"])
        title, description = self.get_merge_message()
        request = {
            "head": head,
            "base": origin_branch,
            "title": title,
            "body": description,
        }
        response, error_message = self.request("post", credentials, pr_url, request)

        # Check for an error. If the error has a message saying A pull request already
        # exists, then we ignore that, else raise an error. Currently, since the API
        # doesn't return any other separate indication for a pull request existing
        # compared to other errors, checking message seems to be the only option
        if "url" not in response:
            # Gracefully handle pull request already exists or nothing to merge cases
            if (
                "A pull request already exists" in error_message
                or "No commits between " in error_message
            ):
                return

            if "Validation Failed" in error_message:
                for error in response["errors"]:
                    if error.get("field") == "head" and retry_fork:
                        # This most likely indicates that Weblate repository has moved
                        # and we should create a fresh fork.
                        self.create_fork(credentials)
                        self.create_pull_request(
                            credentials,
                            origin_branch,
                            fork_remote,
                            fork_branch,
                            retry_fork=False,
                        )
                        return

            raise RepositoryException(0, f"Pull request failed: {error_message}")
Beispiel #15
0
    def create_pull_request(
        self, credentials: Dict, origin_branch: str, fork_remote: str, fork_branch: str
    ):
        """Create pull request.

        Use to merge branch in forked repository into branch of remote repository.
        """
        target_project_id = None
        pr_url = "{}/merge_requests".format(credentials["url"])
        if fork_remote != "origin":
            # Gitlab MR works a little different from Github. The MR needs
            # to be sent with the fork's API URL along with a parameter mentioning
            # the target project id
            target_project_id = self.get_target_project_id(credentials)
            pr_url = "{}/merge_requests".format(self.get_forked_url(credentials))

        title, description = self.get_merge_message()
        request = {
            "source_branch": fork_branch,
            "target_branch": origin_branch,
            "title": title,
            "description": description,
            "target_project_id": target_project_id,
        }
        response = requests.post(
            pr_url,
            headers={"Authorization": "Bearer {}".format(credentials["token"])},
            json=request,
        ).json()

        # Extract messages
        messages = response.get("message", [])
        if not isinstance(messages, list):
            messages = [messages]

        # Log messages
        for message in messages:
            self.log(message, level=logging.INFO)

        if (
            "web_url" not in response
            and "open merge request already exists" not in messages[0]
        ):
            raise RepositoryException(-1, ", ".join(messages))
Beispiel #16
0
    def configure_remote(self, pull_url, push_url, branch):
        """Initialize the git-svn repository.

        This does not support switching remote as it's quite complex:
        https://git.wiki.kernel.org/index.php/GitSvnSwitch

        The git svn init errors in case the URL is not matching.
        """
        try:
            existing = self.get_config("svn-remote.svn.url")
        except RepositoryException:
            existing = None
        if existing:
            # The URL is root of the repository, while we get full path
            if not pull_url.startswith(existing):
                raise RepositoryException(-1, "Can not switch subversion URL")
            return
        args, self._fetch_revision = self.get_remote_args(pull_url, self.path)
        self.execute(["svn", "init"] + args)
Beispiel #17
0
    def disable_fork_features(self, credentials: Dict, forked_url: str):
        """Disable features in fork.

        Gitlab initializes a lot of the features in the fork
        that are not desirable, such as merge requests, issues, etc.
        This function is intended to disable all such features by
        editing the forked repo.
        """
        access_level_dict = {
            "issues_access_level": "disabled",
            "forking_access_level": "disabled",
            "builds_access_level": "disabled",
            "wiki_access_level": "disabled",
            "snippets_access_level": "disabled",
            "pages_access_level": "disabled",
        }
        response, error = self.request("put", credentials, forked_url,
                                       access_level_dict)
        if "web_url" not in response:
            raise RepositoryException(0, error or "Failed to modify fork")
Beispiel #18
0
    def create_fork(self, credentials: Dict):
        get_fork_url = "{}/forks?owned=True".format(credentials["url"])
        fork_url = "{}/fork".format(credentials["url"])
        forked_repo = None

        # Check if Fork already exists owned by current user. If the
        # fork already exists, set that fork as remote.
        # Else, create a new fork
        response, error = self.request("get", credentials, get_fork_url)
        for fork in response:
            # Since owned=True returns forks from both the user's repo and the forks
            # in all the groups owned by the user, hence we need the below logic
            # to find the fork within the user repo and not the groups
            if "owner" in fork and fork["owner"]["username"] == credentials[
                    "username"]:
                forked_repo = fork

        if forked_repo is None:
            forked_repo, error = self.request("post", credentials, fork_url)
            # If a repo with the name of the fork already exist, append numeric
            # as suffix to name and path to use that as repo name and path.
            if "ssh_url_to_repo" not in response and "has already been taken" in error:
                fork_name = "{}-{}".format(credentials["url"].split("%2F")[-1],
                                           random.randint(1000, 9999))
                forked_repo, error = self.request(
                    "post",
                    credentials,
                    fork_url,
                    {
                        "name": fork_name,
                        "path": fork_name
                    },
                )

            if "ssh_url_to_repo" not in forked_repo:
                raise RepositoryException(0, f"Failed to create fork: {error}")

        self.configure_fork_features(credentials,
                                     forked_repo["_links"]["self"])
        self.configure_fork_remote(forked_repo["ssh_url_to_repo"],
                                   credentials["username"])
Beispiel #19
0
    def create_pull_request(self, origin_branch, fork_remote, fork_branch):
        """Create pull request.

        Use to merge branch in forked repository into branch of remote repository.
        """
        target_project_id = None
        pr_url = "{}/merge_requests".format(self.api_url())
        if fork_remote != "origin":
            # Gitlab MR works a little different from Github. The MR needs
            # to be sent with the fork's API URL along with a parameter mentioning
            # the target project id
            target_project_id = self.get_target_project_id()
            pr_url = "{}/merge_requests".format(self.get_forked_url())

        title, description = self.get_merge_message()
        r = requests.post(
            pr_url,
            headers={
                "Authorization": "Bearer {}".format(settings.GITLAB_TOKEN)
            },
            json={
                "source_branch": fork_branch,
                "target_branch": origin_branch,
                "title": title,
                "description": description,
                "target_project_id": target_project_id,
            },
        )
        response = r.json()

        # Log messages
        if "message" in response and isinstance(response["message"], list):
            for message in response["message"]:
                self.log(message, level=logging.INFO)

        if "web_url" not in response and (not isinstance(
                response["message"], list) or response["message"][0].find(
                    "open merge request already exists") == -1):
            raise RepositoryException(-1, ", ".join(response["message"]))
Beispiel #20
0
 def get_target_project_id(self, credentials: Dict):
     response, error = self.request("get", credentials, credentials["url"])
     if "id" not in response:
         raise RepositoryException(0, error or "Failed to get project")
     return response["id"]