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
|