def _GetAbsPath(project):
    """Returns the full path to a given project (either Chromium or Android)."""
    if project in merge_common.ALL_PROJECTS:
        abs_path = os.path.join(merge_common.REPOSITORY_ROOT, project)
    else:
        abs_path = os.path.join(os.environ['ANDROID_BUILD_TOP'], project)
    if not os.path.exists(abs_path):
        raise merge_common.MergeError('Cannot find path ' + abs_path)
    return abs_path
def Snapshot(svn_revision, root_sha1, release, target, unattended,
             buildspec_url):
    """Takes a snapshot of the Chromium tree and merges it into Android.

  Android makefiles and a top-level NOTICE file are generated and committed
  after the merge.

  Args:
    svn_revision: The SVN revision in the Chromium repository to merge from.
    root_sha1: The sha1 in the Chromium git mirror to merge from.
    release: The Chromium release version to merge from (e.g. "30.0.1599.20").
             Only one of svn_revision, root_sha1 and release should be
             specified.
    target: The target branch to merge to.
    unattended: Run in unattended mode.
    buildspec_url: URL for buildspec repository, used when merging a release.

  Returns:
    True if new commits were merged; False if no new commits were present.
  """
    if svn_revision:
        svn_revision, root_sha1 = _GetSVNRevisionAndSHA1(
            SRC_GIT_BRANCH, svn_revision)
    elif root_sha1:
        svn_revision = _GetSVNRevisionFromSha(root_sha1)

    if svn_revision and root_sha1:
        version = svn_revision
        if not merge_common.GetCommandStdout(
            ['git', 'rev-list', '-1', 'HEAD..' + root_sha1]):
            logging.info('No new commits to merge at %s (%s)', svn_revision,
                         root_sha1)
            return False
    elif release:
        version = release
        root_sha1 = None
    else:
        raise merge_common.MergeError('No merge source specified')

    logging.info('Snapshotting Chromium at %s (%s)', version, root_sha1)

    # 1. Merge, accounting for excluded directories
    _MergeProjects(version, root_sha1, target, unattended, buildspec_url)

    # 2. Generate Android makefiles
    _GenerateMakefiles(version, unattended)

    # 3. Check for incompatible licenses
    _CheckLicenses()

    # 4. Generate Android NOTICE file
    _GenerateNoticeFile(version)

    # 5. Generate LASTCHANGE file
    _GenerateLastChange(version)

    return True
def _MergeWithRepoProp(repo_prop_file, target_branch, force):
    """Performs a merge using a repo.prop file (from Android build waterfall).

  This does NOT merge (unless forced with force=True) the pinned
  revisions in repo.prop, as a repo.prop can snapshot an intermediate state
  (between two automerger cycles). Instead, this looks up the archived snapshot
  (generated by the chromium->master-chromium auto-merger) which is closest to
  the given repo.prop (following the main Chromium project) and merges that one.
  If the projects revisions don't match, it fails with detailed error messages.

  Args:
    repo_prop_file: Path to a downloaded repo.prop file.
    target_branch: name of the target branch to merget to.
    force: ignores the aforementioned check and merged anyways.
  """
    chromium_sha = None
    webview_sha = None
    repo_shas = {}  # 'project/path' -> 'sha'
    with open(repo_prop_file) as prop:
        for line in prop:
            repo, sha = line.split()
            # Translate the Android repo paths into the relative project paths used in
            # merge_common (e.g., platform/external/chromium_org/foo -> foo).
            m = (re.match(r'^platform/(frameworks/.+)$', repo) or re.match(
                r'^platform/external/chromium_org/?(.*?)(-history)?$', repo))
            if m:
                project = m.group(1) if m.group(
                    1) else '.'  # '.' = Main project.
                repo_shas[project] = sha

    chromium_sha = repo_shas.get('.')
    webview_sha = repo_shas.get(WEBVIEW_PROJECT)
    if not chromium_sha or not webview_sha:
        raise merge_common.MergeError('SHAs for projects not found; '
                                      'invalid build.prop?')

    # Check that the revisions in repo.prop and the on in the archive match.
    archived_chromium_revision = _GetNearestUpstreamAbbrevSHA(chromium_sha)
    logging.info('Merging Chromium at %s and WebView at %s',
                 archived_chromium_revision, webview_sha)
    _MergeChromiumProjects(archived_chromium_revision, target_branch,
                           repo_shas, force)

    _CheckoutSingleProject(WEBVIEW_PROJECT, target_branch)
    _MergeSingleProject(WEBVIEW_PROJECT,
                        webview_sha,
                        archived_chromium_revision,
                        target_branch,
                        flatten=False)
def _GetNearestUpstreamAbbrevSHA(reference='history/master-chromium'):
    """Returns the abbrev. upstream SHA which closest to the given reference."""
    logging.debug('Getting upstream SHA for %s...', reference)
    merge_common.GetCommandStdout(['git', 'remote', 'update', 'history'])
    upstream_commit = merge_common.Abbrev(
        merge_common.GetCommandStdout(
            ['git', 'merge-base', 'history/upstream-master', reference]))

    # Pedantic check: look for the existence of a merge commit which contains the
    # |upstream_commit| in its message and is its children.
    merge_parents = merge_common.GetCommandStdout([
        'git', 'rev-list', reference, '--grep', upstream_commit, '--merges',
        '--parents', '-1'
    ])
    if upstream_commit not in merge_parents:
        raise merge_common.MergeError(
            'Found upstream commit %s, but the merge child (%s) could not be found '
            'or is not a parent of the upstream SHA')
    logging.debug('Found nearest Chromium revision %s', upstream_commit)
    return upstream_commit
def _MergeChromiumProjects(revision,
                           target_branch,
                           repo_shas=None,
                           force=False):
    """Merges the Chromium projects from master-chromium to target_branch.

  The larger projects' histories are flattened in the process.
  When repo_shas != None, it checks that the SHAs of the projects in the
  archive match exactly the SHAs of the projects in repo.prop.

  Args:
    revision: Abbrev. commitish in the main Chromium repository.
    target_branch: target branch name to merge and push to.
    repo_shas: optional dict. of expected revisions (only for --repo-prop).
    force: True: merge anyways using the SHAs from repo.prop; False: bail out if
                 projects mismatch (archive vs repo.prop).
  """
    # Sync and checkout ToT for all projects (creating the merge-to-XXX branch)
    # and fetch the archive snapshot.
    fetched_shas = {}
    remote_ref = 'refs/archive/chromium-%s' % revision
    for project in merge_common.PROJECTS_WITH_FLAT_HISTORY:
        _CheckoutSingleProject(project, target_branch)
        fetched_shas[project] = _FetchSingleProject(project, 'history',
                                                    remote_ref)
    for project in merge_common.PROJECTS_WITH_FULL_HISTORY:
        _CheckoutSingleProject(project, target_branch)
        fetched_shas[project] = _FetchSingleProject(project, 'goog',
                                                    remote_ref)

    if repo_shas:
        project_shas_mismatch = False
        for project, merge_sha in fetched_shas.items(
        ):  # the dict can be modified.
            expected_sha = repo_shas.get(project)
            if expected_sha != merge_sha:
                logging.warn(
                    'The SHA for project %s specified in the repo.prop (%s) '
                    'and the one in the archive (%s) differ.', project,
                    expected_sha, merge_sha)
                dest_dir = _GetAbsPath(project)
                if expected_sha is None:
                    reason = 'cannot find a SHA in the repo.pro for %s' % project
                elif _IsAncestor(merge_sha, expected_sha, cwd=dest_dir):
                    reason = 'the SHA in repo.prop is ahead of the SHA in the archive. '
                    log_cmd = [
                        'git', 'log', '--oneline', '--graph', '--max-count=10',
                        '%s..%s' % (merge_sha, expected_sha)
                    ]
                    log_cmd_output = merge_common.GetCommandStdout(
                        log_cmd, cwd=dest_dir)
                    reason += 'showing partial log (%s): \n %s' % (
                        ' '.join(log_cmd), log_cmd_output)
                elif _IsAncestor(expected_sha, merge_sha, cwd=dest_dir):
                    reason = 'The SHA is already merged in the archive'
                else:
                    reason = 'The project history diverged. Consult your Git historian.'

                project_shas_mismatch = True
                if force:
                    logging.debug(
                        'Merging the SHA in repo.prop anyways (due to --force)'
                    )
                    fetched_shas[project] = expected_sha
                else:
                    logging.debug('Reason: %s', reason)
        if not force and project_shas_mismatch:
            raise merge_common.MergeError(
                'The revision of some projects in the archive is different from the '
                'one provided in build.prop. See the log for more details. Re-run '
                'with --force to continue.')

    for project in merge_common.PROJECTS_WITH_FLAT_HISTORY:
        _MergeSingleProject(project,
                            fetched_shas[project],
                            revision,
                            target_branch,
                            flatten=True)
    for project in merge_common.PROJECTS_WITH_FULL_HISTORY:
        _MergeSingleProject(project,
                            fetched_shas[project],
                            revision,
                            target_branch,
                            flatten=False)