Home | History | Annotate | Download | only in tools
      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 
      9 import os
     10 import subprocess
     11 
     12 import misc_utils
     13 
     14 
     15 class 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 
    115 def 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 
    134 def 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 
    150 def 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