1 from __future__
import absolute_import
, division
, print_function
, unicode_literals
2 from binascii
import unhexlify
9 from qtpy
import QtCore
10 from qtpy
.QtCore
import Signal
15 from .compat
import int_types
16 from .compat
import ustr
20 """Create GitConfig instances"""
21 return GitConfig(context
)
24 def _cache_key_from_paths(paths
):
25 """Return a stat cache from the given paths"""
29 for path
in sorted(paths
):
31 mtimes
.append(core
.stat(path
).st_mtime
)
39 def _config_to_python(v
):
40 """Convert a Git config string into a Python value"""
41 if v
in ('true', 'yes'):
43 elif v
in ('false', 'no'):
47 v
= int(v
) # pylint: disable=redefined-variable-type
54 """Convert a value (int or hex string) into bytes"""
55 if isinstance(value
, int_types
):
56 # If the value is an integer then it's a value that was converted
57 # by the config reader. Zero-pad it into a 6-digit hex number.
58 value
= '%06d' % value
59 return unhexlify(core
.encode(value
.lstrip('#')))
62 def _config_key_value(line
, splitchar
):
63 """Split a config line into a (key, value) pair"""
65 k
, v
= line
.split(splitchar
, 1)
67 # the user has an empty entry in their git config,
68 # which Git interprets as meaning "true"
71 return k
, _config_to_python(v
)
74 def _append_tab(value
):
75 """Return a value and the same value with tab appended"""
76 return (value
, value
+ '\t')
79 class GitConfig(QtCore
.QObject
):
80 """Encapsulate access to git-config values."""
82 user_config_changed
= Signal(str, object)
83 repo_config_changed
= Signal(str, object)
86 def __init__(self
, context
):
87 super(GitConfig
, self
).__init
__()
88 self
.context
= context
89 self
.git
= context
.git
92 self
._global
_or
_system
= {}
95 self
._renamed
_keys
= {}
96 self
._multi
_values
= collections
.defaultdict(list)
97 self
._cache
_key
= None
98 self
._cache
_paths
= []
100 self
._binary
_cache
= {}
103 self
._cache
_key
= None
104 self
._cache
_paths
= []
105 self
._attr
_cache
.clear()
106 self
._binary
_cache
.clear()
109 def reset_values(self
):
112 self
._global
_or
_system
.clear()
115 self
._renamed
_keys
.clear()
116 self
._multi
_values
.clear()
119 return copy
.deepcopy(self
._global
)
122 return copy
.deepcopy(self
._local
)
125 return copy
.deepcopy(self
._all
)
127 def _is_cached(self
):
129 Return True when the cache matches.
131 Updates the cache and returns False when the cache does not match.
134 cache_key
= _cache_key_from_paths(self
._cache
_paths
)
135 return self
._cache
_key
and cache_key
== self
._cache
_key
138 """Read git config value into the system, user and repo dicts."""
139 if self
._is
_cached
():
144 show_scope
= version
.check_git(self
.context
, 'config-show-scope')
145 status
, config_output
, _
= self
.git
.config(
146 show_origin
=True, show_scope
=show_scope
, list=True, includes
=True
152 reader
= _read_config_with_scope
154 reader
= _read_config_with_scope
156 unknown_scope
= 'unknown'
157 system_scope
= 'system'
158 global_scope
= 'global'
159 local_scope
= 'local'
160 worktree_scope
= 'worktree'
164 current_scope
, current_key
, current_value
, continuation
165 ) in reader(config_output
, cache_paths
, self
._renamed
_keys
):
166 # Store the values for fast cached lookup.
167 self
._all
[current_key
] = current_value
169 # macOS has credential.helper=osxkeychain in the "unknown" scope from
170 # /Applications/Xcode.app/Contents/Developer/usr/share/git-core/gitconfig.
171 # Treat "unknown" as equivalent to "system" (lowest priority).
172 if current_scope
in (system_scope
, unknown_scope
):
173 self
._system
[current_key
] = current_value
174 self
._global
_or
_system
[current_key
] = current_value
175 elif current_scope
== global_scope
:
176 self
._global
[current_key
] = current_value
177 self
._global
_or
_system
[current_key
] = current_value
178 # "worktree" is treated as equivalent to "local".
179 elif current_scope
in (local_scope
, worktree_scope
):
180 self
._local
[current_key
] = current_value
182 # Add this value to the multi-values storage used by get_all().
183 # This allows us to handle keys that store multiple values.
185 # If this is a continuation line then we should *not* append to its
186 # multi-values list. We should update it in-place.
187 self
._multi
_values
[current_key
][-1] = current_value
189 self
._multi
_values
[current_key
].append(current_value
)
192 self
._cache
_paths
= sorted(cache_paths
)
193 self
._cache
_key
= _cache_key_from_paths(self
._cache
_paths
)
195 # Send a notification that the configuration has been updated.
198 def _get(self
, src
, key
, default
, fn
=None, cached
=True):
199 if not cached
or not src
:
202 value
= self
._get
_value
(src
, key
)
210 def _get_value(self
, src
, key
):
211 """Return a value from the map"""
216 # Try the original key name.
217 key
= self
._renamed
_keys
.get(key
.lower(), key
)
222 # Allow the final KeyError to bubble up
223 return src
[key
.lower()]
225 def get(self
, key
, default
=None, fn
=None, cached
=True):
226 """Return the string value for a config key."""
227 return self
._get
(self
._all
, key
, default
, fn
=fn
, cached
=cached
)
229 def get_all(self
, key
):
230 """Return all values for a key sorted in priority order
232 The purpose of this function is to group the values returned by
233 `git config --show-origin --list` so that the relative order is
234 preserved and can be overridden at each level.
236 One use case is the `cola.icontheme` variable, which is an ordered
237 list of icon themes to load. This value can be set both in
238 ~/.gitconfig as well as .git/config, and we want to allow a
239 relative order to be defined in either file.
241 The problem is that git will read the system /etc/gitconfig,
242 global ~/.gitconfig, and then the local .git/config settings
243 and return them in that order, so we must post-process them to
244 get them in an order which makes sense for use for our values.
245 Otherwise, we cannot replace the order, or make a specific theme used
246 first, in our local .git/config since the native order returned by
247 git will always list the global config before the local one.
249 get_all() allows for this use case by reading from a defaultdict
250 that contains all of the per-config values separately so that the
251 caller can order them according to its preferred precedence.
253 if not self
._multi
_values
:
255 # Check for this key as-is.
256 if key
in self
._multi
_values
:
257 return self
._multi
_values
[key
]
259 # Check for a renamed version of this key (x.kittycat -> x.kittyCat)
260 renamed_key
= self
._renamed
_keys
.get(key
.lower(), key
)
261 if renamed_key
in self
._multi
_values
:
262 return self
._multi
_values
[renamed_key
]
264 key_lower
= key
.lower()
265 if key_lower
in self
._multi
_values
:
266 return self
._multi
_values
[key_lower
]
267 # Nothing found -> empty list.
270 def get_user(self
, key
, default
=None):
271 return self
._get
(self
._global
, key
, default
)
273 def get_repo(self
, key
, default
=None):
274 return self
._get
(self
._local
, key
, default
)
276 def get_user_or_system(self
, key
, default
=None):
277 return self
._get
(self
._global
_or
_system
, key
, default
)
279 def set_user(self
, key
, value
):
280 if value
in (None, ''):
281 self
.git
.config('--global', key
, unset
=True, _readonly
=True)
283 self
.git
.config('--global', key
, python_to_git(value
), _readonly
=True)
285 self
.user_config_changed
.emit(key
, value
)
287 def set_repo(self
, key
, value
):
288 if value
in (None, ''):
289 self
.git
.config(key
, unset
=True, _readonly
=True)
291 self
.git
.config(key
, python_to_git(value
), _readonly
=True)
293 self
.repo_config_changed
.emit(key
, value
)
296 """Return a a dict of values for all keys matching the specified pattern"""
298 match
= fnmatch
.fnmatch
302 for key
, val
in self
._all
.items():
303 if match(key
.lower(), pat
):
308 """Return True when git-annex is enabled"""
309 return bool(self
.get('annex.uuid', default
=False))
311 def gui_encoding(self
):
312 return self
.get('gui.encoding', default
=None)
314 def is_per_file_attrs_enabled(self
):
316 'cola.fileattributes', fn
=lambda: os
.path
.exists('.gitattributes')
319 def is_binary(self
, path
):
320 """Return True if the file has the binary attribute set"""
321 if not self
.is_per_file_attrs_enabled():
323 cache
= self
._binary
_cache
327 value
= cache
[path
] = self
._is
_binary
(path
)
330 def _is_binary(self
, path
):
331 """Return the file encoding for a path"""
332 value
= self
.check_attr('binary', path
)
333 return value
== 'set'
335 def file_encoding(self
, path
):
336 if not self
.is_per_file_attrs_enabled():
337 return self
.gui_encoding()
338 cache
= self
._attr
_cache
342 value
= cache
[path
] = self
._file
_encoding
(path
) or self
.gui_encoding()
345 def _file_encoding(self
, path
):
346 """Return the file encoding for a path"""
347 encoding
= self
.check_attr('encoding', path
)
348 if encoding
in ('unspecified', 'unset', 'set'):
354 def check_attr(self
, attr
, path
):
355 """Check file attributes for a path"""
357 status
, out
, _
= self
.git
.check_attr(attr
, '--', path
, _readonly
=True)
359 header
= '%s: %s: ' % (path
, attr
)
360 if out
.startswith(header
):
361 value
= out
[len(header
) :].strip()
364 def get_guitool_opts(self
, name
):
365 """Return the guitool.<name> namespace as a dict
367 The dict keys are simplified so that "guitool.$name.cmd" is accessible
371 prefix
= len('guitool.%s.' % name
)
372 guitools
= self
.find('guitool.%s.*' % name
)
373 return dict([(key
[prefix
:], value
) for (key
, value
) in guitools
.items()])
375 def get_guitool_names(self
):
376 guitools
= self
.find('guitool.*.cmd')
377 prefix
= len('guitool.')
379 return sorted([name
[prefix
:-suffix
] for (name
, _
) in guitools
.items()])
381 def get_guitool_names_and_shortcuts(self
):
382 """Return guitool names and their configured shortcut"""
383 names
= self
.get_guitool_names()
384 return [(name
, self
.get('guitool.%s.shortcut' % name
)) for name
in names
]
387 term
= self
.get('cola.terminal', default
=None)
389 # find a suitable default terminal
390 term
= 'xterm -e' # for mac osx
392 # Try to find Git's sh.exe directory in
393 # one of the typical locations
394 pf
= os
.environ
.get('ProgramFiles', r
'C:\Program Files')
395 pf32
= os
.environ
.get('ProgramFiles(x86)', r
'C:\Program Files (x86)')
396 pf64
= os
.environ
.get('ProgramW6432', r
'C:\Program Files')
398 for p
in [pf64
, pf32
, pf
, 'C:\\']:
399 candidate
= os
.path
.join(p
, r
'Git\bin\sh.exe')
400 if os
.path
.isfile(candidate
):
404 candidates
= ('xfce4-terminal', 'konsole', 'gnome-terminal')
405 for basename
in candidates
:
406 if core
.exists('/usr/bin/%s' % basename
):
407 if basename
== 'gnome-terminal':
408 term
= '%s --' % basename
410 term
= '%s -e' % basename
414 def color(self
, key
, default
):
415 value
= self
.get('cola.color.%s' % key
, default
=default
)
416 struct_layout
= core
.encode('BBB')
418 # pylint: disable=no-member
419 r
, g
, b
= struct
.unpack(struct_layout
, unhex(value
))
420 except (struct
.error
, TypeError):
421 # pylint: disable=no-member
422 r
, g
, b
= struct
.unpack(struct_layout
, unhex(default
))
426 """Return the path to the git hooks directory"""
427 gitdir_hooks
= self
.git
.git_path('hooks')
428 return self
.get('core.hookspath', default
=gitdir_hooks
)
430 def hooks_path(self
, *paths
):
431 """Return a path from within the git hooks directory"""
432 return os
.path
.join(self
.hooks(), *paths
)
435 def _read_config_with_scope(config_output
, cache_paths
, renamed_keys
):
436 """Read the output from "git config --show-scope --show-origin --list
438 ``--show-scope`` was introduced in Git v2.26.0.
440 unknown_key
= 'unknown\t'
441 system_key
= 'system\t'
442 global_key
= 'global\t'
443 local_key
= 'local\t'
444 worktree_key
= 'worktree\t'
445 command_scope
, command_key
= _append_tab('command')
446 command_line
= 'command line:'
447 file_scheme
= 'file:'
454 for line
in config_output
.splitlines():
457 # pylint: disable=too-many-boolean-expressions
459 line
.startswith(system_key
)
460 or line
.startswith(global_key
)
461 or line
.startswith(local_key
)
462 or line
.startswith(command_key
)
463 or line
.startswith(worktree_key
) # worktree and unknown are uncommon.
464 or line
.startswith(unknown_key
)
467 current_scope
, current_path
, rest
= line
.split('\t', 2)
468 if current_scope
== command_scope
:
470 current_key
, current_value
= _config_key_value(rest
, '=')
471 if current_path
.startswith(file_scheme
):
472 cache_paths
.add(current_path
[len(file_scheme
):])
473 elif current_path
== command_line
:
475 renamed_keys
[current_key
.lower()] = current_key
477 # Values are allowed to span multiple lines when \n is embedded
478 # in the value. Detect this and append to the previous value.
480 if current_value
and isinstance(current_value
, str):
481 current_value
+= '\n'
482 current_value
+= line
486 yield current_scope
, current_key
, current_value
, continuation
489 def python_to_git(value
):
490 if isinstance(value
, bool):
491 return 'true' if value
else 'false'
492 if isinstance(value
, int_types
):
497 def get_remotes(cfg
):
498 """Get all of the configured git remotes"""
499 # Gather all of the remote.*.url entries.
500 prefix
= len('remote.')
502 return sorted(key
[prefix
:-suffix
] for key
in cfg
.find('remote.*.url'))