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