gitcfg: Make find() work with a cold cache
[git-cola.git] / cola / gitcfg.py
blob3f7c4e79f9f08ed1cf68cb1fe8d5fad2ef487f34
1 import os
2 import copy
3 import fnmatch
5 from cola import core
6 from cola import git
7 from cola import observable
8 from cola.decorators import memoize
11 @memoize
12 def instance():
13 """Return a static GitConfig instance."""
14 return GitConfig()
17 def _stat_info():
18 # Try /etc/gitconfig as a fallback for the system config
19 userconfig = os.path.expanduser(os.path.join('~', '.gitconfig'))
20 paths = (('system', '/etc/gitconfig'),
21 ('user', core.decode(userconfig)),
22 ('repo', core.decode(git.instance().git_path('config'))))
23 statinfo = []
24 for category, path in paths:
25 try:
26 statinfo.append((category, path, os.stat(path).st_mtime))
27 except OSError:
28 continue
29 return statinfo
32 def _cache_key():
33 # Try /etc/gitconfig as a fallback for the system config
34 userconfig = os.path.expanduser(os.path.join('~', '.gitconfig'))
35 paths = ('/etc/gitconfig',
36 userconfig,
37 git.instance().git_path('config'))
38 mtimes = []
39 for path in paths:
40 try:
41 mtimes.append(os.stat(path).st_mtime)
42 except OSError:
43 continue
44 return mtimes
47 class GitConfig(observable.Observable):
48 """Encapsulate access to git-config values."""
50 message_user_config_changed = 'user_config_changed'
51 message_repo_config_changed = 'repo_config_changed'
53 def __init__(self):
54 observable.Observable.__init__(self)
55 self.git = git.instance()
56 self._system = {}
57 self._user = {}
58 self._repo = {}
59 self._all = {}
60 self._cache_key = None
61 self._configs = []
62 self._config_files = {}
63 self._find_config_files()
65 def reset(self):
66 self._system = {}
67 self._user = {}
68 self._repo = {}
69 self._all = {}
70 self._cache_key = None
71 self._configs = []
72 self._config_files = {}
73 self._find_config_files()
75 def user(self):
76 return copy.deepcopy(self._user)
78 def repo(self):
79 return copy.deepcopy(self._repo)
81 def all(self):
82 return copy.deepcopy(self._all)
84 def _find_config_files(self):
85 """
86 Classify git config files into 'system', 'user', and 'repo'.
88 Populates self._configs with a list of the files in
89 reverse-precedence order. self._config_files is populated with
90 {category: path} where category is one of 'system', 'user', or 'repo'.
92 """
93 # Try the git config in git's installation prefix
94 statinfo = _stat_info()
95 self._configs = map(lambda x: x[1], statinfo)
96 self._config_files = {}
97 for (cat, path, mtime) in statinfo:
98 self._config_files[cat] = path
100 def update(self):
101 """Read config values from git."""
102 if self._cached():
103 return
104 self._read_configs()
106 def _cached(self):
108 Return True when the cache matches.
110 Updates the cache and returns False when the cache does not match.
113 cache_key = _cache_key()
114 if not self._cache_key or cache_key != self._cache_key:
115 self._cache_key = cache_key
116 return False
117 return True
119 def _read_configs(self):
120 """Read git config value into the system, user and repo dicts."""
121 self._system = {}
122 self._user = {}
123 self._repo = {}
124 self._all = {}
126 if 'system' in self._config_files:
127 self._system = self.read_config(self._config_files['system'])
129 if 'user' in self._config_files:
130 self._user = self.read_config(self._config_files['user'])
132 if 'repo' in self._config_files:
133 self._repo = self.read_config(self._config_files['repo'])
135 self._all = {}
136 for dct in (self._system, self._user, self._repo):
137 self._all.update(dct)
139 def read_config(self, path):
140 """Return git config data from a path as a dictionary."""
141 dest = {}
142 args = ('--null', '--file', path, '--list')
143 config_lines = self.git.config(*args).split('\0')
144 for line in config_lines:
145 try:
146 k, v = line.split('\n', 1)
147 except ValueError:
148 # the user has an invalid entry in their git config
149 if not line:
150 continue
151 k = line
152 v = 'true'
153 v = core.decode(v)
155 if v in ('true', 'yes'):
156 v = True
157 elif v in ('false', 'no'):
158 v = False
159 else:
160 try:
161 v = int(v)
162 except ValueError:
163 pass
164 dest[k.lower()] = v
165 return dest
167 def get(self, key, default=None, source=None):
168 """Return the string value for a config key."""
169 self.update()
170 return self._all.get(key, default)
172 def get_user(self, key, default=None):
173 self.update()
174 return self._user.get(key, default)
176 def get_repo(self, key, default=None):
177 self.update()
178 return self._repo.get(key, default)
180 def python_to_git(self, value):
181 if type(value) is bool:
182 if value:
183 return 'true'
184 else:
185 return 'false'
186 if type(value) is int:
187 return unicode(value)
188 return value
190 def set_user(self, key, value):
191 msg = self.message_user_config_changed
192 self.git.config('--global', key, self.python_to_git(value))
193 self.update()
194 self.notify_observers(msg, key, value)
196 def set_repo(self, key, value):
197 msg = self.message_repo_config_changed
198 self.git.config(key, self.python_to_git(value))
199 self.update()
200 self.notify_observers(msg, key, value)
202 def find(self, pat):
203 self.update()
204 result = {}
205 for key, val in self._all.items():
206 if fnmatch.fnmatch(key, pat):
207 result[key] = val
208 return result
210 def get_encoding(self, default='utf-8'):
211 return self.get('gui.encoding', default=default)
213 guitool_opts = ('cmd', 'needsfile', 'noconsole', 'norescan', 'confirm',
214 'argprompt', 'revprompt', 'revunmerged', 'title', 'prompt')
216 def get_guitool_opts(self, name):
217 """Return the guitool.<name> namespace as a dict"""
218 keyprefix = 'guitool.' + name + '.'
219 opts = {}
220 for cfg in self.guitool_opts:
221 value = self.get(keyprefix + cfg)
222 if value is None:
223 continue
224 opts[cfg] = value
225 return opts
227 def get_guitool_names(self):
228 guitools = self.find('guitool.*.cmd')
229 prefix = len('guitool.')
230 suffix = len('.cmd')
231 return sorted([name[prefix:-suffix]
232 for (name, cmd) in guitools.items()])