blob: a4ea1fafb4b63ff83ff4ab4f1e33f49053fc973b [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
39import shutil
40import sys
41import tempfile
42
43from chromite.buildbot import constants
Chris Sosaefc35722012-09-11 18:55:37 -070044from chromite.buildbot import repository
45from chromite.lib import commandline
46from chromite.lib import cros_build_lib
Brian Harring511055e2012-10-10 02:58:59 -070047from chromite.lib import gerrit
David James97d95872012-11-16 15:09:56 -080048from chromite.lib import git
Brian Harring511055e2012-10-10 02:58:59 -070049from chromite.lib import patch as cros_patch
Chris Sosaefc35722012-09-11 18:55:37 -070050
51
52_USAGE = """
53cros_merge_to_branch [*]change_number1 [[*]change_number2 ...] branch\n\n %s
54""" % __doc__
55
56
57def _GetParser():
58 """Returns the parser to use for this module."""
59 parser = commandline.OptionParser(usage=_USAGE)
60 parser.add_option('-d', '--draft', default=False, action='store_true',
61 help='Upload a draft to Gerrit rather than a change.')
Mike Frysingerb6c6fab2012-12-06 00:00:24 -050062 parser.add_option('-n', '--dry-run', default=False, action='store_true',
63 dest='dryrun',
Chris Sosaefc35722012-09-11 18:55:37 -070064 help='Apply changes locally but do not upload them.')
65 parser.add_option('-e', '--email',
66 help='If specified, use this email instead of '
67 'the email you would upload changes as. Must be set if '
68 'nomirror is set.')
69 parser.add_option('--nomirror', default=True, dest='mirror',
70 action='store_false', help='Disable mirroring -- requires '
71 'email to be set.')
72 parser.add_option('--nowipe', default=True, dest='wipe', action='store_false',
73 help='Do not wipe the work directory after finishing.')
74 return parser
75
76
77def _UploadChangeToBranch(work_dir, patch, branch, draft, dryrun):
78 """Creates a new change from GerritPatch |patch| to |branch| from |work_dir|.
79
80 Args:
81 patch: Instance of GerritPatch to upload.
82 branch: Branch to upload to.
83 work_dir: Local directory where repository is checked out in.
84 draft: If True, upload to refs/draft/|branch| rather than refs/for/|branch|.
85 dryrun: Don't actually upload a change but go through all the steps up to
Mike Frysingerb6c6fab2012-12-06 00:00:24 -050086 and including git push --dry-run.
Chris Sosaefc35722012-09-11 18:55:37 -070087 """
88 upload_type = 'drafts' if draft else 'for'
89 # Apply the actual change.
90 patch.CherryPick(work_dir, inflight=True, leave_dirty=True)
91
92 # Get the new sha1 after apply.
David James97d95872012-11-16 15:09:56 -080093 new_sha1 = git.GetGitRepoRevision(work_dir)
Chris Sosaefc35722012-09-11 18:55:37 -070094
95 # Create and use a LocalPatch to Upload the change to Gerrit.
96 local_patch = cros_patch.LocalPatch(
97 work_dir, patch.project_url, constants.PATCH_BRANCH,
98 patch.tracking_branch, patch.remote, new_sha1)
Mike Frysinger075e6592012-10-27 04:41:09 -040099 return local_patch.Upload(
Chris Sosaefc35722012-09-11 18:55:37 -0700100 patch.project_url, 'refs/%s/%s' % (upload_type, branch),
101 carbon_copy=False, dryrun=dryrun)
102
103
104def _SetupWorkDirectoryForPatch(work_dir, patch, branch, manifest, email):
105 """Set up local dir for uploading changes to the given patch's project."""
106 logging.info('Setting up dir %s for uploading changes to %s', work_dir,
107 patch.project_url)
108
109 # Clone the git repo from reference if we have a pointer to a
110 # ManifestCheckout object.
111 reference = None
112 if manifest:
113 reference = os.path.join(constants.SOURCE_ROOT,
114 manifest.GetProjectPath(patch.project))
115 # Use the email if email wasn't specified.
116 if not email:
David James97d95872012-11-16 15:09:56 -0800117 email = git.GetProjectUserEmail(reference)
Chris Sosaefc35722012-09-11 18:55:37 -0700118
119 repository.CloneGitRepo(work_dir, patch.project_url, reference=reference)
120
121 # Set the git committer.
David James97d95872012-11-16 15:09:56 -0800122 git.RunGit(work_dir, ['config', '--replace-all', 'user.email', email])
Chris Sosaefc35722012-09-11 18:55:37 -0700123
David James97d95872012-11-16 15:09:56 -0800124 mbranch = git.MatchSingleBranchName(
Mike Frysinger94f91be2012-10-19 16:09:06 -0400125 work_dir, branch, namespace='refs/remotes/origin/')
126 logging.info('Auto resolved branch name "%s" to "%s"' % (branch, mbranch))
127 branch = mbranch
128
Chris Sosaefc35722012-09-11 18:55:37 -0700129 # Finally, create a local branch for uploading changes to the given remote
130 # branch.
David James97d95872012-11-16 15:09:56 -0800131 git.CreatePushBranch(
Chris Sosaefc35722012-09-11 18:55:37 -0700132 constants.PATCH_BRANCH, work_dir, sync=False,
133 remote_push_branch=('ignore', 'origin/%s' % branch))
134
Mike Frysinger94f91be2012-10-19 16:09:06 -0400135 return branch
136
Chris Sosaefc35722012-09-11 18:55:37 -0700137
138def _ManifestContainsAllPatches(manifest, patches):
139 """Returns true if the given manifest contains all the patches.
140
141 Args:
David James97d95872012-11-16 15:09:56 -0800142 manifest - an instance of git.Manifest
Chris Sosaefc35722012-09-11 18:55:37 -0700143 patches - a collection GerritPatch objects.
144 """
145 for patch in patches:
146 project_path = None
147 if manifest.ProjectExists(patch.project):
148 project_path = manifest.GetProjectPath(patch.project)
149
150 if not project_path:
151 logging.error('Your manifest does not have the repository %s for '
152 'change %s. Please re-run with --nomirror and '
153 '--email set', patch.project, patch.gerrit_number)
154 return False
155
156 return True
157
158
159def main(argv):
160 parser = _GetParser()
161 options, args = parser.parse_args(argv)
162
163 if len(args) < 2:
164 parser.error('Not enough arguments specified')
165
166 changes = args[0:-1]
Brian Harring511055e2012-10-10 02:58:59 -0700167 patches = gerrit.GetGerritPatchInfo(changes)
Chris Sosaefc35722012-09-11 18:55:37 -0700168 branch = args[-1]
169
170 # Suppress all cros_build_lib info output unless we're running debug.
171 if not options.debug:
172 cros_build_lib.logger.setLevel(logging.ERROR)
173
174 # Get a pointer to your repo checkout to look up the local project paths for
175 # both email addresses and for using your checkout as a git mirror.
176 manifest = None
177 if options.mirror:
David James97d95872012-11-16 15:09:56 -0800178 manifest = git.ManifestCheckout.Cached(constants.SOURCE_ROOT)
Chris Sosaefc35722012-09-11 18:55:37 -0700179 if not _ManifestContainsAllPatches(manifest, patches):
180 return 1
181 else:
182 if not options.email:
183 chromium_email = '%s@chromium.org' % os.environ['USER']
184 logging.info('--nomirror set without email, using %s', chromium_email)
185 options.email = chromium_email
186
187 index = 0
188 work_dir = None
189 root_work_dir = tempfile.mkdtemp(prefix='cros_merge_to_branch')
190 try:
191 for index, (change, patch) in enumerate(zip(changes, patches)):
192 # We only clone the project and set the committer the first time.
193 work_dir = os.path.join(root_work_dir, patch.project)
194 if not os.path.isdir(work_dir):
Mike Frysinger94f91be2012-10-19 16:09:06 -0400195 branch = _SetupWorkDirectoryForPatch(work_dir, patch, branch, manifest,
196 options.email)
Chris Sosaefc35722012-09-11 18:55:37 -0700197
198 # Now that we have the project checked out, let's apply our change and
199 # create a new change on Gerrit.
200 logging.info('Uploading change %s to branch %s', change, branch)
Mike Frysinger075e6592012-10-27 04:41:09 -0400201 url = _UploadChangeToBranch(work_dir, patch, branch, options.draft,
202 options.dryrun)
Chris Sosaefc35722012-09-11 18:55:37 -0700203 logging.info('Successfully uploaded %s to %s', change, branch)
Mike Frysinger075e6592012-10-27 04:41:09 -0400204 if url:
205 logging.info(' URL: %s', url)
Chris Sosaefc35722012-09-11 18:55:37 -0700206
Mike Frysinger94f91be2012-10-19 16:09:06 -0400207 except (cros_build_lib.RunCommandError, cros_patch.ApplyPatchException,
David James97d95872012-11-16 15:09:56 -0800208 git.AmbiguousBranchName) as e:
Chris Sosaefc35722012-09-11 18:55:37 -0700209 # Tell the user how far we got.
210 good_changes = changes[:index]
211 bad_changes = changes[index:]
212
213 logging.warning('############## SOME CHANGES FAILED TO UPLOAD ############')
214
215 if good_changes:
216 logging.info('Successfully uploaded change(s) %s', ' '.join(good_changes))
217
218 # Printing out the error here so that we can see exactly what failed. This
219 # is especially useful to debug without using --debug.
220 logging.error('Upload failed with %s', str(e).strip())
221 if not options.wipe:
222 logging.info('Not wiping the directory. You can inspect the failed '
Mike Frysinger9f606d72012-10-12 00:55:57 -0400223 'change at %s; After fixing the change (if trivial) you can '
Chris Sosaefc35722012-09-11 18:55:37 -0700224 'try to upload the change by running:\n'
225 'git push %s HEAD:refs/for/%s', work_dir, patch.project_url,
226 branch)
227 else:
228 logging.error('--nowipe not set thus deleting the work directory. If you '
229 'wish to debug this, re-run the script with change(s) '
Mike Frysinger1244fb22012-10-19 14:14:10 -0400230 '%s and --nowipe by running:\n %s %s %s --nowipe',
231 ' '.join(bad_changes), sys.argv[0], ' '.join(bad_changes),
232 branch)
Chris Sosaefc35722012-09-11 18:55:37 -0700233
234 # Suppress the stack trace if we're not debugging.
235 if options.debug:
236 raise
237 else:
238 return 1
239
240 finally:
241 if options.wipe:
242 shutil.rmtree(root_work_dir)
243
244 if options.dryrun:
Mike Frysingerb6c6fab2012-12-06 00:00:24 -0500245 logging.info('Success! To actually upload changes, re-run without '
246 '--dry-run.')
Chris Sosaefc35722012-09-11 18:55:37 -0700247 else:
248 logging.info('Successfully uploaded all changes requested.')
249
250 return 0