aboutsummaryrefslogtreecommitdiffhomepage
path: root/tools/git_utils.py
blob: a35c85e20e1cd96da4c651dd9c92590cd4b628ed (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# 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