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