1 from __future__
import division
, absolute_import
, unicode_literals
2 from functools
import partial
5 from os
.path
import join
10 from .compat
import int_types
11 from .compat
import ustr
12 from .compat
import WIN32
13 from .decorators
import memoize
14 from .interaction
import Interaction
17 GIT_COLA_TRACE
= core
.getenv('GIT_COLA_TRACE', '')
18 GIT
= core
.getenv('GIT_COLA_GIT', 'git')
23 # Object ID / SHA1-related constants
24 # Git's empty tree is a built-in constant object name.
25 EMPTY_TREE_OID
= '4b825dc642cb6eb9a060e54bf8d69288fbee4904'
26 # Git's diff machinery returns zeroes for modified files whose content exists
27 # in the worktree only.
28 MISSING_BLOB_OID
= '0000000000000000000000000000000000000000'
29 # Git's SHA-1 object IDs are 40 characters long.
30 # This will need to change when Git moves away from SHA-1.
31 # When that happens we'll have to detect and update this at runtime in
32 # order to support both old and new git.
35 _index_lock
= threading
.Lock()
39 return s
.replace('_', '-')
42 def is_git_dir(git_dir
):
43 """From git's setup.c:is_git_directory()."""
46 headref
= join(git_dir
, 'HEAD')
48 if (core
.isdir(git_dir
) and
49 (core
.isdir(join(git_dir
, 'objects')) and
50 core
.isdir(join(git_dir
, 'refs'))) or
51 (core
.isfile(join(git_dir
, 'gitdir')) and
52 core
.isfile(join(git_dir
, 'commondir')))):
54 result
= (core
.isfile(headref
) or
55 (core
.islink(headref
) and
56 core
.readlink(headref
).startswith('refs/')))
58 result
= is_git_file(git_dir
)
64 return core
.isfile(f
) and os
.path
.basename(f
) == '.git'
67 def is_git_worktree(d
):
68 return is_git_dir(join(d
, '.git'))
71 def is_git_repository(path
):
72 return is_git_worktree(path
) or is_git_dir(path
)
75 def read_git_file(path
):
76 """Read the path from a .git-file
78 `None` is returned when <path> is not a .git-file.
82 if path
and is_git_file(path
):
84 data
= core
.read(path
).strip()
85 if data
.startswith(header
):
86 result
= data
[len(header
):]
87 if result
and not os
.path
.isabs(result
):
88 path_folder
= os
.path
.dirname(path
)
89 repo_relative
= join(path_folder
, result
)
90 result
= os
.path
.normpath(repo_relative
)
95 """Git repository paths of interest"""
97 def __init__(self
, git_dir
=None, git_file
=None,
98 worktree
=None, common_dir
=None):
99 self
.git_dir
= git_dir
100 self
.git_file
= git_file
101 self
.worktree
= worktree
102 self
.common_dir
= common_dir
105 def find_git_directory(curpath
):
106 """Perform Git repository discovery
109 paths
= Paths(git_dir
=core
.getenv('GIT_DIR'),
110 worktree
=core
.getenv('GIT_WORK_TREE'),
112 return find_git_paths(curpath
, paths
)
115 def find_git_paths(curpath
, paths
):
117 ceiling
= core
.getenv('GIT_CEILING_DIRECTORIES')
119 ceiling_dirs
.update([x
for x
in ceiling
.split(':') if x
])
121 if not paths
.git_dir
or not paths
.worktree
:
123 curpath
= core
.abspath(curpath
)
125 # Search for a .git directory
127 if curpath
in ceiling_dirs
:
129 if is_git_dir(curpath
):
130 paths
.git_dir
= curpath
131 if os
.path
.basename(curpath
) == '.git':
132 paths
.worktree
= os
.path
.dirname(curpath
)
134 gitpath
= join(curpath
, '.git')
135 if is_git_dir(gitpath
):
136 paths
.git_dir
= gitpath
137 paths
.worktree
= curpath
139 curpath
, dummy
= os
.path
.split(curpath
)
143 git_dir_path
= read_git_file(paths
.git_dir
)
145 paths
.git_file
= paths
.git_dir
146 paths
.git_dir
= git_dir_path
148 commondir_file
= join(git_dir_path
, 'commondir')
149 if core
.exists(commondir_file
):
150 common_path
= core
.read(commondir_file
).strip()
152 if os
.path
.isabs(common_path
):
153 common_dir
= common_path
155 common_dir
= join(git_dir_path
, common_path
)
156 common_dir
= os
.path
.normpath(common_dir
)
157 paths
.common_dir
= common_dir
164 The Git class manages communication with the Git binary
169 self
._git
_cwd
= None #: The working directory used by execute()
170 self
._valid
= {} #: Store the result of is_git_dir() for performance
171 self
.set_worktree(core
.getcwd())
173 def is_git_repository(self
, path
):
174 return is_git_repository(path
)
179 def _find_git_directory(self
, path
):
181 self
.paths
= find_git_directory(path
)
183 # Update the current directory for executing commands
184 if self
.paths
.worktree
:
185 self
._git
_cwd
= self
.paths
.worktree
186 elif self
.paths
.git_dir
:
187 self
._git
_cwd
= self
.paths
.git_dir
189 def set_worktree(self
, path
):
190 path
= core
.decode(path
)
191 self
._find
_git
_directory
(path
)
192 return self
.paths
.worktree
195 if not self
.paths
.worktree
:
196 path
= core
.abspath(core
.getcwd())
197 self
._find
_git
_directory
(path
)
198 return self
.paths
.worktree
201 """Is this a valid git repository?
203 Cache the result to avoid hitting the filesystem.
206 git_dir
= self
.paths
.git_dir
208 valid
= bool(git_dir
) and self
._valid
[git_dir
]
210 valid
= self
._valid
[git_dir
] = is_git_dir(git_dir
)
214 def git_path(self
, *paths
):
216 if self
.paths
.git_dir
:
217 result
= join(self
.paths
.git_dir
, *paths
)
218 if result
and self
.paths
.common_dir
and not core
.exists(result
):
219 common_result
= join(self
.paths
.common_dir
, *paths
)
220 if core
.exists(common_result
):
221 result
= common_result
225 if not self
.paths
.git_dir
:
226 path
= core
.abspath(core
.getcwd())
227 self
._find
_git
_directory
(path
)
228 return self
.paths
.git_dir
230 def __getattr__(self
, name
):
231 git_cmd
= partial(self
.git
, name
)
232 setattr(self
, name
, git_cmd
)
242 _stderr
=subprocess
.PIPE
,
243 _stdout
=subprocess
.PIPE
,
245 _no_win32_startupinfo
=False):
247 Execute a command and returns its output
249 :param command: argument list to execute.
250 :param _cwd: working directory, defaults to the current directory.
251 :param _decode: whether to decode output, defaults to True.
252 :param _encoding: default encoding, defaults to None (utf-8).
253 :param _raw: do not strip trailing whitespace.
254 :param _stdin: optional stdin filehandle.
255 :returns (status, out, err): exit status, stdout, stderr
258 # Allow the user to have the command executed in their working dir.
264 if hasattr(os
, 'setsid'):
265 # SSH uses the SSH_ASKPASS variable only if the process is really
266 # detached from the TTY (stdin redirection and setting the
267 # SSH_ASKPASS environment variable is not enough). To detach a
268 # process from the console it should fork and call os.setsid().
269 extra
['preexec_fn'] = os
.setsid
272 # Guard against thread-unsafe .git/index.lock files
274 _index_lock
.acquire()
276 status
, out
, err
= core
.run_command(
277 command
, cwd
=_cwd
, encoding
=_encoding
,
278 stdin
=_stdin
, stdout
=_stdout
, stderr
=_stderr
,
279 no_win32_startupinfo
=_no_win32_startupinfo
, **extra
)
281 # Let the next thread in
283 _index_lock
.release()
285 if not _raw
and out
is not None:
286 out
= core
.UStr(out
.rstrip('\n'), out
.encoding
)
288 cola_trace
= GIT_COLA_TRACE
289 if cola_trace
== 'trace':
290 msg
= 'trace: ' + core
.list2cmdline(command
)
291 Interaction
.log_status(status
, msg
, '')
292 elif cola_trace
== 'full':
295 "%s -> %d: '%s' '%s'"
296 % (' '.join(command
), status
, out
, err
))
298 core
.print_stderr("%s -> %d" % (' '.join(command
), status
))
300 core
.print_stderr(' '.join(command
))
302 # Allow access to the command's status code
303 return (status
, out
, err
)
305 def git(self
, cmd
, *args
, **kwargs
):
306 # Handle optional arguments prior to calling transform_kwargs
307 # otherwise they'll end up in args, which is bad.
308 _kwargs
= dict(_cwd
=self
._git
_cwd
)
318 '_no_win32_startupinfo',
321 for kwarg
in execute_kwargs
:
323 _kwargs
[kwarg
] = kwargs
.pop(kwarg
)
325 # Prepare the argument list
326 git_args
= [GIT
, '-c', 'diff.suppressBlankEmpty=false', dashify(cmd
)]
327 opt_args
= transform_kwargs(**kwargs
)
328 call
= git_args
+ opt_args
331 result
= self
.execute(call
, **_kwargs
)
333 if WIN32
and e
.errno
== errno
.ENOENT
:
334 # see if git exists at all. on win32 it can fail with ENOENT in
335 # case of argv overflow. we should be safe from that but use
336 # defensive coding for the worst-case scenario. On UNIX
337 # we have ENAMETOOLONG but that doesn't exist on Windows.
338 if _git_is_installed():
340 _print_win32_git_hint()
341 result
= (1, '', "error: unable to execute '%s'" % GIT
)
345 def _git_is_installed():
346 """Return True if git is installed"""
347 # On win32 Git commands can fail with ENOENT in case of argv overflow. we
348 # should be safe from that but use defensive coding for the worst-case
349 # scenario. On UNIX we have ENAMETOOLONG but that doesn't exist on
352 status
, _
, _
= Git
.execute([GIT
, '--version'])
359 def transform_kwargs(**kwargs
):
360 """Transform kwargs into git command line options
362 Callers can assume the following behavior:
364 Passing foo=None ignores foo, so that callers can
365 use default values of None that are ignored unless
368 Passing foo=False ignore foo, for the same reason.
370 Passing foo={string-or-number} results in ['--foo=<value>']
371 in the resulting arguments.
375 types_to_stringify
= (ustr
, float, str) + int_types
377 for k
, v
in kwargs
.items():
384 # isinstance(False, int) is True, so we have to check bool first
385 if isinstance(v
, bool):
387 args
.append('%s%s' % (dashes
, dashify(k
)))
388 # else: pass # False is ignored; flag=False inhibits --flag
389 elif isinstance(v
, types_to_stringify
):
390 args
.append('%s%s%s%s' % (dashes
, dashify(k
), eq
, v
))
395 def win32_git_error_hint():
398 'NOTE: If you have Git installed in a custom location, e.g.\n'
399 'C:\\Tools\\Git, then you can create a file at\n'
400 '~/.config/git-cola/git-bindir with following text\n'
401 'and git-cola will add the specified location to your $PATH\n'
402 'automatically when starting cola:\n'
408 def _print_win32_git_hint():
409 hint
= '\n' + win32_git_error_hint() + '\n'
410 core
.print_stderr("error: unable to execute 'git'" + hint
)
414 """Create Git instances
417 >>> status, out, err = git.version()
418 >>> 'git' == out[:3].lower()