#!/usr/bin/python
#
# Copyright (C) 2012 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Merge master-chromium to master within the Android tree."""

import logging
import optparse
import os
import re
import shutil
import sys

import merge_common


AUTOGEN_MESSAGE = 'This commit was generated by merge_to_master.py.'


def _MergeProjects(svn_revision, target):
  """Merges the Chromium projects from master-chromium to target.

  The larger projects' histories are flattened in the process.

  Args:
    svn_revision: The SVN revision for the main Chromium repository
  """
  for path in merge_common.PROJECTS_WITH_FLAT_HISTORY:
    dest_dir = os.path.join(merge_common.REPOSITORY_ROOT, path)
    merge_common.GetCommandStdout(['git', 'remote', 'update',
                                   'goog', 'history'], cwd=dest_dir)
    merge_common.GetCommandStdout(['git', 'checkout',
                                   '-b', 'merge-to-' + target,
                                   '-t', 'goog/' + target], cwd=dest_dir)
    merge_common.GetCommandStdout(['git', 'fetch', 'history',
                                   'refs/archive/chromium-%s' % svn_revision],
                                  cwd=dest_dir)
    merge_sha1 = merge_common.GetCommandStdout(['git', 'rev-parse',
                                                'FETCH_HEAD'],
                                               cwd=dest_dir).strip()
    old_sha1 = merge_common.GetCommandStdout(['git', 'rev-parse', 'HEAD'],
                                             cwd=dest_dir).strip()
    # Make the previous merges into grafts so we can do a correct merge.
    merge_log = os.path.join(dest_dir, '.merged-revisions')
    if os.path.exists(merge_log):
      shutil.copyfile(merge_log,
                      os.path.join(dest_dir, '.git', 'info', 'grafts'))
    if merge_common.GetCommandStdout(['git', 'rev-list', '-1',
                                      'HEAD..' + merge_sha1], cwd=dest_dir):
      logging.debug('Merging project %s ...', path)
      # Merge conflicts cause 'git merge' to return 1, so ignore errors
      merge_common.GetCommandStdout(['git', 'merge', '--no-commit', '--squash',
                                     merge_sha1],
                                    cwd=dest_dir, ignore_errors=True)
      dirs_to_prune = merge_common.PRUNE_WHEN_FLATTENING.get(path, [])
      if dirs_to_prune:
        merge_common.GetCommandStdout(['git', 'rm', '--ignore-unmatch', '-rf'] +
                                      dirs_to_prune, cwd=dest_dir)
      merge_common.CheckNoConflictsAndCommitMerge(
          'Merge from Chromium at DEPS revision %s\n\n%s' %
          (svn_revision, AUTOGEN_MESSAGE), cwd=dest_dir)
      new_sha1 = merge_common.GetCommandStdout(['git', 'rev-parse', 'HEAD'],
                                               cwd=dest_dir).strip()
      with open(merge_log, 'a+') as f:
        f.write('%s %s %s\n' % (new_sha1, old_sha1, merge_sha1))
      merge_common.GetCommandStdout(['git', 'add', '.merged-revisions'],
                                    cwd=dest_dir)
      merge_common.GetCommandStdout(
          ['git', 'commit', '-m',
           'Record Chromium merge at DEPS revision %s\n\n%s' %
           (svn_revision, AUTOGEN_MESSAGE)], cwd=dest_dir)
    else:
      logging.debug('No new commits to merge in project %s', path)

  for path in merge_common.PROJECTS_WITH_FULL_HISTORY:
    dest_dir = os.path.join(merge_common.REPOSITORY_ROOT, path)
    merge_common.GetCommandStdout(['git', 'remote', 'update', 'goog'],
                                  cwd=dest_dir)
    merge_common.GetCommandStdout(['git', 'checkout',
                                   '-b', 'merge-to-' + target,
                                   '-t', 'goog/' + target], cwd=dest_dir)
    merge_common.GetCommandStdout(['git', 'fetch', 'goog',
                                   'refs/archive/chromium-%s' % svn_revision],
                                  cwd=dest_dir)
    if merge_common.GetCommandStdout(['git', 'rev-list', '-1',
                                      'HEAD..FETCH_HEAD'],
                                     cwd=dest_dir):
      logging.debug('Merging project %s ...', path)
      # Merge conflicts cause 'git merge' to return 1, so ignore errors
      merge_common.GetCommandStdout(['git', 'merge', '--no-commit', '--no-ff',
                                     'FETCH_HEAD'],
                                    cwd=dest_dir, ignore_errors=True)
      merge_common.CheckNoConflictsAndCommitMerge(
          'Merge from Chromium at DEPS revision %s\n\n%s' %
          (svn_revision, AUTOGEN_MESSAGE), cwd=dest_dir)
    else:
      logging.debug('No new commits to merge in project %s', path)


def _GetSVNRevision(commitish='history/master-chromium'):
  logging.debug('Getting SVN revision ...')
  commit = merge_common.GetCommandStdout([
      'git', 'log', '-n1', '--grep=git-svn-id:', '--format=%H%n%b', commitish])
  svn_revision = re.search(r'^git-svn-id: .*@([0-9]+)', commit,
                           flags=re.MULTILINE).group(1)
  return svn_revision


def _MergeWithRepoProp(repo_prop_file, target):
  chromium_sha = None
  webview_sha = None
  with open(repo_prop_file) as prop:
    for line in prop:
      project, sha = line.split()
      if project == 'platform/external/chromium_org-history':
        chromium_sha = sha
      elif project == 'platform/frameworks/webview':
        webview_sha = sha
  if not chromium_sha or not webview_sha:
    logging.error('SHA1s for projects not found; invalid build.prop?')
    return 1
  chromium_revision = _GetSVNRevision(chromium_sha)
  logging.info('Merging Chromium at r%s and WebView at %s', chromium_revision,
               webview_sha)
  _MergeProjects(chromium_revision, target)

  dest_dir = os.path.join(os.environ['ANDROID_BUILD_TOP'], 'frameworks/webview')
  merge_common.GetCommandStdout(['git', 'remote', 'update', 'goog'],
                                cwd=dest_dir)
  merge_common.GetCommandStdout(['git', 'checkout',
                                 '-b', 'merge-to-' + target,
                                 '-t', 'goog/' + target], cwd=dest_dir)
  if merge_common.GetCommandStdout(['git', 'rev-list', '-1',
                                    'HEAD..' + webview_sha], cwd=dest_dir):
    logging.debug('Creating merge for framework...')
    # Merge conflicts cause 'git merge' to return 1, so ignore errors
    merge_common.GetCommandStdout(['git', 'merge', '--no-commit', '--no-ff',
                                   webview_sha], cwd=dest_dir,
                                  ignore_errors=True)
    merge_common.CheckNoConflictsAndCommitMerge(
        'Merge master-chromium into %s at r%s\n\n%s' %
        (target, chromium_revision, AUTOGEN_MESSAGE), cwd=dest_dir)
    upload = merge_common.GetCommandStdout(['git', 'push', 'goog',
                                            'HEAD:refs/for/' + target],
                                           cwd=dest_dir)
    logging.info(upload)
  else:
    logging.debug('No new commits to merge in framework')
  return 0


def Push(target):
  """Push the finished snapshot to the Android repository."""
  logging.debug('Pushing to server ...')
  refspec = 'merge-to-%s:%s' % (target, target)
  for path in merge_common.ALL_PROJECTS:
    logging.debug('Pushing %s', path)
    dest_dir = os.path.join(merge_common.REPOSITORY_ROOT, path)
    # Delete the graft before pushing otherwise git will attempt to push all the
    # grafted-in objects to the server as well as the ones we want.
    graftfile = os.path.join(dest_dir, '.git', 'info', 'grafts')
    if os.path.exists(graftfile):
      os.remove(graftfile)
    merge_common.GetCommandStdout(['git', 'push', 'goog', refspec],
                                  cwd=dest_dir)



def main():
  parser = optparse.OptionParser(usage='%prog [options]')
  parser.epilog = ('Takes the current master-chromium branch of the Chromium '
                   'projects in Android and merges them into master to publish '
                   'them.')
  parser.add_option(
      '', '--svn_revision', '--release',
      default=None,
      help=('Merge to the specified archived master-chromium SVN revision,'
            'rather than using HEAD.'))
  parser.add_option(
      '', '--repo-prop',
      default=None, metavar='FILE',
      help=('Merge to the revisions specified in this repo.prop file.'))
  parser.add_option(
      '', '--push',
      default=False, action='store_true',
      help=('Push the result of a previous merge to the server.'))
  parser.add_option(
      '', '--target',
      default='master', metavar='BRANCH',
      help=('Target branch to push to. Defaults to master.'))
  (options, args) = parser.parse_args()
  if args:
    parser.print_help()
    return 1

  logging.basicConfig(format='%(message)s', level=logging.DEBUG,
                      stream=sys.stdout)

  if options.push:
    Push(options.target)
  elif options.repo_prop:
    return _MergeWithRepoProp(os.path.expanduser(options.repo_prop),
                              options.target)
  elif options.svn_revision:
    _MergeProjects(options.svn_revision, options.target)
  else:
    svn_revision = _GetSVNRevision()
    _MergeProjects(svn_revision, options.target)

  return 0

if __name__ == '__main__':
  sys.exit(main())