blob: 309fc90f6ae2882ae1187c4cf3aaac8e09f470d6 [file] [log] [blame]
Chris Sosaefc35722012-09-11 18:55:37 -07001# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
Mike Frysinger61ef29a2014-12-17 02:05:27 -05005"""Developer helper tool for merging CLs from ToT to branches.
6
7This simple program takes changes from gerrit/gerrit-int and creates new
Chris Sosaefc35722012-09-11 18:55:37 -07008changes for them on the desired branch using your gerrit/ssh credentials. To
9specify a change on gerrit-int, you must prefix the change with a *.
10
11Note that this script is best used from within an existing checkout of
12Chromium OS that already has the changes you want merged to the branch in it
13i.e. if you want to push changes to crosutils.git, you must have src/scripts
14checked out. If this isn't true e.g. you are running this script from a
15minilayout or trying to upload an internal change from a non internal checkout,
16you must specify some extra options: use the --nomirror option and use -e to
17specify your email address. This tool will then checkout the git repo fresh
18using the credentials for the -e/email you specified and upload the change. Note
19you can always use this method but it's slower than the "mirrored" method and
20requires more typing :(.
21
22Examples:
23 cros_merge_to_branch 32027 32030 32031 release-R22.2723.B
24
25This will create changes for 32027, 32030 and 32031 on the R22 branch. To look
26up the name of a branch, go into a git sub-dir and type 'git branch -a' and the
27find the branch you want to merge to. If you want to upload internal changes
28from gerrit-int, you must prefix the gerrit change number with a * e.g.
29
30 cros_merge_to_branch *26108 release-R22.2723.B
31
32For more information on how to do this yourself you can go here:
Mike Frysingerdcad4e02018-08-03 16:20:02 -040033https://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/working-on-a-br\
Mike Frysinger02e1e072013-11-10 22:11:34 -050034anch
35"""
Chris Sosaefc35722012-09-11 18:55:37 -070036
Mike Frysinger6fc41762013-02-27 16:36:54 -050037import errno
Chris McDonald59650c32021-07-20 15:29:28 -060038import logging
Chris Sosaefc35722012-09-11 18:55:37 -070039import os
Mike Frysinger187af412012-10-27 04:27:04 -040040import re
Chris Sosaefc35722012-09-11 18:55:37 -070041import shutil
42import sys
43import tempfile
44
Chris Sosaefc35722012-09-11 18:55:37 -070045from chromite.lib import commandline
Chris McDonald59650c32021-07-20 15:29:28 -060046from chromite.lib import constants
Chris Sosaefc35722012-09-11 18:55:37 -070047from chromite.lib import cros_build_lib
Brian Harring511055e2012-10-10 02:58:59 -070048from chromite.lib import gerrit
David James97d95872012-11-16 15:09:56 -080049from chromite.lib import git
Brian Harring511055e2012-10-10 02:58:59 -070050from chromite.lib import patch as cros_patch
Chris Sosaefc35722012-09-11 18:55:37 -070051
52
Chris Sosaefc35722012-09-11 18:55:37 -070053def _GetParser():
Alex Klein1699fab2022-09-08 08:46:06 -060054 """Returns the parser to use for this module."""
55 parser = commandline.ArgumentParser(description=__doc__)
56 parser.add_argument(
57 "-d",
58 "--draft",
59 default=False,
60 action="store_true",
61 help="upload a draft to Gerrit rather than a change",
62 )
63 parser.add_argument(
64 "-n",
65 "--dry-run",
66 default=False,
67 action="store_true",
68 dest="dryrun",
69 help="apply changes locally but do not upload them",
70 )
71 parser.add_argument(
72 "-e",
73 "--email",
74 help="use this email instead of the email you would "
75 "upload changes as; required w/--nomirror",
76 )
77 parser.add_argument(
78 "--nomirror",
79 default=True,
80 dest="mirror",
81 action="store_false",
82 help="checkout git repo directly; requires --email",
83 )
84 parser.add_argument(
85 "--nowipe",
86 default=True,
87 dest="wipe",
88 action="store_false",
89 help="do not wipe the work directory after finishing",
90 )
91 parser.add_argument("change", nargs="+", help="CLs to merge")
92 parser.add_argument("branch", help="the branch to merge to")
93 return parser
Chris Sosaefc35722012-09-11 18:55:37 -070094
95
96def _UploadChangeToBranch(work_dir, patch, branch, draft, dryrun):
Alex Klein1699fab2022-09-08 08:46:06 -060097 """Creates a new change from GerritPatch |patch| to |branch| from |work_dir|.
Chris Sosaefc35722012-09-11 18:55:37 -070098
Alex Klein1699fab2022-09-08 08:46:06 -060099 Args:
100 patch: Instance of GerritPatch to upload.
101 branch: Branch to upload to.
102 work_dir: Local directory where repository is checked out in.
103 draft: If True, upload to refs/draft/|branch| rather than refs/for/|branch|.
104 dryrun: Don't actually upload a change but go through all the steps up to
105 and including git push --dry-run.
Mike Frysinger1a736a82013-12-12 01:50:59 -0500106
Alex Klein1699fab2022-09-08 08:46:06 -0600107 Returns:
108 A list of all the gerrit URLs found.
109 """
110 upload_type = "drafts" if draft else "for"
111 # Download & setup the patch if need be.
112 patch.Fetch(work_dir)
113 # Apply the actual change.
114 patch.CherryPick(work_dir, inflight=True, leave_dirty=True)
Chris Sosaefc35722012-09-11 18:55:37 -0700115
Alex Klein1699fab2022-09-08 08:46:06 -0600116 # Get the new sha1 after apply.
117 new_sha1 = git.GetGitRepoRevision(work_dir)
118 reviewers = set()
Chris Sosaefc35722012-09-11 18:55:37 -0700119
Alex Klein1699fab2022-09-08 08:46:06 -0600120 # Filter out tags that are added by gerrit and chromite.
121 filter_re = re.compile(
122 r"((Commit|Trybot)-Ready|Commit-Queue|(Reviewed|Submitted|Tested)-by): "
123 )
Mike Frysingera7a2f002015-04-29 23:23:19 -0400124
Alex Klein1699fab2022-09-08 08:46:06 -0600125 # Rewrite the commit message all the time. Latest gerrit doesn't seem
126 # to like it when you use the same ChangeId on different branches.
127 msg = []
128 for line in patch.commit_message.splitlines():
129 if line.startswith("Reviewed-on: "):
130 line = "Previous-" + line
131 elif filter_re.match(line):
132 # If the tag is malformed, or the person lacks a name,
133 # then that's just too bad -- throw it away.
134 ele = re.split(r"[<>@]+", line)
135 if len(ele) == 4:
136 reviewers.add("@".join(ele[-3:-1]))
137 continue
138 msg.append(line)
139 msg += ["(cherry picked from commit %s)" % patch.sha1]
140 git.RunGit(
141 work_dir,
142 ["commit", "--amend", "-F", "-"],
143 input="\n".join(msg).encode("utf8"),
144 )
Mike Frysinger187af412012-10-27 04:27:04 -0400145
Alex Klein1699fab2022-09-08 08:46:06 -0600146 # Get the new sha1 after rewriting the commit message.
147 new_sha1 = git.GetGitRepoRevision(work_dir)
Mike Frysinger187af412012-10-27 04:27:04 -0400148
Alex Klein1699fab2022-09-08 08:46:06 -0600149 # Create and use a LocalPatch to Upload the change to Gerrit.
150 local_patch = cros_patch.LocalPatch(
151 work_dir,
152 patch.project_url,
153 constants.PATCH_BRANCH,
154 patch.tracking_branch,
155 patch.remote,
156 new_sha1,
157 )
158 for reviewers in (reviewers, ()):
159 try:
160 return local_patch.Upload(
161 patch.project_url,
162 "refs/%s/%s" % (upload_type, branch),
163 carbon_copy=False,
164 dryrun=dryrun,
165 reviewers=reviewers,
166 )
167 except cros_build_lib.RunCommandError as e:
168 if e.returncode == 128 and re.search(
169 r'fatal: user ".*?" not found', e.stderr
170 ):
171 logging.warning(
172 "Some reviewers were not found (%s); "
173 "dropping them & retrying upload",
174 " ".join(reviewers),
175 )
176 continue
177 raise
Chris Sosaefc35722012-09-11 18:55:37 -0700178
179
180def _SetupWorkDirectoryForPatch(work_dir, patch, branch, manifest, email):
Alex Klein1699fab2022-09-08 08:46:06 -0600181 """Set up local dir for uploading changes to the given patch's project."""
182 logging.notice(
183 "Setting up dir %s for uploading changes to %s",
184 work_dir,
185 patch.project_url,
186 )
Chris Sosaefc35722012-09-11 18:55:37 -0700187
Alex Klein1699fab2022-09-08 08:46:06 -0600188 # Clone the git repo from reference if we have a pointer to a
189 # ManifestCheckout object.
190 reference = None
191 if manifest:
192 # Get the path to the first checkout associated with this change. Since
193 # all of the checkouts share git objects, it doesn't matter which checkout
194 # we pick.
195 path = manifest.FindCheckouts(patch.project)[0]["path"]
David Jamesafa4f5f2013-11-20 14:12:55 -0800196
Alex Klein1699fab2022-09-08 08:46:06 -0600197 reference = os.path.join(constants.SOURCE_ROOT, path)
198 if not os.path.isdir(reference):
199 logging.error("Unable to locate git checkout: %s", reference)
200 logging.error("Did you mean to use --nomirror?")
201 # This will do an "raise OSError" with the right values.
202 os.open(reference, os.O_DIRECTORY)
203 # Use the email if email wasn't specified.
204 if not email:
205 email = git.GetProjectUserEmail(reference)
Chris Sosaefc35722012-09-11 18:55:37 -0700206
Alex Klein1699fab2022-09-08 08:46:06 -0600207 git.Clone(work_dir, patch.project_url, reference=reference)
Chris Sosaefc35722012-09-11 18:55:37 -0700208
Alex Klein1699fab2022-09-08 08:46:06 -0600209 # Set the git committer.
210 git.RunGit(work_dir, ["config", "--replace-all", "user.email", email])
Chris Sosaefc35722012-09-11 18:55:37 -0700211
Alex Klein1699fab2022-09-08 08:46:06 -0600212 mbranch = git.MatchSingleBranchName(
213 work_dir, branch, namespace="refs/remotes/origin/"
214 )
215 if branch != mbranch:
216 logging.notice(
217 'Auto resolved branch name "%s" to "%s"', branch, mbranch
218 )
219 branch = mbranch
Mike Frysinger94f91be2012-10-19 16:09:06 -0400220
Alex Klein1699fab2022-09-08 08:46:06 -0600221 # Finally, create a local branch for uploading changes to the given remote
222 # branch.
223 git.CreatePushBranch(
224 constants.PATCH_BRANCH,
225 work_dir,
226 sync=False,
227 remote_push_branch=git.RemoteRef("ignore", "origin/%s" % branch),
228 )
Chris Sosaefc35722012-09-11 18:55:37 -0700229
Alex Klein1699fab2022-09-08 08:46:06 -0600230 return branch
Mike Frysinger94f91be2012-10-19 16:09:06 -0400231
Chris Sosaefc35722012-09-11 18:55:37 -0700232
233def _ManifestContainsAllPatches(manifest, patches):
Alex Klein1699fab2022-09-08 08:46:06 -0600234 """Returns true if the given manifest contains all the patches.
Chris Sosaefc35722012-09-11 18:55:37 -0700235
Alex Klein1699fab2022-09-08 08:46:06 -0600236 Args:
237 manifest: an instance of git.Manifest
238 patches: a collection of GerritPatch objects.
239 """
240 for patch in patches:
241 if not manifest.FindCheckouts(patch.project):
242 logging.error(
243 "Your manifest does not have the repository %s for "
244 "change %s. Please re-run with --nomirror and "
245 "--email set",
246 patch.project,
247 patch.gerrit_number,
248 )
249 return False
Chris Sosaefc35722012-09-11 18:55:37 -0700250
Alex Klein1699fab2022-09-08 08:46:06 -0600251 return True
Chris Sosaefc35722012-09-11 18:55:37 -0700252
253
254def main(argv):
Alex Klein1699fab2022-09-08 08:46:06 -0600255 parser = _GetParser()
256 options = parser.parse_args(argv)
257 changes = options.change
258 branch = options.branch
Chris Sosaefc35722012-09-11 18:55:37 -0700259
Mike Frysinger6fc41762013-02-27 16:36:54 -0500260 try:
Alex Klein1699fab2022-09-08 08:46:06 -0600261 patches = gerrit.GetGerritPatchInfo(changes)
262 except ValueError as e:
263 logging.error("Invalid patch: %s", e)
264 cros_build_lib.Die("Did you swap the branch/gerrit number?")
Chris Sosaefc35722012-09-11 18:55:37 -0700265
Alex Klein1699fab2022-09-08 08:46:06 -0600266 # Suppress all logging info output unless we're running debug.
267 if not options.debug:
268 logging.getLogger().setLevel(logging.NOTICE)
Chris Sosaefc35722012-09-11 18:55:37 -0700269
Alex Klein1699fab2022-09-08 08:46:06 -0600270 # Get a pointer to your repo checkout to look up the local project paths for
271 # both email addresses and for using your checkout as a git mirror.
272 manifest = None
273 if options.mirror:
274 try:
275 manifest = git.ManifestCheckout.Cached(constants.SOURCE_ROOT)
276 except OSError as e:
277 if e.errno == errno.ENOENT:
278 logging.error(
279 "Unable to locate ChromiumOS checkout: %s",
280 constants.SOURCE_ROOT,
281 )
282 logging.error("Did you mean to use --nomirror?")
283 return 1
284 raise
285 if not _ManifestContainsAllPatches(manifest, patches):
286 return 1
Chris Sosaefc35722012-09-11 18:55:37 -0700287 else:
Alex Klein1699fab2022-09-08 08:46:06 -0600288 if not options.email:
289 chromium_email = "%s@chromium.org" % os.environ["USER"]
290 logging.notice(
291 "--nomirror set without email, using %s", chromium_email
292 )
293 options.email = chromium_email
Chris Sosaefc35722012-09-11 18:55:37 -0700294
Alex Klein1699fab2022-09-08 08:46:06 -0600295 index = 0
296 work_dir = None
297 root_work_dir = tempfile.mkdtemp(prefix="cros_merge_to_branch")
298 try:
299 for index, (change, patch) in enumerate(zip(changes, patches)):
300 # We only clone the project and set the committer the first time.
301 work_dir = os.path.join(root_work_dir, patch.project)
302 if not os.path.isdir(work_dir):
303 branch = _SetupWorkDirectoryForPatch(
304 work_dir, patch, branch, manifest, options.email
305 )
306
307 # Now that we have the project checked out, let's apply our change and
308 # create a new change on Gerrit.
309 logging.notice("Uploading change %s to branch %s", change, branch)
310 urls = _UploadChangeToBranch(
311 work_dir, patch, branch, options.draft, options.dryrun
312 )
313 logging.notice("Successfully uploaded %s to %s", change, branch)
314 for url in urls:
315 if url.endswith("\x1b[K"):
316 # Git will often times emit these escape sequences.
317 url = url[0:-3]
318 logging.notice(" URL: %s", url)
319
320 except (
321 cros_build_lib.RunCommandError,
322 cros_patch.ApplyPatchException,
323 git.AmbiguousBranchName,
324 OSError,
325 ) as e:
326 # Tell the user how far we got.
327 good_changes = changes[:index]
328 bad_changes = changes[index:]
329
330 logging.warning(
331 "############## SOME CHANGES FAILED TO UPLOAD ############"
332 )
333
334 if good_changes:
335 logging.notice(
336 "Successfully uploaded change(s) %s", " ".join(good_changes)
337 )
338
339 # Printing out the error here so that we can see exactly what failed. This
340 # is especially useful to debug without using --debug.
341 logging.error("Upload failed with %s", str(e).strip())
342 if not options.wipe:
343 logging.error(
344 "Not wiping the directory. You can inspect the failed "
345 "change at %s; After fixing the change (if trivial) you can"
346 " try to upload the change by running:\n"
347 "git commit -a -c CHERRY_PICK_HEAD\n"
348 "git push %s HEAD:refs/for/%s",
349 work_dir,
350 patch.project_url,
351 branch,
352 )
353 else:
354 logging.error(
355 "--nowipe not set thus deleting the work directory. If you "
356 "wish to debug this, re-run the script with change(s) "
357 "%s and --nowipe by running:\n %s %s %s --nowipe",
358 " ".join(bad_changes),
359 sys.argv[0],
360 " ".join(bad_changes),
361 branch,
362 )
363
364 # Suppress the stack trace if we're not debugging.
365 if options.debug:
366 raise
367 else:
368 return 1
369
370 finally:
371 if options.wipe:
372 shutil.rmtree(root_work_dir)
373
374 if options.dryrun:
375 logging.notice(
376 "Success! To actually upload changes, re-run without " "--dry-run."
377 )
Chris Sosaefc35722012-09-11 18:55:37 -0700378 else:
Alex Klein1699fab2022-09-08 08:46:06 -0600379 logging.notice("Successfully uploaded all changes requested.")
Chris Sosaefc35722012-09-11 18:55:37 -0700380
Alex Klein1699fab2022-09-08 08:46:06 -0600381 return 0