blob: a35c85e20e1cd96da4c651dd9c92590cd4b628ed [file] [log] [blame]
# 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