gitcmds: "globals to locals" micro-optimization
[git-cola.git] / cola / git.py
blobcc0311c30acc622a35cbd4def7f2ff65f1089afd
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 commands
12 import subprocess
13 import threading
15 import cola
16 from cola import core
17 from cola import errors
18 from cola import signals
19 from cola.decorators import memoize
21 cmdlock = threading.Lock()
24 @memoize
25 def instance():
26 """Return the GitCola singleton"""
27 return GitCola()
30 def dashify(string):
31 return string.replace('_', '-')
33 # Enables debugging of GitPython's git commands
34 GIT_PYTHON_TRACE = os.getenv('GIT_PYTHON_TRACE', False)
35 GIT_COLA_TRACE = False
37 execute_kwargs = ('cwd',
38 'istream',
39 'with_exceptions',
40 'with_raw_output',
41 'with_status',
42 'with_stderr')
44 extra = {}
45 if sys.platform == 'win32':
46 extra = {'shell': True}
49 class Git(object):
50 """
51 The Git class manages communication with the Git binary
52 """
53 def __init__(self):
54 self._git_cwd = None #: The working directory used by execute()
56 def set_cwd(self, path):
57 """Sets the current directory."""
58 self._git_cwd = path
60 def __getattr__(self, name):
61 if name[:1] == '_':
62 raise AttributeError(name)
63 return lambda *args, **kwargs: self._call_process(name, *args, **kwargs)
65 @staticmethod
66 def execute(command,
67 cwd=None,
68 istream=None,
69 with_exceptions=False,
70 with_raw_output=False,
71 with_status=False,
72 with_stderr=False):
73 """
74 Execute a command and returns its output
76 ``command``
77 The command argument list to execute
79 ``istream``
80 Readable filehandle passed to subprocess.Popen.
82 ``cwd``
83 The working directory when running commands.
84 Default: os.getcwd()
86 ``with_status``
87 Whether to return a (status, unicode(output)) tuple.
89 ``with_stderr``
90 Whether to include stderr in the output stream
92 ``with_exceptions``
93 Whether to raise an exception when git returns a non-zero status.
95 ``with_raw_output``
96 Whether to avoid stripping off trailing whitespace.
98 Returns
99 unicode(stdout) # Default
100 unicode(stdout+stderr) # with_stderr=True
101 tuple(int(status), unicode(output)) # with_status=True
105 if GIT_PYTHON_TRACE and not GIT_PYTHON_TRACE == 'full':
106 print ' '.join(command)
108 # Allow the user to have the command executed in their working dir.
109 if not cwd:
110 cwd = os.getcwd()
112 if with_stderr:
113 stderr = subprocess.STDOUT
114 else:
115 stderr = None
117 if sys.platform == 'win32':
118 command = map(replace_carot, command)
120 # Start the process
121 # Guard against thread-unsafe .git/index.lock files
122 cmdlock.acquire()
123 while True:
124 try:
125 proc = subprocess.Popen(command,
126 cwd=cwd,
127 stdin=istream,
128 stderr=stderr,
129 stdout=subprocess.PIPE,
130 **extra)
131 break
132 except OSError, e:
133 # Some systems interrupt system calls and throw OSError
134 if e.errno == errno.EINTR:
135 continue
136 cmdlock.release()
137 raise e
138 # Wait for the process to return
139 try:
140 output = core.read_nointr(proc.stdout)
141 proc.stdout.close()
142 status = core.wait_nointr(proc)
143 except:
144 status = 202
145 output = str(e)
147 # Let the next thread in
148 cmdlock.release()
150 if with_exceptions and status != 0:
151 cmdstr = 'Error running: %s\n%s' % (' '.join(command), str(e))
152 raise errors.GitCommandError(cmdstr, status, output)
154 if not with_raw_output:
155 output = output.rstrip()
157 if GIT_PYTHON_TRACE == 'full':
158 if output:
159 print "%s -> %d: '%s'" % (command, status, output)
160 else:
161 print "%s -> %d" % (command, status)
163 if GIT_COLA_TRACE:
164 msg = 'trace: %s' % ' '.join(map(commands.mkarg, command))
165 cola.notifier().broadcast(signals.log_cmd, status, msg)
167 # Allow access to the command's status code
168 if with_status:
169 return (status, output)
170 else:
171 return output
173 def transform_kwargs(self, **kwargs):
175 Transforms Python style kwargs into git command line options.
177 args = []
178 for k, v in kwargs.items():
179 if len(k) == 1:
180 if v is True:
181 args.append("-%s" % k)
182 elif type(v) is not bool:
183 args.append("-%s%s" % (k, v))
184 else:
185 if v is True:
186 args.append("--%s" % dashify(k))
187 elif type(v) is not bool:
188 args.append("--%s=%s" % (dashify(k), v))
189 return args
191 def _call_process(self, cmd, *args, **kwargs):
193 Run the given git command with the specified arguments and return
194 the result as a String
196 ``cmd``
197 is the command
199 ``args``
200 is the list of arguments
202 ``kwargs``
203 is a dict of keyword arguments.
204 This function accepts the same optional keyword arguments
205 as execute().
207 Examples
208 git.rev_list('master', max_count=10, header=True)
210 Returns
211 Same as execute()
215 # Handle optional arguments prior to calling transform_kwargs
216 # otherwise they'll end up in args, which is bad.
217 _kwargs = dict(cwd=self._git_cwd)
218 for kwarg in execute_kwargs:
219 if kwarg in kwargs:
220 _kwargs[kwarg] = kwargs.pop(kwarg)
222 # Prepare the argument list
223 opt_args = self.transform_kwargs(**kwargs)
224 ext_args = map(core.encode, args)
225 args = opt_args + ext_args
227 call = ['git', dashify(cmd)]
228 call.extend(args)
230 return self.execute(call, **_kwargs)
233 def replace_carot(cmd_arg):
235 Guard against the windows command shell.
237 In the Windows shell, a carat character (^) may be used for
238 line continuation. To guard against this, escape the carat
239 by using two of them.
241 http://technet.microsoft.com/en-us/library/cc723564.aspx
244 return cmd_arg.replace('^', '^^')
247 class GitCola(Git):
249 Subclass Git to provide search-for-git-dir
252 def __init__(self):
253 Git.__init__(self)
254 self.load_worktree(os.getcwd())
256 def load_worktree(self, path):
257 self._git_dir = path
258 self._worktree = None
259 self.worktree()
261 def worktree(self):
262 if self._worktree:
263 return self._worktree
264 self.git_dir()
265 if self._git_dir:
266 curdir = self._git_dir
267 else:
268 curdir = os.getcwd()
270 if self._is_git_dir(os.path.join(curdir, '.git')):
271 return curdir
273 # Handle bare repositories
274 if (len(os.path.basename(curdir)) > 4
275 and curdir.endswith('.git')):
276 return curdir
277 if 'GIT_WORK_TREE' in os.environ:
278 self._worktree = os.getenv('GIT_WORK_TREE')
279 if not self._worktree or not os.path.isdir(self._worktree):
280 if self._git_dir:
281 gitparent = os.path.join(os.path.abspath(self._git_dir), '..')
282 self._worktree = os.path.abspath(gitparent)
283 self.set_cwd(self._worktree)
284 return self._worktree
286 def is_valid(self):
287 return self._git_dir and self._is_git_dir(self._git_dir)
289 def git_path(self, *paths):
290 return os.path.join(self.git_dir(), *paths)
292 def git_dir(self):
293 if self.is_valid():
294 return self._git_dir
295 if 'GIT_DIR' in os.environ:
296 self._git_dir = os.getenv('GIT_DIR')
297 if self._git_dir:
298 curpath = os.path.abspath(self._git_dir)
299 else:
300 curpath = os.path.abspath(os.getcwd())
301 # Search for a .git directory
302 while curpath:
303 if self._is_git_dir(curpath):
304 self._git_dir = curpath
305 break
306 gitpath = os.path.join(curpath, '.git')
307 if self._is_git_dir(gitpath):
308 self._git_dir = gitpath
309 break
310 curpath, dummy = os.path.split(curpath)
311 if not dummy:
312 break
313 return self._git_dir
315 def _is_git_dir(self, d):
316 """From git's setup.c:is_git_directory()."""
317 if (os.path.isdir(d)
318 and os.path.isdir(os.path.join(d, 'objects'))
319 and os.path.isdir(os.path.join(d, 'refs'))):
320 headref = os.path.join(d, 'HEAD')
321 return (os.path.isfile(headref)
322 or (os.path.islink(headref)
323 and os.readlink(headref).startswith('refs')))
324 return False