1 from __future__
import division
, absolute_import
, unicode_literals
8 from binascii
import unhexlify
9 from os
.path
import join
13 from . import observable
14 from .compat
import int_types
15 from .decorators
import memoize
16 from .git
import STDOUT
17 from .compat
import ustr
19 BUILTIN_READER
= os
.environ
.get('GIT_COLA_BUILTIN_CONFIG_READER', False)
21 _USER_CONFIG
= core
.expanduser(join('~', '.gitconfig'))
22 _USER_XDG_CONFIG
= core
.expanduser(
23 join(core
.getenv('XDG_CONFIG_HOME', join('~', '.config')),
29 """Return the GitConfig singleton."""
34 # Try /etc/gitconfig as a fallback for the system config
35 paths
= [('system', '/etc/gitconfig'),
36 ('user', _USER_XDG_CONFIG
),
37 ('user', _USER_CONFIG
)]
38 config
= git
.current().git_path('config')
40 paths
.append(('repo', config
))
43 for category
, path
in paths
:
45 statinfo
.append((category
, path
, core
.stat(path
).st_mtime
))
52 # Try /etc/gitconfig as a fallback for the system config
53 paths
= ['/etc/gitconfig',
56 config
= git
.current().git_path('config')
63 mtimes
.append(core
.stat(path
).st_mtime
)
69 def _config_to_python(v
):
70 """Convert a Git config string into a Python value"""
72 if v
in ('true', 'yes'):
74 elif v
in ('false', 'no'):
84 def _config_key_value(line
, splitchar
):
85 """Split a config line into a (key, value) pair"""
88 k
, v
= line
.split(splitchar
, 1)
90 # the user has a emptyentry in their git config,
91 # which Git interprets as meaning "true"
94 return k
, _config_to_python(v
)
97 class GitConfig(observable
.Observable
):
98 """Encapsulate access to git-config values."""
100 message_user_config_changed
= 'user_config_changed'
101 message_repo_config_changed
= 'repo_config_changed'
104 observable
.Observable
.__init
__(self
)
105 self
.git
= git
.current()
109 self
._user
_or
_system
= {}
112 self
._cache
_key
= None
114 self
._config
_files
= {}
115 self
._value
_cache
= {}
116 self
._attr
_cache
= {}
117 self
._find
_config
_files
()
123 self
._user
_or
_system
.clear()
126 self
._cache
_key
= None
128 self
._config
_files
.clear()
129 self
._value
_cache
= {}
130 self
._attr
_cache
= {}
131 self
._find
_config
_files
()
134 return copy
.deepcopy(self
._user
)
137 return copy
.deepcopy(self
._repo
)
140 return copy
.deepcopy(self
._all
)
142 def _find_config_files(self
):
144 Classify git config files into 'system', 'user', and 'repo'.
146 Populates self._configs with a list of the files in
147 reverse-precedence order. self._config_files is populated with
148 {category: path} where category is one of 'system', 'user', or 'repo'.
151 # Try the git config in git's installation prefix
152 statinfo
= _stat_info()
153 self
._configs
= [x
[1] for x
in statinfo
]
154 self
._config
_files
= {}
155 for (cat
, path
, mtime
) in statinfo
:
156 self
._config
_files
[cat
] = path
159 """Read config values from git."""
166 Return True when the cache matches.
168 Updates the cache and returns False when the cache does not match.
171 cache_key
= _cache_key()
172 if self
._cache
_key
is None or cache_key
!= self
._cache
_key
:
173 self
._cache
_key
= cache_key
177 def _read_configs(self
):
178 """Read git config value into the system, user and repo dicts."""
182 self
._user
_or
_system
.clear()
186 if 'system' in self
._config
_files
:
188 self
.read_config(self
._config
_files
['system']))
190 if 'user' in self
._config
_files
:
192 self
.read_config(self
._config
_files
['user']))
194 if 'repo' in self
._config
_files
:
196 self
.read_config(self
._config
_files
['repo']))
198 for dct
in (self
._system
, self
._user
):
199 self
._user
_or
_system
.update(dct
)
201 for dct
in (self
._system
, self
._user
, self
._repo
):
202 self
._all
.update(dct
)
204 def read_config(self
, path
):
205 """Return git config data from a path as a dictionary."""
208 return self
._read
_config
_file
(path
)
211 args
= ('--null', '--file', path
, '--list')
212 config_lines
= self
.git
.config(*args
)[STDOUT
].split('\0')
213 for line
in config_lines
:
215 # the user has an invalid entry in their git config
217 k
, v
= _config_key_value(line
, '\n')
218 self
._map
[k
.lower()] = k
222 def _read_config_file(self
, path
):
223 """Read a .gitconfig file into a dict"""
226 header_simple
= re
.compile(r
'^\[(\s+)]$')
227 header_subkey
= re
.compile(r
'^\[(\s+) "(\s+)"\]$')
229 with core
.xopen(path
, 'rt') as f
:
230 file_lines
= f
.readlines()
232 stripped_lines
= [line
.strip() for line
in file_lines
]
233 lines
= [line
for line
in stripped_lines
if bool(line
)]
236 if line
.startswith('#'):
239 match
= header_simple
.match(line
)
241 prefix
= match
.group(1) + '.'
243 match
= header_subkey
.match(line
)
245 prefix
= match
.group(1) + '.' + match
.group(2) + '.'
248 k
, v
= _config_key_value(line
, '=')
250 self
._map
[k
.lower()] = k
255 def _get(self
, src
, key
, default
):
258 value
= self
._get
_with
_fallback
(src
, key
)
263 def _get_with_fallback(self
, src
, key
):
268 key
= self
._map
.get(key
.lower(), key
)
273 # Allow the final KeyError to bubble up
274 return src
[key
.lower()]
276 def get(self
, key
, default
=None):
277 """Return the string value for a config key."""
278 return self
._get
(self
._all
, key
, default
)
280 def get_all(self
, key
):
281 """Return all values for a key"""
282 status
, out
, err
= self
.git
.config(key
, z
=True, get_all
=True)
284 result
= [x
for x
in out
.rstrip(chr(0)).split(chr(0)) if x
]
289 def get_user(self
, key
, default
=None):
290 return self
._get
(self
._user
, key
, default
)
292 def get_repo(self
, key
, default
=None):
293 return self
._get
(self
._repo
, key
, default
)
295 def get_user_or_system(self
, key
, default
=None):
296 return self
._get
(self
._user
_or
_system
, key
, default
)
298 def python_to_git(self
, value
):
299 if type(value
) is bool:
304 if isinstance(value
, int_types
):
308 def set_user(self
, key
, value
):
312 self
.git
.config('--global', key
, self
.python_to_git(value
))
314 msg
= self
.message_user_config_changed
315 self
.notify_observers(msg
, key
, value
)
317 def set_repo(self
, key
, value
):
318 self
.git
.config(key
, self
.python_to_git(value
))
320 msg
= self
.message_repo_config_changed
321 self
.notify_observers(msg
, key
, value
)
323 def unset_user(self
, key
):
324 self
.git
.config('--global', '--unset', key
)
326 msg
= self
.message_repo_config_changed
327 self
.notify_observers(msg
, key
, None)
331 match
= fnmatch
.fnmatch
334 for key
, val
in self
._all
.items():
335 if match(key
.lower(), pat
):
339 def get_cached(self
, key
, default
=None):
340 cache
= self
._value
_cache
344 value
= cache
[key
] = self
.get(key
, default
=default
)
347 def gui_encoding(self
):
348 return self
.get_cached('gui.encoding', default
='utf-8')
350 def is_per_file_attrs_enabled(self
):
351 return self
.get_cached('cola.fileattributes', default
=False)
353 def file_encoding(self
, path
):
354 if not self
.is_per_file_attrs_enabled():
355 return self
.gui_encoding()
356 cache
= self
._attr
_cache
360 value
= cache
[path
] = (self
._file
_encoding
(path
) or
364 def _file_encoding(self
, path
):
365 """Return the file encoding for a path"""
366 status
, out
, err
= self
.git
.check_attr('encoding', '--', path
)
369 header
= '%s: encoding: ' % path
370 if out
.startswith(header
):
371 encoding
= out
[len(header
):].strip()
372 if (encoding
!= 'unspecified' and
373 encoding
!= 'unset' and
378 def get_guitool_opts(self
, name
):
379 """Return the guitool.<name> namespace as a dict
381 The dict keys are simplified so that "guitool.$name.cmd" is accessible
385 prefix
= len('guitool.%s.' % name
)
386 guitools
= self
.find('guitool.%s.*' % name
)
387 return dict([(key
[prefix
:], value
)
388 for (key
, value
) in guitools
.items()])
390 def get_guitool_names(self
):
391 guitools
= self
.find('guitool.*.cmd')
392 prefix
= len('guitool.')
394 return sorted([name
[prefix
:-suffix
]
395 for (name
, cmd
) in guitools
.items()])
397 def get_guitool_names_and_shortcuts(self
):
398 """Return guitool names and their configured shortcut"""
399 names
= self
.get_guitool_names()
400 return [(name
, self
.get('guitool.%s.shortcut' % name
))
404 term
= self
.get('cola.terminal', None)
406 # find a suitable default terminal
407 term
= 'xterm -e' # for mac osx
408 candidates
= ('xfce4-terminal', 'konsole', 'gnome-terminal')
409 for basename
in candidates
:
410 if core
.exists('/usr/bin/%s' % basename
):
411 term
= '%s -e' % basename
415 def color(self
, key
, default
):
416 string
= self
.get('cola.color.%s' % key
, default
)
417 string
= core
.encode(string
)
418 default
= core
.encode(default
)
419 struct_layout
= core
.encode('BBB')
421 r
, g
, b
= struct
.unpack(struct_layout
, unhexlify(string
))
423 r
, g
, b
= struct
.unpack(struct_layout
, unhexlify(default
))