blob: 3ea8cef070c104d30e1203877e380b34578ea5cb [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
Gabriele Md91609d2018-03-31 14:26:59 +020033from functools import cmp_to_key
Marko Manb58468a2018-03-19 13:01:19 +010034from xml.etree import ElementTree
Pulser72e23242013-09-29 09:56:55 +010035
dianlujitao324541c2024-01-13 21:27:00 +080036# Default to LineageOS Gerrit
37DEFAULT_GERRIT = "https://gerrit.omnirom.org"
38
Pulser72e23242013-09-29 09:56:55 +010039
Luca Weissd1bbac62018-11-25 14:07:12 +010040# cmp() is not available in Python 3, define it manually
41# See https://docs.python.org/3.0/whatsnew/3.0.html#ordering-comparisons
42def cmp(a, b):
43 return (a > b) - (a < b)
44
45
Pulser72e23242013-09-29 09:56:55 +010046# Verifies whether pathA is a subdirectory (or the same) as pathB
Marko Manb58468a2018-03-19 13:01:19 +010047def is_subdir(a, b):
dianlujitao769a9c62024-01-14 17:59:24 +080048 a = os.path.realpath(a) + "/"
49 b = os.path.realpath(b) + "/"
50 return b == a[: len(b)]
Pulser72e23242013-09-29 09:56:55 +010051
Pulser72e23242013-09-29 09:56:55 +010052
Marko Manb58468a2018-03-19 13:01:19 +010053def fetch_query_via_ssh(remote_url, query):
54 """Given a remote_url and a query, return the list of changes that fit it
dianlujitao769a9c62024-01-14 17:59:24 +080055 This function is slightly messy - the ssh api does not return data in the same structure as the HTTP REST API
56 We have to get the data, then transform it to match what we're expecting from the HTTP RESET API
57 """
58 if remote_url.count(":") == 2:
59 (uri, userhost, port) = remote_url.split(":")
Marko Manb58468a2018-03-19 13:01:19 +010060 userhost = userhost[2:]
dianlujitao769a9c62024-01-14 17:59:24 +080061 elif remote_url.count(":") == 1:
62 (uri, userhost) = remote_url.split(":")
Marko Manb58468a2018-03-19 13:01:19 +010063 userhost = userhost[2:]
dianlujitao20f0bdb2024-01-14 14:57:41 +080064 port = "29418"
Pulser72e23242013-09-29 09:56:55 +010065 else:
dianlujitao769a9c62024-01-14 17:59:24 +080066 raise Exception("Malformed URI: Expecting ssh://[user@]host[:port]")
Pulser72e23242013-09-29 09:56:55 +010067
dianlujitao769a9c62024-01-14 17:59:24 +080068 out = subprocess.check_output(
69 [
70 "ssh",
71 "-x",
dianlujitao20f0bdb2024-01-14 14:57:41 +080072 "-p",
73 port,
dianlujitao769a9c62024-01-14 17:59:24 +080074 userhost,
75 "gerrit",
76 "query",
dianlujitao20f0bdb2024-01-14 14:57:41 +080077 "--format",
78 "JSON",
79 "--patch-sets",
80 "--current-patch-set",
dianlujitao769a9c62024-01-14 17:59:24 +080081 query,
dianlujitao20f0bdb2024-01-14 14:57:41 +080082 ],
83 text=True,
dianlujitao769a9c62024-01-14 17:59:24 +080084 )
Marko Manb58468a2018-03-19 13:01:19 +010085 reviews = []
dianlujitao769a9c62024-01-14 17:59:24 +080086 for line in out.split("\n"):
Marko Manb58468a2018-03-19 13:01:19 +010087 try:
88 data = json.loads(line)
89 # make our data look like the http rest api data
90 review = {
dianlujitao769a9c62024-01-14 17:59:24 +080091 "branch": data["branch"],
92 "change_id": data["id"],
93 "current_revision": data["currentPatchSet"]["revision"],
94 "number": int(data["number"]),
95 "revisions": {
96 patch_set["revision"]: {
97 "_number": int(patch_set["number"]),
98 "fetch": {
99 "ssh": {
100 "ref": patch_set["ref"],
101 "url": "ssh://{0}:{1}/{2}".format(
102 userhost, port, data["project"]
103 ),
104 }
105 },
106 "commit": {
107 "parents": [
108 {"commit": parent} for parent in patch_set["parents"]
109 ]
110 },
111 }
112 for patch_set in data["patchSets"]
113 },
114 "subject": data["subject"],
115 "project": data["project"],
116 "status": data["status"],
Marko Manb58468a2018-03-19 13:01:19 +0100117 }
118 reviews.append(review)
119 except:
120 pass
Marko Manb58468a2018-03-19 13:01:19 +0100121 return reviews
Pulser72e23242013-09-29 09:56:55 +0100122
Pulser72e23242013-09-29 09:56:55 +0100123
dianlujitao87a692f2024-01-14 17:01:12 +0800124def build_query_url(remote_url, query, auth):
125 p = urllib.parse.urlparse(remote_url)._asdict()
126 p["path"] = ("/a" if auth else "") + "/changes"
127 p["query"] = urllib.parse.urlencode(
128 {
129 "q": query,
130 "o": ["CURRENT_REVISION", "ALL_REVISIONS", "ALL_COMMITS"],
131 },
132 doseq=True,
133 )
134 return urllib.parse.urlunparse(urllib.parse.ParseResult(**p))
Marko Manb58468a2018-03-19 13:01:19 +0100135
dianlujitao87a692f2024-01-14 17:01:12 +0800136
137def fetch_query_via_http(remote_url, query, auth=True):
138 """Given a query, fetch the change numbers via http"""
139 if auth:
140 gerritrc = os.path.expanduser("~/.gerritrc")
141 username = password = ""
142 if os.path.isfile(gerritrc):
143 with open(gerritrc, "r") as f:
144 for line in f:
145 parts = line.rstrip().split("|")
146 if parts[0] in remote_url:
147 username, password = parts[1], parts[2]
148
149 if username and password:
150 url = build_query_url(remote_url, query, auth)
151 password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
152 password_mgr.add_password(None, url, username, password)
153 auth_handler = urllib.request.HTTPBasicAuthHandler(password_mgr)
154 opener = urllib.request.build_opener(auth_handler)
155 response = opener.open(url)
156 if response.getcode() != 200:
157 # They didn't get good authorization or data, Let's try the old way
158 return fetch_query_via_http(remote_url, query, False)
159 else:
160 return fetch_query_via_http(remote_url, query, False)
161 else:
162 url = build_query_url(remote_url, query, auth)
163 response = urllib.request.urlopen(url)
164
165 data = response.read().decode("utf-8")
166 reviews = json.loads(data[5:])
Marko Manb58468a2018-03-19 13:01:19 +0100167 for review in reviews:
dianlujitao769a9c62024-01-14 17:59:24 +0800168 review["number"] = review.pop("_number")
Marko Manb58468a2018-03-19 13:01:19 +0100169
170 return reviews
171
172
173def fetch_query(remote_url, query):
174 """Wrapper for fetch_query_via_proto functions"""
dianlujitao769a9c62024-01-14 17:59:24 +0800175 if remote_url[0:3] == "ssh":
Marko Manb58468a2018-03-19 13:01:19 +0100176 return fetch_query_via_ssh(remote_url, query)
dianlujitao769a9c62024-01-14 17:59:24 +0800177 elif remote_url[0:4] == "http":
dianlujitao87a692f2024-01-14 17:01:12 +0800178 return fetch_query_via_http(remote_url, query)
Marko Manb58468a2018-03-19 13:01:19 +0100179 else:
dianlujitao769a9c62024-01-14 17:59:24 +0800180 raise Exception(
181 "Gerrit URL should be in the form http[s]://hostname/ or ssh://[user@]host[:port]"
182 )
Marko Manb58468a2018-03-19 13:01:19 +0100183
Aayush Gupta2f4522c2020-07-26 07:19:19 +0000184
dianlujitao324541c2024-01-13 21:27:00 +0800185def is_closed(status):
186 return status not in ("OPEN", "NEW", "DRAFT")
Marko Manb58468a2018-03-19 13:01:19 +0100187
dianlujitao324541c2024-01-13 21:27:00 +0800188
189def main():
dianlujitao769a9c62024-01-14 17:59:24 +0800190 parser = argparse.ArgumentParser(
191 formatter_class=argparse.RawDescriptionHelpFormatter,
192 description=textwrap.dedent(
193 """\
Marko Manb58468a2018-03-19 13:01:19 +0100194 repopick.py is a utility to simplify the process of cherry picking
195 patches from OmniRom's Gerrit instance (or any gerrit instance of your choosing)
196
197 Given a list of change numbers, repopick will cd into the project path
198 and cherry pick the latest patch available.
199
200 With the --start-branch argument, the user can specify that a branch
201 should be created before cherry picking. This is useful for
202 cherry-picking many patches into a common branch which can be easily
203 abandoned later (good for testing other's changes.)
204
205 The --abandon-first argument, when used in conjunction with the
206 --start-branch option, will cause repopick to abandon the specified
dianlujitao769a9c62024-01-14 17:59:24 +0800207 branch in all repos first before performing any cherry picks."""
208 ),
209 )
210 parser.add_argument(
211 "change_number",
212 nargs="*",
213 help="change number to cherry pick. Use {change number}/{patchset number} to get a specific revision.",
214 )
215 parser.add_argument(
216 "-i",
217 "--ignore-missing",
218 action="store_true",
219 help="do not error out if a patch applies to a missing directory",
220 )
221 parser.add_argument(
222 "-s",
223 "--start-branch",
224 nargs=1,
225 metavar="",
226 help="start the specified branch before cherry picking",
227 )
228 parser.add_argument(
229 "-r",
230 "--reset",
231 action="store_true",
232 help="reset to initial state (abort cherry-pick) if there is a conflict",
233 )
234 parser.add_argument(
235 "-a",
236 "--abandon-first",
237 action="store_true",
238 help="before cherry picking, abandon the branch specified in --start-branch",
239 )
240 parser.add_argument(
241 "-b",
242 "--auto-branch",
243 action="store_true",
244 help='shortcut to "--start-branch auto --abandon-first --ignore-missing"',
245 )
246 parser.add_argument(
247 "-q", "--quiet", action="store_true", help="print as little as possible"
248 )
249 parser.add_argument(
250 "-v",
251 "--verbose",
252 action="store_true",
253 help="print extra information to aid in debug",
254 )
255 parser.add_argument(
256 "-f",
257 "--force",
258 action="store_true",
259 help="force cherry pick even if change is closed",
260 )
261 parser.add_argument(
262 "-p", "--pull", action="store_true", help="execute pull instead of cherry-pick"
263 )
264 parser.add_argument(
265 "-P", "--path", metavar="", help="use the specified path for the change"
266 )
267 parser.add_argument(
268 "-t", "--topic", metavar="", help="pick all commits from a specified topic"
269 )
270 parser.add_argument(
271 "-Q", "--query", metavar="", help="pick all commits using the specified query"
272 )
273 parser.add_argument(
274 "-g",
275 "--gerrit",
dianlujitao324541c2024-01-13 21:27:00 +0800276 default=DEFAULT_GERRIT,
dianlujitao769a9c62024-01-14 17:59:24 +0800277 metavar="",
278 help="Gerrit Instance to use. Form proto://[user@]host[:port]",
279 )
280 parser.add_argument(
281 "-e",
282 "--exclude",
283 nargs=1,
284 metavar="",
285 help="exclude a list of commit numbers separated by a ,",
286 )
287 parser.add_argument(
288 "-c",
289 "--check-picked",
290 type=int,
291 default=10,
292 metavar="",
293 help="pass the amount of commits to check for already picked changes",
294 )
Marko Manb58468a2018-03-19 13:01:19 +0100295 args = parser.parse_args()
296 if not args.start_branch and args.abandon_first:
dianlujitao769a9c62024-01-14 17:59:24 +0800297 parser.error(
298 "if --abandon-first is set, you must also give the branch name with --start-branch"
299 )
Marko Manb58468a2018-03-19 13:01:19 +0100300 if args.auto_branch:
301 args.abandon_first = True
302 args.ignore_missing = True
303 if not args.start_branch:
dianlujitao769a9c62024-01-14 17:59:24 +0800304 args.start_branch = ["auto"]
Marko Manb58468a2018-03-19 13:01:19 +0100305 if args.quiet and args.verbose:
dianlujitao769a9c62024-01-14 17:59:24 +0800306 parser.error("--quiet and --verbose cannot be specified together")
Marko Manb58468a2018-03-19 13:01:19 +0100307
308 if (1 << bool(args.change_number) << bool(args.topic) << bool(args.query)) != 2:
dianlujitao769a9c62024-01-14 17:59:24 +0800309 parser.error(
310 "One (and only one) of change_number, topic, and query are allowed"
311 )
Marko Manb58468a2018-03-19 13:01:19 +0100312
313 # Change current directory to the top of the tree
dianlujitao769a9c62024-01-14 17:59:24 +0800314 if "ANDROID_BUILD_TOP" in os.environ:
315 top = os.environ["ANDROID_BUILD_TOP"]
Marko Manb58468a2018-03-19 13:01:19 +0100316
317 if not is_subdir(os.getcwd(), top):
dianlujitao769a9c62024-01-14 17:59:24 +0800318 sys.stderr.write(
319 "ERROR: You must run this tool from within $ANDROID_BUILD_TOP!\n"
320 )
Marko Manb58468a2018-03-19 13:01:19 +0100321 sys.exit(1)
dianlujitao769a9c62024-01-14 17:59:24 +0800322 os.chdir(os.environ["ANDROID_BUILD_TOP"])
Marko Manb58468a2018-03-19 13:01:19 +0100323
324 # Sanity check that we are being run from the top level of the tree
dianlujitao769a9c62024-01-14 17:59:24 +0800325 if not os.path.isdir(".repo"):
326 sys.stderr.write(
327 "ERROR: No .repo directory found. Please run this from the top of your tree.\n"
328 )
Pulser72e23242013-09-29 09:56:55 +0100329 sys.exit(1)
330
Marko Manb58468a2018-03-19 13:01:19 +0100331 # If --abandon-first is given, abandon the branch before starting
332 if args.abandon_first:
333 # Determine if the branch already exists; skip the abandon if it does not
dianlujitao20f0bdb2024-01-14 14:57:41 +0800334 plist = subprocess.check_output(["repo", "info"], text=True)
Marko Manb58468a2018-03-19 13:01:19 +0100335 needs_abandon = False
336 for pline in plist.splitlines():
dianlujitao769a9c62024-01-14 17:59:24 +0800337 matchObj = re.match(r"Local Branches.*\[(.*)\]", pline)
Marko Manb58468a2018-03-19 13:01:19 +0100338 if matchObj:
dianlujitao769a9c62024-01-14 17:59:24 +0800339 local_branches = re.split(r"\s*,\s*", matchObj.group(1))
Marko Manb58468a2018-03-19 13:01:19 +0100340 if any(args.start_branch[0] in s for s in local_branches):
341 needs_abandon = True
Pulser72e23242013-09-29 09:56:55 +0100342
Marko Manb58468a2018-03-19 13:01:19 +0100343 if needs_abandon:
344 # Perform the abandon only if the branch already exists
345 if not args.quiet:
dianlujitao769a9c62024-01-14 17:59:24 +0800346 print("Abandoning branch: %s" % args.start_branch[0])
dianlujitao20f0bdb2024-01-14 14:57:41 +0800347 subprocess.run(["repo", "abandon", args.start_branch[0]])
Marko Manb58468a2018-03-19 13:01:19 +0100348 if not args.quiet:
dianlujitao769a9c62024-01-14 17:59:24 +0800349 print("")
Pulser72e23242013-09-29 09:56:55 +0100350
Marko Manb58468a2018-03-19 13:01:19 +0100351 # Get the master manifest from repo
352 # - convert project name and revision to a path
353 project_name_to_data = {}
dianlujitao20f0bdb2024-01-14 14:57:41 +0800354 manifest = subprocess.check_output(["repo", "manifest"], text=True)
Marko Manb58468a2018-03-19 13:01:19 +0100355 xml_root = ElementTree.fromstring(manifest)
dianlujitao769a9c62024-01-14 17:59:24 +0800356 projects = xml_root.findall("project")
357 remotes = xml_root.findall("remote")
358 default_revision = xml_root.findall("default")[0].get("revision")
Marko Manb58468a2018-03-19 13:01:19 +0100359
Aayush Gupta2f4522c2020-07-26 07:19:19 +0000360 # dump project data into the a list of dicts with the following data:
361 # {project: {path, revision}}
Marko Manb58468a2018-03-19 13:01:19 +0100362
363 for project in projects:
dianlujitao769a9c62024-01-14 17:59:24 +0800364 name = project.get("name")
Aaron Kling83fee0f2020-06-19 18:43:16 -0500365 # 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 +0800366 path = project.get("path", name)
367 revision = project.get("upstream")
Marko Manb58468a2018-03-19 13:01:19 +0100368 if revision is None:
369 for remote in remotes:
dianlujitao769a9c62024-01-14 17:59:24 +0800370 if remote.get("name") == project.get("remote"):
371 revision = remote.get("revision")
Marko Manb58468a2018-03-19 13:01:19 +0100372 if revision is None:
dianlujitao769a9c62024-01-14 17:59:24 +0800373 revision = project.get("revision", default_revision)
Marko Manb58468a2018-03-19 13:01:19 +0100374
Aayush Gupta2f4522c2020-07-26 07:19:19 +0000375 if name not in project_name_to_data:
Marko Manb58468a2018-03-19 13:01:19 +0100376 project_name_to_data[name] = {}
dianlujitao769a9c62024-01-14 17:59:24 +0800377 revision = revision.split("refs/heads/")[-1]
Marko Manb58468a2018-03-19 13:01:19 +0100378 project_name_to_data[name][revision] = path
379
Gabriele Md91609d2018-03-31 14:26:59 +0200380 def cmp_reviews(review_a, review_b):
dianlujitao769a9c62024-01-14 17:59:24 +0800381 current_a = review_a["current_revision"]
382 parents_a = [
383 r["commit"] for r in review_a["revisions"][current_a]["commit"]["parents"]
384 ]
385 current_b = review_b["current_revision"]
386 parents_b = [
387 r["commit"] for r in review_b["revisions"][current_b]["commit"]["parents"]
388 ]
Gabriele Md91609d2018-03-31 14:26:59 +0200389 if current_a in parents_b:
390 return -1
391 elif current_b in parents_a:
392 return 1
393 else:
dianlujitao769a9c62024-01-14 17:59:24 +0800394 return cmp(review_a["number"], review_b["number"])
Gabriele Md91609d2018-03-31 14:26:59 +0200395
dianlujitao324541c2024-01-13 21:27:00 +0800396 # get data on requested changes
Marko Manb58468a2018-03-19 13:01:19 +0100397 if args.topic:
dianlujitao769a9c62024-01-14 17:59:24 +0800398 reviews = fetch_query(args.gerrit, "topic:{0}".format(args.topic))
399 change_numbers = [
400 str(r["number"]) for r in sorted(reviews, key=cmp_to_key(cmp_reviews))
401 ]
dianlujitao324541c2024-01-13 21:27:00 +0800402 elif args.query:
Marko Manb58468a2018-03-19 13:01:19 +0100403 reviews = fetch_query(args.gerrit, args.query)
dianlujitao769a9c62024-01-14 17:59:24 +0800404 change_numbers = [
405 str(r["number"]) for r in sorted(reviews, key=cmp_to_key(cmp_reviews))
406 ]
dianlujitao324541c2024-01-13 21:27:00 +0800407 else:
dianlujitao769a9c62024-01-14 17:59:24 +0800408 change_url_re = re.compile(r"https?://.+?/([0-9]+(?:/[0-9]+)?)/?")
dianlujitao324541c2024-01-13 21:27:00 +0800409 change_numbers = []
Marko Manb58468a2018-03-19 13:01:19 +0100410 for c in args.change_number:
Gabriele M1009aeb2018-04-01 17:50:58 +0200411 change_number = change_url_re.findall(c)
412 if change_number:
413 change_numbers.extend(change_number)
dianlujitao769a9c62024-01-14 17:59:24 +0800414 elif "-" in c:
415 templist = c.split("-")
Marko Manb58468a2018-03-19 13:01:19 +0100416 for i in range(int(templist[0]), int(templist[1]) + 1):
417 change_numbers.append(str(i))
418 else:
419 change_numbers.append(c)
dianlujitao769a9c62024-01-14 17:59:24 +0800420 reviews = fetch_query(
421 args.gerrit,
422 " OR ".join("change:{0}".format(x.split("/")[0]) for x in change_numbers),
423 )
Marko Manb58468a2018-03-19 13:01:19 +0100424
425 # make list of things to actually merge
dianlujitao324541c2024-01-13 21:27:00 +0800426 mergables = defaultdict(list)
Marko Manb58468a2018-03-19 13:01:19 +0100427
428 # If --exclude is given, create the list of commits to ignore
429 exclude = []
430 if args.exclude:
dianlujitao769a9c62024-01-14 17:59:24 +0800431 exclude = args.exclude[0].split(",")
Marko Manb58468a2018-03-19 13:01:19 +0100432
433 for change in change_numbers:
434 patchset = None
dianlujitao769a9c62024-01-14 17:59:24 +0800435 if "/" in change:
436 (change, patchset) = change.split("/")
Marko Manb58468a2018-03-19 13:01:19 +0100437
438 if change in exclude:
439 continue
440
441 change = int(change)
442
443 if patchset is not None:
444 patchset = int(patchset)
445
dianlujitao769a9c62024-01-14 17:59:24 +0800446 review = next((x for x in reviews if x["number"] == change), None)
Marko Manb58468a2018-03-19 13:01:19 +0100447 if review is None:
dianlujitao769a9c62024-01-14 17:59:24 +0800448 print("Change %d not found, skipping" % change)
Marko Manb58468a2018-03-19 13:01:19 +0100449 continue
450
dianlujitao324541c2024-01-13 21:27:00 +0800451 # Check if change is open and exit if it's not, unless -f is specified
452 if is_closed(review["status"]) and not args.force:
453 print(
454 "Change {} status is {}. Skipping the cherry pick.\nUse -f to force this pick.".format(
455 change, review["status"]
456 )
457 )
458 continue
Gabriele M1188cbd2018-04-01 17:50:57 +0200459
dianlujitao324541c2024-01-13 21:27:00 +0800460 # Convert the project name to a project path
461 # - check that the project path exists
462 if (
463 review["project"] in project_name_to_data
464 and review["branch"] in project_name_to_data[review["project"]]
465 ):
466 project_path = project_name_to_data[review["project"]][review["branch"]]
467 elif args.path:
468 project_path = args.path
469 elif (
470 review["project"] in project_name_to_data
471 and len(project_name_to_data[review["project"]]) == 1
472 ):
473 local_branch = list(project_name_to_data[review["project"]])[0]
474 project_path = project_name_to_data[review["project"]][local_branch]
475 print(
476 'WARNING: Project {0} has a different branch ("{1}" != "{2}")'.format(
477 project_path, local_branch, review["branch"]
478 )
479 )
480 elif args.ignore_missing:
481 print(
482 "WARNING: Skipping {0} since there is no project directory for: {1}\n".format(
483 review["id"], review["project"]
484 )
485 )
486 continue
487 else:
488 sys.stderr.write(
489 "ERROR: For {0}, could not determine the project path for project {1}\n".format(
490 review["id"], review["project"]
491 )
492 )
493 sys.exit(1)
494
495 item = {
496 "subject": review["subject"],
497 "project_path": project_path,
498 "branch": review["branch"],
499 "change_id": review["change_id"],
500 "change_number": review["number"],
501 "status": review["status"],
502 "patchset": review["revisions"][review["current_revision"]]["_number"],
503 "fetch": review["revisions"][review["current_revision"]]["fetch"],
504 "id": change,
505 }
506
Marko Manb58468a2018-03-19 13:01:19 +0100507 if patchset:
508 try:
dianlujitao324541c2024-01-13 21:27:00 +0800509 item["fetch"] = [
dianlujitao769a9c62024-01-14 17:59:24 +0800510 review["revisions"][x]["fetch"]
511 for x in review["revisions"]
512 if review["revisions"][x]["_number"] == patchset
513 ][0]
dianlujitao324541c2024-01-13 21:27:00 +0800514 item["id"] = "{0}/{1}".format(change, patchset)
515 item["patchset"] = patchset
Marko Manb58468a2018-03-19 13:01:19 +0100516 except (IndexError, ValueError):
dianlujitao769a9c62024-01-14 17:59:24 +0800517 args.quiet or print(
518 "ERROR: The patch set {0}/{1} could not be found, using CURRENT_REVISION instead.".format(
519 change, patchset
520 )
521 )
Marko Manb58468a2018-03-19 13:01:19 +0100522
dianlujitao324541c2024-01-13 21:27:00 +0800523 mergables[project_path].append(item)
Pulser72e23242013-09-29 09:56:55 +0100524
dianlujitao324541c2024-01-13 21:27:00 +0800525 for project_path, per_path_mergables in mergables.items():
Pulser72e23242013-09-29 09:56:55 +0100526 # If --start-branch is given, create the branch (more than once per path is okay; repo ignores gracefully)
527 if args.start_branch:
dianlujitao20f0bdb2024-01-14 14:57:41 +0800528 subprocess.run(["repo", "start", args.start_branch[0], project_path])
Marko Manb58468a2018-03-19 13:01:19 +0100529
530 # Determine the maximum commits to check already picked changes
531 check_picked_count = args.check_picked
dianlujitao769a9c62024-01-14 17:59:24 +0800532 branch_commits_count = int(
533 subprocess.check_output(
dianlujitao20f0bdb2024-01-14 14:57:41 +0800534 [
535 "git",
536 "rev-list",
537 "--count",
538 "--max-count",
539 str(check_picked_count + 1),
540 "HEAD",
541 ],
542 cwd=project_path,
543 text=True,
dianlujitao769a9c62024-01-14 17:59:24 +0800544 )
545 )
Marko Manb58468a2018-03-19 13:01:19 +0100546 if branch_commits_count <= check_picked_count:
547 check_picked_count = branch_commits_count - 1
548
dianlujitao324541c2024-01-13 21:27:00 +0800549 picked_change_ids = []
550 for i in range(check_picked_count):
dianlujitao769a9c62024-01-14 17:59:24 +0800551 if subprocess.call(
552 ["git", "cat-file", "-e", "HEAD~{0}".format(i)],
553 cwd=project_path,
554 stderr=open(os.devnull, "wb"),
555 ):
Marko Manb58468a2018-03-19 13:01:19 +0100556 continue
dianlujitao769a9c62024-01-14 17:59:24 +0800557 output = subprocess.check_output(
dianlujitao20f0bdb2024-01-14 14:57:41 +0800558 ["git", "show", "-q", f"HEAD~{i}"], cwd=project_path, text=True
dianlujitao769a9c62024-01-14 17:59:24 +0800559 )
Simon Shields6a726492019-11-18 23:56:08 +1100560 output = output.split()
dianlujitao769a9c62024-01-14 17:59:24 +0800561 if "Change-Id:" in output:
Aayush Gupta2f4522c2020-07-26 07:19:19 +0000562 for j, t in enumerate(reversed(output)):
dianlujitao769a9c62024-01-14 17:59:24 +0800563 if t == "Change-Id:":
Marko Manb58468a2018-03-19 13:01:19 +0100564 head_change_id = output[len(output) - j]
dianlujitao324541c2024-01-13 21:27:00 +0800565 picked_change_ids.append(head_change_id.strip())
Marko Manb58468a2018-03-19 13:01:19 +0100566 break
Pulser72e23242013-09-29 09:56:55 +0100567
dianlujitao324541c2024-01-13 21:27:00 +0800568 for item in per_path_mergables:
569 # Check if change is already picked to HEAD...HEAD~check_picked_count
570 if item["change_id"] in picked_change_ids:
571 print(
572 "Skipping {0} - already picked in {1}".format(
573 item["id"], project_path
574 )
dianlujitao769a9c62024-01-14 17:59:24 +0800575 )
dianlujitao324541c2024-01-13 21:27:00 +0800576 continue
577
578 apply_change(args, item)
579
580
581def apply_change(args, item):
582 args.quiet or print("Applying change number {0}...".format(item["id"]))
583 if is_closed(item["status"]):
584 print("!! Force-picking a closed change !!\n")
585
586 project_path = item["project_path"]
587
588 # Print out some useful info
589 if not args.quiet:
590 print('--> Subject: "{0}"'.format(item["subject"]))
591 print("--> Project path: {0}".format(project_path))
592 print(
593 "--> Change number: {0} (Patch Set {1})".format(
594 item["id"], item["patchset"]
dianlujitao769a9c62024-01-14 17:59:24 +0800595 )
dianlujitao324541c2024-01-13 21:27:00 +0800596 )
Pulser72e23242013-09-29 09:56:55 +0100597
dianlujitao324541c2024-01-13 21:27:00 +0800598 if "anonymous http" in item["fetch"]:
599 method = "anonymous http"
600 else:
601 method = "ssh"
Pulser72e23242013-09-29 09:56:55 +0100602
dianlujitao324541c2024-01-13 21:27:00 +0800603 if args.pull:
604 cmd = ["git", "pull", "--no-edit"]
605 else:
606 cmd = ["git", "fetch"]
607 if args.quiet:
608 cmd.append("--quiet")
609 cmd.extend(["", item["fetch"][method]["ref"]])
dianlujitao20f0bdb2024-01-14 14:57:41 +0800610
dianlujitao324541c2024-01-13 21:27:00 +0800611 # Try fetching from GitHub first if using default gerrit
612 if args.gerrit == DEFAULT_GERRIT:
613 if args.verbose:
614 print("Trying to fetch the change from GitHub")
Marko Manb58468a2018-03-19 13:01:19 +0100615
dianlujitao324541c2024-01-13 21:27:00 +0800616 cmd[-2] = "omnirom"
617 if not args.quiet:
618 print(cmd)
619 result = subprocess.call(cmd, cwd=project_path)
620 FETCH_HEAD = "{0}/.git/FETCH_HEAD".format(project_path)
621 if result != 0 and os.stat(FETCH_HEAD).st_size != 0:
622 print("ERROR: git command failed")
623 sys.exit(result)
624 # Check if it worked
625 if args.gerrit != DEFAULT_GERRIT or os.stat(FETCH_HEAD).st_size == 0:
626 # If not using the default gerrit or github failed, fetch from gerrit.
627 if args.verbose:
628 if args.gerrit == DEFAULT_GERRIT:
629 print(
630 "Fetching from GitHub didn't work, trying to fetch the change from Gerrit"
631 )
Marko Manb58468a2018-03-19 13:01:19 +0100632 else:
dianlujitao324541c2024-01-13 21:27:00 +0800633 print("Fetching from {0}".format(args.gerrit))
634
635 cmd[-2] = item["fetch"][method]["url"]
636 if not args.quiet:
637 print(cmd)
638 result = subprocess.call(cmd, cwd=project_path)
639 if result != 0:
640 print("ERROR: git command failed")
641 sys.exit(result)
642 # Perform the cherry-pick
643 if not args.pull:
644 if args.quiet:
645 cmd_out = subprocess.DEVNULL
646 else:
647 cmd_out = None
648 result = subprocess.call(
649 ["git", "cherry-pick", "--ff", item["revision"]],
650 cwd=project_path,
651 stdout=cmd_out,
652 stderr=cmd_out,
653 )
654 if result != 0:
dianlujitao769a9c62024-01-14 17:59:24 +0800655 result = subprocess.call(
dianlujitao324541c2024-01-13 21:27:00 +0800656 ["git", "diff-index", "--quiet", "HEAD", "--"],
dianlujitao20f0bdb2024-01-14 14:57:41 +0800657 cwd=project_path,
658 stdout=cmd_out,
659 stderr=cmd_out,
dianlujitao769a9c62024-01-14 17:59:24 +0800660 )
dianlujitao324541c2024-01-13 21:27:00 +0800661 if result == 0:
662 print(
663 "WARNING: git command resulted with an empty commit, aborting cherry-pick"
664 )
665 subprocess.call(
666 ["git", "cherry-pick", "--abort"],
dianlujitao20f0bdb2024-01-14 14:57:41 +0800667 cwd=project_path,
668 stdout=cmd_out,
669 stderr=cmd_out,
dianlujitao769a9c62024-01-14 17:59:24 +0800670 )
dianlujitao324541c2024-01-13 21:27:00 +0800671 elif args.reset:
672 print("ERROR: git command failed, aborting cherry-pick")
673 subprocess.call(
674 ["git", "cherry-pick", "--abort"],
675 cwd=project_path,
676 stdout=cmd_out,
677 stderr=cmd_out,
678 )
679 sys.exit(result)
680 else:
681 print("ERROR: git command failed")
682 sys.exit(result)
683 if not args.quiet:
684 print("")
685
686
687if __name__ == "__main__":
688 main()