OLD | NEW |
| (Empty) |
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 | |
OLD | NEW |