# 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