blob: 459bfd5195bf5eb9015e0a85ad183b314a1f8e05 [file] [log] [blame]
Mike Frysingere58c0e22017-10-04 15:43:30 -04001# -*- coding: utf-8 -*-
Chris Sosaefc35722012-09-11 18:55:37 -07002# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
Mike Frysinger61ef29a2014-12-17 02:05:27 -05006"""Developer helper tool for merging CLs from ToT to branches.
7
8This simple program takes changes from gerrit/gerrit-int and creates new
Chris Sosaefc35722012-09-11 18:55:37 -07009changes for them on the desired branch using your gerrit/ssh credentials. To
10specify a change on gerrit-int, you must prefix the change with a *.
11
12Note that this script is best used from within an existing checkout of
13Chromium OS that already has the changes you want merged to the branch in it
14i.e. if you want to push changes to crosutils.git, you must have src/scripts
15checked out. If this isn't true e.g. you are running this script from a
16minilayout or trying to upload an internal change from a non internal checkout,
17you must specify some extra options: use the --nomirror option and use -e to
18specify your email address. This tool will then checkout the git repo fresh
19using the credentials for the -e/email you specified and upload the change. Note
20you can always use this method but it's slower than the "mirrored" method and
21requires more typing :(.
22
23Examples:
24 cros_merge_to_branch 32027 32030 32031 release-R22.2723.B
25
26This will create changes for 32027, 32030 and 32031 on the R22 branch. To look
27up the name of a branch, go into a git sub-dir and type 'git branch -a' and the
28find the branch you want to merge to. If you want to upload internal changes
29from gerrit-int, you must prefix the gerrit change number with a * e.g.
30
31 cros_merge_to_branch *26108 release-R22.2723.B
32
33For more information on how to do this yourself you can go here:
34http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/working-on-a-br\
Mike Frysinger02e1e072013-11-10 22:11:34 -050035anch
36"""
Chris Sosaefc35722012-09-11 18:55:37 -070037
Mike Frysinger383367e2014-09-16 15:06:17 -040038from __future__ import print_function
39
Mike Frysinger6fc41762013-02-27 16:36:54 -050040import errno
Chris Sosaefc35722012-09-11 18:55:37 -070041import os
Mike Frysinger187af412012-10-27 04:27:04 -040042import re
Chris Sosaefc35722012-09-11 18:55:37 -070043import shutil
44import sys
45import tempfile
46
Aviv Keshetb7519e12016-10-04 00:50:00 -070047from chromite.lib import constants
Don Garrett88b8d782014-05-13 17:30:55 -070048from chromite.cbuildbot import repository
Chris Sosaefc35722012-09-11 18:55:37 -070049from chromite.lib import commandline
50from chromite.lib import cros_build_lib
Ralph Nathan91874ca2015-03-19 13:29:41 -070051from chromite.lib import cros_logging as logging
Brian Harring511055e2012-10-10 02:58:59 -070052from chromite.lib import gerrit
David James97d95872012-11-16 15:09:56 -080053from chromite.lib import git
Brian Harring511055e2012-10-10 02:58:59 -070054from chromite.lib import patch as cros_patch
Chris Sosaefc35722012-09-11 18:55:37 -070055
56
Chris Sosaefc35722012-09-11 18:55:37 -070057def _GetParser():
58 """Returns the parser to use for this module."""
Mike Frysingerb17d7532015-06-04 01:57:29 -040059 parser = commandline.ArgumentParser(description=__doc__)
60 parser.add_argument('-d', '--draft', default=False, action='store_true',
61 help='upload a draft to Gerrit rather than a change')
62 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
63 dest='dryrun',
64 help='apply changes locally but do not upload them')
65 parser.add_argument('-e', '--email',
66 help='use this email instead of the email you would '
67 'upload changes as; required w/--nomirror')
68 parser.add_argument('--nomirror', default=True, dest='mirror',
69 action='store_false',
70 help='checkout git repo directly; requires --email')
71 parser.add_argument('--nowipe', default=True, dest='wipe',
72 action='store_false',
73 help='do not wipe the work directory after finishing')
74 parser.add_argument('change', nargs='+', help='CLs to merge')
75 parser.add_argument('branch', help='the branch to merge to')
Chris Sosaefc35722012-09-11 18:55:37 -070076 return parser
77
78
79def _UploadChangeToBranch(work_dir, patch, branch, draft, dryrun):
80 """Creates a new change from GerritPatch |patch| to |branch| from |work_dir|.
81
82 Args:
83 patch: Instance of GerritPatch to upload.
84 branch: Branch to upload to.
85 work_dir: Local directory where repository is checked out in.
86 draft: If True, upload to refs/draft/|branch| rather than refs/for/|branch|.
87 dryrun: Don't actually upload a change but go through all the steps up to
Mike Frysingerb6c6fab2012-12-06 00:00:24 -050088 and including git push --dry-run.
Mike Frysinger1a736a82013-12-12 01:50:59 -050089
Mike Frysinger6d9d21b2013-02-20 17:25:11 -050090 Returns:
91 A list of all the gerrit URLs found.
Chris Sosaefc35722012-09-11 18:55:37 -070092 """
93 upload_type = 'drafts' if draft else 'for'
Mike Frysinger187af412012-10-27 04:27:04 -040094 # Download & setup the patch if need be.
95 patch.Fetch(work_dir)
Chris Sosaefc35722012-09-11 18:55:37 -070096 # Apply the actual change.
97 patch.CherryPick(work_dir, inflight=True, leave_dirty=True)
98
99 # Get the new sha1 after apply.
David James97d95872012-11-16 15:09:56 -0800100 new_sha1 = git.GetGitRepoRevision(work_dir)
Chris Sosae02aa892013-01-14 10:59:08 -0800101 reviewers = set()
Chris Sosaefc35722012-09-11 18:55:37 -0700102
Mike Frysingera7a2f002015-04-29 23:23:19 -0400103 # Filter out tags that are added by gerrit and chromite.
104 filter_re = re.compile(
105 r'((Commit|Trybot)-Ready|Commit-Queue|(Reviewed|Submitted|Tested)-by): ')
106
Mike Frysinger4853b172013-04-04 13:03:49 -0400107 # Rewrite the commit message all the time. Latest gerrit doesn't seem
108 # to like it when you use the same ChangeId on different branches.
109 msg = []
110 for line in patch.commit_message.splitlines():
111 if line.startswith('Reviewed-on: '):
112 line = 'Previous-' + line
Mike Frysingera7a2f002015-04-29 23:23:19 -0400113 elif filter_re.match(line):
Mike Frysinger4853b172013-04-04 13:03:49 -0400114 # If the tag is malformed, or the person lacks a name,
115 # then that's just too bad -- throw it away.
116 ele = re.split(r'[<>@]+', line)
117 if len(ele) == 4:
118 reviewers.add('@'.join(ele[-3:-1]))
119 continue
120 msg.append(line)
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500121 msg += ['(cherry picked from commit %s)' % patch.sha1]
Mike Frysinger4853b172013-04-04 13:03:49 -0400122 git.RunGit(work_dir, ['commit', '--amend', '-F', '-'],
123 input='\n'.join(msg).encode('utf8'))
Mike Frysinger187af412012-10-27 04:27:04 -0400124
Mike Frysinger4853b172013-04-04 13:03:49 -0400125 # Get the new sha1 after rewriting the commit message.
126 new_sha1 = git.GetGitRepoRevision(work_dir)
Mike Frysinger187af412012-10-27 04:27:04 -0400127
Chris Sosaefc35722012-09-11 18:55:37 -0700128 # Create and use a LocalPatch to Upload the change to Gerrit.
129 local_patch = cros_patch.LocalPatch(
130 work_dir, patch.project_url, constants.PATCH_BRANCH,
131 patch.tracking_branch, patch.remote, new_sha1)
Mike Frysingerfb446bf2013-02-28 18:36:42 -0500132 for reviewers in (reviewers, ()):
133 try:
Chris Sosae5b7e002013-03-14 17:59:42 -0700134 return local_patch.Upload(
Mike Frysingerfb446bf2013-02-28 18:36:42 -0500135 patch.project_url, 'refs/%s/%s' % (upload_type, branch),
136 carbon_copy=False, dryrun=dryrun, reviewers=reviewers)
137 except cros_build_lib.RunCommandError as e:
138 if (e.result.returncode == 128 and
David James7c352bc2013-03-15 14:19:57 -0700139 re.search(r'fatal: user ".*?" not found', e.result.error)):
Mike Frysingerfb446bf2013-02-28 18:36:42 -0500140 logging.warning('Some reviewers were not found (%s); '
141 'dropping them & retrying upload', ' '.join(reviewers))
142 continue
143 raise
Chris Sosaefc35722012-09-11 18:55:37 -0700144
145
146def _SetupWorkDirectoryForPatch(work_dir, patch, branch, manifest, email):
147 """Set up local dir for uploading changes to the given patch's project."""
Ralph Nathan5182e512015-04-03 15:37:54 -0700148 logging.notice('Setting up dir %s for uploading changes to %s', work_dir,
149 patch.project_url)
Chris Sosaefc35722012-09-11 18:55:37 -0700150
151 # Clone the git repo from reference if we have a pointer to a
152 # ManifestCheckout object.
153 reference = None
154 if manifest:
David Jamesafa4f5f2013-11-20 14:12:55 -0800155 # Get the path to the first checkout associated with this change. Since
156 # all of the checkouts share git objects, it doesn't matter which checkout
157 # we pick.
David Jamese6301e02016-11-03 17:14:03 -0700158 path = manifest.FindCheckouts(patch.project)[0]['path']
David Jamesafa4f5f2013-11-20 14:12:55 -0800159
David Jamese3b06062013-11-09 18:52:02 -0800160 reference = os.path.join(constants.SOURCE_ROOT, path)
Mike Frysinger6fc41762013-02-27 16:36:54 -0500161 if not os.path.isdir(reference):
162 logging.error('Unable to locate git checkout: %s', reference)
163 logging.error('Did you mean to use --nomirror?')
164 # This will do an "raise OSError" with the right values.
165 os.open(reference, os.O_DIRECTORY)
Chris Sosaefc35722012-09-11 18:55:37 -0700166 # Use the email if email wasn't specified.
167 if not email:
David James97d95872012-11-16 15:09:56 -0800168 email = git.GetProjectUserEmail(reference)
Chris Sosaefc35722012-09-11 18:55:37 -0700169
170 repository.CloneGitRepo(work_dir, patch.project_url, reference=reference)
171
172 # Set the git committer.
David James97d95872012-11-16 15:09:56 -0800173 git.RunGit(work_dir, ['config', '--replace-all', 'user.email', email])
Chris Sosaefc35722012-09-11 18:55:37 -0700174
David James97d95872012-11-16 15:09:56 -0800175 mbranch = git.MatchSingleBranchName(
Mike Frysinger94f91be2012-10-19 16:09:06 -0400176 work_dir, branch, namespace='refs/remotes/origin/')
Mike Frysinger35887c62012-12-12 23:29:20 -0500177 if branch != mbranch:
Ralph Nathan5182e512015-04-03 15:37:54 -0700178 logging.notice('Auto resolved branch name "%s" to "%s"', branch, mbranch)
Mike Frysinger94f91be2012-10-19 16:09:06 -0400179 branch = mbranch
180
Chris Sosaefc35722012-09-11 18:55:37 -0700181 # Finally, create a local branch for uploading changes to the given remote
182 # branch.
David James97d95872012-11-16 15:09:56 -0800183 git.CreatePushBranch(
Chris Sosaefc35722012-09-11 18:55:37 -0700184 constants.PATCH_BRANCH, work_dir, sync=False,
Don Garrett0b53c2e2015-04-02 17:18:18 -0700185 remote_push_branch=git.RemoteRef('ignore', 'origin/%s' % branch))
Chris Sosaefc35722012-09-11 18:55:37 -0700186
Mike Frysinger94f91be2012-10-19 16:09:06 -0400187 return branch
188
Chris Sosaefc35722012-09-11 18:55:37 -0700189
190def _ManifestContainsAllPatches(manifest, patches):
191 """Returns true if the given manifest contains all the patches.
192
193 Args:
Gaurav Shah7afb0562013-12-26 15:05:39 -0800194 manifest: an instance of git.Manifest
195 patches: a collection of GerritPatch objects.
Chris Sosaefc35722012-09-11 18:55:37 -0700196 """
197 for patch in patches:
David Jamesafa4f5f2013-11-20 14:12:55 -0800198 if not manifest.FindCheckouts(patch.project):
Chris Sosaefc35722012-09-11 18:55:37 -0700199 logging.error('Your manifest does not have the repository %s for '
200 'change %s. Please re-run with --nomirror and '
201 '--email set', patch.project, patch.gerrit_number)
202 return False
203
204 return True
205
206
207def main(argv):
208 parser = _GetParser()
Mike Frysingerb17d7532015-06-04 01:57:29 -0400209 options = parser.parse_args(argv)
210 changes = options.change
211 branch = options.branch
Chris Sosaefc35722012-09-11 18:55:37 -0700212
Mike Frysinger8c986562014-04-02 10:09:17 -0400213 try:
214 patches = gerrit.GetGerritPatchInfo(changes)
215 except ValueError as e:
216 logging.error('Invalid patch: %s', e)
217 cros_build_lib.Die('Did you swap the branch/gerrit number?')
Chris Sosaefc35722012-09-11 18:55:37 -0700218
Ralph Nathan23a12212015-03-25 10:27:54 -0700219 # Suppress all logging info output unless we're running debug.
Chris Sosaefc35722012-09-11 18:55:37 -0700220 if not options.debug:
Ralph Nathan5182e512015-04-03 15:37:54 -0700221 logging.getLogger().setLevel(logging.NOTICE)
Chris Sosaefc35722012-09-11 18:55:37 -0700222
223 # Get a pointer to your repo checkout to look up the local project paths for
224 # both email addresses and for using your checkout as a git mirror.
225 manifest = None
226 if options.mirror:
Mike Frysinger6fc41762013-02-27 16:36:54 -0500227 try:
228 manifest = git.ManifestCheckout.Cached(constants.SOURCE_ROOT)
229 except OSError as e:
230 if e.errno == errno.ENOENT:
231 logging.error('Unable to locate ChromiumOS checkout: %s',
232 constants.SOURCE_ROOT)
233 logging.error('Did you mean to use --nomirror?')
234 return 1
235 raise
Chris Sosaefc35722012-09-11 18:55:37 -0700236 if not _ManifestContainsAllPatches(manifest, patches):
237 return 1
238 else:
239 if not options.email:
240 chromium_email = '%s@chromium.org' % os.environ['USER']
Ralph Nathan5182e512015-04-03 15:37:54 -0700241 logging.notice('--nomirror set without email, using %s', chromium_email)
Chris Sosaefc35722012-09-11 18:55:37 -0700242 options.email = chromium_email
243
244 index = 0
245 work_dir = None
246 root_work_dir = tempfile.mkdtemp(prefix='cros_merge_to_branch')
247 try:
248 for index, (change, patch) in enumerate(zip(changes, patches)):
249 # We only clone the project and set the committer the first time.
250 work_dir = os.path.join(root_work_dir, patch.project)
251 if not os.path.isdir(work_dir):
Mike Frysinger94f91be2012-10-19 16:09:06 -0400252 branch = _SetupWorkDirectoryForPatch(work_dir, patch, branch, manifest,
253 options.email)
Chris Sosaefc35722012-09-11 18:55:37 -0700254
255 # Now that we have the project checked out, let's apply our change and
256 # create a new change on Gerrit.
Ralph Nathan5182e512015-04-03 15:37:54 -0700257 logging.notice('Uploading change %s to branch %s', change, branch)
Mike Frysinger6d9d21b2013-02-20 17:25:11 -0500258 urls = _UploadChangeToBranch(work_dir, patch, branch, options.draft,
259 options.dryrun)
Ralph Nathan5182e512015-04-03 15:37:54 -0700260 logging.notice('Successfully uploaded %s to %s', change, branch)
Mike Frysinger6d9d21b2013-02-20 17:25:11 -0500261 for url in urls:
262 if url.endswith('\x1b[K'):
263 # Git will often times emit these escape sequences.
264 url = url[0:-3]
Ralph Nathan5182e512015-04-03 15:37:54 -0700265 logging.notice(' URL: %s', url)
Chris Sosaefc35722012-09-11 18:55:37 -0700266
Mike Frysinger94f91be2012-10-19 16:09:06 -0400267 except (cros_build_lib.RunCommandError, cros_patch.ApplyPatchException,
Mike Frysinger6fc41762013-02-27 16:36:54 -0500268 git.AmbiguousBranchName, OSError) as e:
Chris Sosaefc35722012-09-11 18:55:37 -0700269 # Tell the user how far we got.
270 good_changes = changes[:index]
271 bad_changes = changes[index:]
272
273 logging.warning('############## SOME CHANGES FAILED TO UPLOAD ############')
274
275 if good_changes:
Ralph Nathan5182e512015-04-03 15:37:54 -0700276 logging.notice(
277 'Successfully uploaded change(s) %s', ' '.join(good_changes))
Chris Sosaefc35722012-09-11 18:55:37 -0700278
279 # Printing out the error here so that we can see exactly what failed. This
280 # is especially useful to debug without using --debug.
281 logging.error('Upload failed with %s', str(e).strip())
282 if not options.wipe:
Ralph Nathan5182e512015-04-03 15:37:54 -0700283 logging.error('Not wiping the directory. You can inspect the failed '
284 'change at %s; After fixing the change (if trivial) you can'
285 ' try to upload the change by running:\n'
286 'git commit -a -c CHERRY_PICK_HEAD\n'
287 'git push %s HEAD:refs/for/%s', work_dir, patch.project_url,
288 branch)
Chris Sosaefc35722012-09-11 18:55:37 -0700289 else:
290 logging.error('--nowipe not set thus deleting the work directory. If you '
291 'wish to debug this, re-run the script with change(s) '
Mike Frysinger1244fb22012-10-19 14:14:10 -0400292 '%s and --nowipe by running:\n %s %s %s --nowipe',
293 ' '.join(bad_changes), sys.argv[0], ' '.join(bad_changes),
294 branch)
Chris Sosaefc35722012-09-11 18:55:37 -0700295
296 # Suppress the stack trace if we're not debugging.
297 if options.debug:
298 raise
299 else:
300 return 1
301
302 finally:
303 if options.wipe:
304 shutil.rmtree(root_work_dir)
305
306 if options.dryrun:
Ralph Nathan5182e512015-04-03 15:37:54 -0700307 logging.notice('Success! To actually upload changes, re-run without '
308 '--dry-run.')
Chris Sosaefc35722012-09-11 18:55:37 -0700309 else:
Ralph Nathan5182e512015-04-03 15:37:54 -0700310 logging.notice('Successfully uploaded all changes requested.')
Chris Sosaefc35722012-09-11 18:55:37 -0700311
312 return 0