blob: 0a63536a60cfff03b3b365c3ea95465ac7e555f9 [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 Frysinger383367e2014-09-16 15:06:17 -040035from __future__ import print_function
36
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
Don Garrett88b8d782014-05-13 17:30:55 -070045from chromite.cbuildbot import constants
46from chromite.cbuildbot import repository
Chris Sosaefc35722012-09-11 18:55:37 -070047from 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 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 Frysinger4853b172013-04-04 13:03:49 -0400103 # Rewrite the commit message all the time. Latest gerrit doesn't seem
104 # to like it when you use the same ChangeId on different branches.
105 msg = []
106 for line in patch.commit_message.splitlines():
107 if line.startswith('Reviewed-on: '):
108 line = 'Previous-' + line
109 elif line.startswith('Commit-Ready: ') or \
110 line.startswith('Commit-Queue: ') or \
111 line.startswith('Reviewed-by: ') or \
112 line.startswith('Tested-by: '):
113 # If the tag is malformed, or the person lacks a name,
114 # then that's just too bad -- throw it away.
115 ele = re.split(r'[<>@]+', line)
116 if len(ele) == 4:
117 reviewers.add('@'.join(ele[-3:-1]))
118 continue
119 msg.append(line)
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500120 msg += ['(cherry picked from commit %s)' % patch.sha1]
Mike Frysinger4853b172013-04-04 13:03:49 -0400121 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:
David Jamesafa4f5f2013-11-20 14:12:55 -0800154 # Get the path to the first checkout associated with this change. Since
155 # all of the checkouts share git objects, it doesn't matter which checkout
156 # we pick.
Gaurav Shah7afb0562013-12-26 15:05:39 -0800157 path = manifest.FindCheckouts(patch.project, only_patchable=True)[0]['path']
David Jamesafa4f5f2013-11-20 14:12:55 -0800158
David Jamese3b06062013-11-09 18:52:02 -0800159 reference = os.path.join(constants.SOURCE_ROOT, path)
Mike Frysinger6fc41762013-02-27 16:36:54 -0500160 if not os.path.isdir(reference):
161 logging.error('Unable to locate git checkout: %s', reference)
162 logging.error('Did you mean to use --nomirror?')
163 # This will do an "raise OSError" with the right values.
164 os.open(reference, os.O_DIRECTORY)
Chris Sosaefc35722012-09-11 18:55:37 -0700165 # Use the email if email wasn't specified.
166 if not email:
David James97d95872012-11-16 15:09:56 -0800167 email = git.GetProjectUserEmail(reference)
Chris Sosaefc35722012-09-11 18:55:37 -0700168
169 repository.CloneGitRepo(work_dir, patch.project_url, reference=reference)
170
171 # Set the git committer.
David James97d95872012-11-16 15:09:56 -0800172 git.RunGit(work_dir, ['config', '--replace-all', 'user.email', email])
Chris Sosaefc35722012-09-11 18:55:37 -0700173
David James97d95872012-11-16 15:09:56 -0800174 mbranch = git.MatchSingleBranchName(
Mike Frysinger94f91be2012-10-19 16:09:06 -0400175 work_dir, branch, namespace='refs/remotes/origin/')
Mike Frysinger35887c62012-12-12 23:29:20 -0500176 if branch != mbranch:
177 logging.info('Auto resolved branch name "%s" to "%s"', branch, mbranch)
Mike Frysinger94f91be2012-10-19 16:09:06 -0400178 branch = mbranch
179
Chris Sosaefc35722012-09-11 18:55:37 -0700180 # Finally, create a local branch for uploading changes to the given remote
181 # branch.
David James97d95872012-11-16 15:09:56 -0800182 git.CreatePushBranch(
Chris Sosaefc35722012-09-11 18:55:37 -0700183 constants.PATCH_BRANCH, work_dir, sync=False,
184 remote_push_branch=('ignore', 'origin/%s' % branch))
185
Mike Frysinger94f91be2012-10-19 16:09:06 -0400186 return branch
187
Chris Sosaefc35722012-09-11 18:55:37 -0700188
189def _ManifestContainsAllPatches(manifest, patches):
190 """Returns true if the given manifest contains all the patches.
191
192 Args:
Gaurav Shah7afb0562013-12-26 15:05:39 -0800193 manifest: an instance of git.Manifest
194 patches: a collection of GerritPatch objects.
Chris Sosaefc35722012-09-11 18:55:37 -0700195 """
196 for patch in patches:
David Jamesafa4f5f2013-11-20 14:12:55 -0800197 if not manifest.FindCheckouts(patch.project):
Chris Sosaefc35722012-09-11 18:55:37 -0700198 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]
Mike Frysinger8c986562014-04-02 10:09:17 -0400214 try:
215 patches = gerrit.GetGerritPatchInfo(changes)
216 except ValueError as e:
217 logging.error('Invalid patch: %s', e)
218 cros_build_lib.Die('Did you swap the branch/gerrit number?')
Chris Sosaefc35722012-09-11 18:55:37 -0700219 branch = args[-1]
220
221 # Suppress all cros_build_lib info output unless we're running debug.
222 if not options.debug:
223 cros_build_lib.logger.setLevel(logging.ERROR)
224
225 # Get a pointer to your repo checkout to look up the local project paths for
226 # both email addresses and for using your checkout as a git mirror.
227 manifest = None
228 if options.mirror:
Mike Frysinger6fc41762013-02-27 16:36:54 -0500229 try:
230 manifest = git.ManifestCheckout.Cached(constants.SOURCE_ROOT)
231 except OSError as e:
232 if e.errno == errno.ENOENT:
233 logging.error('Unable to locate ChromiumOS checkout: %s',
234 constants.SOURCE_ROOT)
235 logging.error('Did you mean to use --nomirror?')
236 return 1
237 raise
Chris Sosaefc35722012-09-11 18:55:37 -0700238 if not _ManifestContainsAllPatches(manifest, patches):
239 return 1
240 else:
241 if not options.email:
242 chromium_email = '%s@chromium.org' % os.environ['USER']
243 logging.info('--nomirror set without email, using %s', chromium_email)
244 options.email = chromium_email
245
246 index = 0
247 work_dir = None
248 root_work_dir = tempfile.mkdtemp(prefix='cros_merge_to_branch')
249 try:
250 for index, (change, patch) in enumerate(zip(changes, patches)):
251 # We only clone the project and set the committer the first time.
252 work_dir = os.path.join(root_work_dir, patch.project)
253 if not os.path.isdir(work_dir):
Mike Frysinger94f91be2012-10-19 16:09:06 -0400254 branch = _SetupWorkDirectoryForPatch(work_dir, patch, branch, manifest,
255 options.email)
Chris Sosaefc35722012-09-11 18:55:37 -0700256
257 # Now that we have the project checked out, let's apply our change and
258 # create a new change on Gerrit.
259 logging.info('Uploading change %s to branch %s', change, branch)
Mike Frysinger6d9d21b2013-02-20 17:25:11 -0500260 urls = _UploadChangeToBranch(work_dir, patch, branch, options.draft,
261 options.dryrun)
Chris Sosaefc35722012-09-11 18:55:37 -0700262 logging.info('Successfully uploaded %s to %s', change, branch)
Mike Frysinger6d9d21b2013-02-20 17:25:11 -0500263 for url in urls:
264 if url.endswith('\x1b[K'):
265 # Git will often times emit these escape sequences.
266 url = url[0:-3]
Mike Frysinger075e6592012-10-27 04:41:09 -0400267 logging.info(' URL: %s', url)
Chris Sosaefc35722012-09-11 18:55:37 -0700268
Mike Frysinger94f91be2012-10-19 16:09:06 -0400269 except (cros_build_lib.RunCommandError, cros_patch.ApplyPatchException,
Mike Frysinger6fc41762013-02-27 16:36:54 -0500270 git.AmbiguousBranchName, OSError) as e:
Chris Sosaefc35722012-09-11 18:55:37 -0700271 # Tell the user how far we got.
272 good_changes = changes[:index]
273 bad_changes = changes[index:]
274
275 logging.warning('############## SOME CHANGES FAILED TO UPLOAD ############')
276
277 if good_changes:
278 logging.info('Successfully uploaded change(s) %s', ' '.join(good_changes))
279
280 # Printing out the error here so that we can see exactly what failed. This
281 # is especially useful to debug without using --debug.
282 logging.error('Upload failed with %s', str(e).strip())
283 if not options.wipe:
284 logging.info('Not wiping the directory. You can inspect the failed '
Mike Frysinger9f606d72012-10-12 00:55:57 -0400285 'change at %s; After fixing the change (if trivial) you can '
Chris Sosaefc35722012-09-11 18:55:37 -0700286 'try to upload the change by running:\n'
Mike Frysinger92a41e72013-02-21 15:33:07 -0500287 'git commit -a -c CHERRY_PICK_HEAD\n'
Chris Sosaefc35722012-09-11 18:55:37 -0700288 'git push %s HEAD:refs/for/%s', work_dir, patch.project_url,
289 branch)
290 else:
291 logging.error('--nowipe not set thus deleting the work directory. If you '
292 'wish to debug this, re-run the script with change(s) '
Mike Frysinger1244fb22012-10-19 14:14:10 -0400293 '%s and --nowipe by running:\n %s %s %s --nowipe',
294 ' '.join(bad_changes), sys.argv[0], ' '.join(bad_changes),
295 branch)
Chris Sosaefc35722012-09-11 18:55:37 -0700296
297 # Suppress the stack trace if we're not debugging.
298 if options.debug:
299 raise
300 else:
301 return 1
302
303 finally:
304 if options.wipe:
305 shutil.rmtree(root_work_dir)
306
307 if options.dryrun:
Mike Frysingerb6c6fab2012-12-06 00:00:24 -0500308 logging.info('Success! To actually upload changes, re-run without '
309 '--dry-run.')
Chris Sosaefc35722012-09-11 18:55:37 -0700310 else:
311 logging.info('Successfully uploaded all changes requested.')
312
313 return 0