dag: update column allocation algorithm description
[git-cola.git] / cola / git.py
blob46004e3b3e59c2788b0edda015a082f35aa1f2e1
1 from __future__ import division, absolute_import, unicode_literals
3 import functools
4 import errno
5 import os
6 import sys
7 import subprocess
8 import threading
9 from os.path import join
11 from . import core
12 from .compat import int_types
13 from .compat import ustr
14 from .decorators import memoize
15 from .interaction import Interaction
18 INDEX_LOCK = threading.Lock()
19 GIT_COLA_TRACE = core.getenv('GIT_COLA_TRACE', '')
20 STATUS = 0
21 STDOUT = 1
22 STDERR = 2
25 def dashify(s):
26 return s.replace('_', '-')
29 def is_git_dir(git_dir):
30 """From git's setup.c:is_git_directory()."""
31 result = False
32 if git_dir:
33 headref = join(git_dir, 'HEAD')
35 if (core.isdir(git_dir) and
36 (core.isdir(join(git_dir, 'objects')) and
37 core.isdir(join(git_dir, 'refs'))) or
38 (core.isfile(join(git_dir, 'gitdir')) and
39 core.isfile(join(git_dir, 'commondir')))):
41 result = (core.isfile(headref) or
42 (core.islink(headref) and
43 core.readlink(headref).startswith('refs/')))
44 else:
45 result = is_git_file(git_dir)
47 return result
50 def is_git_file(f):
51 return core.isfile(f) and '.git' == os.path.basename(f)
54 def is_git_worktree(d):
55 return is_git_dir(join(d, '.git'))
58 def read_git_file(path):
59 """Read the path from a .git-file
61 `None` is returned when <path> is not a .git-file.
63 """
64 result = None
65 if path and is_git_file(path):
66 header = 'gitdir: '
67 data = core.read(path).strip()
68 if data.startswith(header):
69 result = data[len(header):]
70 return result
73 class Paths(object):
74 """Git repository paths of interest"""
76 def __init__(self, git_dir=None, git_file=None, worktree=None):
77 self.git_dir = git_dir
78 self.git_file = git_file
79 self.worktree = worktree
82 def find_git_directory(curpath):
83 """Perform Git repository discovery
85 """
86 paths = Paths(git_dir=core.getenv('GIT_DIR'),
87 worktree=core.getenv('GIT_WORK_TREE'),
88 git_file=None)
90 ceiling_dirs = set()
91 ceiling = core.getenv('GIT_CEILING_DIRECTORIES')
92 if ceiling:
93 ceiling_dirs.update([x for x in ceiling.split(':') if x])
95 if not paths.git_dir or not paths.worktree:
96 if curpath:
97 curpath = core.abspath(curpath)
99 # Search for a .git directory
100 while curpath:
101 if curpath in ceiling_dirs:
102 break
103 if is_git_dir(curpath):
104 paths.git_dir = curpath
105 if os.path.basename(curpath) == '.git':
106 paths.worktree = os.path.dirname(curpath)
107 break
108 gitpath = join(curpath, '.git')
109 if is_git_dir(gitpath):
110 paths.git_dir = gitpath
111 paths.worktree = curpath
112 break
113 curpath, dummy = os.path.split(curpath)
114 if not dummy:
115 break
117 git_dir_path = read_git_file(paths.git_dir)
118 if git_dir_path:
119 paths.git_file = paths.git_dir
120 paths.git_dir = git_dir_path
122 return paths
125 class Git(object):
127 The Git class manages communication with the Git binary
129 def __init__(self):
130 self.paths = Paths()
132 self._git_cwd = None #: The working directory used by execute()
133 self._valid = {} #: Store the result of is_git_dir() for performance
134 self.set_worktree(core.getcwd())
136 def getcwd(self):
137 return self._git_cwd
139 def _find_git_directory(self, path):
140 self._git_cwd = None
141 self.paths = find_git_directory(path)
143 # Update the current directory for executing commands
144 if self.paths.worktree:
145 self._git_cwd = self.paths.worktree
146 elif self.paths.git_dir:
147 self._git_cwd = self.paths.git_dir
149 def set_worktree(self, path):
150 path = core.decode(path)
151 self._find_git_directory(path)
152 return self.paths.worktree
154 def worktree(self):
155 if not self.paths.worktree:
156 path = core.abspath(core.getcwd())
157 self._find_git_directory(path)
158 return self.paths.worktree
160 def is_valid(self):
161 """Is this a valid git repository?
163 Cache the result to avoid hitting the filesystem.
166 git_dir = self.paths.git_dir
167 try:
168 valid = bool(git_dir) and self._valid[git_dir]
169 except KeyError:
170 valid = self._valid[git_dir] = is_git_dir(git_dir)
172 return valid
174 def git_path(self, *paths):
175 if self.paths.git_dir:
176 result = join(self.paths.git_dir, *paths)
177 else:
178 result = None
179 return result
181 def git_dir(self):
182 if not self.paths.git_dir:
183 path = core.abspath(core.getcwd())
184 self._find_git_directory(path)
185 return self.paths.git_dir
187 def __getattr__(self, name):
188 git_cmd = functools.partial(self.git, name)
189 setattr(self, name, git_cmd)
190 return git_cmd
192 @staticmethod
193 def execute(command,
194 _cwd=None,
195 _decode=True,
196 _encoding=None,
197 _raw=False,
198 _stdin=None,
199 _stderr=subprocess.PIPE,
200 _stdout=subprocess.PIPE,
201 _readonly=False,
202 _no_win32_startupinfo=False):
204 Execute a command and returns its output
206 :param command: argument list to execute.
207 :param _cwd: working directory, defaults to the current directory.
208 :param _decode: whether to decode output, defaults to True.
209 :param _encoding: default encoding, defaults to None (utf-8).
210 :param _raw: do not strip trailing whitespace.
211 :param _stdin: optional stdin filehandle.
212 :returns (status, out, err): exit status, stdout, stderr
215 # Allow the user to have the command executed in their working dir.
216 if not _cwd:
217 _cwd = core.getcwd()
219 extra = {}
221 if hasattr(os, 'setsid'):
222 # SSH uses the SSH_ASKPASS variable only if the process is really
223 # detached from the TTY (stdin redirection and setting the
224 # SSH_ASKPASS environment variable is not enough). To detach a
225 # process from the console it should fork and call os.setsid().
226 extra['preexec_fn'] = os.setsid
228 # Start the process
229 # Guard against thread-unsafe .git/index.lock files
230 if not _readonly:
231 INDEX_LOCK.acquire()
232 status, out, err = core.run_command(
233 command, cwd=_cwd, encoding=_encoding,
234 stdin=_stdin, stdout=_stdout, stderr=_stderr,
235 no_win32_startupinfo=_no_win32_startupinfo, **extra)
236 # Let the next thread in
237 if not _readonly:
238 INDEX_LOCK.release()
240 if not _raw and out is not None:
241 out = out.rstrip('\n')
243 cola_trace = GIT_COLA_TRACE
244 if cola_trace == 'trace':
245 msg = 'trace: ' + core.list2cmdline(command)
246 Interaction.log_status(status, msg, '')
247 elif cola_trace == 'full':
248 if out or err:
249 core.stderr("%s -> %d: '%s' '%s'" %
250 (' '.join(command), status, out, err))
251 else:
252 core.stderr("%s -> %d" % (' '.join(command), status))
253 elif cola_trace:
254 core.stderr(' '.join(command))
256 # Allow access to the command's status code
257 return (status, out, err)
259 def transform_kwargs(self, **kwargs):
260 """Transform kwargs into git command line options
262 Callers can assume the following behavior:
264 Passing foo=None ignores foo, so that callers can
265 use default values of None that are ignored unless
266 set explicitly.
268 Passing foo=False ignore foo, for the same reason.
270 Passing foo={string-or-number} results in ['--foo=<value>']
271 in the resulting arguments.
274 args = []
275 types_to_stringify = set((ustr, float, str) + int_types)
277 for k, v in kwargs.items():
278 if len(k) == 1:
279 dashes = '-'
280 join = ''
281 else:
282 dashes = '--'
283 join = '='
284 type_of_value = type(v)
285 if v is True:
286 args.append('%s%s' % (dashes, dashify(k)))
287 elif type_of_value in types_to_stringify:
288 args.append('%s%s%s%s' % (dashes, dashify(k), join, v))
290 return args
292 def git(self, cmd, *args, **kwargs):
293 # Handle optional arguments prior to calling transform_kwargs
294 # otherwise they'll end up in args, which is bad.
295 _kwargs = dict(_cwd=self._git_cwd)
296 execute_kwargs = (
297 '_cwd',
298 '_decode',
299 '_encoding',
300 '_stdin',
301 '_stdout',
302 '_stderr',
303 '_raw',
304 '_readonly',
305 '_no_win32_startupinfo',
308 for kwarg in execute_kwargs:
309 if kwarg in kwargs:
310 _kwargs[kwarg] = kwargs.pop(kwarg)
312 # Prepare the argument list
313 git_args = ['git', '-c', 'diff.suppressBlankEmpty=false', dashify(cmd)]
314 opt_args = self.transform_kwargs(**kwargs)
315 call = git_args + opt_args
316 call.extend(args)
317 try:
318 return self.execute(call, **_kwargs)
319 except OSError as e:
320 if e.errno != errno.ENOENT:
321 raise e
323 if WIN32:
324 # see if git exists at all. on win32 it can fail with ENOENT in
325 # case of argv overflow. we should be safe from that but use
326 # defensive coding for the worst-case scenario. on other OS-en
327 # we have ENAMETOOLONG which doesn't exist in with32 API.
328 status, out, err = self.execute(['git', '--version'])
329 if status == 0:
330 raise e
332 core.stderr("error: unable to execute 'git'\n"
333 "error: please ensure that 'git' is in your $PATH")
334 if sys.platform == 'win32':
335 _print_win32_git_hint()
336 sys.exit(1)
339 def _print_win32_git_hint():
340 hint = ('\n'
341 'hint: If you have Git installed in a custom location, e.g.\n'
342 'hint: C:\\Tools\\Git, then you can create a file at\n'
343 'hint: ~/.config/git-cola/git-bindir with following text\n'
344 'hint: and git-cola will add the specified location to your $PATH\n'
345 'hint: automatically when starting cola:\n'
346 'hint:\n'
347 'hint: C:\\Tools\\Git\\bin\n')
348 core.stderr(hint)
351 @memoize
352 def current():
353 """Return the Git singleton"""
354 return Git()
357 git = current()
359 Git command singleton
361 >>> git = current()
362 >>> 'git' == git.version()[0][:3].lower()
363 True