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