gitcfg: Use st_mtime as the cache key
[git-cola.git] / cola / git.py
blob6574e73d0ddd214f52bd5118f76df463a9ce0c74
1 # cmd.py
2 # Copyright (C) 2008, 2009 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
7 import re
8 import os
9 import sys
10 import errno
11 import subprocess
12 import threading
14 from cola import core
15 from cola import errors
17 cmdlock = threading.Lock()
20 def dashify(string):
21 return string.replace('_', '-')
23 # Enables debugging of GitPython's git commands
24 GIT_PYTHON_TRACE = os.environ.get("GIT_PYTHON_TRACE", False)
26 execute_kwargs = ('cwd',
27 'istream',
28 'with_exceptions',
29 'with_raw_output',
30 'with_status',
31 'with_stderr')
33 extra = {}
34 if sys.platform == 'win32':
35 extra = {'shell': True}
37 class Git(object):
38 """
39 The Git class manages communication with the Git binary
40 """
41 def __init__(self):
42 self._git_cwd = None #: The working directory used by execute()
44 def set_cwd(self, path):
45 """Sets the current directory."""
46 self._git_cwd = path
48 def __getattr__(self, name):
49 if name[:1] == '_':
50 raise AttributeError(name)
51 return lambda *args, **kwargs: self._call_process(name, *args, **kwargs)
53 @staticmethod
54 def execute(command,
55 cwd=None,
56 istream=None,
57 with_exceptions=False,
58 with_raw_output=False,
59 with_status=False,
60 with_stderr=False):
61 """
62 Execute a command and returns its output
64 ``command``
65 The command argument list to execute
67 ``istream``
68 Readable filehandle passed to subprocess.Popen.
70 ``cwd``
71 The working directory when running commands.
72 Default: os.getcwd()
74 ``with_status``
75 Whether to return a (status, unicode(output)) tuple.
77 ``with_stderr``
78 Whether to include stderr in the output stream
80 ``with_exceptions``
81 Whether to raise an exception when git returns a non-zero status.
83 ``with_raw_output``
84 Whether to avoid stripping off trailing whitespace.
86 Returns
87 unicode(stdout) # Default
88 unicode(stdout+stderr) # with_stderr=True
89 tuple(int(status), unicode(output)) # with_status=True
91 """
93 if GIT_PYTHON_TRACE and not GIT_PYTHON_TRACE == 'full':
94 print ' '.join(command)
96 # Allow the user to have the command executed in their working dir.
97 if not cwd:
98 cwd = os.getcwd()
100 if with_stderr:
101 stderr = subprocess.STDOUT
102 else:
103 stderr = None
105 if sys.platform == 'win32':
106 command = map(replace_carot, command)
108 # Start the process
109 # Guard against thread-unsafe .git/index.lock files
110 cmdlock.acquire()
111 while True:
112 try:
113 proc = subprocess.Popen(command,
114 cwd=cwd,
115 stdin=istream,
116 stderr=stderr,
117 stdout=subprocess.PIPE,
118 **extra)
119 break
120 except OSError, e:
121 # Some systems interrupt system calls and throw OSError
122 if e.errno == errno.EINTR:
123 continue
124 cmdlock.release()
125 raise e
126 # Wait for the process to return
127 try:
128 output = core.read_nointr(proc.stdout)
129 proc.stdout.close()
130 status = core.wait_nointr(proc)
131 except:
132 status = 202
133 output = str(e)
135 # Let the next thread in
136 cmdlock.release()
138 if with_exceptions and status != 0:
139 cmdstr = 'Error running: %s\n%s' % (' '.join(command), str(e))
140 raise errors.GitCommandError(cmdstr, status, output)
142 if not with_raw_output:
143 output = output.rstrip()
145 if GIT_PYTHON_TRACE == 'full':
146 if output:
147 print "%s -> %d: '%s'" % (command, status, output)
148 else:
149 print "%s -> %d" % (command, status)
151 # Allow access to the command's status code
152 if with_status:
153 return (status, output)
154 else:
155 return output
157 def transform_kwargs(self, **kwargs):
159 Transforms Python style kwargs into git command line options.
161 args = []
162 for k, v in kwargs.items():
163 if len(k) == 1:
164 if v is True:
165 args.append("-%s" % k)
166 elif type(v) is not bool:
167 args.append("-%s%s" % (k, v))
168 else:
169 if v is True:
170 args.append("--%s" % dashify(k))
171 elif type(v) is not bool:
172 args.append("--%s=%s" % (dashify(k), v))
173 return args
175 def _call_process(self, cmd, *args, **kwargs):
177 Run the given git command with the specified arguments and return
178 the result as a String
180 ``cmd``
181 is the command
183 ``args``
184 is the list of arguments
186 ``kwargs``
187 is a dict of keyword arguments.
188 This function accepts the same optional keyword arguments
189 as execute().
191 Examples
192 git.rev_list('master', max_count=10, header=True)
194 Returns
195 Same as execute()
198 # Handle optional arguments prior to calling transform_kwargs
199 # otherwise they'll end up in args, which is bad.
200 _kwargs = dict(cwd=self._git_cwd)
201 for kwarg in execute_kwargs:
202 if kwarg in kwargs:
203 _kwargs[kwarg] = kwargs.pop(kwarg)
205 # Prepare the argument list
206 opt_args = self.transform_kwargs(**kwargs)
207 ext_args = map(core.encode, args)
208 args = opt_args + ext_args
210 call = ['git', dashify(cmd)]
211 call.extend(args)
213 return self.execute(call, **_kwargs)
216 def replace_carot(cmd_arg):
218 Guard against the windows command shell.
220 In the Windows shell, a carat character (^) may be used for
221 line continuation. To guard against this, escape the carat
222 by using two of them.
224 http://technet.microsoft.com/en-us/library/cc723564.aspx
227 return cmd_arg.replace('^', '^^')
230 def shell_quote(*strings):
232 Quote strings so that they can be suitably martialled
233 off to the shell. This method supports POSIX sh syntax.
234 This is crucial to properly handle command line arguments
235 with spaces, quotes, double-quotes, etc. on darwin/win32...
238 regex = re.compile('[^\w!%+,\-./:@^]')
239 quote_regex = re.compile("((?:'\\''){2,})")
241 ret = []
242 for s in strings:
243 if not s:
244 continue
246 if '\x00' in s:
247 raise ValueError('No way to quote strings '
248 'containing null(\\000) bytes')
250 # = does need quoting else in command position it's a
251 # program-local environment setting
252 match = regex.search(s)
253 if match and '=' not in s:
254 # ' -> '\''
255 s = s.replace("'", "'\\''")
257 # make multiple ' in a row look simpler
258 # '\'''\'''\'' -> '"'''"'
259 quote_match = quote_regex.match(s)
260 if quote_match:
261 quotes = match.group(1)
262 s.replace(quotes, ("'" *(len(quotes)/4)) + "\"'")
264 s = "'%s'" % s
265 if s.startswith("''"):
266 s = s[2:]
268 if s.endswith("''"):
269 s = s[:-2]
270 ret.append(s)
271 return ' '.join(ret)