qtutils: avoid None in add_items()
[git-cola.git] / cola / gitcfg.py
blobd518704e712940902b5d54c2d380fd9773438feb
1 from __future__ import division, absolute_import, unicode_literals
3 import copy
4 import fnmatch
5 import os
6 import re
7 import struct
8 from binascii import unhexlify
9 from os.path import join
11 from cola import core
12 from cola import git
13 from cola import observable
14 from cola.decorators import memoize
15 from cola.git import STDOUT
16 from cola.compat import ustr
18 BUILTIN_READER = os.environ.get('GIT_COLA_BUILTIN_CONFIG_READER', False)
20 _USER_CONFIG = core.expanduser(join('~', '.gitconfig'))
21 _USER_XDG_CONFIG = core.expanduser(
22 join(core.getenv('XDG_CONFIG_HOME', join('~', '.config')),
23 'git', 'config'))
25 @memoize
26 def current():
27 """Return the GitConfig singleton."""
28 return GitConfig()
31 def _stat_info():
32 # Try /etc/gitconfig as a fallback for the system config
33 paths = (('system', '/etc/gitconfig'),
34 ('user', _USER_XDG_CONFIG),
35 ('user', _USER_CONFIG),
36 ('repo', git.current().git_path('config')))
37 statinfo = []
38 for category, path in paths:
39 try:
40 statinfo.append((category, path, core.stat(path).st_mtime))
41 except OSError:
42 continue
43 return statinfo
46 def _cache_key():
47 # Try /etc/gitconfig as a fallback for the system config
48 paths = ('/etc/gitconfig',
49 _USER_XDG_CONFIG,
50 _USER_CONFIG,
51 git.current().git_path('config'))
52 mtimes = []
53 for path in paths:
54 try:
55 mtimes.append(core.stat(path).st_mtime)
56 except OSError:
57 continue
58 return mtimes
61 def _config_to_python(v):
62 """Convert a Git config string into a Python value"""
64 if v in ('true', 'yes'):
65 v = True
66 elif v in ('false', 'no'):
67 v = False
68 else:
69 try:
70 v = int(v)
71 except ValueError:
72 pass
73 return v
76 def _config_key_value(line, splitchar):
77 """Split a config line into a (key, value) pair"""
79 try:
80 k, v = line.split(splitchar, 1)
81 except ValueError:
82 # the user has a emptyentry in their git config,
83 # which Git interprets as meaning "true"
84 k = line
85 v = 'true'
86 return k, _config_to_python(v)
89 class GitConfig(observable.Observable):
90 """Encapsulate access to git-config values."""
92 message_user_config_changed = 'user_config_changed'
93 message_repo_config_changed = 'repo_config_changed'
95 def __init__(self):
96 observable.Observable.__init__(self)
97 self.git = git.current()
98 self._map = {}
99 self._system = {}
100 self._user = {}
101 self._user_or_system = {}
102 self._repo = {}
103 self._all = {}
104 self._cache_key = None
105 self._configs = []
106 self._config_files = {}
107 self._value_cache = {}
108 self._attr_cache = {}
109 self._find_config_files()
111 def reset(self):
112 self._map.clear()
113 self._system.clear()
114 self._user.clear()
115 self._user_or_system.clear()
116 self._repo.clear()
117 self._all.clear()
118 self._cache_key = None
119 self._configs = []
120 self._config_files.clear()
121 self._value_cache = {}
122 self._attr_cache = {}
123 self._find_config_files()
125 def user(self):
126 return copy.deepcopy(self._user)
128 def repo(self):
129 return copy.deepcopy(self._repo)
131 def all(self):
132 return copy.deepcopy(self._all)
134 def _find_config_files(self):
136 Classify git config files into 'system', 'user', and 'repo'.
138 Populates self._configs with a list of the files in
139 reverse-precedence order. self._config_files is populated with
140 {category: path} where category is one of 'system', 'user', or 'repo'.
143 # Try the git config in git's installation prefix
144 statinfo = _stat_info()
145 self._configs = map(lambda x: x[1], statinfo)
146 self._config_files = {}
147 for (cat, path, mtime) in statinfo:
148 self._config_files[cat] = path
150 def update(self):
151 """Read config values from git."""
152 if self._cached():
153 return
154 self._read_configs()
156 def _cached(self):
158 Return True when the cache matches.
160 Updates the cache and returns False when the cache does not match.
163 cache_key = _cache_key()
164 if self._cache_key is None or cache_key != self._cache_key:
165 self._cache_key = cache_key
166 return False
167 return True
169 def _read_configs(self):
170 """Read git config value into the system, user and repo dicts."""
171 self._map.clear()
172 self._system.clear()
173 self._user.clear()
174 self._user_or_system.clear()
175 self._repo.clear()
176 self._all.clear()
178 if 'system' in self._config_files:
179 self._system.update(
180 self.read_config(self._config_files['system']))
182 if 'user' in self._config_files:
183 self._user.update(
184 self.read_config(self._config_files['user']))
186 if 'repo' in self._config_files:
187 self._repo.update(
188 self.read_config(self._config_files['repo']))
190 for dct in (self._system, self._user):
191 self._user_or_system.update(dct)
193 for dct in (self._system, self._user, self._repo):
194 self._all.update(dct)
196 def read_config(self, path):
197 """Return git config data from a path as a dictionary."""
199 if BUILTIN_READER:
200 return self._read_config_file(path)
202 dest = {}
203 args = ('--null', '--file', path, '--list')
204 config_lines = self.git.config(*args)[STDOUT].split('\0')
205 for line in config_lines:
206 if not line:
207 # the user has an invalid entry in their git config
208 continue
209 k, v = _config_key_value(line, '\n')
210 self._map[k.lower()] = k
211 dest[k] = v
212 return dest
214 def _read_config_file(self, path):
215 """Read a .gitconfig file into a dict"""
217 config = {}
218 header_simple = re.compile(r'^\[(\s+)]$')
219 header_subkey = re.compile(r'^\[(\s+) "(\s+)"\]$')
221 with core.xopen(path, 'rt') as f:
222 lines = filter(bool, [line.strip() for line in f.readlines()])
224 prefix = ''
225 for line in lines:
226 if line.startswith('#'):
227 continue
229 match = header_simple.match(line)
230 if match:
231 prefix = match.group(1) + '.'
232 continue
233 match = header_subkey.match(line)
234 if match:
235 prefix = match.group(1) + '.' + match.group(2) + '.'
236 continue
238 k, v = _config_key_value(line, '=')
239 k = prefix + k
240 self._map[k.lower()] = k
241 config[k] = v
243 return config
245 def _get(self, src, key, default):
246 self.update()
247 try:
248 value = self._get_with_fallback(src, key)
249 except KeyError:
250 value = default
251 return value
253 def _get_with_fallback(self, src, key):
254 try:
255 return src[key]
256 except KeyError:
257 pass
258 key = self._map.get(key.lower(), key)
259 try:
260 return src[key]
261 except KeyError:
262 pass
263 # Allow the final KeyError to bubble up
264 return src[key.lower()]
266 def get(self, key, default=None):
267 """Return the string value for a config key."""
268 return self._get(self._all, key, default)
270 def get_user(self, key, default=None):
271 return self._get(self._user, key, default)
273 def get_repo(self, key, default=None):
274 return self._get(self._repo, key, default)
276 def get_user_or_system(self, key, default=None):
277 return self._get(self._user_or_system, key, default)
279 def python_to_git(self, value):
280 if type(value) is bool:
281 if value:
282 return 'true'
283 else:
284 return 'false'
285 if type(value) is int:
286 return ustr(value)
287 return value
289 def set_user(self, key, value):
290 msg = self.message_user_config_changed
291 self.git.config('--global', key, self.python_to_git(value))
292 self.update()
293 self.notify_observers(msg, key, value)
295 def set_repo(self, key, value):
296 msg = self.message_repo_config_changed
297 self.git.config(key, self.python_to_git(value))
298 self.update()
299 self.notify_observers(msg, key, value)
301 def find(self, pat):
302 pat = pat.lower()
303 match = fnmatch.fnmatch
304 result = {}
305 self.update()
306 for key, val in self._all.items():
307 if match(key.lower(), pat):
308 result[key] = val
309 return result
311 def get_cached(self, key, default=None):
312 cache = self._value_cache
313 try:
314 value = cache[key]
315 except KeyError:
316 value = cache[key] = self.get(key, default=default)
317 return value
319 def gui_encoding(self):
320 return self.get_cached('gui.encoding', default='utf-8')
322 def is_per_file_attrs_enabled(self):
323 return self.get_cached('cola.fileattributes', default=False)
325 def file_encoding(self, path):
326 if not self.is_per_file_attrs_enabled():
327 return self.gui_encoding()
328 cache = self._attr_cache
329 try:
330 value = cache[path]
331 except KeyError:
332 value = cache[path] = (self._file_encoding(path) or
333 self.gui_encoding())
334 return value
336 def _file_encoding(self, path):
337 """Return the file encoding for a path"""
338 status, out, err = self.git.check_attr('encoding', '--', path)
339 if status != 0:
340 return None
341 header = '%s: encoding: ' % path
342 if out.startswith(header):
343 encoding = out[len(header):].strip()
344 if (encoding != 'unspecified' and
345 encoding != 'unset' and
346 encoding != 'set'):
347 return encoding
348 return None
350 def get_guitool_opts(self, name):
351 """Return the guitool.<name> namespace as a dict
353 The dict keys are simplified so that "guitool.$name.cmd" is accessible
354 as `opts[cmd]`.
357 prefix = len('guitool.%s.' % name)
358 guitools = self.find('guitool.%s.*' % name)
359 return dict([(key[prefix:], value)
360 for (key, value) in guitools.items()])
362 def get_guitool_names(self):
363 guitools = self.find('guitool.*.cmd')
364 prefix = len('guitool.')
365 suffix = len('.cmd')
366 return sorted([name[prefix:-suffix]
367 for (name, cmd) in guitools.items()])
369 def get_guitool_names_and_shortcuts(self):
370 """Return guitool names and their configured shortcut"""
371 names = self.get_guitool_names()
372 return [(name, self.get('guitool.%s.shortcut' % name)) for name in names]
374 def terminal(self):
375 term = self.get('cola.terminal', None)
376 if not term:
377 # find a suitable default terminal
378 term = 'xterm -e' # for mac osx
379 candidates = ('xfce4-terminal', 'konsole')
380 for basename in candidates:
381 if core.exists('/usr/bin/%s' % basename):
382 term = '%s -e' % basename
383 break
384 return term
386 def color(self, key, default):
387 string = self.get('cola.color.%s' % key, default)
388 string = core.encode(string)
389 default = core.encode(default)
390 struct_layout = core.encode('BBB')
391 try:
392 r, g, b = struct.unpack(struct_layout, unhexlify(string))
393 except Exception:
394 r, g, b = struct.unpack(struct_layout, unhexlify(default))
395 return (r, g, b)