git-cola v2.0.0
[git-cola.git] / cola / gitcfg.py
blob10d27bdd16b97d7cf017d2de1f0a59ba4eb61a78
1 from __future__ import division, absolute_import, unicode_literals
3 import copy
4 import fnmatch
5 from os.path import join
7 from cola import core
8 from cola import git
9 from cola import observable
10 from cola.decorators import memoize
11 from cola.git import STDOUT
12 from cola.compat import ustr
14 @memoize
15 def instance():
16 """Return a static GitConfig instance."""
17 return GitConfig()
19 _USER_CONFIG = core.expanduser(join('~', '.gitconfig'))
20 _USER_XDG_CONFIG = core.expanduser(
21 join(core.getenv('XDG_CONFIG_HOME', join('~', '.config')),
22 'git', 'config'))
24 def _stat_info():
25 # Try /etc/gitconfig as a fallback for the system config
26 paths = (('system', '/etc/gitconfig'),
27 ('user', _USER_XDG_CONFIG),
28 ('user', _USER_CONFIG),
29 ('repo', git.instance().git_path('config')))
30 statinfo = []
31 for category, path in paths:
32 try:
33 statinfo.append((category, path, core.stat(path).st_mtime))
34 except OSError:
35 continue
36 return statinfo
39 def _cache_key():
40 # Try /etc/gitconfig as a fallback for the system config
41 paths = ('/etc/gitconfig',
42 _USER_XDG_CONFIG,
43 _USER_CONFIG,
44 git.instance().git_path('config'))
45 mtimes = []
46 for path in paths:
47 try:
48 mtimes.append(core.stat(path).st_mtime)
49 except OSError:
50 continue
51 return mtimes
54 class GitConfig(observable.Observable):
55 """Encapsulate access to git-config values."""
57 message_user_config_changed = 'user_config_changed'
58 message_repo_config_changed = 'repo_config_changed'
60 def __init__(self):
61 observable.Observable.__init__(self)
62 self.git = git.instance()
63 self._map = {}
64 self._system = {}
65 self._user = {}
66 self._repo = {}
67 self._all = {}
68 self._cache_key = None
69 self._configs = []
70 self._config_files = {}
71 self._value_cache = {}
72 self._attr_cache = {}
73 self._find_config_files()
75 def reset(self):
76 self._map.clear()
77 self._system.clear()
78 self._user.clear()
79 self._repo.clear()
80 self._all.clear()
81 self._cache_key = None
82 self._configs = []
83 self._config_files.clear()
84 self._value_cache = {}
85 self._attr_cache = {}
86 self._find_config_files()
88 def user(self):
89 return copy.deepcopy(self._user)
91 def repo(self):
92 return copy.deepcopy(self._repo)
94 def all(self):
95 return copy.deepcopy(self._all)
97 def _find_config_files(self):
98 """
99 Classify git config files into 'system', 'user', and 'repo'.
101 Populates self._configs with a list of the files in
102 reverse-precedence order. self._config_files is populated with
103 {category: path} where category is one of 'system', 'user', or 'repo'.
106 # Try the git config in git's installation prefix
107 statinfo = _stat_info()
108 self._configs = map(lambda x: x[1], statinfo)
109 self._config_files = {}
110 for (cat, path, mtime) in statinfo:
111 self._config_files[cat] = path
113 def update(self):
114 """Read config values from git."""
115 if self._cached():
116 return
117 self._read_configs()
119 def _cached(self):
121 Return True when the cache matches.
123 Updates the cache and returns False when the cache does not match.
126 cache_key = _cache_key()
127 if self._cache_key is None or cache_key != self._cache_key:
128 self._cache_key = cache_key
129 return False
130 return True
132 def _read_configs(self):
133 """Read git config value into the system, user and repo dicts."""
134 self._map.clear()
135 self._system.clear()
136 self._user.clear()
137 self._repo.clear()
138 self._all.clear()
140 if 'system' in self._config_files:
141 self._system.update(
142 self.read_config(self._config_files['system']))
144 if 'user' in self._config_files:
145 self._user.update(
146 self.read_config(self._config_files['user']))
148 if 'repo' in self._config_files:
149 self._repo.update(
150 self.read_config(self._config_files['repo']))
152 for dct in (self._system, self._user, self._repo):
153 self._all.update(dct)
155 def read_config(self, path):
156 """Return git config data from a path as a dictionary."""
157 dest = {}
158 args = ('--null', '--file', path, '--list')
159 config_lines = self.git.config(*args)[STDOUT].split('\0')
160 for line in config_lines:
161 try:
162 k, v = line.split('\n', 1)
163 except ValueError:
164 # the user has an invalid entry in their git config
165 if not line:
166 continue
167 k = line
168 v = 'true'
170 if v in ('true', 'yes'):
171 v = True
172 elif v in ('false', 'no'):
173 v = False
174 else:
175 try:
176 v = int(v)
177 except ValueError:
178 pass
179 self._map[k.lower()] = k
180 dest[k] = v
181 return dest
183 def _get(self, src, key, default):
184 self.update()
185 try:
186 return src[key]
187 except KeyError:
188 pass
189 key = self._map.get(key.lower(), key)
190 try:
191 return src[key]
192 except KeyError:
193 return src.get(key.lower(), default)
195 def get(self, key, default=None):
196 """Return the string value for a config key."""
197 return self._get(self._all, key, default)
199 def get_user(self, key, default=None):
200 return self._get(self._user, key, default)
202 def get_repo(self, key, default=None):
203 return self._get(self._repo, key, default)
205 def python_to_git(self, value):
206 if type(value) is bool:
207 if value:
208 return 'true'
209 else:
210 return 'false'
211 if type(value) is int:
212 return ustr(value)
213 return value
215 def set_user(self, key, value):
216 msg = self.message_user_config_changed
217 self.git.config('--global', key, self.python_to_git(value))
218 self.update()
219 self.notify_observers(msg, key, value)
221 def set_repo(self, key, value):
222 msg = self.message_repo_config_changed
223 self.git.config(key, self.python_to_git(value))
224 self.update()
225 self.notify_observers(msg, key, value)
227 def find(self, pat):
228 pat = pat.lower()
229 match = fnmatch.fnmatch
230 result = {}
231 self.update()
232 for key, val in self._all.items():
233 if match(key, pat):
234 result[key] = val
235 return result
237 def get_cached(self, key, default=None):
238 cache = self._value_cache
239 try:
240 value = cache[key]
241 except KeyError:
242 value = cache[key] = self.get(key, default=default)
243 return value
245 def gui_encoding(self):
246 return self.get_cached('gui.encoding', default='utf-8')
248 def is_per_file_attrs_enabled(self):
249 return self.get_cached('cola.fileattributes', default=False)
251 def file_encoding(self, path):
252 if not self.is_per_file_attrs_enabled():
253 return None
254 cache = self._attr_cache
255 try:
256 value = cache[path]
257 except KeyError:
258 value = cache[path] = self._file_encoding(path)
259 return value
261 def _file_encoding(self, path):
262 """Return the file encoding for a path"""
263 status, out, err = self.git.check_attr('encoding', '--', path)
264 if status != 0:
265 return None
266 header = '%s: encoding: ' % path
267 if out.startswith(header):
268 encoding = out[len(header):].strip()
269 if (encoding != 'unspecified' and
270 encoding != 'unset' and
271 encoding != 'set'):
272 return encoding
273 return None
275 guitool_opts = ('cmd', 'needsfile', 'noconsole', 'norescan', 'confirm',
276 'argprompt', 'revprompt', 'revunmerged', 'title', 'prompt')
278 def get_guitool_opts(self, name):
279 """Return the guitool.<name> namespace as a dict"""
280 keyprefix = 'guitool.' + name + '.'
281 opts = {}
282 for cfg in self.guitool_opts:
283 value = self.get(keyprefix + cfg)
284 if value is None:
285 continue
286 opts[cfg] = value
287 return opts
289 def get_guitool_names(self):
290 guitools = self.find('guitool.*.cmd')
291 prefix = len('guitool.')
292 suffix = len('.cmd')
293 return sorted([name[prefix:-suffix]
294 for (name, cmd) in guitools.items()])