blob: c87d5bede2299ce0234c74b49ba354620afb07ab [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
Shawn O. Pearcec12c3602009-04-17 21:03:32 -070016import cPickle
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070017import os
18import re
19import sys
Shawn O. Pearceb54a3922009-01-05 16:18:58 -080020from urllib2 import urlopen, HTTPError
21from error import GitError, UploadError
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070022from git_command import GitCommand
23
24R_HEADS = 'refs/heads/'
25R_TAGS = 'refs/tags/'
26ID_RE = re.compile('^[0-9a-f]{40}$')
27
Shawn O. Pearce146fe902009-03-25 14:06:43 -070028REVIEW_CACHE = dict()
29
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070030def IsId(rev):
31 return ID_RE.match(rev)
32
Shawn O. Pearcef8e32732009-04-17 11:00:31 -070033def _key(name):
34 parts = name.split('.')
35 if len(parts) < 2:
36 return name.lower()
37 parts[ 0] = parts[ 0].lower()
38 parts[-1] = parts[-1].lower()
39 return '.'.join(parts)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070040
41class GitConfig(object):
Shawn O. Pearce90be5c02008-10-29 15:21:24 -070042 _ForUser = None
43
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070044 @classmethod
45 def ForUser(cls):
Shawn O. Pearce90be5c02008-10-29 15:21:24 -070046 if cls._ForUser is None:
47 cls._ForUser = cls(file = os.path.expanduser('~/.gitconfig'))
48 return cls._ForUser
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070049
50 @classmethod
51 def ForRepository(cls, gitdir, defaults=None):
52 return cls(file = os.path.join(gitdir, 'config'),
53 defaults = defaults)
54
55 def __init__(self, file, defaults=None):
56 self.file = file
57 self.defaults = defaults
58 self._cache_dict = None
59 self._remotes = {}
60 self._branches = {}
Shawn O. Pearcec12c3602009-04-17 21:03:32 -070061 self._pickle = os.path.join(
62 os.path.dirname(self.file),
63 '.repopickle_' + os.path.basename(self.file))
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070064
65 def Has(self, name, include_defaults = True):
66 """Return true if this configuration file has the key.
67 """
Shawn O. Pearcef8e32732009-04-17 11:00:31 -070068 if _key(name) in self._cache:
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070069 return True
70 if include_defaults and self.defaults:
71 return self.defaults.Has(name, include_defaults = True)
72 return False
73
74 def GetBoolean(self, name):
75 """Returns a boolean from the configuration file.
76 None : The value was not defined, or is not a boolean.
77 True : The value was set to true or yes.
78 False: The value was set to false or no.
79 """
80 v = self.GetString(name)
81 if v is None:
82 return None
83 v = v.lower()
84 if v in ('true', 'yes'):
85 return True
86 if v in ('false', 'no'):
87 return False
88 return None
89
90 def GetString(self, name, all=False):
91 """Get the first value for a key, or None if it is not defined.
92
93 This configuration file is used first, if the key is not
94 defined or all = True then the defaults are also searched.
95 """
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070096 try:
Shawn O. Pearcef8e32732009-04-17 11:00:31 -070097 v = self._cache[_key(name)]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070098 except KeyError:
99 if self.defaults:
100 return self.defaults.GetString(name, all = all)
101 v = []
102
103 if not all:
104 if v:
105 return v[0]
106 return None
107
108 r = []
109 r.extend(v)
110 if self.defaults:
111 r.extend(self.defaults.GetString(name, all = True))
112 return r
113
114 def SetString(self, name, value):
115 """Set the value(s) for a key.
116 Only this configuration file is modified.
117
118 The supplied value should be either a string,
119 or a list of strings (to store multiple values).
120 """
Shawn O. Pearcef8e32732009-04-17 11:00:31 -0700121 key = _key(name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700122
123 try:
Shawn O. Pearcef8e32732009-04-17 11:00:31 -0700124 old = self._cache[key]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700125 except KeyError:
126 old = []
127
128 if value is None:
129 if old:
Shawn O. Pearcef8e32732009-04-17 11:00:31 -0700130 del self._cache[key]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700131 self._do('--unset-all', name)
132
133 elif isinstance(value, list):
134 if len(value) == 0:
135 self.SetString(name, None)
136
137 elif len(value) == 1:
138 self.SetString(name, value[0])
139
140 elif old != value:
Shawn O. Pearcef8e32732009-04-17 11:00:31 -0700141 self._cache[key] = list(value)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700142 self._do('--replace-all', name, value[0])
143 for i in xrange(1, len(value)):
144 self._do('--add', name, value[i])
145
146 elif len(old) != 1 or old[0] != value:
Shawn O. Pearcef8e32732009-04-17 11:00:31 -0700147 self._cache[key] = [value]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700148 self._do('--replace-all', name, value)
149
150 def GetRemote(self, name):
151 """Get the remote.$name.* configuration values as an object.
152 """
153 try:
154 r = self._remotes[name]
155 except KeyError:
156 r = Remote(self, name)
157 self._remotes[r.name] = r
158 return r
159
160 def GetBranch(self, name):
161 """Get the branch.$name.* configuration values as an object.
162 """
163 try:
164 b = self._branches[name]
165 except KeyError:
166 b = Branch(self, name)
167 self._branches[b.name] = b
168 return b
169
170 @property
171 def _cache(self):
172 if self._cache_dict is None:
173 self._cache_dict = self._Read()
174 return self._cache_dict
175
176 def _Read(self):
Shawn O. Pearcec12c3602009-04-17 21:03:32 -0700177 d = self._ReadPickle()
178 if d is None:
179 d = self._ReadGit()
180 self._SavePickle(d)
181 return d
182
183 def _ReadPickle(self):
184 try:
185 if os.path.getmtime(self._pickle) \
186 <= os.path.getmtime(self.file):
187 os.remove(self._pickle)
188 return None
189 except OSError:
190 return None
191 try:
192 return cPickle.load(open(self._pickle, 'r'))
193 except IOError:
194 os.remove(self._pickle)
195 return None
196 except cPickle.PickleError:
197 os.remove(self._pickle)
198 return None
199
200 def _SavePickle(self, cache):
201 try:
202 cPickle.dump(cache,
203 open(self._pickle, 'w'),
204 cPickle.HIGHEST_PROTOCOL)
205 except IOError:
206 os.remove(self._pickle)
207 except cPickle.PickleError:
208 os.remove(self._pickle)
209
210 def _ReadGit(self):
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700211 d = self._do('--null', '--list')
212 c = {}
213 while d:
214 lf = d.index('\n')
215 nul = d.index('\0', lf + 1)
216
Shawn O. Pearcef8e32732009-04-17 11:00:31 -0700217 key = _key(d[0:lf])
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700218 val = d[lf + 1:nul]
219
220 if key in c:
221 c[key].append(val)
222 else:
223 c[key] = [val]
224
225 d = d[nul + 1:]
226 return c
227
228 def _do(self, *args):
229 command = ['config', '--file', self.file]
230 command.extend(args)
231
232 p = GitCommand(None,
233 command,
234 capture_stdout = True,
235 capture_stderr = True)
236 if p.Wait() == 0:
237 return p.stdout
238 else:
239 GitError('git config %s: %s' % (str(args), p.stderr))
240
241
242class RefSpec(object):
243 """A Git refspec line, split into its components:
244
245 forced: True if the line starts with '+'
246 src: Left side of the line
247 dst: Right side of the line
248 """
249
250 @classmethod
251 def FromString(cls, rs):
252 lhs, rhs = rs.split(':', 2)
253 if lhs.startswith('+'):
254 lhs = lhs[1:]
255 forced = True
256 else:
257 forced = False
258 return cls(forced, lhs, rhs)
259
260 def __init__(self, forced, lhs, rhs):
261 self.forced = forced
262 self.src = lhs
263 self.dst = rhs
264
265 def SourceMatches(self, rev):
266 if self.src:
267 if rev == self.src:
268 return True
269 if self.src.endswith('/*') and rev.startswith(self.src[:-1]):
270 return True
271 return False
272
273 def DestMatches(self, ref):
274 if self.dst:
275 if ref == self.dst:
276 return True
277 if self.dst.endswith('/*') and ref.startswith(self.dst[:-1]):
278 return True
279 return False
280
281 def MapSource(self, rev):
282 if self.src.endswith('/*'):
283 return self.dst[:-1] + rev[len(self.src) - 1:]
284 return self.dst
285
286 def __str__(self):
287 s = ''
288 if self.forced:
289 s += '+'
290 if self.src:
291 s += self.src
292 if self.dst:
293 s += ':'
294 s += self.dst
295 return s
296
297
298class Remote(object):
299 """Configuration options related to a remote.
300 """
301 def __init__(self, config, name):
302 self._config = config
303 self.name = name
304 self.url = self._Get('url')
305 self.review = self._Get('review')
Shawn O. Pearce339ba9f2008-11-06 09:52:51 -0800306 self.projectname = self._Get('projectname')
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700307 self.fetch = map(lambda x: RefSpec.FromString(x),
308 self._Get('fetch', all=True))
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800309 self._review_protocol = None
310
311 @property
312 def ReviewProtocol(self):
313 if self._review_protocol is None:
314 if self.review is None:
315 return None
316
317 u = self.review
318 if not u.startswith('http:') and not u.startswith('https:'):
319 u = 'http://%s' % u
Shawn O. Pearce13cc3842009-03-25 13:54:54 -0700320 if u.endswith('/Gerrit'):
321 u = u[:len(u) - len('/Gerrit')]
322 if not u.endswith('/ssh_info'):
323 if not u.endswith('/'):
324 u += '/'
325 u += 'ssh_info'
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800326
Shawn O. Pearce146fe902009-03-25 14:06:43 -0700327 if u in REVIEW_CACHE:
328 info = REVIEW_CACHE[u]
329 self._review_protocol = info[0]
330 self._review_host = info[1]
331 self._review_port = info[2]
332 else:
333 try:
334 info = urlopen(u).read()
335 if info == 'NOT_AVAILABLE':
336 raise UploadError('Upload over ssh unavailable')
337 if '<' in info:
338 # Assume the server gave us some sort of HTML
339 # response back, like maybe a login page.
340 #
341 raise UploadError('Cannot read %s:\n%s' % (u, info))
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800342
Shawn O. Pearce146fe902009-03-25 14:06:43 -0700343 self._review_protocol = 'ssh'
344 self._review_host = info.split(" ")[0]
345 self._review_port = info.split(" ")[1]
346 except HTTPError, e:
347 if e.code == 404:
348 self._review_protocol = 'http-post'
349 self._review_host = None
350 self._review_port = None
351 else:
352 raise UploadError('Cannot guess Gerrit version')
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800353
Shawn O. Pearce146fe902009-03-25 14:06:43 -0700354 REVIEW_CACHE[u] = (
355 self._review_protocol,
356 self._review_host,
357 self._review_port)
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800358 return self._review_protocol
359
360 def SshReviewUrl(self, userEmail):
361 if self.ReviewProtocol != 'ssh':
362 return None
363 return 'ssh://%s@%s:%s/%s' % (
364 userEmail.split("@")[0],
365 self._review_host,
366 self._review_port,
367 self.projectname)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700368
369 def ToLocal(self, rev):
370 """Convert a remote revision string to something we have locally.
371 """
372 if IsId(rev):
373 return rev
374 if rev.startswith(R_TAGS):
375 return rev
376
377 if not rev.startswith('refs/'):
378 rev = R_HEADS + rev
379
380 for spec in self.fetch:
381 if spec.SourceMatches(rev):
382 return spec.MapSource(rev)
383 raise GitError('remote %s does not have %s' % (self.name, rev))
384
385 def WritesTo(self, ref):
386 """True if the remote stores to the tracking ref.
387 """
388 for spec in self.fetch:
389 if spec.DestMatches(ref):
390 return True
391 return False
392
Shawn O. Pearcee284ad12008-11-04 07:37:10 -0800393 def ResetFetch(self, mirror=False):
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700394 """Set the fetch refspec to its default value.
395 """
Shawn O. Pearcee284ad12008-11-04 07:37:10 -0800396 if mirror:
397 dst = 'refs/heads/*'
398 else:
399 dst = 'refs/remotes/%s/*' % self.name
400 self.fetch = [RefSpec(True, 'refs/heads/*', dst)]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700401
402 def Save(self):
403 """Save this remote to the configuration.
404 """
405 self._Set('url', self.url)
406 self._Set('review', self.review)
Shawn O. Pearce339ba9f2008-11-06 09:52:51 -0800407 self._Set('projectname', self.projectname)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700408 self._Set('fetch', map(lambda x: str(x), self.fetch))
409
410 def _Set(self, key, value):
411 key = 'remote.%s.%s' % (self.name, key)
412 return self._config.SetString(key, value)
413
414 def _Get(self, key, all=False):
415 key = 'remote.%s.%s' % (self.name, key)
416 return self._config.GetString(key, all = all)
417
418
419class Branch(object):
420 """Configuration options related to a single branch.
421 """
422 def __init__(self, config, name):
423 self._config = config
424 self.name = name
425 self.merge = self._Get('merge')
426
427 r = self._Get('remote')
428 if r:
429 self.remote = self._config.GetRemote(r)
430 else:
431 self.remote = None
432
433 @property
434 def LocalMerge(self):
435 """Convert the merge spec to a local name.
436 """
437 if self.remote and self.merge:
438 return self.remote.ToLocal(self.merge)
439 return None
440
441 def Save(self):
442 """Save this branch back into the configuration.
443 """
444 self._Set('merge', self.merge)
445 if self.remote:
446 self._Set('remote', self.remote.name)
447 else:
448 self._Set('remote', None)
449
450 def _Set(self, key, value):
451 key = 'branch.%s.%s' % (self.name, key)
452 return self._config.SetString(key, value)
453
454 def _Get(self, key, all=False):
455 key = 'branch.%s.%s' % (self.name, key)
456 return self._config.GetString(key, all = all)