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

"""Repository module to handle different types of repositories."""

from __future__ import print_function

import constants
import logging
import os
import re
import shutil

from chromite.lib import cros_build_lib
from chromite.lib import git
from chromite.lib import osutils
from chromite.lib import rewrite_git_alternates
from chromite.lib import retry_util


# File that marks a buildroot as being used by a trybot
_TRYBOT_MARKER = '.trybot'


class SrcCheckOutException(Exception):
  """Exception gets thrown for failure to sync sources"""


def IsARepoRoot(directory):
  """Returns True if directory is the root of a repo checkout."""
  return os.path.exists(os.path.join(directory, '.repo'))


def IsInternalRepoCheckout(root):
  """Returns whether root houses an internal 'repo' checkout."""
  manifest_dir = os.path.join(root, '.repo', 'manifests')
  manifest_url = git.RunGit(
      manifest_dir, ['config', 'remote.origin.url']).output.strip()
  return (os.path.splitext(os.path.basename(manifest_url))[0]
          == os.path.splitext(os.path.basename(constants.MANIFEST_INT_URL))[0])


def CloneGitRepo(working_dir, repo_url, reference=None, bare=False,
                 mirror=False, depth=None, branch=None, single_branch=False):
  """Clone given git repo

  Args:
    working_dir: location where it should be cloned to
    repo_url: git repo to clone
    reference: If given, pathway to a git repository to access git objects
      from.  Note that the reference must exist as long as the newly created
      repo is to be usable.
    bare: Clone a bare checkout.
    mirror: Clone a mirror checkout.
    depth: If given, do a shallow clone limiting the objects pulled to just
      that # of revs of history.  This option is mutually exclusive to
      reference.
    branch: If given, clone the given branch from the parent repository.
    single_branch: Clone only one the requested branch.
  """
  osutils.SafeMakedirs(working_dir)
  cmd = ['clone', repo_url, working_dir]
  if reference:
    if depth:
      raise ValueError("reference and depth are mutually exclusive "
                       "options; please pick one or the other.")
    cmd += ['--reference', reference]
  if bare:
    cmd += ['--bare']
  if mirror:
    cmd += ['--mirror']
  if depth:
    cmd += ['--depth', str(int(depth))]
  if branch:
    cmd += ['--branch', branch]
  if single_branch:
    cmd += ['--single-branch']
  git.RunGit(working_dir, cmd)


def UpdateGitRepo(working_dir, repo_url, **kwargs):
  """Update the given git repo, blowing away any local changes.

  If the repo does not exist, clone it from scratch.

  Args:
    working_dir: location where it should be cloned to
    repo_url: git repo to clone
    **kwargs: See CloneGitRepo.
  """
  assert not kwargs.get('bare'), 'Bare checkouts are not supported'
  if git.IsGitRepo(working_dir):
    try:
      git.CleanAndCheckoutUpstream(working_dir)
    except cros_build_lib.RunCommandError:
      cros_build_lib.Warning('Could not update %s', working_dir, exc_info=True)
      shutil.rmtree(working_dir)
      CloneGitRepo(working_dir, repo_url, **kwargs)
  else:
    CloneGitRepo(working_dir, repo_url, **kwargs)


def GetTrybotMarkerPath(buildroot):
  """Get path to trybot marker file given the buildroot."""
  return os.path.join(buildroot, _TRYBOT_MARKER)


def CreateTrybotMarker(buildroot):
  """Create the file that identifies a buildroot as being used by a trybot."""
  osutils.WriteFile(GetTrybotMarkerPath(buildroot), '')


def ClearBuildRoot(buildroot, preserve_paths=()):
  """Remove and recreate the buildroot while preserving the trybot marker."""
  trybot_root = os.path.exists(GetTrybotMarkerPath(buildroot))
  if os.path.exists(buildroot):
    cmd = ['find', buildroot, '-mindepth', '1', '-maxdepth', '1']

    ignores = []
    for path in preserve_paths:
      if ignores:
        ignores.append('-a')
      ignores += ['!', '-name', path]
    cmd.extend(ignores)

    cmd += ['-exec', 'rm', '-rf', '{}', '+']
    cros_build_lib.SudoRunCommand(cmd)
  else:
    os.makedirs(buildroot)
  if trybot_root:
    CreateTrybotMarker(buildroot)


class RepoRepository(object):
  """A Class that encapsulates a repo repository.

  Args:
    manifest_repo_url: URL to fetch repo manifest from.
    directory: local path where to checkout the repository.
    branch: Branch to check out the manifest at.
    referenced_repo: Repository to reference for git objects, if possible.
    manifest: Which manifest.xml within the branch to use.  Effectively
      default.xml if not given.
    depth: Mutually exclusive option to referenced_repo; this limits the
      checkout to a max commit history of the given integer.
    repo_url: URL to fetch repo tool from.
    repo_branch: Branch to check out the repo tool at.
  """
  # Use our own repo, in case android.kernel.org (the default location) is down.
  _INIT_CMD = ['repo', 'init']

  # If a repo hasn't been used in the last 5 runs, wipe it.
  LRU_THRESHOLD = 5

  def __init__(self, manifest_repo_url, directory, branch=None,
               referenced_repo=None, manifest=constants.DEFAULT_MANIFEST,
               depth=None, repo_url=constants.REPO_URL, repo_branch=None):
    self.manifest_repo_url = manifest_repo_url
    self.repo_url = repo_url
    self.repo_branch = repo_branch
    self.directory = directory
    self.branch = branch

    # It's perfectly acceptable to pass in a reference pathway that isn't
    # usable.  Detect it, and suppress the setting so that any depth
    # settings aren't disabled due to it.
    if referenced_repo is not None:
      if depth is not None:
        raise ValueError("referenced_repo and depth are mutually exclusive "
                         "options; please pick one or the other.")
      if not IsARepoRoot(referenced_repo):
        referenced_repo = None
    self._referenced_repo = referenced_repo
    self._manifest = manifest

    # If the repo exists already, force a selfupdate as the first step.
    self._repo_update_needed = IsARepoRoot(self.directory)
    if not self._repo_update_needed and git.FindRepoDir(self.directory):
      raise ValueError('Given directory %s is not the root of a repository.'