blob: f6c5bd1e32957b83d1c7ba7014e70c78bf8037d1 [file] [log] [blame]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -07001#
2# Copyright (C) 2008 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16import os
17import re
18import sys
19from error import GitError
20from git_command import GitCommand
21
22R_HEADS = 'refs/heads/'
23R_TAGS = 'refs/tags/'
24ID_RE = re.compile('^[0-9a-f]{40}$')
25
26def IsId(rev):
27 return ID_RE.match(rev)
28
29
30class GitConfig(object):
31 @classmethod
32 def ForUser(cls):
33 return cls(file = os.path.expanduser('~/.gitconfig'))
34
35 @classmethod
36 def ForRepository(cls, gitdir, defaults=None):
37 return cls(file = os.path.join(gitdir, 'config'),
38 defaults = defaults)
39
40 def __init__(self, file, defaults=None):
41 self.file = file
42 self.defaults = defaults
43 self._cache_dict = None
44 self._remotes = {}
45 self._branches = {}
46
47 def Has(self, name, include_defaults = True):
48 """Return true if this configuration file has the key.
49 """
50 name = name.lower()
51 if name in self._cache:
52 return True
53 if include_defaults and self.defaults:
54 return self.defaults.Has(name, include_defaults = True)
55 return False
56
57 def GetBoolean(self, name):
58 """Returns a boolean from the configuration file.
59 None : The value was not defined, or is not a boolean.
60 True : The value was set to true or yes.
61 False: The value was set to false or no.
62 """
63 v = self.GetString(name)
64 if v is None:
65 return None
66 v = v.lower()
67 if v in ('true', 'yes'):
68 return True
69 if v in ('false', 'no'):
70 return False
71 return None
72
73 def GetString(self, name, all=False):
74 """Get the first value for a key, or None if it is not defined.
75
76 This configuration file is used first, if the key is not
77 defined or all = True then the defaults are also searched.
78 """
79 name = name.lower()
80
81 try:
82 v = self._cache[name]
83 except KeyError:
84 if self.defaults:
85 return self.defaults.GetString(name, all = all)
86 v = []
87
88 if not all:
89 if v:
90 return v[0]
91 return None
92
93 r = []
94 r.extend(v)
95 if self.defaults:
96 r.extend(self.defaults.GetString(name, all = True))
97 return r
98
99 def SetString(self, name, value):
100 """Set the value(s) for a key.
101 Only this configuration file is modified.
102
103 The supplied value should be either a string,
104 or a list of strings (to store multiple values).
105 """
106 name = name.lower()
107
108 try:
109 old = self._cache[name]
110 except KeyError:
111 old = []
112
113 if value is None:
114 if old:
115 del self._cache[name]
116 self._do('--unset-all', name)
117
118 elif isinstance(value, list):
119 if len(value) == 0:
120 self.SetString(name, None)
121
122 elif len(value) == 1:
123 self.SetString(name, value[0])
124
125 elif old != value:
126 self._cache[name] = list(value)
127 self._do('--replace-all', name, value[0])
128 for i in xrange(1, len(value)):
129 self._do('--add', name, value[i])
130
131 elif len(old) != 1 or old[0] != value:
132 self._cache[name] = [value]
133 self._do('--replace-all', name, value)
134
135 def GetRemote(self, name):
136 """Get the remote.$name.* configuration values as an object.
137 """
138 try:
139 r = self._remotes[name]
140 except KeyError:
141 r = Remote(self, name)
142 self._remotes[r.name] = r
143 return r
144
145 def GetBranch(self, name):
146 """Get the branch.$name.* configuration values as an object.
147 """
148 try:
149 b = self._branches[name]
150 except KeyError:
151 b = Branch(self, name)
152 self._branches[b.name] = b
153 return b
154
155 @property
156 def _cache(self):
157 if self._cache_dict is None:
158 self._cache_dict = self._Read()
159 return self._cache_dict
160
161 def _Read(self):
162 d = self._do('--null', '--list')
163 c = {}
164 while d:
165 lf = d.index('\n')
166 nul = d.index('\0', lf + 1)
167
168 key = d[0:lf]
169 val = d[lf + 1:nul]
170
171 if key in c:
172 c[key].append(val)
173 else:
174 c[key] = [val]
175
176 d = d[nul + 1:]
177 return c
178
179 def _do(self, *args):
180 command = ['config', '--file', self.file]
181 command.extend(args)
182
183 p = GitCommand(None,
184 command,
185 capture_stdout = True,
186 capture_stderr = True)
187 if p.Wait() == 0:
188 return p.stdout
189 else:
190 GitError('git config %s: %s' % (str(args), p.stderr))
191
192
193class RefSpec(object):
194 """A Git refspec line, split into its components:
195
196 forced: True if the line starts with '+'
197 src: Left side of the line
198 dst: Right side of the line
199 """
200
201 @classmethod
202 def FromString(cls, rs):
203 lhs, rhs = rs.split(':', 2)
204 if lhs.startswith('+'):
205 lhs = lhs[1:]
206 forced = True
207 else:
208 forced = False
209 return cls(forced, lhs, rhs)
210
211 def __init__(self, forced, lhs, rhs):
212 self.forced = forced
213 self.src = lhs
214 self.dst = rhs
215
216 def SourceMatches(self, rev):
217 if self.src:
218 if rev == self.src:
219 return True
220 if self.src.endswith('/*') and rev.startswith(self.src[:-1]):
221 return True
222 return False
223
224 def DestMatches(self, ref):
225 if self.dst:
226 if ref == self.dst:
227 return True
228 if self.dst.endswith('/*') and ref.startswith(self.dst[:-1]):
229 return True
230 return False
231
232 def MapSource(self, rev):
233 if self.src.endswith('/*'):
234 return self.dst[:-1] + rev[len(self.src) - 1:]
235 return self.dst
236
237 def __str__(self):
238 s = ''
239 if self.forced:
240 s += '+'
241 if self.src:
242 s += self.src
243 if self.dst:
244 s += ':'
245 s += self.dst
246 return s
247
248
249class Remote(object):
250 """Configuration options related to a remote.
251 """
252 def __init__(self, config, name):
253 self._config = config
254 self.name = name
255 self.url = self._Get('url')
256 self.review = self._Get('review')
257 self.fetch = map(lambda x: RefSpec.FromString(x),
258 self._Get('fetch', all=True))
259
260 def ToLocal(self, rev):
261 """Convert a remote revision string to something we have locally.
262 """
263 if IsId(rev):
264 return rev
265 if rev.startswith(R_TAGS):
266 return rev
267
268 if not rev.startswith('refs/'):
269 rev = R_HEADS + rev
270
271 for spec in self.fetch:
272 if spec.SourceMatches(rev):
273 return spec.MapSource(rev)
274 raise GitError('remote %s does not have %s' % (self.name, rev))
275
276 def WritesTo(self, ref):
277 """True if the remote stores to the tracking ref.
278 """
279 for spec in self.fetch:
280 if spec.DestMatches(ref):
281 return True
282 return False
283
284 def ResetFetch(self):
285 """Set the fetch refspec to its default value.
286 """
287 self.fetch = [RefSpec(True,
288 'refs/heads/*',
289 'refs/remotes/%s/*' % self.name)]
290
291 def Save(self):
292 """Save this remote to the configuration.
293 """
294 self._Set('url', self.url)
295 self._Set('review', self.review)
296 self._Set('fetch', map(lambda x: str(x), self.fetch))
297
298 def _Set(self, key, value):
299 key = 'remote.%s.%s' % (self.name, key)
300 return self._config.SetString(key, value)
301
302 def _Get(self, key, all=False):
303 key = 'remote.%s.%s' % (self.name, key)
304 return self._config.GetString(key, all = all)
305
306
307class Branch(object):
308 """Configuration options related to a single branch.
309 """
310 def __init__(self, config, name):
311 self._config = config
312 self.name = name
313 self.merge = self._Get('merge')
314
315 r = self._Get('remote')
316 if r:
317 self.remote = self._config.GetRemote(r)
318 else:
319 self.remote = None
320
321 @property
322 def LocalMerge(self):
323 """Convert the merge spec to a local name.
324 """
325 if self.remote and self.merge:
326 return self.remote.ToLocal(self.merge)
327 return None
328
329 def Save(self):
330 """Save this branch back into the configuration.
331 """
332 self._Set('merge', self.merge)
333 if self.remote:
334 self._Set('remote', self.remote.name)
335 else:
336 self._Set('remote', None)
337
338 def _Set(self, key, value):
339 key = 'branch.%s.%s' % (self.name, key)
340 return self._config.SetString(key, value)
341
342 def _Get(self, key, all=False):
343 key = 'branch.%s.%s' % (self.name, key)
344 return self._config.GetString(key, all = all)