xref: /aosp_15_r20/tools/external_updater/git_utils.py (revision 3c875a214f382db1236d28570d1304ce57138f32)
1*3c875a21SAndroid Build Coastguard Worker# Copyright (C) 2018 The Android Open Source Project
2*3c875a21SAndroid Build Coastguard Worker#
3*3c875a21SAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the 'License');
4*3c875a21SAndroid Build Coastguard Worker# you may not use this file except in compliance with the License.
5*3c875a21SAndroid Build Coastguard Worker# You may obtain a copy of the License at
6*3c875a21SAndroid Build Coastguard Worker#
7*3c875a21SAndroid Build Coastguard Worker#      http://www.apache.org/licenses/LICENSE-2.0
8*3c875a21SAndroid Build Coastguard Worker#
9*3c875a21SAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software
10*3c875a21SAndroid Build Coastguard Worker# distributed under the License is distributed on an 'AS IS' BASIS,
11*3c875a21SAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12*3c875a21SAndroid Build Coastguard Worker# See the License for the specific language governing permissions and
13*3c875a21SAndroid Build Coastguard Worker# limitations under the License.
14*3c875a21SAndroid Build Coastguard Worker"""Helper functions to communicate with Git."""
15*3c875a21SAndroid Build Coastguard Worker
16*3c875a21SAndroid Build Coastguard Workerimport datetime
17*3c875a21SAndroid Build Coastguard Workerimport re
18*3c875a21SAndroid Build Coastguard Workerimport subprocess
19*3c875a21SAndroid Build Coastguard Workerfrom pathlib import Path
20*3c875a21SAndroid Build Coastguard Worker
21*3c875a21SAndroid Build Coastguard Workerimport hashtags
22*3c875a21SAndroid Build Coastguard Workerimport reviewers
23*3c875a21SAndroid Build Coastguard Worker
24*3c875a21SAndroid Build Coastguard WorkerUNWANTED_TAGS = ["*alpha*", "*Alpha*", "*beta*", "*Beta*", "*rc*", "*RC*", "*test*"]
25*3c875a21SAndroid Build Coastguard Worker
26*3c875a21SAndroid Build Coastguard Worker
27*3c875a21SAndroid Build Coastguard Workerdef fetch(proj_path: Path, remote_name: str, branch: str | None = None) -> None:
28*3c875a21SAndroid Build Coastguard Worker    """Runs git fetch.
29*3c875a21SAndroid Build Coastguard Worker
30*3c875a21SAndroid Build Coastguard Worker    Args:
31*3c875a21SAndroid Build Coastguard Worker        proj_path: Path to Git repository.
32*3c875a21SAndroid Build Coastguard Worker        remote_name: A string to specify remote names.
33*3c875a21SAndroid Build Coastguard Worker    """
34*3c875a21SAndroid Build Coastguard Worker    cmd = ['git', 'fetch', '--tags', remote_name] + ([branch] if branch is not None else [])
35*3c875a21SAndroid Build Coastguard Worker    subprocess.run(cmd, capture_output=True, cwd=proj_path, check=True)
36*3c875a21SAndroid Build Coastguard Worker
37*3c875a21SAndroid Build Coastguard Worker
38*3c875a21SAndroid Build Coastguard Workerdef add_remote(proj_path: Path, name: str, url: str) -> None:
39*3c875a21SAndroid Build Coastguard Worker    """Adds a git remote.
40*3c875a21SAndroid Build Coastguard Worker
41*3c875a21SAndroid Build Coastguard Worker    Args:
42*3c875a21SAndroid Build Coastguard Worker        proj_path: Path to Git repository.
43*3c875a21SAndroid Build Coastguard Worker        name: Name of the new remote.
44*3c875a21SAndroid Build Coastguard Worker        url: Url of the new remote.
45*3c875a21SAndroid Build Coastguard Worker    """
46*3c875a21SAndroid Build Coastguard Worker    cmd = ['git', 'remote', 'add', name, url]
47*3c875a21SAndroid Build Coastguard Worker    subprocess.run(cmd, cwd=proj_path, check=True)
48*3c875a21SAndroid Build Coastguard Worker
49*3c875a21SAndroid Build Coastguard Worker
50*3c875a21SAndroid Build Coastguard Workerdef remove_remote(proj_path: Path, name: str) -> None:
51*3c875a21SAndroid Build Coastguard Worker    """Removes a git remote."""
52*3c875a21SAndroid Build Coastguard Worker    cmd = ['git', 'remote', 'remove', name]
53*3c875a21SAndroid Build Coastguard Worker    subprocess.run(cmd, cwd=proj_path, check=True)
54*3c875a21SAndroid Build Coastguard Worker
55*3c875a21SAndroid Build Coastguard Worker
56*3c875a21SAndroid Build Coastguard Workerdef list_remotes(proj_path: Path) -> dict[str, str]:
57*3c875a21SAndroid Build Coastguard Worker    """Lists all Git remotes.
58*3c875a21SAndroid Build Coastguard Worker
59*3c875a21SAndroid Build Coastguard Worker    Args:
60*3c875a21SAndroid Build Coastguard Worker        proj_path: Path to Git repository.
61*3c875a21SAndroid Build Coastguard Worker
62*3c875a21SAndroid Build Coastguard Worker    Returns:
63*3c875a21SAndroid Build Coastguard Worker        A dict from remote name to remote url.
64*3c875a21SAndroid Build Coastguard Worker    """
65*3c875a21SAndroid Build Coastguard Worker    def parse_remote(line: str) -> tuple[str, str]:
66*3c875a21SAndroid Build Coastguard Worker        split = line.split()
67*3c875a21SAndroid Build Coastguard Worker        return split[0], split[1]
68*3c875a21SAndroid Build Coastguard Worker
69*3c875a21SAndroid Build Coastguard Worker    cmd = ['git', 'remote', '-v']
70*3c875a21SAndroid Build Coastguard Worker    out = subprocess.run(cmd, capture_output=True, cwd=proj_path, check=True,
71*3c875a21SAndroid Build Coastguard Worker                         text=True).stdout
72*3c875a21SAndroid Build Coastguard Worker    lines = out.splitlines()
73*3c875a21SAndroid Build Coastguard Worker    return dict([parse_remote(line) for line in lines])
74*3c875a21SAndroid Build Coastguard Worker
75*3c875a21SAndroid Build Coastguard Worker
76*3c875a21SAndroid Build Coastguard Workerdef detect_default_branch(proj_path: Path, remote_name: str) -> str:
77*3c875a21SAndroid Build Coastguard Worker    """Gets the name of the upstream's default branch to use."""
78*3c875a21SAndroid Build Coastguard Worker    cmd = ['git', 'remote', 'show', remote_name]
79*3c875a21SAndroid Build Coastguard Worker    out = subprocess.run(cmd, capture_output=True, cwd=proj_path, check=True,
80*3c875a21SAndroid Build Coastguard Worker                         text=True).stdout
81*3c875a21SAndroid Build Coastguard Worker    lines = out.splitlines()
82*3c875a21SAndroid Build Coastguard Worker    for line in lines:
83*3c875a21SAndroid Build Coastguard Worker        if "HEAD branch" in line:
84*3c875a21SAndroid Build Coastguard Worker            return line.split()[-1]
85*3c875a21SAndroid Build Coastguard Worker    raise RuntimeError(
86*3c875a21SAndroid Build Coastguard Worker        f"Could not find HEAD branch in 'git remote show {remote_name}'"
87*3c875a21SAndroid Build Coastguard Worker    )
88*3c875a21SAndroid Build Coastguard Worker
89*3c875a21SAndroid Build Coastguard Worker
90*3c875a21SAndroid Build Coastguard Workerdef get_sha_for_branch(proj_path: Path, branch: str):
91*3c875a21SAndroid Build Coastguard Worker    """Gets the hash SHA for a branch."""
92*3c875a21SAndroid Build Coastguard Worker    cmd = ['git', 'rev-parse', branch]
93*3c875a21SAndroid Build Coastguard Worker    return subprocess.run(cmd, capture_output=True, cwd=proj_path, check=True,
94*3c875a21SAndroid Build Coastguard Worker                          text=True).stdout.strip()
95*3c875a21SAndroid Build Coastguard Worker
96*3c875a21SAndroid Build Coastguard Worker
97*3c875a21SAndroid Build Coastguard Workerdef get_most_recent_tag(proj_path: Path, branch: str) -> str | None:
98*3c875a21SAndroid Build Coastguard Worker    """Finds the most recent tag that is reachable from HEAD."""
99*3c875a21SAndroid Build Coastguard Worker    cmd = ['git', 'describe', '--tags', branch, '--abbrev=0'] + \
100*3c875a21SAndroid Build Coastguard Worker          [f'--exclude={unwanted_tag}' for unwanted_tag in UNWANTED_TAGS]
101*3c875a21SAndroid Build Coastguard Worker    try:
102*3c875a21SAndroid Build Coastguard Worker        out = subprocess.run(cmd, capture_output=True, cwd=proj_path, check=True,
103*3c875a21SAndroid Build Coastguard Worker                            text=True).stdout.strip()
104*3c875a21SAndroid Build Coastguard Worker        return out
105*3c875a21SAndroid Build Coastguard Worker    except subprocess.CalledProcessError as ex:
106*3c875a21SAndroid Build Coastguard Worker        if "fatal: No names found" in ex.stderr:
107*3c875a21SAndroid Build Coastguard Worker            return None
108*3c875a21SAndroid Build Coastguard Worker        if "fatal: No tags can describe" in ex.stderr:
109*3c875a21SAndroid Build Coastguard Worker            return None
110*3c875a21SAndroid Build Coastguard Worker        raise
111*3c875a21SAndroid Build Coastguard Worker
112*3c875a21SAndroid Build Coastguard Worker
113*3c875a21SAndroid Build Coastguard Worker# pylint: disable=redefined-outer-name
114*3c875a21SAndroid Build Coastguard Workerdef get_commit_time(proj_path: Path, commit: str) -> datetime.datetime:
115*3c875a21SAndroid Build Coastguard Worker    """Gets commit time of one commit."""
116*3c875a21SAndroid Build Coastguard Worker    cmd = ['git', 'show', '-s', '--format=%ct', commit]
117*3c875a21SAndroid Build Coastguard Worker    out = subprocess.run(cmd, capture_output=True, cwd=proj_path, check=True,
118*3c875a21SAndroid Build Coastguard Worker                         text=True).stdout
119*3c875a21SAndroid Build Coastguard Worker    return datetime.datetime.fromtimestamp(int(out.strip()))
120*3c875a21SAndroid Build Coastguard Worker
121*3c875a21SAndroid Build Coastguard Worker
122*3c875a21SAndroid Build Coastguard Workerdef list_remote_branches(proj_path: Path, remote_name: str) -> list[str]:
123*3c875a21SAndroid Build Coastguard Worker    """Lists all branches for a remote."""
124*3c875a21SAndroid Build Coastguard Worker    cmd = ['git', 'branch', '-r']
125*3c875a21SAndroid Build Coastguard Worker    lines = subprocess.run(cmd, capture_output=True, cwd=proj_path, check=True,
126*3c875a21SAndroid Build Coastguard Worker                           text=True).stdout.splitlines()
127*3c875a21SAndroid Build Coastguard Worker    stripped = [line.strip() for line in lines]
128*3c875a21SAndroid Build Coastguard Worker    remote_path = remote_name + '/'
129*3c875a21SAndroid Build Coastguard Worker    return [
130*3c875a21SAndroid Build Coastguard Worker        line[len(remote_path):] for line in stripped
131*3c875a21SAndroid Build Coastguard Worker        if line.startswith(remote_path)
132*3c875a21SAndroid Build Coastguard Worker    ]
133*3c875a21SAndroid Build Coastguard Worker
134*3c875a21SAndroid Build Coastguard Worker
135*3c875a21SAndroid Build Coastguard Workerdef list_local_branches(proj_path: Path) -> list[str]:
136*3c875a21SAndroid Build Coastguard Worker    """Lists all local branches."""
137*3c875a21SAndroid Build Coastguard Worker    cmd = ['git', 'branch', '--format=%(refname:short)']
138*3c875a21SAndroid Build Coastguard Worker    lines = subprocess.run(cmd, capture_output=True, cwd=proj_path, check=True,
139*3c875a21SAndroid Build Coastguard Worker                           text=True).stdout.splitlines()
140*3c875a21SAndroid Build Coastguard Worker    return lines
141*3c875a21SAndroid Build Coastguard Worker
142*3c875a21SAndroid Build Coastguard Worker
143*3c875a21SAndroid Build Coastguard WorkerCOMMIT_PATTERN = r'^[a-f0-9]{40}$'
144*3c875a21SAndroid Build Coastguard WorkerCOMMIT_RE = re.compile(COMMIT_PATTERN)
145*3c875a21SAndroid Build Coastguard Worker
146*3c875a21SAndroid Build Coastguard Worker
147*3c875a21SAndroid Build Coastguard Worker# pylint: disable=redefined-outer-name
148*3c875a21SAndroid Build Coastguard Workerdef is_commit(commit: str) -> bool:
149*3c875a21SAndroid Build Coastguard Worker    """Whether a string looks like a SHA1 hash."""
150*3c875a21SAndroid Build Coastguard Worker    return bool(COMMIT_RE.match(commit))
151*3c875a21SAndroid Build Coastguard Worker
152*3c875a21SAndroid Build Coastguard Worker
153*3c875a21SAndroid Build Coastguard Workerdef merge(proj_path: Path, branch: str) -> None:
154*3c875a21SAndroid Build Coastguard Worker    """Merges a branch."""
155*3c875a21SAndroid Build Coastguard Worker    try:
156*3c875a21SAndroid Build Coastguard Worker        cmd = ['git', 'merge', branch, '--no-commit']
157*3c875a21SAndroid Build Coastguard Worker        subprocess.run(cmd, cwd=proj_path, check=True)
158*3c875a21SAndroid Build Coastguard Worker    except subprocess.CalledProcessError as err:
159*3c875a21SAndroid Build Coastguard Worker        if hasattr(err, "output"):
160*3c875a21SAndroid Build Coastguard Worker            print(err.output)
161*3c875a21SAndroid Build Coastguard Worker        if not merge_conflict(proj_path):
162*3c875a21SAndroid Build Coastguard Worker            raise
163*3c875a21SAndroid Build Coastguard Worker
164*3c875a21SAndroid Build Coastguard Worker
165*3c875a21SAndroid Build Coastguard Workerdef merge_conflict(proj_path: Path) -> bool:
166*3c875a21SAndroid Build Coastguard Worker    """Checks if there was a merge conflict."""
167*3c875a21SAndroid Build Coastguard Worker    cmd = ['git', 'ls-files', '--unmerged']
168*3c875a21SAndroid Build Coastguard Worker    out = subprocess.run(cmd, capture_output=True, cwd=proj_path, check=True,
169*3c875a21SAndroid Build Coastguard Worker                         text=True).stdout
170*3c875a21SAndroid Build Coastguard Worker    return bool(out)
171*3c875a21SAndroid Build Coastguard Worker
172*3c875a21SAndroid Build Coastguard Worker
173*3c875a21SAndroid Build Coastguard Workerdef add_file(proj_path: Path, file_name: str) -> None:
174*3c875a21SAndroid Build Coastguard Worker    """Stages a file."""
175*3c875a21SAndroid Build Coastguard Worker    cmd = ['git', 'add', file_name]
176*3c875a21SAndroid Build Coastguard Worker    subprocess.run(cmd, cwd=proj_path, check=True)
177*3c875a21SAndroid Build Coastguard Worker
178*3c875a21SAndroid Build Coastguard Worker
179*3c875a21SAndroid Build Coastguard Workerdef remove_gitmodules(proj_path: Path) -> None:
180*3c875a21SAndroid Build Coastguard Worker    """Deletes .gitmodules files."""
181*3c875a21SAndroid Build Coastguard Worker    cmd = ['find', '.', '-name', '.gitmodules', '-delete']
182*3c875a21SAndroid Build Coastguard Worker    subprocess.run(cmd, cwd=proj_path, check=True)
183*3c875a21SAndroid Build Coastguard Worker
184*3c875a21SAndroid Build Coastguard Worker
185*3c875a21SAndroid Build Coastguard Workerdef delete_branch(proj_path: Path, branch_name: str) -> None:
186*3c875a21SAndroid Build Coastguard Worker    """Force delete a branch."""
187*3c875a21SAndroid Build Coastguard Worker    cmd = ['git', 'branch', '-D', branch_name]
188*3c875a21SAndroid Build Coastguard Worker    subprocess.run(cmd, cwd=proj_path, check=True)
189*3c875a21SAndroid Build Coastguard Worker
190*3c875a21SAndroid Build Coastguard Worker
191*3c875a21SAndroid Build Coastguard Workerdef start_branch(proj_path: Path, branch_name: str) -> None:
192*3c875a21SAndroid Build Coastguard Worker    """Starts a new repo branch."""
193*3c875a21SAndroid Build Coastguard Worker    subprocess.run(['repo', 'start', branch_name], cwd=proj_path, check=True)
194*3c875a21SAndroid Build Coastguard Worker
195*3c875a21SAndroid Build Coastguard Worker
196*3c875a21SAndroid Build Coastguard Workerdef commit(proj_path: Path, message: str, no_verify: bool) -> None:
197*3c875a21SAndroid Build Coastguard Worker    """Commits changes."""
198*3c875a21SAndroid Build Coastguard Worker    cmd = ['git', 'commit', '-m', message] + (['--no-verify'] if no_verify is True else [])
199*3c875a21SAndroid Build Coastguard Worker    subprocess.run(cmd, cwd=proj_path, check=True)
200*3c875a21SAndroid Build Coastguard Worker
201*3c875a21SAndroid Build Coastguard Worker
202*3c875a21SAndroid Build Coastguard Workerdef commit_amend(proj_path: Path) -> None:
203*3c875a21SAndroid Build Coastguard Worker    """Commits changes."""
204*3c875a21SAndroid Build Coastguard Worker    cmd = ['git', 'commit', '--amend', '--no-edit']
205*3c875a21SAndroid Build Coastguard Worker    subprocess.run(cmd, cwd=proj_path, check=True)
206*3c875a21SAndroid Build Coastguard Worker
207*3c875a21SAndroid Build Coastguard Worker
208*3c875a21SAndroid Build Coastguard Workerdef checkout(proj_path: Path, branch_name: str) -> None:
209*3c875a21SAndroid Build Coastguard Worker    """Checkouts a branch."""
210*3c875a21SAndroid Build Coastguard Worker    cmd = ['git', 'checkout', branch_name]
211*3c875a21SAndroid Build Coastguard Worker    subprocess.run(cmd, cwd=proj_path, check=True)
212*3c875a21SAndroid Build Coastguard Worker
213*3c875a21SAndroid Build Coastguard Worker
214*3c875a21SAndroid Build Coastguard Workerdef detach_to_android_head(proj_path: Path) -> None:
215*3c875a21SAndroid Build Coastguard Worker    """Detaches the project HEAD back to the manifest revision."""
216*3c875a21SAndroid Build Coastguard Worker    # -d detaches the project back to the manifest revision without updating.
217*3c875a21SAndroid Build Coastguard Worker    # -l avoids fetching new revisions from the remote. This might be superfluous with
218*3c875a21SAndroid Build Coastguard Worker    # -d, but I'm not sure, and it certainly doesn't harm anything.
219*3c875a21SAndroid Build Coastguard Worker    subprocess.run(['repo', 'sync', '-l', '-d', proj_path], cwd=proj_path, check=True)
220*3c875a21SAndroid Build Coastguard Worker
221*3c875a21SAndroid Build Coastguard Worker
222*3c875a21SAndroid Build Coastguard Workerdef push(proj_path: Path, remote_name: str, has_errors: bool) -> None:
223*3c875a21SAndroid Build Coastguard Worker    """Pushes change to remote."""
224*3c875a21SAndroid Build Coastguard Worker    cmd = ['git', 'push', remote_name, 'HEAD:refs/for/main', '-o', 'banned-words~skip']
225*3c875a21SAndroid Build Coastguard Worker    if revs := reviewers.find_reviewers(str(proj_path)):
226*3c875a21SAndroid Build Coastguard Worker        cmd.extend(['-o', revs])
227*3c875a21SAndroid Build Coastguard Worker    if tag := hashtags.find_hashtag(proj_path):
228*3c875a21SAndroid Build Coastguard Worker        cmd.extend(['-o', 't=' + tag])
229*3c875a21SAndroid Build Coastguard Worker    if has_errors:
230*3c875a21SAndroid Build Coastguard Worker        cmd.extend(['-o', 'l=Verified-1'])
231*3c875a21SAndroid Build Coastguard Worker    subprocess.run(cmd, cwd=proj_path, check=True)
232*3c875a21SAndroid Build Coastguard Worker
233*3c875a21SAndroid Build Coastguard Worker
234*3c875a21SAndroid Build Coastguard Workerdef reset_hard(proj_path: Path) -> None:
235*3c875a21SAndroid Build Coastguard Worker    """Resets current HEAD and discards changes to tracked files."""
236*3c875a21SAndroid Build Coastguard Worker    cmd = ['git', 'reset', '--hard']
237*3c875a21SAndroid Build Coastguard Worker    subprocess.run(cmd, cwd=proj_path, check=True)
238*3c875a21SAndroid Build Coastguard Worker
239*3c875a21SAndroid Build Coastguard Worker
240*3c875a21SAndroid Build Coastguard Workerdef clean(proj_path: Path) -> None:
241*3c875a21SAndroid Build Coastguard Worker    """Removes untracked files and directories."""
242*3c875a21SAndroid Build Coastguard Worker    cmd = ['git', 'clean', '-fdx']
243*3c875a21SAndroid Build Coastguard Worker    subprocess.run(cmd, cwd=proj_path, check=True)
244*3c875a21SAndroid Build Coastguard Worker
245*3c875a21SAndroid Build Coastguard Worker
246*3c875a21SAndroid Build Coastguard Workerdef is_valid_url(proj_path: Path, url: str) -> bool:
247*3c875a21SAndroid Build Coastguard Worker    cmd = ['git', "ls-remote", url]
248*3c875a21SAndroid Build Coastguard Worker    return subprocess.run(cmd, cwd=proj_path, check=False, stdin=subprocess.DEVNULL,
249*3c875a21SAndroid Build Coastguard Worker                          stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
250*3c875a21SAndroid Build Coastguard Worker                          start_new_session=True).returncode == 0
251*3c875a21SAndroid Build Coastguard Worker
252*3c875a21SAndroid Build Coastguard Worker
253*3c875a21SAndroid Build Coastguard Workerdef list_remote_tags(proj_path: Path, remote_name: str) -> list[str]:
254*3c875a21SAndroid Build Coastguard Worker    """Lists tags in a remote repository."""
255*3c875a21SAndroid Build Coastguard Worker    cmd = ['git', "ls-remote", "--tags", remote_name]
256*3c875a21SAndroid Build Coastguard Worker    out = subprocess.run(cmd, capture_output=True, cwd=proj_path, check=True,
257*3c875a21SAndroid Build Coastguard Worker                         text=True).stdout
258*3c875a21SAndroid Build Coastguard Worker    lines = out.splitlines()
259*3c875a21SAndroid Build Coastguard Worker    return lines
260*3c875a21SAndroid Build Coastguard Worker
261*3c875a21SAndroid Build Coastguard Worker
262*3c875a21SAndroid Build Coastguard Workerdef diff(proj_path: Path, diff_filter: str, revision: str) -> str:
263*3c875a21SAndroid Build Coastguard Worker    try:
264*3c875a21SAndroid Build Coastguard Worker        cmd = ['git', 'diff', revision, '--stat', f'--diff-filter={diff_filter}']
265*3c875a21SAndroid Build Coastguard Worker        out = subprocess.run(cmd, capture_output=True, cwd=proj_path,
266*3c875a21SAndroid Build Coastguard Worker                             check=True, text=True).stdout
267*3c875a21SAndroid Build Coastguard Worker        return out
268*3c875a21SAndroid Build Coastguard Worker    except subprocess.CalledProcessError as err:
269*3c875a21SAndroid Build Coastguard Worker        return f"Could not calculate the diff: {err}"
270*3c875a21SAndroid Build Coastguard Worker
271*3c875a21SAndroid Build Coastguard Worker
272*3c875a21SAndroid Build Coastguard Workerdef is_ancestor(proj_path: Path, ancestor: str, child: str) -> bool:
273*3c875a21SAndroid Build Coastguard Worker    cmd = ['git', 'merge-base', '--is-ancestor', ancestor, child]
274*3c875a21SAndroid Build Coastguard Worker    # https://git-scm.com/docs/git-merge-base#Documentation/git-merge-base.txt---is-ancestor
275*3c875a21SAndroid Build Coastguard Worker    # Exit status of 0 means yes, 1 means no, and all others mean an error occurred.
276*3c875a21SAndroid Build Coastguard Worker    # Although a commit is an ancestor of itself, we don't want to return True
277*3c875a21SAndroid Build Coastguard Worker    # if ancestor points to the same commit as child.
278*3c875a21SAndroid Build Coastguard Worker    if get_sha_for_branch(proj_path, ancestor) == child:
279*3c875a21SAndroid Build Coastguard Worker        return False
280*3c875a21SAndroid Build Coastguard Worker    try:
281*3c875a21SAndroid Build Coastguard Worker        subprocess.run(
282*3c875a21SAndroid Build Coastguard Worker            cmd,
283*3c875a21SAndroid Build Coastguard Worker            cwd=proj_path,
284*3c875a21SAndroid Build Coastguard Worker            text=True,
285*3c875a21SAndroid Build Coastguard Worker            stderr=subprocess.STDOUT,
286*3c875a21SAndroid Build Coastguard Worker            check=True,
287*3c875a21SAndroid Build Coastguard Worker            stdout=subprocess.PIPE
288*3c875a21SAndroid Build Coastguard Worker        )
289*3c875a21SAndroid Build Coastguard Worker        return True
290*3c875a21SAndroid Build Coastguard Worker    except subprocess.CalledProcessError as ex:
291*3c875a21SAndroid Build Coastguard Worker        if ex.returncode == 1:
292*3c875a21SAndroid Build Coastguard Worker            return False
293*3c875a21SAndroid Build Coastguard Worker        raise
294*3c875a21SAndroid Build Coastguard Worker
295*3c875a21SAndroid Build Coastguard Worker
296*3c875a21SAndroid Build Coastguard Workerdef list_branches_with_commit(proj_path: Path, commit: str, remote_name: str) -> list[str]:
297*3c875a21SAndroid Build Coastguard Worker    """Lists upstream branches which contain the specified commit"""
298*3c875a21SAndroid Build Coastguard Worker    cmd = ['git', 'branch', '-r', '--contains', commit]
299*3c875a21SAndroid Build Coastguard Worker    out = subprocess.run(cmd, capture_output=True, cwd=proj_path, check=True,
300*3c875a21SAndroid Build Coastguard Worker                         text=True).stdout
301*3c875a21SAndroid Build Coastguard Worker    lines = out.splitlines()
302*3c875a21SAndroid Build Coastguard Worker    remote_branches = [line for line in lines if remote_name in line]
303*3c875a21SAndroid Build Coastguard Worker    return remote_branches
304