1 from functools
import partial
4 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 / SHA-1 / SHA-256-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 (20 bytes).
30 # Git's SHA-256 object IDs are 64 characters long (32 bytes).
31 # This will need to change when Git moves away from SHA-1.
32 # When that happens we'll have to detect and update this at runtime in
33 # order to support both old and new git.
35 OID_LENGTH_SHA256
= 64
37 _index_lock
= threading
.Lock()
41 return value
.replace('_', '-')
44 def is_git_dir(git_dir
):
45 """From git's setup.c:is_git_directory()."""
48 headref
= join(git_dir
, 'HEAD')
53 core
.isdir(join(git_dir
, 'objects'))
54 and core
.isdir(join(git_dir
, 'refs'))
57 core
.isfile(join(git_dir
, 'gitdir'))
58 and core
.isfile(join(git_dir
, 'commondir'))
61 result
= core
.isfile(headref
) or (
62 core
.islink(headref
) and core
.readlink(headref
).startswith('refs/')
65 result
= is_git_file(git_dir
)
70 def is_git_file(filename
):
71 return core
.isfile(filename
) and os
.path
.basename(filename
) == '.git'
74 def is_git_worktree(dirname
):
75 return is_git_dir(join(dirname
, '.git'))
78 def is_git_repository(path
):
79 return is_git_worktree(path
) or is_git_dir(path
)
82 def read_git_file(path
):
83 """Read the path from a .git-file
85 `None` is returned when <path> is not a .git-file.
89 if path
and is_git_file(path
):
91 data
= core
.read(path
).strip()
92 if data
.startswith(header
):
93 result
= data
[len(header
) :]
94 if result
and not os
.path
.isabs(result
):
95 path_folder
= os
.path
.dirname(path
)
96 repo_relative
= join(path_folder
, result
)
97 result
= os
.path
.normpath(repo_relative
)
102 """Git repository paths of interest"""
104 def __init__(self
, git_dir
=None, git_file
=None, worktree
=None, common_dir
=None):
105 if git_dir
and not is_git_dir(git_dir
):
107 self
.git_dir
= git_dir
108 self
.git_file
= git_file
109 self
.worktree
= worktree
110 self
.common_dir
= common_dir
113 """Search for git worktrees and bare repositories"""
114 if not self
.git_dir
or not self
.worktree
:
116 ceiling
= core
.getenv('GIT_CEILING_DIRECTORIES')
118 ceiling_dirs
.update([x
for x
in ceiling
.split(os
.pathsep
) if x
])
120 path
= core
.abspath(path
)
121 self
._search
_for
_git
(path
, ceiling_dirs
)
124 git_dir_path
= read_git_file(self
.git_dir
)
126 self
.git_file
= self
.git_dir
127 self
.git_dir
= git_dir_path
129 commondir_file
= join(git_dir_path
, 'commondir')
130 if core
.exists(commondir_file
):
131 common_path
= core
.read(commondir_file
).strip()
133 if os
.path
.isabs(common_path
):
134 common_dir
= common_path
136 common_dir
= join(git_dir_path
, common_path
)
137 common_dir
= os
.path
.normpath(common_dir
)
138 self
.common_dir
= common_dir
139 # usage: Paths().get()
142 def _search_for_git(self
, path
, ceiling_dirs
):
143 """Search for git repositories located at path or above"""
145 if path
in ceiling_dirs
:
150 basename
= os
.path
.basename(path
)
151 if not self
.worktree
and basename
== '.git':
152 self
.worktree
= os
.path
.dirname(path
)
153 # We are either in a bare repository, or someone set GIT_DIR
154 # but did not set GIT_WORK_TREE.
156 if not self
.worktree
:
157 basename
= os
.path
.basename(self
.git_dir
)
158 if basename
== '.git':
159 self
.worktree
= os
.path
.dirname(self
.git_dir
)
160 elif path
and not is_git_dir(path
):
163 gitpath
= join(path
, '.git')
164 if is_git_dir(gitpath
):
166 self
.git_dir
= gitpath
167 if not self
.worktree
:
170 path
, dummy
= os
.path
.split(path
)
175 def find_git_directory(path
):
176 """Perform Git repository discovery"""
178 git_dir
=core
.getenv('GIT_DIR'), worktree
=core
.getenv('GIT_WORK_TREE')
184 The Git class manages communication with the Git binary
190 self
._valid
= {} #: Store the result of is_git_dir() for performance
191 self
.set_worktree(core
.getcwd())
193 def is_git_repository(self
, path
):
194 return is_git_repository(path
)
197 """Return the working directory used by git()"""
198 return self
.paths
.worktree
or self
.paths
.git_dir
200 def set_worktree(self
, path
):
201 path
= core
.decode(path
)
202 self
.paths
= find_git_directory(path
)
203 return self
.paths
.worktree
206 if not self
.paths
.worktree
:
207 path
= core
.abspath(core
.getcwd())
208 self
.paths
= find_git_directory(path
)
209 return self
.paths
.worktree
212 """Is this a valid git repository?
214 Cache the result to avoid hitting the filesystem.
217 git_dir
= self
.paths
.git_dir
219 valid
= bool(git_dir
) and self
._valid
[git_dir
]
221 valid
= self
._valid
[git_dir
] = is_git_dir(git_dir
)
225 def git_path(self
, *paths
):
227 if self
.paths
.git_dir
:
228 result
= join(self
.paths
.git_dir
, *paths
)
229 if result
and self
.paths
.common_dir
and not core
.exists(result
):
230 common_result
= join(self
.paths
.common_dir
, *paths
)
231 if core
.exists(common_result
):
232 result
= common_result
236 if not self
.paths
.git_dir
:
237 path
= core
.abspath(core
.getcwd())
238 self
.paths
= find_git_directory(path
)
239 return self
.paths
.git_dir
241 def __getattr__(self
, name
):
242 git_cmd
= partial(self
.git
, name
)
243 setattr(self
, name
, git_cmd
)
254 _stderr
=subprocess
.PIPE
,
255 _stdout
=subprocess
.PIPE
,
257 _no_win32_startupinfo
=False,
260 Execute a command and returns its output
262 :param command: argument list to execute.
263 :param _cwd: working directory, defaults to the current directory.
264 :param _decode: whether to decode output, defaults to True.
265 :param _encoding: default encoding, defaults to None (utf-8).
266 :param _readonly: avoid taking the index lock. Assume the command is read-only.
267 :param _raw: do not strip trailing whitespace.
268 :param _stdin: optional stdin filehandle.
269 :returns (status, out, err): exit status, stdout, stderr
272 # Allow the user to have the command executed in their working dir.
278 if hasattr(os
, 'setsid'):
279 # SSH uses the SSH_ASKPASS variable only if the process is really
280 # detached from the TTY (stdin redirection and setting the
281 # SSH_ASKPASS environment variable is not enough). To detach a
282 # process from the console it should fork and call os.setsid().
283 extra
['preexec_fn'] = os
.setsid
285 start_time
= time
.time()
288 # Guard against thread-unsafe .git/index.lock files
290 _index_lock
.acquire()
292 status
, out
, err
= core
.run_command(
299 no_win32_startupinfo
=_no_win32_startupinfo
,
303 # Let the next thread in
305 _index_lock
.release()
307 end_time
= time
.time()
308 elapsed_time
= abs(end_time
- start_time
)
310 if not _raw
and out
is not None:
311 out
= core
.UStr(out
.rstrip('\n'), out
.encoding
)
313 cola_trace
= GIT_COLA_TRACE
314 if cola_trace
== 'trace':
315 msg
= f
'trace: {elapsed_time:.3f}s: {core.list2cmdline(command)}'
316 Interaction
.log_status(status
, msg
, '')
317 elif cola_trace
== 'full':
320 "# %.3fs: %s -> %d: '%s' '%s'"
321 % (elapsed_time
, ' '.join(command
), status
, out
, err
)
325 '# %.3fs: %s -> %d' % (elapsed_time
, ' '.join(command
), status
)
328 core
.print_stderr('# {:.3f}s: {}'.format(elapsed_time
, ' '.join(command
)))
330 # Allow access to the command's status code
331 return (status
, out
, err
)
333 def git(self
, cmd
, *args
, **kwargs
):
334 # Handle optional arguments prior to calling transform_kwargs
335 # otherwise they'll end up in args, which is bad.
336 _kwargs
= {'_cwd': self
.getcwd()}
346 '_no_win32_startupinfo',
349 for kwarg
in execute_kwargs
:
351 _kwargs
[kwarg
] = kwargs
.pop(kwarg
)
353 # Prepare the argument list
357 'diff.suppressBlankEmpty=false',
359 'log.showSignature=false',
362 opt_args
= transform_kwargs(**kwargs
)
363 call
= git_args
+ opt_args
366 result
= self
.execute(call
, **_kwargs
)
367 except OSError as exc
:
368 if WIN32
and exc
.errno
== errno
.ENOENT
:
369 # see if git exists at all. On win32 it can fail with ENOENT in
370 # case of argv overflow. We should be safe from that but use
371 # defensive coding for the worst-case scenario. On UNIX
372 # we have ENAMETOOLONG but that doesn't exist on Windows.
373 if _git_is_installed():
375 _print_win32_git_hint()
376 result
= (1, '', "error: unable to execute '%s'" % GIT
)
380 def _git_is_installed():
381 """Return True if git is installed"""
382 # On win32 Git commands can fail with ENOENT in case of argv overflow. We
383 # should be safe from that but use defensive coding for the worst-case
384 # scenario. On UNIX we have ENAMETOOLONG but that doesn't exist on
387 status
, _
, _
= Git
.execute([GIT
, '--version'])
394 def transform_kwargs(**kwargs
):
395 """Transform kwargs into git command line options
397 Callers can assume the following behavior:
399 Passing foo=None ignores foo, so that callers can
400 use default values of None that are ignored unless
403 Passing foo=False ignore foo, for the same reason.
405 Passing foo={string-or-number} results in ['--foo=<value>']
406 in the resulting arguments.
410 types_to_stringify
= (ustr
, float, str) + int_types
412 for k
, value
in kwargs
.items():
419 # isinstance(False, int) is True, so we have to check bool first
420 if isinstance(value
, bool):
422 args
.append(f
'{dashes}{dashify(k)}')
423 # else: pass # False is ignored; flag=False inhibits --flag
424 elif isinstance(value
, types_to_stringify
):
425 args
.append(f
'{dashes}{dashify(k)}{equals}{value}')
430 def win32_git_error_hint():
433 'NOTE: If you have Git installed in a custom location, e.g.\n'
434 'C:\\Tools\\Git, then you can create a file at\n'
435 '~/.config/git-cola/git-bindir with following text\n'
436 'and git-cola will add the specified location to your $PATH\n'
437 'automatically when starting cola:\n'
444 def _print_win32_git_hint():
445 hint
= '\n' + win32_git_error_hint() + '\n'
446 core
.print_stderr("error: unable to execute 'git'" + hint
)
450 """Create Git instances
453 >>> status, out, err = git.version()
454 >>> 'git' == out[:3].lower()