blob: 4692bb114353ed82ec10fe89c82084a03086225f [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 Frysinger61ef29a2014-12-17 02:05:27 -05005"""Developer helper tool for merging CLs from ToT to branches.
6
7This 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 Frysinger02e1e072013-11-10 22:11:34 -050034anch
35"""
Chris Sosaefc35722012-09-11 18:55:37 -070036
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 os
Mike Frysinger187af412012-10-27 04:27:04 -040041import re
Chris Sosaefc35722012-09-11 18:55:37 -070042import shutil
43import sys
44import tempfile
45
Don Garrett88b8d782014-05-13 17:30:55 -070046from chromite.cbuildbot import constants
47from chromite.cbuildbot import repository
Chris Sosaefc35722012-09-11 18:55:37 -070048from chromite.lib import commandline
49from chromite.lib import cros_build_lib
Ralph Nathan91874ca2015-03-19 13:29:41 -070050from chromite.lib import cros_logging as logging
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
Chris Sosaefc35722012-09-11 18:55:37 -070056def _GetParser():
57 """Returns the parser to use for this module."""
Mike Frysingerb17d7532015-06-04 01:57:29 -040058 parser = commandline.ArgumentParser(description=__doc__)
59 parser.add_argument('-d', '--draft', default=False, action='store_true',
60 help='upload a draft to Gerrit rather than a change')
61 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
62 dest='dryrun',
63 help='apply changes locally but do not upload them')
64 parser.add_argument('-e', '--email',
65 help='use this email instead of the email you would '
66 'upload changes as; required w/--nomirror')
67 parser.add_argument('--nomirror', default=True, dest='mirror',
68 action='store_false',
69 help='checkout git repo directly; requires --email')
70 parser.add_argument('--nowipe', default=True, dest='wipe',
71 action='store_false',
72 help='do not wipe the work directory after finishing')
73 parser.add_argument('change', nargs='+', help='CLs to merge')
74 parser.add_argument('branch', help='the branch to merge to')
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 Frysinger1a736a82013-12-12 01:50:59 -050088
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 Frysingera7a2f002015-04-29 23:23:19 -0400102 # Filter out tags that are added by gerrit and chromite.
103 filter_re = re.compile(
104 r'((Commit|Trybot)-Ready|Commit-Queue|(Reviewed|Submitted|Tested)-by): ')
105
Mike Frysinger4853b172013-04-04 13:03:49 -0400106 # Rewrite the commit message all the time. Latest gerrit doesn't seem
107 # to like it when you use the same ChangeId on different branches.
108 msg = []
109 for line in patch.commit_message.splitlines():
110 if line.startswith('Reviewed-on: '):
111 line = 'Previous-' + line
Mike Frysingera7a2f002015-04-29 23:23:19 -0400112 elif filter_re.match(line):
Mike Frysinger4853b172013-04-04 13:03:49 -0400113 # 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."""
Ralph Nathan5182e512015-04-03 15:37:54 -0700147 logging.notice('Setting up dir %s for uploading changes to %s', work_dir,
148 patch.project_url)
Chris Sosaefc35722012-09-11 18:55:37 -0700149
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:
Ralph Nathan5182e512015-04-03 15:37:54 -0700177 logging.notice('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,
Don Garrett0b53c2e2015-04-02 17:18:18 -0700184 remote_push_branch=git.RemoteRef('ignore', 'origin/%s' % branch))
Chris Sosaefc35722012-09-11 18:55:37 -0700185
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()
Mike Frysingerb17d7532015-06-04 01:57:29 -0400208 options = parser.parse_args(argv)
209 changes = options.change
210 branch = options.branch
Chris Sosaefc35722012-09-11 18:55:37 -0700211
Mike Frysinger8c986562014-04-02 10:09:17 -0400212 try:
213 patches = gerrit.GetGerritPatchInfo(changes)
214 except ValueError as e:
215 logging.error('Invalid patch: %s', e)
216 cros_build_lib.Die('Did you swap the branch/gerrit number?')
Chris Sosaefc35722012-09-11 18:55:37 -0700217
Ralph Nathan23a12212015-03-25 10:27:54 -0700218 # Suppress all logging info output unless we're running debug.
Chris Sosaefc35722012-09-11 18:55:37 -0700219 if not options.debug:
Ralph Nathan5182e512015-04-03 15:37:54 -0700220 logging.getLogger().setLevel(logging.NOTICE)
Chris Sosaefc35722012-09-11 18:55:37 -0700221
222 # Get a pointer to your repo checkout to look up the local project paths for
223 # both email addresses and for using your checkout as a git mirror.
224 manifest = None
225 if options.mirror:
Mike Frysinger6fc41762013-02-27 16:36:54 -0500226 try:
227 manifest = git.ManifestCheckout.Cached(constants.SOURCE_ROOT)
228 except OSError as e:
229 if e.errno == errno.ENOENT:
230 logging.error('Unable to locate ChromiumOS checkout: %s',
231 constants.SOURCE_ROOT)
232 logging.error('Did you mean to use --nomirror?')
233 return 1
234 raise
Chris Sosaefc35722012-09-11 18:55:37 -0700235 if not _ManifestContainsAllPatches(manifest, patches):
236 return 1
237 else:
238 if not options.email:
239 chromium_email = '%s@chromium.org' % os.environ['USER']
Ralph Nathan5182e512015-04-03 15:37:54 -0700240 logging.notice('--nomirror set without email, using %s', chromium_email)
Chris Sosaefc35722012-09-11 18:55:37 -0700241 options.email = chromium_email
242
243 index = 0
244 work_dir = None
245 root_work_dir = tempfile.mkdtemp(prefix='cros_merge_to_branch')
246 try:
247 for index, (change, patch) in enumerate(zip(changes, patches)):
248 # We only clone the project and set the committer the first time.
249 work_dir = os.path.join(root_work_dir, patch.project)
250 if not os.path.isdir(work_dir):
Mike Frysinger94f91be2012-10-19 16:09:06 -0400251 branch = _SetupWorkDirectoryForPatch(work_dir, patch, branch, manifest,
252 options.email)
Chris Sosaefc35722012-09-11 18:55:37 -0700253
254 # Now that we have the project checked out, let's apply our change and
255 # create a new change on Gerrit.
Ralph Nathan5182e512015-04-03 15:37:54 -0700256 logging.notice('Uploading change %s to branch %s', change, branch)
Mike Frysinger6d9d21b2013-02-20 17:25:11 -0500257 urls = _UploadChangeToBranch(work_dir, patch, branch, options.draft,
258 options.dryrun)
Ralph Nathan5182e512015-04-03 15:37:54 -0700259 logging.notice('Successfully uploaded %s to %s', change, branch)
Mike Frysinger6d9d21b2013-02-20 17:25:11 -0500260 for url in urls:
261 if url.endswith('\x1b[K'):
262 # Git will often times emit these escape sequences.
263 url = url[0:-3]
Ralph Nathan5182e512015-04-03 15:37:54 -0700264 logging.notice(' URL: %s', url)
Chris Sosaefc35722012-09-11 18:55:37 -0700265
Mike Frysinger94f91be2012-10-19 16:09:06 -0400266 except (cros_build_lib.RunCommandError, cros_patch.ApplyPatchException,
Mike Frysinger6fc41762013-02-27 16:36:54 -0500267 git.AmbiguousBranchName, OSError) as e:
Chris Sosaefc35722012-09-11 18:55:37 -0700268 # Tell the user how far we got.
269 good_changes = changes[:index]
270 bad_changes = changes[index:]
271
272 logging.warning('############## SOME CHANGES FAILED TO UPLOAD ############')
273
274 if good_changes:
Ralph Nathan5182e512015-04-03 15:37:54 -0700275 logging.notice(
276 'Successfully uploaded change(s) %s', ' '.join(good_changes))
Chris Sosaefc35722012-09-11 18:55:37 -0700277
278 # Printing out the error here so that we can see exactly what failed. This
279 # is especially useful to debug without using --debug.
280 logging.error('Upload failed with %s', str(e).strip())
281 if not options.wipe:
Ralph Nathan5182e512015-04-03 15:37:54 -0700282 logging.error('Not wiping the directory. You can inspect the failed '
283 'change at %s; After fixing the change (if trivial) you can'
284 ' try to upload the change by running:\n'
285 'git commit -a -c CHERRY_PICK_HEAD\n'
286 'git push %s HEAD:refs/for/%s', work_dir, patch.project_url,
287 branch)
Chris Sosaefc35722012-09-11 18:55:37 -0700288 else:
289 logging.error('--nowipe not set thus deleting the work directory. If you '
290 'wish to debug this, re-run the script with change(s) '
Mike Frysinger1244fb22012-10-19 14:14:10 -0400291 '%s and --nowipe by running:\n %s %s %s --nowipe',
292 ' '.join(bad_changes), sys.argv[0], ' '.join(bad_changes),
293 branch)
Chris Sosaefc35722012-09-11 18:55:37 -0700294
295 # Suppress the stack trace if we're not debugging.
296 if options.debug:
297 raise
298 else:
299 return 1
300
301 finally:
302 if options.wipe:
303 shutil.rmtree(root_work_dir)
304
305 if options.dryrun:
Ralph Nathan5182e512015-04-03 15:37:54 -0700306 logging.notice('Success! To actually upload changes, re-run without '
307 '--dry-run.')
Chris Sosaefc35722012-09-11 18:55:37 -0700308 else:
Ralph Nathan5182e512015-04-03 15:37:54 -0700309 logging.notice('Successfully uploaded all changes requested.')
Chris Sosaefc35722012-09-11 18:55:37 -0700310
311 return 0