Merge pull request #520 from bariscelik/master
[git-cola.git] / cola / git.py
blob945cbc822041c6debfaec56e51efbfa1836a252e
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 cola import core
12 from cola.compat import ustr, PY3
13 from cola.decorators import memoize
14 from cola.interaction import Interaction
17 INDEX_LOCK = threading.Lock()
18 GIT_COLA_TRACE = core.getenv('GIT_COLA_TRACE', '')
19 STATUS = 0
20 STDOUT = 1
21 STDERR = 2
24 def dashify(s):
25 return s.replace('_', '-')
28 def is_git_dir(d):
29 """From git's setup.c:is_git_directory()."""
30 if (core.isdir(d) and core.isdir(join(d, 'objects')) and
31 core.isdir(join(d, 'refs'))):
32 headref = join(d, 'HEAD')
33 return (core.isfile(headref) or
34 (core.islink(headref) and
35 core.readlink(headref).startswith('refs')))
37 return is_git_file(d)
40 def is_git_file(f):
41 return core.isfile(f) and '.git' == os.path.basename(f)
44 def is_git_worktree(d):
45 return is_git_dir(join(d, '.git'))
48 def read_git_file(path):
49 if path is None:
50 return None
51 if is_git_file(path):
52 data = core.read(path).strip()
53 if data.startswith('gitdir: '):
54 return data[len('gitdir: '):]
55 return None
58 class Git(object):
59 """
60 The Git class manages communication with the Git binary
61 """
62 def __init__(self):
63 self._git_cwd = None #: The working directory used by execute()
64 self._worktree = None
65 self._git_file_path = None
66 self.set_worktree(core.getcwd())
68 def set_worktree(self, path):
69 self._git_dir = core.decode(path)
70 self._git_file_path = None
71 self._worktree = None
72 return self.worktree()
74 def worktree(self):
75 if self._worktree:
76 return self._worktree
77 self.git_dir()
78 if self._git_dir:
79 curdir = self._git_dir
80 else:
81 curdir = core.getcwd()
83 if is_git_dir(join(curdir, '.git')):
84 return curdir
86 # Handle bare repositories
87 if (len(os.path.basename(curdir)) > 4
88 and curdir.endswith('.git')):
89 return curdir
90 if 'GIT_WORK_TREE' in os.environ:
91 self._worktree = core.getenv('GIT_WORK_TREE')
92 if not self._worktree or not core.isdir(self._worktree):
93 if self._git_dir:
94 gitparent = join(core.abspath(self._git_dir), '..')
95 self._worktree = core.abspath(gitparent)
96 self.set_cwd(self._worktree)
97 return self._worktree
99 def is_valid(self):
100 return self._git_dir and is_git_dir(self._git_dir)
102 def git_path(self, *paths):
103 if self._git_file_path is None:
104 return join(self.git_dir(), *paths)
105 else:
106 return join(self._git_file_path, *paths)
108 def git_dir(self):
109 if self.is_valid():
110 return self._git_dir
111 if 'GIT_DIR' in os.environ:
112 self._git_dir = core.getenv('GIT_DIR')
113 if self._git_dir:
114 curpath = core.abspath(self._git_dir)
115 else:
116 curpath = core.abspath(core.getcwd())
117 # Search for a .git directory
118 while curpath:
119 if is_git_dir(curpath):
120 self._git_dir = curpath
121 break
122 gitpath = join(curpath, '.git')
123 if is_git_dir(gitpath):
124 self._git_dir = gitpath
125 break
126 curpath, dummy = os.path.split(curpath)
127 if not dummy:
128 break
129 self._git_file_path = read_git_file(self._git_dir)
130 return self._git_dir
132 def set_cwd(self, path):
133 """Sets the current directory."""
134 self._git_cwd = path
136 def __getattr__(self, name):
137 git_cmd = functools.partial(self.git, name)
138 setattr(self, name, git_cmd)
139 return git_cmd
141 @staticmethod
142 def execute(command,
143 _cwd=None,
144 _decode=True,
145 _encoding=None,
146 _raw=False,
147 _stdin=None,
148 _stderr=subprocess.PIPE,
149 _stdout=subprocess.PIPE,
150 _readonly=False):
152 Execute a command and returns its output
154 :param command: argument list to execute.
155 :param _cwd: working directory, defaults to the current directory.
156 :param _decode: whether to decode output, defaults to True.
157 :param _encoding: default encoding, defaults to None (utf-8).
158 :param _raw: do not strip trailing whitespace.
159 :param _stdin: optional stdin filehandle.
160 :returns (status, out, err): exit status, stdout, stderr
163 # Allow the user to have the command executed in their working dir.
164 if not _cwd:
165 _cwd = core.getcwd()
167 extra = {}
168 if sys.platform == 'win32':
169 # If git-cola is invoked on Windows using "start pythonw git-cola",
170 # a console window will briefly flash on the screen each time
171 # git-cola invokes git, which is very annoying. The code below
172 # prevents this by ensuring that any window will be hidden.
173 startupinfo = subprocess.STARTUPINFO()
174 startupinfo.dwFlags = subprocess.STARTF_USESHOWWINDOW
175 startupinfo.wShowWindow = subprocess.SW_HIDE
176 extra['startupinfo'] = startupinfo
178 if hasattr(os, 'setsid'):
179 # SSH uses the SSH_ASKPASS variable only if the process is really
180 # detached from the TTY (stdin redirection and setting the
181 # SSH_ASKPASS environment variable is not enough). To detach a
182 # process from the console it should fork and call os.setsid().
183 extra['preexec_fn'] = os.setsid
185 # Start the process
186 # Guard against thread-unsafe .git/index.lock files
187 if not _readonly:
188 INDEX_LOCK.acquire()
189 status, out, err = core.run_command(command,
190 cwd=_cwd,
191 encoding=_encoding,
192 stdin=_stdin, stdout=_stdout, stderr=_stderr,
193 **extra)
194 # Let the next thread in
195 if not _readonly:
196 INDEX_LOCK.release()
198 if not _raw and out is not None:
199 out = out.rstrip('\n')
201 cola_trace = GIT_COLA_TRACE
202 if cola_trace == 'trace':
203 msg = 'trace: ' + subprocess.list2cmdline(command)
204 Interaction.log_status(status, msg, '')
205 elif cola_trace == 'full':
206 if out or err:
207 core.stderr("%s -> %d: '%s' '%s'" %
208 (' '.join(command), status, out, err))
209 else:
210 core.stderr("%s -> %d" % (' '.join(command), status))
211 elif cola_trace:
212 core.stderr(' '.join(command))
214 # Allow access to the command's status code
215 return (status, out, err)
217 def transform_kwargs(self, **kwargs):
218 """Transform kwargs into git command line options
220 Callers can assume the following behavior:
222 Passing foo=None ignores foo, so that callers can
223 use default values of None that are ignored unless
224 set explicitly.
226 Passing foo=False ignore foo, for the same reason.
228 Passing foo={string-or-number} results in ['--foo=<value>']
229 in the resulting arguments.
232 args = []
233 types_to_stringify = (ustr, int, float, str)
234 if not PY3:
235 types_to_stringify += (long,)
237 for k, v in kwargs.items():
238 if len(k) == 1:
239 dashes = '-'
240 join = ''
241 else:
242 dashes = '--'
243 join = '='
244 type_of_value = type(v)
245 if v is True:
246 args.append('%s%s' % (dashes, dashify(k)))
247 elif type_of_value in types_to_stringify:
248 args.append('%s%s%s%s' % (dashes, dashify(k), join, v))
250 return args
252 def git(self, cmd, *args, **kwargs):
253 # Handle optional arguments prior to calling transform_kwargs
254 # otherwise they'll end up in args, which is bad.
255 _kwargs = dict(_cwd=self._git_cwd)
256 execute_kwargs = (
257 '_cwd',
258 '_decode',
259 '_encoding',
260 '_stdin',
261 '_stdout',
262 '_stderr',
263 '_raw',
264 '_readonly',
266 for kwarg in execute_kwargs:
267 if kwarg in kwargs:
268 _kwargs[kwarg] = kwargs.pop(kwarg)
270 # Prepare the argument list
271 opt_args = self.transform_kwargs(**kwargs)
272 call = ['git', dashify(cmd)] + opt_args
273 call.extend(args)
274 try:
275 return self.execute(call, **_kwargs)
276 except OSError as e:
277 if e.errno != errno.ENOENT:
278 raise e
279 core.stderr("error: unable to execute 'git'\n"
280 "error: please ensure that 'git' is in your $PATH")
281 if sys.platform == 'win32':
282 hint = ('\n'
283 'hint: If you have Git installed in a custom location, e.g.\n'
284 'hint: C:\\Tools\\Git, then you can create a file at\n'
285 'hint: ~/.config/git-cola/git-bindir with the following text\n'
286 'hint: and git-cola will add the specified location to your $PATH\n'
287 'hint: automatically when starting cola:\n'
288 'hint:\n'
289 'hint: C:\\Tools\\Git\\bin\n')
290 core.stderr(hint)
291 sys.exit(1)
294 @memoize
295 def current():
296 """Return the Git singleton"""
297 return Git()
300 git = current()
302 Git command singleton
304 >>> from cola.git import git
305 >>> from cola.git import STDOUT
306 >>> 'git' == git.version()[STDOUT][:3].lower()
307 True