blob: fc3383fe8174de741f5d7f749bca5fb167618ee1 [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
Gabriele Md91609d2018-03-31 14:26:59 +020032from functools import cmp_to_key
Marko Manb58468a2018-03-19 13:01:19 +010033from xml.etree import ElementTree
Pulser72e23242013-09-29 09:56:55 +010034
Pulser72e23242013-09-29 09:56:55 +010035
Luca Weissd1bbac62018-11-25 14:07:12 +010036# cmp() is not available in Python 3, define it manually
37# See https://docs.python.org/3.0/whatsnew/3.0.html#ordering-comparisons
38def cmp(a, b):
39 return (a > b) - (a < b)
40
41
Pulser72e23242013-09-29 09:56:55 +010042# Verifies whether pathA is a subdirectory (or the same) as pathB
Marko Manb58468a2018-03-19 13:01:19 +010043def is_subdir(a, b):
dianlujitao769a9c62024-01-14 17:59:24 +080044 a = os.path.realpath(a) + "/"
45 b = os.path.realpath(b) + "/"
46 return b == a[: len(b)]
Pulser72e23242013-09-29 09:56:55 +010047
Pulser72e23242013-09-29 09:56:55 +010048
Marko Manb58468a2018-03-19 13:01:19 +010049def fetch_query_via_ssh(remote_url, query):
50 """Given a remote_url and a query, return the list of changes that fit it
dianlujitao769a9c62024-01-14 17:59:24 +080051 This function is slightly messy - the ssh api does not return data in the same structure as the HTTP REST API
52 We have to get the data, then transform it to match what we're expecting from the HTTP RESET API
53 """
54 if remote_url.count(":") == 2:
55 (uri, userhost, port) = remote_url.split(":")
Marko Manb58468a2018-03-19 13:01:19 +010056 userhost = userhost[2:]
dianlujitao769a9c62024-01-14 17:59:24 +080057 elif remote_url.count(":") == 1:
58 (uri, userhost) = remote_url.split(":")
Marko Manb58468a2018-03-19 13:01:19 +010059 userhost = userhost[2:]
dianlujitao20f0bdb2024-01-14 14:57:41 +080060 port = "29418"
Pulser72e23242013-09-29 09:56:55 +010061 else:
dianlujitao769a9c62024-01-14 17:59:24 +080062 raise Exception("Malformed URI: Expecting ssh://[user@]host[:port]")
Pulser72e23242013-09-29 09:56:55 +010063
dianlujitao769a9c62024-01-14 17:59:24 +080064 out = subprocess.check_output(
65 [
66 "ssh",
67 "-x",
dianlujitao20f0bdb2024-01-14 14:57:41 +080068 "-p",
69 port,
dianlujitao769a9c62024-01-14 17:59:24 +080070 userhost,
71 "gerrit",
72 "query",
dianlujitao20f0bdb2024-01-14 14:57:41 +080073 "--format",
74 "JSON",
75 "--patch-sets",
76 "--current-patch-set",
dianlujitao769a9c62024-01-14 17:59:24 +080077 query,
dianlujitao20f0bdb2024-01-14 14:57:41 +080078 ],
79 text=True,
dianlujitao769a9c62024-01-14 17:59:24 +080080 )
Marko Manb58468a2018-03-19 13:01:19 +010081 reviews = []
dianlujitao769a9c62024-01-14 17:59:24 +080082 for line in out.split("\n"):
Marko Manb58468a2018-03-19 13:01:19 +010083 try:
84 data = json.loads(line)
85 # make our data look like the http rest api data
86 review = {
dianlujitao769a9c62024-01-14 17:59:24 +080087 "branch": data["branch"],
88 "change_id": data["id"],
89 "current_revision": data["currentPatchSet"]["revision"],
90 "number": int(data["number"]),
91 "revisions": {
92 patch_set["revision"]: {
93 "_number": int(patch_set["number"]),
94 "fetch": {
95 "ssh": {
96 "ref": patch_set["ref"],
97 "url": "ssh://{0}:{1}/{2}".format(
98 userhost, port, data["project"]
99 ),
100 }
101 },
102 "commit": {
103 "parents": [
104 {"commit": parent} for parent in patch_set["parents"]
105 ]
106 },
107 }
108 for patch_set in data["patchSets"]
109 },
110 "subject": data["subject"],
111 "project": data["project"],
112 "status": data["status"],
Marko Manb58468a2018-03-19 13:01:19 +0100113 }
114 reviews.append(review)
115 except:
116 pass
dianlujitao769a9c62024-01-14 17:59:24 +0800117 args.quiet or print("Found {0} reviews".format(len(reviews)))
Marko Manb58468a2018-03-19 13:01:19 +0100118 return reviews
Pulser72e23242013-09-29 09:56:55 +0100119
Pulser72e23242013-09-29 09:56:55 +0100120
dianlujitao87a692f2024-01-14 17:01:12 +0800121def build_query_url(remote_url, query, auth):
122 p = urllib.parse.urlparse(remote_url)._asdict()
123 p["path"] = ("/a" if auth else "") + "/changes"
124 p["query"] = urllib.parse.urlencode(
125 {
126 "q": query,
127 "o": ["CURRENT_REVISION", "ALL_REVISIONS", "ALL_COMMITS"],
128 },
129 doseq=True,
130 )
131 return urllib.parse.urlunparse(urllib.parse.ParseResult(**p))
Marko Manb58468a2018-03-19 13:01:19 +0100132
dianlujitao87a692f2024-01-14 17:01:12 +0800133
134def fetch_query_via_http(remote_url, query, auth=True):
135 """Given a query, fetch the change numbers via http"""
136 if auth:
137 gerritrc = os.path.expanduser("~/.gerritrc")
138 username = password = ""
139 if os.path.isfile(gerritrc):
140 with open(gerritrc, "r") as f:
141 for line in f:
142 parts = line.rstrip().split("|")
143 if parts[0] in remote_url:
144 username, password = parts[1], parts[2]
145
146 if username and password:
147 url = build_query_url(remote_url, query, auth)
148 password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
149 password_mgr.add_password(None, url, username, password)
150 auth_handler = urllib.request.HTTPBasicAuthHandler(password_mgr)
151 opener = urllib.request.build_opener(auth_handler)
152 response = opener.open(url)
153 if response.getcode() != 200:
154 # They didn't get good authorization or data, Let's try the old way
155 return fetch_query_via_http(remote_url, query, False)
156 else:
157 return fetch_query_via_http(remote_url, query, False)
158 else:
159 url = build_query_url(remote_url, query, auth)
160 response = urllib.request.urlopen(url)
161
162 data = response.read().decode("utf-8")
163 reviews = json.loads(data[5:])
Marko Manb58468a2018-03-19 13:01:19 +0100164 for review in reviews:
dianlujitao769a9c62024-01-14 17:59:24 +0800165 review["number"] = review.pop("_number")
Marko Manb58468a2018-03-19 13:01:19 +0100166
167 return reviews
168
169
170def fetch_query(remote_url, query):
171 """Wrapper for fetch_query_via_proto functions"""
dianlujitao769a9c62024-01-14 17:59:24 +0800172 if remote_url[0:3] == "ssh":
Marko Manb58468a2018-03-19 13:01:19 +0100173 return fetch_query_via_ssh(remote_url, query)
dianlujitao769a9c62024-01-14 17:59:24 +0800174 elif remote_url[0:4] == "http":
dianlujitao87a692f2024-01-14 17:01:12 +0800175 return fetch_query_via_http(remote_url, query)
Marko Manb58468a2018-03-19 13:01:19 +0100176 else:
dianlujitao769a9c62024-01-14 17:59:24 +0800177 raise Exception(
178 "Gerrit URL should be in the form http[s]://hostname/ or ssh://[user@]host[:port]"
179 )
Marko Manb58468a2018-03-19 13:01:19 +0100180
Aayush Gupta2f4522c2020-07-26 07:19:19 +0000181
dianlujitao769a9c62024-01-14 17:59:24 +0800182if __name__ == "__main__":
Marko Manb58468a2018-03-19 13:01:19 +0100183 # Default to OmniRom Gerrit
dianlujitao769a9c62024-01-14 17:59:24 +0800184 default_gerrit = "https://gerrit.omnirom.org"
Marko Manb58468a2018-03-19 13:01:19 +0100185
dianlujitao769a9c62024-01-14 17:59:24 +0800186 parser = argparse.ArgumentParser(
187 formatter_class=argparse.RawDescriptionHelpFormatter,
188 description=textwrap.dedent(
189 """\
Marko Manb58468a2018-03-19 13:01:19 +0100190 repopick.py is a utility to simplify the process of cherry picking
191 patches from OmniRom's Gerrit instance (or any gerrit instance of your choosing)
192
193 Given a list of change numbers, repopick will cd into the project path
194 and cherry pick the latest patch available.
195
196 With the --start-branch argument, the user can specify that a branch
197 should be created before cherry picking. This is useful for
198 cherry-picking many patches into a common branch which can be easily
199 abandoned later (good for testing other's changes.)
200
201 The --abandon-first argument, when used in conjunction with the
202 --start-branch option, will cause repopick to abandon the specified
dianlujitao769a9c62024-01-14 17:59:24 +0800203 branch in all repos first before performing any cherry picks."""
204 ),
205 )
206 parser.add_argument(
207 "change_number",
208 nargs="*",
209 help="change number to cherry pick. Use {change number}/{patchset number} to get a specific revision.",
210 )
211 parser.add_argument(
212 "-i",
213 "--ignore-missing",
214 action="store_true",
215 help="do not error out if a patch applies to a missing directory",
216 )
217 parser.add_argument(
218 "-s",
219 "--start-branch",
220 nargs=1,
221 metavar="",
222 help="start the specified branch before cherry picking",
223 )
224 parser.add_argument(
225 "-r",
226 "--reset",
227 action="store_true",
228 help="reset to initial state (abort cherry-pick) if there is a conflict",
229 )
230 parser.add_argument(
231 "-a",
232 "--abandon-first",
233 action="store_true",
234 help="before cherry picking, abandon the branch specified in --start-branch",
235 )
236 parser.add_argument(
237 "-b",
238 "--auto-branch",
239 action="store_true",
240 help='shortcut to "--start-branch auto --abandon-first --ignore-missing"',
241 )
242 parser.add_argument(
243 "-q", "--quiet", action="store_true", help="print as little as possible"
244 )
245 parser.add_argument(
246 "-v",
247 "--verbose",
248 action="store_true",
249 help="print extra information to aid in debug",
250 )
251 parser.add_argument(
252 "-f",
253 "--force",
254 action="store_true",
255 help="force cherry pick even if change is closed",
256 )
257 parser.add_argument(
258 "-p", "--pull", action="store_true", help="execute pull instead of cherry-pick"
259 )
260 parser.add_argument(
261 "-P", "--path", metavar="", help="use the specified path for the change"
262 )
263 parser.add_argument(
264 "-t", "--topic", metavar="", help="pick all commits from a specified topic"
265 )
266 parser.add_argument(
267 "-Q", "--query", metavar="", help="pick all commits using the specified query"
268 )
269 parser.add_argument(
270 "-g",
271 "--gerrit",
272 default=default_gerrit,
273 metavar="",
274 help="Gerrit Instance to use. Form proto://[user@]host[:port]",
275 )
276 parser.add_argument(
277 "-e",
278 "--exclude",
279 nargs=1,
280 metavar="",
281 help="exclude a list of commit numbers separated by a ,",
282 )
283 parser.add_argument(
284 "-c",
285 "--check-picked",
286 type=int,
287 default=10,
288 metavar="",
289 help="pass the amount of commits to check for already picked changes",
290 )
Marko Manb58468a2018-03-19 13:01:19 +0100291 args = parser.parse_args()
292 if not args.start_branch and args.abandon_first:
dianlujitao769a9c62024-01-14 17:59:24 +0800293 parser.error(
294 "if --abandon-first is set, you must also give the branch name with --start-branch"
295 )
Marko Manb58468a2018-03-19 13:01:19 +0100296 if args.auto_branch:
297 args.abandon_first = True
298 args.ignore_missing = True
299 if not args.start_branch:
dianlujitao769a9c62024-01-14 17:59:24 +0800300 args.start_branch = ["auto"]
Marko Manb58468a2018-03-19 13:01:19 +0100301 if args.quiet and args.verbose:
dianlujitao769a9c62024-01-14 17:59:24 +0800302 parser.error("--quiet and --verbose cannot be specified together")
Marko Manb58468a2018-03-19 13:01:19 +0100303
304 if (1 << bool(args.change_number) << bool(args.topic) << bool(args.query)) != 2:
dianlujitao769a9c62024-01-14 17:59:24 +0800305 parser.error(
306 "One (and only one) of change_number, topic, and query are allowed"
307 )
Marko Manb58468a2018-03-19 13:01:19 +0100308
309 # Change current directory to the top of the tree
dianlujitao769a9c62024-01-14 17:59:24 +0800310 if "ANDROID_BUILD_TOP" in os.environ:
311 top = os.environ["ANDROID_BUILD_TOP"]
Marko Manb58468a2018-03-19 13:01:19 +0100312
313 if not is_subdir(os.getcwd(), top):
dianlujitao769a9c62024-01-14 17:59:24 +0800314 sys.stderr.write(
315 "ERROR: You must run this tool from within $ANDROID_BUILD_TOP!\n"
316 )
Marko Manb58468a2018-03-19 13:01:19 +0100317 sys.exit(1)
dianlujitao769a9c62024-01-14 17:59:24 +0800318 os.chdir(os.environ["ANDROID_BUILD_TOP"])
Marko Manb58468a2018-03-19 13:01:19 +0100319
320 # Sanity check that we are being run from the top level of the tree
dianlujitao769a9c62024-01-14 17:59:24 +0800321 if not os.path.isdir(".repo"):
322 sys.stderr.write(
323 "ERROR: No .repo directory found. Please run this from the top of your tree.\n"
324 )
Pulser72e23242013-09-29 09:56:55 +0100325 sys.exit(1)
326
Marko Manb58468a2018-03-19 13:01:19 +0100327 # If --abandon-first is given, abandon the branch before starting
328 if args.abandon_first:
329 # Determine if the branch already exists; skip the abandon if it does not
dianlujitao20f0bdb2024-01-14 14:57:41 +0800330 plist = subprocess.check_output(["repo", "info"], text=True)
Marko Manb58468a2018-03-19 13:01:19 +0100331 needs_abandon = False
332 for pline in plist.splitlines():
dianlujitao769a9c62024-01-14 17:59:24 +0800333 matchObj = re.match(r"Local Branches.*\[(.*)\]", pline)
Marko Manb58468a2018-03-19 13:01:19 +0100334 if matchObj:
dianlujitao769a9c62024-01-14 17:59:24 +0800335 local_branches = re.split(r"\s*,\s*", matchObj.group(1))
Marko Manb58468a2018-03-19 13:01:19 +0100336 if any(args.start_branch[0] in s for s in local_branches):
337 needs_abandon = True
Pulser72e23242013-09-29 09:56:55 +0100338
Marko Manb58468a2018-03-19 13:01:19 +0100339 if needs_abandon:
340 # Perform the abandon only if the branch already exists
341 if not args.quiet:
dianlujitao769a9c62024-01-14 17:59:24 +0800342 print("Abandoning branch: %s" % args.start_branch[0])
dianlujitao20f0bdb2024-01-14 14:57:41 +0800343 subprocess.run(["repo", "abandon", args.start_branch[0]])
Marko Manb58468a2018-03-19 13:01:19 +0100344 if not args.quiet:
dianlujitao769a9c62024-01-14 17:59:24 +0800345 print("")
Pulser72e23242013-09-29 09:56:55 +0100346
Marko Manb58468a2018-03-19 13:01:19 +0100347 # Get the master manifest from repo
348 # - convert project name and revision to a path
349 project_name_to_data = {}
dianlujitao20f0bdb2024-01-14 14:57:41 +0800350 manifest = subprocess.check_output(["repo", "manifest"], text=True)
Marko Manb58468a2018-03-19 13:01:19 +0100351 xml_root = ElementTree.fromstring(manifest)
dianlujitao769a9c62024-01-14 17:59:24 +0800352 projects = xml_root.findall("project")
353 remotes = xml_root.findall("remote")
354 default_revision = xml_root.findall("default")[0].get("revision")
Marko Manb58468a2018-03-19 13:01:19 +0100355
Aayush Gupta2f4522c2020-07-26 07:19:19 +0000356 # dump project data into the a list of dicts with the following data:
357 # {project: {path, revision}}
Marko Manb58468a2018-03-19 13:01:19 +0100358
359 for project in projects:
dianlujitao769a9c62024-01-14 17:59:24 +0800360 name = project.get("name")
Aaron Kling83fee0f2020-06-19 18:43:16 -0500361 # 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 +0800362 path = project.get("path", name)
363 revision = project.get("upstream")
Marko Manb58468a2018-03-19 13:01:19 +0100364 if revision is None:
365 for remote in remotes:
dianlujitao769a9c62024-01-14 17:59:24 +0800366 if remote.get("name") == project.get("remote"):
367 revision = remote.get("revision")
Marko Manb58468a2018-03-19 13:01:19 +0100368 if revision is None:
dianlujitao769a9c62024-01-14 17:59:24 +0800369 revision = project.get("revision", default_revision)
Marko Manb58468a2018-03-19 13:01:19 +0100370
Aayush Gupta2f4522c2020-07-26 07:19:19 +0000371 if name not in project_name_to_data:
Marko Manb58468a2018-03-19 13:01:19 +0100372 project_name_to_data[name] = {}
dianlujitao769a9c62024-01-14 17:59:24 +0800373 revision = revision.split("refs/heads/")[-1]
Marko Manb58468a2018-03-19 13:01:19 +0100374 project_name_to_data[name][revision] = path
375
376 # get data on requested changes
377 reviews = []
378 change_numbers = []
Gabriele Md91609d2018-03-31 14:26:59 +0200379
380 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
Marko Manb58468a2018-03-19 13:01:19 +0100396 if args.topic:
dianlujitao769a9c62024-01-14 17:59:24 +0800397 reviews = fetch_query(args.gerrit, "topic:{0}".format(args.topic))
398 change_numbers = [
399 str(r["number"]) for r in sorted(reviews, key=cmp_to_key(cmp_reviews))
400 ]
Marko Manb58468a2018-03-19 13:01:19 +0100401 if args.query:
402 reviews = fetch_query(args.gerrit, args.query)
dianlujitao769a9c62024-01-14 17:59:24 +0800403 change_numbers = [
404 str(r["number"]) for r in sorted(reviews, key=cmp_to_key(cmp_reviews))
405 ]
Marko Manb58468a2018-03-19 13:01:19 +0100406 if args.change_number:
dianlujitao769a9c62024-01-14 17:59:24 +0800407 change_url_re = re.compile(r"https?://.+?/([0-9]+(?:/[0-9]+)?)/?")
Marko Manb58468a2018-03-19 13:01:19 +0100408 for c in args.change_number:
Gabriele M1009aeb2018-04-01 17:50:58 +0200409 change_number = change_url_re.findall(c)
410 if change_number:
411 change_numbers.extend(change_number)
dianlujitao769a9c62024-01-14 17:59:24 +0800412 elif "-" in c:
413 templist = c.split("-")
Marko Manb58468a2018-03-19 13:01:19 +0100414 for i in range(int(templist[0]), int(templist[1]) + 1):
415 change_numbers.append(str(i))
416 else:
417 change_numbers.append(c)
dianlujitao769a9c62024-01-14 17:59:24 +0800418 reviews = fetch_query(
419 args.gerrit,
420 " OR ".join("change:{0}".format(x.split("/")[0]) for x in change_numbers),
421 )
Marko Manb58468a2018-03-19 13:01:19 +0100422
423 # make list of things to actually merge
424 mergables = []
425
426 # If --exclude is given, create the list of commits to ignore
427 exclude = []
428 if args.exclude:
dianlujitao769a9c62024-01-14 17:59:24 +0800429 exclude = args.exclude[0].split(",")
Marko Manb58468a2018-03-19 13:01:19 +0100430
431 for change in change_numbers:
432 patchset = None
dianlujitao769a9c62024-01-14 17:59:24 +0800433 if "/" in change:
434 (change, patchset) = change.split("/")
Marko Manb58468a2018-03-19 13:01:19 +0100435
436 if change in exclude:
437 continue
438
439 change = int(change)
440
441 if patchset is not None:
442 patchset = int(patchset)
443
dianlujitao769a9c62024-01-14 17:59:24 +0800444 review = next((x for x in reviews if x["number"] == change), None)
Marko Manb58468a2018-03-19 13:01:19 +0100445 if review is None:
dianlujitao769a9c62024-01-14 17:59:24 +0800446 print("Change %d not found, skipping" % change)
Marko Manb58468a2018-03-19 13:01:19 +0100447 continue
448
dianlujitao769a9c62024-01-14 17:59:24 +0800449 mergables.append(
450 {
451 "subject": review["subject"],
452 "project": review["project"],
453 "branch": review["branch"],
454 "change_id": review["change_id"],
455 "change_number": review["number"],
456 "status": review["status"],
457 "fetch": None,
458 "patchset": review["revisions"][review["current_revision"]]["_number"],
459 }
460 )
Gabriele M1188cbd2018-04-01 17:50:57 +0200461
dianlujitao769a9c62024-01-14 17:59:24 +0800462 mergables[-1]["fetch"] = review["revisions"][review["current_revision"]][
463 "fetch"
464 ]
465 mergables[-1]["id"] = change
Marko Manb58468a2018-03-19 13:01:19 +0100466 if patchset:
467 try:
dianlujitao769a9c62024-01-14 17:59:24 +0800468 mergables[-1]["fetch"] = [
469 review["revisions"][x]["fetch"]
470 for x in review["revisions"]
471 if review["revisions"][x]["_number"] == patchset
472 ][0]
473 mergables[-1]["id"] = "{0}/{1}".format(change, patchset)
474 mergables[-1]["patchset"] = patchset
Marko Manb58468a2018-03-19 13:01:19 +0100475 except (IndexError, ValueError):
dianlujitao769a9c62024-01-14 17:59:24 +0800476 args.quiet or print(
477 "ERROR: The patch set {0}/{1} could not be found, using CURRENT_REVISION instead.".format(
478 change, patchset
479 )
480 )
Marko Manb58468a2018-03-19 13:01:19 +0100481
482 for item in mergables:
dianlujitao769a9c62024-01-14 17:59:24 +0800483 args.quiet or print("Applying change number {0}...".format(item["id"]))
Marko Manb58468a2018-03-19 13:01:19 +0100484 # Check if change is open and exit if it's not, unless -f is specified
dianlujitao769a9c62024-01-14 17:59:24 +0800485 if (
486 item["status"] != "OPEN"
487 and item["status"] != "NEW"
488 and item["status"] != "DRAFT"
489 ):
Marko Manb58468a2018-03-19 13:01:19 +0100490 if args.force:
dianlujitao769a9c62024-01-14 17:59:24 +0800491 print("!! Force-picking a closed change !!\n")
Marko Manb58468a2018-03-19 13:01:19 +0100492 else:
dianlujitao769a9c62024-01-14 17:59:24 +0800493 print(
494 "Change status is "
495 + item["status"]
496 + ". Skipping the cherry pick.\nUse -f to force this pick."
497 )
Marko Manb58468a2018-03-19 13:01:19 +0100498 continue
Pulser72e23242013-09-29 09:56:55 +0100499
500 # Convert the project name to a project path
501 # - check that the project path exists
Marko Manb58468a2018-03-19 13:01:19 +0100502 project_path = None
503
dianlujitao769a9c62024-01-14 17:59:24 +0800504 if (
505 item["project"] in project_name_to_data
506 and item["branch"] in project_name_to_data[item["project"]]
507 ):
508 project_path = project_name_to_data[item["project"]][item["branch"]]
Marko Manb58468a2018-03-19 13:01:19 +0100509 elif args.path:
510 project_path = args.path
dianlujitao769a9c62024-01-14 17:59:24 +0800511 elif (
512 item["project"] in project_name_to_data
513 and len(project_name_to_data[item["project"]]) == 1
514 ):
515 local_branch = list(project_name_to_data[item["project"]])[0]
516 project_path = project_name_to_data[item["project"]][local_branch]
517 print(
518 'WARNING: Project {0} has a different branch ("{1}" != "{2}")'.format(
519 project_path, local_branch, item["branch"]
520 )
521 )
Pulser72e23242013-09-29 09:56:55 +0100522 elif args.ignore_missing:
dianlujitao769a9c62024-01-14 17:59:24 +0800523 print(
524 "WARNING: Skipping {0} since there is no project directory for: {1}\n".format(
525 item["id"], item["project"]
526 )
527 )
Marko Manb58468a2018-03-19 13:01:19 +0100528 continue
Pulser72e23242013-09-29 09:56:55 +0100529 else:
dianlujitao769a9c62024-01-14 17:59:24 +0800530 sys.stderr.write(
531 "ERROR: For {0}, could not determine the project path for project {1}\n".format(
532 item["id"], item["project"]
533 )
534 )
Marko Manb58468a2018-03-19 13:01:19 +0100535 sys.exit(1)
Pulser72e23242013-09-29 09:56:55 +0100536
537 # If --start-branch is given, create the branch (more than once per path is okay; repo ignores gracefully)
538 if args.start_branch:
dianlujitao20f0bdb2024-01-14 14:57:41 +0800539 subprocess.run(["repo", "start", args.start_branch[0], project_path])
Marko Manb58468a2018-03-19 13:01:19 +0100540
541 # Determine the maximum commits to check already picked changes
542 check_picked_count = args.check_picked
dianlujitao769a9c62024-01-14 17:59:24 +0800543 branch_commits_count = int(
544 subprocess.check_output(
dianlujitao20f0bdb2024-01-14 14:57:41 +0800545 [
546 "git",
547 "rev-list",
548 "--count",
549 "--max-count",
550 str(check_picked_count + 1),
551 "HEAD",
552 ],
553 cwd=project_path,
554 text=True,
dianlujitao769a9c62024-01-14 17:59:24 +0800555 )
556 )
Marko Manb58468a2018-03-19 13:01:19 +0100557 if branch_commits_count <= check_picked_count:
558 check_picked_count = branch_commits_count - 1
559
560 # Check if change is already picked to HEAD...HEAD~check_picked_count
561 found_change = False
562 for i in range(0, check_picked_count):
dianlujitao769a9c62024-01-14 17:59:24 +0800563 if subprocess.call(
564 ["git", "cat-file", "-e", "HEAD~{0}".format(i)],
565 cwd=project_path,
566 stderr=open(os.devnull, "wb"),
567 ):
Marko Manb58468a2018-03-19 13:01:19 +0100568 continue
dianlujitao769a9c62024-01-14 17:59:24 +0800569 output = subprocess.check_output(
dianlujitao20f0bdb2024-01-14 14:57:41 +0800570 ["git", "show", "-q", f"HEAD~{i}"], cwd=project_path, text=True
dianlujitao769a9c62024-01-14 17:59:24 +0800571 )
Simon Shields6a726492019-11-18 23:56:08 +1100572 output = output.split()
dianlujitao769a9c62024-01-14 17:59:24 +0800573 if "Change-Id:" in output:
574 head_change_id = ""
Aayush Gupta2f4522c2020-07-26 07:19:19 +0000575 for j, t in enumerate(reversed(output)):
dianlujitao769a9c62024-01-14 17:59:24 +0800576 if t == "Change-Id:":
Marko Manb58468a2018-03-19 13:01:19 +0100577 head_change_id = output[len(output) - j]
578 break
dianlujitao769a9c62024-01-14 17:59:24 +0800579 if head_change_id.strip() == item["change_id"]:
580 print(
581 "Skipping {0} - already picked in {1} as HEAD~{2}".format(
582 item["id"], project_path, i
583 )
584 )
Marko Manb58468a2018-03-19 13:01:19 +0100585 found_change = True
586 break
587 if found_change:
588 continue
Pulser72e23242013-09-29 09:56:55 +0100589
590 # Print out some useful info
591 if not args.quiet:
dianlujitao769a9c62024-01-14 17:59:24 +0800592 print('--> Subject: "{0}"'.format(item["subject"]))
593 print("--> Project path: {0}".format(project_path))
594 print(
595 "--> Change number: {0} (Patch Set {1})".format(
596 item["id"], item["patchset"]
597 )
598 )
Pulser72e23242013-09-29 09:56:55 +0100599
dianlujitao769a9c62024-01-14 17:59:24 +0800600 if "anonymous http" in item["fetch"]:
601 method = "anonymous http"
Pulser72e23242013-09-29 09:56:55 +0100602 else:
dianlujitao769a9c62024-01-14 17:59:24 +0800603 method = "ssh"
Pulser72e23242013-09-29 09:56:55 +0100604
dianlujitao20f0bdb2024-01-14 14:57:41 +0800605 if args.pull:
606 cmd = ["git", "pull", "--no-edit"]
607 else:
608 cmd = ["git", "fetch"]
609 if args.quiet:
610 cmd.append("--quiet")
611 cmd.extend(["", item["fetch"][method]["ref"]])
612
Marko Manb58468a2018-03-19 13:01:19 +0100613 # Try fetching from GitHub first if using default gerrit
614 if args.gerrit == default_gerrit:
615 if args.verbose:
dianlujitao769a9c62024-01-14 17:59:24 +0800616 print("Trying to fetch the change from GitHub")
Marko Manb58468a2018-03-19 13:01:19 +0100617
dianlujitao20f0bdb2024-01-14 14:57:41 +0800618 cmd[-2] = "omnirom"
619 if not args.quiet:
Marko Manb58468a2018-03-19 13:01:19 +0100620 print(cmd)
dianlujitao20f0bdb2024-01-14 14:57:41 +0800621 result = subprocess.call(cmd, cwd=project_path)
dianlujitao769a9c62024-01-14 17:59:24 +0800622 FETCH_HEAD = "{0}/.git/FETCH_HEAD".format(project_path)
Marko Manb58468a2018-03-19 13:01:19 +0100623 if result != 0 and os.stat(FETCH_HEAD).st_size != 0:
dianlujitao769a9c62024-01-14 17:59:24 +0800624 print("ERROR: git command failed")
Marko Manb58468a2018-03-19 13:01:19 +0100625 sys.exit(result)
626 # Check if it worked
627 if args.gerrit != default_gerrit or os.stat(FETCH_HEAD).st_size == 0:
628 # If not using the default gerrit or github failed, fetch from gerrit.
629 if args.verbose:
630 if args.gerrit == default_gerrit:
dianlujitao769a9c62024-01-14 17:59:24 +0800631 print(
632 "Fetching from GitHub didn't work, trying to fetch the change from Gerrit"
633 )
Marko Manb58468a2018-03-19 13:01:19 +0100634 else:
dianlujitao769a9c62024-01-14 17:59:24 +0800635 print("Fetching from {0}".format(args.gerrit))
Marko Manb58468a2018-03-19 13:01:19 +0100636
dianlujitao20f0bdb2024-01-14 14:57:41 +0800637 cmd[-2] = item["fetch"][method]["url"]
638 if not args.quiet:
Marko Manb58468a2018-03-19 13:01:19 +0100639 print(cmd)
dianlujitao20f0bdb2024-01-14 14:57:41 +0800640 result = subprocess.call(cmd, cwd=project_path)
Marko Manb58468a2018-03-19 13:01:19 +0100641 if result != 0:
dianlujitao769a9c62024-01-14 17:59:24 +0800642 print("ERROR: git command failed")
Marko Manb58468a2018-03-19 13:01:19 +0100643 sys.exit(result)
644 # Perform the cherry-pick
645 if not args.pull:
Marko Manb58468a2018-03-19 13:01:19 +0100646 if args.quiet:
dianlujitao20f0bdb2024-01-14 14:57:41 +0800647 cmd_out = subprocess.DEVNULL
Marko Manb58468a2018-03-19 13:01:19 +0100648 else:
649 cmd_out = None
dianlujitao769a9c62024-01-14 17:59:24 +0800650 result = subprocess.call(
dianlujitao20f0bdb2024-01-14 14:57:41 +0800651 ["git", "cherry-pick", "--ff", item["revision"]],
652 cwd=project_path,
653 stdout=cmd_out,
654 stderr=cmd_out,
dianlujitao769a9c62024-01-14 17:59:24 +0800655 )
Marko Manb58468a2018-03-19 13:01:19 +0100656 if result != 0:
dianlujitao769a9c62024-01-14 17:59:24 +0800657 result = subprocess.call(
dianlujitao20f0bdb2024-01-14 14:57:41 +0800658 ["git", "diff-index", "--quiet", "HEAD", "--"],
659 cwd=project_path,
660 stdout=cmd_out,
661 stderr=cmd_out,
dianlujitao769a9c62024-01-14 17:59:24 +0800662 )
Adrian DC7bc808f2018-08-30 23:07:23 +0200663 if result == 0:
dianlujitao769a9c62024-01-14 17:59:24 +0800664 print(
665 "WARNING: git command resulted with an empty commit, aborting cherry-pick"
666 )
dianlujitao769a9c62024-01-14 17:59:24 +0800667 subprocess.call(
dianlujitao20f0bdb2024-01-14 14:57:41 +0800668 ["git", "cherry-pick", "--abort"],
dianlujitao769a9c62024-01-14 17:59:24 +0800669 cwd=project_path,
dianlujitao769a9c62024-01-14 17:59:24 +0800670 stdout=cmd_out,
671 stderr=cmd_out,
672 )
Adrian DC7bc808f2018-08-30 23:07:23 +0200673 elif args.reset:
dianlujitao769a9c62024-01-14 17:59:24 +0800674 print("ERROR: git command failed, aborting cherry-pick")
dianlujitao769a9c62024-01-14 17:59:24 +0800675 subprocess.call(
dianlujitao20f0bdb2024-01-14 14:57:41 +0800676 ["git", "cherry-pick", "--abort"],
dianlujitao769a9c62024-01-14 17:59:24 +0800677 cwd=project_path,
dianlujitao769a9c62024-01-14 17:59:24 +0800678 stdout=cmd_out,
679 stderr=cmd_out,
680 )
Adrian DC7bc808f2018-08-30 23:07:23 +0200681 sys.exit(result)
Marko Manb58468a2018-03-19 13:01:19 +0100682 else:
dianlujitao769a9c62024-01-14 17:59:24 +0800683 print("ERROR: git command failed")
Adrian DC7bc808f2018-08-30 23:07:23 +0200684 sys.exit(result)
Pulser72e23242013-09-29 09:56:55 +0100685 if not args.quiet:
dianlujitao769a9c62024-01-14 17:59:24 +0800686 print("")