blob: 0085f23fa9164dba6521458b93f809ccff737d6f [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
37import 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 = """
54cros_merge_to_branch [*]change_number1 [[*]change_number2 ...] branch\n\n %s
55""" % __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',
62 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',
Chris Sosaefc35722012-09-11 18:55:37 -070065 help='Apply changes locally but do not upload them.')
66 parser.add_option('-e', '--email',
67 help='If specified, use this email instead of '
68 'the email you would upload changes as. Must be set if '
69 'nomirror is set.')
70 parser.add_option('--nomirror', default=True, dest='mirror',
71 action='store_false', help='Disable mirroring -- requires '
72 'email to be set.')
73 parser.add_option('--nowipe', default=True, dest='wipe', action='store_false',
74 help='Do not wipe the work directory after finishing.')
75 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 Frysinger187af412012-10-27 04:27:04 -0400101 # If the sha1 has changed, then rewrite the commit message.
102 if patch.sha1 != new_sha1:
103 msg = []
Mike Frysinger187af412012-10-27 04:27:04 -0400104 for line in patch.commit_message.splitlines():
105 if line.startswith('Reviewed-on: '):
106 line = 'Previous-' + line
107 elif line.startswith('Commit-Ready: ') or \
Mike Frysinger7a82c9b2013-02-20 16:59:54 -0500108 line.startswith('Commit-Queue: ') or \
Mike Frysinger187af412012-10-27 04:27:04 -0400109 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('[<>@]+', 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'))
123
124 # Get the new sha1 after rewriting the commit message.
125 new_sha1 = git.GetGitRepoRevision(work_dir)
126
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 Frysinger075e6592012-10-27 04:41:09 -0400131 return local_patch.Upload(
Chris Sosaefc35722012-09-11 18:55:37 -0700132 patch.project_url, 'refs/%s/%s' % (upload_type, branch),
Mike Frysinger187af412012-10-27 04:27:04 -0400133 carbon_copy=False, dryrun=dryrun, reviewers=reviewers)
Chris Sosaefc35722012-09-11 18:55:37 -0700134
135
136def _SetupWorkDirectoryForPatch(work_dir, patch, branch, manifest, email):
137 """Set up local dir for uploading changes to the given patch's project."""
138 logging.info('Setting up dir %s for uploading changes to %s', work_dir,
139 patch.project_url)
140
141 # Clone the git repo from reference if we have a pointer to a
142 # ManifestCheckout object.
143 reference = None
144 if manifest:
145 reference = os.path.join(constants.SOURCE_ROOT,
146 manifest.GetProjectPath(patch.project))
147 # Use the email if email wasn't specified.
148 if not email:
David James97d95872012-11-16 15:09:56 -0800149 email = git.GetProjectUserEmail(reference)
Chris Sosaefc35722012-09-11 18:55:37 -0700150
151 repository.CloneGitRepo(work_dir, patch.project_url, reference=reference)
152
153 # Set the git committer.
David James97d95872012-11-16 15:09:56 -0800154 git.RunGit(work_dir, ['config', '--replace-all', 'user.email', email])
Chris Sosaefc35722012-09-11 18:55:37 -0700155
David James97d95872012-11-16 15:09:56 -0800156 mbranch = git.MatchSingleBranchName(
Mike Frysinger94f91be2012-10-19 16:09:06 -0400157 work_dir, branch, namespace='refs/remotes/origin/')
Mike Frysinger35887c62012-12-12 23:29:20 -0500158 if branch != mbranch:
159 logging.info('Auto resolved branch name "%s" to "%s"', branch, mbranch)
Mike Frysinger94f91be2012-10-19 16:09:06 -0400160 branch = mbranch
161
Chris Sosaefc35722012-09-11 18:55:37 -0700162 # Finally, create a local branch for uploading changes to the given remote
163 # branch.
David James97d95872012-11-16 15:09:56 -0800164 git.CreatePushBranch(
Chris Sosaefc35722012-09-11 18:55:37 -0700165 constants.PATCH_BRANCH, work_dir, sync=False,
166 remote_push_branch=('ignore', 'origin/%s' % branch))
167
Mike Frysinger94f91be2012-10-19 16:09:06 -0400168 return branch
169
Chris Sosaefc35722012-09-11 18:55:37 -0700170
171def _ManifestContainsAllPatches(manifest, patches):
172 """Returns true if the given manifest contains all the patches.
173
174 Args:
David James97d95872012-11-16 15:09:56 -0800175 manifest - an instance of git.Manifest
Chris Sosaefc35722012-09-11 18:55:37 -0700176 patches - a collection GerritPatch objects.
177 """
178 for patch in patches:
179 project_path = None
180 if manifest.ProjectExists(patch.project):
181 project_path = manifest.GetProjectPath(patch.project)
182
183 if not project_path:
184 logging.error('Your manifest does not have the repository %s for '
185 'change %s. Please re-run with --nomirror and '
186 '--email set', patch.project, patch.gerrit_number)
187 return False
188
189 return True
190
191
192def main(argv):
193 parser = _GetParser()
194 options, args = parser.parse_args(argv)
195
196 if len(args) < 2:
197 parser.error('Not enough arguments specified')
198
199 changes = args[0:-1]
Brian Harring511055e2012-10-10 02:58:59 -0700200 patches = gerrit.GetGerritPatchInfo(changes)
Chris Sosaefc35722012-09-11 18:55:37 -0700201 branch = args[-1]
202
203 # Suppress all cros_build_lib info output unless we're running debug.
204 if not options.debug:
205 cros_build_lib.logger.setLevel(logging.ERROR)
206
207 # Get a pointer to your repo checkout to look up the local project paths for
208 # both email addresses and for using your checkout as a git mirror.
209 manifest = None
210 if options.mirror:
David James97d95872012-11-16 15:09:56 -0800211 manifest = git.ManifestCheckout.Cached(constants.SOURCE_ROOT)
Chris Sosaefc35722012-09-11 18:55:37 -0700212 if not _ManifestContainsAllPatches(manifest, patches):
213 return 1
214 else:
215 if not options.email:
216 chromium_email = '%s@chromium.org' % os.environ['USER']
217 logging.info('--nomirror set without email, using %s', chromium_email)
218 options.email = chromium_email
219
220 index = 0
221 work_dir = None
222 root_work_dir = tempfile.mkdtemp(prefix='cros_merge_to_branch')
223 try:
224 for index, (change, patch) in enumerate(zip(changes, patches)):
225 # We only clone the project and set the committer the first time.
226 work_dir = os.path.join(root_work_dir, patch.project)
227 if not os.path.isdir(work_dir):
Mike Frysinger94f91be2012-10-19 16:09:06 -0400228 branch = _SetupWorkDirectoryForPatch(work_dir, patch, branch, manifest,
229 options.email)
Chris Sosaefc35722012-09-11 18:55:37 -0700230
231 # Now that we have the project checked out, let's apply our change and
232 # create a new change on Gerrit.
233 logging.info('Uploading change %s to branch %s', change, branch)
Mike Frysinger6d9d21b2013-02-20 17:25:11 -0500234 urls = _UploadChangeToBranch(work_dir, patch, branch, options.draft,
235 options.dryrun)
Chris Sosaefc35722012-09-11 18:55:37 -0700236 logging.info('Successfully uploaded %s to %s', change, branch)
Mike Frysinger6d9d21b2013-02-20 17:25:11 -0500237 for url in urls:
238 if url.endswith('\x1b[K'):
239 # Git will often times emit these escape sequences.
240 url = url[0:-3]
Mike Frysinger075e6592012-10-27 04:41:09 -0400241 logging.info(' URL: %s', url)
Chris Sosaefc35722012-09-11 18:55:37 -0700242
Mike Frysinger94f91be2012-10-19 16:09:06 -0400243 except (cros_build_lib.RunCommandError, cros_patch.ApplyPatchException,
David James97d95872012-11-16 15:09:56 -0800244 git.AmbiguousBranchName) as e:
Chris Sosaefc35722012-09-11 18:55:37 -0700245 # Tell the user how far we got.
246 good_changes = changes[:index]
247 bad_changes = changes[index:]
248
249 logging.warning('############## SOME CHANGES FAILED TO UPLOAD ############')
250
251 if good_changes:
252 logging.info('Successfully uploaded change(s) %s', ' '.join(good_changes))
253
254 # Printing out the error here so that we can see exactly what failed. This
255 # is especially useful to debug without using --debug.
256 logging.error('Upload failed with %s', str(e).strip())
257 if not options.wipe:
258 logging.info('Not wiping the directory. You can inspect the failed '
Mike Frysinger9f606d72012-10-12 00:55:57 -0400259 'change at %s; After fixing the change (if trivial) you can '
Chris Sosaefc35722012-09-11 18:55:37 -0700260 'try to upload the change by running:\n'
Mike Frysinger92a41e72013-02-21 15:33:07 -0500261 'git commit -a -c CHERRY_PICK_HEAD\n'
Chris Sosaefc35722012-09-11 18:55:37 -0700262 'git push %s HEAD:refs/for/%s', work_dir, patch.project_url,
263 branch)
264 else:
265 logging.error('--nowipe not set thus deleting the work directory. If you '
266 'wish to debug this, re-run the script with change(s) '
Mike Frysinger1244fb22012-10-19 14:14:10 -0400267 '%s and --nowipe by running:\n %s %s %s --nowipe',
268 ' '.join(bad_changes), sys.argv[0], ' '.join(bad_changes),
269 branch)
Chris Sosaefc35722012-09-11 18:55:37 -0700270
271 # Suppress the stack trace if we're not debugging.
272 if options.debug:
273 raise
274 else:
275 return 1
276
277 finally:
278 if options.wipe:
279 shutil.rmtree(root_work_dir)
280
281 if options.dryrun:
Mike Frysingerb6c6fab2012-12-06 00:00:24 -0500282 logging.info('Success! To actually upload changes, re-run without '
283 '--dry-run.')
Chris Sosaefc35722012-09-11 18:55:37 -0700284 else:
285 logging.info('Successfully uploaded all changes requested.')
286
287 return 0