Passed
Push — main ( 87238c...9f1476 )
by Douglas
02:33
created

pocketutils.tools.git_tools   A

Complexity

Total Complexity 7

Size/Duplication

Total Lines 139
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 7
eloc 73
dl 0
loc 139
rs 10
c 0
b 0
f 0

5 Methods

Rating   Name   Duplication   Size   Complexity  
A GitDescription.__str__() 0 2 1
A GitUtils.git_description() 0 25 1
A GitUtils.config() 0 7 1
A GitUtils.clone() 0 2 1
A GitUtils._parse() 0 13 2

1 Function

Rating   Name   Duplication   Size   Complexity  
A _call() 0 2 1
1
# SPDX-FileCopyrightText: Copyright 2020-2023, Contributors to pocketutils
2
# SPDX-PackageHomePage: https://github.com/dmyersturnbull/pocketutils
3
# SPDX-License-Identifier: Apache-2.0
4
"""
5
6
"""
7
8
import re
9
import subprocess  # nosec
10
from dataclasses import dataclass
11
from pathlib import Path, PurePath
12
from typing import Self
13
14
from pocketutils import ValueIllegalError
15
16
__all__ = ["GitDescription", "GitUtils", "GitTools"]
17
18
# ex: 1.8.6-43-g0ceb89d3a954da84070858319f177abe3869752b-dirty
19
_GIT_DESC_PATTERN = re.compile(r"([\d.]+)-(\d+)-g([0-9a-h]{40})(?:-([a-z]+))?")
20
21
22
def _call(cmd: list[str], cwd: Path = Path.cwd()) -> str:
23
    return subprocess.check_output(cmd, cwd=cwd, encoding="utf-8").strip()  # noqa: S603,S607
24
25
26
@dataclass(frozen=True, order=True, slots=True)
27
class GitDescription:
28
    """
29
    Data collected from running `git describe --long --dirty --broken --abbrev=40 --tags`.
30
    """
31
32
    tag: str
33
    commits: str
34
    hash: str
35
    is_dirty: bool
36
    is_broken: bool
37
38
39
@dataclass(frozen=True, order=True, slots=True)
40
class GitConfig:
41
    user: str
42
    email: str
43
    autocrlf: str
44
    gpgsign: bool
45
    rebase: bool
46
47
48
@dataclass(frozen=True, order=True, slots=True)
49
class GitClone:
50
    repo_url: str
51
    repo_path: Path
52
53
54
@dataclass(frozen=True, slots=True, kw_only=True)
55
class GitDescription:
56
    """
57
    Data collected from running `git describe --long --dirty --broken --abbrev=40 --tags`.
58
    """
59
60
    text: str
61
    tag: str
62
    commits: str
63
    hash: str
64
    is_dirty: bool
65
    is_broken: bool
66
67
    def __str__(self: Self) -> str:
68
        return self.__class__.__name__ + "(" + self.text + ")"
69
70
71
@dataclass(slots=True, frozen=True)
72
class GitUtils:
73
    """
74
    Tools for external programs.
75
76
    Warning:
77
        Please note that these tools execute external code
78
        through the `subprocess` module.
79
        These calls are additionally made on partial executable paths,
80
        such as `git` rather than `/usr/bin/git`.
81
        This is an additional security consideration.
82
    """
83
84
    def clone(self: Self, repo: str, path: PurePath | str | None = Path.cwd()) -> None:
85
        _call(["git", "clone", repo], cwd=Path(path))
86
87
    def config(self: Self) -> GitConfig:
88
        return GitConfig(
89
            _call(["git", "config", "user.name"]),
90
            _call(["git", "config", "user.email"]),
91
            _call(["git", "config", "core.autocrlf"]),
92
            _call(["git", "config", "commit.gpgsign"]) == "true",
93
            _call(["git", "config", "pull.rebase"]) == "true",
94
        )
95
96
    def git_description(self: Self, git_repo_dir: PurePath | str = Path.cwd()) -> GitDescription:
97
        """
98
        Runs `git describe` and parses the output.
99
100
        Args:
101
            git_repo_dir: Path to the repository
102
103
        Returns:
104
            A `pocketutils.tools.program_tools.GitDescription` instance
105
106
        Raises:
107
            CalledProcessError:
108
        """
109
        cmd_args = {
110
            "cwd": str(git_repo_dir),
111
            "capture_output": True,
112
            "check": True,
113
            "text": True,
114
            "encoding": "utf-8",
115
        }
116
        cmd = "git describe --long --dirty --broken --abbrev=40 --tags".split(" ")
117
        # ignoring bandit security warning because we explain the security concerns
118
        # in the class docstring
119
        x = subprocess.run(cmd, **cmd_args)  # nosec
120
        return self._parse(x.stdout.strip())
121
122
    def _parse(self: Self, text: str):
123
        m = _GIT_DESC_PATTERN.fullmatch(text)
124
        if m is None:
125
            msg = f"Bad git describe string {text}"
126
            raise ValueIllegalError(msg, value=text)
127
        # noinspection PyArgumentList
128
        return GitDescription(
129
            text=text,
130
            tag=m.group(1),
131
            commits=m.group(2),
132
            hash=m.group(3),
133
            is_dirty=m.group(4) == "dirty",
134
            is_broken=m.group(4) == "broken",
135
        )
136
137
138
GitTools = GitUtils()
139