#!/usr/bin/python
"""Diff a repo (downstream) and its upstream.

This script:
  1. Downloads a repo source tree with specified manifest URL, branch
     and release tag.
  2. Retrieves the BUILD_ID from $downstream/build/core/build_id.mk.
  3. Downloads the upstream using the BUILD_ID.
  4. Diffs each project in these two repos.
"""

import argparse
import datetime
import os
import subprocess
import repo_diff_trees

HELP_MSG = "Diff a repo (downstream) and its upstream"

DOWNSTREAM_WORKSPACE = "downstream"
UPSTREAM_WORKSPACE = "upstream"

DEFAULT_MANIFEST_URL = "https://android.googlesource.com/platform/manifest"
DEFAULT_MANIFEST_BRANCH = "android-8.0.0_r10"
DEFAULT_UPSTREAM_MANIFEST_URL = "https://android.googlesource.com/platform/manifest"
DEFAULT_UPSTREAM_MANIFEST_BRANCH = "android-8.0.0_r1"
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
DEFAULT_EXCLUSIONS_FILE = os.path.join(SCRIPT_DIR, "android_exclusions.txt")


def parse_args():
  """Parse args."""

  parser = argparse.ArgumentParser(description=HELP_MSG)

  parser.add_argument("-u", "--manifest-url",
                      help="manifest url",
                      default=DEFAULT_MANIFEST_URL)
  parser.add_argument("-b", "--manifest-branch",
                      help="manifest branch",
                      default=DEFAULT_MANIFEST_BRANCH)
  parser.add_argument("-r", "--upstream-manifest-url",
                      help="upstream manifest url",
                      default=DEFAULT_UPSTREAM_MANIFEST_URL)
  parser.add_argument("-a", "--upstream-manifest-branch",
                      help="upstream manifest branch",
                      default=DEFAULT_UPSTREAM_MANIFEST_BRANCH)
  parser.add_argument("-e", "--exclusions-file",
                      help="exclusions file",
                      default=DEFAULT_EXCLUSIONS_FILE)
  parser.add_argument("-t", "--tag",
                      help="release tag (optional). If not set then will "
                      "sync the latest in the branch.")
  parser.add_argument("-i", "--ignore_error_during_sync",
                      action="store_true",
                      help="repo sync might fail due to varios reasons. "
                      "Ignore these errors and move on. Use with caution.")

  return parser.parse_args()


def repo_init(url, rev, workspace):
  """Repo init with specific url and rev.

  Args:
    url: manifest url
    rev: manifest branch, or rev
    workspace: the folder to init and sync code
  """

  try:
    subprocess.check_output("repo", stderr=subprocess.PIPE,
                            cwd=os.path.dirname(workspace), shell=True)
  except subprocess.CalledProcessError:
    pass
  else:
    raise ValueError("cannot repo-init workspace (%s), workspace is within an "
                     "existing tree" % workspace)

  print("repo init:\n  url: %s\n  rev: %s\n  workspace: %s" %
        (url, rev, workspace))

  subprocess.check_output("repo init --manifest-url=%s --manifest-branch=%s" %
                          (url, rev), cwd=workspace, shell=True)


def repo_sync(workspace, ignore_error, retry=5):
  """Repo sync."""

  count = 0
  while count < retry:
    count += 1
    print("repo sync (retry=%d/%d):\n  workspace: %s" %
          (count, retry, workspace))

    try:
      command = "repo sync --jobs=24 --current-branch --quiet"
      command += " --no-tags --no-clone-bundle"
      if ignore_error:
        command += " --force-broken"
      subprocess.check_output(command, cwd=workspace, shell=True)
    except subprocess.CalledProcessError as e:
      print "Error: %s" % e.output
      if count == retry and not ignore_error:
        raise e
    # Stop retrying if the repo sync was successful
    else:
      break


def get_commit_with_keyword(project_path, keyword):
  """Get the latest commit in $project_path with the specific keyword."""

  return subprocess.check_output(("git -C %s "
                                  "rev-list --max-count=1 --grep=\"%s\" "
                                  "HEAD") %
                                 (project_path, keyword), shell=True).rstrip()


def get_build_id(workspace):
  """Get BUILD_ID defined in $workspace/build/core/build_id.mk."""

  path = os.path.join(workspace, "build", "core", "build_id.mk")
  return subprocess.check_output("source %s && echo $BUILD_ID" % path,
                                 shell=True).rstrip()


def repo_sync_specific_release(url, branch, tag, workspace, ignore_error):
  """Repo sync source with the specific release tag."""

  if not os.path.exists(workspace):
    os.makedirs(workspace)

  manifest_path = os.path.join(workspace, ".repo", "manifests")

  repo_init(url, branch, workspace)

  if tag:
    rev = get_commit_with_keyword(manifest_path, tag)
    if not rev:
      raise(ValueError("could not find a manifest revision for tag " + tag))
    repo_init(url, rev, workspace)

  repo_sync(workspace, ignore_error)


def diff(manifest_url, manifest_branch, tag, 
         upstream_manifest_url, upstream_manifest_branch,
         exclusions_file, ignore_error_during_sync):
  """Syncs and diffs an Android workspace against an upstream workspace."""

  workspace = os.path.abspath(DOWNSTREAM_WORKSPACE)
  upstream_workspace = os.path.abspath(UPSTREAM_WORKSPACE)
  # repo sync downstream source tree
  repo_sync_specific_release(
      manifest_url,
      manifest_branch,
      tag,
      workspace,
      ignore_error_during_sync)

  build_id = None

  if tag:
    # get the build_id so that we know which rev of upstream we need
    build_id = get_build_id(workspace)
    if not build_id:
      raise(ValueError("Error: could not find the Build ID of " + workspace))

  # repo sync upstream source tree
  repo_sync_specific_release(
      upstream_manifest_url,
      upstream_manifest_branch,
      build_id,
      upstream_workspace,
      ignore_error_during_sync)


  # make output folder
  if tag:
    output_folder = os.path.abspath(tag.replace(" ", "_"))
  else:
    current_time = datetime.datetime.today().strftime('%Y%m%d_%H%M%S')
    output_folder = os.path.abspath(current_time)

  if not os.path.exists(output_folder):
      os.makedirs(output_folder)

  # do the comparison
  repo_diff_trees.diff(
      upstream_workspace,
      workspace,
      os.path.join(output_folder, "project.csv"),
      os.path.join(output_folder, "commit.csv"),
      os.path.abspath(exclusions_file),
  )


def main():
  args = parse_args()

  diff(args.manifest_url,
       args.manifest_branch,
       args.tag,
       args.upstream_manifest_url,
       args.upstream_manifest_branch,
       args.exclusions_file,
       args.ignore_error_during_sync)

if __name__ == "__main__":
  main()
