blob: ed425ec932b6da9f34f0ea038f7d9c3117e33a3c [file] [log] [blame]
Chris Sosaefc35722012-09-11 18:55:37 -07001#!/usr/bin/python
2
3# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
Mike Frysingerda60bbd2013-03-27 23:57:11 -04007"""This 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 Frysinger6fc41762013-02-27 16:36:54 -050037import errno
Chris Sosaefc35722012-09-11 18:55:37 -070038import logging
39import os
Mike Frysinger187af412012-10-27 04:27:04 -040040import re
Chris Sosaefc35722012-09-11 18:55:37 -070041import shutil
42import sys
43import tempfile
44
45from chromite.buildbot import constants
Chris Sosaefc35722012-09-11 18:55:37 -070046from chromite.buildbot import repository
47from chromite.lib import commandline
48from chromite.lib import cros_build_lib
Brian Harring511055e2012-10-10 02:58:59 -070049from chromite.lib import gerrit
David James97d95872012-11-16 15:09:56 -080050from chromite.lib import git
Brian Harring511055e2012-10-10 02:58:59 -070051from chromite.lib import patch as cros_patch
Chris Sosaefc35722012-09-11 18:55:37 -070052
53
54_USAGE = """
Mike Frysingerda60bbd2013-03-27 23:57:11 -040055cros_merge_to_branch [*]change_number1 [[*]change_number2 ...] branch\n\n%s\
Chris Sosaefc35722012-09-11 18:55:37 -070056""" % __doc__
57
58
59def _GetParser():
60 """Returns the parser to use for this module."""
61 parser = commandline.OptionParser(usage=_USAGE)
62 parser.add_option('-d', '--draft', default=False, action='store_true',
Mike Frysingerda60bbd2013-03-27 23:57:11 -040063 help='upload a draft to Gerrit rather than a change')
Mike Frysingerb6c6fab2012-12-06 00:00:24 -050064 parser.add_option('-n', '--dry-run', default=False, action='store_true',
65 dest='dryrun',
Mike Frysingerda60bbd2013-03-27 23:57:11 -040066 help='apply changes locally but do not upload them')
Chris Sosaefc35722012-09-11 18:55:37 -070067 parser.add_option('-e', '--email',
Mike Frysingerda60bbd2013-03-27 23:57:11 -040068 help='if specified, use this email instead of '
69 'the email you would upload changes as; must be set w/'
70 '--nomirror')
Chris Sosaefc35722012-09-11 18:55:37 -070071 parser.add_option('--nomirror', default=True, dest='mirror',
Mike Frysingerda60bbd2013-03-27 23:57:11 -040072 action='store_false', help='checkout git repo directly; '
73 'requires --email')
Chris Sosaefc35722012-09-11 18:55:37 -070074 parser.add_option('--nowipe', default=True, dest='wipe', action='store_false',
Mike Frysingerda60bbd2013-03-27 23:57:11 -040075 help='do not wipe the work directory after finishing')
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 Frysinger6d9d21b2013-02-20 17:25:11 -050089 Returns:
90 A list of all the gerrit URLs found.
Chris Sosaefc35722012-09-11 18:55:37 -070091 """
92 upload_type = 'drafts' if draft else 'for'
Mike Frysinger187af412012-10-27 04:27:04 -040093 # Download & setup the patch if need be.
94 patch.Fetch(work_dir)
Chris Sosaefc35722012-09-11 18:55:37 -070095 # Apply the actual change.
96 patch.CherryPick(work_dir, inflight=True, leave_dirty=True)
97
98 # Get the new sha1 after apply.
David James97d95872012-11-16 15:09:56 -080099 new_sha1 = git.GetGitRepoRevision(work_dir)
Chris Sosae02aa892013-01-14 10:59:08 -0800100 reviewers = set()
Chris Sosaefc35722012-09-11 18:55:37 -0700101
Mike Frysinger4853b172013-04-04 13:03:49 -0400102 # Rewrite the commit message all the time. Latest gerrit doesn't seem
103 # to like it when you use the same ChangeId on different branches.
104 msg = []
105 for line in patch.commit_message.splitlines():
106 if line.startswith('Reviewed-on: '):
107 line = 'Previous-' + line
108 elif line.startswith('Commit-Ready: ') or \
109 line.startswith('Commit-Queue: ') or \
110 line.startswith('Reviewed-by: ') or \
111 line.startswith('Tested-by: '):
112 # If the tag is malformed, or the person lacks a name,
113 # then that's just too bad -- throw it away.
114 ele = re.split(r'[<>@]+', line)
115 if len(ele) == 4:
116 reviewers.add('@'.join(ele[-3:-1]))
117 continue
118 msg.append(line)
119 msg += [
120 '(cherry picked from commit %s)' % patch.sha1,
121 ]
122 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."""
148 logging.info('Setting up dir %s for uploading changes to %s', work_dir,
149 patch.project_url)
150
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.
158 path = manifest.FindCheckouts(patch.project)[0]['path']
159
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:
178 logging.info('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,
185 remote_push_branch=('ignore', 'origin/%s' % branch))
186
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:
David James97d95872012-11-16 15:09:56 -0800194 manifest - an instance of git.Manifest
Chris Sosaefc35722012-09-11 18:55:37 -0700195 patches - a collection GerritPatch objects.
196 """
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()
209 options, args = parser.parse_args(argv)
210
211 if len(args) < 2:
212 parser.error('Not enough arguments specified')
213
214 changes = args[0:-1]
Brian Harring511055e2012-10-10 02:58:59 -0700215 patches = gerrit.GetGerritPatchInfo(changes)
Chris Sosaefc35722012-09-11 18:55:37 -0700216 branch = args[-1]
217
218 # Suppress all cros_build_lib info output unless we're running debug.
219 if not options.debug:
220 cros_build_lib.logger.setLevel(logging.ERROR)
221
222 # Get a pointer to your repo checkout to look up the local project paths for
223 # both email addresses and for using your checkout as a git mirror.
224 manifest = None
225 if options.mirror:
Mike Frysinger6fc41762013-02-27 16:36:54 -0500226 try:
227 manifest = git.ManifestCheckout.Cached(constants.SOURCE_ROOT)
228 except OSError as e:
229 if e.errno == errno.ENOENT:
230 logging.error('Unable to locate ChromiumOS checkout: %s',
231 constants.SOURCE_ROOT)
232 logging.error('Did you mean to use --nomirror?')
233 return 1
234 raise
Chris Sosaefc35722012-09-11 18:55:37 -0700235 if not _ManifestContainsAllPatches(manifest, patches):
236 return 1
237 else:
238 if not options.email:
239 chromium_email = '%s@chromium.org' % os.environ['USER']
240 logging.info('--nomirror set without email, using %s', chromium_email)
241 options.email = chromium_email
242
243 index = 0
244 work_dir = None
245 root_work_dir = tempfile.mkdtemp(prefix='cros_merge_to_branch')
246 try:
247 for index, (change, patch) in enumerate(zip(changes, patches)):
248 # We only clone the project and set the committer the first time.
249 work_dir = os.path.join(root_work_dir, patch.project)
250 if not os.path.isdir(work_dir):
Mike Frysinger94f91be2012-10-19 16:09:06 -0400251 branch = _SetupWorkDirectoryForPatch(work_dir, patch, branch, manifest,
252 options.email)
Chris Sosaefc35722012-09-11 18:55:37 -0700253
254 # Now that we have the project checked out, let's apply our change and
255 # create a new change on Gerrit.
256 logging.info('Uploading change %s to branch %s', change, branch)
Mike Frysinger6d9d21b2013-02-20 17:25:11 -0500257 urls = _UploadChangeToBranch(work_dir, patch, branch, options.draft,
258 options.dryrun)
Chris Sosaefc35722012-09-11 18:55:37 -0700259 logging.info('Successfully uploaded %s to %s', change, branch)
Mike Frysinger6d9d21b2013-02-20 17:25:11 -0500260 for url in urls:
261 if url.endswith('\x1b[K'):
262 # Git will often times emit these escape sequences.
263 url = url[0:-3]
Mike Frysinger075e6592012-10-27 04:41:09 -0400264 logging.info(' URL: %s', url)
Chris Sosaefc35722012-09-11 18:55:37 -0700265
Mike Frysinger94f91be2012-10-19 16:09:06 -0400266 except (cros_build_lib.RunCommandError, cros_patch.ApplyPatchException,
Mike Frysinger6fc41762013-02-27 16:36:54 -0500267 git.AmbiguousBranchName, OSError) as e:
Chris Sosaefc35722012-09-11 18:55:37 -0700268 # Tell the user how far we got.
269 good_changes = changes[:index]
270 bad_changes = changes[index:]
271
272 logging.warning('############## SOME CHANGES FAILED TO UPLOAD ############')
273
274 if good_changes:
275 logging.info('Successfully uploaded change(s) %s', ' '.join(good_changes))
276
277 # Printing out the error here so that we can see exactly what failed. This
278 # is especially useful to debug without using --debug.
279 logging.error('Upload failed with %s', str(e).strip())
280 if not options.wipe:
281 logging.info('Not wiping the directory. You can inspect the failed '
Mike Frysinger9f606d72012-10-12 00:55:57 -0400282 'change at %s; After fixing the change (if trivial) you can '
Chris Sosaefc35722012-09-11 18:55:37 -0700283 'try to upload the change by running:\n'
Mike Frysinger92a41e72013-02-21 15:33:07 -0500284 'git commit -a -c CHERRY_PICK_HEAD\n'
Chris Sosaefc35722012-09-11 18:55:37 -0700285 'git push %s HEAD:refs/for/%s', work_dir, patch.project_url,
286 branch)
287 else:
288 logging.error('--nowipe not set thus deleting the work directory. If you '
289 'wish to debug this, re-run the script with change(s) '
Mike Frysinger1244fb22012-10-19 14:14:10 -0400290 '%s and --nowipe by running:\n %s %s %s --nowipe',
291 ' '.join(bad_changes), sys.argv[0], ' '.join(bad_changes),
292 branch)
Chris Sosaefc35722012-09-11 18:55:37 -0700293
294 # Suppress the stack trace if we're not debugging.
295 if options.debug:
296 raise
297 else:
298 return 1
299
300 finally:
301 if options.wipe:
302 shutil.rmtree(root_work_dir)
303
304 if options.dryrun:
Mike Frysingerb6c6fab2012-12-06 00:00:24 -0500305 logging.info('Success! To actually upload changes, re-run without '
306 '--dry-run.')
Chris Sosaefc35722012-09-11 18:55:37 -0700307 else:
308 logging.info('Successfully uploaded all changes requested.')
309
310 return 0