Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / third_party / chromite / scripts / cros_merge_to_branch.py
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.
4
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 *.
8
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 :(.
19
20 Examples:
21   cros_merge_to_branch 32027 32030 32031 release-R22.2723.B
22
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.
27
28   cros_merge_to_branch *26108 release-R22.2723.B
29
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\
32 anch
33 """
34
35 from __future__ import print_function
36
37 import errno
38 import logging
39 import os
40 import re
41 import shutil
42 import sys
43 import tempfile
44
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
52
53
54 _USAGE = """
55 cros_merge_to_branch [*]change_number1 [[*]change_number2 ...] branch\n\n%s\
56 """ % __doc__
57
58
59 def _GetParser():
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',
65                     dest='dryrun',
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/'
70                     '--nomirror')
71   parser.add_option('--nomirror', default=True, dest='mirror',
72                     action='store_false', help='checkout git repo directly; '
73                     'requires --email')
74   parser.add_option('--nowipe', default=True, dest='wipe', action='store_false',
75                     help='do not wipe the work directory after finishing')
76   return parser
77
78
79 def _UploadChangeToBranch(work_dir, patch, branch, draft, dryrun):
80   """Creates a new change from GerritPatch |patch| to |branch| from |work_dir|.
81
82   Args:
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.
89
90   Returns:
91     A list of all the gerrit URLs found.
92   """
93   upload_type = 'drafts' if draft else 'for'
94   # Download & setup the patch if need be.
95   patch.Fetch(work_dir)
96   # Apply the actual change.
97   patch.CherryPick(work_dir, inflight=True, leave_dirty=True)
98
99   # Get the new sha1 after apply.
100   new_sha1 = git.GetGitRepoRevision(work_dir)
101   reviewers = set()
102
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.
105   msg = []
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)
116       if len(ele) == 4:
117         reviewers.add('@'.join(ele[-3:-1]))
118       continue
119     msg.append(line)
120   msg += [
121     '(cherry picked from commit %s)' % patch.sha1,
122   ]
123   git.RunGit(work_dir, ['commit', '--amend', '-F', '-'],
124              input='\n'.join(msg).encode('utf8'))
125
126   # Get the new sha1 after rewriting the commit message.
127   new_sha1 = git.GetGitRepoRevision(work_dir)
128
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, ()):
134     try:
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))
143         continue
144       raise
145
146
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,
150                patch.project_url)
151
152   # Clone the git repo from reference if we have a pointer to a
153   # ManifestCheckout object.
154   reference = None
155   if manifest:
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
158     # we pick.
159     path = manifest.FindCheckouts(patch.project, only_patchable=True)[0]['path']
160
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.
168     if not email:
169       email = git.GetProjectUserEmail(reference)
170
171   repository.CloneGitRepo(work_dir, patch.project_url, reference=reference)
172
173   # Set the git committer.
174   git.RunGit(work_dir, ['config', '--replace-all', 'user.email', email])
175
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)
180   branch = mbranch
181
182   # Finally, create a local branch for uploading changes to the given remote
183   # branch.
184   git.CreatePushBranch(
185       constants.PATCH_BRANCH, work_dir, sync=False,
186       remote_push_branch=('ignore', 'origin/%s' % branch))
187
188   return branch
189
190
191 def _ManifestContainsAllPatches(manifest, patches):
192   """Returns true if the given manifest contains all the patches.
193
194   Args:
195     manifest: an instance of git.Manifest
196     patches: a collection of GerritPatch objects.
197   """
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)
203       return False
204
205     return True
206
207
208 def main(argv):
209   parser = _GetParser()
210   options, args = parser.parse_args(argv)
211
212   if len(args) < 2:
213     parser.error('Not enough arguments specified')
214
215   changes = args[0:-1]
216   try:
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?')
221   branch = args[-1]
222
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)
226
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.
229   manifest = None
230   if options.mirror:
231     try:
232       manifest = git.ManifestCheckout.Cached(constants.SOURCE_ROOT)
233     except OSError as e:
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?')
238         return 1
239       raise
240     if not _ManifestContainsAllPatches(manifest, patches):
241       return 1
242   else:
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
247
248   index = 0
249   work_dir = None
250   root_work_dir = tempfile.mkdtemp(prefix='cros_merge_to_branch')
251   try:
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,
257                                              options.email)
258
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,
263                                    options.dryrun)
264       logging.info('Successfully uploaded %s to %s', change, branch)
265       for url in urls:
266         if url.endswith('\x1b[K'):
267           # Git will often times emit these escape sequences.
268           url = url[0:-3]
269         logging.info('  URL: %s', url)
270
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:]
276
277     logging.warning('############## SOME CHANGES FAILED TO UPLOAD ############')
278
279     if good_changes:
280       logging.info('Successfully uploaded change(s) %s', ' '.join(good_changes))
281
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())
285     if not options.wipe:
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,
291                    branch)
292     else:
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),
297                     branch)
298
299     # Suppress the stack trace if we're not debugging.
300     if options.debug:
301       raise
302     else:
303       return 1
304
305   finally:
306     if options.wipe:
307       shutil.rmtree(root_work_dir)
308
309   if options.dryrun:
310     logging.info('Success! To actually upload changes, re-run without '
311                  '--dry-run.')
312   else:
313     logging.info('Successfully uploaded all changes requested.')
314
315   return 0