xref: /aosp_15_r20/external/toolchain-utils/llvm_tools/git_llvm_rev.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
1*760c253cSXin Li#!/usr/bin/env python3
2*760c253cSXin Li# Copyright 2019 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"""Maps LLVM git SHAs to synthetic revision numbers and back.
7*760c253cSXin Li
8*760c253cSXin LiRevision numbers are all of the form '(branch_name, r1234)'. As a shorthand,
9*760c253cSXin Lir1234 is parsed as '(main, 1234)'.
10*760c253cSXin Li"""
11*760c253cSXin Li
12*760c253cSXin Liimport argparse
13*760c253cSXin Lifrom pathlib import Path
14*760c253cSXin Liimport re
15*760c253cSXin Liimport subprocess
16*760c253cSXin Liimport sys
17*760c253cSXin Lifrom typing import IO, Iterable, List, NamedTuple, Optional, Tuple, Union
18*760c253cSXin Li
19*760c253cSXin Li
20*760c253cSXin LiMAIN_BRANCH = "main"
21*760c253cSXin Li
22*760c253cSXin Li# Note that after base_llvm_sha, we reach The Wild West(TM) of commits.
23*760c253cSXin Li# So reasonable input that could break us includes:
24*760c253cSXin Li#
25*760c253cSXin Li#   Revert foo
26*760c253cSXin Li#
27*760c253cSXin Li#   This reverts foo, which had the commit message:
28*760c253cSXin Li#
29*760c253cSXin Li#   bar
30*760c253cSXin Li#   llvm-svn: 375505
31*760c253cSXin Li#
32*760c253cSXin Li# While saddening, this is something we should probably try to handle
33*760c253cSXin Li# reasonably.
34*760c253cSXin Libase_llvm_revision = 375505
35*760c253cSXin Libase_llvm_sha = "186155b89c2d2a2f62337081e3ca15f676c9434b"
36*760c253cSXin Li
37*760c253cSXin Li# Known pairs of [revision, SHA] in ascending order.
38*760c253cSXin Li# The first element is the first non-`llvm-svn` commit that exists. Later ones
39*760c253cSXin Li# are functional nops, but speed this script up immensely, since `git` can take
40*760c253cSXin Li# quite a while to walk >100K commits.
41*760c253cSXin Liknown_llvm_rev_sha_pairs = (
42*760c253cSXin Li    (base_llvm_revision, base_llvm_sha),
43*760c253cSXin Li    (425000, "af870e11aed7a5c475ae41a72e3015c4c88597d1"),
44*760c253cSXin Li    (450000, "906ebd5830e6053b50c52bf098e3586b567e8499"),
45*760c253cSXin Li    (475000, "530d14a99611a71f8f3eb811920fd7b5c4d4e1f8"),
46*760c253cSXin Li    (500000, "173855f9b0bdfe45d71272596b510650bfc1ca33"),
47*760c253cSXin Li)
48*760c253cSXin Li
49*760c253cSXin Li# Represents an LLVM git checkout:
50*760c253cSXin Li#  - |dir| is the directory of the LLVM checkout
51*760c253cSXin Li#  - |remote| is the name of the LLVM remote. Generally it's "origin".
52*760c253cSXin LiLLVMConfig = NamedTuple(
53*760c253cSXin Li    "LLVMConfig", (("remote", str), ("dir", Union[Path, str]))
54*760c253cSXin Li)
55*760c253cSXin Li
56*760c253cSXin Li
57*760c253cSXin Liclass Rev(NamedTuple("Rev", (("branch", str), ("number", int)))):
58*760c253cSXin Li    """Represents a LLVM 'revision', a shorthand identifies a LLVM commit."""
59*760c253cSXin Li
60*760c253cSXin Li    @staticmethod
61*760c253cSXin Li    def parse(rev: str) -> "Rev":
62*760c253cSXin Li        """Parses a Rev from the given string.
63*760c253cSXin Li
64*760c253cSXin Li        Raises a ValueError on a failed parse.
65*760c253cSXin Li        """
66*760c253cSXin Li        # Revs are parsed into (${branch_name}, r${commits_since_base_commit})
67*760c253cSXin Li        # pairs.
68*760c253cSXin Li        #
69*760c253cSXin Li        # We support r${commits_since_base_commit} as shorthand for
70*760c253cSXin Li        # (main, r${commits_since_base_commit}).
71*760c253cSXin Li        if rev.startswith("r"):
72*760c253cSXin Li            branch_name = MAIN_BRANCH
73*760c253cSXin Li            rev_string = rev[1:]
74*760c253cSXin Li        else:
75*760c253cSXin Li            match = re.match(r"\((.+), r(\d+)\)", rev)
76*760c253cSXin Li            if not match:
77*760c253cSXin Li                raise ValueError("%r isn't a valid revision" % rev)
78*760c253cSXin Li
79*760c253cSXin Li            branch_name, rev_string = match.groups()
80*760c253cSXin Li
81*760c253cSXin Li        return Rev(branch=branch_name, number=int(rev_string))
82*760c253cSXin Li
83*760c253cSXin Li    def __str__(self) -> str:
84*760c253cSXin Li        branch_name, number = self
85*760c253cSXin Li        if branch_name == MAIN_BRANCH:
86*760c253cSXin Li            return "r%d" % number
87*760c253cSXin Li        return "(%s, r%d)" % (branch_name, number)
88*760c253cSXin Li
89*760c253cSXin Li
90*760c253cSXin Lidef is_git_sha(xs: str) -> bool:
91*760c253cSXin Li    """Returns whether the given string looks like a valid git commit SHA."""
92*760c253cSXin Li    return (
93*760c253cSXin Li        len(xs) > 6
94*760c253cSXin Li        and len(xs) <= 40
95*760c253cSXin Li        and all(x.isdigit() or "a" <= x.lower() <= "f" for x in xs)
96*760c253cSXin Li    )
97*760c253cSXin Li
98*760c253cSXin Li
99*760c253cSXin Lidef check_output(command: List[str], cwd: Union[Path, str]) -> str:
100*760c253cSXin Li    """Shorthand for subprocess.check_output. Auto-decodes any stdout."""
101*760c253cSXin Li    result = subprocess.run(
102*760c253cSXin Li        command,
103*760c253cSXin Li        cwd=cwd,
104*760c253cSXin Li        check=True,
105*760c253cSXin Li        stdin=subprocess.DEVNULL,
106*760c253cSXin Li        stdout=subprocess.PIPE,
107*760c253cSXin Li        encoding="utf-8",
108*760c253cSXin Li    )
109*760c253cSXin Li    return result.stdout
110*760c253cSXin Li
111*760c253cSXin Li
112*760c253cSXin Lidef translate_prebase_sha_to_rev_number(
113*760c253cSXin Li    llvm_config: LLVMConfig, sha: str
114*760c253cSXin Li) -> int:
115*760c253cSXin Li    """Translates a sha to a revision number (e.g., "llvm-svn: 1234").
116*760c253cSXin Li
117*760c253cSXin Li    This function assumes that the given SHA is an ancestor of |base_llvm_sha|.
118*760c253cSXin Li    """
119*760c253cSXin Li    commit_message = check_output(
120*760c253cSXin Li        ["git", "log", "-n1", "--format=%B", sha, "--"],
121*760c253cSXin Li        cwd=llvm_config.dir,
122*760c253cSXin Li    )
123*760c253cSXin Li    last_line = commit_message.strip().splitlines()[-1]
124*760c253cSXin Li    svn_match = re.match(r"^llvm-svn: (\d+)$", last_line)
125*760c253cSXin Li
126*760c253cSXin Li    if not svn_match:
127*760c253cSXin Li        raise ValueError(
128*760c253cSXin Li            f"No llvm-svn line found for {sha}, which... shouldn't happen?"
129*760c253cSXin Li        )
130*760c253cSXin Li
131*760c253cSXin Li    return int(svn_match.group(1))
132*760c253cSXin Li
133*760c253cSXin Li
134*760c253cSXin Lidef translate_sha_to_rev(llvm_config: LLVMConfig, sha_or_ref: str) -> Rev:
135*760c253cSXin Li    """Translates a sha or git ref to a Rev."""
136*760c253cSXin Li
137*760c253cSXin Li    if is_git_sha(sha_or_ref):
138*760c253cSXin Li        sha = sha_or_ref
139*760c253cSXin Li    else:
140*760c253cSXin Li        sha = check_output(
141*760c253cSXin Li            ["git", "rev-parse", "--revs-only", sha_or_ref, "--"],
142*760c253cSXin Li            cwd=llvm_config.dir,
143*760c253cSXin Li        )
144*760c253cSXin Li        sha = sha.strip()
145*760c253cSXin Li
146*760c253cSXin Li    for base_rev, base_sha in reversed(known_llvm_rev_sha_pairs):
147*760c253cSXin Li        merge_base = check_output(
148*760c253cSXin Li            ["git", "merge-base", base_sha, sha, "--"],
149*760c253cSXin Li            cwd=llvm_config.dir,
150*760c253cSXin Li        )
151*760c253cSXin Li        merge_base = merge_base.strip()
152*760c253cSXin Li        if merge_base == base_sha:
153*760c253cSXin Li            result = check_output(
154*760c253cSXin Li                [
155*760c253cSXin Li                    "git",
156*760c253cSXin Li                    "rev-list",
157*760c253cSXin Li                    "--count",
158*760c253cSXin Li                    "--first-parent",
159*760c253cSXin Li                    f"{base_sha}..{sha}",
160*760c253cSXin Li                    "--",
161*760c253cSXin Li                ],
162*760c253cSXin Li                cwd=llvm_config.dir,
163*760c253cSXin Li            )
164*760c253cSXin Li            count = int(result.strip())
165*760c253cSXin Li            return Rev(branch=MAIN_BRANCH, number=count + base_rev)
166*760c253cSXin Li
167*760c253cSXin Li    # Otherwise, either:
168*760c253cSXin Li    # - |merge_base| is |sha| (we have a guaranteed llvm-svn number on |sha|)
169*760c253cSXin Li    # - |merge_base| is neither (we have a guaranteed llvm-svn number on
170*760c253cSXin Li    #                            |merge_base|, but not |sha|)
171*760c253cSXin Li    merge_base_number = translate_prebase_sha_to_rev_number(
172*760c253cSXin Li        llvm_config, merge_base
173*760c253cSXin Li    )
174*760c253cSXin Li    if merge_base == sha:
175*760c253cSXin Li        return Rev(branch=MAIN_BRANCH, number=merge_base_number)
176*760c253cSXin Li
177*760c253cSXin Li    distance_from_base = check_output(
178*760c253cSXin Li        [
179*760c253cSXin Li            "git",
180*760c253cSXin Li            "rev-list",
181*760c253cSXin Li            "--count",
182*760c253cSXin Li            "--first-parent",
183*760c253cSXin Li            f"{merge_base}..{sha}",
184*760c253cSXin Li            "--",
185*760c253cSXin Li        ],
186*760c253cSXin Li        cwd=llvm_config.dir,
187*760c253cSXin Li    )
188*760c253cSXin Li
189*760c253cSXin Li    revision_number = merge_base_number + int(distance_from_base.strip())
190*760c253cSXin Li    branches_containing = check_output(
191*760c253cSXin Li        ["git", "branch", "-r", "--contains", sha],
192*760c253cSXin Li        cwd=llvm_config.dir,
193*760c253cSXin Li    )
194*760c253cSXin Li
195*760c253cSXin Li    candidates = []
196*760c253cSXin Li
197*760c253cSXin Li    prefix = llvm_config.remote + "/"
198*760c253cSXin Li    for branch in branches_containing.splitlines():
199*760c253cSXin Li        branch = branch.strip()
200*760c253cSXin Li        if branch.startswith(prefix):
201*760c253cSXin Li            candidates.append(branch[len(prefix) :])
202*760c253cSXin Li
203*760c253cSXin Li    if not candidates:
204*760c253cSXin Li        raise ValueError(
205*760c253cSXin Li            f"No viable branches found from {llvm_config.remote} with {sha}"
206*760c253cSXin Li        )
207*760c253cSXin Li
208*760c253cSXin Li    # It seems that some `origin/release/.*` branches have
209*760c253cSXin Li    # `origin/upstream/release/.*` equivalents, which is... awkward to deal
210*760c253cSXin Li    # with. Prefer the latter, since that seems to have newer commits than the
211*760c253cSXin Li    # former. Technically n^2, but len(elements) should be like, tens in the
212*760c253cSXin Li    # worst case.
213*760c253cSXin Li    candidates = [x for x in candidates if f"upstream/{x}" not in candidates]
214*760c253cSXin Li    if len(candidates) != 1:
215*760c253cSXin Li        raise ValueError(
216*760c253cSXin Li            f"Ambiguity: multiple branches from {llvm_config.remote} have "
217*760c253cSXin Li            f"{sha}: {sorted(candidates)}"
218*760c253cSXin Li        )
219*760c253cSXin Li
220*760c253cSXin Li    return Rev(branch=candidates[0], number=revision_number)
221*760c253cSXin Li
222*760c253cSXin Li
223*760c253cSXin Lidef parse_git_commit_messages(
224*760c253cSXin Li    stream: Union[Iterable[str], IO[str]], separator: str
225*760c253cSXin Li) -> Iterable[Tuple[str, str]]:
226*760c253cSXin Li    """Parses a stream of git log messages.
227*760c253cSXin Li
228*760c253cSXin Li    These are expected to be in the format:
229*760c253cSXin Li
230*760c253cSXin Li    40 character sha
231*760c253cSXin Li    commit
232*760c253cSXin Li    message
233*760c253cSXin Li    body
234*760c253cSXin Li    separator
235*760c253cSXin Li    40 character sha
236*760c253cSXin Li    commit
237*760c253cSXin Li    message
238*760c253cSXin Li    body
239*760c253cSXin Li    separator
240*760c253cSXin Li    """
241*760c253cSXin Li
242*760c253cSXin Li    lines = iter(stream)
243*760c253cSXin Li    while True:
244*760c253cSXin Li        # Looks like a potential bug in pylint? crbug.com/1041148
245*760c253cSXin Li        # pylint: disable=stop-iteration-return
246*760c253cSXin Li        sha = next(lines, None)
247*760c253cSXin Li        if sha is None:
248*760c253cSXin Li            return
249*760c253cSXin Li
250*760c253cSXin Li        sha = sha.strip()
251*760c253cSXin Li        assert is_git_sha(sha), f"Invalid git SHA: {sha}"
252*760c253cSXin Li
253*760c253cSXin Li        message = []
254*760c253cSXin Li        for line in lines:
255*760c253cSXin Li            if line.strip() == separator:
256*760c253cSXin Li                break
257*760c253cSXin Li            message.append(line)
258*760c253cSXin Li
259*760c253cSXin Li        yield sha, "".join(message)
260*760c253cSXin Li
261*760c253cSXin Li
262*760c253cSXin Lidef translate_prebase_rev_to_sha(llvm_config: LLVMConfig, rev: Rev) -> str:
263*760c253cSXin Li    """Translates a Rev to a SHA.
264*760c253cSXin Li
265*760c253cSXin Li    This function assumes that the given rev refers to a commit that's an
266*760c253cSXin Li    ancestor of |base_llvm_sha|.
267*760c253cSXin Li    """
268*760c253cSXin Li    # Because reverts may include reverted commit messages, we can't just |-n1|
269*760c253cSXin Li    # and pick that.
270*760c253cSXin Li    separator = ">!" * 80
271*760c253cSXin Li    looking_for = f"llvm-svn: {rev.number}"
272*760c253cSXin Li
273*760c253cSXin Li    git_command = [
274*760c253cSXin Li        "git",
275*760c253cSXin Li        "log",
276*760c253cSXin Li        "--grep",
277*760c253cSXin Li        f"^{looking_for}$",
278*760c253cSXin Li        f"--format=%H%n%B{separator}",
279*760c253cSXin Li        base_llvm_sha,
280*760c253cSXin Li    ]
281*760c253cSXin Li
282*760c253cSXin Li    with subprocess.Popen(
283*760c253cSXin Li        git_command,
284*760c253cSXin Li        cwd=llvm_config.dir,
285*760c253cSXin Li        stdin=subprocess.DEVNULL,
286*760c253cSXin Li        stdout=subprocess.PIPE,
287*760c253cSXin Li        encoding="utf-8",
288*760c253cSXin Li    ) as subp:
289*760c253cSXin Li        assert subp.stdout is not None
290*760c253cSXin Li        for sha, message in parse_git_commit_messages(subp.stdout, separator):
291*760c253cSXin Li            last_line = message.splitlines()[-1]
292*760c253cSXin Li            if last_line.strip() == looking_for:
293*760c253cSXin Li                subp.terminate()
294*760c253cSXin Li                return sha
295*760c253cSXin Li        if subp.wait() != 0:
296*760c253cSXin Li            raise subprocess.CalledProcessError(subp.returncode, git_command)
297*760c253cSXin Li
298*760c253cSXin Li    raise ValueError(f"No commit with revision {rev} found")
299*760c253cSXin Li
300*760c253cSXin Li
301*760c253cSXin Lidef translate_rev_to_sha_from_baseline(
302*760c253cSXin Li    llvm_config: LLVMConfig,
303*760c253cSXin Li    parent_sha: str,
304*760c253cSXin Li    parent_rev: int,
305*760c253cSXin Li    child_sha: str,
306*760c253cSXin Li    child_rev: Optional[int],
307*760c253cSXin Li    want_rev: int,
308*760c253cSXin Li    branch_name: str,
309*760c253cSXin Li) -> str:
310*760c253cSXin Li    """Translates a revision number between a parent & child to a SHA.
311*760c253cSXin Li
312*760c253cSXin Li    Args:
313*760c253cSXin Li        llvm_config: LLVM config to use.
314*760c253cSXin Li        parent_sha: SHA of the parent that the revision number is a child of.
315*760c253cSXin Li        parent_rev: Revision number of `parent_sha`.
316*760c253cSXin Li        child_sha: A child of `parent_sha` to find a rev on.
317*760c253cSXin Li        child_rev: Optional note of what the child's revision number is.
318*760c253cSXin Li        want_rev: The desired revision number between child and parent.
319*760c253cSXin Li        branch_name: Name of the branch to refer to if a ValueError is raised.
320*760c253cSXin Li
321*760c253cSXin Li    Raises:
322*760c253cSXin Li        ValueError if the given child isn't far enough away from the parent to
323*760c253cSXin Li        find `want_rev`.
324*760c253cSXin Li    """
325*760c253cSXin Li    # As a convenience, have a fast path for want_rev < parent_rev. In
326*760c253cSXin Li    # particular, branches can hit this case.
327*760c253cSXin Li    if want_rev < parent_rev:
328*760c253cSXin Li        baseline_git_sha = parent_sha
329*760c253cSXin Li        commits_behind_baseline = parent_rev - want_rev
330*760c253cSXin Li    else:
331*760c253cSXin Li        if child_rev is None:
332*760c253cSXin Li            commits_between_parent_and_child = check_output(
333*760c253cSXin Li                [
334*760c253cSXin Li                    "git",
335*760c253cSXin Li                    "rev-list",
336*760c253cSXin Li                    "--count",
337*760c253cSXin Li                    "--first-parent",
338*760c253cSXin Li                    f"{parent_sha}..{child_sha}",
339*760c253cSXin Li                    "--",
340*760c253cSXin Li                ],
341*760c253cSXin Li                cwd=llvm_config.dir,
342*760c253cSXin Li            )
343*760c253cSXin Li            child_rev = parent_rev + int(
344*760c253cSXin Li                commits_between_parent_and_child.strip()
345*760c253cSXin Li            )
346*760c253cSXin Li        if child_rev < want_rev:
347*760c253cSXin Li            raise ValueError(
348*760c253cSXin Li                "Revision {want_rev} is past "
349*760c253cSXin Li                f"{llvm_config.remote}/{branch_name}. Try updating your tree?"
350*760c253cSXin Li            )
351*760c253cSXin Li        baseline_git_sha = child_sha
352*760c253cSXin Li        commits_behind_baseline = child_rev - want_rev
353*760c253cSXin Li
354*760c253cSXin Li    if not commits_behind_baseline:
355*760c253cSXin Li        return baseline_git_sha
356*760c253cSXin Li
357*760c253cSXin Li    result = check_output(
358*760c253cSXin Li        [
359*760c253cSXin Li            "git",
360*760c253cSXin Li            "rev-parse",
361*760c253cSXin Li            "--revs-only",
362*760c253cSXin Li            f"{baseline_git_sha}~{commits_behind_baseline}",
363*760c253cSXin Li        ],
364*760c253cSXin Li        cwd=llvm_config.dir,
365*760c253cSXin Li    )
366*760c253cSXin Li    return result.strip()
367*760c253cSXin Li
368*760c253cSXin Li
369*760c253cSXin Lidef translate_rev_to_sha(llvm_config: LLVMConfig, rev: Rev) -> str:
370*760c253cSXin Li    """Translates a Rev to a SHA.
371*760c253cSXin Li
372*760c253cSXin Li    Raises a ValueError if the given Rev doesn't exist in the given config.
373*760c253cSXin Li    """
374*760c253cSXin Li    branch, number = rev
375*760c253cSXin Li
376*760c253cSXin Li    branch_tip = check_output(
377*760c253cSXin Li        ["git", "rev-parse", "--revs-only", f"{llvm_config.remote}/{branch}"],
378*760c253cSXin Li        cwd=llvm_config.dir,
379*760c253cSXin Li    ).strip()
380*760c253cSXin Li
381*760c253cSXin Li    if branch != MAIN_BRANCH:
382*760c253cSXin Li        main_merge_point = check_output(
383*760c253cSXin Li            [
384*760c253cSXin Li                "git",
385*760c253cSXin Li                "merge-base",
386*760c253cSXin Li                f"{llvm_config.remote}/{MAIN_BRANCH}",
387*760c253cSXin Li                branch_tip,
388*760c253cSXin Li            ],
389*760c253cSXin Li            cwd=llvm_config.dir,
390*760c253cSXin Li        )
391*760c253cSXin Li        main_merge_point = main_merge_point.strip()
392*760c253cSXin Li        main_rev = translate_sha_to_rev(llvm_config, main_merge_point)
393*760c253cSXin Li        return translate_rev_to_sha_from_baseline(
394*760c253cSXin Li            llvm_config,
395*760c253cSXin Li            parent_sha=main_merge_point,
396*760c253cSXin Li            parent_rev=main_rev.number,
397*760c253cSXin Li            child_sha=branch_tip,
398*760c253cSXin Li            child_rev=None,
399*760c253cSXin Li            want_rev=number,
400*760c253cSXin Li            branch_name=branch,
401*760c253cSXin Li        )
402*760c253cSXin Li
403*760c253cSXin Li    if number < base_llvm_revision:
404*760c253cSXin Li        return translate_prebase_rev_to_sha(llvm_config, rev)
405*760c253cSXin Li
406*760c253cSXin Li    # Technically this could be a binary search, but the list has fewer than 10
407*760c253cSXin Li    # elems, and won't grow fast. Linear is easier.
408*760c253cSXin Li    last_cached_rev = None
409*760c253cSXin Li    last_cached_sha = branch_tip
410*760c253cSXin Li    for cached_rev, cached_sha in reversed(known_llvm_rev_sha_pairs):
411*760c253cSXin Li        if cached_rev == number:
412*760c253cSXin Li            return cached_sha
413*760c253cSXin Li
414*760c253cSXin Li        if cached_rev < number:
415*760c253cSXin Li            return translate_rev_to_sha_from_baseline(
416*760c253cSXin Li                llvm_config,
417*760c253cSXin Li                parent_sha=cached_sha,
418*760c253cSXin Li                parent_rev=cached_rev,
419*760c253cSXin Li                child_sha=last_cached_sha,
420*760c253cSXin Li                child_rev=last_cached_rev,
421*760c253cSXin Li                want_rev=number,
422*760c253cSXin Li                branch_name=branch,
423*760c253cSXin Li            )
424*760c253cSXin Li
425*760c253cSXin Li        last_cached_rev = cached_rev
426*760c253cSXin Li        last_cached_sha = cached_sha
427*760c253cSXin Li
428*760c253cSXin Li    # This is only hit if `number >= base_llvm_revision` _and_ there's no
429*760c253cSXin Li    # coverage for `number` in `known_llvm_rev_sha_pairs`, which contains
430*760c253cSXin Li    # `base_llvm_revision`.
431*760c253cSXin Li    assert False, "Couldn't find a base SHA for a rev on main?"
432*760c253cSXin Li
433*760c253cSXin Li
434*760c253cSXin Lidef find_root_llvm_dir(root_dir: str = ".") -> str:
435*760c253cSXin Li    """Finds the root of an LLVM directory starting at |root_dir|.
436*760c253cSXin Li
437*760c253cSXin Li    Raises a subprocess.CalledProcessError if no git directory is found.
438*760c253cSXin Li    """
439*760c253cSXin Li    result = check_output(
440*760c253cSXin Li        ["git", "rev-parse", "--show-toplevel"],
441*760c253cSXin Li        cwd=root_dir,
442*760c253cSXin Li    )
443*760c253cSXin Li    return result.strip()
444*760c253cSXin Li
445*760c253cSXin Li
446*760c253cSXin Lidef main(argv: List[str]) -> None:
447*760c253cSXin Li    parser = argparse.ArgumentParser(description=__doc__)
448*760c253cSXin Li    parser.add_argument(
449*760c253cSXin Li        "--llvm_dir",
450*760c253cSXin Li        help="LLVM directory to consult for git history, etc. Autodetected "
451*760c253cSXin Li        "if cwd is inside of an LLVM tree",
452*760c253cSXin Li    )
453*760c253cSXin Li    parser.add_argument(
454*760c253cSXin Li        "--upstream",
455*760c253cSXin Li        default="origin",
456*760c253cSXin Li        help="LLVM upstream's remote name. Defaults to %(default)s.",
457*760c253cSXin Li    )
458*760c253cSXin Li    sha_or_rev = parser.add_mutually_exclusive_group(required=True)
459*760c253cSXin Li    sha_or_rev.add_argument(
460*760c253cSXin Li        "--sha", help="A git SHA (or ref) to convert to a rev"
461*760c253cSXin Li    )
462*760c253cSXin Li    sha_or_rev.add_argument("--rev", help="A rev to convert into a sha")
463*760c253cSXin Li    opts = parser.parse_args(argv)
464*760c253cSXin Li
465*760c253cSXin Li    llvm_dir = opts.llvm_dir
466*760c253cSXin Li    if llvm_dir is None:
467*760c253cSXin Li        try:
468*760c253cSXin Li            llvm_dir = find_root_llvm_dir()
469*760c253cSXin Li        except subprocess.CalledProcessError:
470*760c253cSXin Li            parser.error(
471*760c253cSXin Li                "Couldn't autodetect an LLVM tree; please use --llvm_dir"
472*760c253cSXin Li            )
473*760c253cSXin Li
474*760c253cSXin Li    config = LLVMConfig(
475*760c253cSXin Li        remote=opts.upstream,
476*760c253cSXin Li        dir=opts.llvm_dir or find_root_llvm_dir(),
477*760c253cSXin Li    )
478*760c253cSXin Li
479*760c253cSXin Li    if opts.sha:
480*760c253cSXin Li        rev = translate_sha_to_rev(config, opts.sha)
481*760c253cSXin Li        print(rev)
482*760c253cSXin Li    else:
483*760c253cSXin Li        sha = translate_rev_to_sha(config, Rev.parse(opts.rev))
484*760c253cSXin Li        print(sha)
485*760c253cSXin Li
486*760c253cSXin Li
487*760c253cSXin Liif __name__ == "__main__":
488*760c253cSXin Li    main(sys.argv[1:])
489