blob: aca2d02a3cae5bf859a7deae25dbb1507cc15b4b [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
7""" This simple program takes changes from gerrit/gerrit-int and creates new
8changes 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\
34anch
35"""
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
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 = """
55cros_merge_to_branch [*]change_number1 [[*]change_number2 ...] branch\n\n %s
56""" % __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',
63 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',
Chris Sosaefc35722012-09-11 18:55:37 -070066 help='Apply changes locally but do not upload them.')
67 parser.add_option('-e', '--email',
68 help='If specified, use this email instead of '
69 'the email you would upload changes as. Must be set if '
70 'nomirror is set.')
71 parser.add_option('--nomirror', default=True, dest='mirror',
72 action='store_false', help='Disable mirroring -- requires '
73 'email to be set.')
74 parser.add_option('--nowipe', default=True, dest='wipe', action='store_false',
75 help='Do not wipe the work directory after finishing.')
76 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 Frysinger187af412012-10-27 04:27:04 -0400102 # If the sha1 has changed, then rewrite the commit message.
103 if patch.sha1 != new_sha1:
104 msg = []
Mike Frysinger187af412012-10-27 04:27:04 -0400105 for line in patch.commit_message.splitlines():
106 if line.startswith('Reviewed-on: '):
107 line = 'Previous-' + line
108 elif line.startswith('Commit-Ready: ') or \
Mike Frysinger7a82c9b2013-02-20 16:59:54 -0500109 line.startswith('Commit-Queue: ') or \
Mike Frysinger187af412012-10-27 04:27:04 -0400110 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('[<>@]+', 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'))
124
125 # Get the new sha1 after rewriting the commit message.
126 new_sha1 = git.GetGitRepoRevision(work_dir)
127
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 Frysinger075e6592012-10-27 04:41:09 -0400132 return local_patch.Upload(
Chris Sosaefc35722012-09-11 18:55:37 -0700133 patch.project_url, 'refs/%s/%s' % (upload_type, branch),
Mike Frysinger187af412012-10-27 04:27:04 -0400134 carbon_copy=False, dryrun=dryrun, reviewers=reviewers)
Chris Sosaefc35722012-09-11 18:55:37 -0700135
136
137def _SetupWorkDirectoryForPatch(work_dir, patch, branch, manifest, email):
138 """Set up local dir for uploading changes to the given patch's project."""
139 logging.info('Setting up dir %s for uploading changes to %s', work_dir,
140 patch.project_url)
141
142 # Clone the git repo from reference if we have a pointer to a
143 # ManifestCheckout object.
144 reference = None
145 if manifest:
146 reference = os.path.join(constants.SOURCE_ROOT,
147 manifest.GetProjectPath(patch.project))
Mike Frysinger6fc41762013-02-27 16:36:54 -0500148 if not os.path.isdir(reference):
149 logging.error('Unable to locate git checkout: %s', reference)
150 logging.error('Did you mean to use --nomirror?')
151 # This will do an "raise OSError" with the right values.
152 os.open(reference, os.O_DIRECTORY)
Chris Sosaefc35722012-09-11 18:55:37 -0700153 # Use the email if email wasn't specified.
154 if not email:
David James97d95872012-11-16 15:09:56 -0800155 email = git.GetProjectUserEmail(reference)
Chris Sosaefc35722012-09-11 18:55:37 -0700156
157 repository.CloneGitRepo(work_dir, patch.project_url, reference=reference)
158
159 # Set the git committer.
David James97d95872012-11-16 15:09:56 -0800160 git.RunGit(work_dir, ['config', '--replace-all', 'user.email', email])
Chris Sosaefc35722012-09-11 18:55:37 -0700161
David James97d95872012-11-16 15:09:56 -0800162 mbranch = git.MatchSingleBranchName(
Mike Frysinger94f91be2012-10-19 16:09:06 -0400163 work_dir, branch, namespace='refs/remotes/origin/')
Mike Frysinger35887c62012-12-12 23:29:20 -0500164 if branch != mbranch:
165 logging.info('Auto resolved branch name "%s" to "%s"', branch, mbranch)
Mike Frysinger94f91be2012-10-19 16:09:06 -0400166 branch = mbranch
167
Chris Sosaefc35722012-09-11 18:55:37 -0700168 # Finally, create a local branch for uploading changes to the given remote
169 # branch.
David James97d95872012-11-16 15:09:56 -0800170 git.CreatePushBranch(
Chris Sosaefc35722012-09-11 18:55:37 -0700171 constants.PATCH_BRANCH, work_dir, sync=False,
172 remote_push_branch=('ignore', 'origin/%s' % branch))
173
Mike Frysinger94f91be2012-10-19 16:09:06 -0400174 return branch
175
Chris Sosaefc35722012-09-11 18:55:37 -0700176
177def _ManifestContainsAllPatches(manifest, patches):
178 """Returns true if the given manifest contains all the patches.
179
180 Args:
David James97d95872012-11-16 15:09:56 -0800181 manifest - an instance of git.Manifest
Chris Sosaefc35722012-09-11 18:55:37 -0700182 patches - a collection GerritPatch objects.
183 """
184 for patch in patches:
185 project_path = None
186 if manifest.ProjectExists(patch.project):
187 project_path = manifest.GetProjectPath(patch.project)
188
189 if not project_path:
190 logging.error('Your manifest does not have the repository %s for '
191 'change %s. Please re-run with --nomirror and '
192 '--email set', patch.project, patch.gerrit_number)
193 return False
194
195 return True
196
197
198def main(argv):
199 parser = _GetParser()
200 options, args = parser.parse_args(argv)
201
202 if len(args) < 2:
203 parser.error('Not enough arguments specified')
204
205 changes = args[0:-1]
Brian Harring511055e2012-10-10 02:58:59 -0700206 patches = gerrit.GetGerritPatchInfo(changes)
Chris Sosaefc35722012-09-11 18:55:37 -0700207 branch = args[-1]
208
209 # Suppress all cros_build_lib info output unless we're running debug.
210 if not options.debug:
211 cros_build_lib.logger.setLevel(logging.ERROR)
212
213 # Get a pointer to your repo checkout to look up the local project paths for
214 # both email addresses and for using your checkout as a git mirror.
215 manifest = None
216 if options.mirror:
Mike Frysinger6fc41762013-02-27 16:36:54 -0500217 try:
218 manifest = git.ManifestCheckout.Cached(constants.SOURCE_ROOT)
219 except OSError as e:
220 if e.errno == errno.ENOENT:
221 logging.error('Unable to locate ChromiumOS checkout: %s',
222 constants.SOURCE_ROOT)
223 logging.error('Did you mean to use --nomirror?')
224 return 1
225 raise
Chris Sosaefc35722012-09-11 18:55:37 -0700226 if not _ManifestContainsAllPatches(manifest, patches):
227 return 1
228 else:
229 if not options.email:
230 chromium_email = '%s@chromium.org' % os.environ['USER']
231 logging.info('--nomirror set without email, using %s', chromium_email)
232 options.email = chromium_email
233
234 index = 0
235 work_dir = None
236 root_work_dir = tempfile.mkdtemp(prefix='cros_merge_to_branch')
237 try:
238 for index, (change, patch) in enumerate(zip(changes, patches)):
239 # We only clone the project and set the committer the first time.
240 work_dir = os.path.join(root_work_dir, patch.project)
241 if not os.path.isdir(work_dir):
Mike Frysinger94f91be2012-10-19 16:09:06 -0400242 branch = _SetupWorkDirectoryForPatch(work_dir, patch, branch, manifest,
243 options.email)
Chris Sosaefc35722012-09-11 18:55:37 -0700244
245 # Now that we have the project checked out, let's apply our change and
246 # create a new change on Gerrit.
247 logging.info('Uploading change %s to branch %s', change, branch)
Mike Frysinger6d9d21b2013-02-20 17:25:11 -0500248 urls = _UploadChangeToBranch(work_dir, patch, branch, options.draft,
249 options.dryrun)
Chris Sosaefc35722012-09-11 18:55:37 -0700250 logging.info('Successfully uploaded %s to %s', change, branch)
Mike Frysinger6d9d21b2013-02-20 17:25:11 -0500251 for url in urls:
252 if url.endswith('\x1b[K'):
253 # Git will often times emit these escape sequences.
254 url = url[0:-3]
Mike Frysinger075e6592012-10-27 04:41:09 -0400255 logging.info(' URL: %s', url)
Chris Sosaefc35722012-09-11 18:55:37 -0700256
Mike Frysinger94f91be2012-10-19 16:09:06 -0400257 except (cros_build_lib.RunCommandError, cros_patch.ApplyPatchException,
Mike Frysinger6fc41762013-02-27 16:36:54 -0500258 git.AmbiguousBranchName, OSError) as e:
Chris Sosaefc35722012-09-11 18:55:37 -0700259 # Tell the user how far we got.
260 good_changes = changes[:index]
261 bad_changes = changes[index:]
262
263 logging.warning('############## SOME CHANGES FAILED TO UPLOAD ############')
264
265 if good_changes:
266 logging.info('Successfully uploaded change(s) %s', ' '.join(good_changes))
267
268 # Printing out the error here so that we can see exactly what failed. This
269 # is especially useful to debug without using --debug.
270 logging.error('Upload failed with %s', str(e).strip())
271 if not options.wipe:
272 logging.info('Not wiping the directory. You can inspect the failed '
Mike Frysinger9f606d72012-10-12 00:55:57 -0400273 'change at %s; After fixing the change (if trivial) you can '
Chris Sosaefc35722012-09-11 18:55:37 -0700274 'try to upload the change by running:\n'
Mike Frysinger92a41e72013-02-21 15:33:07 -0500275 'git commit -a -c CHERRY_PICK_HEAD\n'
Chris Sosaefc35722012-09-11 18:55:37 -0700276 'git push %s HEAD:refs/for/%s', work_dir, patch.project_url,
277 branch)
278 else:
279 logging.error('--nowipe not set thus deleting the work directory. If you '
280 'wish to debug this, re-run the script with change(s) '
Mike Frysinger1244fb22012-10-19 14:14:10 -0400281 '%s and --nowipe by running:\n %s %s %s --nowipe',
282 ' '.join(bad_changes), sys.argv[0], ' '.join(bad_changes),
283 branch)
Chris Sosaefc35722012-09-11 18:55:37 -0700284
285 # Suppress the stack trace if we're not debugging.
286 if options.debug:
287 raise
288 else:
289 return 1
290
291 finally:
292 if options.wipe:
293 shutil.rmtree(root_work_dir)
294
295 if options.dryrun:
Mike Frysingerb6c6fab2012-12-06 00:00:24 -0500296 logging.info('Success! To actually upload changes, re-run without '
297 '--dry-run.')
Chris Sosaefc35722012-09-11 18:55:37 -0700298 else:
299 logging.info('Successfully uploaded all changes requested.')
300
301 return 0