git.py: workaround osx interrupted system calls
[git-cola.git] / cola / git.py
blobd3b95674b0f4da6abe4ef0f46cdfd5e1ae49feae
1 # cmd.py
2 # Copyright (C) 2008 Michael Trier (mtrier@gmail.com) and contributors
4 # This module is part of GitPython and is released under
5 # the BSD License: http://www.opensource.org/licenses/bsd-license.php
6 import re
7 import os
8 import sys
9 import subprocess
10 from cola.exception import GitCommandError
12 def dashify(string):
13 return string.replace('_', '-')
15 # Enables debugging of GitPython's git commands
16 GIT_PYTHON_TRACE = os.environ.get("GIT_PYTHON_TRACE", False)
18 execute_kwargs = ('istream', 'with_keep_cwd', 'with_extended_output',
19 'with_exceptions', 'with_raw_output')
21 class Git(object):
22 """
23 The Git class manages communication with the Git binary
24 """
25 def __init__(self):
26 self._git_cwd = None
28 def set_cwd(self, path):
29 self._git_cwd = path
31 def __getattr__(self, name):
32 if name[0] == '_':
33 raise AttributeError(name)
34 return lambda *args, **kwargs: self._call_process(name, *args, **kwargs)
36 @staticmethod
37 def execute(command,
38 cwd=None,
39 istream=None,
40 with_keep_cwd=False,
41 with_extended_output=False,
42 with_exceptions=True,
43 with_raw_output=False):
44 """
45 Handles executing the command on the shell and consumes and returns
46 the returned information (stdout)
48 ``command``
49 The command argument list to execute
51 ``istream``
52 Standard input filehandle passed to subprocess.Popen.
54 ``with_keep_cwd``
55 Whether to use the current working directory from os.getcwd().
56 GitPython uses the cwd set by set_cwd() by default.
58 ``with_extended_output``
59 Whether to return a (status, stdout, stderr) tuple.
61 ``with_exceptions``
62 Whether to raise an exception when git returns a non-zero status.
64 ``with_raw_output``
65 Whether to avoid stripping off trailing whitespace.
67 Returns
68 str(output) # extended_output = False (Default)
69 tuple(int(status), str(output)) # extended_output = True
70 """
72 if GIT_PYTHON_TRACE and not GIT_PYTHON_TRACE == 'full':
73 print ' '.join(command)
75 # Allow the user to have the command executed in their working dir.
76 if with_keep_cwd or not cwd:
77 cwd = os.getcwd()
79 # Start the process
80 wanky = sys.platform in ('win32',)
81 if wanky:
82 command = shell_quote(*command)
84 proc = subprocess.Popen(command,
85 cwd=cwd,
86 shell=wanky,
87 stdin=istream,
88 stderr=subprocess.PIPE,
89 stdout=subprocess.PIPE)
90 count = 0
91 stdout_value = None
92 stderr_value = None
93 while count < 4: # osx interrupts system calls
94 count += 1
95 try:
96 stdout_value, stderr_value = proc.communicate()
97 break
98 except:
99 pass
100 status = proc.returncode
101 if with_exceptions and status:
102 raise GitCommandError(command, status, stderr_value)
104 if not stdout_value:
105 stdout_value = ''
106 if not stderr_value:
107 stderr_value = ''
108 if not with_raw_output:
109 stdout_value = stdout_value.strip()
110 stderr_value = stderr_value.strip()
112 if GIT_PYTHON_TRACE == 'full':
113 if stderr_value:
114 print "%s -> %d: '%s' !! '%s'" % (command, status, stdout_value, stderr_value)
115 elif stdout_value:
116 print "%s -> %d: '%s'" % (command, status, stdout_value)
117 else:
118 print "%s -> %d" % (command, status)
120 # Allow access to the command's status code
121 if with_extended_output:
122 return (status, stdout_value, stderr_value)
123 else:
124 if stdout_value and stderr_value:
125 return stderr_value + '\n' + stdout_value
126 elif stdout_value:
127 return stdout_value
128 else:
129 return stderr_value
131 def transform_kwargs(self, **kwargs):
133 Transforms Python style kwargs into git command line options.
135 args = []
136 for k, v in kwargs.items():
137 if len(k) == 1:
138 if v is True:
139 args.append("-%s" % k)
140 elif type(v) is not bool:
141 args.append("-%s%s" % (k, v))
142 else:
143 if v is True:
144 args.append("--%s" % dashify(k))
145 elif type(v) is not bool:
146 args.append("--%s=%s" % (dashify(k), v))
147 return args
149 def _call_process(self, method, *args, **kwargs):
151 Run the given git command with the specified arguments and return
152 the result as a String
154 ``method``
155 is the command
157 ``args``
158 is the list of arguments
160 ``kwargs``
161 is a dict of keyword arguments.
162 This function accepts the same optional keyword arguments
163 as execute().
165 Examples
166 git.rev_list('master', max_count=10, header=True)
168 Returns
169 Same as execute()
172 # Handle optional arguments prior to calling transform_kwargs
173 # otherwise these'll end up in args, which is bad.
174 _kwargs = dict(cwd=self._git_cwd)
175 for kwarg in execute_kwargs:
176 try:
177 _kwargs[kwarg] = kwargs.pop(kwarg)
178 except KeyError:
179 pass
181 # Prepare the argument list
182 opt_args = self.transform_kwargs(**kwargs)
183 ext_args = [a.encode('utf-8') for a in args]
184 args = opt_args + ext_args
186 call = ['git', dashify(method)]
187 call.extend(args)
189 return Git.execute(call, **_kwargs)
192 def shell_quote(*inputs):
194 Quote strings so that they can be suitably martialled
195 off to the shell. This method supports POSIX sh syntax.
196 This is crucial to properly handle command line arguments
197 with spaces, quotes, double-quotes, etc. on darwin/win32...
200 regex = re.compile('[^\w!%+,\-./:@^]')
201 quote_regex = re.compile("((?:'\\''){2,})")
203 ret = []
204 for input in inputs:
205 if not input:
206 continue
208 if '\x00' in input:
209 raise AssertionError,('No way to quote strings '
210 'containing null(\\000) bytes')
212 # = does need quoting else in command position it's a
213 # program-local environment setting
214 match = regex.search(input)
215 if match and '=' not in input:
216 # ' -> '\''
217 input = input.replace("'", "'\\''")
219 # make multiple ' in a row look simpler
220 # '\'''\'''\'' -> '"'''"'
221 quote_match = quote_regex.match(input)
222 if quote_match:
223 quotes = match.group(1)
224 input.replace(quotes, ("'" *(len(quotes)/4)) + "\"'")
226 input = "'%s'" % input
227 if input.startswith("''"):
228 input = input[2:]
230 if input.endswith("''"):
231 input = input[:-2]
232 ret.append(input)
233 return ' '.join(ret)