# Copyright 2014 Google Inc.
#
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Module to host the ChangeGitBranch class and test_git_executable function.
"""

import os
import subprocess

import misc_utils


class ChangeGitBranch(object):
    """Class to manage git branches.

    This class allows one to create a new branch in a repository based
    off of a given commit, and restore the original tree state.

    Assumes current working directory is a git repository.

    Example:
        with ChangeGitBranch():
            edit_files(files)
            git_add(files)
            git_commit()
            git_format_patch('HEAD~')
        # At this point, the repository is returned to its original
        # state.

    Constructor Args:
        branch_name: (string) if not None, the name of the branch to
            use.  If None, then use a temporary branch that will be
            deleted.  If the branch already exists, then a different
            branch name will be created.  Use git_branch_name() to
            find the actual branch name used.
        upstream_branch: (string) if not None, the name of the branch or
            commit to branch from.  If None, then use origin/master
        verbose: (boolean) if true, makes debugging easier.

    Raises:
        OSError: the git executable disappeared.
        subprocess.CalledProcessError: git returned unexpected status.
        Exception: if the given branch name exists, or if the repository
            isn't clean on exit, or git can't be found.
    """
    # pylint: disable=I0011,R0903,R0902

    def __init__(self,
                 branch_name=None,
                 upstream_branch=None,
                 verbose=False):
        # pylint: disable=I0011,R0913
        if branch_name:
            self._branch_name = branch_name
            self._delete_branch = False
        else:
            self._branch_name = 'ChangeGitBranchTempBranch'
            self._delete_branch = True

        if upstream_branch:
            self._upstream_branch = upstream_branch
        else:
            self._upstream_branch = 'origin/master'

        self._git = git_executable()
        if not self._git:
            raise Exception('Git can\'t be found.')

        self._stash = None
        self._original_branch = None
        self._vsp = misc_utils.VerboseSubprocess(verbose)

    def _has_git_diff(self):
        """Return true iff repository has uncommited changes."""
        return bool(self._vsp.call([self._git, 'diff', '--quiet', 'HEAD']))

    def _branch_exists(self, branch):
        """Return true iff branch exists."""
        return 0 == self._vsp.call([self._git, 'show-ref', '--quiet', branch])

    def __enter__(self):
        git, vsp = self._git, self._vsp

        if self._branch_exists(self._branch_name):
            i, branch_name = 0, self._branch_name
            while self._branch_exists(branch_name):
                i += 1
                branch_name = '%s_%03d' % (self._branch_name, i)
            self._branch_name = branch_name

        self._stash = self._has_git_diff()
        if self._stash:
            vsp.check_call([git, 'stash', 'save'])
        self._original_branch = git_branch_name(vsp.verbose)
        vsp.check_call(
            [git, 'checkout', '-q', '-b',
             self._branch_name, self._upstream_branch])

    def __exit__(self, etype, value, traceback):
        git, vsp = self._git, self._vsp

        if self._has_git_diff():
            status = vsp.check_output([git, 'status', '-s'])
            raise Exception('git checkout not clean:\n%s' % status)
        vsp.check_call([git, 'checkout', '-q', self._original_branch])
        if self._stash:
            vsp.check_call([git, 'stash', 'pop'])
        if self._delete_branch:
            assert self._original_branch != self._branch_name
            vsp.check_call([git, 'branch', '-D', self._branch_name])


def git_branch_name(verbose=False):
    """Return a description of the current branch.

    Args:
        verbose: (boolean) makes debugging easier

    Returns:
        A string suitable for passing to `git checkout` later.
    """
    git = git_executable()
    vsp = misc_utils.VerboseSubprocess(verbose)
    try:
        full_branch = vsp.strip_output([git, 'symbolic-ref', 'HEAD'])
        return full_branch.split('/')[-1]
    except (subprocess.CalledProcessError,):
        # "fatal: ref HEAD is not a symbolic ref"
        return vsp.strip_output([git, 'rev-parse', 'HEAD'])


def test_git_executable(git):
    """Test the git executable.

    Args:
        git: git executable path.
    Returns:
        True if test is successful.
    """
    with open(os.devnull, 'w') as devnull:
        try:
            subprocess.call([git, '--version'], stdout=devnull)
        except (OSError,):
            return False
    return True


def git_executable():
    """Find the git executable.

    If the GIT_EXECUTABLE environment variable is set, that will
    override whatever is found in the PATH.

    If no suitable executable is found, return None

    Returns:
        A string suiable for passing to subprocess functions, or None.
    """
    env_git = os.environ.get('GIT_EXECUTABLE')
    if env_git and test_git_executable(env_git):
        return env_git
    for git in ('git', 'git.exe', 'git.bat'):
        if test_git_executable(git):
            return git
    return None