1#!/usr/bin/python
2#
3# Copyright (C) 2012 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#      http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""Merge master-chromium to master within the Android tree."""
18
19import logging
20import optparse
21import os
22import re
23import shutil
24import sys
25
26import merge_common
27
28
29AUTOGEN_MESSAGE = 'This commit was generated by merge_to_master.py.'
30
31
32def _MergeProjects(svn_revision, target):
33  """Merges the Chromium projects from master-chromium to target.
34
35  The larger projects' histories are flattened in the process.
36
37  Args:
38    svn_revision: The SVN revision for the main Chromium repository
39  """
40  for path in merge_common.PROJECTS_WITH_FLAT_HISTORY:
41    dest_dir = os.path.join(merge_common.REPOSITORY_ROOT, path)
42    merge_common.GetCommandStdout(['git', 'remote', 'update',
43                                   'goog', 'history'], cwd=dest_dir)
44    merge_common.GetCommandStdout(['git', 'checkout',
45                                   '-b', 'merge-to-' + target,
46                                   '-t', 'goog/' + target], cwd=dest_dir)
47    merge_common.GetCommandStdout(['git', 'fetch', 'history',
48                                   'refs/archive/chromium-%s' % svn_revision],
49                                  cwd=dest_dir)
50    merge_sha1 = merge_common.GetCommandStdout(['git', 'rev-parse',
51                                                'FETCH_HEAD'],
52                                               cwd=dest_dir).strip()
53    old_sha1 = merge_common.GetCommandStdout(['git', 'rev-parse', 'HEAD'],
54                                             cwd=dest_dir).strip()
55    # Make the previous merges into grafts so we can do a correct merge.
56    merge_log = os.path.join(dest_dir, '.merged-revisions')
57    if os.path.exists(merge_log):
58      shutil.copyfile(merge_log,
59                      os.path.join(dest_dir, '.git', 'info', 'grafts'))
60    if merge_common.GetCommandStdout(['git', 'rev-list', '-1',
61                                      'HEAD..' + merge_sha1], cwd=dest_dir):
62      logging.debug('Merging project %s ...', path)
63      # Merge conflicts cause 'git merge' to return 1, so ignore errors
64      merge_common.GetCommandStdout(['git', 'merge', '--no-commit', '--squash',
65                                     merge_sha1],
66                                    cwd=dest_dir, ignore_errors=True)
67      dirs_to_prune = merge_common.PRUNE_WHEN_FLATTENING.get(path, [])
68      if dirs_to_prune:
69        merge_common.GetCommandStdout(['git', 'rm', '--ignore-unmatch', '-rf'] +
70                                      dirs_to_prune, cwd=dest_dir)
71      merge_common.CheckNoConflictsAndCommitMerge(
72          'Merge from Chromium at DEPS revision %s\n\n%s' %
73          (svn_revision, AUTOGEN_MESSAGE), cwd=dest_dir)
74      new_sha1 = merge_common.GetCommandStdout(['git', 'rev-parse', 'HEAD'],
75                                               cwd=dest_dir).strip()
76      with open(merge_log, 'a+') as f:
77        f.write('%s %s %s\n' % (new_sha1, old_sha1, merge_sha1))
78      merge_common.GetCommandStdout(['git', 'add', '.merged-revisions'],
79                                    cwd=dest_dir)
80      merge_common.GetCommandStdout(
81          ['git', 'commit', '-m',
82           'Record Chromium merge at DEPS revision %s\n\n%s' %
83           (svn_revision, AUTOGEN_MESSAGE)], cwd=dest_dir)
84    else:
85      logging.debug('No new commits to merge in project %s', path)
86
87  for path in merge_common.PROJECTS_WITH_FULL_HISTORY:
88    dest_dir = os.path.join(merge_common.REPOSITORY_ROOT, path)
89    merge_common.GetCommandStdout(['git', 'remote', 'update', 'goog'],
90                                  cwd=dest_dir)
91    merge_common.GetCommandStdout(['git', 'checkout',
92                                   '-b', 'merge-to-' + target,
93                                   '-t', 'goog/' + target], cwd=dest_dir)
94    merge_common.GetCommandStdout(['git', 'fetch', 'goog',
95                                   'refs/archive/chromium-%s' % svn_revision],
96                                  cwd=dest_dir)
97    if merge_common.GetCommandStdout(['git', 'rev-list', '-1',
98                                      'HEAD..FETCH_HEAD'],
99                                     cwd=dest_dir):
100      logging.debug('Merging project %s ...', path)
101      # Merge conflicts cause 'git merge' to return 1, so ignore errors
102      merge_common.GetCommandStdout(['git', 'merge', '--no-commit', '--no-ff',
103                                     'FETCH_HEAD'],
104                                    cwd=dest_dir, ignore_errors=True)
105      merge_common.CheckNoConflictsAndCommitMerge(
106          'Merge from Chromium at DEPS revision %s\n\n%s' %
107          (svn_revision, AUTOGEN_MESSAGE), cwd=dest_dir)
108    else:
109      logging.debug('No new commits to merge in project %s', path)
110
111
112def _GetSVNRevision(commitish='history/master-chromium'):
113  logging.debug('Getting SVN revision ...')
114  commit = merge_common.GetCommandStdout([
115      'git', 'log', '-n1', '--grep=git-svn-id:', '--format=%H%n%b', commitish])
116  svn_revision = re.search(r'^git-svn-id: .*@([0-9]+)', commit,
117                           flags=re.MULTILINE).group(1)
118  return svn_revision
119
120
121def _MergeWithRepoProp(repo_prop_file, target):
122  chromium_sha = None
123  webview_sha = None
124  with open(repo_prop_file) as prop:
125    for line in prop:
126      project, sha = line.split()
127      if project == 'platform/external/chromium_org-history':
128        chromium_sha = sha
129      elif project == 'platform/frameworks/webview':
130        webview_sha = sha
131  if not chromium_sha or not webview_sha:
132    logging.error('SHA1s for projects not found; invalid build.prop?')
133    return 1
134  chromium_revision = _GetSVNRevision(chromium_sha)
135  logging.info('Merging Chromium at r%s and WebView at %s', chromium_revision,
136               webview_sha)
137  _MergeProjects(chromium_revision, target)
138
139  dest_dir = os.path.join(os.environ['ANDROID_BUILD_TOP'], 'frameworks/webview')
140  merge_common.GetCommandStdout(['git', 'remote', 'update', 'goog'],
141                                cwd=dest_dir)
142  merge_common.GetCommandStdout(['git', 'checkout',
143                                 '-b', 'merge-to-' + target,
144                                 '-t', 'goog/' + target], cwd=dest_dir)
145  if merge_common.GetCommandStdout(['git', 'rev-list', '-1',
146                                    'HEAD..' + webview_sha], cwd=dest_dir):
147    logging.debug('Creating merge for framework...')
148    # Merge conflicts cause 'git merge' to return 1, so ignore errors
149    merge_common.GetCommandStdout(['git', 'merge', '--no-commit', '--no-ff',
150                                   webview_sha], cwd=dest_dir,
151                                  ignore_errors=True)
152    merge_common.CheckNoConflictsAndCommitMerge(
153        'Merge master-chromium into %s at r%s\n\n%s' %
154        (target, chromium_revision, AUTOGEN_MESSAGE), cwd=dest_dir)
155    upload = merge_common.GetCommandStdout(['git', 'push', 'goog',
156                                            'HEAD:refs/for/' + target],
157                                           cwd=dest_dir)
158    logging.info(upload)
159  else:
160    logging.debug('No new commits to merge in framework')
161  return 0
162
163
164def Push(target):
165  """Push the finished snapshot to the Android repository."""
166  logging.debug('Pushing to server ...')
167  refspec = 'merge-to-%s:%s' % (target, target)
168  for path in merge_common.ALL_PROJECTS:
169    logging.debug('Pushing %s', path)
170    dest_dir = os.path.join(merge_common.REPOSITORY_ROOT, path)
171    # Delete the graft before pushing otherwise git will attempt to push all the
172    # grafted-in objects to the server as well as the ones we want.
173    graftfile = os.path.join(dest_dir, '.git', 'info', 'grafts')
174    if os.path.exists(graftfile):
175      os.remove(graftfile)
176    merge_common.GetCommandStdout(['git', 'push', 'goog', refspec],
177                                  cwd=dest_dir)
178
179
180
181def main():
182  parser = optparse.OptionParser(usage='%prog [options]')
183  parser.epilog = ('Takes the current master-chromium branch of the Chromium '
184                   'projects in Android and merges them into master to publish '
185                   'them.')
186  parser.add_option(
187      '', '--svn_revision', '--release',
188      default=None,
189      help=('Merge to the specified archived master-chromium SVN revision,'
190            'rather than using HEAD.'))
191  parser.add_option(
192      '', '--repo-prop',
193      default=None, metavar='FILE',
194      help=('Merge to the revisions specified in this repo.prop file.'))
195  parser.add_option(
196      '', '--push',
197      default=False, action='store_true',
198      help=('Push the result of a previous merge to the server.'))
199  parser.add_option(
200      '', '--target',
201      default='master', metavar='BRANCH',
202      help=('Target branch to push to. Defaults to master.'))
203  (options, args) = parser.parse_args()
204  if args:
205    parser.print_help()
206    return 1
207
208  logging.basicConfig(format='%(message)s', level=logging.DEBUG,
209                      stream=sys.stdout)
210
211  if options.push:
212    Push(options.target)
213  elif options.repo_prop:
214    return _MergeWithRepoProp(os.path.expanduser(options.repo_prop),
215                              options.target)
216  elif options.svn_revision:
217    _MergeProjects(options.svn_revision, options.target)
218  else:
219    svn_revision = _GetSVNRevision()
220    _MergeProjects(svn_revision, options.target)
221
222  return 0
223
224if __name__ == '__main__':
225  sys.exit(main())
226