main.view: Fix interactive diff font setting
[git-cola.git] / cola / git.py
blob00436f85ba348e3b0fc1950d829c827025538d47
1 import os
2 import sys
3 import errno
4 import subprocess
5 import threading
6 cmdlock = threading.Lock()
8 import cola
9 from cola import core
10 from cola import signals
11 from cola.decorators import memoize
13 GIT_COLA_TRACE = os.getenv('GIT_COLA_TRACE', '')
16 def dashify(string):
17 return string.replace('_', '-')
20 class Git(object):
21 """
22 The Git class manages communication with the Git binary
23 """
24 def __init__(self):
25 self._git_cwd = None #: The working directory used by execute()
26 self.set_worktree(os.getcwd())
28 def set_worktree(self, path):
29 self._git_dir = path
30 self._worktree = None
31 self.worktree()
33 def worktree(self):
34 if self._worktree:
35 return self._worktree
36 self.git_dir()
37 if self._git_dir:
38 curdir = self._git_dir
39 else:
40 curdir = os.getcwd()
42 if self._is_git_dir(os.path.join(curdir, '.git')):
43 return curdir
45 # Handle bare repositories
46 if (len(os.path.basename(curdir)) > 4
47 and curdir.endswith('.git')):
48 return curdir
49 if 'GIT_WORK_TREE' in os.environ:
50 self._worktree = os.getenv('GIT_WORK_TREE')
51 if not self._worktree or not os.path.isdir(self._worktree):
52 if self._git_dir:
53 gitparent = os.path.join(os.path.abspath(self._git_dir), '..')
54 self._worktree = os.path.abspath(gitparent)
55 self.set_cwd(self._worktree)
56 return self._worktree
58 def is_valid(self):
59 return self._git_dir and self._is_git_dir(self._git_dir)
61 def git_path(self, *paths):
62 return os.path.join(self.git_dir(), *paths)
64 def git_dir(self):
65 if self.is_valid():
66 return self._git_dir
67 if 'GIT_DIR' in os.environ:
68 self._git_dir = os.getenv('GIT_DIR')
69 if self._git_dir:
70 curpath = os.path.abspath(self._git_dir)
71 else:
72 curpath = os.path.abspath(os.getcwd())
73 # Search for a .git directory
74 while curpath:
75 if self._is_git_dir(curpath):
76 self._git_dir = curpath
77 break
78 gitpath = os.path.join(curpath, '.git')
79 if self._is_git_dir(gitpath):
80 self._git_dir = gitpath
81 break
82 curpath, dummy = os.path.split(curpath)
83 if not dummy:
84 break
85 return self._git_dir
87 def _is_git_dir(self, d):
88 """From git's setup.c:is_git_directory()."""
89 if (os.path.isdir(d)
90 and os.path.isdir(os.path.join(d, 'objects'))
91 and os.path.isdir(os.path.join(d, 'refs'))):
92 headref = os.path.join(d, 'HEAD')
93 return (os.path.isfile(headref)
94 or (os.path.islink(headref)
95 and os.readlink(headref).startswith('refs')))
96 return False
98 def set_cwd(self, path):
99 """Sets the current directory."""
100 self._git_cwd = path
102 def __getattr__(self, name):
103 if name[:1] == '_':
104 raise AttributeError(name)
105 return lambda *args, **kwargs: self._call_process(name, *args, **kwargs)
107 @staticmethod
108 def execute(command,
109 cwd=None,
110 istream=None,
111 with_exceptions=False,
112 with_raw_output=False,
113 with_status=False,
114 with_stderr=False,
115 cola_trace=GIT_COLA_TRACE):
117 Execute a command and returns its output
119 ``command``
120 The command argument list to execute
122 ``istream``
123 Readable filehandle passed to subprocess.Popen.
125 ``cwd``
126 The working directory when running commands.
127 Default: os.getcwd()
129 ``with_status``
130 Whether to return a (status, unicode(output)) tuple.
132 ``with_stderr``
133 Whether to include stderr in the output stream
135 ``with_exceptions``
136 Whether to raise an exception when git returns a non-zero status.
138 ``with_raw_output``
139 Whether to avoid stripping off trailing whitespace.
141 Returns
142 unicode(stdout) # Default
143 unicode(stdout+stderr) # with_stderr=True
144 tuple(int(status), unicode(output)) # with_status=True
148 # Allow the user to have the command executed in their working dir.
149 if not cwd:
150 cwd = os.getcwd()
152 extra = {}
153 if sys.platform == 'win32':
154 command = map(replace_carot, command)
155 extra = {'shell': True}
157 # Start the process
158 # Guard against thread-unsafe .git/index.lock files
159 cmdlock.acquire()
160 # Some systems (e.g. darwin) interrupt system calls
161 count = 0
162 while count < 13:
163 try:
164 proc = subprocess.Popen(command,
165 cwd=cwd,
166 stdin=istream,
167 stderr=subprocess.PIPE,
168 stdout=subprocess.PIPE,
169 **extra)
170 # Wait for the process to return
171 out, err = proc.communicate()
172 status = proc.returncode
173 break
174 except OSError, e:
175 if e.errno == errno.EINTR or e.errno == errno.ENOMEM:
176 count += 1
177 continue
178 cmdlock.release()
179 raise
180 except:
181 cmdlock.release()
182 raise
183 # Let the next thread in
184 cmdlock.release()
185 output = with_stderr and (out+err) or out
186 if not with_raw_output:
187 output = output.rstrip('\n')
189 if cola_trace == 'trace':
190 msg = 'trace: ' + subprocess.list2cmdline(command)
191 cola.notifier().broadcast(signals.log_cmd, status, msg)
192 elif cola_trace == 'full':
193 if output:
194 print "%s -> %d: '%s'" % (command, status, output)
195 else:
196 print "%s -> %d" % (command, status)
197 elif cola_trace:
198 print ' '.join(command)
200 # Allow access to the command's status code
201 if with_status:
202 return (status, output)
203 else:
204 return output
206 def transform_kwargs(self, **kwargs):
208 Transforms Python style kwargs into git command line options.
210 args = []
211 for k, v in kwargs.items():
212 if len(k) == 1:
213 if v is True:
214 args.append("-%s" % k)
215 elif type(v) is not bool:
216 args.append("-%s%s" % (k, v))
217 else:
218 if v is True:
219 args.append("--%s" % dashify(k))
220 elif type(v) is not bool:
221 args.append("--%s=%s" % (dashify(k), v))
222 return args
224 def _call_process(self, cmd, *args, **kwargs):
226 Run the given git command with the specified arguments and return
227 the result as a String
229 ``cmd``
230 is the command
232 ``args``
233 is the list of arguments
235 ``kwargs``
236 is a dict of keyword arguments.
237 This function accepts the same optional keyword arguments
238 as execute().
240 Examples
241 git.rev_list('master', max_count=10, header=True)
243 Returns
244 Same as execute()
248 # Handle optional arguments prior to calling transform_kwargs
249 # otherwise they'll end up in args, which is bad.
250 _kwargs = dict(cwd=self._git_cwd)
251 execute_kwargs = ('cwd', 'istream',
252 'with_exceptions',
253 'with_raw_output',
254 'with_status',
255 'with_stderr')
257 for kwarg in execute_kwargs:
258 if kwarg in kwargs:
259 _kwargs[kwarg] = kwargs.pop(kwarg)
261 # Prepare the argument list
262 opt_args = self.transform_kwargs(**kwargs)
263 ext_args = map(core.encode, args)
264 args = opt_args + ext_args
266 call = ['git', dashify(cmd)]
267 call.extend(args)
269 return self.execute(call, **_kwargs)
272 def replace_carot(cmd_arg):
274 Guard against the windows command shell.
276 In the Windows shell, a carat character (^) may be used for
277 line continuation. To guard against this, escape the carat
278 by using two of them.
280 http://technet.microsoft.com/en-us/library/cc723564.aspx
283 return cmd_arg.replace('^', '^^')
286 @memoize
287 def instance():
288 """Return the Git singleton"""
289 return Git()
292 git = instance()
294 Git command singleton
296 >>> from cola.git import git
297 >>> 'git' == git.version()[:3]
298 True