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