1 from __future__
import division
, absolute_import
, unicode_literals
2 from binascii
import unhexlify
6 from os
.path
import join
11 from . import observable
13 from .compat
import int_types
14 from .git
import STDOUT
15 from .compat
import ustr
17 BUILTIN_READER
= os
.environ
.get('GIT_COLA_BUILTIN_CONFIG_READER', False)
19 _USER_CONFIG
= core
.expanduser(join('~', '.gitconfig'))
20 _USER_XDG_CONFIG
= core
.expanduser(
21 join(core
.getenv('XDG_CONFIG_HOME', join('~', '.config')),
26 """Create GitConfig instances"""
27 return GitConfig(context
)
31 # Try /etc/gitconfig as a fallback for the system config
32 paths
= [('system', '/etc/gitconfig'),
33 ('user', _USER_XDG_CONFIG
),
34 ('user', _USER_CONFIG
)]
35 config
= git
.git_path('config')
37 paths
.append(('repo', config
))
40 for category
, path
in paths
:
42 statinfo
.append((category
, path
, core
.stat(path
).st_mtime
))
49 # Try /etc/gitconfig as a fallback for the system config
55 config
= git
.git_path('config')
62 mtimes
.append(core
.stat(path
).st_mtime
)
68 def _config_to_python(v
):
69 """Convert a Git config string into a Python value"""
71 if v
in ('true', 'yes'):
73 elif v
in ('false', 'no'):
84 """Convert a value (int or hex string) into bytes"""
85 if isinstance(value
, int_types
):
86 # If the value is an integer then it's a value that was converted
87 # by the config reader. Zero-pad it into a 6-digit hex number.
88 value
= '%06d' % value
89 return unhexlify(core
.encode(value
.lstrip('#')))
92 def _config_key_value(line
, splitchar
):
93 """Split a config line into a (key, value) pair"""
96 k
, v
= line
.split(splitchar
, 1)
98 # the user has an empty entry in their git config,
99 # which Git interprets as meaning "true"
102 return k
, _config_to_python(v
)
105 class GitConfig(observable
.Observable
):
106 """Encapsulate access to git-config values."""
108 message_user_config_changed
= 'user_config_changed'
109 message_repo_config_changed
= 'repo_config_changed'
110 message_updated
= 'updated'
112 def __init__(self
, context
):
113 observable
.Observable
.__init
__(self
)
114 self
.git
= context
.git
118 self
._user
_or
_system
= {}
121 self
._cache
_key
= None
123 self
._config
_files
= {}
124 self
._attr
_cache
= {}
125 self
._find
_config
_files
()
128 self
._cache
_key
= None
130 self
._config
_files
.clear()
131 self
._attr
_cache
= {}
132 self
._find
_config
_files
()
135 def reset_values(self
):
139 self
._user
_or
_system
.clear()
144 return copy
.deepcopy(self
._user
)
147 return copy
.deepcopy(self
._repo
)
150 return copy
.deepcopy(self
._all
)
152 def _find_config_files(self
):
154 Classify git config files into 'system', 'user', and 'repo'.
156 Populates self._configs with a list of the files in
157 reverse-precedence order. self._config_files is populated with
158 {category: path} where category is one of 'system', 'user', or 'repo'.
161 # Try the git config in git's installation prefix
162 statinfo
= _stat_info(self
.git
)
163 self
._configs
= [x
[1] for x
in statinfo
]
164 self
._config
_files
= {}
165 for (cat
, path
, _
) in statinfo
:
166 self
._config
_files
[cat
] = path
170 Return True when the cache matches.
172 Updates the cache and returns False when the cache does not match.
175 cache_key
= _cache_key(self
.git
)
176 if self
._cache
_key
is None or cache_key
!= self
._cache
_key
:
177 self
._cache
_key
= cache_key
182 """Read git config value into the system, user and repo dicts."""
188 if 'system' in self
._config
_files
:
190 self
.read_config(self
._config
_files
['system']))
192 if 'user' in self
._config
_files
:
194 self
.read_config(self
._config
_files
['user']))
196 if 'repo' in self
._config
_files
:
198 self
.read_config(self
._config
_files
['repo']))
200 for dct
in (self
._system
, self
._user
):
201 self
._user
_or
_system
.update(dct
)
203 for dct
in (self
._system
, self
._user
, self
._repo
):
204 self
._all
.update(dct
)
206 self
.notify_observers(self
.message_updated
)
208 def read_config(self
, path
):
209 """Return git config data from a path as a dictionary."""
212 return self
._read
_config
_file
(path
)
215 args
= ('--null', '--file', path
, '--list')
216 config_lines
= self
.git
.config(*args
)[STDOUT
].split('\0')
217 for line
in config_lines
:
219 # the user has an invalid entry in their git config
221 k
, v
= _config_key_value(line
, '\n')
222 self
._map
[k
.lower()] = k
226 def _read_config_file(self
, path
):
227 """Read a .gitconfig file into a dict"""
230 header_simple
= re
.compile(r
'^\[(\s+)]$')
231 header_subkey
= re
.compile(r
'^\[(\s+) "(\s+)"\]$')
233 with core
.xopen(path
, 'rt') as f
:
234 file_lines
= f
.readlines()
236 stripped_lines
= [line
.strip() for line
in file_lines
]
237 lines
= [line
for line
in stripped_lines
if bool(line
)]
240 if line
.startswith('#'):
243 match
= header_simple
.match(line
)
245 prefix
= match
.group(1) + '.'
247 match
= header_subkey
.match(line
)
249 prefix
= match
.group(1) + '.' + match
.group(2) + '.'
252 k
, v
= _config_key_value(line
, '=')
254 self
._map
[k
.lower()] = k
259 def _get(self
, src
, key
, default
, fn
=None, cached
=True):
260 if not cached
or not src
:
263 value
= self
._get
_with
_fallback
(src
, key
)
271 def _get_with_fallback(self
, src
, key
):
276 key
= self
._map
.get(key
.lower(), key
)
281 # Allow the final KeyError to bubble up
282 return src
[key
.lower()]
284 def get(self
, key
, default
=None, fn
=None, cached
=True):
285 """Return the string value for a config key."""
286 return self
._get
(self
._all
, key
, default
, fn
=fn
, cached
=cached
)
288 def get_all(self
, key
):
289 """Return all values for a key sorted in priority order
291 The purpose of this function is to group the values returned by
292 `git config --show-origin --get-all` so that the relative order is
293 preserved but can still be overridden at each level.
295 One use case is the `cola.icontheme` variable, which is an ordered
296 list of icon themes to load. This value can be set both in
297 ~/.gitconfig as well as .git/config, and we want to allow a
298 relative order to be defined in either file.
300 The problem is that git will read the system /etc/gitconfig,
301 global ~/.gitconfig, and then the local .git/config settings
302 and return them in that order, so we must post-process them to
303 get them in an order which makes sense for use for our values.
304 Otherwise, we cannot replace the order, or make a specific theme used
305 first, in our local .git/config since the native order returned by
306 git will always list the global config before the local one.
308 get_all() allows for this use case by gathering all of the per-config
309 values separately and then orders them according to the expected
310 local > global > system order.
314 status
, out
, _
= self
.git
.config(
315 key
, z
=True, get_all
=True, show_origin
=True)
320 items
= [x
for x
in out
.rstrip(chr(0)).split(chr(0)) if x
]
321 for i
in range(len(items
) // 2):
322 source
= items
[i
* 2]
323 value
= items
[i
* 2 + 1]
324 if source
!= current_source
:
325 current_source
= source
327 partial_results
.append(current_result
)
328 current_result
.append(value
)
329 # Git's results are ordered System, Global, Local.
330 # Reverse the order here so that Local has the highest priority.
331 for partial_result
in reversed(partial_results
):
332 result
.extend(partial_result
)
336 def get_user(self
, key
, default
=None):
337 return self
._get
(self
._user
, key
, default
)
339 def get_repo(self
, key
, default
=None):
340 return self
._get
(self
._repo
, key
, default
)
342 def get_user_or_system(self
, key
, default
=None):
343 return self
._get
(self
._user
_or
_system
, key
, default
)
345 def set_user(self
, key
, value
):
346 if value
in (None, ''):
347 self
.git
.config('--global', key
, unset
=True)
349 self
.git
.config('--global', key
, python_to_git(value
))
351 msg
= self
.message_user_config_changed
352 self
.notify_observers(msg
, key
, value
)
354 def set_repo(self
, key
, value
):
355 if value
in (None, ''):
356 self
.git
.config(key
, unset
=True)
358 self
.git
.config(key
, python_to_git(value
))
360 msg
= self
.message_repo_config_changed
361 self
.notify_observers(msg
, key
, value
)
365 match
= fnmatch
.fnmatch
369 for key
, val
in self
._all
.items():
370 if match(key
.lower(), pat
):
375 """Return True when git-annex is enabled"""
376 return bool(self
.get('annex.uuid', default
=False))
378 def gui_encoding(self
):
379 return self
.get('gui.encoding', default
=None)
381 def is_per_file_attrs_enabled(self
):
382 return self
.get('cola.fileattributes',
383 fn
=lambda: os
.path
.exists('.gitattributes'))
385 def file_encoding(self
, path
):
386 if not self
.is_per_file_attrs_enabled():
387 return self
.gui_encoding()
388 cache
= self
._attr
_cache
392 value
= cache
[path
] = (self
._file
_encoding
(path
) or
396 def _file_encoding(self
, path
):
397 """Return the file encoding for a path"""
398 status
, out
, _
= self
.git
.check_attr('encoding', '--', path
)
401 header
= '%s: encoding: ' % path
402 if out
.startswith(header
):
403 encoding
= out
[len(header
):].strip()
404 if encoding
not in ('unspecified', 'unset', 'set'):
408 def get_guitool_opts(self
, name
):
409 """Return the guitool.<name> namespace as a dict
411 The dict keys are simplified so that "guitool.$name.cmd" is accessible
415 prefix
= len('guitool.%s.' % name
)
416 guitools
= self
.find('guitool.%s.*' % name
)
417 return dict([(key
[prefix
:], value
)
418 for (key
, value
) in guitools
.items()])
420 def get_guitool_names(self
):
421 guitools
= self
.find('guitool.*.cmd')
422 prefix
= len('guitool.')
424 return sorted([name
[prefix
:-suffix
]
425 for (name
, _
) in guitools
.items()])
427 def get_guitool_names_and_shortcuts(self
):
428 """Return guitool names and their configured shortcut"""
429 names
= self
.get_guitool_names()
430 return [(name
, self
.get('guitool.%s.shortcut' % name
))
434 term
= self
.get('cola.terminal', default
=None)
436 # find a suitable default terminal
437 term
= 'xterm -e' # for mac osx
439 # Try to find Git's sh.exe directory in
440 # one of the typical locations
441 pf
= os
.environ
.get('ProgramFiles', 'C:\\Program Files')
442 pf32
= os
.environ
.get('ProgramFiles(x86)',
443 'C:\\Program Files (x86)')
444 pf64
= os
.environ
.get('ProgramW6432', 'C:\\Program Files')
446 for p
in [pf64
, pf32
, pf
, 'C:\\']:
447 candidate
= os
.path
.join(p
, 'Git\\bin\\sh.exe')
448 if os
.path
.isfile(candidate
):
452 candidates
= ('xfce4-terminal', 'konsole', 'gnome-terminal')
453 for basename
in candidates
:
454 if core
.exists('/usr/bin/%s' % basename
):
455 if basename
== 'gnome-terminal':
456 term
= '%s --' % basename
458 term
= '%s -e' % basename
462 def color(self
, key
, default
):
463 value
= self
.get('cola.color.%s' % key
, default
=default
)
464 struct_layout
= core
.encode('BBB')
466 r
, g
, b
= struct
.unpack(struct_layout
, unhex(value
))
467 except (struct
.error
, TypeError):
468 r
, g
, b
= struct
.unpack(struct_layout
, unhex(default
))
472 def python_to_git(value
):
473 if isinstance(value
, bool):
474 return 'true' if value
else 'false'
475 if isinstance(value
, int_types
):