xref: /aosp_15_r20/external/crosvm/tools/cl (revision bb4ee6a4ae7042d18b07a98463b9c8b875e44b39)
1*bb4ee6a4SAndroid Build Coastguard Worker#!/usr/bin/env python3
2*bb4ee6a4SAndroid Build Coastguard Worker# Copyright 2022 The ChromiumOS Authors
3*bb4ee6a4SAndroid Build Coastguard Worker# Use of this source code is governed by a BSD-style license that can be
4*bb4ee6a4SAndroid Build Coastguard Worker# found in the LICENSE file.
5*bb4ee6a4SAndroid Build Coastguard Worker
6*bb4ee6a4SAndroid Build Coastguard Workerimport argparse
7*bb4ee6a4SAndroid Build Coastguard Workerimport functools
8*bb4ee6a4SAndroid Build Coastguard Workerimport re
9*bb4ee6a4SAndroid Build Coastguard Workerimport sys
10*bb4ee6a4SAndroid Build Coastguard Workerfrom argh import arg  # type: ignore
11*bb4ee6a4SAndroid Build Coastguard Workerfrom os import chdir
12*bb4ee6a4SAndroid Build Coastguard Workerfrom pathlib import Path
13*bb4ee6a4SAndroid Build Coastguard Workerfrom typing import List, Optional, Tuple
14*bb4ee6a4SAndroid Build Coastguard Worker
15*bb4ee6a4SAndroid Build Coastguard Workerfrom impl.common import CROSVM_ROOT, GerritChange, cmd, confirm, run_commands
16*bb4ee6a4SAndroid Build Coastguard Worker
17*bb4ee6a4SAndroid Build Coastguard WorkerUSAGE = """\
18*bb4ee6a4SAndroid Build Coastguard Worker./tools/cl [upload|rebase|status|prune]
19*bb4ee6a4SAndroid Build Coastguard Worker
20*bb4ee6a4SAndroid Build Coastguard WorkerUpload changes to the upstream crosvm gerrit.
21*bb4ee6a4SAndroid Build Coastguard Worker
22*bb4ee6a4SAndroid Build Coastguard WorkerMultiple projects have their own downstream repository of crosvm and tooling
23*bb4ee6a4SAndroid Build Coastguard Workerto upload to those.
24*bb4ee6a4SAndroid Build Coastguard Worker
25*bb4ee6a4SAndroid Build Coastguard WorkerThis tool allows developers to send commits to the upstream gerrit review site
26*bb4ee6a4SAndroid Build Coastguard Workerof crosvm and helps rebase changes if needed.
27*bb4ee6a4SAndroid Build Coastguard Worker
28*bb4ee6a4SAndroid Build Coastguard WorkerYou need to be on a local branch tracking a remote one. `repo start` does this
29*bb4ee6a4SAndroid Build Coastguard Workerfor AOSP and chromiumos, or you can do this yourself:
30*bb4ee6a4SAndroid Build Coastguard Worker
31*bb4ee6a4SAndroid Build Coastguard Worker    $ git checkout -b mybranch --track origin/main
32*bb4ee6a4SAndroid Build Coastguard Worker
33*bb4ee6a4SAndroid Build Coastguard WorkerThen to upload commits you have made:
34*bb4ee6a4SAndroid Build Coastguard Worker
35*bb4ee6a4SAndroid Build Coastguard Worker    [mybranch] $ ./tools/cl upload
36*bb4ee6a4SAndroid Build Coastguard Worker
37*bb4ee6a4SAndroid Build Coastguard WorkerIf you are tracking a different branch (e.g. aosp/main or cros/chromeos), the upload may
38*bb4ee6a4SAndroid Build Coastguard Workerfail if your commits do not apply cleanly. This tool can help rebase the changes, it will
39*bb4ee6a4SAndroid Build Coastguard Workercreate a new branch tracking origin/main and cherry-picks your commits.
40*bb4ee6a4SAndroid Build Coastguard Worker
41*bb4ee6a4SAndroid Build Coastguard Worker    [mybranch] $ ./tools/cl rebase
42*bb4ee6a4SAndroid Build Coastguard Worker    [mybranch-upstream] ... resolve conflicts
43*bb4ee6a4SAndroid Build Coastguard Worker    [mybranch-upstream] $ git add .
44*bb4ee6a4SAndroid Build Coastguard Worker    [mybranch-upstream] $ git cherry-pick --continue
45*bb4ee6a4SAndroid Build Coastguard Worker    [mybranch-upstream] $ ./tools/cl upload
46*bb4ee6a4SAndroid Build Coastguard Worker
47*bb4ee6a4SAndroid Build Coastguard Worker"""
48*bb4ee6a4SAndroid Build Coastguard Worker
49*bb4ee6a4SAndroid Build Coastguard WorkerGERRIT_URL = "https://chromium-review.googlesource.com"
50*bb4ee6a4SAndroid Build Coastguard WorkerCROSVM_URL = "https://chromium.googlesource.com/crosvm/crosvm"
51*bb4ee6a4SAndroid Build Coastguard WorkerCROSVM_SSO = "sso://chromium/crosvm/crosvm"
52*bb4ee6a4SAndroid Build Coastguard Worker
53*bb4ee6a4SAndroid Build Coastguard Workergit = cmd("git")
54*bb4ee6a4SAndroid Build Coastguard Workercurl = cmd("curl --silent --fail")
55*bb4ee6a4SAndroid Build Coastguard Workerchmod = cmd("chmod")
56*bb4ee6a4SAndroid Build Coastguard Worker
57*bb4ee6a4SAndroid Build Coastguard Worker
58*bb4ee6a4SAndroid Build Coastguard Workerclass LocalChange(object):
59*bb4ee6a4SAndroid Build Coastguard Worker    sha: str
60*bb4ee6a4SAndroid Build Coastguard Worker    title: str
61*bb4ee6a4SAndroid Build Coastguard Worker    branch: str
62*bb4ee6a4SAndroid Build Coastguard Worker
63*bb4ee6a4SAndroid Build Coastguard Worker    def __init__(self, sha: str, title: str):
64*bb4ee6a4SAndroid Build Coastguard Worker        self.sha = sha
65*bb4ee6a4SAndroid Build Coastguard Worker        self.title = title
66*bb4ee6a4SAndroid Build Coastguard Worker
67*bb4ee6a4SAndroid Build Coastguard Worker    @classmethod
68*bb4ee6a4SAndroid Build Coastguard Worker    def list_changes(cls, branch: str):
69*bb4ee6a4SAndroid Build Coastguard Worker        upstream = get_upstream(branch)
70*bb4ee6a4SAndroid Build Coastguard Worker        for line in git(f'log "--format=%H %s" --first-parent {upstream}..{branch}').lines():
71*bb4ee6a4SAndroid Build Coastguard Worker            sha_title = line.split(" ", 1)
72*bb4ee6a4SAndroid Build Coastguard Worker            yield cls(sha_title[0], sha_title[1])
73*bb4ee6a4SAndroid Build Coastguard Worker
74*bb4ee6a4SAndroid Build Coastguard Worker    @functools.cached_property
75*bb4ee6a4SAndroid Build Coastguard Worker    def change_id(self):
76*bb4ee6a4SAndroid Build Coastguard Worker        msg = git("log -1 --format=email", self.sha).stdout()
77*bb4ee6a4SAndroid Build Coastguard Worker        match = re.search("^Change-Id: (I[a-f0-9]+)", msg, re.MULTILINE)
78*bb4ee6a4SAndroid Build Coastguard Worker        if not match:
79*bb4ee6a4SAndroid Build Coastguard Worker            return None
80*bb4ee6a4SAndroid Build Coastguard Worker        return match.group(1)
81*bb4ee6a4SAndroid Build Coastguard Worker
82*bb4ee6a4SAndroid Build Coastguard Worker    @functools.cached_property
83*bb4ee6a4SAndroid Build Coastguard Worker    def gerrit(self):
84*bb4ee6a4SAndroid Build Coastguard Worker        if not self.change_id:
85*bb4ee6a4SAndroid Build Coastguard Worker            return None
86*bb4ee6a4SAndroid Build Coastguard Worker        results = GerritChange.query("project:crosvm/crosvm", self.change_id)
87*bb4ee6a4SAndroid Build Coastguard Worker        if len(results) > 1:
88*bb4ee6a4SAndroid Build Coastguard Worker            raise Exception(f"Multiple gerrit changes found for commit {self.sha}: {self.title}.")
89*bb4ee6a4SAndroid Build Coastguard Worker        return results[0] if results else None
90*bb4ee6a4SAndroid Build Coastguard Worker
91*bb4ee6a4SAndroid Build Coastguard Worker    @property
92*bb4ee6a4SAndroid Build Coastguard Worker    def status(self):
93*bb4ee6a4SAndroid Build Coastguard Worker        if not self.gerrit:
94*bb4ee6a4SAndroid Build Coastguard Worker            return "NOT_UPLOADED"
95*bb4ee6a4SAndroid Build Coastguard Worker        else:
96*bb4ee6a4SAndroid Build Coastguard Worker            return self.gerrit.status
97*bb4ee6a4SAndroid Build Coastguard Worker
98*bb4ee6a4SAndroid Build Coastguard Worker
99*bb4ee6a4SAndroid Build Coastguard Workerdef get_upstream(branch: str = ""):
100*bb4ee6a4SAndroid Build Coastguard Worker    try:
101*bb4ee6a4SAndroid Build Coastguard Worker        return git(f"rev-parse --abbrev-ref --symbolic-full-name {branch}@{{u}}").stdout()
102*bb4ee6a4SAndroid Build Coastguard Worker    except:
103*bb4ee6a4SAndroid Build Coastguard Worker        return None
104*bb4ee6a4SAndroid Build Coastguard Worker
105*bb4ee6a4SAndroid Build Coastguard Worker
106*bb4ee6a4SAndroid Build Coastguard Workerdef list_local_branches():
107*bb4ee6a4SAndroid Build Coastguard Worker    return git("for-each-ref --format=%(refname:short) refs/heads").lines()
108*bb4ee6a4SAndroid Build Coastguard Worker
109*bb4ee6a4SAndroid Build Coastguard Worker
110*bb4ee6a4SAndroid Build Coastguard Workerdef get_active_upstream():
111*bb4ee6a4SAndroid Build Coastguard Worker    upstream = get_upstream()
112*bb4ee6a4SAndroid Build Coastguard Worker    if not upstream:
113*bb4ee6a4SAndroid Build Coastguard Worker        default_upstream = "origin/main"
114*bb4ee6a4SAndroid Build Coastguard Worker        if confirm(f"You are not tracking an upstream branch. Set upstream to {default_upstream}?"):
115*bb4ee6a4SAndroid Build Coastguard Worker            git(f"branch --set-upstream-to {default_upstream}").fg()
116*bb4ee6a4SAndroid Build Coastguard Worker            upstream = get_upstream()
117*bb4ee6a4SAndroid Build Coastguard Worker    if not upstream:
118*bb4ee6a4SAndroid Build Coastguard Worker        raise Exception("You are not tracking an upstream branch.")
119*bb4ee6a4SAndroid Build Coastguard Worker    parts = upstream.split("/")
120*bb4ee6a4SAndroid Build Coastguard Worker    if len(parts) != 2:
121*bb4ee6a4SAndroid Build Coastguard Worker        raise Exception(f"Your upstream branch '{upstream}' is not remote.")
122*bb4ee6a4SAndroid Build Coastguard Worker    return (parts[0], parts[1])
123*bb4ee6a4SAndroid Build Coastguard Worker
124*bb4ee6a4SAndroid Build Coastguard Worker
125*bb4ee6a4SAndroid Build Coastguard Workerdef prerequisites():
126*bb4ee6a4SAndroid Build Coastguard Worker    if not git("remote get-url origin").success():
127*bb4ee6a4SAndroid Build Coastguard Worker        print("Setting up origin")
128*bb4ee6a4SAndroid Build Coastguard Worker        git("remote add origin", CROSVM_URL).fg()
129*bb4ee6a4SAndroid Build Coastguard Worker    if git("remote get-url origin").stdout() not in [CROSVM_URL, CROSVM_SSO]:
130*bb4ee6a4SAndroid Build Coastguard Worker        print("Your remote 'origin' does not point to the main crosvm repository.")
131*bb4ee6a4SAndroid Build Coastguard Worker        if confirm(f"Do you want to fix it?"):
132*bb4ee6a4SAndroid Build Coastguard Worker            git("remote set-url origin", CROSVM_URL).fg()
133*bb4ee6a4SAndroid Build Coastguard Worker        else:
134*bb4ee6a4SAndroid Build Coastguard Worker            sys.exit(1)
135*bb4ee6a4SAndroid Build Coastguard Worker
136*bb4ee6a4SAndroid Build Coastguard Worker    # Install gerrit commit hook
137*bb4ee6a4SAndroid Build Coastguard Worker    hooks_dir = Path(git("rev-parse --git-path hooks").stdout())
138*bb4ee6a4SAndroid Build Coastguard Worker    hook_path = hooks_dir / "commit-msg"
139*bb4ee6a4SAndroid Build Coastguard Worker    if not hook_path.exists():
140*bb4ee6a4SAndroid Build Coastguard Worker        hook_path.parent.mkdir(exist_ok=True)
141*bb4ee6a4SAndroid Build Coastguard Worker        curl(f"{GERRIT_URL}/tools/hooks/commit-msg").write_to(hook_path)
142*bb4ee6a4SAndroid Build Coastguard Worker        chmod("+x", hook_path).fg()
143*bb4ee6a4SAndroid Build Coastguard Worker
144*bb4ee6a4SAndroid Build Coastguard Worker
145*bb4ee6a4SAndroid Build Coastguard Workerdef print_branch_summary(branch: str):
146*bb4ee6a4SAndroid Build Coastguard Worker    upstream = get_upstream(branch)
147*bb4ee6a4SAndroid Build Coastguard Worker    if not upstream:
148*bb4ee6a4SAndroid Build Coastguard Worker        print("Branch", branch, "is not tracking an upstream branch")
149*bb4ee6a4SAndroid Build Coastguard Worker        print()
150*bb4ee6a4SAndroid Build Coastguard Worker        return
151*bb4ee6a4SAndroid Build Coastguard Worker    print("Branch", branch, "tracking", upstream)
152*bb4ee6a4SAndroid Build Coastguard Worker    changes = [*LocalChange.list_changes(branch)]
153*bb4ee6a4SAndroid Build Coastguard Worker    for change in changes:
154*bb4ee6a4SAndroid Build Coastguard Worker        if change.gerrit:
155*bb4ee6a4SAndroid Build Coastguard Worker            print(" ", change.status, change.title, f"({change.gerrit.short_url()})")
156*bb4ee6a4SAndroid Build Coastguard Worker        else:
157*bb4ee6a4SAndroid Build Coastguard Worker            print(" ", change.status, change.title)
158*bb4ee6a4SAndroid Build Coastguard Worker
159*bb4ee6a4SAndroid Build Coastguard Worker    if not changes:
160*bb4ee6a4SAndroid Build Coastguard Worker        print("  No changes")
161*bb4ee6a4SAndroid Build Coastguard Worker    print()
162*bb4ee6a4SAndroid Build Coastguard Worker
163*bb4ee6a4SAndroid Build Coastguard Worker
164*bb4ee6a4SAndroid Build Coastguard Workerdef status():
165*bb4ee6a4SAndroid Build Coastguard Worker    """
166*bb4ee6a4SAndroid Build Coastguard Worker    Lists all branches and their local commits.
167*bb4ee6a4SAndroid Build Coastguard Worker    """
168*bb4ee6a4SAndroid Build Coastguard Worker    for branch in list_local_branches():
169*bb4ee6a4SAndroid Build Coastguard Worker        print_branch_summary(branch)
170*bb4ee6a4SAndroid Build Coastguard Worker
171*bb4ee6a4SAndroid Build Coastguard Worker
172*bb4ee6a4SAndroid Build Coastguard Workerdef prune(force: bool = False):
173*bb4ee6a4SAndroid Build Coastguard Worker    """
174*bb4ee6a4SAndroid Build Coastguard Worker    Deletes branches with changes that have been submitted or abandoned
175*bb4ee6a4SAndroid Build Coastguard Worker    """
176*bb4ee6a4SAndroid Build Coastguard Worker    current_branch = git("branch --show-current").stdout()
177*bb4ee6a4SAndroid Build Coastguard Worker    branches_to_delete = [
178*bb4ee6a4SAndroid Build Coastguard Worker        branch
179*bb4ee6a4SAndroid Build Coastguard Worker        for branch in list_local_branches()
180*bb4ee6a4SAndroid Build Coastguard Worker        if branch != current_branch
181*bb4ee6a4SAndroid Build Coastguard Worker        and get_upstream(branch) is not None
182*bb4ee6a4SAndroid Build Coastguard Worker        and all(
183*bb4ee6a4SAndroid Build Coastguard Worker            change.status in ["ABANDONED", "MERGED"] for change in LocalChange.list_changes(branch)
184*bb4ee6a4SAndroid Build Coastguard Worker        )
185*bb4ee6a4SAndroid Build Coastguard Worker    ]
186*bb4ee6a4SAndroid Build Coastguard Worker    if not branches_to_delete:
187*bb4ee6a4SAndroid Build Coastguard Worker        print("No obsolete branches to delete.")
188*bb4ee6a4SAndroid Build Coastguard Worker        return
189*bb4ee6a4SAndroid Build Coastguard Worker
190*bb4ee6a4SAndroid Build Coastguard Worker    print("Obsolete branches:")
191*bb4ee6a4SAndroid Build Coastguard Worker    print()
192*bb4ee6a4SAndroid Build Coastguard Worker    for branch in branches_to_delete:
193*bb4ee6a4SAndroid Build Coastguard Worker        print_branch_summary(branch)
194*bb4ee6a4SAndroid Build Coastguard Worker
195*bb4ee6a4SAndroid Build Coastguard Worker    if force or confirm("Do you want to delete the above branches?"):
196*bb4ee6a4SAndroid Build Coastguard Worker        git("branch", "-D", *branches_to_delete).fg()
197*bb4ee6a4SAndroid Build Coastguard Worker
198*bb4ee6a4SAndroid Build Coastguard Worker
199*bb4ee6a4SAndroid Build Coastguard Workerdef rebase():
200*bb4ee6a4SAndroid Build Coastguard Worker    """
201*bb4ee6a4SAndroid Build Coastguard Worker    Rebases changes from the current branch onto origin/main.
202*bb4ee6a4SAndroid Build Coastguard Worker
203*bb4ee6a4SAndroid Build Coastguard Worker    Will create a new branch called 'current-branch'-upstream tracking origin/main. Changes from
204*bb4ee6a4SAndroid Build Coastguard Worker    the current branch will then be rebased into the -upstream branch.
205*bb4ee6a4SAndroid Build Coastguard Worker    """
206*bb4ee6a4SAndroid Build Coastguard Worker    branch_name = git("branch --show-current").stdout()
207*bb4ee6a4SAndroid Build Coastguard Worker    upstream_branch_name = branch_name + "-upstream"
208*bb4ee6a4SAndroid Build Coastguard Worker
209*bb4ee6a4SAndroid Build Coastguard Worker    if git("rev-parse", upstream_branch_name).success():
210*bb4ee6a4SAndroid Build Coastguard Worker        print(f"Overwriting existing branch {upstream_branch_name}")
211*bb4ee6a4SAndroid Build Coastguard Worker        git("log -n1", upstream_branch_name).fg()
212*bb4ee6a4SAndroid Build Coastguard Worker
213*bb4ee6a4SAndroid Build Coastguard Worker    git("fetch -q origin main").fg()
214*bb4ee6a4SAndroid Build Coastguard Worker    git("checkout -B", upstream_branch_name, "origin/main").fg()
215*bb4ee6a4SAndroid Build Coastguard Worker
216*bb4ee6a4SAndroid Build Coastguard Worker    print(f"Cherry-picking changes from {branch_name}")
217*bb4ee6a4SAndroid Build Coastguard Worker    git(f"cherry-pick {branch_name}@{{u}}..{branch_name}").fg()
218*bb4ee6a4SAndroid Build Coastguard Worker
219*bb4ee6a4SAndroid Build Coastguard Worker
220*bb4ee6a4SAndroid Build Coastguard Workerdef upload(
221*bb4ee6a4SAndroid Build Coastguard Worker    dry_run: bool = False,
222*bb4ee6a4SAndroid Build Coastguard Worker    reviewer: Optional[str] = None,
223*bb4ee6a4SAndroid Build Coastguard Worker    auto_submit: bool = False,
224*bb4ee6a4SAndroid Build Coastguard Worker    submit: bool = False,
225*bb4ee6a4SAndroid Build Coastguard Worker    try_: bool = False,
226*bb4ee6a4SAndroid Build Coastguard Worker):
227*bb4ee6a4SAndroid Build Coastguard Worker    """
228*bb4ee6a4SAndroid Build Coastguard Worker    Uploads changes to the crosvm main branch.
229*bb4ee6a4SAndroid Build Coastguard Worker    """
230*bb4ee6a4SAndroid Build Coastguard Worker    remote, branch = get_active_upstream()
231*bb4ee6a4SAndroid Build Coastguard Worker    changes = [*LocalChange.list_changes("HEAD")]
232*bb4ee6a4SAndroid Build Coastguard Worker    if not changes:
233*bb4ee6a4SAndroid Build Coastguard Worker        print("No changes to upload")
234*bb4ee6a4SAndroid Build Coastguard Worker        return
235*bb4ee6a4SAndroid Build Coastguard Worker
236*bb4ee6a4SAndroid Build Coastguard Worker    print("Uploading to origin/main:")
237*bb4ee6a4SAndroid Build Coastguard Worker    for change in changes:
238*bb4ee6a4SAndroid Build Coastguard Worker        print(" ", change.sha, change.title)
239*bb4ee6a4SAndroid Build Coastguard Worker    print()
240*bb4ee6a4SAndroid Build Coastguard Worker
241*bb4ee6a4SAndroid Build Coastguard Worker    if len(changes) > 1:
242*bb4ee6a4SAndroid Build Coastguard Worker        if not confirm("Uploading {} changes, continue?".format(len(changes))):
243*bb4ee6a4SAndroid Build Coastguard Worker            return
244*bb4ee6a4SAndroid Build Coastguard Worker
245*bb4ee6a4SAndroid Build Coastguard Worker    if (remote, branch) != ("origin", "main"):
246*bb4ee6a4SAndroid Build Coastguard Worker        print(f"WARNING! Your changes are based on {remote}/{branch}, not origin/main.")
247*bb4ee6a4SAndroid Build Coastguard Worker        print("If gerrit rejects your changes, try `./tools/cl rebase -h`.")
248*bb4ee6a4SAndroid Build Coastguard Worker        print()
249*bb4ee6a4SAndroid Build Coastguard Worker        if not confirm("Upload anyway?"):
250*bb4ee6a4SAndroid Build Coastguard Worker            return
251*bb4ee6a4SAndroid Build Coastguard Worker        print()
252*bb4ee6a4SAndroid Build Coastguard Worker
253*bb4ee6a4SAndroid Build Coastguard Worker    extra_args: List[str] = []
254*bb4ee6a4SAndroid Build Coastguard Worker    if auto_submit:
255*bb4ee6a4SAndroid Build Coastguard Worker        extra_args.append("l=Auto-Submit+1")
256*bb4ee6a4SAndroid Build Coastguard Worker        try_ = True
257*bb4ee6a4SAndroid Build Coastguard Worker    if try_:
258*bb4ee6a4SAndroid Build Coastguard Worker        extra_args.append("l=Commit-Queue+1")
259*bb4ee6a4SAndroid Build Coastguard Worker    if submit:
260*bb4ee6a4SAndroid Build Coastguard Worker        extra_args.append(f"l=Commit-Queue+2")
261*bb4ee6a4SAndroid Build Coastguard Worker    if reviewer:
262*bb4ee6a4SAndroid Build Coastguard Worker        extra_args.append(f"r={reviewer}")
263*bb4ee6a4SAndroid Build Coastguard Worker
264*bb4ee6a4SAndroid Build Coastguard Worker    git(f"push origin HEAD:refs/for/main%{','.join(extra_args)}").fg(dry_run=dry_run)
265*bb4ee6a4SAndroid Build Coastguard Worker
266*bb4ee6a4SAndroid Build Coastguard Worker
267*bb4ee6a4SAndroid Build Coastguard Workerif __name__ == "__main__":
268*bb4ee6a4SAndroid Build Coastguard Worker    chdir(CROSVM_ROOT)
269*bb4ee6a4SAndroid Build Coastguard Worker    prerequisites()
270*bb4ee6a4SAndroid Build Coastguard Worker    run_commands(upload, rebase, status, prune, usage=USAGE)
271