blob: 0a28d3738f13f9e2c2fc1af885c17089cc815f94 [file] [log] [blame]
Pulser72e23242013-09-29 09:56:55 +01001#!/usr/bin/env python
2#
dianlujitaoe34c26d2024-01-13 17:33:41 +08003# Copyright (C) 2013-2015 The CyanogenMod Project
4# (C) 2017-2024 The LineageOS Project
Pulser72e23242013-09-29 09:56:55 +01005#
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
dianlujitao769a9c62024-01-14 17:59:24 +080023import argparse
Pulser72e23242013-09-29 09:56:55 +010024import json
25import os
Pulser72e23242013-09-29 09:56:55 +010026import re
dianlujitao769a9c62024-01-14 17:59:24 +080027import subprocess
28import sys
Pulser72e23242013-09-29 09:56:55 +010029import textwrap
dianlujitao87a692f2024-01-14 17:01:12 +080030import urllib.parse
31import urllib.request
dianlujitao324541c2024-01-13 21:27:00 +080032from collections import defaultdict
dianlujitao49475322024-01-13 23:56:59 +080033from concurrent.futures import ThreadPoolExecutor
34from functools import cmp_to_key, partial
Marko Manb58468a2018-03-19 13:01:19 +010035from xml.etree import ElementTree
Pulser72e23242013-09-29 09:56:55 +010036
dianlujitao324541c2024-01-13 21:27:00 +080037# Default to LineageOS Gerrit
38DEFAULT_GERRIT = "https://gerrit.omnirom.org"
39
Pulser72e23242013-09-29 09:56:55 +010040
Luca Weissd1bbac62018-11-25 14:07:12 +010041# cmp() is not available in Python 3, define it manually
42# See https://docs.python.org/3.0/whatsnew/3.0.html#ordering-comparisons
43def cmp(a, b):
44 return (a > b) - (a < b)
45
46
Pulser72e23242013-09-29 09:56:55 +010047# Verifies whether pathA is a subdirectory (or the same) as pathB
Marko Manb58468a2018-03-19 13:01:19 +010048def is_subdir(a, b):
dianlujitao769a9c62024-01-14 17:59:24 +080049 a = os.path.realpath(a) + "/"
50 b = os.path.realpath(b) + "/"
51 return b == a[: len(b)]
Pulser72e23242013-09-29 09:56:55 +010052
Pulser72e23242013-09-29 09:56:55 +010053
Marko Manb58468a2018-03-19 13:01:19 +010054def fetch_query_via_ssh(remote_url, query):
55 """Given a remote_url and a query, return the list of changes that fit it
dianlujitao769a9c62024-01-14 17:59:24 +080056 This function is slightly messy - the ssh api does not return data in the same structure as the HTTP REST API
57 We have to get the data, then transform it to match what we're expecting from the HTTP RESET API
58 """
59 if remote_url.count(":") == 2:
dianlujitao99e500b2024-01-14 17:27:02 +080060 (_, userhost, port) = remote_url.split(":")
Marko Manb58468a2018-03-19 13:01:19 +010061 userhost = userhost[2:]
dianlujitao769a9c62024-01-14 17:59:24 +080062 elif remote_url.count(":") == 1:
dianlujitao99e500b2024-01-14 17:27:02 +080063 (_, userhost) = remote_url.split(":")
Marko Manb58468a2018-03-19 13:01:19 +010064 userhost = userhost[2:]
dianlujitao20f0bdb2024-01-14 14:57:41 +080065 port = "29418"
Pulser72e23242013-09-29 09:56:55 +010066 else:
dianlujitao769a9c62024-01-14 17:59:24 +080067 raise Exception("Malformed URI: Expecting ssh://[user@]host[:port]")
Pulser72e23242013-09-29 09:56:55 +010068
dianlujitao769a9c62024-01-14 17:59:24 +080069 out = subprocess.check_output(
70 [
71 "ssh",
72 "-x",
dianlujitao20f0bdb2024-01-14 14:57:41 +080073 "-p",
74 port,
dianlujitao769a9c62024-01-14 17:59:24 +080075 userhost,
76 "gerrit",
77 "query",
dianlujitao20f0bdb2024-01-14 14:57:41 +080078 "--format",
79 "JSON",
80 "--patch-sets",
81 "--current-patch-set",
dianlujitao769a9c62024-01-14 17:59:24 +080082 query,
dianlujitao20f0bdb2024-01-14 14:57:41 +080083 ],
84 text=True,
dianlujitao769a9c62024-01-14 17:59:24 +080085 )
Marko Manb58468a2018-03-19 13:01:19 +010086 reviews = []
dianlujitao769a9c62024-01-14 17:59:24 +080087 for line in out.split("\n"):
Marko Manb58468a2018-03-19 13:01:19 +010088 try:
89 data = json.loads(line)
90 # make our data look like the http rest api data
91 review = {
dianlujitao769a9c62024-01-14 17:59:24 +080092 "branch": data["branch"],
93 "change_id": data["id"],
94 "current_revision": data["currentPatchSet"]["revision"],
95 "number": int(data["number"]),
96 "revisions": {
97 patch_set["revision"]: {
98 "_number": int(patch_set["number"]),
99 "fetch": {
100 "ssh": {
101 "ref": patch_set["ref"],
102 "url": "ssh://{0}:{1}/{2}".format(
103 userhost, port, data["project"]
104 ),
105 }
106 },
107 "commit": {
108 "parents": [
109 {"commit": parent} for parent in patch_set["parents"]
110 ]
111 },
112 }
113 for patch_set in data["patchSets"]
114 },
115 "subject": data["subject"],
116 "project": data["project"],
117 "status": data["status"],
Marko Manb58468a2018-03-19 13:01:19 +0100118 }
119 reviews.append(review)
dianlujitao99e500b2024-01-14 17:27:02 +0800120 except Exception:
Marko Manb58468a2018-03-19 13:01:19 +0100121 pass
Marko Manb58468a2018-03-19 13:01:19 +0100122 return reviews
Pulser72e23242013-09-29 09:56:55 +0100123
Pulser72e23242013-09-29 09:56:55 +0100124
dianlujitao87a692f2024-01-14 17:01:12 +0800125def build_query_url(remote_url, query, auth):
126 p = urllib.parse.urlparse(remote_url)._asdict()
127 p["path"] = ("/a" if auth else "") + "/changes"
128 p["query"] = urllib.parse.urlencode(
129 {
130 "q": query,
131 "o": ["CURRENT_REVISION", "ALL_REVISIONS", "ALL_COMMITS"],
132 },
133 doseq=True,
134 )
135 return urllib.parse.urlunparse(urllib.parse.ParseResult(**p))
Marko Manb58468a2018-03-19 13:01:19 +0100136
dianlujitao87a692f2024-01-14 17:01:12 +0800137
138def fetch_query_via_http(remote_url, query, auth=True):
139 """Given a query, fetch the change numbers via http"""
140 if auth:
141 gerritrc = os.path.expanduser("~/.gerritrc")
142 username = password = ""
143 if os.path.isfile(gerritrc):
144 with open(gerritrc, "r") as f:
145 for line in f:
146 parts = line.rstrip().split("|")
147 if parts[0] in remote_url:
148 username, password = parts[1], parts[2]
149
150 if username and password:
151 url = build_query_url(remote_url, query, auth)
152 password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
153 password_mgr.add_password(None, url, username, password)
154 auth_handler = urllib.request.HTTPBasicAuthHandler(password_mgr)
155 opener = urllib.request.build_opener(auth_handler)
156 response = opener.open(url)
157 if response.getcode() != 200:
158 # They didn't get good authorization or data, Let's try the old way
159 return fetch_query_via_http(remote_url, query, False)
160 else:
161 return fetch_query_via_http(remote_url, query, False)
162 else:
163 url = build_query_url(remote_url, query, auth)
164 response = urllib.request.urlopen(url)
165
166 data = response.read().decode("utf-8")
167 reviews = json.loads(data[5:])
Marko Manb58468a2018-03-19 13:01:19 +0100168 for review in reviews:
dianlujitao769a9c62024-01-14 17:59:24 +0800169 review["number"] = review.pop("_number")
Marko Manb58468a2018-03-19 13:01:19 +0100170
171 return reviews
172
173
174def fetch_query(remote_url, query):
175 """Wrapper for fetch_query_via_proto functions"""
dianlujitao769a9c62024-01-14 17:59:24 +0800176 if remote_url[0:3] == "ssh":
Marko Manb58468a2018-03-19 13:01:19 +0100177 return fetch_query_via_ssh(remote_url, query)
dianlujitao769a9c62024-01-14 17:59:24 +0800178 elif remote_url[0:4] == "http":
dianlujitao87a692f2024-01-14 17:01:12 +0800179 return fetch_query_via_http(remote_url, query)
Marko Manb58468a2018-03-19 13:01:19 +0100180 else:
dianlujitao769a9c62024-01-14 17:59:24 +0800181 raise Exception(
182 "Gerrit URL should be in the form http[s]://hostname/ or ssh://[user@]host[:port]"
183 )
Marko Manb58468a2018-03-19 13:01:19 +0100184
Aayush Gupta2f4522c2020-07-26 07:19:19 +0000185
dianlujitao324541c2024-01-13 21:27:00 +0800186def is_closed(status):
187 return status not in ("OPEN", "NEW", "DRAFT")
Marko Manb58468a2018-03-19 13:01:19 +0100188
dianlujitao324541c2024-01-13 21:27:00 +0800189
dianlujitao49475322024-01-13 23:56:59 +0800190def commit_exists(project_path, revision):
191 return (
192 subprocess.call(
193 ["git", "cat-file", "-e", revision],
194 cwd=project_path,
195 stderr=subprocess.DEVNULL,
196 )
197 == 0
198 )
199
200
dianlujitao324541c2024-01-13 21:27:00 +0800201def main():
dianlujitao769a9c62024-01-14 17:59:24 +0800202 parser = argparse.ArgumentParser(
203 formatter_class=argparse.RawDescriptionHelpFormatter,
204 description=textwrap.dedent(
205 """\
Marko Manb58468a2018-03-19 13:01:19 +0100206 repopick.py is a utility to simplify the process of cherry picking
207 patches from OmniRom's Gerrit instance (or any gerrit instance of your choosing)
208
209 Given a list of change numbers, repopick will cd into the project path
210 and cherry pick the latest patch available.
211
212 With the --start-branch argument, the user can specify that a branch
213 should be created before cherry picking. This is useful for
214 cherry-picking many patches into a common branch which can be easily
215 abandoned later (good for testing other's changes.)
216
217 The --abandon-first argument, when used in conjunction with the
218 --start-branch option, will cause repopick to abandon the specified
dianlujitao769a9c62024-01-14 17:59:24 +0800219 branch in all repos first before performing any cherry picks."""
220 ),
221 )
222 parser.add_argument(
223 "change_number",
224 nargs="*",
225 help="change number to cherry pick. Use {change number}/{patchset number} to get a specific revision.",
226 )
227 parser.add_argument(
228 "-i",
229 "--ignore-missing",
230 action="store_true",
231 help="do not error out if a patch applies to a missing directory",
232 )
233 parser.add_argument(
234 "-s",
235 "--start-branch",
236 nargs=1,
237 metavar="",
238 help="start the specified branch before cherry picking",
239 )
240 parser.add_argument(
241 "-r",
242 "--reset",
243 action="store_true",
244 help="reset to initial state (abort cherry-pick) if there is a conflict",
245 )
246 parser.add_argument(
247 "-a",
248 "--abandon-first",
249 action="store_true",
250 help="before cherry picking, abandon the branch specified in --start-branch",
251 )
252 parser.add_argument(
253 "-b",
254 "--auto-branch",
255 action="store_true",
256 help='shortcut to "--start-branch auto --abandon-first --ignore-missing"',
257 )
258 parser.add_argument(
259 "-q", "--quiet", action="store_true", help="print as little as possible"
260 )
261 parser.add_argument(
262 "-v",
263 "--verbose",
264 action="store_true",
265 help="print extra information to aid in debug",
266 )
267 parser.add_argument(
268 "-f",
269 "--force",
270 action="store_true",
271 help="force cherry pick even if change is closed",
272 )
273 parser.add_argument(
274 "-p", "--pull", action="store_true", help="execute pull instead of cherry-pick"
275 )
276 parser.add_argument(
277 "-P", "--path", metavar="", help="use the specified path for the change"
278 )
279 parser.add_argument(
280 "-t", "--topic", metavar="", help="pick all commits from a specified topic"
281 )
282 parser.add_argument(
283 "-Q", "--query", metavar="", help="pick all commits using the specified query"
284 )
285 parser.add_argument(
286 "-g",
287 "--gerrit",
dianlujitao324541c2024-01-13 21:27:00 +0800288 default=DEFAULT_GERRIT,
dianlujitao769a9c62024-01-14 17:59:24 +0800289 metavar="",
290 help="Gerrit Instance to use. Form proto://[user@]host[:port]",
291 )
292 parser.add_argument(
293 "-e",
294 "--exclude",
295 nargs=1,
296 metavar="",
297 help="exclude a list of commit numbers separated by a ,",
298 )
299 parser.add_argument(
300 "-c",
301 "--check-picked",
302 type=int,
303 default=10,
304 metavar="",
305 help="pass the amount of commits to check for already picked changes",
306 )
dianlujitao49475322024-01-13 23:56:59 +0800307 parser.add_argument(
308 "-j",
309 "--jobs",
310 type=int,
311 default=4,
312 metavar="",
313 help="max number of changes to pick in parallel",
314 )
Marko Manb58468a2018-03-19 13:01:19 +0100315 args = parser.parse_args()
316 if not args.start_branch and args.abandon_first:
dianlujitao769a9c62024-01-14 17:59:24 +0800317 parser.error(
318 "if --abandon-first is set, you must also give the branch name with --start-branch"
319 )
Marko Manb58468a2018-03-19 13:01:19 +0100320 if args.auto_branch:
321 args.abandon_first = True
322 args.ignore_missing = True
323 if not args.start_branch:
dianlujitao769a9c62024-01-14 17:59:24 +0800324 args.start_branch = ["auto"]
Marko Manb58468a2018-03-19 13:01:19 +0100325 if args.quiet and args.verbose:
dianlujitao769a9c62024-01-14 17:59:24 +0800326 parser.error("--quiet and --verbose cannot be specified together")
Marko Manb58468a2018-03-19 13:01:19 +0100327
328 if (1 << bool(args.change_number) << bool(args.topic) << bool(args.query)) != 2:
dianlujitao769a9c62024-01-14 17:59:24 +0800329 parser.error(
330 "One (and only one) of change_number, topic, and query are allowed"
331 )
Marko Manb58468a2018-03-19 13:01:19 +0100332
333 # Change current directory to the top of the tree
dianlujitao769a9c62024-01-14 17:59:24 +0800334 if "ANDROID_BUILD_TOP" in os.environ:
335 top = os.environ["ANDROID_BUILD_TOP"]
Marko Manb58468a2018-03-19 13:01:19 +0100336
337 if not is_subdir(os.getcwd(), top):
dianlujitao769a9c62024-01-14 17:59:24 +0800338 sys.stderr.write(
339 "ERROR: You must run this tool from within $ANDROID_BUILD_TOP!\n"
340 )
Marko Manb58468a2018-03-19 13:01:19 +0100341 sys.exit(1)
dianlujitao769a9c62024-01-14 17:59:24 +0800342 os.chdir(os.environ["ANDROID_BUILD_TOP"])
Marko Manb58468a2018-03-19 13:01:19 +0100343
344 # Sanity check that we are being run from the top level of the tree
dianlujitao769a9c62024-01-14 17:59:24 +0800345 if not os.path.isdir(".repo"):
346 sys.stderr.write(
347 "ERROR: No .repo directory found. Please run this from the top of your tree.\n"
348 )
Pulser72e23242013-09-29 09:56:55 +0100349 sys.exit(1)
350
Marko Manb58468a2018-03-19 13:01:19 +0100351 # If --abandon-first is given, abandon the branch before starting
352 if args.abandon_first:
353 # Determine if the branch already exists; skip the abandon if it does not
dianlujitao20f0bdb2024-01-14 14:57:41 +0800354 plist = subprocess.check_output(["repo", "info"], text=True)
Marko Manb58468a2018-03-19 13:01:19 +0100355 needs_abandon = False
356 for pline in plist.splitlines():
dianlujitao769a9c62024-01-14 17:59:24 +0800357 matchObj = re.match(r"Local Branches.*\[(.*)\]", pline)
Marko Manb58468a2018-03-19 13:01:19 +0100358 if matchObj:
dianlujitao769a9c62024-01-14 17:59:24 +0800359 local_branches = re.split(r"\s*,\s*", matchObj.group(1))
Marko Manb58468a2018-03-19 13:01:19 +0100360 if any(args.start_branch[0] in s for s in local_branches):
361 needs_abandon = True
Pulser72e23242013-09-29 09:56:55 +0100362
Marko Manb58468a2018-03-19 13:01:19 +0100363 if needs_abandon:
364 # Perform the abandon only if the branch already exists
365 if not args.quiet:
dianlujitao769a9c62024-01-14 17:59:24 +0800366 print("Abandoning branch: %s" % args.start_branch[0])
dianlujitao20f0bdb2024-01-14 14:57:41 +0800367 subprocess.run(["repo", "abandon", args.start_branch[0]])
Marko Manb58468a2018-03-19 13:01:19 +0100368 if not args.quiet:
dianlujitao769a9c62024-01-14 17:59:24 +0800369 print("")
Pulser72e23242013-09-29 09:56:55 +0100370
Marko Manb58468a2018-03-19 13:01:19 +0100371 # Get the master manifest from repo
372 # - convert project name and revision to a path
373 project_name_to_data = {}
dianlujitao20f0bdb2024-01-14 14:57:41 +0800374 manifest = subprocess.check_output(["repo", "manifest"], text=True)
Marko Manb58468a2018-03-19 13:01:19 +0100375 xml_root = ElementTree.fromstring(manifest)
dianlujitao769a9c62024-01-14 17:59:24 +0800376 projects = xml_root.findall("project")
377 remotes = xml_root.findall("remote")
378 default_revision = xml_root.findall("default")[0].get("revision")
dianlujitao99e500b2024-01-14 17:27:02 +0800379 if not default_revision:
380 raise ValueError("Failed to get revision from manifest")
Marko Manb58468a2018-03-19 13:01:19 +0100381
Aayush Gupta2f4522c2020-07-26 07:19:19 +0000382 # dump project data into the a list of dicts with the following data:
383 # {project: {path, revision}}
Marko Manb58468a2018-03-19 13:01:19 +0100384
385 for project in projects:
dianlujitao769a9c62024-01-14 17:59:24 +0800386 name = project.get("name")
Aaron Kling83fee0f2020-06-19 18:43:16 -0500387 # when name and path are equal, "repo manifest" doesn't return a path at all, so fall back to name
dianlujitao769a9c62024-01-14 17:59:24 +0800388 path = project.get("path", name)
389 revision = project.get("upstream")
Marko Manb58468a2018-03-19 13:01:19 +0100390 if revision is None:
391 for remote in remotes:
dianlujitao769a9c62024-01-14 17:59:24 +0800392 if remote.get("name") == project.get("remote"):
393 revision = remote.get("revision")
Marko Manb58468a2018-03-19 13:01:19 +0100394 if revision is None:
dianlujitao769a9c62024-01-14 17:59:24 +0800395 revision = project.get("revision", default_revision)
Marko Manb58468a2018-03-19 13:01:19 +0100396
Aayush Gupta2f4522c2020-07-26 07:19:19 +0000397 if name not in project_name_to_data:
Marko Manb58468a2018-03-19 13:01:19 +0100398 project_name_to_data[name] = {}
dianlujitao769a9c62024-01-14 17:59:24 +0800399 revision = revision.split("refs/heads/")[-1]
Marko Manb58468a2018-03-19 13:01:19 +0100400 project_name_to_data[name][revision] = path
401
Gabriele Md91609d2018-03-31 14:26:59 +0200402 def cmp_reviews(review_a, review_b):
dianlujitao769a9c62024-01-14 17:59:24 +0800403 current_a = review_a["current_revision"]
404 parents_a = [
405 r["commit"] for r in review_a["revisions"][current_a]["commit"]["parents"]
406 ]
407 current_b = review_b["current_revision"]
408 parents_b = [
409 r["commit"] for r in review_b["revisions"][current_b]["commit"]["parents"]
410 ]
Gabriele Md91609d2018-03-31 14:26:59 +0200411 if current_a in parents_b:
412 return -1
413 elif current_b in parents_a:
414 return 1
415 else:
dianlujitao769a9c62024-01-14 17:59:24 +0800416 return cmp(review_a["number"], review_b["number"])
Gabriele Md91609d2018-03-31 14:26:59 +0200417
dianlujitao324541c2024-01-13 21:27:00 +0800418 # get data on requested changes
Marko Manb58468a2018-03-19 13:01:19 +0100419 if args.topic:
dianlujitao769a9c62024-01-14 17:59:24 +0800420 reviews = fetch_query(args.gerrit, "topic:{0}".format(args.topic))
421 change_numbers = [
422 str(r["number"]) for r in sorted(reviews, key=cmp_to_key(cmp_reviews))
423 ]
dianlujitao324541c2024-01-13 21:27:00 +0800424 elif args.query:
Marko Manb58468a2018-03-19 13:01:19 +0100425 reviews = fetch_query(args.gerrit, args.query)
dianlujitao769a9c62024-01-14 17:59:24 +0800426 change_numbers = [
427 str(r["number"]) for r in sorted(reviews, key=cmp_to_key(cmp_reviews))
428 ]
dianlujitao324541c2024-01-13 21:27:00 +0800429 else:
dianlujitao769a9c62024-01-14 17:59:24 +0800430 change_url_re = re.compile(r"https?://.+?/([0-9]+(?:/[0-9]+)?)/?")
dianlujitao324541c2024-01-13 21:27:00 +0800431 change_numbers = []
Marko Manb58468a2018-03-19 13:01:19 +0100432 for c in args.change_number:
Gabriele M1009aeb2018-04-01 17:50:58 +0200433 change_number = change_url_re.findall(c)
434 if change_number:
435 change_numbers.extend(change_number)
dianlujitao769a9c62024-01-14 17:59:24 +0800436 elif "-" in c:
437 templist = c.split("-")
Marko Manb58468a2018-03-19 13:01:19 +0100438 for i in range(int(templist[0]), int(templist[1]) + 1):
439 change_numbers.append(str(i))
440 else:
441 change_numbers.append(c)
dianlujitao769a9c62024-01-14 17:59:24 +0800442 reviews = fetch_query(
443 args.gerrit,
444 " OR ".join("change:{0}".format(x.split("/")[0]) for x in change_numbers),
445 )
Marko Manb58468a2018-03-19 13:01:19 +0100446
447 # make list of things to actually merge
dianlujitao324541c2024-01-13 21:27:00 +0800448 mergables = defaultdict(list)
Marko Manb58468a2018-03-19 13:01:19 +0100449
450 # If --exclude is given, create the list of commits to ignore
451 exclude = []
452 if args.exclude:
dianlujitao769a9c62024-01-14 17:59:24 +0800453 exclude = args.exclude[0].split(",")
Marko Manb58468a2018-03-19 13:01:19 +0100454
455 for change in change_numbers:
456 patchset = None
dianlujitao769a9c62024-01-14 17:59:24 +0800457 if "/" in change:
458 (change, patchset) = change.split("/")
Marko Manb58468a2018-03-19 13:01:19 +0100459
460 if change in exclude:
461 continue
462
463 change = int(change)
464
465 if patchset is not None:
466 patchset = int(patchset)
467
dianlujitao769a9c62024-01-14 17:59:24 +0800468 review = next((x for x in reviews if x["number"] == change), None)
Marko Manb58468a2018-03-19 13:01:19 +0100469 if review is None:
dianlujitao769a9c62024-01-14 17:59:24 +0800470 print("Change %d not found, skipping" % change)
Marko Manb58468a2018-03-19 13:01:19 +0100471 continue
472
dianlujitao324541c2024-01-13 21:27:00 +0800473 # Check if change is open and exit if it's not, unless -f is specified
474 if is_closed(review["status"]) and not args.force:
475 print(
476 "Change {} status is {}. Skipping the cherry pick.\nUse -f to force this pick.".format(
477 change, review["status"]
478 )
479 )
480 continue
Gabriele M1188cbd2018-04-01 17:50:57 +0200481
dianlujitao324541c2024-01-13 21:27:00 +0800482 # Convert the project name to a project path
483 # - check that the project path exists
484 if (
485 review["project"] in project_name_to_data
486 and review["branch"] in project_name_to_data[review["project"]]
487 ):
488 project_path = project_name_to_data[review["project"]][review["branch"]]
489 elif args.path:
490 project_path = args.path
491 elif (
492 review["project"] in project_name_to_data
493 and len(project_name_to_data[review["project"]]) == 1
494 ):
495 local_branch = list(project_name_to_data[review["project"]])[0]
496 project_path = project_name_to_data[review["project"]][local_branch]
497 print(
498 'WARNING: Project {0} has a different branch ("{1}" != "{2}")'.format(
499 project_path, local_branch, review["branch"]
500 )
501 )
502 elif args.ignore_missing:
503 print(
504 "WARNING: Skipping {0} since there is no project directory for: {1}\n".format(
505 review["id"], review["project"]
506 )
507 )
508 continue
509 else:
510 sys.stderr.write(
511 "ERROR: For {0}, could not determine the project path for project {1}\n".format(
512 review["id"], review["project"]
513 )
514 )
515 sys.exit(1)
516
517 item = {
518 "subject": review["subject"],
519 "project_path": project_path,
520 "branch": review["branch"],
521 "change_id": review["change_id"],
522 "change_number": review["number"],
523 "status": review["status"],
524 "patchset": review["revisions"][review["current_revision"]]["_number"],
525 "fetch": review["revisions"][review["current_revision"]]["fetch"],
526 "id": change,
dianlujitao49475322024-01-13 23:56:59 +0800527 "revision": review["current_revision"],
dianlujitao324541c2024-01-13 21:27:00 +0800528 }
529
Marko Manb58468a2018-03-19 13:01:19 +0100530 if patchset:
dianlujitao49475322024-01-13 23:56:59 +0800531 for x in review["revisions"]:
532 if review["revisions"][x]["_number"] == patchset:
533 item["fetch"] = review["revisions"][x]["fetch"]
534 item["id"] = "{0}/{1}".format(change, patchset)
535 item["patchset"] = patchset
536 item["revision"] = x
537 break
538 else:
dianlujitao99e500b2024-01-14 17:27:02 +0800539 if not args.quiet:
540 print(
541 "ERROR: The patch set {0}/{1} could not be found, using CURRENT_REVISION instead.".format(
542 change, patchset
543 )
dianlujitao769a9c62024-01-14 17:59:24 +0800544 )
Marko Manb58468a2018-03-19 13:01:19 +0100545
dianlujitao324541c2024-01-13 21:27:00 +0800546 mergables[project_path].append(item)
Pulser72e23242013-09-29 09:56:55 +0100547
dianlujitao49475322024-01-13 23:56:59 +0800548 # round 1: start branch and drop picked changes
dianlujitao324541c2024-01-13 21:27:00 +0800549 for project_path, per_path_mergables in mergables.items():
Pulser72e23242013-09-29 09:56:55 +0100550 # If --start-branch is given, create the branch (more than once per path is okay; repo ignores gracefully)
551 if args.start_branch:
dianlujitao20f0bdb2024-01-14 14:57:41 +0800552 subprocess.run(["repo", "start", args.start_branch[0], project_path])
Marko Manb58468a2018-03-19 13:01:19 +0100553
554 # Determine the maximum commits to check already picked changes
555 check_picked_count = args.check_picked
dianlujitao769a9c62024-01-14 17:59:24 +0800556 branch_commits_count = int(
557 subprocess.check_output(
dianlujitao20f0bdb2024-01-14 14:57:41 +0800558 [
559 "git",
560 "rev-list",
561 "--count",
562 "--max-count",
563 str(check_picked_count + 1),
564 "HEAD",
565 ],
566 cwd=project_path,
567 text=True,
dianlujitao769a9c62024-01-14 17:59:24 +0800568 )
569 )
Marko Manb58468a2018-03-19 13:01:19 +0100570 if branch_commits_count <= check_picked_count:
571 check_picked_count = branch_commits_count - 1
572
dianlujitao324541c2024-01-13 21:27:00 +0800573 picked_change_ids = []
574 for i in range(check_picked_count):
dianlujitao49475322024-01-13 23:56:59 +0800575 if not commit_exists(project_path, "HEAD~{0}".format(i)):
Marko Manb58468a2018-03-19 13:01:19 +0100576 continue
dianlujitao769a9c62024-01-14 17:59:24 +0800577 output = subprocess.check_output(
dianlujitao20f0bdb2024-01-14 14:57:41 +0800578 ["git", "show", "-q", f"HEAD~{i}"], cwd=project_path, text=True
dianlujitao769a9c62024-01-14 17:59:24 +0800579 )
Simon Shields6a726492019-11-18 23:56:08 +1100580 output = output.split()
dianlujitao769a9c62024-01-14 17:59:24 +0800581 if "Change-Id:" in output:
Aayush Gupta2f4522c2020-07-26 07:19:19 +0000582 for j, t in enumerate(reversed(output)):
dianlujitao769a9c62024-01-14 17:59:24 +0800583 if t == "Change-Id:":
Marko Manb58468a2018-03-19 13:01:19 +0100584 head_change_id = output[len(output) - j]
dianlujitao324541c2024-01-13 21:27:00 +0800585 picked_change_ids.append(head_change_id.strip())
Marko Manb58468a2018-03-19 13:01:19 +0100586 break
Pulser72e23242013-09-29 09:56:55 +0100587
dianlujitao324541c2024-01-13 21:27:00 +0800588 for item in per_path_mergables:
589 # Check if change is already picked to HEAD...HEAD~check_picked_count
590 if item["change_id"] in picked_change_ids:
591 print(
592 "Skipping {0} - already picked in {1}".format(
593 item["id"], project_path
594 )
dianlujitao769a9c62024-01-14 17:59:24 +0800595 )
dianlujitao49475322024-01-13 23:56:59 +0800596 per_path_mergables.remove(item)
dianlujitao324541c2024-01-13 21:27:00 +0800597
dianlujitao49475322024-01-13 23:56:59 +0800598 # round 2: fetch changes in parallel if not pull
599 if not args.pull:
600 with ThreadPoolExecutor(max_workers=args.jobs) as e:
601 for per_path_mergables in mergables.values():
602 # changes are sorted so loop in reversed order to fetch top commits first
603 for item in reversed(per_path_mergables):
604 e.submit(partial(do_git_fetch_pull, args), item)
605
606 # round 3: apply changes in parallel for different projects, but sequential
607 # within each project
608 with ThreadPoolExecutor(max_workers=args.jobs) as e:
609
610 def bulk_pick_change(per_path_mergables):
611 for item in per_path_mergables:
612 apply_change(args, item)
613
614 for per_path_mergables in mergables.values():
615 e.submit(bulk_pick_change, per_path_mergables)
dianlujitao324541c2024-01-13 21:27:00 +0800616
617
dianlujitao49475322024-01-13 23:56:59 +0800618def do_git_fetch_pull(args, item):
dianlujitao324541c2024-01-13 21:27:00 +0800619 project_path = item["project_path"]
620
dianlujitao49475322024-01-13 23:56:59 +0800621 # commit object already exists, no need to fetch
622 if commit_exists(project_path, item["revision"]):
623 return
Pulser72e23242013-09-29 09:56:55 +0100624
dianlujitao324541c2024-01-13 21:27:00 +0800625 if "anonymous http" in item["fetch"]:
626 method = "anonymous http"
627 else:
628 method = "ssh"
Pulser72e23242013-09-29 09:56:55 +0100629
dianlujitao324541c2024-01-13 21:27:00 +0800630 if args.pull:
631 cmd = ["git", "pull", "--no-edit"]
632 else:
633 cmd = ["git", "fetch"]
634 if args.quiet:
635 cmd.append("--quiet")
636 cmd.extend(["", item["fetch"][method]["ref"]])
dianlujitao20f0bdb2024-01-14 14:57:41 +0800637
dianlujitao324541c2024-01-13 21:27:00 +0800638 # Try fetching from GitHub first if using default gerrit
639 if args.gerrit == DEFAULT_GERRIT:
640 if args.verbose:
641 print("Trying to fetch the change from GitHub")
Marko Manb58468a2018-03-19 13:01:19 +0100642
dianlujitao324541c2024-01-13 21:27:00 +0800643 cmd[-2] = "omnirom"
644 if not args.quiet:
645 print(cmd)
646 result = subprocess.call(cmd, cwd=project_path)
dianlujitao24df6a42024-01-14 17:21:29 +0800647 # Check if it worked
648 if result == 0 or commit_exists(project_path, item["revision"]):
649 return
650 print("ERROR: git command failed")
dianlujitao324541c2024-01-13 21:27:00 +0800651
dianlujitao24df6a42024-01-14 17:21:29 +0800652 # If not using the default gerrit or github failed, fetch from gerrit.
653 if args.verbose:
654 if args.gerrit == DEFAULT_GERRIT:
655 print(
656 "Fetching from GitHub didn't work, trying to fetch the change from Gerrit"
657 )
658 else:
659 print("Fetching from {0}".format(args.gerrit))
660
661 cmd[-2] = item["fetch"][method]["url"]
662 if not args.quiet:
663 print(cmd)
664 result = subprocess.call(cmd, cwd=project_path)
665 if result != 0 and not commit_exists(project_path, item["revision"]):
666 print("ERROR: git command failed")
667 sys.exit(result)
dianlujitao49475322024-01-13 23:56:59 +0800668
669
670def apply_change(args, item):
dianlujitao99e500b2024-01-14 17:27:02 +0800671 if not args.quiet:
672 print("Applying change number {0}...".format(item["id"]))
dianlujitao49475322024-01-13 23:56:59 +0800673 if is_closed(item["status"]):
674 print("!! Force-picking a closed change !!\n")
675
676 project_path = item["project_path"]
677
678 # Print out some useful info
679 if not args.quiet:
680 print('--> Subject: "{0}"'.format(item["subject"]))
681 print("--> Project path: {0}".format(project_path))
682 print(
683 "--> Change number: {0} (Patch Set {1})".format(
684 item["id"], item["patchset"]
685 )
686 )
687
688 if args.pull:
689 do_git_fetch_pull(args, item)
690 else:
691 # Perform the cherry-pick
dianlujitao324541c2024-01-13 21:27:00 +0800692 if args.quiet:
693 cmd_out = subprocess.DEVNULL
694 else:
695 cmd_out = None
696 result = subprocess.call(
697 ["git", "cherry-pick", "--ff", item["revision"]],
698 cwd=project_path,
699 stdout=cmd_out,
700 stderr=cmd_out,
701 )
702 if result != 0:
dianlujitao769a9c62024-01-14 17:59:24 +0800703 result = subprocess.call(
dianlujitao324541c2024-01-13 21:27:00 +0800704 ["git", "diff-index", "--quiet", "HEAD", "--"],
dianlujitao20f0bdb2024-01-14 14:57:41 +0800705 cwd=project_path,
706 stdout=cmd_out,
707 stderr=cmd_out,
dianlujitao769a9c62024-01-14 17:59:24 +0800708 )
dianlujitao324541c2024-01-13 21:27:00 +0800709 if result == 0:
710 print(
711 "WARNING: git command resulted with an empty commit, aborting cherry-pick"
712 )
713 subprocess.call(
714 ["git", "cherry-pick", "--abort"],
dianlujitao20f0bdb2024-01-14 14:57:41 +0800715 cwd=project_path,
716 stdout=cmd_out,
717 stderr=cmd_out,
dianlujitao769a9c62024-01-14 17:59:24 +0800718 )
dianlujitao324541c2024-01-13 21:27:00 +0800719 elif args.reset:
720 print("ERROR: git command failed, aborting cherry-pick")
721 subprocess.call(
722 ["git", "cherry-pick", "--abort"],
723 cwd=project_path,
724 stdout=cmd_out,
725 stderr=cmd_out,
726 )
727 sys.exit(result)
728 else:
729 print("ERROR: git command failed")
730 sys.exit(result)
731 if not args.quiet:
732 print("")
733
734
735if __name__ == "__main__":
736 main()