blob: 9ddb2edc58a4210c4c30c5e0861707951e0c6729 [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):
Shawn O. Pearce90be5c02008-10-29 15:21:24 -070031 _ForUser = None
32
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070033 @classmethod
34 def ForUser(cls):
Shawn O. Pearce90be5c02008-10-29 15:21:24 -070035 if cls._ForUser is None:
36 cls._ForUser = cls(file = os.path.expanduser('~/.gitconfig'))
37 return cls._ForUser
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070038
39 @classmethod
40 def ForRepository(cls, gitdir, defaults=None):
41 return cls(file = os.path.join(gitdir, 'config'),
42 defaults = defaults)
43
44 def __init__(self, file, defaults=None):
45 self.file = file
46 self.defaults = defaults
47 self._cache_dict = None
48 self._remotes = {}
49 self._branches = {}
50
51 def Has(self, name, include_defaults = True):
52 """Return true if this configuration file has the key.
53 """
54 name = name.lower()
55 if name in self._cache:
56 return True
57 if include_defaults and self.defaults:
58 return self.defaults.Has(name, include_defaults = True)
59 return False
60
61 def GetBoolean(self, name):
62 """Returns a boolean from the configuration file.
63 None : The value was not defined, or is not a boolean.
64 True : The value was set to true or yes.
65 False: The value was set to false or no.
66 """
67 v = self.GetString(name)
68 if v is None:
69 return None
70 v = v.lower()
71 if v in ('true', 'yes'):
72 return True
73 if v in ('false', 'no'):
74 return False
75 return None
76
77 def GetString(self, name, all=False):
78 """Get the first value for a key, or None if it is not defined.
79
80 This configuration file is used first, if the key is not
81 defined or all = True then the defaults are also searched.
82 """
83 name = name.lower()
84
85 try:
86 v = self._cache[name]
87 except KeyError:
88 if self.defaults:
89 return self.defaults.GetString(name, all = all)
90 v = []
91
92 if not all:
93 if v:
94 return v[0]
95 return None
96
97 r = []
98 r.extend(v)
99 if self.defaults:
100 r.extend(self.defaults.GetString(name, all = True))
101 return r
102
103 def SetString(self, name, value):
104 """Set the value(s) for a key.
105 Only this configuration file is modified.
106
107 The supplied value should be either a string,
108 or a list of strings (to store multiple values).
109 """
110 name = name.lower()
111
112 try:
113 old = self._cache[name]
114 except KeyError:
115 old = []
116
117 if value is None:
118 if old:
119 del self._cache[name]
120 self._do('--unset-all', name)
121
122 elif isinstance(value, list):
123 if len(value) == 0:
124 self.SetString(name, None)
125
126 elif len(value) == 1:
127 self.SetString(name, value[0])
128
129 elif old != value:
130 self._cache[name] = list(value)
131 self._do('--replace-all', name, value[0])
132 for i in xrange(1, len(value)):
133 self._do('--add', name, value[i])
134
135 elif len(old) != 1 or old[0] != value:
136 self._cache[name] = [value]
137 self._do('--replace-all', name, value)
138
139 def GetRemote(self, name):
140 """Get the remote.$name.* configuration values as an object.
141 """
142 try:
143 r = self._remotes[name]
144 except KeyError:
145 r = Remote(self, name)
146 self._remotes[r.name] = r
147 return r
148
149 def GetBranch(self, name):
150 """Get the branch.$name.* configuration values as an object.
151 """
152 try:
153 b = self._branches[name]
154 except KeyError:
155 b = Branch(self, name)
156 self._branches[b.name] = b
157 return b
158
159 @property
160 def _cache(self):
161 if self._cache_dict is None:
162 self._cache_dict = self._Read()
163 return self._cache_dict
164
165 def _Read(self):
166 d = self._do('--null', '--list')
167 c = {}
168 while d:
169 lf = d.index('\n')
170 nul = d.index('\0', lf + 1)
171
172 key = d[0:lf]
173 val = d[lf + 1:nul]
174
175 if key in c:
176 c[key].append(val)
177 else:
178 c[key] = [val]
179
180 d = d[nul + 1:]
181 return c
182
183 def _do(self, *args):
184 command = ['config', '--file', self.file]
185 command.extend(args)
186
187 p = GitCommand(None,
188 command,
189 capture_stdout = True,
190 capture_stderr = True)
191 if p.Wait() == 0:
192 return p.stdout
193 else:
194 GitError('git config %s: %s' % (str(args), p.stderr))
195
196
197class RefSpec(object):
198 """A Git refspec line, split into its components:
199
200 forced: True if the line starts with '+'
201 src: Left side of the line
202 dst: Right side of the line
203 """
204
205 @classmethod
206 def FromString(cls, rs):
207 lhs, rhs = rs.split(':', 2)
208 if lhs.startswith('+'):
209 lhs = lhs[1:]
210 forced = True
211 else:
212 forced = False
213 return cls(forced, lhs, rhs)
214
215 def __init__(self, forced, lhs, rhs):
216 self.forced = forced
217 self.src = lhs
218 self.dst = rhs
219
220 def SourceMatches(self, rev):
221 if self.src:
222 if rev == self.src:
223 return True
224 if self.src.endswith('/*') and rev.startswith(self.src[:-1]):
225 return True
226 return False
227
228 def DestMatches(self, ref):
229 if self.dst:
230 if ref == self.dst:
231 return True
232 if self.dst.endswith('/*') and ref.startswith(self.dst[:-1]):
233 return True
234 return False
235
236 def MapSource(self, rev):
237 if self.src.endswith('/*'):
238 return self.dst[:-1] + rev[len(self.src) - 1:]
239 return self.dst
240
241 def __str__(self):
242 s = ''
243 if self.forced:
244 s += '+'
245 if self.src:
246 s += self.src
247 if self.dst:
248 s += ':'
249 s += self.dst
250 return s
251
252
253class Remote(object):
254 """Configuration options related to a remote.
255 """
256 def __init__(self, config, name):
257 self._config = config
258 self.name = name
259 self.url = self._Get('url')
260 self.review = self._Get('review')
Shawn O. Pearce339ba9f2008-11-06 09:52:51 -0800261 self.projectname = self._Get('projectname')
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700262 self.fetch = map(lambda x: RefSpec.FromString(x),
263 self._Get('fetch', all=True))
264
265 def ToLocal(self, rev):
266 """Convert a remote revision string to something we have locally.
267 """
268 if IsId(rev):
269 return rev
270 if rev.startswith(R_TAGS):
271 return rev
272
273 if not rev.startswith('refs/'):
274 rev = R_HEADS + rev
275
276 for spec in self.fetch:
277 if spec.SourceMatches(rev):
278 return spec.MapSource(rev)
279 raise GitError('remote %s does not have %s' % (self.name, rev))
280
281 def WritesTo(self, ref):
282 """True if the remote stores to the tracking ref.
283 """
284 for spec in self.fetch:
285 if spec.DestMatches(ref):
286 return True
287 return False
288
Shawn O. Pearcee284ad12008-11-04 07:37:10 -0800289 def ResetFetch(self, mirror=False):
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700290 """Set the fetch refspec to its default value.
291 """
Shawn O. Pearcee284ad12008-11-04 07:37:10 -0800292 if mirror:
293 dst = 'refs/heads/*'
294 else:
295 dst = 'refs/remotes/%s/*' % self.name
296 self.fetch = [RefSpec(True, 'refs/heads/*', dst)]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700297
298 def Save(self):
299 """Save this remote to the configuration.
300 """
301 self._Set('url', self.url)
302 self._Set('review', self.review)
Shawn O. Pearce339ba9f2008-11-06 09:52:51 -0800303 self._Set('projectname', self.projectname)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700304 self._Set('fetch', map(lambda x: str(x), self.fetch))
305
306 def _Set(self, key, value):
307 key = 'remote.%s.%s' % (self.name, key)
308 return self._config.SetString(key, value)
309
310 def _Get(self, key, all=False):
311 key = 'remote.%s.%s' % (self.name, key)
312 return self._config.GetString(key, all = all)
313
314
315class Branch(object):
316 """Configuration options related to a single branch.
317 """
318 def __init__(self, config, name):
319 self._config = config
320 self.name = name
321 self.merge = self._Get('merge')
322
323 r = self._Get('remote')
324 if r:
325 self.remote = self._config.GetRemote(r)
326 else:
327 self.remote = None
328
329 @property
330 def LocalMerge(self):
331 """Convert the merge spec to a local name.
332 """
333 if self.remote and self.merge:
334 return self.remote.ToLocal(self.merge)
335 return None
336
337 def Save(self):
338 """Save this branch back into the configuration.
339 """
340 self._Set('merge', self.merge)
341 if self.remote:
342 self._Set('remote', self.remote.name)
343 else:
344 self._Set('remote', None)
345
346 def _Set(self, key, value):
347 key = 'branch.%s.%s' % (self.name, key)
348 return self._config.SetString(key, value)
349
350 def _Get(self, key, all=False):
351 key = 'branch.%s.%s' % (self.name, key)
352 return self._config.GetString(key, all = all)