blob: e9ff69e3e3eaae00cec6fce95609d3668e1c9b85 [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 Frysingerda60bbd2013-03-27 23:57:11 -040034anch"""
Chris Sosaefc35722012-09-11 18:55:37 -070035
Mike Frysinger6fc41762013-02-27 16:36:54 -050036import errno
Chris Sosaefc35722012-09-11 18:55:37 -070037import logging
38import os
Mike Frysinger187af412012-10-27 04:27:04 -040039import re
Chris Sosaefc35722012-09-11 18:55:37 -070040import shutil
41import sys
42import tempfile
43
44from chromite.buildbot import constants
Chris Sosaefc35722012-09-11 18:55:37 -070045from chromite.buildbot import repository
46from chromite.lib import commandline
47from 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
53_USAGE = """
Mike Frysingerda60bbd2013-03-27 23:57:11 -040054cros_merge_to_branch [*]change_number1 [[*]change_number2 ...] branch\n\n%s\
Chris Sosaefc35722012-09-11 18:55:37 -070055""" % __doc__
56
57
58def _GetParser():
59 """Returns the parser to use for this module."""
60 parser = commandline.OptionParser(usage=_USAGE)
61 parser.add_option('-d', '--draft', default=False, action='store_true',
Mike Frysingerda60bbd2013-03-27 23:57:11 -040062 help='upload a draft to Gerrit rather than a change')
Mike Frysingerb6c6fab2012-12-06 00:00:24 -050063 parser.add_option('-n', '--dry-run', default=False, action='store_true',
64 dest='dryrun',
Mike Frysingerda60bbd2013-03-27 23:57:11 -040065 help='apply changes locally but do not upload them')
Chris Sosaefc35722012-09-11 18:55:37 -070066 parser.add_option('-e', '--email',
Mike Frysingerda60bbd2013-03-27 23:57:11 -040067 help='if specified, use this email instead of '
68 'the email you would upload changes as; must be set w/'
69 '--nomirror')
Chris Sosaefc35722012-09-11 18:55:37 -070070 parser.add_option('--nomirror', default=True, dest='mirror',
Mike Frysingerda60bbd2013-03-27 23:57:11 -040071 action='store_false', help='checkout git repo directly; '
72 'requires --email')
Chris Sosaefc35722012-09-11 18:55:37 -070073 parser.add_option('--nowipe', default=True, dest='wipe', action='store_false',
Mike Frysingerda60bbd2013-03-27 23:57:11 -040074 help='do not wipe the work directory after finishing')
Chris Sosaefc35722012-09-11 18:55:37 -070075 return parser
76
77
78def _UploadChangeToBranch(work_dir, patch, branch, draft, dryrun):
79 """Creates a new change from GerritPatch |patch| to |branch| from |work_dir|.
80
81 Args:
82 patch: Instance of GerritPatch to upload.
83 branch: Branch to upload to.
84 work_dir: Local directory where repository is checked out in.
85 draft: If True, upload to refs/draft/|branch| rather than refs/for/|branch|.
86 dryrun: Don't actually upload a change but go through all the steps up to
Mike Frysingerb6c6fab2012-12-06 00:00:24 -050087 and including git push --dry-run.
Mike Frysinger6d9d21b2013-02-20 17:25:11 -050088 Returns:
89 A list of all the gerrit URLs found.
Chris Sosaefc35722012-09-11 18:55:37 -070090 """
91 upload_type = 'drafts' if draft else 'for'
Mike Frysinger187af412012-10-27 04:27:04 -040092 # Download & setup the patch if need be.
93 patch.Fetch(work_dir)
Chris Sosaefc35722012-09-11 18:55:37 -070094 # Apply the actual change.
95 patch.CherryPick(work_dir, inflight=True, leave_dirty=True)
96
97 # Get the new sha1 after apply.
David James97d95872012-11-16 15:09:56 -080098 new_sha1 = git.GetGitRepoRevision(work_dir)
Chris Sosae02aa892013-01-14 10:59:08 -080099 reviewers = set()
Chris Sosaefc35722012-09-11 18:55:37 -0700100
Mike Frysinger4853b172013-04-04 13:03:49 -0400101 # Rewrite the commit message all the time. Latest gerrit doesn't seem
102 # to like it when you use the same ChangeId on different branches.
103 msg = []
104 for line in patch.commit_message.splitlines():
105 if line.startswith('Reviewed-on: '):
106 line = 'Previous-' + line
107 elif line.startswith('Commit-Ready: ') or \
108 line.startswith('Commit-Queue: ') or \
109 line.startswith('Reviewed-by: ') or \
110 line.startswith('Tested-by: '):
111 # If the tag is malformed, or the person lacks a name,
112 # then that's just too bad -- throw it away.
113 ele = re.split(r'[<>@]+', line)
114 if len(ele) == 4:
115 reviewers.add('@'.join(ele[-3:-1]))
116 continue
117 msg.append(line)
118 msg += [
119 '(cherry picked from commit %s)' % patch.sha1,
120 ]
121 git.RunGit(work_dir, ['commit', '--amend', '-F', '-'],
122 input='\n'.join(msg).encode('utf8'))
Mike Frysinger187af412012-10-27 04:27:04 -0400123
Mike Frysinger4853b172013-04-04 13:03:49 -0400124 # Get the new sha1 after rewriting the commit message.
125 new_sha1 = git.GetGitRepoRevision(work_dir)
Mike Frysinger187af412012-10-27 04:27:04 -0400126
Chris Sosaefc35722012-09-11 18:55:37 -0700127 # Create and use a LocalPatch to Upload the change to Gerrit.
128 local_patch = cros_patch.LocalPatch(
129 work_dir, patch.project_url, constants.PATCH_BRANCH,
130 patch.tracking_branch, patch.remote, new_sha1)
Mike Frysingerfb446bf2013-02-28 18:36:42 -0500131 for reviewers in (reviewers, ()):
132 try:
Chris Sosae5b7e002013-03-14 17:59:42 -0700133 return local_patch.Upload(
Mike Frysingerfb446bf2013-02-28 18:36:42 -0500134 patch.project_url, 'refs/%s/%s' % (upload_type, branch),
135 carbon_copy=False, dryrun=dryrun, reviewers=reviewers)
136 except cros_build_lib.RunCommandError as e:
137 if (e.result.returncode == 128 and
David James7c352bc2013-03-15 14:19:57 -0700138 re.search(r'fatal: user ".*?" not found', e.result.error)):
Mike Frysingerfb446bf2013-02-28 18:36:42 -0500139 logging.warning('Some reviewers were not found (%s); '
140 'dropping them & retrying upload', ' '.join(reviewers))
141 continue
142 raise
Chris Sosaefc35722012-09-11 18:55:37 -0700143
144
145def _SetupWorkDirectoryForPatch(work_dir, patch, branch, manifest, email):
146 """Set up local dir for uploading changes to the given patch's project."""
147 logging.info('Setting up dir %s for uploading changes to %s', work_dir,
148 patch.project_url)
149
150 # Clone the git repo from reference if we have a pointer to a
151 # ManifestCheckout object.
152 reference = None
153 if manifest:
154 reference = os.path.join(constants.SOURCE_ROOT,
155 manifest.GetProjectPath(patch.project))
Mike Frysinger6fc41762013-02-27 16:36:54 -0500156 if not os.path.isdir(reference):
157 logging.error('Unable to locate git checkout: %s', reference)
158 logging.error('Did you mean to use --nomirror?')
159 # This will do an "raise OSError" with the right values.
160 os.open(reference, os.O_DIRECTORY)
Chris Sosaefc35722012-09-11 18:55:37 -0700161 # Use the email if email wasn't specified.
162 if not email:
David James97d95872012-11-16 15:09:56 -0800163 email = git.GetProjectUserEmail(reference)
Chris Sosaefc35722012-09-11 18:55:37 -0700164
165 repository.CloneGitRepo(work_dir, patch.project_url, reference=reference)
166
167 # Set the git committer.
David James97d95872012-11-16 15:09:56 -0800168 git.RunGit(work_dir, ['config', '--replace-all', 'user.email', email])
Chris Sosaefc35722012-09-11 18:55:37 -0700169
David James97d95872012-11-16 15:09:56 -0800170 mbranch = git.MatchSingleBranchName(
Mike Frysinger94f91be2012-10-19 16:09:06 -0400171 work_dir, branch, namespace='refs/remotes/origin/')
Mike Frysinger35887c62012-12-12 23:29:20 -0500172 if branch != mbranch:
173 logging.info('Auto resolved branch name "%s" to "%s"', branch, mbranch)
Mike Frysinger94f91be2012-10-19 16:09:06 -0400174 branch = mbranch
175
Chris Sosaefc35722012-09-11 18:55:37 -0700176 # Finally, create a local branch for uploading changes to the given remote
177 # branch.
David James97d95872012-11-16 15:09:56 -0800178 git.CreatePushBranch(
Chris Sosaefc35722012-09-11 18:55:37 -0700179 constants.PATCH_BRANCH, work_dir, sync=False,
180 remote_push_branch=('ignore', 'origin/%s' % branch))
181
Mike Frysinger94f91be2012-10-19 16:09:06 -0400182 return branch
183
Chris Sosaefc35722012-09-11 18:55:37 -0700184
185def _ManifestContainsAllPatches(manifest, patches):
186 """Returns true if the given manifest contains all the patches.
187
188 Args:
David James97d95872012-11-16 15:09:56 -0800189 manifest - an instance of git.Manifest
Chris Sosaefc35722012-09-11 18:55:37 -0700190 patches - a collection GerritPatch objects.
191 """
192 for patch in patches:
193 project_path = None
194 if manifest.ProjectExists(patch.project):
195 project_path = manifest.GetProjectPath(patch.project)
196
197 if not project_path:
198 logging.error('Your manifest does not have the repository %s for '
199 'change %s. Please re-run with --nomirror and '
200 '--email set', patch.project, patch.gerrit_number)
201 return False
202
203 return True
204
205
206def main(argv):
207 parser = _GetParser()
208 options, args = parser.parse_args(argv)
209
210 if len(args) < 2:
211 parser.error('Not enough arguments specified')
212
213 changes = args[0:-1]
Brian Harring511055e2012-10-10 02:58:59 -0700214 patches = gerrit.GetGerritPatchInfo(changes)
Chris Sosaefc35722012-09-11 18:55:37 -0700215 branch = args[-1]
216
217 # Suppress all cros_build_lib info output unless we're running debug.
218 if not options.debug:
219 cros_build_lib.logger.setLevel(logging.ERROR)
220
221 # Get a pointer to your repo checkout to look up the local project paths for
222 # both email addresses and for using your checkout as a git mirror.
223 manifest = None
224 if options.mirror:
Mike Frysinger6fc41762013-02-27 16:36:54 -0500225 try:
226 manifest = git.ManifestCheckout.Cached(constants.SOURCE_ROOT)
227 except OSError as e:
228 if e.errno == errno.ENOENT:
229 logging.error('Unable to locate ChromiumOS checkout: %s',
230 constants.SOURCE_ROOT)
231 logging.error('Did you mean to use --nomirror?')
232 return 1
233 raise
Chris Sosaefc35722012-09-11 18:55:37 -0700234 if not _ManifestContainsAllPatches(manifest, patches):
235 return 1
236 else:
237 if not options.email:
238 chromium_email = '%s@chromium.org' % os.environ['USER']
239 logging.info('--nomirror set without email, using %s', chromium_email)
240 options.email = chromium_email
241
242 index = 0
243 work_dir = None
244 root_work_dir = tempfile.mkdtemp(prefix='cros_merge_to_branch')
245 try:
246 for index, (change, patch) in enumerate(zip(changes, patches)):
247 # We only clone the project and set the committer the first time.
248 work_dir = os.path.join(root_work_dir, patch.project)
249 if not os.path.isdir(work_dir):
Mike Frysinger94f91be2012-10-19 16:09:06 -0400250 branch = _SetupWorkDirectoryForPatch(work_dir, patch, branch, manifest,
251 options.email)
Chris Sosaefc35722012-09-11 18:55:37 -0700252
253 # Now that we have the project checked out, let's apply our change and
254 # create a new change on Gerrit.
255 logging.info('Uploading change %s to branch %s', change, branch)
Mike Frysinger6d9d21b2013-02-20 17:25:11 -0500256 urls = _UploadChangeToBranch(work_dir, patch, branch, options.draft,
257 options.dryrun)
Chris Sosaefc35722012-09-11 18:55:37 -0700258 logging.info('Successfully uploaded %s to %s', change, branch)
Mike Frysinger6d9d21b2013-02-20 17:25:11 -0500259 for url in urls:
260 if url.endswith('\x1b[K'):
261 # Git will often times emit these escape sequences.
262 url = url[0:-3]
Mike Frysinger075e6592012-10-27 04:41:09 -0400263 logging.info(' URL: %s', url)
Chris Sosaefc35722012-09-11 18:55:37 -0700264
Mike Frysinger94f91be2012-10-19 16:09:06 -0400265 except (cros_build_lib.RunCommandError, cros_patch.ApplyPatchException,
Mike Frysinger6fc41762013-02-27 16:36:54 -0500266 git.AmbiguousBranchName, OSError) as e:
Chris Sosaefc35722012-09-11 18:55:37 -0700267 # Tell the user how far we got.
268 good_changes = changes[:index]
269 bad_changes = changes[index:]
270
271 logging.warning('############## SOME CHANGES FAILED TO UPLOAD ############')
272
273 if good_changes:
274 logging.info('Successfully uploaded change(s) %s', ' '.join(good_changes))
275
276 # Printing out the error here so that we can see exactly what failed. This
277 # is especially useful to debug without using --debug.
278 logging.error('Upload failed with %s', str(e).strip())
279 if not options.wipe:
280 logging.info('Not wiping the directory. You can inspect the failed '
Mike Frysinger9f606d72012-10-12 00:55:57 -0400281 'change at %s; After fixing the change (if trivial) you can '
Chris Sosaefc35722012-09-11 18:55:37 -0700282 'try to upload the change by running:\n'
Mike Frysinger92a41e72013-02-21 15:33:07 -0500283 'git commit -a -c CHERRY_PICK_HEAD\n'
Chris Sosaefc35722012-09-11 18:55:37 -0700284 'git push %s HEAD:refs/for/%s', work_dir, patch.project_url,
285 branch)
286 else:
287 logging.error('--nowipe not set thus deleting the work directory. If you '
288 'wish to debug this, re-run the script with change(s) '
Mike Frysinger1244fb22012-10-19 14:14:10 -0400289 '%s and --nowipe by running:\n %s %s %s --nowipe',
290 ' '.join(bad_changes), sys.argv[0], ' '.join(bad_changes),
291 branch)
Chris Sosaefc35722012-09-11 18:55:37 -0700292
293 # Suppress the stack trace if we're not debugging.
294 if options.debug:
295 raise
296 else:
297 return 1
298
299 finally:
300 if options.wipe:
301 shutil.rmtree(root_work_dir)
302
303 if options.dryrun:
Mike Frysingerb6c6fab2012-12-06 00:00:24 -0500304 logging.info('Success! To actually upload changes, re-run without '
305 '--dry-run.')
Chris Sosaefc35722012-09-11 18:55:37 -0700306 else:
307 logging.info('Successfully uploaded all changes requested.')
308
309 return 0