utils.py 6.81 KB
Newer Older
Jonah Brüchert's avatar
Jonah Brüchert committed
1
2
3
4
"""
Module containing classes for common tasks
"""

Jonah Brüchert's avatar
Jonah Brüchert committed
5
6
7
# SPDX-FileCopyrightText: 2020 Jonah Brüchert <jbb@kaidan.im>
#
# SPDX-License-Identifier: GPL-2.0-or-later
Benjamin Port's avatar
Benjamin Port committed
8
import shutil
Benjamin Port's avatar
Benjamin Port committed
9
import subprocess
Jonah Brüchert's avatar
Jonah Brüchert committed
10

11
import sys
12
import os
Jonah Brüchert's avatar
Jonah Brüchert committed
13
import shlex
Jonah Brüchert's avatar
Jonah Brüchert committed
14
from typing import List, Optional, Final
15

Jonah Brüchert's avatar
Jonah Brüchert committed
16
from urllib.parse import ParseResult, urlparse, quote_plus
Jonah Brüchert's avatar
Jonah Brüchert committed
17
from enum import Enum, auto
Jonah Brüchert's avatar
Jonah Brüchert committed
18

19
20
21
from git import Repo
from git.exc import InvalidGitRepositoryError

22

23
24
25
26
27
28
29
30
31
32
33
34
35
def removesuffix(string: str, suffix: str) -> str:
    """
    Compatiblity function for python < 3.9
    """
    if sys.version_info >= (3, 9):
        return string.removesuffix(suffix)

    if string.endswith(suffix):
        return string[: -len(suffix)]

    return string


Jonah Brüchert's avatar
Jonah Brüchert committed
36
class LogType(Enum):
Jonah Brüchert's avatar
Jonah Brüchert committed
37
    """
38
    Enum representing the type of log message
Jonah Brüchert's avatar
Jonah Brüchert committed
39
40
    """

Jonah Brüchert's avatar
Jonah Brüchert committed
41
42
43
    INFO = auto()
    WARNING = auto()
    ERROR = auto()
Jonah Brüchert's avatar
Jonah Brüchert committed
44

45

46
47
48
49
class TextFormatting:  # pylint: disable=too-few-public-methods
    """
    Structure containing constants for working with text formatting
    """
50

Jonah Brüchert's avatar
Jonah Brüchert committed
51
52
53
54
55
56
57
58
59
60
61
    PURPLE: Final[str] = "\033[0;95m"
    CYAN: Final[str] = "\033[0;36m"
    DARKCYAN: Final[str] = "\033[0;96m"
    BLUE: Final[str] = "\033[0;34m"
    GREEN: Final[str] = "\033[0;32m"
    YELLOW: Final[str] = "\033[0;33m"
    RED: Final[str] = "\033[0;31m"
    LIGHTRED: Final[str] = "\033[1;31m"
    BOLD: Final[str] = "\033[1m"
    UNDERLINE: Final[str] = "\033[4m"
    END: Final[str] = "\033[0m"
62

63

64
65
66
67
class Utils:
    """
    This class contains static methods for common tasks
    """
Jonah Brüchert's avatar
Jonah Brüchert committed
68

Jonah Brüchert's avatar
Jonah Brüchert committed
69
70
    @staticmethod
    def str_id_for_url(url: str) -> str:
Jonah Brüchert's avatar
Jonah Brüchert committed
71
72
73
        """
        Returns the url encoded string id for a repository
        """
74
        normalized_url: str = Utils.normalize_url(url)
75
76
        normalized_url = removesuffix(normalized_url, ".git")
        normalized_url = removesuffix(normalized_url, "/")
77
78

        repository_url: ParseResult = urlparse(normalized_url)
Jonah Brüchert's avatar
Jonah Brüchert committed
79
        return quote_plus(repository_url.path[1:])
80
81

    @staticmethod
Jonah Brüchert's avatar
Jonah Brüchert committed
82
83
84
85
    def log(log_type: LogType, *message: str) -> None:
        """
        Prints a message in a colorful and consistent way
        """
Jonah Brüchert's avatar
Jonah Brüchert committed
86
87
        prefix = TextFormatting.BOLD
        if log_type == LogType.INFO:
Jonah Brüchert's avatar
Jonah Brüchert committed
88
            prefix += "Info"
Jonah Brüchert's avatar
Jonah Brüchert committed
89
90
91
92
        elif log_type == LogType.WARNING:
            prefix += TextFormatting.YELLOW + "Warning" + TextFormatting.END
        elif log_type == LogType.ERROR:
            prefix += TextFormatting.RED + "Error" + TextFormatting.END
Jonah Brüchert's avatar
Jonah Brüchert committed
93

Jonah Brüchert's avatar
Jonah Brüchert committed
94
        prefix += TextFormatting.END
95

Jonah Brüchert's avatar
Jonah Brüchert committed
96
        if len(prefix) > 0:
Jonah Brüchert's avatar
Jonah Brüchert committed
97
98
            prefix += ":"

99
        print(prefix, *message)
100
101
102

    @staticmethod
    def normalize_url(url: str) -> str:
Jonah Brüchert's avatar
Jonah Brüchert committed
103
104
105
106
        """
        Creates a correctly parsable url from a git remote url.
        Git remote urls can also be written in scp syntax, which is technically not a real url.

107
        Example: git@invent.kde.org:KDE/kaidan becomes ssh://git@invent.kde.org/KDE/kaidan
Jonah Brüchert's avatar
Jonah Brüchert committed
108
        """
109
110
111
112
113
114
115
116
117
        result = urlparse(url)

        # url is already fine
        if result.scheme != "":
            return url

        if "@" in url and ":" in url:
            return "ssh://" + url.replace(":", "/")

Jonah Brüchert's avatar
Jonah Brüchert committed
118
        Utils.log(LogType.ERROR, "Invalid url", url)
Jonah Brüchert's avatar
Jonah Brüchert committed
119
        sys.exit(1)
120

Jonah Brüchert's avatar
Jonah Brüchert committed
121
122
123
124
125
126
127
128
129
130
    @staticmethod
    def ssh_url_from_http(url: str) -> str:
        """
        Creates an ssh url from a http url

        :return ssh url
        """

        return url.replace("https://", "ssh://git@").replace("http://", "ssh://git@")

131
132
    @staticmethod
    def gitlab_instance_url(repository: str) -> str:
Jonah Brüchert's avatar
Jonah Brüchert committed
133
134
135
        """
        returns the gitlab instance url of a git remote url
        """
136
137
138
139
140
141
142
        # parse url
        repository_url: ParseResult = urlparse(repository)

        # Valid url
        if repository_url.scheme != "" and repository_url.path != "":
            # If the repository is using some kind of http, can know whether to use http or https
            if "http" in repository_url.scheme:
Jonah Brüchert's avatar
Jonah Brüchert committed
143
144
                if repository_url.scheme and repository_url.hostname:
                    return repository_url.scheme + "://" + repository_url.hostname
145
146

            # Else assume https.
Jonah Brüchert's avatar
Jonah Brüchert committed
147
148
149
150
            # redirects don't work according to
            # https://python-gitlab.readthedocs.io/en/stable/api-usage.html.
            if repository_url.hostname:
                return "https://" + repository_url.hostname
151
152
153
154
155
156

        # non valid url (probably scp syntax)
        if "@" in repository and ":" in repository:
            # create url in form of ssh://git@github.com/KDE/kaidan
            repository_url = urlparse("ssh://" + repository.replace(":", "/"))

Jonah Brüchert's avatar
Jonah Brüchert committed
157
158
            if repository_url.hostname:
                return "https://" + repository_url.hostname
159
160

        # If everything failed, exit
Jonah Brüchert's avatar
Jonah Brüchert committed
161
        Utils.log(LogType.ERROR, "Failed to detect GitLab instance url")
162
        sys.exit(1)
163
164
165

    @staticmethod
    def get_cwd_repo() -> Repo:
166
        """
167
168
        Creates a Repo object from one of the parent directories of the current directories.
        If it can not find a git repository, an error is shown.
169
        """
170
        try:
171
            return Repo(Utils.find_dotgit(os.getcwd()))
172
        except InvalidGitRepositoryError:
Jonah Brüchert's avatar
Jonah Brüchert committed
173
            Utils.log(LogType.ERROR, "Current directory is not a git repository")
174
            sys.exit(1)
Benjamin Port's avatar
Benjamin Port committed
175
176

    @staticmethod
Jonah Brüchert's avatar
Jonah Brüchert committed
177
    def editor() -> List[str]:
Benjamin Port's avatar
Benjamin Port committed
178
179
180
181
182
        """
        return prefered user editor using git configuration
        """
        repo = Utils.get_cwd_repo()
        config = repo.config_reader()
Benjamin Port's avatar
Benjamin Port committed
183
        editor: str = config.get_value("core", "editor", "")
Benjamin Port's avatar
Benjamin Port committed
184
185
186
187
188
189
190
191
192
        if not editor:
            if "EDITOR" in os.environ:
                editor = os.environ["EDITOR"]
            elif "VISUAL" in os.environ:
                editor = os.environ["VISUAL"]
            elif shutil.which("editor"):
                editor = "editor"
            else:
                editor = "vi"
Jonah Brüchert's avatar
Jonah Brüchert committed
193
194

        return shlex.split(editor)
Benjamin Port's avatar
Benjamin Port committed
195
196
197
198
199
200
201

    @staticmethod
    def xdg_open(path: str) -> None:
        """
        Open path with xdg-open
        :param path: path to open
        """
Benjamin Port's avatar
Benjamin Port committed
202
        subprocess.call(("xdg-open", path))
203
204
205
206
207
208
209
210

    @staticmethod
    def ask_bool(question: str) -> bool:
        """
        Ask a yes or no question
        :param question: text for the question
        :return: whether the user answered yes
        """
Jonah Brüchert's avatar
Jonah Brüchert committed
211
        answer: str = input("{} [y/n] ".format(question))
Jonah Brüchert's avatar
Jonah Brüchert committed
212
213
        if answer == "y":
            return True
214

Jonah Brüchert's avatar
Jonah Brüchert committed
215
        return False
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235

    @staticmethod
    def find_dotgit(path: str) -> Optional[str]:
        """
        Finds the parent directory containing .git, and returns it
        :param: path to start climbing from
        :return: resulting path
        """
        abspath = os.path.abspath(path)

        if ".git" in os.listdir(abspath):
            return abspath

        parent_dir: str = os.path.abspath(abspath + os.path.sep + os.path.pardir)

        # No parent dir exists, we are at the root filesystem
        if os.path.samefile(parent_dir, path):
            return None

        return Utils.find_dotgit(parent_dir)