cl 8.2 KB

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