blob: e41b733a91004060f60d4d9a1d200396d80953a1 [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
Pulser72e23242013-09-29 09:56:55 +010037
Luca Weissd1bbac62018-11-25 14:07:12 +010038# cmp() is not available in Python 3, define it manually
39# See https://docs.python.org/3.0/whatsnew/3.0.html#ordering-comparisons
40def cmp(a, b):
41 return (a > b) - (a < b)
42
43
Pulser72e23242013-09-29 09:56:55 +010044# Verifies whether pathA is a subdirectory (or the same) as pathB
Marko Manb58468a2018-03-19 13:01:19 +010045def is_subdir(a, b):
dianlujitao769a9c62024-01-14 17:59:24 +080046 a = os.path.realpath(a) + "/"
47 b = os.path.realpath(b) + "/"
48 return b == a[: len(b)]
Pulser72e23242013-09-29 09:56:55 +010049
Pulser72e23242013-09-29 09:56:55 +010050
Marko Manb58468a2018-03-19 13:01:19 +010051def fetch_query_via_ssh(remote_url, query):
52 """Given a remote_url and a query, return the list of changes that fit it
dianlujitao769a9c62024-01-14 17:59:24 +080053 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:
dianlujitao99e500b2024-01-14 17:27:02 +080057 (_, userhost, port) = remote_url.split(":")
Marko Manb58468a2018-03-19 13:01:19 +010058 userhost = userhost[2:]
dianlujitao769a9c62024-01-14 17:59:24 +080059 elif remote_url.count(":") == 1:
dianlujitao99e500b2024-01-14 17:27:02 +080060 (_, userhost) = remote_url.split(":")
Marko Manb58468a2018-03-19 13:01:19 +010061 userhost = userhost[2:]
dianlujitao20f0bdb2024-01-14 14:57:41 +080062 port = "29418"
Pulser72e23242013-09-29 09:56:55 +010063 else:
dianlujitao769a9c62024-01-14 17:59:24 +080064 raise Exception("Malformed URI: Expecting ssh://[user@]host[:port]")
Pulser72e23242013-09-29 09:56:55 +010065
dianlujitao769a9c62024-01-14 17:59:24 +080066 out = subprocess.check_output(
67 [
68 "ssh",
69 "-x",
dianlujitao20f0bdb2024-01-14 14:57:41 +080070 "-p",
71 port,
dianlujitao769a9c62024-01-14 17:59:24 +080072 userhost,
73 "gerrit",
74 "query",
dianlujitao20f0bdb2024-01-14 14:57:41 +080075 "--format",
76 "JSON",
77 "--patch-sets",
78 "--current-patch-set",
dianlujitao769a9c62024-01-14 17:59:24 +080079 query,
dianlujitao20f0bdb2024-01-14 14:57:41 +080080 ],
81 text=True,
dianlujitao769a9c62024-01-14 17:59:24 +080082 )
Marko Manb58468a2018-03-19 13:01:19 +010083 reviews = []
dianlujitao769a9c62024-01-14 17:59:24 +080084 for line in out.split("\n"):
Marko Manb58468a2018-03-19 13:01:19 +010085 try:
86 data = json.loads(line)
87 # make our data look like the http rest api data
88 review = {
dianlujitao769a9c62024-01-14 17:59:24 +080089 "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 Manb58468a2018-03-19 13:01:19 +0100115 }
116 reviews.append(review)
dianlujitao99e500b2024-01-14 17:27:02 +0800117 except Exception:
Marko Manb58468a2018-03-19 13:01:19 +0100118 pass
Marko Manb58468a2018-03-19 13:01:19 +0100119 return reviews
Pulser72e23242013-09-29 09:56:55 +0100120
Pulser72e23242013-09-29 09:56:55 +0100121
dianlujitao87a692f2024-01-14 17:01:12 +0800122def 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 Manb58468a2018-03-19 13:01:19 +0100133
dianlujitao87a692f2024-01-14 17:01:12 +0800134
135def 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 Manb58468a2018-03-19 13:01:19 +0100165 for review in reviews:
dianlujitao769a9c62024-01-14 17:59:24 +0800166 review["number"] = review.pop("_number")
Marko Manb58468a2018-03-19 13:01:19 +0100167
168 return reviews
169
170
171def fetch_query(remote_url, query):
172 """Wrapper for fetch_query_via_proto functions"""
dianlujitao769a9c62024-01-14 17:59:24 +0800173 if remote_url[0:3] == "ssh":
Marko Manb58468a2018-03-19 13:01:19 +0100174 return fetch_query_via_ssh(remote_url, query)
dianlujitao769a9c62024-01-14 17:59:24 +0800175 elif remote_url[0:4] == "http":
dianlujitao87a692f2024-01-14 17:01:12 +0800176 return fetch_query_via_http(remote_url, query)
Marko Manb58468a2018-03-19 13:01:19 +0100177 else:
dianlujitao769a9c62024-01-14 17:59:24 +0800178 raise Exception(
179 "Gerrit URL should be in the form http[s]://hostname/ or ssh://[user@]host[:port]"
180 )
Marko Manb58468a2018-03-19 13:01:19 +0100181
Aayush Gupta2f4522c2020-07-26 07:19:19 +0000182
dianlujitao324541c2024-01-13 21:27:00 +0800183def is_closed(status):
184 return status not in ("OPEN", "NEW", "DRAFT")
Marko Manb58468a2018-03-19 13:01:19 +0100185
dianlujitao324541c2024-01-13 21:27:00 +0800186
dianlujitao27e18992024-02-08 20:23:35 +0800187def is_omnirom_gerrit(remote_url):
188 p = urllib.parse.urlparse(remote_url)
189 return p.hostname == "gerrit.omnirom.org"
190
191
dianlujitao49475322024-01-13 23:56:59 +0800192def 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
dianlujitao324541c2024-01-13 21:27:00 +0800203def main():
dianlujitao769a9c62024-01-14 17:59:24 +0800204 parser = argparse.ArgumentParser(
205 formatter_class=argparse.RawDescriptionHelpFormatter,
206 description=textwrap.dedent(
207 """\
Marko Manb58468a2018-03-19 13:01:19 +0100208 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
dianlujitao769a9c62024-01-14 17:59:24 +0800221 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",
dianlujitao27e18992024-02-08 20:23:35 +0800290 default="https://gerrit.omnirom.org",
dianlujitao769a9c62024-01-14 17:59:24 +0800291 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 )
dianlujitao49475322024-01-13 23:56:59 +0800309 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 Manb58468a2018-03-19 13:01:19 +0100317 args = parser.parse_args()
318 if not args.start_branch and args.abandon_first:
dianlujitao769a9c62024-01-14 17:59:24 +0800319 parser.error(
320 "if --abandon-first is set, you must also give the branch name with --start-branch"
321 )
Marko Manb58468a2018-03-19 13:01:19 +0100322 if args.auto_branch:
323 args.abandon_first = True
324 args.ignore_missing = True
325 if not args.start_branch:
dianlujitao769a9c62024-01-14 17:59:24 +0800326 args.start_branch = ["auto"]
Marko Manb58468a2018-03-19 13:01:19 +0100327 if args.quiet and args.verbose:
dianlujitao769a9c62024-01-14 17:59:24 +0800328 parser.error("--quiet and --verbose cannot be specified together")
Marko Manb58468a2018-03-19 13:01:19 +0100329
330 if (1 << bool(args.change_number) << bool(args.topic) << bool(args.query)) != 2:
dianlujitao769a9c62024-01-14 17:59:24 +0800331 parser.error(
332 "One (and only one) of change_number, topic, and query are allowed"
333 )
Marko Manb58468a2018-03-19 13:01:19 +0100334
335 # Change current directory to the top of the tree
dianlujitao769a9c62024-01-14 17:59:24 +0800336 if "ANDROID_BUILD_TOP" in os.environ:
337 top = os.environ["ANDROID_BUILD_TOP"]
Marko Manb58468a2018-03-19 13:01:19 +0100338
339 if not is_subdir(os.getcwd(), top):
dianlujitao769a9c62024-01-14 17:59:24 +0800340 sys.stderr.write(
341 "ERROR: You must run this tool from within $ANDROID_BUILD_TOP!\n"
342 )
Marko Manb58468a2018-03-19 13:01:19 +0100343 sys.exit(1)
dianlujitao769a9c62024-01-14 17:59:24 +0800344 os.chdir(os.environ["ANDROID_BUILD_TOP"])
Marko Manb58468a2018-03-19 13:01:19 +0100345
346 # Sanity check that we are being run from the top level of the tree
dianlujitao769a9c62024-01-14 17:59:24 +0800347 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 )
Pulser72e23242013-09-29 09:56:55 +0100351 sys.exit(1)
352
Marko Manb58468a2018-03-19 13:01:19 +0100353 # 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
dianlujitao20f0bdb2024-01-14 14:57:41 +0800356 plist = subprocess.check_output(["repo", "info"], text=True)
Marko Manb58468a2018-03-19 13:01:19 +0100357 needs_abandon = False
358 for pline in plist.splitlines():
dianlujitao769a9c62024-01-14 17:59:24 +0800359 matchObj = re.match(r"Local Branches.*\[(.*)\]", pline)
Marko Manb58468a2018-03-19 13:01:19 +0100360 if matchObj:
dianlujitao769a9c62024-01-14 17:59:24 +0800361 local_branches = re.split(r"\s*,\s*", matchObj.group(1))
Marko Manb58468a2018-03-19 13:01:19 +0100362 if any(args.start_branch[0] in s for s in local_branches):
363 needs_abandon = True
Pulser72e23242013-09-29 09:56:55 +0100364
Marko Manb58468a2018-03-19 13:01:19 +0100365 if needs_abandon:
366 # Perform the abandon only if the branch already exists
367 if not args.quiet:
dianlujitao769a9c62024-01-14 17:59:24 +0800368 print("Abandoning branch: %s" % args.start_branch[0])
dianlujitao20f0bdb2024-01-14 14:57:41 +0800369 subprocess.run(["repo", "abandon", args.start_branch[0]])
Marko Manb58468a2018-03-19 13:01:19 +0100370 if not args.quiet:
dianlujitao769a9c62024-01-14 17:59:24 +0800371 print("")
Pulser72e23242013-09-29 09:56:55 +0100372
Marko Manb58468a2018-03-19 13:01:19 +0100373 # Get the master manifest from repo
374 # - convert project name and revision to a path
375 project_name_to_data = {}
dianlujitao20f0bdb2024-01-14 14:57:41 +0800376 manifest = subprocess.check_output(["repo", "manifest"], text=True)
Marko Manb58468a2018-03-19 13:01:19 +0100377 xml_root = ElementTree.fromstring(manifest)
dianlujitao769a9c62024-01-14 17:59:24 +0800378 projects = xml_root.findall("project")
379 remotes = xml_root.findall("remote")
380 default_revision = xml_root.findall("default")[0].get("revision")
dianlujitao99e500b2024-01-14 17:27:02 +0800381 if not default_revision:
382 raise ValueError("Failed to get revision from manifest")
Marko Manb58468a2018-03-19 13:01:19 +0100383
Aayush Gupta2f4522c2020-07-26 07:19:19 +0000384 # dump project data into the a list of dicts with the following data:
385 # {project: {path, revision}}
Marko Manb58468a2018-03-19 13:01:19 +0100386
387 for project in projects:
dianlujitao769a9c62024-01-14 17:59:24 +0800388 name = project.get("name")
Aaron Kling83fee0f2020-06-19 18:43:16 -0500389 # 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 +0800390 path = project.get("path", name)
391 revision = project.get("upstream")
Marko Manb58468a2018-03-19 13:01:19 +0100392 if revision is None:
393 for remote in remotes:
dianlujitao769a9c62024-01-14 17:59:24 +0800394 if remote.get("name") == project.get("remote"):
395 revision = remote.get("revision")
Marko Manb58468a2018-03-19 13:01:19 +0100396 if revision is None:
dianlujitao769a9c62024-01-14 17:59:24 +0800397 revision = project.get("revision", default_revision)
Marko Manb58468a2018-03-19 13:01:19 +0100398
Aayush Gupta2f4522c2020-07-26 07:19:19 +0000399 if name not in project_name_to_data:
Marko Manb58468a2018-03-19 13:01:19 +0100400 project_name_to_data[name] = {}
dianlujitao769a9c62024-01-14 17:59:24 +0800401 revision = revision.split("refs/heads/")[-1]
Marko Manb58468a2018-03-19 13:01:19 +0100402 project_name_to_data[name][revision] = path
403
Gabriele Md91609d2018-03-31 14:26:59 +0200404 def cmp_reviews(review_a, review_b):
dianlujitao769a9c62024-01-14 17:59:24 +0800405 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 Md91609d2018-03-31 14:26:59 +0200413 if current_a in parents_b:
414 return -1
415 elif current_b in parents_a:
416 return 1
417 else:
dianlujitao769a9c62024-01-14 17:59:24 +0800418 return cmp(review_a["number"], review_b["number"])
Gabriele Md91609d2018-03-31 14:26:59 +0200419
dianlujitao324541c2024-01-13 21:27:00 +0800420 # get data on requested changes
Marko Manb58468a2018-03-19 13:01:19 +0100421 if args.topic:
dianlujitao769a9c62024-01-14 17:59:24 +0800422 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 ]
dianlujitao324541c2024-01-13 21:27:00 +0800426 elif args.query:
Marko Manb58468a2018-03-19 13:01:19 +0100427 reviews = fetch_query(args.gerrit, args.query)
dianlujitao769a9c62024-01-14 17:59:24 +0800428 change_numbers = [
429 str(r["number"]) for r in sorted(reviews, key=cmp_to_key(cmp_reviews))
430 ]
dianlujitao324541c2024-01-13 21:27:00 +0800431 else:
dianlujitao769a9c62024-01-14 17:59:24 +0800432 change_url_re = re.compile(r"https?://.+?/([0-9]+(?:/[0-9]+)?)/?")
dianlujitao324541c2024-01-13 21:27:00 +0800433 change_numbers = []
Marko Manb58468a2018-03-19 13:01:19 +0100434 for c in args.change_number:
Gabriele M1009aeb2018-04-01 17:50:58 +0200435 change_number = change_url_re.findall(c)
436 if change_number:
437 change_numbers.extend(change_number)
dianlujitao769a9c62024-01-14 17:59:24 +0800438 elif "-" in c:
439 templist = c.split("-")
Marko Manb58468a2018-03-19 13:01:19 +0100440 for i in range(int(templist[0]), int(templist[1]) + 1):
441 change_numbers.append(str(i))
442 else:
443 change_numbers.append(c)
dianlujitao769a9c62024-01-14 17:59:24 +0800444 reviews = fetch_query(
445 args.gerrit,
446 " OR ".join("change:{0}".format(x.split("/")[0]) for x in change_numbers),
447 )
Marko Manb58468a2018-03-19 13:01:19 +0100448
449 # make list of things to actually merge
dianlujitao324541c2024-01-13 21:27:00 +0800450 mergables = defaultdict(list)
Marko Manb58468a2018-03-19 13:01:19 +0100451
452 # If --exclude is given, create the list of commits to ignore
453 exclude = []
454 if args.exclude:
dianlujitao769a9c62024-01-14 17:59:24 +0800455 exclude = args.exclude[0].split(",")
Marko Manb58468a2018-03-19 13:01:19 +0100456
457 for change in change_numbers:
458 patchset = None
dianlujitao769a9c62024-01-14 17:59:24 +0800459 if "/" in change:
460 (change, patchset) = change.split("/")
Marko Manb58468a2018-03-19 13:01:19 +0100461
462 if change in exclude:
463 continue
464
465 change = int(change)
466
467 if patchset is not None:
468 patchset = int(patchset)
469
dianlujitao769a9c62024-01-14 17:59:24 +0800470 review = next((x for x in reviews if x["number"] == change), None)
Marko Manb58468a2018-03-19 13:01:19 +0100471 if review is None:
dianlujitao769a9c62024-01-14 17:59:24 +0800472 print("Change %d not found, skipping" % change)
Marko Manb58468a2018-03-19 13:01:19 +0100473 continue
474
dianlujitao324541c2024-01-13 21:27:00 +0800475 # 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 M1188cbd2018-04-01 17:50:57 +0200483
dianlujitao324541c2024-01-13 21:27:00 +0800484 # 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,
dianlujitao49475322024-01-13 23:56:59 +0800529 "revision": review["current_revision"],
dianlujitao324541c2024-01-13 21:27:00 +0800530 }
531
Marko Manb58468a2018-03-19 13:01:19 +0100532 if patchset:
dianlujitao49475322024-01-13 23:56:59 +0800533 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:
dianlujitao99e500b2024-01-14 17:27:02 +0800541 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 )
dianlujitao769a9c62024-01-14 17:59:24 +0800546 )
Marko Manb58468a2018-03-19 13:01:19 +0100547
dianlujitao324541c2024-01-13 21:27:00 +0800548 mergables[project_path].append(item)
Pulser72e23242013-09-29 09:56:55 +0100549
dianlujitao49475322024-01-13 23:56:59 +0800550 # round 1: start branch and drop picked changes
dianlujitao324541c2024-01-13 21:27:00 +0800551 for project_path, per_path_mergables in mergables.items():
Pulser72e23242013-09-29 09:56:55 +0100552 # If --start-branch is given, create the branch (more than once per path is okay; repo ignores gracefully)
553 if args.start_branch:
dianlujitao20f0bdb2024-01-14 14:57:41 +0800554 subprocess.run(["repo", "start", args.start_branch[0], project_path])
Marko Manb58468a2018-03-19 13:01:19 +0100555
556 # Determine the maximum commits to check already picked changes
557 check_picked_count = args.check_picked
dianlujitao769a9c62024-01-14 17:59:24 +0800558 branch_commits_count = int(
559 subprocess.check_output(
dianlujitao20f0bdb2024-01-14 14:57:41 +0800560 [
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,
dianlujitao769a9c62024-01-14 17:59:24 +0800570 )
571 )
Marko Manb58468a2018-03-19 13:01:19 +0100572 if branch_commits_count <= check_picked_count:
573 check_picked_count = branch_commits_count - 1
574
dianlujitao324541c2024-01-13 21:27:00 +0800575 picked_change_ids = []
576 for i in range(check_picked_count):
dianlujitao49475322024-01-13 23:56:59 +0800577 if not commit_exists(project_path, "HEAD~{0}".format(i)):
Marko Manb58468a2018-03-19 13:01:19 +0100578 continue
dianlujitao769a9c62024-01-14 17:59:24 +0800579 output = subprocess.check_output(
dianlujitao20f0bdb2024-01-14 14:57:41 +0800580 ["git", "show", "-q", f"HEAD~{i}"], cwd=project_path, text=True
dianlujitao769a9c62024-01-14 17:59:24 +0800581 )
Simon Shields6a726492019-11-18 23:56:08 +1100582 output = output.split()
dianlujitao769a9c62024-01-14 17:59:24 +0800583 if "Change-Id:" in output:
Aayush Gupta2f4522c2020-07-26 07:19:19 +0000584 for j, t in enumerate(reversed(output)):
dianlujitao769a9c62024-01-14 17:59:24 +0800585 if t == "Change-Id:":
Marko Manb58468a2018-03-19 13:01:19 +0100586 head_change_id = output[len(output) - j]
dianlujitao324541c2024-01-13 21:27:00 +0800587 picked_change_ids.append(head_change_id.strip())
Marko Manb58468a2018-03-19 13:01:19 +0100588 break
Pulser72e23242013-09-29 09:56:55 +0100589
dianlujitao324541c2024-01-13 21:27:00 +0800590 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 )
dianlujitao769a9c62024-01-14 17:59:24 +0800597 )
dianlujitao49475322024-01-13 23:56:59 +0800598 per_path_mergables.remove(item)
dianlujitao324541c2024-01-13 21:27:00 +0800599
dianlujitao49475322024-01-13 23:56:59 +0800600 # 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)
dianlujitao324541c2024-01-13 21:27:00 +0800618
619
dianlujitao49475322024-01-13 23:56:59 +0800620def do_git_fetch_pull(args, item):
dianlujitao324541c2024-01-13 21:27:00 +0800621 project_path = item["project_path"]
622
dianlujitao49475322024-01-13 23:56:59 +0800623 # commit object already exists, no need to fetch
624 if commit_exists(project_path, item["revision"]):
625 return
Pulser72e23242013-09-29 09:56:55 +0100626
dianlujitao324541c2024-01-13 21:27:00 +0800627 if "anonymous http" in item["fetch"]:
628 method = "anonymous http"
629 else:
630 method = "ssh"
Pulser72e23242013-09-29 09:56:55 +0100631
dianlujitao324541c2024-01-13 21:27:00 +0800632 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"]])
dianlujitao20f0bdb2024-01-14 14:57:41 +0800639
dianlujitao27e18992024-02-08 20:23:35 +0800640 # Try fetching from GitHub first if using omnirom gerrit
641 if is_omnirom_gerrit(args.gerrit):
dianlujitao324541c2024-01-13 21:27:00 +0800642 if args.verbose:
643 print("Trying to fetch the change from GitHub")
Marko Manb58468a2018-03-19 13:01:19 +0100644
dianlujitao324541c2024-01-13 21:27:00 +0800645 cmd[-2] = "omnirom"
646 if not args.quiet:
647 print(cmd)
648 result = subprocess.call(cmd, cwd=project_path)
dianlujitao24df6a42024-01-14 17:21:29 +0800649 # Check if it worked
650 if result == 0 or commit_exists(project_path, item["revision"]):
651 return
652 print("ERROR: git command failed")
dianlujitao324541c2024-01-13 21:27:00 +0800653
dianlujitao27e18992024-02-08 20:23:35 +0800654 # If not using the omnirom gerrit or github failed, fetch from gerrit.
dianlujitao24df6a42024-01-14 17:21:29 +0800655 if args.verbose:
dianlujitao27e18992024-02-08 20:23:35 +0800656 if is_omnirom_gerrit(args.gerrit):
dianlujitao24df6a42024-01-14 17:21:29 +0800657 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)
dianlujitao49475322024-01-13 23:56:59 +0800670
671
672def apply_change(args, item):
dianlujitao99e500b2024-01-14 17:27:02 +0800673 if not args.quiet:
674 print("Applying change number {0}...".format(item["id"]))
dianlujitao49475322024-01-13 23:56:59 +0800675 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
dianlujitao324541c2024-01-13 21:27:00 +0800694 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:
dianlujitao769a9c62024-01-14 17:59:24 +0800705 result = subprocess.call(
dianlujitao324541c2024-01-13 21:27:00 +0800706 ["git", "diff-index", "--quiet", "HEAD", "--"],
dianlujitao20f0bdb2024-01-14 14:57:41 +0800707 cwd=project_path,
708 stdout=cmd_out,
709 stderr=cmd_out,
dianlujitao769a9c62024-01-14 17:59:24 +0800710 )
dianlujitao324541c2024-01-13 21:27:00 +0800711 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"],
dianlujitao20f0bdb2024-01-14 14:57:41 +0800717 cwd=project_path,
718 stdout=cmd_out,
719 stderr=cmd_out,
dianlujitao769a9c62024-01-14 17:59:24 +0800720 )
dianlujitao324541c2024-01-13 21:27:00 +0800721 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
737if __name__ == "__main__":
738 main()