Pulser | 72e2324 | 2013-09-29 09:56:55 +0100 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | # |
dianlujitao | e34c26d | 2024-01-13 17:33:41 +0800 | [diff] [blame] | 3 | # Copyright (C) 2013-2015 The CyanogenMod Project |
| 4 | # (C) 2017-2024 The LineageOS Project |
Pulser | 72e2324 | 2013-09-29 09:56:55 +0100 | [diff] [blame] | 5 | # |
| 6 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| 7 | # you may not use this file except in compliance with the License. |
| 8 | # You may obtain a copy of the License at |
| 9 | # |
| 10 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 11 | # |
| 12 | # Unless required by applicable law or agreed to in writing, software |
| 13 | # distributed under the License is distributed on an "AS IS" BASIS, |
| 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 15 | # See the License for the specific language governing permissions and |
| 16 | # limitations under the License. |
| 17 | # |
| 18 | |
| 19 | # |
| 20 | # Run repopick.py -h for a description of this utility. |
| 21 | # |
| 22 | |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 23 | import argparse |
Pulser | 72e2324 | 2013-09-29 09:56:55 +0100 | [diff] [blame] | 24 | import json |
| 25 | import os |
Pulser | 72e2324 | 2013-09-29 09:56:55 +0100 | [diff] [blame] | 26 | import re |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 27 | import subprocess |
| 28 | import sys |
Pulser | 72e2324 | 2013-09-29 09:56:55 +0100 | [diff] [blame] | 29 | import textwrap |
dianlujitao | 87a692f | 2024-01-14 17:01:12 +0800 | [diff] [blame] | 30 | import urllib.parse |
| 31 | import urllib.request |
dianlujitao | 324541c | 2024-01-13 21:27:00 +0800 | [diff] [blame] | 32 | from collections import defaultdict |
dianlujitao | 4947532 | 2024-01-13 23:56:59 +0800 | [diff] [blame] | 33 | from concurrent.futures import ThreadPoolExecutor |
| 34 | from functools import cmp_to_key, partial |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 35 | from xml.etree import ElementTree |
Pulser | 72e2324 | 2013-09-29 09:56:55 +0100 | [diff] [blame] | 36 | |
Pulser | 72e2324 | 2013-09-29 09:56:55 +0100 | [diff] [blame] | 37 | |
Luca Weiss | d1bbac6 | 2018-11-25 14:07:12 +0100 | [diff] [blame] | 38 | # cmp() is not available in Python 3, define it manually |
| 39 | # See https://docs.python.org/3.0/whatsnew/3.0.html#ordering-comparisons |
| 40 | def cmp(a, b): |
| 41 | return (a > b) - (a < b) |
| 42 | |
| 43 | |
Pulser | 72e2324 | 2013-09-29 09:56:55 +0100 | [diff] [blame] | 44 | # Verifies whether pathA is a subdirectory (or the same) as pathB |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 45 | def is_subdir(a, b): |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 46 | a = os.path.realpath(a) + "/" |
| 47 | b = os.path.realpath(b) + "/" |
| 48 | return b == a[: len(b)] |
Pulser | 72e2324 | 2013-09-29 09:56:55 +0100 | [diff] [blame] | 49 | |
Pulser | 72e2324 | 2013-09-29 09:56:55 +0100 | [diff] [blame] | 50 | |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 51 | def fetch_query_via_ssh(remote_url, query): |
| 52 | """Given a remote_url and a query, return the list of changes that fit it |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 53 | This function is slightly messy - the ssh api does not return data in the same structure as the HTTP REST API |
| 54 | We have to get the data, then transform it to match what we're expecting from the HTTP RESET API |
| 55 | """ |
| 56 | if remote_url.count(":") == 2: |
dianlujitao | 99e500b | 2024-01-14 17:27:02 +0800 | [diff] [blame] | 57 | (_, userhost, port) = remote_url.split(":") |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 58 | userhost = userhost[2:] |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 59 | elif remote_url.count(":") == 1: |
dianlujitao | 99e500b | 2024-01-14 17:27:02 +0800 | [diff] [blame] | 60 | (_, userhost) = remote_url.split(":") |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 61 | userhost = userhost[2:] |
dianlujitao | 20f0bdb | 2024-01-14 14:57:41 +0800 | [diff] [blame] | 62 | port = "29418" |
Pulser | 72e2324 | 2013-09-29 09:56:55 +0100 | [diff] [blame] | 63 | else: |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 64 | raise Exception("Malformed URI: Expecting ssh://[user@]host[:port]") |
Pulser | 72e2324 | 2013-09-29 09:56:55 +0100 | [diff] [blame] | 65 | |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 66 | out = subprocess.check_output( |
| 67 | [ |
| 68 | "ssh", |
| 69 | "-x", |
dianlujitao | 20f0bdb | 2024-01-14 14:57:41 +0800 | [diff] [blame] | 70 | "-p", |
| 71 | port, |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 72 | userhost, |
| 73 | "gerrit", |
| 74 | "query", |
dianlujitao | 20f0bdb | 2024-01-14 14:57:41 +0800 | [diff] [blame] | 75 | "--format", |
| 76 | "JSON", |
| 77 | "--patch-sets", |
| 78 | "--current-patch-set", |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 79 | query, |
dianlujitao | 20f0bdb | 2024-01-14 14:57:41 +0800 | [diff] [blame] | 80 | ], |
| 81 | text=True, |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 82 | ) |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 83 | reviews = [] |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 84 | for line in out.split("\n"): |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 85 | try: |
| 86 | data = json.loads(line) |
| 87 | # make our data look like the http rest api data |
| 88 | review = { |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 89 | "branch": data["branch"], |
| 90 | "change_id": data["id"], |
| 91 | "current_revision": data["currentPatchSet"]["revision"], |
| 92 | "number": int(data["number"]), |
| 93 | "revisions": { |
| 94 | patch_set["revision"]: { |
| 95 | "_number": int(patch_set["number"]), |
| 96 | "fetch": { |
| 97 | "ssh": { |
| 98 | "ref": patch_set["ref"], |
| 99 | "url": "ssh://{0}:{1}/{2}".format( |
| 100 | userhost, port, data["project"] |
| 101 | ), |
| 102 | } |
| 103 | }, |
| 104 | "commit": { |
| 105 | "parents": [ |
| 106 | {"commit": parent} for parent in patch_set["parents"] |
| 107 | ] |
| 108 | }, |
| 109 | } |
| 110 | for patch_set in data["patchSets"] |
| 111 | }, |
| 112 | "subject": data["subject"], |
| 113 | "project": data["project"], |
| 114 | "status": data["status"], |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 115 | } |
| 116 | reviews.append(review) |
dianlujitao | 99e500b | 2024-01-14 17:27:02 +0800 | [diff] [blame] | 117 | except Exception: |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 118 | pass |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 119 | return reviews |
Pulser | 72e2324 | 2013-09-29 09:56:55 +0100 | [diff] [blame] | 120 | |
Pulser | 72e2324 | 2013-09-29 09:56:55 +0100 | [diff] [blame] | 121 | |
dianlujitao | 87a692f | 2024-01-14 17:01:12 +0800 | [diff] [blame] | 122 | def build_query_url(remote_url, query, auth): |
| 123 | p = urllib.parse.urlparse(remote_url)._asdict() |
| 124 | p["path"] = ("/a" if auth else "") + "/changes" |
| 125 | p["query"] = urllib.parse.urlencode( |
| 126 | { |
| 127 | "q": query, |
| 128 | "o": ["CURRENT_REVISION", "ALL_REVISIONS", "ALL_COMMITS"], |
| 129 | }, |
| 130 | doseq=True, |
| 131 | ) |
| 132 | return urllib.parse.urlunparse(urllib.parse.ParseResult(**p)) |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 133 | |
dianlujitao | 87a692f | 2024-01-14 17:01:12 +0800 | [diff] [blame] | 134 | |
| 135 | def fetch_query_via_http(remote_url, query, auth=True): |
| 136 | """Given a query, fetch the change numbers via http""" |
| 137 | if auth: |
| 138 | gerritrc = os.path.expanduser("~/.gerritrc") |
| 139 | username = password = "" |
| 140 | if os.path.isfile(gerritrc): |
| 141 | with open(gerritrc, "r") as f: |
| 142 | for line in f: |
| 143 | parts = line.rstrip().split("|") |
| 144 | if parts[0] in remote_url: |
| 145 | username, password = parts[1], parts[2] |
| 146 | |
| 147 | if username and password: |
| 148 | url = build_query_url(remote_url, query, auth) |
| 149 | password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm() |
| 150 | password_mgr.add_password(None, url, username, password) |
| 151 | auth_handler = urllib.request.HTTPBasicAuthHandler(password_mgr) |
| 152 | opener = urllib.request.build_opener(auth_handler) |
| 153 | response = opener.open(url) |
| 154 | if response.getcode() != 200: |
| 155 | # They didn't get good authorization or data, Let's try the old way |
| 156 | return fetch_query_via_http(remote_url, query, False) |
| 157 | else: |
| 158 | return fetch_query_via_http(remote_url, query, False) |
| 159 | else: |
| 160 | url = build_query_url(remote_url, query, auth) |
| 161 | response = urllib.request.urlopen(url) |
| 162 | |
| 163 | data = response.read().decode("utf-8") |
| 164 | reviews = json.loads(data[5:]) |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 165 | for review in reviews: |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 166 | review["number"] = review.pop("_number") |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 167 | |
| 168 | return reviews |
| 169 | |
| 170 | |
| 171 | def fetch_query(remote_url, query): |
| 172 | """Wrapper for fetch_query_via_proto functions""" |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 173 | if remote_url[0:3] == "ssh": |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 174 | return fetch_query_via_ssh(remote_url, query) |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 175 | elif remote_url[0:4] == "http": |
dianlujitao | 87a692f | 2024-01-14 17:01:12 +0800 | [diff] [blame] | 176 | return fetch_query_via_http(remote_url, query) |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 177 | else: |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 178 | raise Exception( |
| 179 | "Gerrit URL should be in the form http[s]://hostname/ or ssh://[user@]host[:port]" |
| 180 | ) |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 181 | |
Aayush Gupta | 2f4522c | 2020-07-26 07:19:19 +0000 | [diff] [blame] | 182 | |
dianlujitao | 324541c | 2024-01-13 21:27:00 +0800 | [diff] [blame] | 183 | def is_closed(status): |
| 184 | return status not in ("OPEN", "NEW", "DRAFT") |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 185 | |
dianlujitao | 324541c | 2024-01-13 21:27:00 +0800 | [diff] [blame] | 186 | |
dianlujitao | 27e1899 | 2024-02-08 20:23:35 +0800 | [diff] [blame^] | 187 | def is_omnirom_gerrit(remote_url): |
| 188 | p = urllib.parse.urlparse(remote_url) |
| 189 | return p.hostname == "gerrit.omnirom.org" |
| 190 | |
| 191 | |
dianlujitao | 4947532 | 2024-01-13 23:56:59 +0800 | [diff] [blame] | 192 | def commit_exists(project_path, revision): |
| 193 | return ( |
| 194 | subprocess.call( |
| 195 | ["git", "cat-file", "-e", revision], |
| 196 | cwd=project_path, |
| 197 | stderr=subprocess.DEVNULL, |
| 198 | ) |
| 199 | == 0 |
| 200 | ) |
| 201 | |
| 202 | |
dianlujitao | 324541c | 2024-01-13 21:27:00 +0800 | [diff] [blame] | 203 | def main(): |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 204 | parser = argparse.ArgumentParser( |
| 205 | formatter_class=argparse.RawDescriptionHelpFormatter, |
| 206 | description=textwrap.dedent( |
| 207 | """\ |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 208 | repopick.py is a utility to simplify the process of cherry picking |
| 209 | patches from OmniRom's Gerrit instance (or any gerrit instance of your choosing) |
| 210 | |
| 211 | Given a list of change numbers, repopick will cd into the project path |
| 212 | and cherry pick the latest patch available. |
| 213 | |
| 214 | With the --start-branch argument, the user can specify that a branch |
| 215 | should be created before cherry picking. This is useful for |
| 216 | cherry-picking many patches into a common branch which can be easily |
| 217 | abandoned later (good for testing other's changes.) |
| 218 | |
| 219 | The --abandon-first argument, when used in conjunction with the |
| 220 | --start-branch option, will cause repopick to abandon the specified |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 221 | branch in all repos first before performing any cherry picks.""" |
| 222 | ), |
| 223 | ) |
| 224 | parser.add_argument( |
| 225 | "change_number", |
| 226 | nargs="*", |
| 227 | help="change number to cherry pick. Use {change number}/{patchset number} to get a specific revision.", |
| 228 | ) |
| 229 | parser.add_argument( |
| 230 | "-i", |
| 231 | "--ignore-missing", |
| 232 | action="store_true", |
| 233 | help="do not error out if a patch applies to a missing directory", |
| 234 | ) |
| 235 | parser.add_argument( |
| 236 | "-s", |
| 237 | "--start-branch", |
| 238 | nargs=1, |
| 239 | metavar="", |
| 240 | help="start the specified branch before cherry picking", |
| 241 | ) |
| 242 | parser.add_argument( |
| 243 | "-r", |
| 244 | "--reset", |
| 245 | action="store_true", |
| 246 | help="reset to initial state (abort cherry-pick) if there is a conflict", |
| 247 | ) |
| 248 | parser.add_argument( |
| 249 | "-a", |
| 250 | "--abandon-first", |
| 251 | action="store_true", |
| 252 | help="before cherry picking, abandon the branch specified in --start-branch", |
| 253 | ) |
| 254 | parser.add_argument( |
| 255 | "-b", |
| 256 | "--auto-branch", |
| 257 | action="store_true", |
| 258 | help='shortcut to "--start-branch auto --abandon-first --ignore-missing"', |
| 259 | ) |
| 260 | parser.add_argument( |
| 261 | "-q", "--quiet", action="store_true", help="print as little as possible" |
| 262 | ) |
| 263 | parser.add_argument( |
| 264 | "-v", |
| 265 | "--verbose", |
| 266 | action="store_true", |
| 267 | help="print extra information to aid in debug", |
| 268 | ) |
| 269 | parser.add_argument( |
| 270 | "-f", |
| 271 | "--force", |
| 272 | action="store_true", |
| 273 | help="force cherry pick even if change is closed", |
| 274 | ) |
| 275 | parser.add_argument( |
| 276 | "-p", "--pull", action="store_true", help="execute pull instead of cherry-pick" |
| 277 | ) |
| 278 | parser.add_argument( |
| 279 | "-P", "--path", metavar="", help="use the specified path for the change" |
| 280 | ) |
| 281 | parser.add_argument( |
| 282 | "-t", "--topic", metavar="", help="pick all commits from a specified topic" |
| 283 | ) |
| 284 | parser.add_argument( |
| 285 | "-Q", "--query", metavar="", help="pick all commits using the specified query" |
| 286 | ) |
| 287 | parser.add_argument( |
| 288 | "-g", |
| 289 | "--gerrit", |
dianlujitao | 27e1899 | 2024-02-08 20:23:35 +0800 | [diff] [blame^] | 290 | default="https://gerrit.omnirom.org", |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 291 | metavar="", |
| 292 | help="Gerrit Instance to use. Form proto://[user@]host[:port]", |
| 293 | ) |
| 294 | parser.add_argument( |
| 295 | "-e", |
| 296 | "--exclude", |
| 297 | nargs=1, |
| 298 | metavar="", |
| 299 | help="exclude a list of commit numbers separated by a ,", |
| 300 | ) |
| 301 | parser.add_argument( |
| 302 | "-c", |
| 303 | "--check-picked", |
| 304 | type=int, |
| 305 | default=10, |
| 306 | metavar="", |
| 307 | help="pass the amount of commits to check for already picked changes", |
| 308 | ) |
dianlujitao | 4947532 | 2024-01-13 23:56:59 +0800 | [diff] [blame] | 309 | parser.add_argument( |
| 310 | "-j", |
| 311 | "--jobs", |
| 312 | type=int, |
| 313 | default=4, |
| 314 | metavar="", |
| 315 | help="max number of changes to pick in parallel", |
| 316 | ) |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 317 | args = parser.parse_args() |
| 318 | if not args.start_branch and args.abandon_first: |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 319 | parser.error( |
| 320 | "if --abandon-first is set, you must also give the branch name with --start-branch" |
| 321 | ) |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 322 | if args.auto_branch: |
| 323 | args.abandon_first = True |
| 324 | args.ignore_missing = True |
| 325 | if not args.start_branch: |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 326 | args.start_branch = ["auto"] |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 327 | if args.quiet and args.verbose: |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 328 | parser.error("--quiet and --verbose cannot be specified together") |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 329 | |
| 330 | if (1 << bool(args.change_number) << bool(args.topic) << bool(args.query)) != 2: |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 331 | parser.error( |
| 332 | "One (and only one) of change_number, topic, and query are allowed" |
| 333 | ) |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 334 | |
| 335 | # Change current directory to the top of the tree |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 336 | if "ANDROID_BUILD_TOP" in os.environ: |
| 337 | top = os.environ["ANDROID_BUILD_TOP"] |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 338 | |
| 339 | if not is_subdir(os.getcwd(), top): |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 340 | sys.stderr.write( |
| 341 | "ERROR: You must run this tool from within $ANDROID_BUILD_TOP!\n" |
| 342 | ) |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 343 | sys.exit(1) |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 344 | os.chdir(os.environ["ANDROID_BUILD_TOP"]) |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 345 | |
| 346 | # Sanity check that we are being run from the top level of the tree |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 347 | if not os.path.isdir(".repo"): |
| 348 | sys.stderr.write( |
| 349 | "ERROR: No .repo directory found. Please run this from the top of your tree.\n" |
| 350 | ) |
Pulser | 72e2324 | 2013-09-29 09:56:55 +0100 | [diff] [blame] | 351 | sys.exit(1) |
| 352 | |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 353 | # If --abandon-first is given, abandon the branch before starting |
| 354 | if args.abandon_first: |
| 355 | # Determine if the branch already exists; skip the abandon if it does not |
dianlujitao | 20f0bdb | 2024-01-14 14:57:41 +0800 | [diff] [blame] | 356 | plist = subprocess.check_output(["repo", "info"], text=True) |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 357 | needs_abandon = False |
| 358 | for pline in plist.splitlines(): |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 359 | matchObj = re.match(r"Local Branches.*\[(.*)\]", pline) |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 360 | if matchObj: |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 361 | local_branches = re.split(r"\s*,\s*", matchObj.group(1)) |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 362 | if any(args.start_branch[0] in s for s in local_branches): |
| 363 | needs_abandon = True |
Pulser | 72e2324 | 2013-09-29 09:56:55 +0100 | [diff] [blame] | 364 | |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 365 | if needs_abandon: |
| 366 | # Perform the abandon only if the branch already exists |
| 367 | if not args.quiet: |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 368 | print("Abandoning branch: %s" % args.start_branch[0]) |
dianlujitao | 20f0bdb | 2024-01-14 14:57:41 +0800 | [diff] [blame] | 369 | subprocess.run(["repo", "abandon", args.start_branch[0]]) |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 370 | if not args.quiet: |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 371 | print("") |
Pulser | 72e2324 | 2013-09-29 09:56:55 +0100 | [diff] [blame] | 372 | |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 373 | # Get the master manifest from repo |
| 374 | # - convert project name and revision to a path |
| 375 | project_name_to_data = {} |
dianlujitao | 20f0bdb | 2024-01-14 14:57:41 +0800 | [diff] [blame] | 376 | manifest = subprocess.check_output(["repo", "manifest"], text=True) |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 377 | xml_root = ElementTree.fromstring(manifest) |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 378 | projects = xml_root.findall("project") |
| 379 | remotes = xml_root.findall("remote") |
| 380 | default_revision = xml_root.findall("default")[0].get("revision") |
dianlujitao | 99e500b | 2024-01-14 17:27:02 +0800 | [diff] [blame] | 381 | if not default_revision: |
| 382 | raise ValueError("Failed to get revision from manifest") |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 383 | |
Aayush Gupta | 2f4522c | 2020-07-26 07:19:19 +0000 | [diff] [blame] | 384 | # dump project data into the a list of dicts with the following data: |
| 385 | # {project: {path, revision}} |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 386 | |
| 387 | for project in projects: |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 388 | name = project.get("name") |
Aaron Kling | 83fee0f | 2020-06-19 18:43:16 -0500 | [diff] [blame] | 389 | # when name and path are equal, "repo manifest" doesn't return a path at all, so fall back to name |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 390 | path = project.get("path", name) |
| 391 | revision = project.get("upstream") |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 392 | if revision is None: |
| 393 | for remote in remotes: |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 394 | if remote.get("name") == project.get("remote"): |
| 395 | revision = remote.get("revision") |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 396 | if revision is None: |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 397 | revision = project.get("revision", default_revision) |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 398 | |
Aayush Gupta | 2f4522c | 2020-07-26 07:19:19 +0000 | [diff] [blame] | 399 | if name not in project_name_to_data: |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 400 | project_name_to_data[name] = {} |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 401 | revision = revision.split("refs/heads/")[-1] |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 402 | project_name_to_data[name][revision] = path |
| 403 | |
Gabriele M | d91609d | 2018-03-31 14:26:59 +0200 | [diff] [blame] | 404 | def cmp_reviews(review_a, review_b): |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 405 | current_a = review_a["current_revision"] |
| 406 | parents_a = [ |
| 407 | r["commit"] for r in review_a["revisions"][current_a]["commit"]["parents"] |
| 408 | ] |
| 409 | current_b = review_b["current_revision"] |
| 410 | parents_b = [ |
| 411 | r["commit"] for r in review_b["revisions"][current_b]["commit"]["parents"] |
| 412 | ] |
Gabriele M | d91609d | 2018-03-31 14:26:59 +0200 | [diff] [blame] | 413 | if current_a in parents_b: |
| 414 | return -1 |
| 415 | elif current_b in parents_a: |
| 416 | return 1 |
| 417 | else: |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 418 | return cmp(review_a["number"], review_b["number"]) |
Gabriele M | d91609d | 2018-03-31 14:26:59 +0200 | [diff] [blame] | 419 | |
dianlujitao | 324541c | 2024-01-13 21:27:00 +0800 | [diff] [blame] | 420 | # get data on requested changes |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 421 | if args.topic: |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 422 | reviews = fetch_query(args.gerrit, "topic:{0}".format(args.topic)) |
| 423 | change_numbers = [ |
| 424 | str(r["number"]) for r in sorted(reviews, key=cmp_to_key(cmp_reviews)) |
| 425 | ] |
dianlujitao | 324541c | 2024-01-13 21:27:00 +0800 | [diff] [blame] | 426 | elif args.query: |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 427 | reviews = fetch_query(args.gerrit, args.query) |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 428 | change_numbers = [ |
| 429 | str(r["number"]) for r in sorted(reviews, key=cmp_to_key(cmp_reviews)) |
| 430 | ] |
dianlujitao | 324541c | 2024-01-13 21:27:00 +0800 | [diff] [blame] | 431 | else: |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 432 | change_url_re = re.compile(r"https?://.+?/([0-9]+(?:/[0-9]+)?)/?") |
dianlujitao | 324541c | 2024-01-13 21:27:00 +0800 | [diff] [blame] | 433 | change_numbers = [] |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 434 | for c in args.change_number: |
Gabriele M | 1009aeb | 2018-04-01 17:50:58 +0200 | [diff] [blame] | 435 | change_number = change_url_re.findall(c) |
| 436 | if change_number: |
| 437 | change_numbers.extend(change_number) |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 438 | elif "-" in c: |
| 439 | templist = c.split("-") |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 440 | for i in range(int(templist[0]), int(templist[1]) + 1): |
| 441 | change_numbers.append(str(i)) |
| 442 | else: |
| 443 | change_numbers.append(c) |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 444 | reviews = fetch_query( |
| 445 | args.gerrit, |
| 446 | " OR ".join("change:{0}".format(x.split("/")[0]) for x in change_numbers), |
| 447 | ) |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 448 | |
| 449 | # make list of things to actually merge |
dianlujitao | 324541c | 2024-01-13 21:27:00 +0800 | [diff] [blame] | 450 | mergables = defaultdict(list) |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 451 | |
| 452 | # If --exclude is given, create the list of commits to ignore |
| 453 | exclude = [] |
| 454 | if args.exclude: |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 455 | exclude = args.exclude[0].split(",") |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 456 | |
| 457 | for change in change_numbers: |
| 458 | patchset = None |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 459 | if "/" in change: |
| 460 | (change, patchset) = change.split("/") |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 461 | |
| 462 | if change in exclude: |
| 463 | continue |
| 464 | |
| 465 | change = int(change) |
| 466 | |
| 467 | if patchset is not None: |
| 468 | patchset = int(patchset) |
| 469 | |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 470 | review = next((x for x in reviews if x["number"] == change), None) |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 471 | if review is None: |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 472 | print("Change %d not found, skipping" % change) |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 473 | continue |
| 474 | |
dianlujitao | 324541c | 2024-01-13 21:27:00 +0800 | [diff] [blame] | 475 | # Check if change is open and exit if it's not, unless -f is specified |
| 476 | if is_closed(review["status"]) and not args.force: |
| 477 | print( |
| 478 | "Change {} status is {}. Skipping the cherry pick.\nUse -f to force this pick.".format( |
| 479 | change, review["status"] |
| 480 | ) |
| 481 | ) |
| 482 | continue |
Gabriele M | 1188cbd | 2018-04-01 17:50:57 +0200 | [diff] [blame] | 483 | |
dianlujitao | 324541c | 2024-01-13 21:27:00 +0800 | [diff] [blame] | 484 | # Convert the project name to a project path |
| 485 | # - check that the project path exists |
| 486 | if ( |
| 487 | review["project"] in project_name_to_data |
| 488 | and review["branch"] in project_name_to_data[review["project"]] |
| 489 | ): |
| 490 | project_path = project_name_to_data[review["project"]][review["branch"]] |
| 491 | elif args.path: |
| 492 | project_path = args.path |
| 493 | elif ( |
| 494 | review["project"] in project_name_to_data |
| 495 | and len(project_name_to_data[review["project"]]) == 1 |
| 496 | ): |
| 497 | local_branch = list(project_name_to_data[review["project"]])[0] |
| 498 | project_path = project_name_to_data[review["project"]][local_branch] |
| 499 | print( |
| 500 | 'WARNING: Project {0} has a different branch ("{1}" != "{2}")'.format( |
| 501 | project_path, local_branch, review["branch"] |
| 502 | ) |
| 503 | ) |
| 504 | elif args.ignore_missing: |
| 505 | print( |
| 506 | "WARNING: Skipping {0} since there is no project directory for: {1}\n".format( |
| 507 | review["id"], review["project"] |
| 508 | ) |
| 509 | ) |
| 510 | continue |
| 511 | else: |
| 512 | sys.stderr.write( |
| 513 | "ERROR: For {0}, could not determine the project path for project {1}\n".format( |
| 514 | review["id"], review["project"] |
| 515 | ) |
| 516 | ) |
| 517 | sys.exit(1) |
| 518 | |
| 519 | item = { |
| 520 | "subject": review["subject"], |
| 521 | "project_path": project_path, |
| 522 | "branch": review["branch"], |
| 523 | "change_id": review["change_id"], |
| 524 | "change_number": review["number"], |
| 525 | "status": review["status"], |
| 526 | "patchset": review["revisions"][review["current_revision"]]["_number"], |
| 527 | "fetch": review["revisions"][review["current_revision"]]["fetch"], |
| 528 | "id": change, |
dianlujitao | 4947532 | 2024-01-13 23:56:59 +0800 | [diff] [blame] | 529 | "revision": review["current_revision"], |
dianlujitao | 324541c | 2024-01-13 21:27:00 +0800 | [diff] [blame] | 530 | } |
| 531 | |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 532 | if patchset: |
dianlujitao | 4947532 | 2024-01-13 23:56:59 +0800 | [diff] [blame] | 533 | for x in review["revisions"]: |
| 534 | if review["revisions"][x]["_number"] == patchset: |
| 535 | item["fetch"] = review["revisions"][x]["fetch"] |
| 536 | item["id"] = "{0}/{1}".format(change, patchset) |
| 537 | item["patchset"] = patchset |
| 538 | item["revision"] = x |
| 539 | break |
| 540 | else: |
dianlujitao | 99e500b | 2024-01-14 17:27:02 +0800 | [diff] [blame] | 541 | if not args.quiet: |
| 542 | print( |
| 543 | "ERROR: The patch set {0}/{1} could not be found, using CURRENT_REVISION instead.".format( |
| 544 | change, patchset |
| 545 | ) |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 546 | ) |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 547 | |
dianlujitao | 324541c | 2024-01-13 21:27:00 +0800 | [diff] [blame] | 548 | mergables[project_path].append(item) |
Pulser | 72e2324 | 2013-09-29 09:56:55 +0100 | [diff] [blame] | 549 | |
dianlujitao | 4947532 | 2024-01-13 23:56:59 +0800 | [diff] [blame] | 550 | # round 1: start branch and drop picked changes |
dianlujitao | 324541c | 2024-01-13 21:27:00 +0800 | [diff] [blame] | 551 | for project_path, per_path_mergables in mergables.items(): |
Pulser | 72e2324 | 2013-09-29 09:56:55 +0100 | [diff] [blame] | 552 | # If --start-branch is given, create the branch (more than once per path is okay; repo ignores gracefully) |
| 553 | if args.start_branch: |
dianlujitao | 20f0bdb | 2024-01-14 14:57:41 +0800 | [diff] [blame] | 554 | subprocess.run(["repo", "start", args.start_branch[0], project_path]) |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 555 | |
| 556 | # Determine the maximum commits to check already picked changes |
| 557 | check_picked_count = args.check_picked |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 558 | branch_commits_count = int( |
| 559 | subprocess.check_output( |
dianlujitao | 20f0bdb | 2024-01-14 14:57:41 +0800 | [diff] [blame] | 560 | [ |
| 561 | "git", |
| 562 | "rev-list", |
| 563 | "--count", |
| 564 | "--max-count", |
| 565 | str(check_picked_count + 1), |
| 566 | "HEAD", |
| 567 | ], |
| 568 | cwd=project_path, |
| 569 | text=True, |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 570 | ) |
| 571 | ) |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 572 | if branch_commits_count <= check_picked_count: |
| 573 | check_picked_count = branch_commits_count - 1 |
| 574 | |
dianlujitao | 324541c | 2024-01-13 21:27:00 +0800 | [diff] [blame] | 575 | picked_change_ids = [] |
| 576 | for i in range(check_picked_count): |
dianlujitao | 4947532 | 2024-01-13 23:56:59 +0800 | [diff] [blame] | 577 | if not commit_exists(project_path, "HEAD~{0}".format(i)): |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 578 | continue |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 579 | output = subprocess.check_output( |
dianlujitao | 20f0bdb | 2024-01-14 14:57:41 +0800 | [diff] [blame] | 580 | ["git", "show", "-q", f"HEAD~{i}"], cwd=project_path, text=True |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 581 | ) |
Simon Shields | 6a72649 | 2019-11-18 23:56:08 +1100 | [diff] [blame] | 582 | output = output.split() |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 583 | if "Change-Id:" in output: |
Aayush Gupta | 2f4522c | 2020-07-26 07:19:19 +0000 | [diff] [blame] | 584 | for j, t in enumerate(reversed(output)): |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 585 | if t == "Change-Id:": |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 586 | head_change_id = output[len(output) - j] |
dianlujitao | 324541c | 2024-01-13 21:27:00 +0800 | [diff] [blame] | 587 | picked_change_ids.append(head_change_id.strip()) |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 588 | break |
Pulser | 72e2324 | 2013-09-29 09:56:55 +0100 | [diff] [blame] | 589 | |
dianlujitao | 324541c | 2024-01-13 21:27:00 +0800 | [diff] [blame] | 590 | for item in per_path_mergables: |
| 591 | # Check if change is already picked to HEAD...HEAD~check_picked_count |
| 592 | if item["change_id"] in picked_change_ids: |
| 593 | print( |
| 594 | "Skipping {0} - already picked in {1}".format( |
| 595 | item["id"], project_path |
| 596 | ) |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 597 | ) |
dianlujitao | 4947532 | 2024-01-13 23:56:59 +0800 | [diff] [blame] | 598 | per_path_mergables.remove(item) |
dianlujitao | 324541c | 2024-01-13 21:27:00 +0800 | [diff] [blame] | 599 | |
dianlujitao | 4947532 | 2024-01-13 23:56:59 +0800 | [diff] [blame] | 600 | # round 2: fetch changes in parallel if not pull |
| 601 | if not args.pull: |
| 602 | with ThreadPoolExecutor(max_workers=args.jobs) as e: |
| 603 | for per_path_mergables in mergables.values(): |
| 604 | # changes are sorted so loop in reversed order to fetch top commits first |
| 605 | for item in reversed(per_path_mergables): |
| 606 | e.submit(partial(do_git_fetch_pull, args), item) |
| 607 | |
| 608 | # round 3: apply changes in parallel for different projects, but sequential |
| 609 | # within each project |
| 610 | with ThreadPoolExecutor(max_workers=args.jobs) as e: |
| 611 | |
| 612 | def bulk_pick_change(per_path_mergables): |
| 613 | for item in per_path_mergables: |
| 614 | apply_change(args, item) |
| 615 | |
| 616 | for per_path_mergables in mergables.values(): |
| 617 | e.submit(bulk_pick_change, per_path_mergables) |
dianlujitao | 324541c | 2024-01-13 21:27:00 +0800 | [diff] [blame] | 618 | |
| 619 | |
dianlujitao | 4947532 | 2024-01-13 23:56:59 +0800 | [diff] [blame] | 620 | def do_git_fetch_pull(args, item): |
dianlujitao | 324541c | 2024-01-13 21:27:00 +0800 | [diff] [blame] | 621 | project_path = item["project_path"] |
| 622 | |
dianlujitao | 4947532 | 2024-01-13 23:56:59 +0800 | [diff] [blame] | 623 | # commit object already exists, no need to fetch |
| 624 | if commit_exists(project_path, item["revision"]): |
| 625 | return |
Pulser | 72e2324 | 2013-09-29 09:56:55 +0100 | [diff] [blame] | 626 | |
dianlujitao | 324541c | 2024-01-13 21:27:00 +0800 | [diff] [blame] | 627 | if "anonymous http" in item["fetch"]: |
| 628 | method = "anonymous http" |
| 629 | else: |
| 630 | method = "ssh" |
Pulser | 72e2324 | 2013-09-29 09:56:55 +0100 | [diff] [blame] | 631 | |
dianlujitao | 324541c | 2024-01-13 21:27:00 +0800 | [diff] [blame] | 632 | if args.pull: |
| 633 | cmd = ["git", "pull", "--no-edit"] |
| 634 | else: |
| 635 | cmd = ["git", "fetch"] |
| 636 | if args.quiet: |
| 637 | cmd.append("--quiet") |
| 638 | cmd.extend(["", item["fetch"][method]["ref"]]) |
dianlujitao | 20f0bdb | 2024-01-14 14:57:41 +0800 | [diff] [blame] | 639 | |
dianlujitao | 27e1899 | 2024-02-08 20:23:35 +0800 | [diff] [blame^] | 640 | # Try fetching from GitHub first if using omnirom gerrit |
| 641 | if is_omnirom_gerrit(args.gerrit): |
dianlujitao | 324541c | 2024-01-13 21:27:00 +0800 | [diff] [blame] | 642 | if args.verbose: |
| 643 | print("Trying to fetch the change from GitHub") |
Marko Man | b58468a | 2018-03-19 13:01:19 +0100 | [diff] [blame] | 644 | |
dianlujitao | 324541c | 2024-01-13 21:27:00 +0800 | [diff] [blame] | 645 | cmd[-2] = "omnirom" |
| 646 | if not args.quiet: |
| 647 | print(cmd) |
| 648 | result = subprocess.call(cmd, cwd=project_path) |
dianlujitao | 24df6a4 | 2024-01-14 17:21:29 +0800 | [diff] [blame] | 649 | # Check if it worked |
| 650 | if result == 0 or commit_exists(project_path, item["revision"]): |
| 651 | return |
| 652 | print("ERROR: git command failed") |
dianlujitao | 324541c | 2024-01-13 21:27:00 +0800 | [diff] [blame] | 653 | |
dianlujitao | 27e1899 | 2024-02-08 20:23:35 +0800 | [diff] [blame^] | 654 | # If not using the omnirom gerrit or github failed, fetch from gerrit. |
dianlujitao | 24df6a4 | 2024-01-14 17:21:29 +0800 | [diff] [blame] | 655 | if args.verbose: |
dianlujitao | 27e1899 | 2024-02-08 20:23:35 +0800 | [diff] [blame^] | 656 | if is_omnirom_gerrit(args.gerrit): |
dianlujitao | 24df6a4 | 2024-01-14 17:21:29 +0800 | [diff] [blame] | 657 | print( |
| 658 | "Fetching from GitHub didn't work, trying to fetch the change from Gerrit" |
| 659 | ) |
| 660 | else: |
| 661 | print("Fetching from {0}".format(args.gerrit)) |
| 662 | |
| 663 | cmd[-2] = item["fetch"][method]["url"] |
| 664 | if not args.quiet: |
| 665 | print(cmd) |
| 666 | result = subprocess.call(cmd, cwd=project_path) |
| 667 | if result != 0 and not commit_exists(project_path, item["revision"]): |
| 668 | print("ERROR: git command failed") |
| 669 | sys.exit(result) |
dianlujitao | 4947532 | 2024-01-13 23:56:59 +0800 | [diff] [blame] | 670 | |
| 671 | |
| 672 | def apply_change(args, item): |
dianlujitao | 99e500b | 2024-01-14 17:27:02 +0800 | [diff] [blame] | 673 | if not args.quiet: |
| 674 | print("Applying change number {0}...".format(item["id"])) |
dianlujitao | 4947532 | 2024-01-13 23:56:59 +0800 | [diff] [blame] | 675 | if is_closed(item["status"]): |
| 676 | print("!! Force-picking a closed change !!\n") |
| 677 | |
| 678 | project_path = item["project_path"] |
| 679 | |
| 680 | # Print out some useful info |
| 681 | if not args.quiet: |
| 682 | print('--> Subject: "{0}"'.format(item["subject"])) |
| 683 | print("--> Project path: {0}".format(project_path)) |
| 684 | print( |
| 685 | "--> Change number: {0} (Patch Set {1})".format( |
| 686 | item["id"], item["patchset"] |
| 687 | ) |
| 688 | ) |
| 689 | |
| 690 | if args.pull: |
| 691 | do_git_fetch_pull(args, item) |
| 692 | else: |
| 693 | # Perform the cherry-pick |
dianlujitao | 324541c | 2024-01-13 21:27:00 +0800 | [diff] [blame] | 694 | if args.quiet: |
| 695 | cmd_out = subprocess.DEVNULL |
| 696 | else: |
| 697 | cmd_out = None |
| 698 | result = subprocess.call( |
| 699 | ["git", "cherry-pick", "--ff", item["revision"]], |
| 700 | cwd=project_path, |
| 701 | stdout=cmd_out, |
| 702 | stderr=cmd_out, |
| 703 | ) |
| 704 | if result != 0: |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 705 | result = subprocess.call( |
dianlujitao | 324541c | 2024-01-13 21:27:00 +0800 | [diff] [blame] | 706 | ["git", "diff-index", "--quiet", "HEAD", "--"], |
dianlujitao | 20f0bdb | 2024-01-14 14:57:41 +0800 | [diff] [blame] | 707 | cwd=project_path, |
| 708 | stdout=cmd_out, |
| 709 | stderr=cmd_out, |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 710 | ) |
dianlujitao | 324541c | 2024-01-13 21:27:00 +0800 | [diff] [blame] | 711 | if result == 0: |
| 712 | print( |
| 713 | "WARNING: git command resulted with an empty commit, aborting cherry-pick" |
| 714 | ) |
| 715 | subprocess.call( |
| 716 | ["git", "cherry-pick", "--abort"], |
dianlujitao | 20f0bdb | 2024-01-14 14:57:41 +0800 | [diff] [blame] | 717 | cwd=project_path, |
| 718 | stdout=cmd_out, |
| 719 | stderr=cmd_out, |
dianlujitao | 769a9c6 | 2024-01-14 17:59:24 +0800 | [diff] [blame] | 720 | ) |
dianlujitao | 324541c | 2024-01-13 21:27:00 +0800 | [diff] [blame] | 721 | elif args.reset: |
| 722 | print("ERROR: git command failed, aborting cherry-pick") |
| 723 | subprocess.call( |
| 724 | ["git", "cherry-pick", "--abort"], |
| 725 | cwd=project_path, |
| 726 | stdout=cmd_out, |
| 727 | stderr=cmd_out, |
| 728 | ) |
| 729 | sys.exit(result) |
| 730 | else: |
| 731 | print("ERROR: git command failed") |
| 732 | sys.exit(result) |
| 733 | if not args.quiet: |
| 734 | print("") |
| 735 | |
| 736 | |
| 737 | if __name__ == "__main__": |
| 738 | main() |