blob: deb2112b608a6eb7bb7c9ae33696117001e50094 [file] [log] [blame]
Mike Frysingerf1ba7ad2022-09-12 05:42:57 -04001# Copyright 2012 The ChromiumOS Authors
Chris Sosaefc35722012-09-11 18:55:37 -07002# 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."""
Mike Frysingerc9e729b2023-02-02 09:04:04 -050055 parser = commandline.ArgumentParser(description=__doc__, dryrun=True)
Alex Klein1699fab2022-09-08 08:46:06 -060056 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(
Alex Klein1699fab2022-09-08 08:46:06 -060064 "-e",
65 "--email",
66 help="use this email instead of the email you would "
67 "upload changes as; required w/--nomirror",
68 )
69 parser.add_argument(
70 "--nomirror",
71 default=True,
72 dest="mirror",
73 action="store_false",
74 help="checkout git repo directly; requires --email",
75 )
76 parser.add_argument(
77 "--nowipe",
78 default=True,
79 dest="wipe",
80 action="store_false",
81 help="do not wipe the work directory after finishing",
82 )
83 parser.add_argument("change", nargs="+", help="CLs to merge")
84 parser.add_argument("branch", help="the branch to merge to")
85 return parser
Chris Sosaefc35722012-09-11 18:55:37 -070086
87
88def _UploadChangeToBranch(work_dir, patch, branch, draft, dryrun):
Alex Klein1699fab2022-09-08 08:46:06 -060089 """Creates a new change from GerritPatch |patch| to |branch| from |work_dir|.
Chris Sosaefc35722012-09-11 18:55:37 -070090
Alex Klein1699fab2022-09-08 08:46:06 -060091 Args:
92 patch: Instance of GerritPatch to upload.
93 branch: Branch to upload to.
94 work_dir: Local directory where repository is checked out in.
95 draft: If True, upload to refs/draft/|branch| rather than refs/for/|branch|.
96 dryrun: Don't actually upload a change but go through all the steps up to
97 and including git push --dry-run.
Mike Frysinger1a736a82013-12-12 01:50:59 -050098
Alex Klein1699fab2022-09-08 08:46:06 -060099 Returns:
100 A list of all the gerrit URLs found.
101 """
102 upload_type = "drafts" if draft else "for"
103 # Download & setup the patch if need be.
104 patch.Fetch(work_dir)
105 # Apply the actual change.
106 patch.CherryPick(work_dir, inflight=True, leave_dirty=True)
Chris Sosaefc35722012-09-11 18:55:37 -0700107
Alex Klein1699fab2022-09-08 08:46:06 -0600108 # Get the new sha1 after apply.
109 new_sha1 = git.GetGitRepoRevision(work_dir)
110 reviewers = set()
Chris Sosaefc35722012-09-11 18:55:37 -0700111
Alex Klein1699fab2022-09-08 08:46:06 -0600112 # Filter out tags that are added by gerrit and chromite.
113 filter_re = re.compile(
114 r"((Commit|Trybot)-Ready|Commit-Queue|(Reviewed|Submitted|Tested)-by): "
115 )
Mike Frysingera7a2f002015-04-29 23:23:19 -0400116
Alex Klein1699fab2022-09-08 08:46:06 -0600117 # Rewrite the commit message all the time. Latest gerrit doesn't seem
118 # to like it when you use the same ChangeId on different branches.
119 msg = []
120 for line in patch.commit_message.splitlines():
121 if line.startswith("Reviewed-on: "):
122 line = "Previous-" + line
123 elif filter_re.match(line):
124 # If the tag is malformed, or the person lacks a name,
125 # then that's just too bad -- throw it away.
126 ele = re.split(r"[<>@]+", line)
127 if len(ele) == 4:
128 reviewers.add("@".join(ele[-3:-1]))
129 continue
130 msg.append(line)
131 msg += ["(cherry picked from commit %s)" % patch.sha1]
132 git.RunGit(
133 work_dir,
134 ["commit", "--amend", "-F", "-"],
135 input="\n".join(msg).encode("utf8"),
136 )
Mike Frysinger187af412012-10-27 04:27:04 -0400137
Alex Klein1699fab2022-09-08 08:46:06 -0600138 # Get the new sha1 after rewriting the commit message.
139 new_sha1 = git.GetGitRepoRevision(work_dir)
Mike Frysinger187af412012-10-27 04:27:04 -0400140
Alex Klein1699fab2022-09-08 08:46:06 -0600141 # Create and use a LocalPatch to Upload the change to Gerrit.
142 local_patch = cros_patch.LocalPatch(
143 work_dir,
144 patch.project_url,
145 constants.PATCH_BRANCH,
146 patch.tracking_branch,
147 patch.remote,
148 new_sha1,
149 )
150 for reviewers in (reviewers, ()):
151 try:
152 return local_patch.Upload(
153 patch.project_url,
154 "refs/%s/%s" % (upload_type, branch),
155 carbon_copy=False,
156 dryrun=dryrun,
157 reviewers=reviewers,
158 )
159 except cros_build_lib.RunCommandError as e:
160 if e.returncode == 128 and re.search(
161 r'fatal: user ".*?" not found', e.stderr
162 ):
163 logging.warning(
164 "Some reviewers were not found (%s); "
165 "dropping them & retrying upload",
166 " ".join(reviewers),
167 )
168 continue
169 raise
Chris Sosaefc35722012-09-11 18:55:37 -0700170
171
172def _SetupWorkDirectoryForPatch(work_dir, patch, branch, manifest, email):
Alex Klein1699fab2022-09-08 08:46:06 -0600173 """Set up local dir for uploading changes to the given patch's project."""
174 logging.notice(
175 "Setting up dir %s for uploading changes to %s",
176 work_dir,
177 patch.project_url,
178 )
Chris Sosaefc35722012-09-11 18:55:37 -0700179
Alex Klein1699fab2022-09-08 08:46:06 -0600180 # Clone the git repo from reference if we have a pointer to a
181 # ManifestCheckout object.
182 reference = None
183 if manifest:
184 # Get the path to the first checkout associated with this change. Since
185 # all of the checkouts share git objects, it doesn't matter which checkout
186 # we pick.
187 path = manifest.FindCheckouts(patch.project)[0]["path"]
David Jamesafa4f5f2013-11-20 14:12:55 -0800188
Alex Klein1699fab2022-09-08 08:46:06 -0600189 reference = os.path.join(constants.SOURCE_ROOT, path)
190 if not os.path.isdir(reference):
191 logging.error("Unable to locate git checkout: %s", reference)
192 logging.error("Did you mean to use --nomirror?")
193 # This will do an "raise OSError" with the right values.
194 os.open(reference, os.O_DIRECTORY)
195 # Use the email if email wasn't specified.
196 if not email:
197 email = git.GetProjectUserEmail(reference)
Chris Sosaefc35722012-09-11 18:55:37 -0700198
Alex Klein1699fab2022-09-08 08:46:06 -0600199 git.Clone(work_dir, patch.project_url, reference=reference)
Chris Sosaefc35722012-09-11 18:55:37 -0700200
Alex Klein1699fab2022-09-08 08:46:06 -0600201 # Set the git committer.
202 git.RunGit(work_dir, ["config", "--replace-all", "user.email", email])
Chris Sosaefc35722012-09-11 18:55:37 -0700203
Alex Klein1699fab2022-09-08 08:46:06 -0600204 mbranch = git.MatchSingleBranchName(
205 work_dir, branch, namespace="refs/remotes/origin/"
206 )
207 if branch != mbranch:
208 logging.notice(
209 'Auto resolved branch name "%s" to "%s"', branch, mbranch
210 )
211 branch = mbranch
Mike Frysinger94f91be2012-10-19 16:09:06 -0400212
Alex Klein1699fab2022-09-08 08:46:06 -0600213 # Finally, create a local branch for uploading changes to the given remote
214 # branch.
215 git.CreatePushBranch(
216 constants.PATCH_BRANCH,
217 work_dir,
218 sync=False,
219 remote_push_branch=git.RemoteRef("ignore", "origin/%s" % branch),
220 )
Chris Sosaefc35722012-09-11 18:55:37 -0700221
Alex Klein1699fab2022-09-08 08:46:06 -0600222 return branch
Mike Frysinger94f91be2012-10-19 16:09:06 -0400223
Chris Sosaefc35722012-09-11 18:55:37 -0700224
225def _ManifestContainsAllPatches(manifest, patches):
Alex Klein1699fab2022-09-08 08:46:06 -0600226 """Returns true if the given manifest contains all the patches.
Chris Sosaefc35722012-09-11 18:55:37 -0700227
Alex Klein1699fab2022-09-08 08:46:06 -0600228 Args:
229 manifest: an instance of git.Manifest
230 patches: a collection of GerritPatch objects.
231 """
232 for patch in patches:
233 if not manifest.FindCheckouts(patch.project):
234 logging.error(
235 "Your manifest does not have the repository %s for "
236 "change %s. Please re-run with --nomirror and "
237 "--email set",
238 patch.project,
239 patch.gerrit_number,
240 )
241 return False
Chris Sosaefc35722012-09-11 18:55:37 -0700242
Alex Klein1699fab2022-09-08 08:46:06 -0600243 return True
Chris Sosaefc35722012-09-11 18:55:37 -0700244
245
246def main(argv):
Alex Klein1699fab2022-09-08 08:46:06 -0600247 parser = _GetParser()
248 options = parser.parse_args(argv)
249 changes = options.change
250 branch = options.branch
Chris Sosaefc35722012-09-11 18:55:37 -0700251
Mike Frysinger6fc41762013-02-27 16:36:54 -0500252 try:
Alex Klein1699fab2022-09-08 08:46:06 -0600253 patches = gerrit.GetGerritPatchInfo(changes)
254 except ValueError as e:
255 logging.error("Invalid patch: %s", e)
256 cros_build_lib.Die("Did you swap the branch/gerrit number?")
Chris Sosaefc35722012-09-11 18:55:37 -0700257
Alex Klein1699fab2022-09-08 08:46:06 -0600258 # Suppress all logging info output unless we're running debug.
259 if not options.debug:
260 logging.getLogger().setLevel(logging.NOTICE)
Chris Sosaefc35722012-09-11 18:55:37 -0700261
Alex Klein1699fab2022-09-08 08:46:06 -0600262 # Get a pointer to your repo checkout to look up the local project paths for
263 # both email addresses and for using your checkout as a git mirror.
264 manifest = None
265 if options.mirror:
266 try:
267 manifest = git.ManifestCheckout.Cached(constants.SOURCE_ROOT)
268 except OSError as e:
269 if e.errno == errno.ENOENT:
270 logging.error(
271 "Unable to locate ChromiumOS checkout: %s",
272 constants.SOURCE_ROOT,
273 )
274 logging.error("Did you mean to use --nomirror?")
275 return 1
276 raise
277 if not _ManifestContainsAllPatches(manifest, patches):
278 return 1
Chris Sosaefc35722012-09-11 18:55:37 -0700279 else:
Alex Klein1699fab2022-09-08 08:46:06 -0600280 if not options.email:
281 chromium_email = "%s@chromium.org" % os.environ["USER"]
282 logging.notice(
283 "--nomirror set without email, using %s", chromium_email
284 )
285 options.email = chromium_email
Chris Sosaefc35722012-09-11 18:55:37 -0700286
Alex Klein1699fab2022-09-08 08:46:06 -0600287 index = 0
288 work_dir = None
289 root_work_dir = tempfile.mkdtemp(prefix="cros_merge_to_branch")
290 try:
291 for index, (change, patch) in enumerate(zip(changes, patches)):
292 # We only clone the project and set the committer the first time.
293 work_dir = os.path.join(root_work_dir, patch.project)
294 if not os.path.isdir(work_dir):
295 branch = _SetupWorkDirectoryForPatch(
296 work_dir, patch, branch, manifest, options.email
297 )
298
299 # Now that we have the project checked out, let's apply our change and
300 # create a new change on Gerrit.
301 logging.notice("Uploading change %s to branch %s", change, branch)
302 urls = _UploadChangeToBranch(
303 work_dir, patch, branch, options.draft, options.dryrun
304 )
305 logging.notice("Successfully uploaded %s to %s", change, branch)
306 for url in urls:
307 if url.endswith("\x1b[K"):
308 # Git will often times emit these escape sequences.
309 url = url[0:-3]
310 logging.notice(" URL: %s", url)
311
312 except (
313 cros_build_lib.RunCommandError,
314 cros_patch.ApplyPatchException,
315 git.AmbiguousBranchName,
316 OSError,
317 ) as e:
318 # Tell the user how far we got.
319 good_changes = changes[:index]
320 bad_changes = changes[index:]
321
322 logging.warning(
323 "############## SOME CHANGES FAILED TO UPLOAD ############"
324 )
325
326 if good_changes:
327 logging.notice(
328 "Successfully uploaded change(s) %s", " ".join(good_changes)
329 )
330
331 # Printing out the error here so that we can see exactly what failed. This
332 # is especially useful to debug without using --debug.
333 logging.error("Upload failed with %s", str(e).strip())
334 if not options.wipe:
335 logging.error(
336 "Not wiping the directory. You can inspect the failed "
337 "change at %s; After fixing the change (if trivial) you can"
338 " try to upload the change by running:\n"
339 "git commit -a -c CHERRY_PICK_HEAD\n"
340 "git push %s HEAD:refs/for/%s",
341 work_dir,
342 patch.project_url,
343 branch,
344 )
345 else:
346 logging.error(
347 "--nowipe not set thus deleting the work directory. If you "
348 "wish to debug this, re-run the script with change(s) "
349 "%s and --nowipe by running:\n %s %s %s --nowipe",
350 " ".join(bad_changes),
351 sys.argv[0],
352 " ".join(bad_changes),
353 branch,
354 )
355
356 # Suppress the stack trace if we're not debugging.
357 if options.debug:
358 raise
359 else:
360 return 1
361
362 finally:
363 if options.wipe:
364 shutil.rmtree(root_work_dir)
365
366 if options.dryrun:
367 logging.notice(
368 "Success! To actually upload changes, re-run without " "--dry-run."
369 )
Chris Sosaefc35722012-09-11 18:55:37 -0700370 else:
Alex Klein1699fab2022-09-08 08:46:06 -0600371 logging.notice("Successfully uploaded all changes requested.")
Chris Sosaefc35722012-09-11 18:55:37 -0700372
Alex Klein1699fab2022-09-08 08:46:06 -0600373 return 0