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_CONFIG
),
38 ('user', _USER_XDG_CONFIG
),
40 config
= git
.git_path('config')
42 paths
.append(('repo', config
))
45 categories_completed
= set()
46 for category
, path
in paths
:
47 # We should only read ~/.gitconfig xor ~/.config/git/config (not both).
48 # Only processing the first entry in each category to accomplish this.
49 if category
in categories_completed
:
52 statinfo
.append((category
, path
, core
.stat(path
).st_mtime
))
55 categories_completed
.add(category
)
60 # Try /etc/gitconfig as a fallback for the system config
66 config
= git
.git_path('config')
73 mtimes
.append(core
.stat(path
).st_mtime
)
79 def _config_to_python(v
):
80 """Convert a Git config string into a Python value"""
81 if v
in ('true', 'yes'):
83 elif v
in ('false', 'no'):
87 v
= int(v
) # pylint: disable=redefined-variable-type
94 """Convert a value (int or hex string) into bytes"""
95 if isinstance(value
, int_types
):
96 # If the value is an integer then it's a value that was converted
97 # by the config reader. Zero-pad it into a 6-digit hex number.
98 value
= '%06d' % value
99 return unhexlify(core
.encode(value
.lstrip('#')))
102 def _config_key_value(line
, splitchar
):
103 """Split a config line into a (key, value) pair"""
106 k
, v
= line
.split(splitchar
, 1)
108 # the user has an empty entry in their git config,
109 # which Git interprets as meaning "true"
112 return k
, _config_to_python(v
)
115 class GitConfig(QtCore
.QObject
):
116 """Encapsulate access to git-config values."""
118 user_config_changed
= Signal(str, object)
119 repo_config_changed
= Signal(str, object)
122 def __init__(self
, context
):
123 super(GitConfig
, self
).__init
__()
124 self
.context
= context
125 self
.git
= context
.git
129 self
._user
_or
_system
= {}
132 self
._cache
_key
= None
134 self
._config
_files
= {}
135 self
._attr
_cache
= {}
136 self
._binary
_cache
= {}
137 self
._find
_config
_files
()
140 self
._cache
_key
= None
142 self
._config
_files
.clear()
143 self
._attr
_cache
= {}
144 self
._binary
_cache
= {}
145 self
._find
_config
_files
()
148 def reset_values(self
):
152 self
._user
_or
_system
.clear()
157 return copy
.deepcopy(self
._user
)
160 return copy
.deepcopy(self
._repo
)
163 return copy
.deepcopy(self
._all
)
165 def _find_config_files(self
):
167 Classify git config files into 'system', 'user', and 'repo'.
169 Populates self._configs with a list of the files in
170 reverse-precedence order. self._config_files is populated with
171 {category: path} where category is one of 'system', 'user', or 'repo'.
174 # Try the git config in git's installation prefix
175 statinfo
= _stat_info(self
.git
)
176 self
._configs
= [x
[1] for x
in statinfo
]
177 self
._config
_files
= {}
178 for (cat
, path
, _
) in statinfo
:
179 self
._config
_files
[cat
] = path
183 Return True when the cache matches.
185 Updates the cache and returns False when the cache does not match.
188 cache_key
= _cache_key(self
.git
)
189 if self
._cache
_key
is None or cache_key
!= self
._cache
_key
:
190 self
._cache
_key
= cache_key
195 """Read git config value into the system, user and repo dicts."""
201 if 'system' in self
._config
_files
:
202 self
._system
.update(self
.read_config(self
._config
_files
['system']))
204 if 'user' in self
._config
_files
:
205 self
._user
.update(self
.read_config(self
._config
_files
['user']))
207 if 'repo' in self
._config
_files
:
208 self
._repo
.update(self
.read_config(self
._config
_files
['repo']))
210 for dct
in (self
._system
, self
._user
):
211 self
._user
_or
_system
.update(dct
)
213 for dct
in (self
._system
, self
._user
, self
._repo
):
214 self
._all
.update(dct
)
218 def read_config(self
, path
):
219 """Return git config data from a path as a dictionary."""
222 return self
._read
_config
_file
(path
)
225 if version
.check_git(self
.context
, 'config-includes'):
226 args
= ('--null', '--file', path
, '--list', '--includes')
228 args
= ('--null', '--file', path
, '--list')
229 config_lines
= self
.git
.config(_readonly
=True, *args
)[STDOUT
].split('\0')
230 for line
in config_lines
:
232 # the user has an invalid entry in their git config
234 k
, v
= _config_key_value(line
, '\n')
235 self
._map
[k
.lower()] = k
239 def _read_config_file(self
, path
):
240 """Read a .gitconfig file into a dict"""
243 header_simple
= re
.compile(r
'^\[(\s+)]$')
244 header_subkey
= re
.compile(r
'^\[(\s+) "(\s+)"\]$')
246 with core
.xopen(path
, 'rt') as f
:
247 file_lines
= f
.readlines()
249 stripped_lines
= [line
.strip() for line
in file_lines
]
250 lines
= [line
for line
in stripped_lines
if bool(line
)]
253 if line
.startswith('#'):
256 match
= header_simple
.match(line
)
258 prefix
= match
.group(1) + '.'
260 match
= header_subkey
.match(line
)
262 prefix
= match
.group(1) + '.' + match
.group(2) + '.'
265 k
, v
= _config_key_value(line
, '=')
267 self
._map
[k
.lower()] = k
272 def _get(self
, src
, key
, default
, fn
=None, cached
=True):
273 if not cached
or not src
:
276 value
= self
._get
_with
_fallback
(src
, key
)
284 def _get_with_fallback(self
, src
, key
):
289 key
= self
._map
.get(key
.lower(), key
)
294 # Allow the final KeyError to bubble up
295 return src
[key
.lower()]
297 def get(self
, key
, default
=None, fn
=None, cached
=True):
298 """Return the string value for a config key."""
299 return self
._get
(self
._all
, key
, default
, fn
=fn
, cached
=cached
)
301 def get_all(self
, key
):
302 """Return all values for a key sorted in priority order
304 The purpose of this function is to group the values returned by
305 `git config --show-origin --get-all` so that the relative order is
306 preserved but can still be overridden at each level.
308 One use case is the `cola.icontheme` variable, which is an ordered
309 list of icon themes to load. This value can be set both in
310 ~/.gitconfig as well as .git/config, and we want to allow a
311 relative order to be defined in either file.
313 The problem is that git will read the system /etc/gitconfig,
314 global ~/.gitconfig, and then the local .git/config settings
315 and return them in that order, so we must post-process them to
316 get them in an order which makes sense for use for our values.
317 Otherwise, we cannot replace the order, or make a specific theme used
318 first, in our local .git/config since the native order returned by
319 git will always list the global config before the local one.
321 get_all() allows for this use case by gathering all of the per-config
322 values separately and then orders them according to the expected
323 local > user > system precedence order.
327 status
, out
, _
= self
.git
.config(
328 key
, z
=True, get_all
=True, show_origin
=True, _readonly
=True
334 items
= [x
for x
in out
.rstrip(chr(0)).split(chr(0)) if x
]
335 for i
in range(len(items
) // 2):
336 source
= items
[i
* 2]
337 value
= items
[i
* 2 + 1]
338 if source
!= current_source
:
339 current_source
= source
341 partial_results
.append(current_result
)
342 current_result
.append(value
)
343 # Git's results are ordered System, Global, Local.
344 # Reverse the order here so that Local has the highest priority.
345 for partial_result
in reversed(partial_results
):
346 result
.extend(partial_result
)
350 def get_user(self
, key
, default
=None):
351 return self
._get
(self
._user
, key
, default
)
353 def get_repo(self
, key
, default
=None):
354 return self
._get
(self
._repo
, key
, default
)
356 def get_user_or_system(self
, key
, default
=None):
357 return self
._get
(self
._user
_or
_system
, key
, default
)
359 def set_user(self
, key
, value
):
360 if value
in (None, ''):
361 self
.git
.config('--global', key
, unset
=True, _readonly
=True)
363 self
.git
.config('--global', key
, python_to_git(value
), _readonly
=True)
365 self
.user_config_changed
.emit(key
, value
)
367 def set_repo(self
, key
, value
):
368 if value
in (None, ''):
369 self
.git
.config(key
, unset
=True, _readonly
=True)
371 self
.git
.config(key
, python_to_git(value
), _readonly
=True)
373 self
.repo_config_changed
.emit(key
, value
)
376 """Return a a dict of values for all keys matching the specified pattern"""
378 match
= fnmatch
.fnmatch
382 for key
, val
in self
._all
.items():
383 if match(key
.lower(), pat
):
388 """Return True when git-annex is enabled"""
389 return bool(self
.get('annex.uuid', default
=False))
391 def gui_encoding(self
):
392 return self
.get('gui.encoding', default
=None)
394 def is_per_file_attrs_enabled(self
):
396 'cola.fileattributes', fn
=lambda: os
.path
.exists('.gitattributes')
399 def is_binary(self
, path
):
400 """Return True if the file has the binary attribute set"""
401 if not self
.is_per_file_attrs_enabled():
403 cache
= self
._binary
_cache
407 value
= cache
[path
] = self
._is
_binary
(path
)
410 def _is_binary(self
, path
):
411 """Return the file encoding for a path"""
412 value
= self
.check_attr('binary', path
)
413 return value
== 'set'
415 def file_encoding(self
, path
):
416 if not self
.is_per_file_attrs_enabled():
417 return self
.gui_encoding()
418 cache
= self
._attr
_cache
422 value
= cache
[path
] = self
._file
_encoding
(path
) or self
.gui_encoding()
425 def _file_encoding(self
, path
):
426 """Return the file encoding for a path"""
427 encoding
= self
.check_attr('encoding', path
)
428 if encoding
in ('unspecified', 'unset', 'set'):
434 def check_attr(self
, attr
, path
):
435 """Check file attributes for a path"""
437 status
, out
, _
= self
.git
.check_attr(attr
, '--', path
, _readonly
=True)
439 header
= '%s: %s: ' % (path
, attr
)
440 if out
.startswith(header
):
441 value
= out
[len(header
) :].strip()
444 def get_guitool_opts(self
, name
):
445 """Return the guitool.<name> namespace as a dict
447 The dict keys are simplified so that "guitool.$name.cmd" is accessible
451 prefix
= len('guitool.%s.' % name
)
452 guitools
= self
.find('guitool.%s.*' % name
)
453 return dict([(key
[prefix
:], value
) for (key
, value
) in guitools
.items()])
455 def get_guitool_names(self
):
456 guitools
= self
.find('guitool.*.cmd')
457 prefix
= len('guitool.')
459 return sorted([name
[prefix
:-suffix
] for (name
, _
) in guitools
.items()])
461 def get_guitool_names_and_shortcuts(self
):
462 """Return guitool names and their configured shortcut"""
463 names
= self
.get_guitool_names()
464 return [(name
, self
.get('guitool.%s.shortcut' % name
)) for name
in names
]
467 term
= self
.get('cola.terminal', default
=None)
469 # find a suitable default terminal
470 term
= 'xterm -e' # for mac osx
472 # Try to find Git's sh.exe directory in
473 # one of the typical locations
474 pf
= os
.environ
.get('ProgramFiles', r
'C:\Program Files')
475 pf32
= os
.environ
.get('ProgramFiles(x86)', r
'C:\Program Files (x86)')
476 pf64
= os
.environ
.get('ProgramW6432', r
'C:\Program Files')
478 for p
in [pf64
, pf32
, pf
, 'C:\\']:
479 candidate
= os
.path
.join(p
, r
'Git\bin\sh.exe')
480 if os
.path
.isfile(candidate
):
484 candidates
= ('xfce4-terminal', 'konsole', 'gnome-terminal')
485 for basename
in candidates
:
486 if core
.exists('/usr/bin/%s' % basename
):
487 if basename
== 'gnome-terminal':
488 term
= '%s --' % basename
490 term
= '%s -e' % basename
494 def color(self
, key
, default
):
495 value
= self
.get('cola.color.%s' % key
, default
=default
)
496 struct_layout
= core
.encode('BBB')
498 # pylint: disable=no-member
499 r
, g
, b
= struct
.unpack(struct_layout
, unhex(value
))
500 except (struct
.error
, TypeError):
501 # pylint: disable=no-member
502 r
, g
, b
= struct
.unpack(struct_layout
, unhex(default
))
506 """Return the path to the git hooks directory"""
507 gitdir_hooks
= self
.git
.git_path('hooks')
508 return self
.get('core.hookspath', default
=gitdir_hooks
)
510 def hooks_path(self
, *paths
):
511 """Return a path from within the git hooks directory"""
512 return os
.path
.join(self
.hooks(), *paths
)
515 def python_to_git(value
):
516 if isinstance(value
, bool):
517 return 'true' if value
else 'false'
518 if isinstance(value
, int_types
):
523 def get_remotes(cfg
):
524 """Get all of the configured git remotes"""
525 # Gather all of the remote.*.url entries.
526 prefix
= len('remote.')
528 return sorted(key
[prefix
:-suffix
] for key
in cfg
.find('remote.*.url'))