blob: 16fab5fd1f24fcb2d48541258e96b8c349625a45 [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
Gabriele Md91609d2018-03-31 14:26:59 +020030from functools import cmp_to_key
Marko Manb58468a2018-03-19 13:01:19 +010031from xml.etree import ElementTree
Pulser72e23242013-09-29 09:56:55 +010032
33try:
Marko Manb58468a2018-03-19 13:01:19 +010034 import requests
Pulser72e23242013-09-29 09:56:55 +010035except ImportError:
dianlujitaoe34c26d2024-01-13 17:33:41 +080036 import urllib.request
Pulser72e23242013-09-29 09:56:55 +010037
Pulser72e23242013-09-29 09:56:55 +010038
Luca Weissd1bbac62018-11-25 14:07:12 +010039# cmp() is not available in Python 3, define it manually
40# See https://docs.python.org/3.0/whatsnew/3.0.html#ordering-comparisons
41def cmp(a, b):
42 return (a > b) - (a < b)
43
44
Pulser72e23242013-09-29 09:56:55 +010045# Verifies whether pathA is a subdirectory (or the same) as pathB
Marko Manb58468a2018-03-19 13:01:19 +010046def is_subdir(a, b):
dianlujitao769a9c62024-01-14 17:59:24 +080047 a = os.path.realpath(a) + "/"
48 b = os.path.realpath(b) + "/"
49 return b == a[: len(b)]
Pulser72e23242013-09-29 09:56:55 +010050
Pulser72e23242013-09-29 09:56:55 +010051
Marko Manb58468a2018-03-19 13:01:19 +010052def fetch_query_via_ssh(remote_url, query):
53 """Given a remote_url and a query, return the list of changes that fit it
dianlujitao769a9c62024-01-14 17:59:24 +080054 This function is slightly messy - the ssh api does not return data in the same structure as the HTTP REST API
55 We have to get the data, then transform it to match what we're expecting from the HTTP RESET API
56 """
57 if remote_url.count(":") == 2:
58 (uri, userhost, port) = remote_url.split(":")
Marko Manb58468a2018-03-19 13:01:19 +010059 userhost = userhost[2:]
dianlujitao769a9c62024-01-14 17:59:24 +080060 elif remote_url.count(":") == 1:
61 (uri, userhost) = remote_url.split(":")
Marko Manb58468a2018-03-19 13:01:19 +010062 userhost = userhost[2:]
63 port = 29418
Pulser72e23242013-09-29 09:56:55 +010064 else:
dianlujitao769a9c62024-01-14 17:59:24 +080065 raise Exception("Malformed URI: Expecting ssh://[user@]host[:port]")
Pulser72e23242013-09-29 09:56:55 +010066
dianlujitao769a9c62024-01-14 17:59:24 +080067 out = subprocess.check_output(
68 [
69 "ssh",
70 "-x",
71 "-p{0}".format(port),
72 userhost,
73 "gerrit",
74 "query",
75 "--format=JSON --patch-sets --current-patch-set",
76 query,
77 ]
78 )
79 if not hasattr(out, "encode"):
Marko Manb58468a2018-03-19 13:01:19 +010080 out = out.decode()
81 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
Marko Manb58468a2018-03-19 13:01:19 +0100121def fetch_query_via_http(remote_url, query):
122 if "requests" in sys.modules:
123 auth = None
124 if os.path.isfile(os.getenv("HOME") + "/.gerritrc"):
125 f = open(os.getenv("HOME") + "/.gerritrc", "r")
126 for line in f:
127 parts = line.rstrip().split("|")
128 if parts[0] in remote_url:
dianlujitao769a9c62024-01-14 17:59:24 +0800129 auth = requests.auth.HTTPBasicAuth(
130 username=parts[1], password=parts[2]
131 )
132 status_code = "-1"
Marko Manb58468a2018-03-19 13:01:19 +0100133 if auth:
dianlujitao769a9c62024-01-14 17:59:24 +0800134 url = "{0}/a/changes/?q={1}&o=CURRENT_REVISION&o=ALL_REVISIONS&o=ALL_COMMITS".format(
135 remote_url, query
136 )
Marko Manb58468a2018-03-19 13:01:19 +0100137 data = requests.get(url, auth=auth)
Aayush Gupta2f4522c2020-07-26 07:19:19 +0000138 status_code = str(data.status_code)
dianlujitao769a9c62024-01-14 17:59:24 +0800139 if status_code != "200":
140 # They didn't get good authorization or data, Let's try the old way
141 url = "{0}/changes/?q={1}&o=CURRENT_REVISION&o=ALL_REVISIONS&o=ALL_COMMITS".format(
142 remote_url, query
143 )
Marko Manb58468a2018-03-19 13:01:19 +0100144 data = requests.get(url)
145 reviews = json.loads(data.text[5:])
146 else:
147 """Given a query, fetch the change numbers via http"""
dianlujitao769a9c62024-01-14 17:59:24 +0800148 url = "{0}/changes/?q={1}&o=CURRENT_REVISION&o=ALL_REVISIONS&o=ALL_COMMITS".format(
149 remote_url, query
150 )
151 data = urllib.request.urlopen(url).read().decode("utf-8")
Marko Manb58468a2018-03-19 13:01:19 +0100152 reviews = json.loads(data[5:])
153
154 for review in reviews:
dianlujitao769a9c62024-01-14 17:59:24 +0800155 review["number"] = review.pop("_number")
Marko Manb58468a2018-03-19 13:01:19 +0100156
157 return reviews
158
159
160def fetch_query(remote_url, query):
161 """Wrapper for fetch_query_via_proto functions"""
dianlujitao769a9c62024-01-14 17:59:24 +0800162 if remote_url[0:3] == "ssh":
Marko Manb58468a2018-03-19 13:01:19 +0100163 return fetch_query_via_ssh(remote_url, query)
dianlujitao769a9c62024-01-14 17:59:24 +0800164 elif remote_url[0:4] == "http":
165 return fetch_query_via_http(remote_url, query.replace(" ", "+"))
Marko Manb58468a2018-03-19 13:01:19 +0100166 else:
dianlujitao769a9c62024-01-14 17:59:24 +0800167 raise Exception(
168 "Gerrit URL should be in the form http[s]://hostname/ or ssh://[user@]host[:port]"
169 )
Marko Manb58468a2018-03-19 13:01:19 +0100170
Aayush Gupta2f4522c2020-07-26 07:19:19 +0000171
dianlujitao769a9c62024-01-14 17:59:24 +0800172if __name__ == "__main__":
Marko Manb58468a2018-03-19 13:01:19 +0100173 # Default to OmniRom Gerrit
dianlujitao769a9c62024-01-14 17:59:24 +0800174 default_gerrit = "https://gerrit.omnirom.org"
Marko Manb58468a2018-03-19 13:01:19 +0100175
dianlujitao769a9c62024-01-14 17:59:24 +0800176 parser = argparse.ArgumentParser(
177 formatter_class=argparse.RawDescriptionHelpFormatter,
178 description=textwrap.dedent(
179 """\
Marko Manb58468a2018-03-19 13:01:19 +0100180 repopick.py is a utility to simplify the process of cherry picking
181 patches from OmniRom's Gerrit instance (or any gerrit instance of your choosing)
182
183 Given a list of change numbers, repopick will cd into the project path
184 and cherry pick the latest patch available.
185
186 With the --start-branch argument, the user can specify that a branch
187 should be created before cherry picking. This is useful for
188 cherry-picking many patches into a common branch which can be easily
189 abandoned later (good for testing other's changes.)
190
191 The --abandon-first argument, when used in conjunction with the
192 --start-branch option, will cause repopick to abandon the specified
dianlujitao769a9c62024-01-14 17:59:24 +0800193 branch in all repos first before performing any cherry picks."""
194 ),
195 )
196 parser.add_argument(
197 "change_number",
198 nargs="*",
199 help="change number to cherry pick. Use {change number}/{patchset number} to get a specific revision.",
200 )
201 parser.add_argument(
202 "-i",
203 "--ignore-missing",
204 action="store_true",
205 help="do not error out if a patch applies to a missing directory",
206 )
207 parser.add_argument(
208 "-s",
209 "--start-branch",
210 nargs=1,
211 metavar="",
212 help="start the specified branch before cherry picking",
213 )
214 parser.add_argument(
215 "-r",
216 "--reset",
217 action="store_true",
218 help="reset to initial state (abort cherry-pick) if there is a conflict",
219 )
220 parser.add_argument(
221 "-a",
222 "--abandon-first",
223 action="store_true",
224 help="before cherry picking, abandon the branch specified in --start-branch",
225 )
226 parser.add_argument(
227 "-b",
228 "--auto-branch",
229 action="store_true",
230 help='shortcut to "--start-branch auto --abandon-first --ignore-missing"',
231 )
232 parser.add_argument(
233 "-q", "--quiet", action="store_true", help="print as little as possible"
234 )
235 parser.add_argument(
236 "-v",
237 "--verbose",
238 action="store_true",
239 help="print extra information to aid in debug",
240 )
241 parser.add_argument(
242 "-f",
243 "--force",
244 action="store_true",
245 help="force cherry pick even if change is closed",
246 )
247 parser.add_argument(
248 "-p", "--pull", action="store_true", help="execute pull instead of cherry-pick"
249 )
250 parser.add_argument(
251 "-P", "--path", metavar="", help="use the specified path for the change"
252 )
253 parser.add_argument(
254 "-t", "--topic", metavar="", help="pick all commits from a specified topic"
255 )
256 parser.add_argument(
257 "-Q", "--query", metavar="", help="pick all commits using the specified query"
258 )
259 parser.add_argument(
260 "-g",
261 "--gerrit",
262 default=default_gerrit,
263 metavar="",
264 help="Gerrit Instance to use. Form proto://[user@]host[:port]",
265 )
266 parser.add_argument(
267 "-e",
268 "--exclude",
269 nargs=1,
270 metavar="",
271 help="exclude a list of commit numbers separated by a ,",
272 )
273 parser.add_argument(
274 "-c",
275 "--check-picked",
276 type=int,
277 default=10,
278 metavar="",
279 help="pass the amount of commits to check for already picked changes",
280 )
Marko Manb58468a2018-03-19 13:01:19 +0100281 args = parser.parse_args()
282 if not args.start_branch and args.abandon_first:
dianlujitao769a9c62024-01-14 17:59:24 +0800283 parser.error(
284 "if --abandon-first is set, you must also give the branch name with --start-branch"
285 )
Marko Manb58468a2018-03-19 13:01:19 +0100286 if args.auto_branch:
287 args.abandon_first = True
288 args.ignore_missing = True
289 if not args.start_branch:
dianlujitao769a9c62024-01-14 17:59:24 +0800290 args.start_branch = ["auto"]
Marko Manb58468a2018-03-19 13:01:19 +0100291 if args.quiet and args.verbose:
dianlujitao769a9c62024-01-14 17:59:24 +0800292 parser.error("--quiet and --verbose cannot be specified together")
Marko Manb58468a2018-03-19 13:01:19 +0100293
294 if (1 << bool(args.change_number) << bool(args.topic) << bool(args.query)) != 2:
dianlujitao769a9c62024-01-14 17:59:24 +0800295 parser.error(
296 "One (and only one) of change_number, topic, and query are allowed"
297 )
Marko Manb58468a2018-03-19 13:01:19 +0100298
299 # Change current directory to the top of the tree
dianlujitao769a9c62024-01-14 17:59:24 +0800300 if "ANDROID_BUILD_TOP" in os.environ:
301 top = os.environ["ANDROID_BUILD_TOP"]
Marko Manb58468a2018-03-19 13:01:19 +0100302
303 if not is_subdir(os.getcwd(), top):
dianlujitao769a9c62024-01-14 17:59:24 +0800304 sys.stderr.write(
305 "ERROR: You must run this tool from within $ANDROID_BUILD_TOP!\n"
306 )
Marko Manb58468a2018-03-19 13:01:19 +0100307 sys.exit(1)
dianlujitao769a9c62024-01-14 17:59:24 +0800308 os.chdir(os.environ["ANDROID_BUILD_TOP"])
Marko Manb58468a2018-03-19 13:01:19 +0100309
310 # Sanity check that we are being run from the top level of the tree
dianlujitao769a9c62024-01-14 17:59:24 +0800311 if not os.path.isdir(".repo"):
312 sys.stderr.write(
313 "ERROR: No .repo directory found. Please run this from the top of your tree.\n"
314 )
Pulser72e23242013-09-29 09:56:55 +0100315 sys.exit(1)
316
Marko Manb58468a2018-03-19 13:01:19 +0100317 # If --abandon-first is given, abandon the branch before starting
318 if args.abandon_first:
319 # Determine if the branch already exists; skip the abandon if it does not
dianlujitao769a9c62024-01-14 17:59:24 +0800320 plist = subprocess.check_output(["repo", "info"])
321 if not hasattr(plist, "encode"):
Marko Manb58468a2018-03-19 13:01:19 +0100322 plist = plist.decode()
323 needs_abandon = False
324 for pline in plist.splitlines():
dianlujitao769a9c62024-01-14 17:59:24 +0800325 matchObj = re.match(r"Local Branches.*\[(.*)\]", pline)
Marko Manb58468a2018-03-19 13:01:19 +0100326 if matchObj:
dianlujitao769a9c62024-01-14 17:59:24 +0800327 local_branches = re.split(r"\s*,\s*", matchObj.group(1))
Marko Manb58468a2018-03-19 13:01:19 +0100328 if any(args.start_branch[0] in s for s in local_branches):
329 needs_abandon = True
Pulser72e23242013-09-29 09:56:55 +0100330
Marko Manb58468a2018-03-19 13:01:19 +0100331 if needs_abandon:
332 # Perform the abandon only if the branch already exists
333 if not args.quiet:
dianlujitao769a9c62024-01-14 17:59:24 +0800334 print("Abandoning branch: %s" % args.start_branch[0])
335 subprocess.check_output(["repo", "abandon", args.start_branch[0]])
Marko Manb58468a2018-03-19 13:01:19 +0100336 if not args.quiet:
dianlujitao769a9c62024-01-14 17:59:24 +0800337 print("")
Pulser72e23242013-09-29 09:56:55 +0100338
Marko Manb58468a2018-03-19 13:01:19 +0100339 # Get the master manifest from repo
340 # - convert project name and revision to a path
341 project_name_to_data = {}
dianlujitao769a9c62024-01-14 17:59:24 +0800342 manifest = subprocess.check_output(["repo", "manifest"])
Marko Manb58468a2018-03-19 13:01:19 +0100343 xml_root = ElementTree.fromstring(manifest)
dianlujitao769a9c62024-01-14 17:59:24 +0800344 projects = xml_root.findall("project")
345 remotes = xml_root.findall("remote")
346 default_revision = xml_root.findall("default")[0].get("revision")
Marko Manb58468a2018-03-19 13:01:19 +0100347
Aayush Gupta2f4522c2020-07-26 07:19:19 +0000348 # dump project data into the a list of dicts with the following data:
349 # {project: {path, revision}}
Marko Manb58468a2018-03-19 13:01:19 +0100350
351 for project in projects:
dianlujitao769a9c62024-01-14 17:59:24 +0800352 name = project.get("name")
Aaron Kling83fee0f2020-06-19 18:43:16 -0500353 # 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 +0800354 path = project.get("path", name)
355 revision = project.get("upstream")
Marko Manb58468a2018-03-19 13:01:19 +0100356 if revision is None:
357 for remote in remotes:
dianlujitao769a9c62024-01-14 17:59:24 +0800358 if remote.get("name") == project.get("remote"):
359 revision = remote.get("revision")
Marko Manb58468a2018-03-19 13:01:19 +0100360 if revision is None:
dianlujitao769a9c62024-01-14 17:59:24 +0800361 revision = project.get("revision", default_revision)
Marko Manb58468a2018-03-19 13:01:19 +0100362
Aayush Gupta2f4522c2020-07-26 07:19:19 +0000363 if name not in project_name_to_data:
Marko Manb58468a2018-03-19 13:01:19 +0100364 project_name_to_data[name] = {}
dianlujitao769a9c62024-01-14 17:59:24 +0800365 revision = revision.split("refs/heads/")[-1]
Marko Manb58468a2018-03-19 13:01:19 +0100366 project_name_to_data[name][revision] = path
367
368 # get data on requested changes
369 reviews = []
370 change_numbers = []
Gabriele Md91609d2018-03-31 14:26:59 +0200371
372 def cmp_reviews(review_a, review_b):
dianlujitao769a9c62024-01-14 17:59:24 +0800373 current_a = review_a["current_revision"]
374 parents_a = [
375 r["commit"] for r in review_a["revisions"][current_a]["commit"]["parents"]
376 ]
377 current_b = review_b["current_revision"]
378 parents_b = [
379 r["commit"] for r in review_b["revisions"][current_b]["commit"]["parents"]
380 ]
Gabriele Md91609d2018-03-31 14:26:59 +0200381 if current_a in parents_b:
382 return -1
383 elif current_b in parents_a:
384 return 1
385 else:
dianlujitao769a9c62024-01-14 17:59:24 +0800386 return cmp(review_a["number"], review_b["number"])
Gabriele Md91609d2018-03-31 14:26:59 +0200387
Marko Manb58468a2018-03-19 13:01:19 +0100388 if args.topic:
dianlujitao769a9c62024-01-14 17:59:24 +0800389 reviews = fetch_query(args.gerrit, "topic:{0}".format(args.topic))
390 change_numbers = [
391 str(r["number"]) for r in sorted(reviews, key=cmp_to_key(cmp_reviews))
392 ]
Marko Manb58468a2018-03-19 13:01:19 +0100393 if args.query:
394 reviews = fetch_query(args.gerrit, args.query)
dianlujitao769a9c62024-01-14 17:59:24 +0800395 change_numbers = [
396 str(r["number"]) for r in sorted(reviews, key=cmp_to_key(cmp_reviews))
397 ]
Marko Manb58468a2018-03-19 13:01:19 +0100398 if args.change_number:
dianlujitao769a9c62024-01-14 17:59:24 +0800399 change_url_re = re.compile(r"https?://.+?/([0-9]+(?:/[0-9]+)?)/?")
Marko Manb58468a2018-03-19 13:01:19 +0100400 for c in args.change_number:
Gabriele M1009aeb2018-04-01 17:50:58 +0200401 change_number = change_url_re.findall(c)
402 if change_number:
403 change_numbers.extend(change_number)
dianlujitao769a9c62024-01-14 17:59:24 +0800404 elif "-" in c:
405 templist = c.split("-")
Marko Manb58468a2018-03-19 13:01:19 +0100406 for i in range(int(templist[0]), int(templist[1]) + 1):
407 change_numbers.append(str(i))
408 else:
409 change_numbers.append(c)
dianlujitao769a9c62024-01-14 17:59:24 +0800410 reviews = fetch_query(
411 args.gerrit,
412 " OR ".join("change:{0}".format(x.split("/")[0]) for x in change_numbers),
413 )
Marko Manb58468a2018-03-19 13:01:19 +0100414
415 # make list of things to actually merge
416 mergables = []
417
418 # If --exclude is given, create the list of commits to ignore
419 exclude = []
420 if args.exclude:
dianlujitao769a9c62024-01-14 17:59:24 +0800421 exclude = args.exclude[0].split(",")
Marko Manb58468a2018-03-19 13:01:19 +0100422
423 for change in change_numbers:
424 patchset = None
dianlujitao769a9c62024-01-14 17:59:24 +0800425 if "/" in change:
426 (change, patchset) = change.split("/")
Marko Manb58468a2018-03-19 13:01:19 +0100427
428 if change in exclude:
429 continue
430
431 change = int(change)
432
433 if patchset is not None:
434 patchset = int(patchset)
435
dianlujitao769a9c62024-01-14 17:59:24 +0800436 review = next((x for x in reviews if x["number"] == change), None)
Marko Manb58468a2018-03-19 13:01:19 +0100437 if review is None:
dianlujitao769a9c62024-01-14 17:59:24 +0800438 print("Change %d not found, skipping" % change)
Marko Manb58468a2018-03-19 13:01:19 +0100439 continue
440
dianlujitao769a9c62024-01-14 17:59:24 +0800441 mergables.append(
442 {
443 "subject": review["subject"],
444 "project": review["project"],
445 "branch": review["branch"],
446 "change_id": review["change_id"],
447 "change_number": review["number"],
448 "status": review["status"],
449 "fetch": None,
450 "patchset": review["revisions"][review["current_revision"]]["_number"],
451 }
452 )
Gabriele M1188cbd2018-04-01 17:50:57 +0200453
dianlujitao769a9c62024-01-14 17:59:24 +0800454 mergables[-1]["fetch"] = review["revisions"][review["current_revision"]][
455 "fetch"
456 ]
457 mergables[-1]["id"] = change
Marko Manb58468a2018-03-19 13:01:19 +0100458 if patchset:
459 try:
dianlujitao769a9c62024-01-14 17:59:24 +0800460 mergables[-1]["fetch"] = [
461 review["revisions"][x]["fetch"]
462 for x in review["revisions"]
463 if review["revisions"][x]["_number"] == patchset
464 ][0]
465 mergables[-1]["id"] = "{0}/{1}".format(change, patchset)
466 mergables[-1]["patchset"] = patchset
Marko Manb58468a2018-03-19 13:01:19 +0100467 except (IndexError, ValueError):
dianlujitao769a9c62024-01-14 17:59:24 +0800468 args.quiet or print(
469 "ERROR: The patch set {0}/{1} could not be found, using CURRENT_REVISION instead.".format(
470 change, patchset
471 )
472 )
Marko Manb58468a2018-03-19 13:01:19 +0100473
474 for item in mergables:
dianlujitao769a9c62024-01-14 17:59:24 +0800475 args.quiet or print("Applying change number {0}...".format(item["id"]))
Marko Manb58468a2018-03-19 13:01:19 +0100476 # Check if change is open and exit if it's not, unless -f is specified
dianlujitao769a9c62024-01-14 17:59:24 +0800477 if (
478 item["status"] != "OPEN"
479 and item["status"] != "NEW"
480 and item["status"] != "DRAFT"
481 ):
Marko Manb58468a2018-03-19 13:01:19 +0100482 if args.force:
dianlujitao769a9c62024-01-14 17:59:24 +0800483 print("!! Force-picking a closed change !!\n")
Marko Manb58468a2018-03-19 13:01:19 +0100484 else:
dianlujitao769a9c62024-01-14 17:59:24 +0800485 print(
486 "Change status is "
487 + item["status"]
488 + ". Skipping the cherry pick.\nUse -f to force this pick."
489 )
Marko Manb58468a2018-03-19 13:01:19 +0100490 continue
Pulser72e23242013-09-29 09:56:55 +0100491
492 # Convert the project name to a project path
493 # - check that the project path exists
Marko Manb58468a2018-03-19 13:01:19 +0100494 project_path = None
495
dianlujitao769a9c62024-01-14 17:59:24 +0800496 if (
497 item["project"] in project_name_to_data
498 and item["branch"] in project_name_to_data[item["project"]]
499 ):
500 project_path = project_name_to_data[item["project"]][item["branch"]]
Marko Manb58468a2018-03-19 13:01:19 +0100501 elif args.path:
502 project_path = args.path
dianlujitao769a9c62024-01-14 17:59:24 +0800503 elif (
504 item["project"] in project_name_to_data
505 and len(project_name_to_data[item["project"]]) == 1
506 ):
507 local_branch = list(project_name_to_data[item["project"]])[0]
508 project_path = project_name_to_data[item["project"]][local_branch]
509 print(
510 'WARNING: Project {0} has a different branch ("{1}" != "{2}")'.format(
511 project_path, local_branch, item["branch"]
512 )
513 )
Pulser72e23242013-09-29 09:56:55 +0100514 elif args.ignore_missing:
dianlujitao769a9c62024-01-14 17:59:24 +0800515 print(
516 "WARNING: Skipping {0} since there is no project directory for: {1}\n".format(
517 item["id"], item["project"]
518 )
519 )
Marko Manb58468a2018-03-19 13:01:19 +0100520 continue
Pulser72e23242013-09-29 09:56:55 +0100521 else:
dianlujitao769a9c62024-01-14 17:59:24 +0800522 sys.stderr.write(
523 "ERROR: For {0}, could not determine the project path for project {1}\n".format(
524 item["id"], item["project"]
525 )
526 )
Marko Manb58468a2018-03-19 13:01:19 +0100527 sys.exit(1)
Pulser72e23242013-09-29 09:56:55 +0100528
529 # If --start-branch is given, create the branch (more than once per path is okay; repo ignores gracefully)
530 if args.start_branch:
dianlujitao769a9c62024-01-14 17:59:24 +0800531 subprocess.check_output(
532 ["repo", "start", args.start_branch[0], project_path]
533 )
Marko Manb58468a2018-03-19 13:01:19 +0100534
535 # Determine the maximum commits to check already picked changes
536 check_picked_count = args.check_picked
dianlujitao769a9c62024-01-14 17:59:24 +0800537 max_count = "--max-count={0}".format(check_picked_count + 1)
538 branch_commits_count = int(
539 subprocess.check_output(
540 ["git", "rev-list", "--count", max_count, "HEAD"], cwd=project_path
541 )
542 )
Marko Manb58468a2018-03-19 13:01:19 +0100543 if branch_commits_count <= check_picked_count:
544 check_picked_count = branch_commits_count - 1
545
546 # Check if change is already picked to HEAD...HEAD~check_picked_count
547 found_change = False
548 for i in range(0, check_picked_count):
dianlujitao769a9c62024-01-14 17:59:24 +0800549 if subprocess.call(
550 ["git", "cat-file", "-e", "HEAD~{0}".format(i)],
551 cwd=project_path,
552 stderr=open(os.devnull, "wb"),
553 ):
Marko Manb58468a2018-03-19 13:01:19 +0100554 continue
dianlujitao769a9c62024-01-14 17:59:24 +0800555 output = subprocess.check_output(
556 ["git", "show", "-q", "HEAD~{0}".format(i)], cwd=project_path
557 )
Simon Shields6a726492019-11-18 23:56:08 +1100558 # make sure we have a string on Python 3
559 if isinstance(output, bytes):
dianlujitao769a9c62024-01-14 17:59:24 +0800560 output = output.decode("utf-8")
Simon Shields6a726492019-11-18 23:56:08 +1100561 output = output.split()
dianlujitao769a9c62024-01-14 17:59:24 +0800562 if "Change-Id:" in output:
563 head_change_id = ""
Aayush Gupta2f4522c2020-07-26 07:19:19 +0000564 for j, t in enumerate(reversed(output)):
dianlujitao769a9c62024-01-14 17:59:24 +0800565 if t == "Change-Id:":
Marko Manb58468a2018-03-19 13:01:19 +0100566 head_change_id = output[len(output) - j]
567 break
dianlujitao769a9c62024-01-14 17:59:24 +0800568 if head_change_id.strip() == item["change_id"]:
569 print(
570 "Skipping {0} - already picked in {1} as HEAD~{2}".format(
571 item["id"], project_path, i
572 )
573 )
Marko Manb58468a2018-03-19 13:01:19 +0100574 found_change = True
575 break
576 if found_change:
577 continue
Pulser72e23242013-09-29 09:56:55 +0100578
579 # Print out some useful info
580 if not args.quiet:
dianlujitao769a9c62024-01-14 17:59:24 +0800581 print('--> Subject: "{0}"'.format(item["subject"]))
582 print("--> Project path: {0}".format(project_path))
583 print(
584 "--> Change number: {0} (Patch Set {1})".format(
585 item["id"], item["patchset"]
586 )
587 )
Pulser72e23242013-09-29 09:56:55 +0100588
dianlujitao769a9c62024-01-14 17:59:24 +0800589 if "anonymous http" in item["fetch"]:
590 method = "anonymous http"
Pulser72e23242013-09-29 09:56:55 +0100591 else:
dianlujitao769a9c62024-01-14 17:59:24 +0800592 method = "ssh"
Pulser72e23242013-09-29 09:56:55 +0100593
Marko Manb58468a2018-03-19 13:01:19 +0100594 # Try fetching from GitHub first if using default gerrit
595 if args.gerrit == default_gerrit:
596 if args.verbose:
dianlujitao769a9c62024-01-14 17:59:24 +0800597 print("Trying to fetch the change from GitHub")
Marko Manb58468a2018-03-19 13:01:19 +0100598
599 if args.pull:
dianlujitao769a9c62024-01-14 17:59:24 +0800600 cmd = ["git pull --no-edit omnirom", item["fetch"][method]["ref"]]
Marko Manb58468a2018-03-19 13:01:19 +0100601 else:
dianlujitao769a9c62024-01-14 17:59:24 +0800602 cmd = ["git fetch omnirom", item["fetch"][method]["ref"]]
Marko Manb58468a2018-03-19 13:01:19 +0100603 if args.quiet:
dianlujitao769a9c62024-01-14 17:59:24 +0800604 cmd.append("--quiet")
Marko Manb58468a2018-03-19 13:01:19 +0100605 else:
606 print(cmd)
dianlujitao769a9c62024-01-14 17:59:24 +0800607 result = subprocess.call([" ".join(cmd)], cwd=project_path, shell=True)
608 FETCH_HEAD = "{0}/.git/FETCH_HEAD".format(project_path)
Marko Manb58468a2018-03-19 13:01:19 +0100609 if result != 0 and os.stat(FETCH_HEAD).st_size != 0:
dianlujitao769a9c62024-01-14 17:59:24 +0800610 print("ERROR: git command failed")
Marko Manb58468a2018-03-19 13:01:19 +0100611 sys.exit(result)
612 # Check if it worked
613 if args.gerrit != default_gerrit or os.stat(FETCH_HEAD).st_size == 0:
614 # If not using the default gerrit or github failed, fetch from gerrit.
615 if args.verbose:
616 if args.gerrit == default_gerrit:
dianlujitao769a9c62024-01-14 17:59:24 +0800617 print(
618 "Fetching from GitHub didn't work, trying to fetch the change from Gerrit"
619 )
Marko Manb58468a2018-03-19 13:01:19 +0100620 else:
dianlujitao769a9c62024-01-14 17:59:24 +0800621 print("Fetching from {0}".format(args.gerrit))
Marko Manb58468a2018-03-19 13:01:19 +0100622
623 if args.pull:
dianlujitao769a9c62024-01-14 17:59:24 +0800624 cmd = [
625 "git pull --no-edit",
626 item["fetch"][method]["url"],
627 item["fetch"][method]["ref"],
628 ]
Marko Manb58468a2018-03-19 13:01:19 +0100629 else:
dianlujitao769a9c62024-01-14 17:59:24 +0800630 cmd = [
631 "git fetch",
632 item["fetch"][method]["url"],
633 item["fetch"][method]["ref"],
634 ]
Marko Manb58468a2018-03-19 13:01:19 +0100635 if args.quiet:
dianlujitao769a9c62024-01-14 17:59:24 +0800636 cmd.append("--quiet")
Marko Manb58468a2018-03-19 13:01:19 +0100637 else:
638 print(cmd)
dianlujitao769a9c62024-01-14 17:59:24 +0800639 result = subprocess.call([" ".join(cmd)], cwd=project_path, shell=True)
Marko Manb58468a2018-03-19 13:01:19 +0100640 if result != 0:
dianlujitao769a9c62024-01-14 17:59:24 +0800641 print("ERROR: git command failed")
Marko Manb58468a2018-03-19 13:01:19 +0100642 sys.exit(result)
643 # Perform the cherry-pick
644 if not args.pull:
dianlujitao769a9c62024-01-14 17:59:24 +0800645 cmd = ["git cherry-pick --ff FETCH_HEAD"]
Marko Manb58468a2018-03-19 13:01:19 +0100646 if args.quiet:
dianlujitao769a9c62024-01-14 17:59:24 +0800647 cmd_out = open(os.devnull, "wb")
Marko Manb58468a2018-03-19 13:01:19 +0100648 else:
649 cmd_out = None
dianlujitao769a9c62024-01-14 17:59:24 +0800650 result = subprocess.call(
651 cmd, cwd=project_path, shell=True, stdout=cmd_out, stderr=cmd_out
652 )
Marko Manb58468a2018-03-19 13:01:19 +0100653 if result != 0:
dianlujitao769a9c62024-01-14 17:59:24 +0800654 cmd = ["git diff-index --quiet HEAD --"]
655 result = subprocess.call(
656 cmd, cwd=project_path, shell=True, stdout=cmd_out, stderr=cmd_out
657 )
Adrian DC7bc808f2018-08-30 23:07:23 +0200658 if result == 0:
dianlujitao769a9c62024-01-14 17:59:24 +0800659 print(
660 "WARNING: git command resulted with an empty commit, aborting cherry-pick"
661 )
662 cmd = ["git cherry-pick --abort"]
663 subprocess.call(
664 cmd,
665 cwd=project_path,
666 shell=True,
667 stdout=cmd_out,
668 stderr=cmd_out,
669 )
Adrian DC7bc808f2018-08-30 23:07:23 +0200670 elif args.reset:
dianlujitao769a9c62024-01-14 17:59:24 +0800671 print("ERROR: git command failed, aborting cherry-pick")
672 cmd = ["git cherry-pick --abort"]
673 subprocess.call(
674 cmd,
675 cwd=project_path,
676 shell=True,
677 stdout=cmd_out,
678 stderr=cmd_out,
679 )
Adrian DC7bc808f2018-08-30 23:07:23 +0200680 sys.exit(result)
Marko Manb58468a2018-03-19 13:01:19 +0100681 else:
dianlujitao769a9c62024-01-14 17:59:24 +0800682 print("ERROR: git command failed")
Adrian DC7bc808f2018-08-30 23:07:23 +0200683 sys.exit(result)
Pulser72e23242013-09-29 09:56:55 +0100684 if not args.quiet:
dianlujitao769a9c62024-01-14 17:59:24 +0800685 print("")