blob: df0b813d1678a9912fac4f4293bebf303b62f77b [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 Frysingerda60bbd2013-03-27 23:57:11 -04005"""This simple program takes changes from gerrit/gerrit-int and creates new
Chris Sosaefc35722012-09-11 18:55:37 -07006changes for them on the desired branch using your gerrit/ssh credentials. To
7specify a change on gerrit-int, you must prefix the change with a *.
8
9Note that this script is best used from within an existing checkout of
10Chromium OS that already has the changes you want merged to the branch in it
11i.e. if you want to push changes to crosutils.git, you must have src/scripts
12checked out. If this isn't true e.g. you are running this script from a
13minilayout or trying to upload an internal change from a non internal checkout,
14you must specify some extra options: use the --nomirror option and use -e to
15specify your email address. This tool will then checkout the git repo fresh
16using the credentials for the -e/email you specified and upload the change. Note
17you can always use this method but it's slower than the "mirrored" method and
18requires more typing :(.
19
20Examples:
21 cros_merge_to_branch 32027 32030 32031 release-R22.2723.B
22
23This will create changes for 32027, 32030 and 32031 on the R22 branch. To look
24up the name of a branch, go into a git sub-dir and type 'git branch -a' and the
25find the branch you want to merge to. If you want to upload internal changes
26from gerrit-int, you must prefix the gerrit change number with a * e.g.
27
28 cros_merge_to_branch *26108 release-R22.2723.B
29
30For more information on how to do this yourself you can go here:
31http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/working-on-a-br\
Mike Frysinger02e1e072013-11-10 22:11:34 -050032anch
33"""
Chris Sosaefc35722012-09-11 18:55:37 -070034
Mike Frysinger1d4752b2014-11-08 04:00:18 -050035# pylint: disable=bad-continuation
36
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 logging
41import os
Mike Frysinger187af412012-10-27 04:27:04 -040042import re
Chris Sosaefc35722012-09-11 18:55:37 -070043import shutil
44import sys
45import tempfile
46
Don Garrett88b8d782014-05-13 17:30:55 -070047from chromite.cbuildbot import constants
48from chromite.cbuildbot import repository
Chris Sosaefc35722012-09-11 18:55:37 -070049from chromite.lib import commandline
50from chromite.lib import cros_build_lib
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 Frysinger4853b172013-04-04 13:03:49 -0400105 # Rewrite the commit message all the time. Latest gerrit doesn't seem
106 # to like it when you use the same ChangeId on different branches.
107 msg = []
108 for line in patch.commit_message.splitlines():
109 if line.startswith('Reviewed-on: '):
110 line = 'Previous-' + line
111 elif line.startswith('Commit-Ready: ') or \
112 line.startswith('Commit-Queue: ') or \
113 line.startswith('Reviewed-by: ') or \
114 line.startswith('Tested-by: '):
115 # If the tag is malformed, or the person lacks a name,
116 # then that's just too bad -- throw it away.
117 ele = re.split(r'[<>@]+', line)
118 if len(ele) == 4:
119 reviewers.add('@'.join(ele[-3:-1]))
120 continue
121 msg.append(line)
122 msg += [
123 '(cherry picked from commit %s)' % patch.sha1,
124 ]
125 git.RunGit(work_dir, ['commit', '--amend', '-F', '-'],
126 input='\n'.join(msg).encode('utf8'))
Mike Frysinger187af412012-10-27 04:27:04 -0400127
Mike Frysinger4853b172013-04-04 13:03:49 -0400128 # Get the new sha1 after rewriting the commit message.
129 new_sha1 = git.GetGitRepoRevision(work_dir)
Mike Frysinger187af412012-10-27 04:27:04 -0400130
Chris Sosaefc35722012-09-11 18:55:37 -0700131 # Create and use a LocalPatch to Upload the change to Gerrit.
132 local_patch = cros_patch.LocalPatch(
133 work_dir, patch.project_url, constants.PATCH_BRANCH,
134 patch.tracking_branch, patch.remote, new_sha1)
Mike Frysingerfb446bf2013-02-28 18:36:42 -0500135 for reviewers in (reviewers, ()):
136 try:
Chris Sosae5b7e002013-03-14 17:59:42 -0700137 return local_patch.Upload(
Mike Frysingerfb446bf2013-02-28 18:36:42 -0500138 patch.project_url, 'refs/%s/%s' % (upload_type, branch),
139 carbon_copy=False, dryrun=dryrun, reviewers=reviewers)
140 except cros_build_lib.RunCommandError as e:
141 if (e.result.returncode == 128 and
David James7c352bc2013-03-15 14:19:57 -0700142 re.search(r'fatal: user ".*?" not found', e.result.error)):
Mike Frysingerfb446bf2013-02-28 18:36:42 -0500143 logging.warning('Some reviewers were not found (%s); '
144 'dropping them & retrying upload', ' '.join(reviewers))
145 continue
146 raise
Chris Sosaefc35722012-09-11 18:55:37 -0700147
148
149def _SetupWorkDirectoryForPatch(work_dir, patch, branch, manifest, email):
150 """Set up local dir for uploading changes to the given patch's project."""
151 logging.info('Setting up dir %s for uploading changes to %s', work_dir,
152 patch.project_url)
153
154 # Clone the git repo from reference if we have a pointer to a
155 # ManifestCheckout object.
156 reference = None
157 if manifest:
David Jamesafa4f5f2013-11-20 14:12:55 -0800158 # Get the path to the first checkout associated with this change. Since
159 # all of the checkouts share git objects, it doesn't matter which checkout
160 # we pick.
Gaurav Shah7afb0562013-12-26 15:05:39 -0800161 path = manifest.FindCheckouts(patch.project, only_patchable=True)[0]['path']
David Jamesafa4f5f2013-11-20 14:12:55 -0800162
David Jamese3b06062013-11-09 18:52:02 -0800163 reference = os.path.join(constants.SOURCE_ROOT, path)
Mike Frysinger6fc41762013-02-27 16:36:54 -0500164 if not os.path.isdir(reference):
165 logging.error('Unable to locate git checkout: %s', reference)
166 logging.error('Did you mean to use --nomirror?')
167 # This will do an "raise OSError" with the right values.
168 os.open(reference, os.O_DIRECTORY)
Chris Sosaefc35722012-09-11 18:55:37 -0700169 # Use the email if email wasn't specified.
170 if not email:
David James97d95872012-11-16 15:09:56 -0800171 email = git.GetProjectUserEmail(reference)
Chris Sosaefc35722012-09-11 18:55:37 -0700172
173 repository.CloneGitRepo(work_dir, patch.project_url, reference=reference)
174
175 # Set the git committer.
David James97d95872012-11-16 15:09:56 -0800176 git.RunGit(work_dir, ['config', '--replace-all', 'user.email', email])
Chris Sosaefc35722012-09-11 18:55:37 -0700177
David James97d95872012-11-16 15:09:56 -0800178 mbranch = git.MatchSingleBranchName(
Mike Frysinger94f91be2012-10-19 16:09:06 -0400179 work_dir, branch, namespace='refs/remotes/origin/')
Mike Frysinger35887c62012-12-12 23:29:20 -0500180 if branch != mbranch:
181 logging.info('Auto resolved branch name "%s" to "%s"', branch, mbranch)
Mike Frysinger94f91be2012-10-19 16:09:06 -0400182 branch = mbranch
183
Chris Sosaefc35722012-09-11 18:55:37 -0700184 # Finally, create a local branch for uploading changes to the given remote
185 # branch.
David James97d95872012-11-16 15:09:56 -0800186 git.CreatePushBranch(
Chris Sosaefc35722012-09-11 18:55:37 -0700187 constants.PATCH_BRANCH, work_dir, sync=False,
188 remote_push_branch=('ignore', 'origin/%s' % branch))
189
Mike Frysinger94f91be2012-10-19 16:09:06 -0400190 return branch
191
Chris Sosaefc35722012-09-11 18:55:37 -0700192
193def _ManifestContainsAllPatches(manifest, patches):
194 """Returns true if the given manifest contains all the patches.
195
196 Args:
Gaurav Shah7afb0562013-12-26 15:05:39 -0800197 manifest: an instance of git.Manifest
198 patches: a collection of GerritPatch objects.
Chris Sosaefc35722012-09-11 18:55:37 -0700199 """
200 for patch in patches:
David Jamesafa4f5f2013-11-20 14:12:55 -0800201 if not manifest.FindCheckouts(patch.project):
Chris Sosaefc35722012-09-11 18:55:37 -0700202 logging.error('Your manifest does not have the repository %s for '
203 'change %s. Please re-run with --nomirror and '
204 '--email set', patch.project, patch.gerrit_number)
205 return False
206
207 return True
208
209
210def main(argv):
211 parser = _GetParser()
212 options, args = parser.parse_args(argv)
213
214 if len(args) < 2:
215 parser.error('Not enough arguments specified')
216
217 changes = args[0:-1]
Mike Frysinger8c986562014-04-02 10:09:17 -0400218 try:
219 patches = gerrit.GetGerritPatchInfo(changes)
220 except ValueError as e:
221 logging.error('Invalid patch: %s', e)
222 cros_build_lib.Die('Did you swap the branch/gerrit number?')
Chris Sosaefc35722012-09-11 18:55:37 -0700223 branch = args[-1]
224
225 # Suppress all cros_build_lib info output unless we're running debug.
226 if not options.debug:
227 cros_build_lib.logger.setLevel(logging.ERROR)
228
229 # Get a pointer to your repo checkout to look up the local project paths for
230 # both email addresses and for using your checkout as a git mirror.
231 manifest = None
232 if options.mirror:
Mike Frysinger6fc41762013-02-27 16:36:54 -0500233 try:
234 manifest = git.ManifestCheckout.Cached(constants.SOURCE_ROOT)
235 except OSError as e:
236 if e.errno == errno.ENOENT:
237 logging.error('Unable to locate ChromiumOS checkout: %s',
238 constants.SOURCE_ROOT)
239 logging.error('Did you mean to use --nomirror?')
240 return 1
241 raise
Chris Sosaefc35722012-09-11 18:55:37 -0700242 if not _ManifestContainsAllPatches(manifest, patches):
243 return 1
244 else:
245 if not options.email:
246 chromium_email = '%s@chromium.org' % os.environ['USER']
247 logging.info('--nomirror set without email, using %s', chromium_email)
248 options.email = chromium_email
249
250 index = 0
251 work_dir = None
252 root_work_dir = tempfile.mkdtemp(prefix='cros_merge_to_branch')
253 try:
254 for index, (change, patch) in enumerate(zip(changes, patches)):
255 # We only clone the project and set the committer the first time.
256 work_dir = os.path.join(root_work_dir, patch.project)
257 if not os.path.isdir(work_dir):
Mike Frysinger94f91be2012-10-19 16:09:06 -0400258 branch = _SetupWorkDirectoryForPatch(work_dir, patch, branch, manifest,
259 options.email)
Chris Sosaefc35722012-09-11 18:55:37 -0700260
261 # Now that we have the project checked out, let's apply our change and
262 # create a new change on Gerrit.
263 logging.info('Uploading change %s to branch %s', change, branch)
Mike Frysinger6d9d21b2013-02-20 17:25:11 -0500264 urls = _UploadChangeToBranch(work_dir, patch, branch, options.draft,
265 options.dryrun)
Chris Sosaefc35722012-09-11 18:55:37 -0700266 logging.info('Successfully uploaded %s to %s', change, branch)
Mike Frysinger6d9d21b2013-02-20 17:25:11 -0500267 for url in urls:
268 if url.endswith('\x1b[K'):
269 # Git will often times emit these escape sequences.
270 url = url[0:-3]
Mike Frysinger075e6592012-10-27 04:41:09 -0400271 logging.info(' URL: %s', url)
Chris Sosaefc35722012-09-11 18:55:37 -0700272
Mike Frysinger94f91be2012-10-19 16:09:06 -0400273 except (cros_build_lib.RunCommandError, cros_patch.ApplyPatchException,
Mike Frysinger6fc41762013-02-27 16:36:54 -0500274 git.AmbiguousBranchName, OSError) as e:
Chris Sosaefc35722012-09-11 18:55:37 -0700275 # Tell the user how far we got.
276 good_changes = changes[:index]
277 bad_changes = changes[index:]
278
279 logging.warning('############## SOME CHANGES FAILED TO UPLOAD ############')
280
281 if good_changes:
282 logging.info('Successfully uploaded change(s) %s', ' '.join(good_changes))
283
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:
288 logging.info('Not wiping the directory. You can inspect the failed '
Mike Frysinger9f606d72012-10-12 00:55:57 -0400289 'change at %s; After fixing the change (if trivial) you can '
Chris Sosaefc35722012-09-11 18:55:37 -0700290 'try to upload the change by running:\n'
Mike Frysinger92a41e72013-02-21 15:33:07 -0500291 'git commit -a -c CHERRY_PICK_HEAD\n'
Chris Sosaefc35722012-09-11 18:55:37 -0700292 'git push %s HEAD:refs/for/%s', work_dir, patch.project_url,
293 branch)
294 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:
Mike Frysingerb6c6fab2012-12-06 00:00:24 -0500312 logging.info('Success! To actually upload changes, re-run without '
313 '--dry-run.')
Chris Sosaefc35722012-09-11 18:55:37 -0700314 else:
315 logging.info('Successfully uploaded all changes requested.')
316
317 return 0