blob: 7b2be2b14fe764e5360e799cc3f646041f007544 [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:
33http://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 Frysinger383367e2014-09-16 15:06:17 -040037from __future__ import print_function
38
Mike Frysinger6fc41762013-02-27 16:36:54 -050039import errno
Chris Sosaefc35722012-09-11 18:55:37 -070040import os
Mike Frysinger187af412012-10-27 04:27:04 -040041import re
Chris Sosaefc35722012-09-11 18:55:37 -070042import shutil
43import sys
44import tempfile
45
Don Garrett88b8d782014-05-13 17:30:55 -070046from chromite.cbuildbot import constants
47from chromite.cbuildbot import repository
Chris Sosaefc35722012-09-11 18:55:37 -070048from chromite.lib import commandline
49from chromite.lib import cros_build_lib
Ralph Nathan91874ca2015-03-19 13:29:41 -070050from chromite.lib import cros_logging as logging
Brian Harring511055e2012-10-10 02:58:59 -070051from chromite.lib import gerrit
David James97d95872012-11-16 15:09:56 -080052from chromite.lib import git
Brian Harring511055e2012-10-10 02:58:59 -070053from chromite.lib import patch as cros_patch
Chris Sosaefc35722012-09-11 18:55:37 -070054
55
56_USAGE = """
Mike Frysingerda60bbd2013-03-27 23:57:11 -040057cros_merge_to_branch [*]change_number1 [[*]change_number2 ...] branch\n\n%s\
Chris Sosaefc35722012-09-11 18:55:37 -070058""" % __doc__
59
60
61def _GetParser():
62 """Returns the parser to use for this module."""
63 parser = commandline.OptionParser(usage=_USAGE)
64 parser.add_option('-d', '--draft', default=False, action='store_true',
Mike Frysingerda60bbd2013-03-27 23:57:11 -040065 help='upload a draft to Gerrit rather than a change')
Mike Frysingerb6c6fab2012-12-06 00:00:24 -050066 parser.add_option('-n', '--dry-run', default=False, action='store_true',
67 dest='dryrun',
Mike Frysingerda60bbd2013-03-27 23:57:11 -040068 help='apply changes locally but do not upload them')
Chris Sosaefc35722012-09-11 18:55:37 -070069 parser.add_option('-e', '--email',
Mike Frysingerda60bbd2013-03-27 23:57:11 -040070 help='if specified, use this email instead of '
71 'the email you would upload changes as; must be set w/'
72 '--nomirror')
Chris Sosaefc35722012-09-11 18:55:37 -070073 parser.add_option('--nomirror', default=True, dest='mirror',
Mike Frysingerda60bbd2013-03-27 23:57:11 -040074 action='store_false', help='checkout git repo directly; '
75 'requires --email')
Chris Sosaefc35722012-09-11 18:55:37 -070076 parser.add_option('--nowipe', default=True, dest='wipe', action='store_false',
Mike Frysingerda60bbd2013-03-27 23:57:11 -040077 help='do not wipe the work directory after finishing')
Chris Sosaefc35722012-09-11 18:55:37 -070078 return parser
79
80
81def _UploadChangeToBranch(work_dir, patch, branch, draft, dryrun):
82 """Creates a new change from GerritPatch |patch| to |branch| from |work_dir|.
83
84 Args:
85 patch: Instance of GerritPatch to upload.
86 branch: Branch to upload to.
87 work_dir: Local directory where repository is checked out in.
88 draft: If True, upload to refs/draft/|branch| rather than refs/for/|branch|.
89 dryrun: Don't actually upload a change but go through all the steps up to
Mike Frysingerb6c6fab2012-12-06 00:00:24 -050090 and including git push --dry-run.
Mike Frysinger1a736a82013-12-12 01:50:59 -050091
Mike Frysinger6d9d21b2013-02-20 17:25:11 -050092 Returns:
93 A list of all the gerrit URLs found.
Chris Sosaefc35722012-09-11 18:55:37 -070094 """
95 upload_type = 'drafts' if draft else 'for'
Mike Frysinger187af412012-10-27 04:27:04 -040096 # Download & setup the patch if need be.
97 patch.Fetch(work_dir)
Chris Sosaefc35722012-09-11 18:55:37 -070098 # Apply the actual change.
99 patch.CherryPick(work_dir, inflight=True, leave_dirty=True)
100
101 # Get the new sha1 after apply.
David James97d95872012-11-16 15:09:56 -0800102 new_sha1 = git.GetGitRepoRevision(work_dir)
Chris Sosae02aa892013-01-14 10:59:08 -0800103 reviewers = set()
Chris Sosaefc35722012-09-11 18:55:37 -0700104
Mike Frysingera7a2f002015-04-29 23:23:19 -0400105 # Filter out tags that are added by gerrit and chromite.
106 filter_re = re.compile(
107 r'((Commit|Trybot)-Ready|Commit-Queue|(Reviewed|Submitted|Tested)-by): ')
108
Mike Frysinger4853b172013-04-04 13:03:49 -0400109 # Rewrite the commit message all the time. Latest gerrit doesn't seem
110 # to like it when you use the same ChangeId on different branches.
111 msg = []
112 for line in patch.commit_message.splitlines():
113 if line.startswith('Reviewed-on: '):
114 line = 'Previous-' + line
Mike Frysingera7a2f002015-04-29 23:23:19 -0400115 elif filter_re.match(line):
Mike Frysinger4853b172013-04-04 13:03:49 -0400116 # If the tag is malformed, or the person lacks a name,
117 # then that's just too bad -- throw it away.
118 ele = re.split(r'[<>@]+', line)
119 if len(ele) == 4:
120 reviewers.add('@'.join(ele[-3:-1]))
121 continue
122 msg.append(line)
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500123 msg += ['(cherry picked from commit %s)' % patch.sha1]
Mike Frysinger4853b172013-04-04 13:03:49 -0400124 git.RunGit(work_dir, ['commit', '--amend', '-F', '-'],
125 input='\n'.join(msg).encode('utf8'))
Mike Frysinger187af412012-10-27 04:27:04 -0400126
Mike Frysinger4853b172013-04-04 13:03:49 -0400127 # Get the new sha1 after rewriting the commit message.
128 new_sha1 = git.GetGitRepoRevision(work_dir)
Mike Frysinger187af412012-10-27 04:27:04 -0400129
Chris Sosaefc35722012-09-11 18:55:37 -0700130 # Create and use a LocalPatch to Upload the change to Gerrit.
131 local_patch = cros_patch.LocalPatch(
132 work_dir, patch.project_url, constants.PATCH_BRANCH,
133 patch.tracking_branch, patch.remote, new_sha1)
Mike Frysingerfb446bf2013-02-28 18:36:42 -0500134 for reviewers in (reviewers, ()):
135 try:
Chris Sosae5b7e002013-03-14 17:59:42 -0700136 return local_patch.Upload(
Mike Frysingerfb446bf2013-02-28 18:36:42 -0500137 patch.project_url, 'refs/%s/%s' % (upload_type, branch),
138 carbon_copy=False, dryrun=dryrun, reviewers=reviewers)
139 except cros_build_lib.RunCommandError as e:
140 if (e.result.returncode == 128 and
David James7c352bc2013-03-15 14:19:57 -0700141 re.search(r'fatal: user ".*?" not found', e.result.error)):
Mike Frysingerfb446bf2013-02-28 18:36:42 -0500142 logging.warning('Some reviewers were not found (%s); '
143 'dropping them & retrying upload', ' '.join(reviewers))
144 continue
145 raise
Chris Sosaefc35722012-09-11 18:55:37 -0700146
147
148def _SetupWorkDirectoryForPatch(work_dir, patch, branch, manifest, email):
149 """Set up local dir for uploading changes to the given patch's project."""
Ralph Nathan5182e512015-04-03 15:37:54 -0700150 logging.notice('Setting up dir %s for uploading changes to %s', work_dir,
151 patch.project_url)
Chris Sosaefc35722012-09-11 18:55:37 -0700152
153 # Clone the git repo from reference if we have a pointer to a
154 # ManifestCheckout object.
155 reference = None
156 if manifest:
David Jamesafa4f5f2013-11-20 14:12:55 -0800157 # Get the path to the first checkout associated with this change. Since
158 # all of the checkouts share git objects, it doesn't matter which checkout
159 # we pick.
Gaurav Shah7afb0562013-12-26 15:05:39 -0800160 path = manifest.FindCheckouts(patch.project, only_patchable=True)[0]['path']
David Jamesafa4f5f2013-11-20 14:12:55 -0800161
David Jamese3b06062013-11-09 18:52:02 -0800162 reference = os.path.join(constants.SOURCE_ROOT, path)
Mike Frysinger6fc41762013-02-27 16:36:54 -0500163 if not os.path.isdir(reference):
164 logging.error('Unable to locate git checkout: %s', reference)
165 logging.error('Did you mean to use --nomirror?')
166 # This will do an "raise OSError" with the right values.
167 os.open(reference, os.O_DIRECTORY)
Chris Sosaefc35722012-09-11 18:55:37 -0700168 # Use the email if email wasn't specified.
169 if not email:
David James97d95872012-11-16 15:09:56 -0800170 email = git.GetProjectUserEmail(reference)
Chris Sosaefc35722012-09-11 18:55:37 -0700171
172 repository.CloneGitRepo(work_dir, patch.project_url, reference=reference)
173
174 # Set the git committer.
David James97d95872012-11-16 15:09:56 -0800175 git.RunGit(work_dir, ['config', '--replace-all', 'user.email', email])
Chris Sosaefc35722012-09-11 18:55:37 -0700176
David James97d95872012-11-16 15:09:56 -0800177 mbranch = git.MatchSingleBranchName(
Mike Frysinger94f91be2012-10-19 16:09:06 -0400178 work_dir, branch, namespace='refs/remotes/origin/')
Mike Frysinger35887c62012-12-12 23:29:20 -0500179 if branch != mbranch:
Ralph Nathan5182e512015-04-03 15:37:54 -0700180 logging.notice('Auto resolved branch name "%s" to "%s"', branch, mbranch)
Mike Frysinger94f91be2012-10-19 16:09:06 -0400181 branch = mbranch
182
Chris Sosaefc35722012-09-11 18:55:37 -0700183 # Finally, create a local branch for uploading changes to the given remote
184 # branch.
David James97d95872012-11-16 15:09:56 -0800185 git.CreatePushBranch(
Chris Sosaefc35722012-09-11 18:55:37 -0700186 constants.PATCH_BRANCH, work_dir, sync=False,
Don Garrett0b53c2e2015-04-02 17:18:18 -0700187 remote_push_branch=git.RemoteRef('ignore', 'origin/%s' % branch))
Chris Sosaefc35722012-09-11 18:55:37 -0700188
Mike Frysinger94f91be2012-10-19 16:09:06 -0400189 return branch
190
Chris Sosaefc35722012-09-11 18:55:37 -0700191
192def _ManifestContainsAllPatches(manifest, patches):
193 """Returns true if the given manifest contains all the patches.
194
195 Args:
Gaurav Shah7afb0562013-12-26 15:05:39 -0800196 manifest: an instance of git.Manifest
197 patches: a collection of GerritPatch objects.
Chris Sosaefc35722012-09-11 18:55:37 -0700198 """
199 for patch in patches:
David Jamesafa4f5f2013-11-20 14:12:55 -0800200 if not manifest.FindCheckouts(patch.project):
Chris Sosaefc35722012-09-11 18:55:37 -0700201 logging.error('Your manifest does not have the repository %s for '
202 'change %s. Please re-run with --nomirror and '
203 '--email set', patch.project, patch.gerrit_number)
204 return False
205
206 return True
207
208
209def main(argv):
210 parser = _GetParser()
211 options, args = parser.parse_args(argv)
212
213 if len(args) < 2:
214 parser.error('Not enough arguments specified')
215
216 changes = args[0:-1]
Mike Frysinger8c986562014-04-02 10:09:17 -0400217 try:
218 patches = gerrit.GetGerritPatchInfo(changes)
219 except ValueError as e:
220 logging.error('Invalid patch: %s', e)
221 cros_build_lib.Die('Did you swap the branch/gerrit number?')
Chris Sosaefc35722012-09-11 18:55:37 -0700222 branch = args[-1]
223
Ralph Nathan23a12212015-03-25 10:27:54 -0700224 # Suppress all logging info output unless we're running debug.
Chris Sosaefc35722012-09-11 18:55:37 -0700225 if not options.debug:
Ralph Nathan5182e512015-04-03 15:37:54 -0700226 logging.getLogger().setLevel(logging.NOTICE)
Chris Sosaefc35722012-09-11 18:55:37 -0700227
228 # Get a pointer to your repo checkout to look up the local project paths for
229 # both email addresses and for using your checkout as a git mirror.
230 manifest = None
231 if options.mirror:
Mike Frysinger6fc41762013-02-27 16:36:54 -0500232 try:
233 manifest = git.ManifestCheckout.Cached(constants.SOURCE_ROOT)
234 except OSError as e:
235 if e.errno == errno.ENOENT:
236 logging.error('Unable to locate ChromiumOS checkout: %s',
237 constants.SOURCE_ROOT)
238 logging.error('Did you mean to use --nomirror?')
239 return 1
240 raise
Chris Sosaefc35722012-09-11 18:55:37 -0700241 if not _ManifestContainsAllPatches(manifest, patches):
242 return 1
243 else:
244 if not options.email:
245 chromium_email = '%s@chromium.org' % os.environ['USER']
Ralph Nathan5182e512015-04-03 15:37:54 -0700246 logging.notice('--nomirror set without email, using %s', chromium_email)
Chris Sosaefc35722012-09-11 18:55:37 -0700247 options.email = chromium_email
248
249 index = 0
250 work_dir = None
251 root_work_dir = tempfile.mkdtemp(prefix='cros_merge_to_branch')
252 try:
253 for index, (change, patch) in enumerate(zip(changes, patches)):
254 # We only clone the project and set the committer the first time.
255 work_dir = os.path.join(root_work_dir, patch.project)
256 if not os.path.isdir(work_dir):
Mike Frysinger94f91be2012-10-19 16:09:06 -0400257 branch = _SetupWorkDirectoryForPatch(work_dir, patch, branch, manifest,
258 options.email)
Chris Sosaefc35722012-09-11 18:55:37 -0700259
260 # Now that we have the project checked out, let's apply our change and
261 # create a new change on Gerrit.
Ralph Nathan5182e512015-04-03 15:37:54 -0700262 logging.notice('Uploading change %s to branch %s', change, branch)
Mike Frysinger6d9d21b2013-02-20 17:25:11 -0500263 urls = _UploadChangeToBranch(work_dir, patch, branch, options.draft,
264 options.dryrun)
Ralph Nathan5182e512015-04-03 15:37:54 -0700265 logging.notice('Successfully uploaded %s to %s', change, branch)
Mike Frysinger6d9d21b2013-02-20 17:25:11 -0500266 for url in urls:
267 if url.endswith('\x1b[K'):
268 # Git will often times emit these escape sequences.
269 url = url[0:-3]
Ralph Nathan5182e512015-04-03 15:37:54 -0700270 logging.notice(' URL: %s', url)
Chris Sosaefc35722012-09-11 18:55:37 -0700271
Mike Frysinger94f91be2012-10-19 16:09:06 -0400272 except (cros_build_lib.RunCommandError, cros_patch.ApplyPatchException,
Mike Frysinger6fc41762013-02-27 16:36:54 -0500273 git.AmbiguousBranchName, OSError) as e:
Chris Sosaefc35722012-09-11 18:55:37 -0700274 # Tell the user how far we got.
275 good_changes = changes[:index]
276 bad_changes = changes[index:]
277
278 logging.warning('############## SOME CHANGES FAILED TO UPLOAD ############')
279
280 if good_changes:
Ralph Nathan5182e512015-04-03 15:37:54 -0700281 logging.notice(
282 'Successfully uploaded change(s) %s', ' '.join(good_changes))
Chris Sosaefc35722012-09-11 18:55:37 -0700283
284 # Printing out the error here so that we can see exactly what failed. This
285 # is especially useful to debug without using --debug.
286 logging.error('Upload failed with %s', str(e).strip())
287 if not options.wipe:
Ralph Nathan5182e512015-04-03 15:37:54 -0700288 logging.error('Not wiping the directory. You can inspect the failed '
289 'change at %s; After fixing the change (if trivial) you can'
290 ' try to upload the change by running:\n'
291 'git commit -a -c CHERRY_PICK_HEAD\n'
292 'git push %s HEAD:refs/for/%s', work_dir, patch.project_url,
293 branch)
Chris Sosaefc35722012-09-11 18:55:37 -0700294 else:
295 logging.error('--nowipe not set thus deleting the work directory. If you '
296 'wish to debug this, re-run the script with change(s) '
Mike Frysinger1244fb22012-10-19 14:14:10 -0400297 '%s and --nowipe by running:\n %s %s %s --nowipe',
298 ' '.join(bad_changes), sys.argv[0], ' '.join(bad_changes),
299 branch)
Chris Sosaefc35722012-09-11 18:55:37 -0700300
301 # Suppress the stack trace if we're not debugging.
302 if options.debug:
303 raise
304 else:
305 return 1
306
307 finally:
308 if options.wipe:
309 shutil.rmtree(root_work_dir)
310
311 if options.dryrun:
Ralph Nathan5182e512015-04-03 15:37:54 -0700312 logging.notice('Success! To actually upload changes, re-run without '
313 '--dry-run.')
Chris Sosaefc35722012-09-11 18:55:37 -0700314 else:
Ralph Nathan5182e512015-04-03 15:37:54 -0700315 logging.notice('Successfully uploaded all changes requested.')
Chris Sosaefc35722012-09-11 18:55:37 -0700316
317 return 0