Home | History | Annotate | Download | only in tools
      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 
     19 import logging
     20 import optparse
     21 import os
     22 import re
     23 import shutil
     24 import subprocess
     25 import sys
     26 
     27 import merge_common
     28 
     29 
     30 AUTOGEN_MESSAGE = 'This commit was generated by merge_to_master.py.'
     31 WEBVIEW_PROJECT = 'frameworks/webview'
     32 
     33 
     34 def _GetAbsPath(project):
     35   """Returns the full path to a given project (either Chromium or Android)."""
     36   if project in merge_common.ALL_PROJECTS:
     37     abs_path = os.path.join(merge_common.REPOSITORY_ROOT, project)
     38   else:
     39     abs_path = os.path.join(os.environ['ANDROID_BUILD_TOP'], project)
     40   if not os.path.exists(abs_path):
     41     raise merge_common.MergeError('Cannot find path ' + abs_path)
     42   return abs_path
     43 
     44 
     45 def _CheckoutSingleProject(project, target_branch):
     46   """Checks out the tip of the target_branch into a local branch (merge-to-XXX).
     47 
     48   Args:
     49     project: a Chromium project (., third_party/foo) or frameworks/webview.
     50     target_branch: name of the target branch (in the goog remote).
     51   """
     52   dest_dir = _GetAbsPath(project)
     53   tracking_branch = 'goog/' + target_branch
     54   logging.debug('Check out %-45s at %-16s', project, tracking_branch)
     55   merge_common.GetCommandStdout(['git', 'remote', 'update', 'goog'],
     56                                 cwd=dest_dir)
     57   merge_common.GetCommandStdout(['git', 'checkout',
     58                                  '-b', 'merge-to-' + target_branch,
     59                                  '-t', tracking_branch], cwd=dest_dir)
     60 
     61 
     62 def _FetchSingleProject(project, remote, remote_ref):
     63   """Fetches a remote ref for the given project and returns the fetched SHA.
     64 
     65   Args:
     66     project: a Chromium project (., third_party/foo) or frameworks/webview.
     67     remote: Git remote name (goog for most projects, history for squashed ones).
     68     remote_ref: the remote ref to fetch (e.g., refs/archive/chromium-XXX).
     69 
     70   Returns:
     71     The SHA1 of the FETCH_HEAD.
     72   """
     73   dest_dir = _GetAbsPath(project)
     74   logging.debug('Fetch     %-45s %s:%s', project, remote, remote_ref)
     75   merge_common.GetCommandStdout(['git', 'fetch', remote, remote_ref],
     76                                 cwd=dest_dir)
     77   return merge_common.GetCommandStdout(['git', 'rev-parse', 'FETCH_HEAD'],
     78                                        cwd=dest_dir).strip()
     79 
     80 
     81 def _MergeSingleProject(project, merge_sha, revision, target_branch, flatten):
     82   """Merges a single project at a given SHA.
     83 
     84   Args:
     85     project: a Chromium project (., third_party/foo) or frameworks/webview.
     86     merge_sha: the SHA to merge.
     87     revision: Abbrev. commitish in the main Chromium repository.
     88     target_branch: name of the target branch.
     89     flatten: True: squash history while merging; False: perform a normal merge.
     90   """
     91   dest_dir = _GetAbsPath(project)
     92   if flatten:
     93     # Make the previous merges into grafts so we can do a correct merge.
     94     old_sha = merge_common.GetCommandStdout(['git', 'rev-parse', 'HEAD'],
     95                                             cwd=dest_dir).strip()
     96     merge_log = os.path.join(dest_dir, '.merged-revisions')
     97     if os.path.exists(merge_log):
     98       shutil.copyfile(merge_log,
     99                       os.path.join(dest_dir, '.git', 'info', 'grafts'))
    100 
    101   # Early out if there is nothing to merge.
    102   if not merge_common.GetCommandStdout(['git', 'rev-list', '-1',
    103                                         'HEAD..' + merge_sha], cwd=dest_dir):
    104     logging.debug('No new commits to merge in project %s', project)
    105     return
    106 
    107   logging.debug('Merging project %s (flatten: %s)...', project, flatten)
    108   merge_cmd = ['git', 'merge', '--no-commit']
    109   merge_cmd += ['--squash'] if flatten else ['--no-ff']
    110   merge_cmd += [merge_sha]
    111   # Merge conflicts cause 'git merge' to return 1, so ignore errors
    112   merge_common.GetCommandStdout(merge_cmd, cwd=dest_dir, ignore_errors=True)
    113 
    114   if flatten:
    115     dirs_to_prune = merge_common.PRUNE_WHEN_FLATTENING.get(project, [])
    116     if dirs_to_prune:
    117       merge_common.GetCommandStdout(['git', 'rm', '--ignore-unmatch', '-rf'] +
    118                                     dirs_to_prune, cwd=dest_dir)
    119 
    120   if project in merge_common.ALL_PROJECTS:
    121     commit_msg = 'Merge from Chromium at DEPS revision %s' % revision
    122   else:
    123     commit_msg = 'Merge master-chromium into %s at %s' % (target_branch,
    124                                                           revision)
    125   commit_msg += '\n\n' + AUTOGEN_MESSAGE
    126   merge_common.CheckNoConflictsAndCommitMerge(commit_msg, cwd=dest_dir)
    127 
    128   if flatten:
    129     # Generate the new grafts file and commit it on top of the merge.
    130     new_sha = merge_common.GetCommandStdout(['git', 'rev-parse', 'HEAD'],
    131                                             cwd=dest_dir).strip()
    132     with open(merge_log, 'a+') as f:
    133       f.write('%s %s %s\n' % (new_sha, old_sha, merge_sha))
    134     merge_common.GetCommandStdout(['git', 'add', '.merged-revisions'],
    135                                   cwd=dest_dir)
    136     merge_common.GetCommandStdout(
    137         ['git', 'commit', '-m',
    138          'Record Chromium merge at DEPS revision %s\n\n%s' %
    139          (revision, AUTOGEN_MESSAGE)], cwd=dest_dir)
    140 
    141 
    142 def _IsAncestor(ref1, ref2, cwd):
    143   """Checks whether ref1 is a ancestor of ref2 in the given Git repo."""
    144   cmd = ['git', 'merge-base', '--is-ancestor', ref1, ref2]
    145   ret = subprocess.call(cmd, cwd=cwd)
    146   if ret == 0:
    147     return True
    148   elif ret == 1:
    149     return False
    150   else:
    151     raise merge_common.CommandError(ret, ' '.join(cmd), cwd, 'N/A', 'N/A')
    152 
    153 
    154 def _MergeChromiumProjects(revision, target_branch, repo_shas=None,
    155                            force=False):
    156   """Merges the Chromium projects from master-chromium to target_branch.
    157 
    158   The larger projects' histories are flattened in the process.
    159   When repo_shas != None, it checks that the SHAs of the projects in the
    160   archive match exactly the SHAs of the projects in repo.prop.
    161 
    162   Args:
    163     revision: Abbrev. commitish in the main Chromium repository.
    164     target_branch: target branch name to merge and push to.
    165     repo_shas: optional dict. of expected revisions (only for --repo-prop).
    166     force: True: merge anyways using the SHAs from repo.prop; False: bail out if
    167                  projects mismatch (archive vs repo.prop).
    168   """
    169   # Sync and checkout ToT for all projects (creating the merge-to-XXX branch)
    170   # and fetch the archive snapshot.
    171   fetched_shas = {}
    172   remote_ref = 'refs/archive/chromium-%s' % revision
    173   for project in merge_common.PROJECTS_WITH_FLAT_HISTORY:
    174     _CheckoutSingleProject(project, target_branch)
    175     fetched_shas[project] = _FetchSingleProject(project, 'history', remote_ref)
    176   for project in merge_common.PROJECTS_WITH_FULL_HISTORY:
    177     _CheckoutSingleProject(project, target_branch)
    178     fetched_shas[project] = _FetchSingleProject(project, 'goog', remote_ref)
    179 
    180   if repo_shas:
    181     project_shas_mismatch = False
    182     for project, merge_sha in fetched_shas.items():  # the dict can be modified.
    183       expected_sha = repo_shas.get(project)
    184       if expected_sha != merge_sha:
    185         logging.warn('The SHA for project %s specified in the repo.prop (%s) '
    186                      'and the one in the archive (%s) differ.',
    187                      project, expected_sha, merge_sha)
    188         dest_dir = _GetAbsPath(project)
    189         if expected_sha is None:
    190           reason = 'cannot find a SHA in the repo.pro for %s' % project
    191         elif _IsAncestor(merge_sha, expected_sha, cwd=dest_dir):
    192           reason = 'the SHA in repo.prop is ahead of the SHA in the archive. '
    193           log_cmd = ['git', 'log', '--oneline', '--graph', '--max-count=10',
    194                      '%s..%s' % (merge_sha, expected_sha)]
    195           log_cmd_output = merge_common.GetCommandStdout(log_cmd, cwd=dest_dir)
    196           reason += 'showing partial log (%s): \n %s' % (' '.join(log_cmd),
    197                                                          log_cmd_output)
    198         elif _IsAncestor(expected_sha, merge_sha, cwd=dest_dir):
    199           reason = 'The SHA is already merged in the archive'
    200         else:
    201           reason = 'The project history diverged. Consult your Git historian.'
    202 
    203         project_shas_mismatch = True
    204         if force:
    205           logging.debug('Merging the SHA in repo.prop anyways (due to --force)')
    206           fetched_shas[project] = expected_sha
    207         else:
    208           logging.debug('Reason: %s', reason)
    209     if not force and project_shas_mismatch:
    210       raise merge_common.MergeError(
    211           'The revision of some projects in the archive is different from the '
    212           'one provided in build.prop. See the log for more details. Re-run '
    213           'with --force to continue.')
    214 
    215   for project in merge_common.PROJECTS_WITH_FLAT_HISTORY:
    216     _MergeSingleProject(project, fetched_shas[project], revision, target_branch,
    217                         flatten=True)
    218   for project in merge_common.PROJECTS_WITH_FULL_HISTORY:
    219     _MergeSingleProject(project, fetched_shas[project], revision, target_branch,
    220                         flatten=False)
    221 
    222 
    223 def _GetNearestUpstreamAbbrevSHA(reference='history/master-chromium'):
    224   """Returns the abbrev. upstream SHA which closest to the given reference."""
    225   logging.debug('Getting upstream SHA for %s...', reference)
    226   merge_common.GetCommandStdout(['git', 'remote', 'update', 'history'])
    227   upstream_commit = merge_common.Abbrev(merge_common.GetCommandStdout([
    228       'git', 'merge-base', 'history/upstream-master', reference]))
    229 
    230   # Pedantic check: look for the existence of a merge commit which contains the
    231   # |upstream_commit| in its message and is its children.
    232   merge_parents = merge_common.GetCommandStdout([
    233       'git', 'rev-list', reference, '--grep', upstream_commit, '--merges',
    234       '--parents', '-1'])
    235   if upstream_commit not in merge_parents:
    236     raise merge_common.MergeError(
    237         'Found upstream commit %s, but the merge child (%s) could not be found '
    238         'or is not a parent of the upstream SHA')
    239   logging.debug('Found nearest Chromium revision %s', upstream_commit)
    240   return upstream_commit
    241 
    242 
    243 def _MergeWithRepoProp(repo_prop_file, target_branch, force):
    244   """Performs a merge using a repo.prop file (from Android build waterfall).
    245 
    246   This does NOT merge (unless forced with force=True) the pinned
    247   revisions in repo.prop, as a repo.prop can snapshot an intermediate state
    248   (between two automerger cycles). Instead, this looks up the archived snapshot
    249   (generated by the chromium->master-chromium auto-merger) which is closest to
    250   the given repo.prop (following the main Chromium project) and merges that one.
    251   If the projects revisions don't match, it fails with detailed error messages.
    252 
    253   Args:
    254     repo_prop_file: Path to a downloaded repo.prop file.
    255     target_branch: name of the target branch to merget to.
    256     force: ignores the aforementioned check and merged anyways.
    257   """
    258   chromium_sha = None
    259   webview_sha = None
    260   repo_shas = {}  # 'project/path' -> 'sha'
    261   with open(repo_prop_file) as prop:
    262     for line in prop:
    263       repo, sha = line.split()
    264       # Translate the Android repo paths into the relative project paths used in
    265       # merge_common (e.g., platform/external/chromium_org/foo -> foo).
    266       m = (
    267           re.match(r'^platform/(frameworks/.+)$', repo) or
    268           re.match(r'^platform/external/chromium_org/?(.*?)(-history)?$', repo))
    269       if m:
    270         project = m.group(1) if m.group(1) else '.'  # '.' = Main project.
    271         repo_shas[project] = sha
    272 
    273   chromium_sha = repo_shas.get('.')
    274   webview_sha = repo_shas.get(WEBVIEW_PROJECT)
    275   if not chromium_sha or not webview_sha:
    276     raise merge_common.MergeError('SHAs for projects not found; '
    277                                   'invalid build.prop?')
    278 
    279   # Check that the revisions in repo.prop and the on in the archive match.
    280   archived_chromium_revision = _GetNearestUpstreamAbbrevSHA(chromium_sha)
    281   logging.info('Merging Chromium at %s and WebView at %s',
    282                archived_chromium_revision, webview_sha)
    283   _MergeChromiumProjects(archived_chromium_revision, target_branch, repo_shas,
    284                          force)
    285 
    286   _CheckoutSingleProject(WEBVIEW_PROJECT, target_branch)
    287   _MergeSingleProject(WEBVIEW_PROJECT, webview_sha,
    288                       archived_chromium_revision, target_branch, flatten=False)
    289 
    290 
    291 def Push(target_branch):
    292   """Push the finished snapshot to the Android repository.
    293 
    294   Creates first a CL for frameworks/webview (if the merge-to-XXX branch exists)
    295   then wait for user confirmation and pushes the Chromium merges. This is to
    296   give an opportunity to get a +2 for  frameworks/webview and then push both
    297   frameworks/webview and the Chromium projects atomically(ish).
    298 
    299   Args:
    300     target_branch: name of the target branch (in the goog remote).
    301   """
    302   merge_branch = 'merge-to-%s' % target_branch
    303 
    304   # Create a Gerrit CL for the frameworks/webview project (if needed).
    305   dest_dir = _GetAbsPath(WEBVIEW_PROJECT)
    306   did_upload_webview_cl = False
    307   if merge_common.GetCommandStdout(['git', 'branch', '--list', merge_branch],
    308                                    cwd=dest_dir):
    309     # Check that there was actually something to merge.
    310     merge_range = 'goog/%s..%s' % (target_branch, merge_branch)
    311     if merge_common.GetCommandStdout(['git', 'rev-list', '-1', merge_range],
    312                                      cwd=dest_dir):
    313       logging.info('Uploading a merge CL for %s...', WEBVIEW_PROJECT)
    314       refspec = '%s:refs/for/%s' % (merge_branch, target_branch)
    315       upload = merge_common.GetCommandStdout(['git', 'push', 'goog', refspec],
    316                                              cwd=dest_dir)
    317       logging.info(upload)
    318       did_upload_webview_cl = True
    319 
    320   prompt_msg = 'About push the Chromium projects merge. '
    321   if not did_upload_webview_cl:
    322     logging.info('No merge CL needed for %s.', WEBVIEW_PROJECT)
    323   else:
    324     prompt_msg += ('At this point you should have the CL +2-ed and merge it '
    325                    'together with this push.')
    326   prompt_msg += '\nPress "y" to continue: '
    327   if raw_input(prompt_msg) != 'y':
    328     logging.warn('Push aborted by the user!')
    329     return
    330 
    331   logging.debug('Pushing Chromium projects to %s ...', target_branch)
    332   refspec = '%s:%s' % (merge_branch, target_branch)
    333   for path in merge_common.ALL_PROJECTS:
    334     logging.debug('Pushing %s', path)
    335     dest_dir = _GetAbsPath(path)
    336     # Delete the graft before pushing otherwise git will attempt to push all the
    337     # grafted-in objects to the server as well as the ones we want.
    338     graftfile = os.path.join(dest_dir, '.git', 'info', 'grafts')
    339     if os.path.exists(graftfile):
    340       os.remove(graftfile)
    341     merge_common.GetCommandStdout(['git', 'push', 'goog', refspec],
    342                                   cwd=dest_dir)
    343 
    344 
    345 def main():
    346   parser = optparse.OptionParser(usage='%prog [options]')
    347   parser.epilog = ('Takes the current master-chromium branch of the Chromium '
    348                    'projects in Android and merges them into master to publish '
    349                    'them.')
    350   parser.add_option(
    351       '', '--revision',
    352       default=None,
    353       help=('Merge to the specified archived master-chromium revision (abbrev. '
    354             'SHA or release version) rather than using HEAD. e.g., '
    355             '--revision=a1b2c3d4e5f6 or --revision=38.0.2125.24'))
    356   parser.add_option(
    357       '', '--repo-prop',
    358       default=None, metavar='FILE',
    359       help=('Merge to the revisions specified in this repo.prop file.'))
    360   parser.add_option(
    361       '', '--force',
    362       default=False, action='store_true',
    363       help=('Skip history checks and merged anyways (only for --repo-prop).'))
    364   parser.add_option(
    365       '', '--push',
    366       default=False, action='store_true',
    367       help=('Push the result of a previous merge to the server.'))
    368   parser.add_option(
    369       '', '--target',
    370       default='master', metavar='BRANCH',
    371       help=('Target branch to push to. Defaults to master.'))
    372   (options, args) = parser.parse_args()
    373   if args:
    374     parser.print_help()
    375     return 1
    376 
    377   logging.basicConfig(format='%(message)s', level=logging.DEBUG,
    378                       stream=sys.stdout)
    379 
    380   if options.push:
    381     Push(options.target)
    382   elif options.repo_prop:
    383     _MergeWithRepoProp(os.path.expanduser(options.repo_prop),
    384                        options.target, options.force)
    385   elif options.revision:
    386     _MergeChromiumProjects(options.revision, options.target)
    387   else:
    388     first_upstream_sha = _GetNearestUpstreamAbbrevSHA()
    389     _MergeChromiumProjects(first_upstream_sha, options.target)
    390 
    391   return 0
    392 
    393 if __name__ == '__main__':
    394   sys.exit(main())
    395