1 from __future__
import absolute_import
, division
, print_function
, unicode_literals
2 from binascii
import unhexlify
6 from os
.path
import join
10 from qtpy
import QtCore
11 from qtpy
.QtCore
import Signal
16 from .compat
import int_types
17 from .git
import STDOUT
18 from .compat
import ustr
20 BUILTIN_READER
= os
.environ
.get('GIT_COLA_BUILTIN_CONFIG_READER', False)
22 _USER_CONFIG
= core
.expanduser(join('~', '.gitconfig'))
23 _USER_XDG_CONFIG
= core
.expanduser(
24 join(core
.getenv('XDG_CONFIG_HOME', join('~', '.config')), 'git', 'config')
29 """Create GitConfig instances"""
30 return GitConfig(context
)
34 # Try /etc/gitconfig as a fallback for the system config
36 ('system', '/etc/gitconfig'),
37 ('user', _USER_XDG_CONFIG
),
38 ('user', _USER_CONFIG
),
40 config
= git
.git_path('config')
42 paths
.append(('repo', config
))
45 for category
, path
in paths
:
47 statinfo
.append((category
, path
, core
.stat(path
).st_mtime
))
54 # Try /etc/gitconfig as a fallback for the system config
60 config
= git
.git_path('config')
67 mtimes
.append(core
.stat(path
).st_mtime
)
73 def _config_to_python(v
):
74 """Convert a Git config string into a Python value"""
75 if v
in ('true', 'yes'):
77 elif v
in ('false', 'no'):
81 v
= int(v
) # pylint: disable=redefined-variable-type
88 """Convert a value (int or hex string) into bytes"""
89 if isinstance(value
, int_types
):
90 # If the value is an integer then it's a value that was converted
91 # by the config reader. Zero-pad it into a 6-digit hex number.
92 value
= '%06d' % value
93 return unhexlify(core
.encode(value
.lstrip('#')))
96 def _config_key_value(line
, splitchar
):
97 """Split a config line into a (key, value) pair"""
100 k
, v
= line
.split(splitchar
, 1)
102 # the user has an empty entry in their git config,
103 # which Git interprets as meaning "true"
106 return k
, _config_to_python(v
)
109 class GitConfig(QtCore
.QObject
):
110 """Encapsulate access to git-config values."""
112 user_config_changed
= Signal(str, object)
113 repo_config_changed
= Signal(str, object)
116 def __init__(self
, context
):
117 super(GitConfig
, self
).__init
__()
118 self
.git
= context
.git
122 self
._user
_or
_system
= {}
125 self
._cache
_key
= None
127 self
._config
_files
= {}
128 self
._attr
_cache
= {}
129 self
._binary
_cache
= {}
130 self
._find
_config
_files
()
133 self
._cache
_key
= None
135 self
._config
_files
.clear()
136 self
._attr
_cache
= {}
137 self
._binary
_cache
= {}
138 self
._find
_config
_files
()
141 def reset_values(self
):
145 self
._user
_or
_system
.clear()
150 return copy
.deepcopy(self
._user
)
153 return copy
.deepcopy(self
._repo
)
156 return copy
.deepcopy(self
._all
)
158 def _find_config_files(self
):
160 Classify git config files into 'system', 'user', and 'repo'.
162 Populates self._configs with a list of the files in
163 reverse-precedence order. self._config_files is populated with
164 {category: path} where category is one of 'system', 'user', or 'repo'.
167 # Try the git config in git's installation prefix
168 statinfo
= _stat_info(self
.git
)
169 self
._configs
= [x
[1] for x
in statinfo
]
170 self
._config
_files
= {}
171 for (cat
, path
, _
) in statinfo
:
172 self
._config
_files
[cat
] = path
176 Return True when the cache matches.
178 Updates the cache and returns False when the cache does not match.
181 cache_key
= _cache_key(self
.git
)
182 if self
._cache
_key
is None or cache_key
!= self
._cache
_key
:
183 self
._cache
_key
= cache_key
188 """Read git config value into the system, user and repo dicts."""
194 if 'system' in self
._config
_files
:
195 self
._system
.update(self
.read_config(self
._config
_files
['system']))
197 if 'user' in self
._config
_files
:
198 self
._user
.update(self
.read_config(self
._config
_files
['user']))
200 if 'repo' in self
._config
_files
:
201 self
._repo
.update(self
.read_config(self
._config
_files
['repo']))
203 for dct
in (self
._system
, self
._user
):
204 self
._user
_or
_system
.update(dct
)
206 for dct
in (self
._system
, self
._user
, self
._repo
):
207 self
._all
.update(dct
)
211 def read_config(self
, path
):
212 """Return git config data from a path as a dictionary."""
215 return self
._read
_config
_file
(path
)
218 if version
.check_git(self
, 'config-includes'):
219 args
= ('--null', '--file', path
, '--list', '--includes')
221 args
= ('--null', '--file', path
, '--list')
222 config_lines
= self
.git
.config(*args
)[STDOUT
].split('\0')
223 for line
in config_lines
:
225 # the user has an invalid entry in their git config
227 k
, v
= _config_key_value(line
, '\n')
228 self
._map
[k
.lower()] = k
232 def _read_config_file(self
, path
):
233 """Read a .gitconfig file into a dict"""
236 header_simple
= re
.compile(r
'^\[(\s+)]$')
237 header_subkey
= re
.compile(r
'^\[(\s+) "(\s+)"\]$')
239 with core
.xopen(path
, 'rt') as f
:
240 file_lines
= f
.readlines()
242 stripped_lines
= [line
.strip() for line
in file_lines
]
243 lines
= [line
for line
in stripped_lines
if bool(line
)]
246 if line
.startswith('#'):
249 match
= header_simple
.match(line
)
251 prefix
= match
.group(1) + '.'
253 match
= header_subkey
.match(line
)
255 prefix
= match
.group(1) + '.' + match
.group(2) + '.'
258 k
, v
= _config_key_value(line
, '=')
260 self
._map
[k
.lower()] = k
265 def _get(self
, src
, key
, default
, fn
=None, cached
=True):
266 if not cached
or not src
:
269 value
= self
._get
_with
_fallback
(src
, key
)
277 def _get_with_fallback(self
, src
, key
):
282 key
= self
._map
.get(key
.lower(), key
)
287 # Allow the final KeyError to bubble up
288 return src
[key
.lower()]
290 def get(self
, key
, default
=None, fn
=None, cached
=True):
291 """Return the string value for a config key."""
292 return self
._get
(self
._all
, key
, default
, fn
=fn
, cached
=cached
)
294 def get_all(self
, key
):
295 """Return all values for a key sorted in priority order
297 The purpose of this function is to group the values returned by
298 `git config --show-origin --get-all` so that the relative order is
299 preserved but can still be overridden at each level.
301 One use case is the `cola.icontheme` variable, which is an ordered
302 list of icon themes to load. This value can be set both in
303 ~/.gitconfig as well as .git/config, and we want to allow a
304 relative order to be defined in either file.
306 The problem is that git will read the system /etc/gitconfig,
307 global ~/.gitconfig, and then the local .git/config settings
308 and return them in that order, so we must post-process them to
309 get them in an order which makes sense for use for our values.
310 Otherwise, we cannot replace the order, or make a specific theme used
311 first, in our local .git/config since the native order returned by
312 git will always list the global config before the local one.
314 get_all() allows for this use case by gathering all of the per-config
315 values separately and then orders them according to the expected
316 local > global > system order.
320 status
, out
, _
= self
.git
.config(key
, z
=True, get_all
=True, show_origin
=True)
325 items
= [x
for x
in out
.rstrip(chr(0)).split(chr(0)) if x
]
326 for i
in range(len(items
) // 2):
327 source
= items
[i
* 2]
328 value
= items
[i
* 2 + 1]
329 if source
!= current_source
:
330 current_source
= source
332 partial_results
.append(current_result
)
333 current_result
.append(value
)
334 # Git's results are ordered System, Global, Local.
335 # Reverse the order here so that Local has the highest priority.
336 for partial_result
in reversed(partial_results
):
337 result
.extend(partial_result
)
341 def get_user(self
, key
, default
=None):
342 return self
._get
(self
._user
, key
, default
)
344 def get_repo(self
, key
, default
=None):
345 return self
._get
(self
._repo
, key
, default
)
347 def get_user_or_system(self
, key
, default
=None):
348 return self
._get
(self
._user
_or
_system
, key
, default
)
350 def set_user(self
, key
, value
):
351 if value
in (None, ''):
352 self
.git
.config('--global', key
, unset
=True)
354 self
.git
.config('--global', key
, python_to_git(value
))
356 self
.user_config_changed
.emit(key
, value
)
358 def set_repo(self
, key
, value
):
359 if value
in (None, ''):
360 self
.git
.config(key
, unset
=True)
362 self
.git
.config(key
, python_to_git(value
))
364 self
.repo_config_changed
.emit(key
, value
)
368 match
= fnmatch
.fnmatch
372 for key
, val
in self
._all
.items():
373 if match(key
.lower(), pat
):
378 """Return True when git-annex is enabled"""
379 return bool(self
.get('annex.uuid', default
=False))
381 def gui_encoding(self
):
382 return self
.get('gui.encoding', default
=None)
384 def is_per_file_attrs_enabled(self
):
386 'cola.fileattributes', fn
=lambda: os
.path
.exists('.gitattributes')
389 def is_binary(self
, path
):
390 """Return True if the file has the binary attribute set"""
391 if not self
.is_per_file_attrs_enabled():
393 cache
= self
._binary
_cache
397 value
= cache
[path
] = self
._is
_binary
(path
)
400 def _is_binary(self
, path
):
401 """Return the file encoding for a path"""
402 value
= self
.check_attr('binary', path
)
403 return value
== 'set'
405 def file_encoding(self
, path
):
406 if not self
.is_per_file_attrs_enabled():
407 return self
.gui_encoding()
408 cache
= self
._attr
_cache
412 value
= cache
[path
] = self
._file
_encoding
(path
) or self
.gui_encoding()
415 def _file_encoding(self
, path
):
416 """Return the file encoding for a path"""
417 encoding
= self
.check_attr('encoding', path
)
418 if encoding
in ('unspecified', 'unset', 'set'):
424 def check_attr(self
, attr
, path
):
425 """Check file attributes for a path"""
427 status
, out
, _
= self
.git
.check_attr(attr
, '--', path
)
429 header
= '%s: %s: ' % (path
, attr
)
430 if out
.startswith(header
):
431 value
= out
[len(header
) :].strip()
434 def get_guitool_opts(self
, name
):
435 """Return the guitool.<name> namespace as a dict
437 The dict keys are simplified so that "guitool.$name.cmd" is accessible
441 prefix
= len('guitool.%s.' % name
)
442 guitools
= self
.find('guitool.%s.*' % name
)
443 return dict([(key
[prefix
:], value
) for (key
, value
) in guitools
.items()])
445 def get_guitool_names(self
):
446 guitools
= self
.find('guitool.*.cmd')
447 prefix
= len('guitool.')
449 return sorted([name
[prefix
:-suffix
] for (name
, _
) in guitools
.items()])
451 def get_guitool_names_and_shortcuts(self
):
452 """Return guitool names and their configured shortcut"""
453 names
= self
.get_guitool_names()
454 return [(name
, self
.get('guitool.%s.shortcut' % name
)) for name
in names
]
457 term
= self
.get('cola.terminal', default
=None)
459 # find a suitable default terminal
460 term
= 'xterm -e' # for mac osx
462 # Try to find Git's sh.exe directory in
463 # one of the typical locations
464 pf
= os
.environ
.get('ProgramFiles', 'C:\\Program Files')
465 pf32
= os
.environ
.get('ProgramFiles(x86)', 'C:\\Program Files (x86)')
466 pf64
= os
.environ
.get('ProgramW6432', 'C:\\Program Files')
468 for p
in [pf64
, pf32
, pf
, 'C:\\']:
469 candidate
= os
.path
.join(p
, 'Git\\bin\\sh.exe')
470 if os
.path
.isfile(candidate
):
474 candidates
= ('xfce4-terminal', 'konsole', 'gnome-terminal')
475 for basename
in candidates
:
476 if core
.exists('/usr/bin/%s' % basename
):
477 if basename
== 'gnome-terminal':
478 term
= '%s --' % basename
480 term
= '%s -e' % basename
484 def color(self
, key
, default
):
485 value
= self
.get('cola.color.%s' % key
, default
=default
)
486 struct_layout
= core
.encode('BBB')
488 # pylint: disable=no-member
489 r
, g
, b
= struct
.unpack(struct_layout
, unhex(value
))
490 except (struct
.error
, TypeError):
491 # pylint: disable=no-member
492 r
, g
, b
= struct
.unpack(struct_layout
, unhex(default
))
496 """Return the path to the git hooks directory"""
497 gitdir_hooks
= self
.git
.git_path('hooks')
498 return self
.get('core.hookspath', default
=gitdir_hooks
)
500 def hooks_path(self
, *paths
):
501 """Return a path from within the git hooks directory"""
502 return os
.path
.join(self
.hooks(), *paths
)
505 def python_to_git(value
):
506 if isinstance(value
, bool):
507 return 'true' if value
else 'false'
508 if isinstance(value
, int_types
):