blob: 8e52b80f69d3a7df26832c74a5082d015758fddf [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:
Mike Frysingerdcad4e02018-08-03 16:20:02 -040033https://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 Frysinger6fc41762013-02-27 16:36:54 -050037import errno
Chris McDonald59650c32021-07-20 15:29:28 -060038import logging
Chris Sosaefc35722012-09-11 18:55:37 -070039import os
Mike Frysinger187af412012-10-27 04:27:04 -040040import re
Chris Sosaefc35722012-09-11 18:55:37 -070041import shutil
42import sys
43import tempfile
44
Chris Sosaefc35722012-09-11 18:55:37 -070045from chromite.lib import commandline
Chris McDonald59650c32021-07-20 15:29:28 -060046from chromite.lib import constants
Chris Sosaefc35722012-09-11 18:55:37 -070047from 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
Chris Sosaefc35722012-09-11 18:55:37 -070053def _GetParser():
54 """Returns the parser to use for this module."""
Mike Frysingerb17d7532015-06-04 01:57:29 -040055 parser = commandline.ArgumentParser(description=__doc__)
56 parser.add_argument('-d', '--draft', default=False, action='store_true',
57 help='upload a draft to Gerrit rather than a change')
58 parser.add_argument('-n', '--dry-run', default=False, action='store_true',
59 dest='dryrun',
60 help='apply changes locally but do not upload them')
61 parser.add_argument('-e', '--email',
62 help='use this email instead of the email you would '
63 'upload changes as; required w/--nomirror')
64 parser.add_argument('--nomirror', default=True, dest='mirror',
65 action='store_false',
66 help='checkout git repo directly; requires --email')
67 parser.add_argument('--nowipe', default=True, dest='wipe',
68 action='store_false',
69 help='do not wipe the work directory after finishing')
70 parser.add_argument('change', nargs='+', help='CLs to merge')
71 parser.add_argument('branch', help='the branch to merge to')
Chris Sosaefc35722012-09-11 18:55:37 -070072 return parser
73
74
75def _UploadChangeToBranch(work_dir, patch, branch, draft, dryrun):
76 """Creates a new change from GerritPatch |patch| to |branch| from |work_dir|.
77
78 Args:
79 patch: Instance of GerritPatch to upload.
80 branch: Branch to upload to.
81 work_dir: Local directory where repository is checked out in.
82 draft: If True, upload to refs/draft/|branch| rather than refs/for/|branch|.
83 dryrun: Don't actually upload a change but go through all the steps up to
Mike Frysingerb6c6fab2012-12-06 00:00:24 -050084 and including git push --dry-run.
Mike Frysinger1a736a82013-12-12 01:50:59 -050085
Mike Frysinger6d9d21b2013-02-20 17:25:11 -050086 Returns:
87 A list of all the gerrit URLs found.
Chris Sosaefc35722012-09-11 18:55:37 -070088 """
89 upload_type = 'drafts' if draft else 'for'
Mike Frysinger187af412012-10-27 04:27:04 -040090 # Download & setup the patch if need be.
91 patch.Fetch(work_dir)
Chris Sosaefc35722012-09-11 18:55:37 -070092 # Apply the actual change.
93 patch.CherryPick(work_dir, inflight=True, leave_dirty=True)
94
95 # Get the new sha1 after apply.
David James97d95872012-11-16 15:09:56 -080096 new_sha1 = git.GetGitRepoRevision(work_dir)
Chris Sosae02aa892013-01-14 10:59:08 -080097 reviewers = set()
Chris Sosaefc35722012-09-11 18:55:37 -070098
Mike Frysingera7a2f002015-04-29 23:23:19 -040099 # Filter out tags that are added by gerrit and chromite.
100 filter_re = re.compile(
101 r'((Commit|Trybot)-Ready|Commit-Queue|(Reviewed|Submitted|Tested)-by): ')
102
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
Mike Frysingera7a2f002015-04-29 23:23:19 -0400109 elif filter_re.match(line):
Mike Frysinger4853b172013-04-04 13:03:49 -0400110 # If the tag is malformed, or the person lacks a name,
111 # then that's just too bad -- throw it away.
112 ele = re.split(r'[<>@]+', line)
113 if len(ele) == 4:
114 reviewers.add('@'.join(ele[-3:-1]))
115 continue
116 msg.append(line)
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500117 msg += ['(cherry picked from commit %s)' % patch.sha1]
Mike Frysinger4853b172013-04-04 13:03:49 -0400118 git.RunGit(work_dir, ['commit', '--amend', '-F', '-'],
119 input='\n'.join(msg).encode('utf8'))
Mike Frysinger187af412012-10-27 04:27:04 -0400120
Mike Frysinger4853b172013-04-04 13:03:49 -0400121 # Get the new sha1 after rewriting the commit message.
122 new_sha1 = git.GetGitRepoRevision(work_dir)
Mike Frysinger187af412012-10-27 04:27:04 -0400123
Chris Sosaefc35722012-09-11 18:55:37 -0700124 # Create and use a LocalPatch to Upload the change to Gerrit.
125 local_patch = cros_patch.LocalPatch(
126 work_dir, patch.project_url, constants.PATCH_BRANCH,
127 patch.tracking_branch, patch.remote, new_sha1)
Mike Frysingerfb446bf2013-02-28 18:36:42 -0500128 for reviewers in (reviewers, ()):
129 try:
Chris Sosae5b7e002013-03-14 17:59:42 -0700130 return local_patch.Upload(
Mike Frysingerfb446bf2013-02-28 18:36:42 -0500131 patch.project_url, 'refs/%s/%s' % (upload_type, branch),
132 carbon_copy=False, dryrun=dryrun, reviewers=reviewers)
133 except cros_build_lib.RunCommandError as e:
134 if (e.result.returncode == 128 and
David James7c352bc2013-03-15 14:19:57 -0700135 re.search(r'fatal: user ".*?" not found', e.result.error)):
Mike Frysingerfb446bf2013-02-28 18:36:42 -0500136 logging.warning('Some reviewers were not found (%s); '
137 'dropping them & retrying upload', ' '.join(reviewers))
138 continue
139 raise
Chris Sosaefc35722012-09-11 18:55:37 -0700140
141
142def _SetupWorkDirectoryForPatch(work_dir, patch, branch, manifest, email):
143 """Set up local dir for uploading changes to the given patch's project."""
Ralph Nathan5182e512015-04-03 15:37:54 -0700144 logging.notice('Setting up dir %s for uploading changes to %s', work_dir,
145 patch.project_url)
Chris Sosaefc35722012-09-11 18:55:37 -0700146
147 # Clone the git repo from reference if we have a pointer to a
148 # ManifestCheckout object.
149 reference = None
150 if manifest:
David Jamesafa4f5f2013-11-20 14:12:55 -0800151 # Get the path to the first checkout associated with this change. Since
152 # all of the checkouts share git objects, it doesn't matter which checkout
153 # we pick.
David Jamese6301e02016-11-03 17:14:03 -0700154 path = manifest.FindCheckouts(patch.project)[0]['path']
David Jamesafa4f5f2013-11-20 14:12:55 -0800155
David Jamese3b06062013-11-09 18:52:02 -0800156 reference = os.path.join(constants.SOURCE_ROOT, path)
Mike Frysinger6fc41762013-02-27 16:36:54 -0500157 if not os.path.isdir(reference):
158 logging.error('Unable to locate git checkout: %s', reference)
159 logging.error('Did you mean to use --nomirror?')
160 # This will do an "raise OSError" with the right values.
161 os.open(reference, os.O_DIRECTORY)
Chris Sosaefc35722012-09-11 18:55:37 -0700162 # Use the email if email wasn't specified.
163 if not email:
David James97d95872012-11-16 15:09:56 -0800164 email = git.GetProjectUserEmail(reference)
Chris Sosaefc35722012-09-11 18:55:37 -0700165
Lann Martine71a8482018-08-16 14:26:22 -0600166 git.Clone(work_dir, patch.project_url, reference=reference)
Chris Sosaefc35722012-09-11 18:55:37 -0700167
168 # Set the git committer.
David James97d95872012-11-16 15:09:56 -0800169 git.RunGit(work_dir, ['config', '--replace-all', 'user.email', email])
Chris Sosaefc35722012-09-11 18:55:37 -0700170
David James97d95872012-11-16 15:09:56 -0800171 mbranch = git.MatchSingleBranchName(
Mike Frysinger94f91be2012-10-19 16:09:06 -0400172 work_dir, branch, namespace='refs/remotes/origin/')
Mike Frysinger35887c62012-12-12 23:29:20 -0500173 if branch != mbranch:
Ralph Nathan5182e512015-04-03 15:37:54 -0700174 logging.notice('Auto resolved branch name "%s" to "%s"', branch, mbranch)
Mike Frysinger94f91be2012-10-19 16:09:06 -0400175 branch = mbranch
176
Chris Sosaefc35722012-09-11 18:55:37 -0700177 # Finally, create a local branch for uploading changes to the given remote
178 # branch.
David James97d95872012-11-16 15:09:56 -0800179 git.CreatePushBranch(
Chris Sosaefc35722012-09-11 18:55:37 -0700180 constants.PATCH_BRANCH, work_dir, sync=False,
Don Garrett0b53c2e2015-04-02 17:18:18 -0700181 remote_push_branch=git.RemoteRef('ignore', 'origin/%s' % branch))
Chris Sosaefc35722012-09-11 18:55:37 -0700182
Mike Frysinger94f91be2012-10-19 16:09:06 -0400183 return branch
184
Chris Sosaefc35722012-09-11 18:55:37 -0700185
186def _ManifestContainsAllPatches(manifest, patches):
187 """Returns true if the given manifest contains all the patches.
188
189 Args:
Gaurav Shah7afb0562013-12-26 15:05:39 -0800190 manifest: an instance of git.Manifest
191 patches: a collection of GerritPatch objects.
Chris Sosaefc35722012-09-11 18:55:37 -0700192 """
193 for patch in patches:
David Jamesafa4f5f2013-11-20 14:12:55 -0800194 if not manifest.FindCheckouts(patch.project):
Chris Sosaefc35722012-09-11 18:55:37 -0700195 logging.error('Your manifest does not have the repository %s for '
196 'change %s. Please re-run with --nomirror and '
197 '--email set', patch.project, patch.gerrit_number)
198 return False
199
200 return True
201
202
203def main(argv):
204 parser = _GetParser()
Mike Frysingerb17d7532015-06-04 01:57:29 -0400205 options = parser.parse_args(argv)
206 changes = options.change
207 branch = options.branch
Chris Sosaefc35722012-09-11 18:55:37 -0700208
Mike Frysinger8c986562014-04-02 10:09:17 -0400209 try:
210 patches = gerrit.GetGerritPatchInfo(changes)
211 except ValueError as e:
212 logging.error('Invalid patch: %s', e)
213 cros_build_lib.Die('Did you swap the branch/gerrit number?')
Chris Sosaefc35722012-09-11 18:55:37 -0700214
Ralph Nathan23a12212015-03-25 10:27:54 -0700215 # Suppress all logging info output unless we're running debug.
Chris Sosaefc35722012-09-11 18:55:37 -0700216 if not options.debug:
Ralph Nathan5182e512015-04-03 15:37:54 -0700217 logging.getLogger().setLevel(logging.NOTICE)
Chris Sosaefc35722012-09-11 18:55:37 -0700218
219 # Get a pointer to your repo checkout to look up the local project paths for
220 # both email addresses and for using your checkout as a git mirror.
221 manifest = None
222 if options.mirror:
Mike Frysinger6fc41762013-02-27 16:36:54 -0500223 try:
224 manifest = git.ManifestCheckout.Cached(constants.SOURCE_ROOT)
225 except OSError as e:
226 if e.errno == errno.ENOENT:
227 logging.error('Unable to locate ChromiumOS checkout: %s',
228 constants.SOURCE_ROOT)
229 logging.error('Did you mean to use --nomirror?')
230 return 1
231 raise
Chris Sosaefc35722012-09-11 18:55:37 -0700232 if not _ManifestContainsAllPatches(manifest, patches):
233 return 1
234 else:
235 if not options.email:
236 chromium_email = '%s@chromium.org' % os.environ['USER']
Ralph Nathan5182e512015-04-03 15:37:54 -0700237 logging.notice('--nomirror set without email, using %s', chromium_email)
Chris Sosaefc35722012-09-11 18:55:37 -0700238 options.email = chromium_email
239
240 index = 0
241 work_dir = None
242 root_work_dir = tempfile.mkdtemp(prefix='cros_merge_to_branch')
243 try:
244 for index, (change, patch) in enumerate(zip(changes, patches)):
245 # We only clone the project and set the committer the first time.
246 work_dir = os.path.join(root_work_dir, patch.project)
247 if not os.path.isdir(work_dir):
Mike Frysinger94f91be2012-10-19 16:09:06 -0400248 branch = _SetupWorkDirectoryForPatch(work_dir, patch, branch, manifest,
249 options.email)
Chris Sosaefc35722012-09-11 18:55:37 -0700250
251 # Now that we have the project checked out, let's apply our change and
252 # create a new change on Gerrit.
Ralph Nathan5182e512015-04-03 15:37:54 -0700253 logging.notice('Uploading change %s to branch %s', change, branch)
Mike Frysinger6d9d21b2013-02-20 17:25:11 -0500254 urls = _UploadChangeToBranch(work_dir, patch, branch, options.draft,
255 options.dryrun)
Ralph Nathan5182e512015-04-03 15:37:54 -0700256 logging.notice('Successfully uploaded %s to %s', change, branch)
Mike Frysinger6d9d21b2013-02-20 17:25:11 -0500257 for url in urls:
258 if url.endswith('\x1b[K'):
259 # Git will often times emit these escape sequences.
260 url = url[0:-3]
Ralph Nathan5182e512015-04-03 15:37:54 -0700261 logging.notice(' URL: %s', url)
Chris Sosaefc35722012-09-11 18:55:37 -0700262
Mike Frysinger94f91be2012-10-19 16:09:06 -0400263 except (cros_build_lib.RunCommandError, cros_patch.ApplyPatchException,
Mike Frysinger6fc41762013-02-27 16:36:54 -0500264 git.AmbiguousBranchName, OSError) as e:
Chris Sosaefc35722012-09-11 18:55:37 -0700265 # Tell the user how far we got.
266 good_changes = changes[:index]
267 bad_changes = changes[index:]
268
269 logging.warning('############## SOME CHANGES FAILED TO UPLOAD ############')
270
271 if good_changes:
Ralph Nathan5182e512015-04-03 15:37:54 -0700272 logging.notice(
273 'Successfully uploaded change(s) %s', ' '.join(good_changes))
Chris Sosaefc35722012-09-11 18:55:37 -0700274
275 # Printing out the error here so that we can see exactly what failed. This
276 # is especially useful to debug without using --debug.
277 logging.error('Upload failed with %s', str(e).strip())
278 if not options.wipe:
Ralph Nathan5182e512015-04-03 15:37:54 -0700279 logging.error('Not wiping the directory. You can inspect the failed '
280 'change at %s; After fixing the change (if trivial) you can'
281 ' try to upload the change by running:\n'
282 'git commit -a -c CHERRY_PICK_HEAD\n'
283 'git push %s HEAD:refs/for/%s', work_dir, patch.project_url,
284 branch)
Chris Sosaefc35722012-09-11 18:55:37 -0700285 else:
286 logging.error('--nowipe not set thus deleting the work directory. If you '
287 'wish to debug this, re-run the script with change(s) '
Mike Frysinger1244fb22012-10-19 14:14:10 -0400288 '%s and --nowipe by running:\n %s %s %s --nowipe',
289 ' '.join(bad_changes), sys.argv[0], ' '.join(bad_changes),
290 branch)
Chris Sosaefc35722012-09-11 18:55:37 -0700291
292 # Suppress the stack trace if we're not debugging.
293 if options.debug:
294 raise
295 else:
296 return 1
297
298 finally:
299 if options.wipe:
300 shutil.rmtree(root_work_dir)
301
302 if options.dryrun:
Ralph Nathan5182e512015-04-03 15:37:54 -0700303 logging.notice('Success! To actually upload changes, re-run without '
304 '--dry-run.')
Chris Sosaefc35722012-09-11 18:55:37 -0700305 else:
Ralph Nathan5182e512015-04-03 15:37:54 -0700306 logging.notice('Successfully uploaded all changes requested.')
Chris Sosaefc35722012-09-11 18:55:37 -0700307
308 return 0