blob: 7f15f9b2de5404801cc4a80acff6af787d0f049b [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
Peter Pentchev81fa7662024-01-18 13:19:34 +02007import subprocess # noqa: S404
Peter Pentchevfd046a02024-01-18 13:41:06 +02008import typing
Peter Pentchevb91fc932023-01-19 15:27:13 +02009
10from . import defs
11
Peter Pentchev2c01f8f2024-01-18 13:37:48 +020012
Peter Pentchevfd046a02024-01-18 13:41:06 +020013if typing.TYPE_CHECKING:
Peter Pentchevb91fc932023-01-19 15:27:13 +020014 import pathlib
Peter Pentchevb91fc932023-01-19 15:27:13 +020015 from typing import Final
16
17
18class GitError(defs.GApplyError):
19 """An error that occurred during a Git-related operation."""
20
21
22def repo_clone(cfg: defs.Config, repo: defs.Repo, tempd: pathlib.Path) -> pathlib.Path:
23 """Clone a Git repository."""
24 loc: Final = cfg.repo_urls.get(repo.origin.upper())
25 if loc is None:
26 raise GitError(f"Unknown repository origin {repo.origin!r}")
27 cfg.log.info(
28 "Cloning the %(origin)s %(repo)s repository from %(loc)s",
29 {"origin": repo.origin, "repo": repo.repo, "loc": loc.url.geturl()},
30 )
31
32 repo_url: Final = loc.url._replace(
Peter Pentchev85a2ad12024-01-18 13:34:37 +020033 path=loc.url.path + ("" if loc.url.path.endswith("/") else "/") + repo.repo,
Peter Pentchevb91fc932023-01-19 15:27:13 +020034 )
35 repo_dir: Final = tempd / repo.origin / repo.repo
36 if repo_dir.exists() or repo_dir.is_symlink():
37 raise GitError(f"Did not expect {repo_dir} to exist")
38 repo_dir.parent.mkdir(mode=0o755, exist_ok=True, parents=True)
39
40 cfg.log.debug(
41 "About to clone %(repo_url)s into %(repo_dir)s",
42 {"repo_url": repo_url.geturl(), "repo_dir": repo_dir},
43 )
44 try:
45 subprocess.run(
Peter Pentchev78ab37b2024-01-18 13:07:33 +020046 ["git", "clone", repo_url.geturl(), repo.repo, "-b", "master"], # noqa: S603,S607
Peter Pentchevb91fc932023-01-19 15:27:13 +020047 check=True,
48 cwd=repo_dir.parent,
49 )
50 except (OSError, subprocess.CalledProcessError) as err:
51 raise GitError(
Peter Pentchev85a2ad12024-01-18 13:34:37 +020052 f"Could not run `git clone {repo_url.geturl()} {repo.repo}` in {tempd}: {err}",
Peter Pentchevb91fc932023-01-19 15:27:13 +020053 ) from err
54 if not repo_dir.is_dir():
55 raise GitError(f"`git clone` did not create {repo_dir}")
56
57 return repo_dir
58
59
60def list_change_ids(cfg: defs.Config, repo_dir: pathlib.Path) -> list[str]:
61 """Get the Change-Id fields of all the commits reachable from the current head."""
62 cfg.log.info("Getting the change IDs in %(repo_dir)s", {"repo_dir": repo_dir})
63 try:
64 lines = [
65 line
66 for line in subprocess.check_output(
Peter Pentchev78ab37b2024-01-18 13:07:33 +020067 ["git", "log", "--pretty=%(trailers:key=Change-Id)", "--reverse"], # noqa: S607
Peter Pentchevb91fc932023-01-19 15:27:13 +020068 cwd=repo_dir,
69 encoding="UTF-8",
Peter Pentchev78ab37b2024-01-18 13:07:33 +020070 shell=False, # noqa: S603
Peter Pentchevb91fc932023-01-19 15:27:13 +020071 ).splitlines()
72 if line
73 ]
74 except (OSError, subprocess.CalledProcessError) as err:
75 raise GitError(f"Could not run `git log` for change IDs in {repo_dir}: {err}") from err
76 except ValueError as err:
77 raise GitError(
Peter Pentchev85a2ad12024-01-18 13:34:37 +020078 f"Could not decode the output of `git log` in {repo_dir} into UTF-8 change IDs: {err}",
Peter Pentchevb91fc932023-01-19 15:27:13 +020079 ) from err
80
81 def parse_line(line: str) -> str:
82 """Parse a "Change-Id: Ixxx" line."""
83 fields: Final = line.split()
84 # The magic value will go away once we can use structural pattern matching
85 if (
Peter Pentchev5ce7fdb2024-01-18 13:25:01 +020086 len(fields) != 2 # noqa: PLR2004
87 or fields[0] != "Change-Id:"
Peter Pentchevb91fc932023-01-19 15:27:13 +020088 or not fields[1].startswith("I")
89 ):
90 raise GitError(f"Unexpected `git log` ouput for change IDs: {line!r}")
91 return fields[1]
92
93 return [parse_line(line) for line in lines]