Index: tools/git_utils.py |
diff --git a/tools/git_utils.py b/tools/git_utils.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..a35c85e20e1cd96da4c651dd9c92590cd4b628ed |
--- /dev/null |
+++ b/tools/git_utils.py |
@@ -0,0 +1,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 |
+ |