blob: 341a95efe297631fb1223a8b27337a7ac6109f99 [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:]
dianlujitao20f0bdb2024-01-14 14:57:41 +080063 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",
dianlujitao20f0bdb2024-01-14 14:57:41 +080071 "-p",
72 port,
dianlujitao769a9c62024-01-14 17:59:24 +080073 userhost,
74 "gerrit",
75 "query",
dianlujitao20f0bdb2024-01-14 14:57:41 +080076 "--format",
77 "JSON",
78 "--patch-sets",
79 "--current-patch-set",
dianlujitao769a9c62024-01-14 17:59:24 +080080 query,
dianlujitao20f0bdb2024-01-14 14:57:41 +080081 ],
82 text=True,
dianlujitao769a9c62024-01-14 17:59:24 +080083 )
Marko Manb58468a2018-03-19 13:01:19 +010084 reviews = []
dianlujitao769a9c62024-01-14 17:59:24 +080085 for line in out.split("\n"):
Marko Manb58468a2018-03-19 13:01:19 +010086 try:
87 data = json.loads(line)
88 # make our data look like the http rest api data
89 review = {
dianlujitao769a9c62024-01-14 17:59:24 +080090 "branch": data["branch"],
91 "change_id": data["id"],
92 "current_revision": data["currentPatchSet"]["revision"],
93 "number": int(data["number"]),
94 "revisions": {
95 patch_set["revision"]: {
96 "_number": int(patch_set["number"]),
97 "fetch": {
98 "ssh": {
99 "ref": patch_set["ref"],
100 "url": "ssh://{0}:{1}/{2}".format(
101 userhost, port, data["project"]
102 ),
103 }
104 },
105 "commit": {
106 "parents": [
107 {"commit": parent} for parent in patch_set["parents"]
108 ]
109 },
110 }
111 for patch_set in data["patchSets"]
112 },
113 "subject": data["subject"],
114 "project": data["project"],
115 "status": data["status"],
Marko Manb58468a2018-03-19 13:01:19 +0100116 }
117 reviews.append(review)
118 except:
119 pass
dianlujitao769a9c62024-01-14 17:59:24 +0800120 args.quiet or print("Found {0} reviews".format(len(reviews)))
Marko Manb58468a2018-03-19 13:01:19 +0100121 return reviews
Pulser72e23242013-09-29 09:56:55 +0100122
Pulser72e23242013-09-29 09:56:55 +0100123
Marko Manb58468a2018-03-19 13:01:19 +0100124def fetch_query_via_http(remote_url, query):
125 if "requests" in sys.modules:
126 auth = None
127 if os.path.isfile(os.getenv("HOME") + "/.gerritrc"):
128 f = open(os.getenv("HOME") + "/.gerritrc", "r")
129 for line in f:
130 parts = line.rstrip().split("|")
131 if parts[0] in remote_url:
dianlujitao769a9c62024-01-14 17:59:24 +0800132 auth = requests.auth.HTTPBasicAuth(
133 username=parts[1], password=parts[2]
134 )
135 status_code = "-1"
Marko Manb58468a2018-03-19 13:01:19 +0100136 if auth:
dianlujitao769a9c62024-01-14 17:59:24 +0800137 url = "{0}/a/changes/?q={1}&o=CURRENT_REVISION&o=ALL_REVISIONS&o=ALL_COMMITS".format(
138 remote_url, query
139 )
Marko Manb58468a2018-03-19 13:01:19 +0100140 data = requests.get(url, auth=auth)
Aayush Gupta2f4522c2020-07-26 07:19:19 +0000141 status_code = str(data.status_code)
dianlujitao769a9c62024-01-14 17:59:24 +0800142 if status_code != "200":
143 # They didn't get good authorization or data, Let's try the old way
144 url = "{0}/changes/?q={1}&o=CURRENT_REVISION&o=ALL_REVISIONS&o=ALL_COMMITS".format(
145 remote_url, query
146 )
Marko Manb58468a2018-03-19 13:01:19 +0100147 data = requests.get(url)
148 reviews = json.loads(data.text[5:])
149 else:
150 """Given a query, fetch the change numbers via http"""
dianlujitao769a9c62024-01-14 17:59:24 +0800151 url = "{0}/changes/?q={1}&o=CURRENT_REVISION&o=ALL_REVISIONS&o=ALL_COMMITS".format(
152 remote_url, query
153 )
154 data = urllib.request.urlopen(url).read().decode("utf-8")
Marko Manb58468a2018-03-19 13:01:19 +0100155 reviews = json.loads(data[5:])
156
157 for review in reviews:
dianlujitao769a9c62024-01-14 17:59:24 +0800158 review["number"] = review.pop("_number")
Marko Manb58468a2018-03-19 13:01:19 +0100159
160 return reviews
161
162
163def fetch_query(remote_url, query):
164 """Wrapper for fetch_query_via_proto functions"""
dianlujitao769a9c62024-01-14 17:59:24 +0800165 if remote_url[0:3] == "ssh":
Marko Manb58468a2018-03-19 13:01:19 +0100166 return fetch_query_via_ssh(remote_url, query)
dianlujitao769a9c62024-01-14 17:59:24 +0800167 elif remote_url[0:4] == "http":
168 return fetch_query_via_http(remote_url, query.replace(" ", "+"))
Marko Manb58468a2018-03-19 13:01:19 +0100169 else:
dianlujitao769a9c62024-01-14 17:59:24 +0800170 raise Exception(
171 "Gerrit URL should be in the form http[s]://hostname/ or ssh://[user@]host[:port]"
172 )
Marko Manb58468a2018-03-19 13:01:19 +0100173
Aayush Gupta2f4522c2020-07-26 07:19:19 +0000174
dianlujitao769a9c62024-01-14 17:59:24 +0800175if __name__ == "__main__":
Marko Manb58468a2018-03-19 13:01:19 +0100176 # Default to OmniRom Gerrit
dianlujitao769a9c62024-01-14 17:59:24 +0800177 default_gerrit = "https://gerrit.omnirom.org"
Marko Manb58468a2018-03-19 13:01:19 +0100178
dianlujitao769a9c62024-01-14 17:59:24 +0800179 parser = argparse.ArgumentParser(
180 formatter_class=argparse.RawDescriptionHelpFormatter,
181 description=textwrap.dedent(
182 """\
Marko Manb58468a2018-03-19 13:01:19 +0100183 repopick.py is a utility to simplify the process of cherry picking
184 patches from OmniRom's Gerrit instance (or any gerrit instance of your choosing)
185
186 Given a list of change numbers, repopick will cd into the project path
187 and cherry pick the latest patch available.
188
189 With the --start-branch argument, the user can specify that a branch
190 should be created before cherry picking. This is useful for
191 cherry-picking many patches into a common branch which can be easily
192 abandoned later (good for testing other's changes.)
193
194 The --abandon-first argument, when used in conjunction with the
195 --start-branch option, will cause repopick to abandon the specified
dianlujitao769a9c62024-01-14 17:59:24 +0800196 branch in all repos first before performing any cherry picks."""
197 ),
198 )
199 parser.add_argument(
200 "change_number",
201 nargs="*",
202 help="change number to cherry pick. Use {change number}/{patchset number} to get a specific revision.",
203 )
204 parser.add_argument(
205 "-i",
206 "--ignore-missing",
207 action="store_true",
208 help="do not error out if a patch applies to a missing directory",
209 )
210 parser.add_argument(
211 "-s",
212 "--start-branch",
213 nargs=1,
214 metavar="",
215 help="start the specified branch before cherry picking",
216 )
217 parser.add_argument(
218 "-r",
219 "--reset",
220 action="store_true",
221 help="reset to initial state (abort cherry-pick) if there is a conflict",
222 )
223 parser.add_argument(
224 "-a",
225 "--abandon-first",
226 action="store_true",
227 help="before cherry picking, abandon the branch specified in --start-branch",
228 )
229 parser.add_argument(
230 "-b",
231 "--auto-branch",
232 action="store_true",
233 help='shortcut to "--start-branch auto --abandon-first --ignore-missing"',
234 )
235 parser.add_argument(
236 "-q", "--quiet", action="store_true", help="print as little as possible"
237 )
238 parser.add_argument(
239 "-v",
240 "--verbose",
241 action="store_true",
242 help="print extra information to aid in debug",
243 )
244 parser.add_argument(
245 "-f",
246 "--force",
247 action="store_true",
248 help="force cherry pick even if change is closed",
249 )
250 parser.add_argument(
251 "-p", "--pull", action="store_true", help="execute pull instead of cherry-pick"
252 )
253 parser.add_argument(
254 "-P", "--path", metavar="", help="use the specified path for the change"
255 )
256 parser.add_argument(
257 "-t", "--topic", metavar="", help="pick all commits from a specified topic"
258 )
259 parser.add_argument(
260 "-Q", "--query", metavar="", help="pick all commits using the specified query"
261 )
262 parser.add_argument(
263 "-g",
264 "--gerrit",
265 default=default_gerrit,
266 metavar="",
267 help="Gerrit Instance to use. Form proto://[user@]host[:port]",
268 )
269 parser.add_argument(
270 "-e",
271 "--exclude",
272 nargs=1,
273 metavar="",
274 help="exclude a list of commit numbers separated by a ,",
275 )
276 parser.add_argument(
277 "-c",
278 "--check-picked",
279 type=int,
280 default=10,
281 metavar="",
282 help="pass the amount of commits to check for already picked changes",
283 )
Marko Manb58468a2018-03-19 13:01:19 +0100284 args = parser.parse_args()
285 if not args.start_branch and args.abandon_first:
dianlujitao769a9c62024-01-14 17:59:24 +0800286 parser.error(
287 "if --abandon-first is set, you must also give the branch name with --start-branch"
288 )
Marko Manb58468a2018-03-19 13:01:19 +0100289 if args.auto_branch:
290 args.abandon_first = True
291 args.ignore_missing = True
292 if not args.start_branch:
dianlujitao769a9c62024-01-14 17:59:24 +0800293 args.start_branch = ["auto"]
Marko Manb58468a2018-03-19 13:01:19 +0100294 if args.quiet and args.verbose:
dianlujitao769a9c62024-01-14 17:59:24 +0800295 parser.error("--quiet and --verbose cannot be specified together")
Marko Manb58468a2018-03-19 13:01:19 +0100296
297 if (1 << bool(args.change_number) << bool(args.topic) << bool(args.query)) != 2:
dianlujitao769a9c62024-01-14 17:59:24 +0800298 parser.error(
299 "One (and only one) of change_number, topic, and query are allowed"
300 )
Marko Manb58468a2018-03-19 13:01:19 +0100301
302 # Change current directory to the top of the tree
dianlujitao769a9c62024-01-14 17:59:24 +0800303 if "ANDROID_BUILD_TOP" in os.environ:
304 top = os.environ["ANDROID_BUILD_TOP"]
Marko Manb58468a2018-03-19 13:01:19 +0100305
306 if not is_subdir(os.getcwd(), top):
dianlujitao769a9c62024-01-14 17:59:24 +0800307 sys.stderr.write(
308 "ERROR: You must run this tool from within $ANDROID_BUILD_TOP!\n"
309 )
Marko Manb58468a2018-03-19 13:01:19 +0100310 sys.exit(1)
dianlujitao769a9c62024-01-14 17:59:24 +0800311 os.chdir(os.environ["ANDROID_BUILD_TOP"])
Marko Manb58468a2018-03-19 13:01:19 +0100312
313 # Sanity check that we are being run from the top level of the tree
dianlujitao769a9c62024-01-14 17:59:24 +0800314 if not os.path.isdir(".repo"):
315 sys.stderr.write(
316 "ERROR: No .repo directory found. Please run this from the top of your tree.\n"
317 )
Pulser72e23242013-09-29 09:56:55 +0100318 sys.exit(1)
319
Marko Manb58468a2018-03-19 13:01:19 +0100320 # If --abandon-first is given, abandon the branch before starting
321 if args.abandon_first:
322 # Determine if the branch already exists; skip the abandon if it does not
dianlujitao20f0bdb2024-01-14 14:57:41 +0800323 plist = subprocess.check_output(["repo", "info"], text=True)
Marko Manb58468a2018-03-19 13:01:19 +0100324 needs_abandon = False
325 for pline in plist.splitlines():
dianlujitao769a9c62024-01-14 17:59:24 +0800326 matchObj = re.match(r"Local Branches.*\[(.*)\]", pline)
Marko Manb58468a2018-03-19 13:01:19 +0100327 if matchObj:
dianlujitao769a9c62024-01-14 17:59:24 +0800328 local_branches = re.split(r"\s*,\s*", matchObj.group(1))
Marko Manb58468a2018-03-19 13:01:19 +0100329 if any(args.start_branch[0] in s for s in local_branches):
330 needs_abandon = True
Pulser72e23242013-09-29 09:56:55 +0100331
Marko Manb58468a2018-03-19 13:01:19 +0100332 if needs_abandon:
333 # Perform the abandon only if the branch already exists
334 if not args.quiet:
dianlujitao769a9c62024-01-14 17:59:24 +0800335 print("Abandoning branch: %s" % args.start_branch[0])
dianlujitao20f0bdb2024-01-14 14:57:41 +0800336 subprocess.run(["repo", "abandon", args.start_branch[0]])
Marko Manb58468a2018-03-19 13:01:19 +0100337 if not args.quiet:
dianlujitao769a9c62024-01-14 17:59:24 +0800338 print("")
Pulser72e23242013-09-29 09:56:55 +0100339
Marko Manb58468a2018-03-19 13:01:19 +0100340 # Get the master manifest from repo
341 # - convert project name and revision to a path
342 project_name_to_data = {}
dianlujitao20f0bdb2024-01-14 14:57:41 +0800343 manifest = subprocess.check_output(["repo", "manifest"], text=True)
Marko Manb58468a2018-03-19 13:01:19 +0100344 xml_root = ElementTree.fromstring(manifest)
dianlujitao769a9c62024-01-14 17:59:24 +0800345 projects = xml_root.findall("project")
346 remotes = xml_root.findall("remote")
347 default_revision = xml_root.findall("default")[0].get("revision")
Marko Manb58468a2018-03-19 13:01:19 +0100348
Aayush Gupta2f4522c2020-07-26 07:19:19 +0000349 # dump project data into the a list of dicts with the following data:
350 # {project: {path, revision}}
Marko Manb58468a2018-03-19 13:01:19 +0100351
352 for project in projects:
dianlujitao769a9c62024-01-14 17:59:24 +0800353 name = project.get("name")
Aaron Kling83fee0f2020-06-19 18:43:16 -0500354 # 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 +0800355 path = project.get("path", name)
356 revision = project.get("upstream")
Marko Manb58468a2018-03-19 13:01:19 +0100357 if revision is None:
358 for remote in remotes:
dianlujitao769a9c62024-01-14 17:59:24 +0800359 if remote.get("name") == project.get("remote"):
360 revision = remote.get("revision")
Marko Manb58468a2018-03-19 13:01:19 +0100361 if revision is None:
dianlujitao769a9c62024-01-14 17:59:24 +0800362 revision = project.get("revision", default_revision)
Marko Manb58468a2018-03-19 13:01:19 +0100363
Aayush Gupta2f4522c2020-07-26 07:19:19 +0000364 if name not in project_name_to_data:
Marko Manb58468a2018-03-19 13:01:19 +0100365 project_name_to_data[name] = {}
dianlujitao769a9c62024-01-14 17:59:24 +0800366 revision = revision.split("refs/heads/")[-1]
Marko Manb58468a2018-03-19 13:01:19 +0100367 project_name_to_data[name][revision] = path
368
369 # get data on requested changes
370 reviews = []
371 change_numbers = []
Gabriele Md91609d2018-03-31 14:26:59 +0200372
373 def cmp_reviews(review_a, review_b):
dianlujitao769a9c62024-01-14 17:59:24 +0800374 current_a = review_a["current_revision"]
375 parents_a = [
376 r["commit"] for r in review_a["revisions"][current_a]["commit"]["parents"]
377 ]
378 current_b = review_b["current_revision"]
379 parents_b = [
380 r["commit"] for r in review_b["revisions"][current_b]["commit"]["parents"]
381 ]
Gabriele Md91609d2018-03-31 14:26:59 +0200382 if current_a in parents_b:
383 return -1
384 elif current_b in parents_a:
385 return 1
386 else:
dianlujitao769a9c62024-01-14 17:59:24 +0800387 return cmp(review_a["number"], review_b["number"])
Gabriele Md91609d2018-03-31 14:26:59 +0200388
Marko Manb58468a2018-03-19 13:01:19 +0100389 if args.topic:
dianlujitao769a9c62024-01-14 17:59:24 +0800390 reviews = fetch_query(args.gerrit, "topic:{0}".format(args.topic))
391 change_numbers = [
392 str(r["number"]) for r in sorted(reviews, key=cmp_to_key(cmp_reviews))
393 ]
Marko Manb58468a2018-03-19 13:01:19 +0100394 if args.query:
395 reviews = fetch_query(args.gerrit, args.query)
dianlujitao769a9c62024-01-14 17:59:24 +0800396 change_numbers = [
397 str(r["number"]) for r in sorted(reviews, key=cmp_to_key(cmp_reviews))
398 ]
Marko Manb58468a2018-03-19 13:01:19 +0100399 if args.change_number:
dianlujitao769a9c62024-01-14 17:59:24 +0800400 change_url_re = re.compile(r"https?://.+?/([0-9]+(?:/[0-9]+)?)/?")
Marko Manb58468a2018-03-19 13:01:19 +0100401 for c in args.change_number:
Gabriele M1009aeb2018-04-01 17:50:58 +0200402 change_number = change_url_re.findall(c)
403 if change_number:
404 change_numbers.extend(change_number)
dianlujitao769a9c62024-01-14 17:59:24 +0800405 elif "-" in c:
406 templist = c.split("-")
Marko Manb58468a2018-03-19 13:01:19 +0100407 for i in range(int(templist[0]), int(templist[1]) + 1):
408 change_numbers.append(str(i))
409 else:
410 change_numbers.append(c)
dianlujitao769a9c62024-01-14 17:59:24 +0800411 reviews = fetch_query(
412 args.gerrit,
413 " OR ".join("change:{0}".format(x.split("/")[0]) for x in change_numbers),
414 )
Marko Manb58468a2018-03-19 13:01:19 +0100415
416 # make list of things to actually merge
417 mergables = []
418
419 # If --exclude is given, create the list of commits to ignore
420 exclude = []
421 if args.exclude:
dianlujitao769a9c62024-01-14 17:59:24 +0800422 exclude = args.exclude[0].split(",")
Marko Manb58468a2018-03-19 13:01:19 +0100423
424 for change in change_numbers:
425 patchset = None
dianlujitao769a9c62024-01-14 17:59:24 +0800426 if "/" in change:
427 (change, patchset) = change.split("/")
Marko Manb58468a2018-03-19 13:01:19 +0100428
429 if change in exclude:
430 continue
431
432 change = int(change)
433
434 if patchset is not None:
435 patchset = int(patchset)
436
dianlujitao769a9c62024-01-14 17:59:24 +0800437 review = next((x for x in reviews if x["number"] == change), None)
Marko Manb58468a2018-03-19 13:01:19 +0100438 if review is None:
dianlujitao769a9c62024-01-14 17:59:24 +0800439 print("Change %d not found, skipping" % change)
Marko Manb58468a2018-03-19 13:01:19 +0100440 continue
441
dianlujitao769a9c62024-01-14 17:59:24 +0800442 mergables.append(
443 {
444 "subject": review["subject"],
445 "project": review["project"],
446 "branch": review["branch"],
447 "change_id": review["change_id"],
448 "change_number": review["number"],
449 "status": review["status"],
450 "fetch": None,
451 "patchset": review["revisions"][review["current_revision"]]["_number"],
452 }
453 )
Gabriele M1188cbd2018-04-01 17:50:57 +0200454
dianlujitao769a9c62024-01-14 17:59:24 +0800455 mergables[-1]["fetch"] = review["revisions"][review["current_revision"]][
456 "fetch"
457 ]
458 mergables[-1]["id"] = change
Marko Manb58468a2018-03-19 13:01:19 +0100459 if patchset:
460 try:
dianlujitao769a9c62024-01-14 17:59:24 +0800461 mergables[-1]["fetch"] = [
462 review["revisions"][x]["fetch"]
463 for x in review["revisions"]
464 if review["revisions"][x]["_number"] == patchset
465 ][0]
466 mergables[-1]["id"] = "{0}/{1}".format(change, patchset)
467 mergables[-1]["patchset"] = patchset
Marko Manb58468a2018-03-19 13:01:19 +0100468 except (IndexError, ValueError):
dianlujitao769a9c62024-01-14 17:59:24 +0800469 args.quiet or print(
470 "ERROR: The patch set {0}/{1} could not be found, using CURRENT_REVISION instead.".format(
471 change, patchset
472 )
473 )
Marko Manb58468a2018-03-19 13:01:19 +0100474
475 for item in mergables:
dianlujitao769a9c62024-01-14 17:59:24 +0800476 args.quiet or print("Applying change number {0}...".format(item["id"]))
Marko Manb58468a2018-03-19 13:01:19 +0100477 # Check if change is open and exit if it's not, unless -f is specified
dianlujitao769a9c62024-01-14 17:59:24 +0800478 if (
479 item["status"] != "OPEN"
480 and item["status"] != "NEW"
481 and item["status"] != "DRAFT"
482 ):
Marko Manb58468a2018-03-19 13:01:19 +0100483 if args.force:
dianlujitao769a9c62024-01-14 17:59:24 +0800484 print("!! Force-picking a closed change !!\n")
Marko Manb58468a2018-03-19 13:01:19 +0100485 else:
dianlujitao769a9c62024-01-14 17:59:24 +0800486 print(
487 "Change status is "
488 + item["status"]
489 + ". Skipping the cherry pick.\nUse -f to force this pick."
490 )
Marko Manb58468a2018-03-19 13:01:19 +0100491 continue
Pulser72e23242013-09-29 09:56:55 +0100492
493 # Convert the project name to a project path
494 # - check that the project path exists
Marko Manb58468a2018-03-19 13:01:19 +0100495 project_path = None
496
dianlujitao769a9c62024-01-14 17:59:24 +0800497 if (
498 item["project"] in project_name_to_data
499 and item["branch"] in project_name_to_data[item["project"]]
500 ):
501 project_path = project_name_to_data[item["project"]][item["branch"]]
Marko Manb58468a2018-03-19 13:01:19 +0100502 elif args.path:
503 project_path = args.path
dianlujitao769a9c62024-01-14 17:59:24 +0800504 elif (
505 item["project"] in project_name_to_data
506 and len(project_name_to_data[item["project"]]) == 1
507 ):
508 local_branch = list(project_name_to_data[item["project"]])[0]
509 project_path = project_name_to_data[item["project"]][local_branch]
510 print(
511 'WARNING: Project {0} has a different branch ("{1}" != "{2}")'.format(
512 project_path, local_branch, item["branch"]
513 )
514 )
Pulser72e23242013-09-29 09:56:55 +0100515 elif args.ignore_missing:
dianlujitao769a9c62024-01-14 17:59:24 +0800516 print(
517 "WARNING: Skipping {0} since there is no project directory for: {1}\n".format(
518 item["id"], item["project"]
519 )
520 )
Marko Manb58468a2018-03-19 13:01:19 +0100521 continue
Pulser72e23242013-09-29 09:56:55 +0100522 else:
dianlujitao769a9c62024-01-14 17:59:24 +0800523 sys.stderr.write(
524 "ERROR: For {0}, could not determine the project path for project {1}\n".format(
525 item["id"], item["project"]
526 )
527 )
Marko Manb58468a2018-03-19 13:01:19 +0100528 sys.exit(1)
Pulser72e23242013-09-29 09:56:55 +0100529
530 # If --start-branch is given, create the branch (more than once per path is okay; repo ignores gracefully)
531 if args.start_branch:
dianlujitao20f0bdb2024-01-14 14:57:41 +0800532 subprocess.run(["repo", "start", args.start_branch[0], project_path])
Marko Manb58468a2018-03-19 13:01:19 +0100533
534 # Determine the maximum commits to check already picked changes
535 check_picked_count = args.check_picked
dianlujitao769a9c62024-01-14 17:59:24 +0800536 branch_commits_count = int(
537 subprocess.check_output(
dianlujitao20f0bdb2024-01-14 14:57:41 +0800538 [
539 "git",
540 "rev-list",
541 "--count",
542 "--max-count",
543 str(check_picked_count + 1),
544 "HEAD",
545 ],
546 cwd=project_path,
547 text=True,
dianlujitao769a9c62024-01-14 17:59:24 +0800548 )
549 )
Marko Manb58468a2018-03-19 13:01:19 +0100550 if branch_commits_count <= check_picked_count:
551 check_picked_count = branch_commits_count - 1
552
553 # Check if change is already picked to HEAD...HEAD~check_picked_count
554 found_change = False
555 for i in range(0, check_picked_count):
dianlujitao769a9c62024-01-14 17:59:24 +0800556 if subprocess.call(
557 ["git", "cat-file", "-e", "HEAD~{0}".format(i)],
558 cwd=project_path,
559 stderr=open(os.devnull, "wb"),
560 ):
Marko Manb58468a2018-03-19 13:01:19 +0100561 continue
dianlujitao769a9c62024-01-14 17:59:24 +0800562 output = subprocess.check_output(
dianlujitao20f0bdb2024-01-14 14:57:41 +0800563 ["git", "show", "-q", f"HEAD~{i}"], cwd=project_path, text=True
dianlujitao769a9c62024-01-14 17:59:24 +0800564 )
Simon Shields6a726492019-11-18 23:56:08 +1100565 output = output.split()
dianlujitao769a9c62024-01-14 17:59:24 +0800566 if "Change-Id:" in output:
567 head_change_id = ""
Aayush Gupta2f4522c2020-07-26 07:19:19 +0000568 for j, t in enumerate(reversed(output)):
dianlujitao769a9c62024-01-14 17:59:24 +0800569 if t == "Change-Id:":
Marko Manb58468a2018-03-19 13:01:19 +0100570 head_change_id = output[len(output) - j]
571 break
dianlujitao769a9c62024-01-14 17:59:24 +0800572 if head_change_id.strip() == item["change_id"]:
573 print(
574 "Skipping {0} - already picked in {1} as HEAD~{2}".format(
575 item["id"], project_path, i
576 )
577 )
Marko Manb58468a2018-03-19 13:01:19 +0100578 found_change = True
579 break
580 if found_change:
581 continue
Pulser72e23242013-09-29 09:56:55 +0100582
583 # Print out some useful info
584 if not args.quiet:
dianlujitao769a9c62024-01-14 17:59:24 +0800585 print('--> Subject: "{0}"'.format(item["subject"]))
586 print("--> Project path: {0}".format(project_path))
587 print(
588 "--> Change number: {0} (Patch Set {1})".format(
589 item["id"], item["patchset"]
590 )
591 )
Pulser72e23242013-09-29 09:56:55 +0100592
dianlujitao769a9c62024-01-14 17:59:24 +0800593 if "anonymous http" in item["fetch"]:
594 method = "anonymous http"
Pulser72e23242013-09-29 09:56:55 +0100595 else:
dianlujitao769a9c62024-01-14 17:59:24 +0800596 method = "ssh"
Pulser72e23242013-09-29 09:56:55 +0100597
dianlujitao20f0bdb2024-01-14 14:57:41 +0800598 if args.pull:
599 cmd = ["git", "pull", "--no-edit"]
600 else:
601 cmd = ["git", "fetch"]
602 if args.quiet:
603 cmd.append("--quiet")
604 cmd.extend(["", item["fetch"][method]["ref"]])
605
Marko Manb58468a2018-03-19 13:01:19 +0100606 # Try fetching from GitHub first if using default gerrit
607 if args.gerrit == default_gerrit:
608 if args.verbose:
dianlujitao769a9c62024-01-14 17:59:24 +0800609 print("Trying to fetch the change from GitHub")
Marko Manb58468a2018-03-19 13:01:19 +0100610
dianlujitao20f0bdb2024-01-14 14:57:41 +0800611 cmd[-2] = "omnirom"
612 if not args.quiet:
Marko Manb58468a2018-03-19 13:01:19 +0100613 print(cmd)
dianlujitao20f0bdb2024-01-14 14:57:41 +0800614 result = subprocess.call(cmd, cwd=project_path)
dianlujitao769a9c62024-01-14 17:59:24 +0800615 FETCH_HEAD = "{0}/.git/FETCH_HEAD".format(project_path)
Marko Manb58468a2018-03-19 13:01:19 +0100616 if result != 0 and os.stat(FETCH_HEAD).st_size != 0:
dianlujitao769a9c62024-01-14 17:59:24 +0800617 print("ERROR: git command failed")
Marko Manb58468a2018-03-19 13:01:19 +0100618 sys.exit(result)
619 # Check if it worked
620 if args.gerrit != default_gerrit or os.stat(FETCH_HEAD).st_size == 0:
621 # If not using the default gerrit or github failed, fetch from gerrit.
622 if args.verbose:
623 if args.gerrit == default_gerrit:
dianlujitao769a9c62024-01-14 17:59:24 +0800624 print(
625 "Fetching from GitHub didn't work, trying to fetch the change from Gerrit"
626 )
Marko Manb58468a2018-03-19 13:01:19 +0100627 else:
dianlujitao769a9c62024-01-14 17:59:24 +0800628 print("Fetching from {0}".format(args.gerrit))
Marko Manb58468a2018-03-19 13:01:19 +0100629
dianlujitao20f0bdb2024-01-14 14:57:41 +0800630 cmd[-2] = item["fetch"][method]["url"]
631 if not args.quiet:
Marko Manb58468a2018-03-19 13:01:19 +0100632 print(cmd)
dianlujitao20f0bdb2024-01-14 14:57:41 +0800633 result = subprocess.call(cmd, cwd=project_path)
Marko Manb58468a2018-03-19 13:01:19 +0100634 if result != 0:
dianlujitao769a9c62024-01-14 17:59:24 +0800635 print("ERROR: git command failed")
Marko Manb58468a2018-03-19 13:01:19 +0100636 sys.exit(result)
637 # Perform the cherry-pick
638 if not args.pull:
Marko Manb58468a2018-03-19 13:01:19 +0100639 if args.quiet:
dianlujitao20f0bdb2024-01-14 14:57:41 +0800640 cmd_out = subprocess.DEVNULL
Marko Manb58468a2018-03-19 13:01:19 +0100641 else:
642 cmd_out = None
dianlujitao769a9c62024-01-14 17:59:24 +0800643 result = subprocess.call(
dianlujitao20f0bdb2024-01-14 14:57:41 +0800644 ["git", "cherry-pick", "--ff", item["revision"]],
645 cwd=project_path,
646 stdout=cmd_out,
647 stderr=cmd_out,
dianlujitao769a9c62024-01-14 17:59:24 +0800648 )
Marko Manb58468a2018-03-19 13:01:19 +0100649 if result != 0:
dianlujitao769a9c62024-01-14 17:59:24 +0800650 result = subprocess.call(
dianlujitao20f0bdb2024-01-14 14:57:41 +0800651 ["git", "diff-index", "--quiet", "HEAD", "--"],
652 cwd=project_path,
653 stdout=cmd_out,
654 stderr=cmd_out,
dianlujitao769a9c62024-01-14 17:59:24 +0800655 )
Adrian DC7bc808f2018-08-30 23:07:23 +0200656 if result == 0:
dianlujitao769a9c62024-01-14 17:59:24 +0800657 print(
658 "WARNING: git command resulted with an empty commit, aborting cherry-pick"
659 )
dianlujitao769a9c62024-01-14 17:59:24 +0800660 subprocess.call(
dianlujitao20f0bdb2024-01-14 14:57:41 +0800661 ["git", "cherry-pick", "--abort"],
dianlujitao769a9c62024-01-14 17:59:24 +0800662 cwd=project_path,
dianlujitao769a9c62024-01-14 17:59:24 +0800663 stdout=cmd_out,
664 stderr=cmd_out,
665 )
Adrian DC7bc808f2018-08-30 23:07:23 +0200666 elif args.reset:
dianlujitao769a9c62024-01-14 17:59:24 +0800667 print("ERROR: git command failed, aborting cherry-pick")
dianlujitao769a9c62024-01-14 17:59:24 +0800668 subprocess.call(
dianlujitao20f0bdb2024-01-14 14:57:41 +0800669 ["git", "cherry-pick", "--abort"],
dianlujitao769a9c62024-01-14 17:59:24 +0800670 cwd=project_path,
dianlujitao769a9c62024-01-14 17:59:24 +0800671 stdout=cmd_out,
672 stderr=cmd_out,
673 )
Adrian DC7bc808f2018-08-30 23:07:23 +0200674 sys.exit(result)
Marko Manb58468a2018-03-19 13:01:19 +0100675 else:
dianlujitao769a9c62024-01-14 17:59:24 +0800676 print("ERROR: git command failed")
Adrian DC7bc808f2018-08-30 23:07:23 +0200677 sys.exit(result)
Pulser72e23242013-09-29 09:56:55 +0100678 if not args.quiet:
dianlujitao769a9c62024-01-14 17:59:24 +0800679 print("")