# Copyright 2014 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""This module contains functions for performing source control operations."""

import bisect_utils


def IsInGitRepository():
  output, _ = bisect_utils.RunGit(['rev-parse', '--is-inside-work-tree'])
  return output.strip() == 'true'


def GetRevisionList(end_revision_hash, start_revision_hash, cwd=None):
  """Retrieves a list of git commit hashes in a range.

  Args:
    end_revision_hash: The SHA1 for the end of the range, inclusive.
    start_revision_hash: The SHA1 for the beginning of the range, inclusive.

  Returns:
    A list of the git commit hashes in the range, in reverse time order --
    that is, starting with |end_revision_hash|.
  """
  revision_range = '%s..%s' % (start_revision_hash, end_revision_hash)
  cmd = ['log', '--format=%H', '-10000', '--first-parent', revision_range]
  log_output = bisect_utils.CheckRunGit(cmd, cwd=cwd)

  revision_hash_list = log_output.split()
  revision_hash_list.append(start_revision_hash)

  return revision_hash_list


def SyncToRevision(revision, sync_client=None):
  """Syncs or checks out a revision based on sync_client argument.

  Args:
    revision: Git hash for the solutions with the format <repo>@rev.
        E.g., "src@2ae43f...", "src/third_party/webkit@asr1234" etc.
    sync_client: Syncs to revision when this is True otherwise checks out
        the revision.

  Returns:
    True if sync or checkout is successful, False otherwise.
  """
  if not sync_client:
    _, return_code = bisect_utils.RunGit(['checkout', revision])
  elif sync_client == 'gclient':
    return_code = bisect_utils.RunGClientAndSync([revision])
  else:
    raise NotImplementedError('Unsupported sync_client: "%s"' % sync_client)

  return not return_code


def GetCurrentRevision(cwd=None):
  """Gets current revision of the given repository."""
  return bisect_utils.CheckRunGit(['rev-parse', 'HEAD'], cwd=cwd).strip()


def ResolveToRevision(revision_to_check, depot, depot_deps_dict,
                      search, cwd=None):
  """Tries to resolve an SVN revision or commit position to a git SHA1.

  Args:
    revision_to_check: The user supplied revision string that may need to be
        resolved to a git commit hash. This may be an SVN revision, git commit
        position, or a git commit hash.
    depot: The depot (dependency repository) that |revision_to_check| is from.
    depot_deps_dict: A dictionary with information about different depots.
    search: How many revisions forward or backward to search. If the value is
        negative, the function will search backwards chronologically, otherwise
        it will search forward.

  Returns:
    A string containing a git SHA1 hash, otherwise None.
  """
  # Android-chrome is git only, so no need to resolve this to anything else.
  if depot == 'android-chrome':
    return revision_to_check

  # If the given revision can't be parsed as an integer, then it may already
  # be a git commit hash.
  if not bisect_utils.IsStringInt(revision_to_check):
    return revision_to_check

  depot_svn = 'svn://svn.chromium.org/chrome/trunk/src'

  if depot != 'chromium':
    depot_svn = depot_deps_dict[depot]['svn']
  svn_revision = int(revision_to_check)
  git_revision = None

  if search > 0:
    search_range = xrange(svn_revision, svn_revision + search, 1)
  else:
    search_range = xrange(svn_revision, svn_revision + search, -1)

  for i in search_range:
    # NOTE: Checking for the git-svn-id footer is for backwards compatibility.
    # When we can assume that all the revisions we care about are from after
    # git commit positions started getting added, we don't need to check this.
    svn_pattern = 'git-svn-id: %s@%d' % (depot_svn, i)
    commit_position_pattern = '^Cr-Commit-Position: .*@{#%d}' % i
    cmd = ['log', '--format=%H', '-1', '--grep', svn_pattern,
           '--grep', commit_position_pattern, 'origin/master']
    log_output = bisect_utils.CheckRunGit(cmd, cwd=cwd)
    log_output = log_output.strip()

    if log_output:
      git_revision = log_output
      break

  return git_revision


def IsInProperBranch():
  """Checks whether the current branch is "master"."""
  cmd = ['rev-parse', '--abbrev-ref', 'HEAD']
  log_output = bisect_utils.CheckRunGit(cmd)
  log_output = log_output.strip()
  return log_output == 'master'


def GetCommitPosition(git_revision, cwd=None):
  """Finds git commit position for the given git hash.

  This function executes "git footer --position-num <git hash>" command to get
  commit position the given revision.

  Args:
    git_revision: The git SHA1 to use.
    cwd: Working directory to run the command from.

  Returns:
    Git commit position as integer or None.
  """
  # Some of the repositories are pure git based, unlike other repositories
  # they doesn't have commit position. e.g., skia, angle.
  cmd = ['footers', '--position-num', git_revision]
  output, return_code = bisect_utils.RunGit(cmd, cwd)
  if not return_code:
    commit_position = output.strip()
    if bisect_utils.IsStringInt(commit_position):
      return int(commit_position)
  return None


def GetCommitTime(git_revision, cwd=None):
  """Returns commit time for the given revision in UNIX timestamp."""
  cmd = ['log', '--format=%ct', '-1', git_revision]
  output = bisect_utils.CheckRunGit(cmd, cwd=cwd)
  return int(output)


def QueryRevisionInfo(revision, cwd=None):
  """Gathers information on a particular revision, such as author's name,
  email, subject, and date.

  Args:
    revision: Revision you want to gather information on; a git commit hash.

  Returns:
    A dict in the following format:
    {
      'author': %s,
      'email': %s,
      'date': %s,
      'subject': %s,
      'body': %s,
    }
  """
  commit_info = {}

  formats = ['%aN', '%aE', '%s', '%cD', '%b']
  targets = ['author', 'email', 'subject', 'date', 'body']

  for i in xrange(len(formats)):
    cmd = ['log', '--format=%s' % formats[i], '-1', revision]
    output = bisect_utils.CheckRunGit(cmd, cwd=cwd)
    commit_info[targets[i]] = output.rstrip()

  return commit_info


def CheckoutFileAtRevision(file_name, revision, cwd=None):
  """Performs a checkout on a file at the given revision.

  Returns:
    True if successful.
  """
  command = ['checkout', revision, file_name]
  _, return_code = bisect_utils.RunGit(command, cwd=cwd)
  return not return_code


def RevertFileToHead(file_name):
  """Un-stages a file and resets the file's state to HEAD.

  Returns:
    True if successful.
  """
  # Reset doesn't seem to return 0 on success.
  bisect_utils.RunGit(['reset', 'HEAD', file_name])
  _, return_code = bisect_utils.RunGit(
      ['checkout', bisect_utils.FILE_DEPS_GIT])
  return not return_code


def QueryFileRevisionHistory(filename, revision_start, revision_end):
  """Returns a list of commits that modified this file.

  Args:
    filename: Name of file.
    revision_start: Start of revision range (inclusive).
    revision_end: End of revision range.

  Returns:
    Returns a list of commits that touched this file.
  """
  cmd = [
      'log',
      '--format=%H',
      '%s~1..%s' % (revision_start, revision_end),
      '--',
      filename,
  ]
  output = bisect_utils.CheckRunGit(cmd)
  lines = output.split('\n')
  return [o for o in lines if o]