1 from __future__
import division
, absolute_import
, unicode_literals
9 from os
.path
import join
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', '')
26 return s
.replace('_', '-')
29 def is_git_dir(git_dir
):
30 """From git's setup.c:is_git_directory()."""
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/')))
45 result
= is_git_file(git_dir
)
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.
65 if path
and is_git_file(path
):
67 data
= core
.read(path
).strip()
68 if data
.startswith(header
):
69 result
= data
[len(header
):]
70 if result
and not os
.path
.isabs(result
):
71 path_folder
= os
.path
.dirname(path
)
72 repo_relative
= os
.path
.join(path_folder
, result
)
73 result
= os
.path
.normpath(repo_relative
)
78 """Git repository paths of interest"""
80 def __init__(self
, git_dir
=None, git_file
=None, worktree
=None):
81 self
.git_dir
= git_dir
82 self
.git_file
= git_file
83 self
.worktree
= worktree
86 def find_git_directory(curpath
):
87 """Perform Git repository discovery
90 paths
= Paths(git_dir
=core
.getenv('GIT_DIR'),
91 worktree
=core
.getenv('GIT_WORK_TREE'),
95 ceiling
= core
.getenv('GIT_CEILING_DIRECTORIES')
97 ceiling_dirs
.update([x
for x
in ceiling
.split(':') if x
])
99 if not paths
.git_dir
or not paths
.worktree
:
101 curpath
= core
.abspath(curpath
)
103 # Search for a .git directory
105 if curpath
in ceiling_dirs
:
107 if is_git_dir(curpath
):
108 paths
.git_dir
= curpath
109 if os
.path
.basename(curpath
) == '.git':
110 paths
.worktree
= os
.path
.dirname(curpath
)
112 gitpath
= join(curpath
, '.git')
113 if is_git_dir(gitpath
):
114 paths
.git_dir
= gitpath
115 paths
.worktree
= curpath
117 curpath
, dummy
= os
.path
.split(curpath
)
121 git_dir_path
= read_git_file(paths
.git_dir
)
123 paths
.git_file
= paths
.git_dir
124 paths
.git_dir
= git_dir_path
131 The Git class manages communication with the Git binary
136 self
._git
_cwd
= None #: The working directory used by execute()
137 self
._valid
= {} #: Store the result of is_git_dir() for performance
138 self
.set_worktree(core
.getcwd())
143 def _find_git_directory(self
, path
):
145 self
.paths
= find_git_directory(path
)
147 # Update the current directory for executing commands
148 if self
.paths
.worktree
:
149 self
._git
_cwd
= self
.paths
.worktree
150 elif self
.paths
.git_dir
:
151 self
._git
_cwd
= self
.paths
.git_dir
153 def set_worktree(self
, path
):
154 path
= core
.decode(path
)
155 self
._find
_git
_directory
(path
)
156 return self
.paths
.worktree
159 if not self
.paths
.worktree
:
160 path
= core
.abspath(core
.getcwd())
161 self
._find
_git
_directory
(path
)
162 return self
.paths
.worktree
165 """Is this a valid git repository?
167 Cache the result to avoid hitting the filesystem.
170 git_dir
= self
.paths
.git_dir
172 valid
= bool(git_dir
) and self
._valid
[git_dir
]
174 valid
= self
._valid
[git_dir
] = is_git_dir(git_dir
)
178 def git_path(self
, *paths
):
179 if self
.paths
.git_dir
:
180 result
= join(self
.paths
.git_dir
, *paths
)
186 if not self
.paths
.git_dir
:
187 path
= core
.abspath(core
.getcwd())
188 self
._find
_git
_directory
(path
)
189 return self
.paths
.git_dir
191 def __getattr__(self
, name
):
192 git_cmd
= functools
.partial(self
.git
, name
)
193 setattr(self
, name
, git_cmd
)
203 _stderr
=subprocess
.PIPE
,
204 _stdout
=subprocess
.PIPE
,
206 _no_win32_startupinfo
=False):
208 Execute a command and returns its output
210 :param command: argument list to execute.
211 :param _cwd: working directory, defaults to the current directory.
212 :param _decode: whether to decode output, defaults to True.
213 :param _encoding: default encoding, defaults to None (utf-8).
214 :param _raw: do not strip trailing whitespace.
215 :param _stdin: optional stdin filehandle.
216 :returns (status, out, err): exit status, stdout, stderr
219 # Allow the user to have the command executed in their working dir.
225 if hasattr(os
, 'setsid'):
226 # SSH uses the SSH_ASKPASS variable only if the process is really
227 # detached from the TTY (stdin redirection and setting the
228 # SSH_ASKPASS environment variable is not enough). To detach a
229 # process from the console it should fork and call os.setsid().
230 extra
['preexec_fn'] = os
.setsid
233 # Guard against thread-unsafe .git/index.lock files
236 status
, out
, err
= core
.run_command(
237 command
, cwd
=_cwd
, encoding
=_encoding
,
238 stdin
=_stdin
, stdout
=_stdout
, stderr
=_stderr
,
239 no_win32_startupinfo
=_no_win32_startupinfo
, **extra
)
240 # Let the next thread in
244 if not _raw
and out
is not None:
245 out
= out
.rstrip('\n')
247 cola_trace
= GIT_COLA_TRACE
248 if cola_trace
== 'trace':
249 msg
= 'trace: ' + core
.list2cmdline(command
)
250 Interaction
.log_status(status
, msg
, '')
251 elif cola_trace
== 'full':
253 core
.stderr("%s -> %d: '%s' '%s'" %
254 (' '.join(command
), status
, out
, err
))
256 core
.stderr("%s -> %d" % (' '.join(command
), status
))
258 core
.stderr(' '.join(command
))
260 # Allow access to the command's status code
261 return (status
, out
, err
)
263 def transform_kwargs(self
, **kwargs
):
264 """Transform kwargs into git command line options
266 Callers can assume the following behavior:
268 Passing foo=None ignores foo, so that callers can
269 use default values of None that are ignored unless
272 Passing foo=False ignore foo, for the same reason.
274 Passing foo={string-or-number} results in ['--foo=<value>']
275 in the resulting arguments.
279 types_to_stringify
= set((ustr
, float, str) + int_types
)
281 for k
, v
in kwargs
.items():
288 type_of_value
= type(v
)
290 args
.append('%s%s' % (dashes
, dashify(k
)))
291 elif type_of_value
in types_to_stringify
:
292 args
.append('%s%s%s%s' % (dashes
, dashify(k
), join
, v
))
296 def git(self
, cmd
, *args
, **kwargs
):
297 # Handle optional arguments prior to calling transform_kwargs
298 # otherwise they'll end up in args, which is bad.
299 _kwargs
= dict(_cwd
=self
._git
_cwd
)
309 '_no_win32_startupinfo',
312 for kwarg
in execute_kwargs
:
314 _kwargs
[kwarg
] = kwargs
.pop(kwarg
)
316 # Prepare the argument list
317 git_args
= ['git', '-c', 'diff.suppressBlankEmpty=false', dashify(cmd
)]
318 opt_args
= self
.transform_kwargs(**kwargs
)
319 call
= git_args
+ opt_args
322 return self
.execute(call
, **_kwargs
)
324 if e
.errno
!= errno
.ENOENT
:
328 # see if git exists at all. on win32 it can fail with ENOENT in
329 # case of argv overflow. we should be safe from that but use
330 # defensive coding for the worst-case scenario. on other OS-en
331 # we have ENAMETOOLONG which doesn't exist in with32 API.
332 status
, out
, err
= self
.execute(['git', '--version'])
336 core
.stderr("error: unable to execute 'git'\n"
337 "error: please ensure that 'git' is in your $PATH")
338 if sys
.platform
== 'win32':
339 _print_win32_git_hint()
343 def _print_win32_git_hint():
345 'hint: If you have Git installed in a custom location, e.g.\n'
346 'hint: C:\\Tools\\Git, then you can create a file at\n'
347 'hint: ~/.config/git-cola/git-bindir with following text\n'
348 'hint: and git-cola will add the specified location to your $PATH\n'
349 'hint: automatically when starting cola:\n'
351 'hint: C:\\Tools\\Git\\bin\n')
357 """Return the Git singleton"""
363 Git command singleton
366 >>> 'git' == git.version()[0][:3].lower()