xref: /aosp_15_r20/external/toolchain-utils/llvm_tools/git.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
1#!/usr/bin/env python3
2# Copyright 2020 The ChromiumOS Authors
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Git helper functions."""
7
8import collections
9import os
10from pathlib import Path
11import re
12import subprocess
13import tempfile
14from typing import Iterable, Optional, Union
15
16
17CommitContents = collections.namedtuple("CommitContents", ["url", "cl_number"])
18
19
20def IsFullGitSHA(s: str) -> bool:
21    """Returns if `s` looks like a git SHA."""
22    return len(s) == 40 and all(x.isdigit() or "a" <= x <= "f" for x in s)
23
24
25def CreateBranch(repo: Union[Path, str], branch: str) -> None:
26    """Creates a branch in the given repo.
27
28    Args:
29        repo: The absolute path to the repo.
30        branch: The name of the branch to create.
31
32    Raises:
33        ValueError: Failed to create a repo in that directory.
34    """
35
36    if not os.path.isdir(repo):
37        raise ValueError("Invalid directory path provided: %s" % repo)
38
39    subprocess.check_output(["git", "-C", repo, "reset", "HEAD", "--hard"])
40
41    subprocess.check_output(["repo", "start", branch], cwd=repo)
42
43
44def DeleteBranch(repo: Union[Path, str], branch: str) -> None:
45    """Deletes a branch in the given repo.
46
47    Args:
48        repo: The absolute path of the repo.
49        branch: The name of the branch to delete.
50
51    Raises:
52        ValueError: Failed to delete the repo in that directory.
53    """
54
55    if not os.path.isdir(repo):
56        raise ValueError("Invalid directory path provided: %s" % repo)
57
58    def run_checked(cmd):
59        subprocess.run(["git", "-C", repo] + cmd, check=True)
60
61    run_checked(["checkout", "-q", "m/main"])
62    run_checked(["reset", "-q", "HEAD", "--hard"])
63    run_checked(["branch", "-q", "-D", branch])
64
65
66def CommitChanges(
67    repo: Union[Path, str], commit_messages: Iterable[str]
68) -> None:
69    """Commit changes without uploading them.
70
71    Args:
72        repo: The absolute path to the repo where changes were made.
73        commit_messages: Messages to concatenate to form the commit message.
74    """
75    if not os.path.isdir(repo):
76        raise ValueError("Invalid path provided: %s" % repo)
77
78    # Create a git commit.
79    with tempfile.NamedTemporaryFile(mode="w+t", encoding="utf-8") as f:
80        f.write("\n".join(commit_messages))
81        f.flush()
82
83        subprocess.check_output(["git", "commit", "-F", f.name], cwd=repo)
84
85
86def UploadChanges(
87    repo: Union[Path, str],
88    branch: str,
89    reviewers: Optional[Iterable[str]] = None,
90    cc: Optional[Iterable[str]] = None,
91    wip: bool = False,
92) -> CommitContents:
93    """Uploads the changes in the specifed branch of the given repo for review.
94
95    Args:
96        repo: The absolute path to the repo where changes were made.
97        branch: The name of the branch to upload.
98        of the changes made.
99        reviewers: A list of reviewers to add to the CL.
100        cc: A list of contributors to CC about the CL.
101        wip: Whether to upload the change as a work-in-progress.
102
103    Returns:
104        A CommitContents value containing the commit URL and change list number.
105
106    Raises:
107        ValueError: Failed to create a commit or failed to upload the
108        changes for review.
109    """
110
111    if not os.path.isdir(repo):
112        raise ValueError("Invalid path provided: %s" % repo)
113
114    # Upload the changes for review.
115    git_args = [
116        "repo",
117        "upload",
118        "--yes",
119        f'--reviewers={",".join(reviewers)}' if reviewers else "--ne",
120        "--no-verify",
121        f"--br={branch}",
122    ]
123
124    if cc:
125        git_args.append(f'--cc={",".join(cc)}')
126    if wip:
127        git_args.append("--wip")
128
129    out = subprocess.check_output(
130        git_args,
131        stderr=subprocess.STDOUT,
132        cwd=repo,
133        encoding="utf-8",
134    )
135
136    print(out)
137    # Matches both internal and external CLs.
138    found_url = re.search(
139        r"https?://[\w-]*-review.googlesource.com/c/.*/([0-9]+)",
140        out.rstrip(),
141    )
142    if not found_url:
143        raise ValueError("Failed to find change list URL.")
144
145    return CommitContents(
146        url=found_url.group(0), cl_number=int(found_url.group(1))
147    )
148