1# Copyright 2014 Google Inc.
2#
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Module to host the ChangeGitBranch class and test_git_executable function.
7"""
8
9import os
10import subprocess
11
12import misc_utils
13
14
15class ChangeGitBranch(object):
16    """Class to manage git branches.
17
18    This class allows one to create a new branch in a repository based
19    off of a given commit, and restore the original tree state.
20
21    Assumes current working directory is a git repository.
22
23    Example:
24        with ChangeGitBranch():
25            edit_files(files)
26            git_add(files)
27            git_commit()
28            git_format_patch('HEAD~')
29        # At this point, the repository is returned to its original
30        # state.
31
32    Constructor Args:
33        branch_name: (string) if not None, the name of the branch to
34            use.  If None, then use a temporary branch that will be
35            deleted.  If the branch already exists, then a different
36            branch name will be created.  Use git_branch_name() to
37            find the actual branch name used.
38        upstream_branch: (string) if not None, the name of the branch or
39            commit to branch from.  If None, then use origin/master
40        verbose: (boolean) if true, makes debugging easier.
41
42    Raises:
43        OSError: the git executable disappeared.
44        subprocess.CalledProcessError: git returned unexpected status.
45        Exception: if the given branch name exists, or if the repository
46            isn't clean on exit, or git can't be found.
47    """
48    # pylint: disable=I0011,R0903,R0902
49
50    def __init__(self,
51                 branch_name=None,
52                 upstream_branch=None,
53                 verbose=False):
54        # pylint: disable=I0011,R0913
55        if branch_name:
56            self._branch_name = branch_name
57            self._delete_branch = False
58        else:
59            self._branch_name = 'ChangeGitBranchTempBranch'
60            self._delete_branch = True
61
62        if upstream_branch:
63            self._upstream_branch = upstream_branch
64        else:
65            self._upstream_branch = 'origin/master'
66
67        self._git = git_executable()
68        if not self._git:
69            raise Exception('Git can\'t be found.')
70
71        self._stash = None
72        self._original_branch = None
73        self._vsp = misc_utils.VerboseSubprocess(verbose)
74
75    def _has_git_diff(self):
76        """Return true iff repository has uncommited changes."""
77        return bool(self._vsp.call([self._git, 'diff', '--quiet', 'HEAD']))
78
79    def _branch_exists(self, branch):
80        """Return true iff branch exists."""
81        return 0 == self._vsp.call([self._git, 'show-ref', '--quiet', branch])
82
83    def __enter__(self):
84        git, vsp = self._git, self._vsp
85
86        if self._branch_exists(self._branch_name):
87            i, branch_name = 0, self._branch_name
88            while self._branch_exists(branch_name):
89                i += 1
90                branch_name = '%s_%03d' % (self._branch_name, i)
91            self._branch_name = branch_name
92
93        self._stash = self._has_git_diff()
94        if self._stash:
95            vsp.check_call([git, 'stash', 'save'])
96        self._original_branch = git_branch_name(vsp.verbose)
97        vsp.check_call(
98            [git, 'checkout', '-q', '-b',
99             self._branch_name, self._upstream_branch])
100
101    def __exit__(self, etype, value, traceback):
102        git, vsp = self._git, self._vsp
103
104        if self._has_git_diff():
105            status = vsp.check_output([git, 'status', '-s'])
106            raise Exception('git checkout not clean:\n%s' % status)
107        vsp.check_call([git, 'checkout', '-q', self._original_branch])
108        if self._stash:
109            vsp.check_call([git, 'stash', 'pop'])
110        if self._delete_branch:
111            assert self._original_branch != self._branch_name
112            vsp.check_call([git, 'branch', '-D', self._branch_name])
113
114
115def git_branch_name(verbose=False):
116    """Return a description of the current branch.
117
118    Args:
119        verbose: (boolean) makes debugging easier
120
121    Returns:
122        A string suitable for passing to `git checkout` later.
123    """
124    git = git_executable()
125    vsp = misc_utils.VerboseSubprocess(verbose)
126    try:
127        full_branch = vsp.strip_output([git, 'symbolic-ref', 'HEAD'])
128        return full_branch.split('/')[-1]
129    except (subprocess.CalledProcessError,):
130        # "fatal: ref HEAD is not a symbolic ref"
131        return vsp.strip_output([git, 'rev-parse', 'HEAD'])
132
133
134def test_git_executable(git):
135    """Test the git executable.
136
137    Args:
138        git: git executable path.
139    Returns:
140        True if test is successful.
141    """
142    with open(os.devnull, 'w') as devnull:
143        try:
144            subprocess.call([git, '--version'], stdout=devnull)
145        except (OSError,):
146            return False
147    return True
148
149
150def git_executable():
151    """Find the git executable.
152
153    If the GIT_EXECUTABLE environment variable is set, that will
154    override whatever is found in the PATH.
155
156    If no suitable executable is found, return None
157
158    Returns:
159        A string suiable for passing to subprocess functions, or None.
160    """
161    env_git = os.environ.get('GIT_EXECUTABLE')
162    if env_git and test_git_executable(env_git):
163        return env_git
164    for git in ('git', 'git.exe', 'git.bat'):
165        if test_git_executable(git):
166            return git
167    return None
168
169