blob: 7e642a4cae078875db727d9bf5fa6a08e45cdd09 [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
Shawn O. Pearcead3193a2009-04-18 09:54:51 -070022from trace import Trace
23from git_command import GitCommand
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070024
25R_HEADS = 'refs/heads/'
26R_TAGS = 'refs/tags/'
27ID_RE = re.compile('^[0-9a-f]{40}$')
28
Shawn O. Pearce146fe902009-03-25 14:06:43 -070029REVIEW_CACHE = dict()
30
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070031def IsId(rev):
32 return ID_RE.match(rev)
33
Shawn O. Pearcef8e32732009-04-17 11:00:31 -070034def _key(name):
35 parts = name.split('.')
36 if len(parts) < 2:
37 return name.lower()
38 parts[ 0] = parts[ 0].lower()
39 parts[-1] = parts[-1].lower()
40 return '.'.join(parts)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070041
42class GitConfig(object):
Shawn O. Pearce90be5c02008-10-29 15:21:24 -070043 _ForUser = None
44
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070045 @classmethod
46 def ForUser(cls):
Shawn O. Pearce90be5c02008-10-29 15:21:24 -070047 if cls._ForUser is None:
48 cls._ForUser = cls(file = os.path.expanduser('~/.gitconfig'))
49 return cls._ForUser
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070050
51 @classmethod
52 def ForRepository(cls, gitdir, defaults=None):
53 return cls(file = os.path.join(gitdir, 'config'),
54 defaults = defaults)
55
56 def __init__(self, file, defaults=None):
57 self.file = file
58 self.defaults = defaults
59 self._cache_dict = None
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -070060 self._section_dict = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070061 self._remotes = {}
62 self._branches = {}
Shawn O. Pearcec12c3602009-04-17 21:03:32 -070063 self._pickle = os.path.join(
64 os.path.dirname(self.file),
65 '.repopickle_' + os.path.basename(self.file))
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070066
67 def Has(self, name, include_defaults = True):
68 """Return true if this configuration file has the key.
69 """
Shawn O. Pearcef8e32732009-04-17 11:00:31 -070070 if _key(name) in self._cache:
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070071 return True
72 if include_defaults and self.defaults:
73 return self.defaults.Has(name, include_defaults = True)
74 return False
75
76 def GetBoolean(self, name):
77 """Returns a boolean from the configuration file.
78 None : The value was not defined, or is not a boolean.
79 True : The value was set to true or yes.
80 False: The value was set to false or no.
81 """
82 v = self.GetString(name)
83 if v is None:
84 return None
85 v = v.lower()
86 if v in ('true', 'yes'):
87 return True
88 if v in ('false', 'no'):
89 return False
90 return None
91
92 def GetString(self, name, all=False):
93 """Get the first value for a key, or None if it is not defined.
94
95 This configuration file is used first, if the key is not
96 defined or all = True then the defaults are also searched.
97 """
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070098 try:
Shawn O. Pearcef8e32732009-04-17 11:00:31 -070099 v = self._cache[_key(name)]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700100 except KeyError:
101 if self.defaults:
102 return self.defaults.GetString(name, all = all)
103 v = []
104
105 if not all:
106 if v:
107 return v[0]
108 return None
109
110 r = []
111 r.extend(v)
112 if self.defaults:
113 r.extend(self.defaults.GetString(name, all = True))
114 return r
115
116 def SetString(self, name, value):
117 """Set the value(s) for a key.
118 Only this configuration file is modified.
119
120 The supplied value should be either a string,
121 or a list of strings (to store multiple values).
122 """
Shawn O. Pearcef8e32732009-04-17 11:00:31 -0700123 key = _key(name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700124
125 try:
Shawn O. Pearcef8e32732009-04-17 11:00:31 -0700126 old = self._cache[key]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700127 except KeyError:
128 old = []
129
130 if value is None:
131 if old:
Shawn O. Pearcef8e32732009-04-17 11:00:31 -0700132 del self._cache[key]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700133 self._do('--unset-all', name)
134
135 elif isinstance(value, list):
136 if len(value) == 0:
137 self.SetString(name, None)
138
139 elif len(value) == 1:
140 self.SetString(name, value[0])
141
142 elif old != value:
Shawn O. Pearcef8e32732009-04-17 11:00:31 -0700143 self._cache[key] = list(value)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700144 self._do('--replace-all', name, value[0])
145 for i in xrange(1, len(value)):
146 self._do('--add', name, value[i])
147
148 elif len(old) != 1 or old[0] != value:
Shawn O. Pearcef8e32732009-04-17 11:00:31 -0700149 self._cache[key] = [value]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700150 self._do('--replace-all', name, value)
151
152 def GetRemote(self, name):
153 """Get the remote.$name.* configuration values as an object.
154 """
155 try:
156 r = self._remotes[name]
157 except KeyError:
158 r = Remote(self, name)
159 self._remotes[r.name] = r
160 return r
161
162 def GetBranch(self, name):
163 """Get the branch.$name.* configuration values as an object.
164 """
165 try:
166 b = self._branches[name]
167 except KeyError:
168 b = Branch(self, name)
169 self._branches[b.name] = b
170 return b
171
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700172 def HasSection(self, section, subsection = ''):
173 """Does at least one key in section.subsection exist?
174 """
175 try:
176 return subsection in self._sections[section]
177 except KeyError:
178 return False
179
180 @property
181 def _sections(self):
182 d = self._section_dict
183 if d is None:
184 d = {}
185 for name in self._cache.keys():
186 p = name.split('.')
187 if 2 == len(p):
188 section = p[0]
189 subsect = ''
190 else:
191 section = p[0]
192 subsect = '.'.join(p[1:-1])
193 if section not in d:
194 d[section] = set()
195 d[section].add(subsect)
196 self._section_dict = d
197 return d
198
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700199 @property
200 def _cache(self):
201 if self._cache_dict is None:
202 self._cache_dict = self._Read()
203 return self._cache_dict
204
205 def _Read(self):
Shawn O. Pearcec12c3602009-04-17 21:03:32 -0700206 d = self._ReadPickle()
207 if d is None:
208 d = self._ReadGit()
209 self._SavePickle(d)
210 return d
211
212 def _ReadPickle(self):
213 try:
214 if os.path.getmtime(self._pickle) \
215 <= os.path.getmtime(self.file):
216 os.remove(self._pickle)
217 return None
218 except OSError:
219 return None
220 try:
Shawn O. Pearcead3193a2009-04-18 09:54:51 -0700221 Trace(': unpickle %s', self.file)
Shawn O. Pearce76ca9f82009-04-18 14:48:03 -0700222 fd = open(self._pickle, 'rb')
223 try:
224 return cPickle.load(fd)
225 finally:
226 fd.close()
Shawn O. Pearcec12c3602009-04-17 21:03:32 -0700227 except IOError:
228 os.remove(self._pickle)
229 return None
230 except cPickle.PickleError:
231 os.remove(self._pickle)
232 return None
233
234 def _SavePickle(self, cache):
235 try:
Shawn O. Pearce76ca9f82009-04-18 14:48:03 -0700236 fd = open(self._pickle, 'wb')
237 try:
238 cPickle.dump(cache, fd, cPickle.HIGHEST_PROTOCOL)
239 finally:
240 fd.close()
Shawn O. Pearcec12c3602009-04-17 21:03:32 -0700241 except IOError:
242 os.remove(self._pickle)
243 except cPickle.PickleError:
244 os.remove(self._pickle)
245
246 def _ReadGit(self):
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700247 d = self._do('--null', '--list')
248 c = {}
249 while d:
250 lf = d.index('\n')
251 nul = d.index('\0', lf + 1)
252
Shawn O. Pearcef8e32732009-04-17 11:00:31 -0700253 key = _key(d[0:lf])
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700254 val = d[lf + 1:nul]
255
256 if key in c:
257 c[key].append(val)
258 else:
259 c[key] = [val]
260
261 d = d[nul + 1:]
262 return c
263
264 def _do(self, *args):
265 command = ['config', '--file', self.file]
266 command.extend(args)
267
268 p = GitCommand(None,
269 command,
270 capture_stdout = True,
271 capture_stderr = True)
272 if p.Wait() == 0:
273 return p.stdout
274 else:
275 GitError('git config %s: %s' % (str(args), p.stderr))
276
277
278class RefSpec(object):
279 """A Git refspec line, split into its components:
280
281 forced: True if the line starts with '+'
282 src: Left side of the line
283 dst: Right side of the line
284 """
285
286 @classmethod
287 def FromString(cls, rs):
288 lhs, rhs = rs.split(':', 2)
289 if lhs.startswith('+'):
290 lhs = lhs[1:]
291 forced = True
292 else:
293 forced = False
294 return cls(forced, lhs, rhs)
295
296 def __init__(self, forced, lhs, rhs):
297 self.forced = forced
298 self.src = lhs
299 self.dst = rhs
300
301 def SourceMatches(self, rev):
302 if self.src:
303 if rev == self.src:
304 return True
305 if self.src.endswith('/*') and rev.startswith(self.src[:-1]):
306 return True
307 return False
308
309 def DestMatches(self, ref):
310 if self.dst:
311 if ref == self.dst:
312 return True
313 if self.dst.endswith('/*') and ref.startswith(self.dst[:-1]):
314 return True
315 return False
316
317 def MapSource(self, rev):
318 if self.src.endswith('/*'):
319 return self.dst[:-1] + rev[len(self.src) - 1:]
320 return self.dst
321
322 def __str__(self):
323 s = ''
324 if self.forced:
325 s += '+'
326 if self.src:
327 s += self.src
328 if self.dst:
329 s += ':'
330 s += self.dst
331 return s
332
333
334class Remote(object):
335 """Configuration options related to a remote.
336 """
337 def __init__(self, config, name):
338 self._config = config
339 self.name = name
340 self.url = self._Get('url')
341 self.review = self._Get('review')
Shawn O. Pearce339ba9f2008-11-06 09:52:51 -0800342 self.projectname = self._Get('projectname')
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700343 self.fetch = map(lambda x: RefSpec.FromString(x),
344 self._Get('fetch', all=True))
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800345 self._review_protocol = None
346
347 @property
348 def ReviewProtocol(self):
349 if self._review_protocol is None:
350 if self.review is None:
351 return None
352
353 u = self.review
354 if not u.startswith('http:') and not u.startswith('https:'):
355 u = 'http://%s' % u
Shawn O. Pearce13cc3842009-03-25 13:54:54 -0700356 if u.endswith('/Gerrit'):
357 u = u[:len(u) - len('/Gerrit')]
358 if not u.endswith('/ssh_info'):
359 if not u.endswith('/'):
360 u += '/'
361 u += 'ssh_info'
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800362
Shawn O. Pearce146fe902009-03-25 14:06:43 -0700363 if u in REVIEW_CACHE:
364 info = REVIEW_CACHE[u]
365 self._review_protocol = info[0]
366 self._review_host = info[1]
367 self._review_port = info[2]
368 else:
369 try:
370 info = urlopen(u).read()
371 if info == 'NOT_AVAILABLE':
372 raise UploadError('Upload over ssh unavailable')
373 if '<' in info:
374 # Assume the server gave us some sort of HTML
375 # response back, like maybe a login page.
376 #
377 raise UploadError('Cannot read %s:\n%s' % (u, info))
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800378
Shawn O. Pearce146fe902009-03-25 14:06:43 -0700379 self._review_protocol = 'ssh'
380 self._review_host = info.split(" ")[0]
381 self._review_port = info.split(" ")[1]
382 except HTTPError, e:
383 if e.code == 404:
384 self._review_protocol = 'http-post'
385 self._review_host = None
386 self._review_port = None
387 else:
388 raise UploadError('Cannot guess Gerrit version')
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800389
Shawn O. Pearce146fe902009-03-25 14:06:43 -0700390 REVIEW_CACHE[u] = (
391 self._review_protocol,
392 self._review_host,
393 self._review_port)
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800394 return self._review_protocol
395
396 def SshReviewUrl(self, userEmail):
397 if self.ReviewProtocol != 'ssh':
398 return None
399 return 'ssh://%s@%s:%s/%s' % (
400 userEmail.split("@")[0],
401 self._review_host,
402 self._review_port,
403 self.projectname)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700404
405 def ToLocal(self, rev):
406 """Convert a remote revision string to something we have locally.
407 """
408 if IsId(rev):
409 return rev
410 if rev.startswith(R_TAGS):
411 return rev
412
413 if not rev.startswith('refs/'):
414 rev = R_HEADS + rev
415
416 for spec in self.fetch:
417 if spec.SourceMatches(rev):
418 return spec.MapSource(rev)
419 raise GitError('remote %s does not have %s' % (self.name, rev))
420
421 def WritesTo(self, ref):
422 """True if the remote stores to the tracking ref.
423 """
424 for spec in self.fetch:
425 if spec.DestMatches(ref):
426 return True
427 return False
428
Shawn O. Pearcee284ad12008-11-04 07:37:10 -0800429 def ResetFetch(self, mirror=False):
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700430 """Set the fetch refspec to its default value.
431 """
Shawn O. Pearcee284ad12008-11-04 07:37:10 -0800432 if mirror:
433 dst = 'refs/heads/*'
434 else:
435 dst = 'refs/remotes/%s/*' % self.name
436 self.fetch = [RefSpec(True, 'refs/heads/*', dst)]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700437
438 def Save(self):
439 """Save this remote to the configuration.
440 """
441 self._Set('url', self.url)
442 self._Set('review', self.review)
Shawn O. Pearce339ba9f2008-11-06 09:52:51 -0800443 self._Set('projectname', self.projectname)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700444 self._Set('fetch', map(lambda x: str(x), self.fetch))
445
446 def _Set(self, key, value):
447 key = 'remote.%s.%s' % (self.name, key)
448 return self._config.SetString(key, value)
449
450 def _Get(self, key, all=False):
451 key = 'remote.%s.%s' % (self.name, key)
452 return self._config.GetString(key, all = all)
453
454
455class Branch(object):
456 """Configuration options related to a single branch.
457 """
458 def __init__(self, config, name):
459 self._config = config
460 self.name = name
461 self.merge = self._Get('merge')
462
463 r = self._Get('remote')
464 if r:
465 self.remote = self._config.GetRemote(r)
466 else:
467 self.remote = None
468
469 @property
470 def LocalMerge(self):
471 """Convert the merge spec to a local name.
472 """
473 if self.remote and self.merge:
474 return self.remote.ToLocal(self.merge)
475 return None
476
477 def Save(self):
478 """Save this branch back into the configuration.
479 """
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700480 if self._config.HasSection('branch', self.name):
481 if self.remote:
482 self._Set('remote', self.remote.name)
483 else:
484 self._Set('remote', None)
485 self._Set('merge', self.merge)
486
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700487 else:
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700488 fd = open(self._config.file, 'ab')
489 try:
490 fd.write('[branch "%s"]\n' % self.name)
491 if self.remote:
492 fd.write('\tremote = %s\n' % self.remote.name)
493 if self.merge:
494 fd.write('\tmerge = %s\n' % self.merge)
495 finally:
496 fd.close()
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700497
498 def _Set(self, key, value):
499 key = 'branch.%s.%s' % (self.name, key)
500 return self._config.SetString(key, value)
501
502 def _Get(self, key, all=False):
503 key = 'branch.%s.%s' % (self.name, key)
504 return self._config.GetString(key, all = all)