1 # 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.
5 """This simple program takes changes from gerrit/gerrit-int and creates new
6 changes for them on the desired branch using your gerrit/ssh credentials. To
7 specify a change on gerrit-int, you must prefix the change with a *.
9 Note that this script is best used from within an existing checkout of
10 Chromium OS that already has the changes you want merged to the branch in it
11 i.e. if you want to push changes to crosutils.git, you must have src/scripts
12 checked out. If this isn't true e.g. you are running this script from a
13 minilayout or trying to upload an internal change from a non internal checkout,
14 you must specify some extra options: use the --nomirror option and use -e to
15 specify your email address. This tool will then checkout the git repo fresh
16 using the credentials for the -e/email you specified and upload the change. Note
17 you can always use this method but it's slower than the "mirrored" method and
18 requires more typing :(.
21 cros_merge_to_branch 32027 32030 32031 release-R22.2723.B
23 This will create changes for 32027, 32030 and 32031 on the R22 branch. To look
24 up the name of a branch, go into a git sub-dir and type 'git branch -a' and the
25 find the branch you want to merge to. If you want to upload internal changes
26 from gerrit-int, you must prefix the gerrit change number with a * e.g.
28 cros_merge_to_branch *26108 release-R22.2723.B
30 For more information on how to do this yourself you can go here:
31 http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/working-on-a-br\
35 from __future__ import print_function
45 from chromite.cbuildbot import constants
46 from chromite.cbuildbot import repository
47 from chromite.lib import commandline
48 from chromite.lib import cros_build_lib
49 from chromite.lib import gerrit
50 from chromite.lib import git
51 from chromite.lib import patch as cros_patch
55 cros_merge_to_branch [*]change_number1 [[*]change_number2 ...] branch\n\n%s\
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')
64 parser.add_option('-n', '--dry-run', default=False, action='store_true',
66 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 w/'
71 parser.add_option('--nomirror', default=True, dest='mirror',
72 action='store_false', help='checkout git repo directly; '
74 parser.add_option('--nowipe', default=True, dest='wipe', action='store_false',
75 help='do not wipe the work directory after finishing')
79 def _UploadChangeToBranch(work_dir, patch, branch, draft, dryrun):
80 """Creates a new change from GerritPatch |patch| to |branch| from |work_dir|.
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
88 and including git push --dry-run.
91 A list of all the gerrit URLs found.
93 upload_type = 'drafts' if draft else 'for'
94 # Download & setup the patch if need be.
96 # Apply the actual change.
97 patch.CherryPick(work_dir, inflight=True, leave_dirty=True)
99 # Get the new sha1 after apply.
100 new_sha1 = git.GetGitRepoRevision(work_dir)
103 # 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.
106 for line in patch.commit_message.splitlines():
107 if line.startswith('Reviewed-on: '):
108 line = 'Previous-' + line
109 elif line.startswith('Commit-Ready: ') or \
110 line.startswith('Commit-Queue: ') or \
111 line.startswith('Reviewed-by: ') or \
112 line.startswith('Tested-by: '):
113 # 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)
117 reviewers.add('@'.join(ele[-3:-1]))
121 '(cherry picked from commit %s)' % patch.sha1,
123 git.RunGit(work_dir, ['commit', '--amend', '-F', '-'],
124 input='\n'.join(msg).encode('utf8'))
126 # Get the new sha1 after rewriting the commit message.
127 new_sha1 = git.GetGitRepoRevision(work_dir)
129 # Create and use a LocalPatch to Upload the change to Gerrit.
130 local_patch = cros_patch.LocalPatch(
131 work_dir, patch.project_url, constants.PATCH_BRANCH,
132 patch.tracking_branch, patch.remote, new_sha1)
133 for reviewers in (reviewers, ()):
135 return local_patch.Upload(
136 patch.project_url, 'refs/%s/%s' % (upload_type, branch),
137 carbon_copy=False, dryrun=dryrun, reviewers=reviewers)
138 except cros_build_lib.RunCommandError as e:
139 if (e.result.returncode == 128 and
140 re.search(r'fatal: user ".*?" not found', e.result.error)):
141 logging.warning('Some reviewers were not found (%s); '
142 'dropping them & retrying upload', ' '.join(reviewers))
147 def _SetupWorkDirectoryForPatch(work_dir, patch, branch, manifest, email):
148 """Set up local dir for uploading changes to the given patch's project."""
149 logging.info('Setting up dir %s for uploading changes to %s', work_dir,
152 # Clone the git repo from reference if we have a pointer to a
153 # ManifestCheckout object.
156 # Get the path to the first checkout associated with this change. Since
157 # all of the checkouts share git objects, it doesn't matter which checkout
159 path = manifest.FindCheckouts(patch.project, only_patchable=True)[0]['path']
161 reference = os.path.join(constants.SOURCE_ROOT, path)
162 if not os.path.isdir(reference):
163 logging.error('Unable to locate git checkout: %s', reference)
164 logging.error('Did you mean to use --nomirror?')
165 # This will do an "raise OSError" with the right values.
166 os.open(reference, os.O_DIRECTORY)
167 # Use the email if email wasn't specified.
169 email = git.GetProjectUserEmail(reference)
171 repository.CloneGitRepo(work_dir, patch.project_url, reference=reference)
173 # Set the git committer.
174 git.RunGit(work_dir, ['config', '--replace-all', 'user.email', email])
176 mbranch = git.MatchSingleBranchName(
177 work_dir, branch, namespace='refs/remotes/origin/')
178 if branch != mbranch:
179 logging.info('Auto resolved branch name "%s" to "%s"', branch, mbranch)
182 # Finally, create a local branch for uploading changes to the given remote
184 git.CreatePushBranch(
185 constants.PATCH_BRANCH, work_dir, sync=False,
186 remote_push_branch=('ignore', 'origin/%s' % branch))
191 def _ManifestContainsAllPatches(manifest, patches):
192 """Returns true if the given manifest contains all the patches.
195 manifest: an instance of git.Manifest
196 patches: a collection of GerritPatch objects.
198 for patch in patches:
199 if not manifest.FindCheckouts(patch.project):
200 logging.error('Your manifest does not have the repository %s for '
201 'change %s. Please re-run with --nomirror and '
202 '--email set', patch.project, patch.gerrit_number)
209 parser = _GetParser()
210 options, args = parser.parse_args(argv)
213 parser.error('Not enough arguments specified')
217 patches = gerrit.GetGerritPatchInfo(changes)
218 except ValueError as e:
219 logging.error('Invalid patch: %s', e)
220 cros_build_lib.Die('Did you swap the branch/gerrit number?')
223 # Suppress all cros_build_lib info output unless we're running debug.
224 if not options.debug:
225 cros_build_lib.logger.setLevel(logging.ERROR)
227 # Get a pointer to your repo checkout to look up the local project paths for
228 # both email addresses and for using your checkout as a git mirror.
232 manifest = git.ManifestCheckout.Cached(constants.SOURCE_ROOT)
234 if e.errno == errno.ENOENT:
235 logging.error('Unable to locate ChromiumOS checkout: %s',
236 constants.SOURCE_ROOT)
237 logging.error('Did you mean to use --nomirror?')
240 if not _ManifestContainsAllPatches(manifest, patches):
243 if not options.email:
244 chromium_email = '%s@chromium.org' % os.environ['USER']
245 logging.info('--nomirror set without email, using %s', chromium_email)
246 options.email = chromium_email
250 root_work_dir = tempfile.mkdtemp(prefix='cros_merge_to_branch')
252 for index, (change, patch) in enumerate(zip(changes, patches)):
253 # We only clone the project and set the committer the first time.
254 work_dir = os.path.join(root_work_dir, patch.project)
255 if not os.path.isdir(work_dir):
256 branch = _SetupWorkDirectoryForPatch(work_dir, patch, branch, manifest,
259 # Now that we have the project checked out, let's apply our change and
260 # create a new change on Gerrit.
261 logging.info('Uploading change %s to branch %s', change, branch)
262 urls = _UploadChangeToBranch(work_dir, patch, branch, options.draft,
264 logging.info('Successfully uploaded %s to %s', change, branch)
266 if url.endswith('\x1b[K'):
267 # Git will often times emit these escape sequences.
269 logging.info(' URL: %s', url)
271 except (cros_build_lib.RunCommandError, cros_patch.ApplyPatchException,
272 git.AmbiguousBranchName, OSError) as e:
273 # Tell the user how far we got.
274 good_changes = changes[:index]
275 bad_changes = changes[index:]
277 logging.warning('############## SOME CHANGES FAILED TO UPLOAD ############')
280 logging.info('Successfully uploaded change(s) %s', ' '.join(good_changes))
282 # Printing out the error here so that we can see exactly what failed. This
283 # is especially useful to debug without using --debug.
284 logging.error('Upload failed with %s', str(e).strip())
286 logging.info('Not wiping the directory. You can inspect the failed '
287 'change at %s; After fixing the change (if trivial) you can '
288 'try to upload the change by running:\n'
289 'git commit -a -c CHERRY_PICK_HEAD\n'
290 'git push %s HEAD:refs/for/%s', work_dir, patch.project_url,
293 logging.error('--nowipe not set thus deleting the work directory. If you '
294 'wish to debug this, re-run the script with change(s) '
295 '%s and --nowipe by running:\n %s %s %s --nowipe',
296 ' '.join(bad_changes), sys.argv[0], ' '.join(bad_changes),
299 # Suppress the stack trace if we're not debugging.
307 shutil.rmtree(root_work_dir)
310 logging.info('Success! To actually upload changes, re-run without '
313 logging.info('Successfully uploaded all changes requested.')