blob: 038d167e6979b3a09ed5c388c2dad6b0933b5d32 [file] [log] [blame]
Peter Pentchevb91fc932023-01-19 15:27:13 +02001# SPDX-FileCopyrightText: StorPool <support@storpool.com>
2# SPDX-License-Identifier: BSD-2-Clause
3"""Clone a Git repository."""
4
5from __future__ import annotations
6
7import subprocess
8
9from typing import TYPE_CHECKING
10
11from . import defs
12
13if TYPE_CHECKING:
14 import pathlib
15
16 from typing import Final
17
18
19class GitError(defs.GApplyError):
20 """An error that occurred during a Git-related operation."""
21
22
23def repo_clone(cfg: defs.Config, repo: defs.Repo, tempd: pathlib.Path) -> pathlib.Path:
24 """Clone a Git repository."""
25 loc: Final = cfg.repo_urls.get(repo.origin.upper())
26 if loc is None:
27 raise GitError(f"Unknown repository origin {repo.origin!r}")
28 cfg.log.info(
29 "Cloning the %(origin)s %(repo)s repository from %(loc)s",
30 {"origin": repo.origin, "repo": repo.repo, "loc": loc.url.geturl()},
31 )
32
33 repo_url: Final = loc.url._replace(
34 path=loc.url.path + ("" if loc.url.path.endswith("/") else "/") + repo.repo
35 )
36 repo_dir: Final = tempd / repo.origin / repo.repo
37 if repo_dir.exists() or repo_dir.is_symlink():
38 raise GitError(f"Did not expect {repo_dir} to exist")
39 repo_dir.parent.mkdir(mode=0o755, exist_ok=True, parents=True)
40
41 cfg.log.debug(
42 "About to clone %(repo_url)s into %(repo_dir)s",
43 {"repo_url": repo_url.geturl(), "repo_dir": repo_dir},
44 )
45 try:
46 subprocess.run(
47 ["git", "clone", repo_url.geturl(), repo.repo, "-b", "master"],
48 check=True,
49 cwd=repo_dir.parent,
50 )
51 except (OSError, subprocess.CalledProcessError) as err:
52 raise GitError(
53 f"Could not run `git clone {repo_url.geturl()} {repo.repo}` in {tempd}: {err}"
54 ) from err
55 if not repo_dir.is_dir():
56 raise GitError(f"`git clone` did not create {repo_dir}")
57
58 return repo_dir
59
60
61def list_change_ids(cfg: defs.Config, repo_dir: pathlib.Path) -> list[str]:
62 """Get the Change-Id fields of all the commits reachable from the current head."""
63 cfg.log.info("Getting the change IDs in %(repo_dir)s", {"repo_dir": repo_dir})
64 try:
65 lines = [
66 line
67 for line in subprocess.check_output(
68 ["git", "log", "--pretty=%(trailers:key=Change-Id)", "--reverse"],
69 cwd=repo_dir,
70 encoding="UTF-8",
71 shell=False,
72 ).splitlines()
73 if line
74 ]
75 except (OSError, subprocess.CalledProcessError) as err:
76 raise GitError(f"Could not run `git log` for change IDs in {repo_dir}: {err}") from err
77 except ValueError as err:
78 raise GitError(
79 f"Could not decode the output of `git log` in {repo_dir} into UTF-8 change IDs: {err}"
80 ) from err
81
82 def parse_line(line: str) -> str:
83 """Parse a "Change-Id: Ixxx" line."""
84 fields: Final = line.split()
85 # The magic value will go away once we can use structural pattern matching
86 if (
87 len(fields) != 2 # noqa: PLR2004 # pylint: disable=magic-value-comparison
88 or fields[0] != "Change-Id:" # pylint: disable=magic-value-comparison
89 or not fields[1].startswith("I")
90 ):
91 raise GitError(f"Unexpected `git log` ouput for change IDs: {line!r}")
92 return fields[1]
93
94 return [parse_line(line) for line in lines]