blob: 78069c5d042e310c736fe61dd4efeaff979e95e3 [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
60 self._remotes = {}
61 self._branches = {}
Shawn O. Pearcec12c3602009-04-17 21:03:32 -070062 self._pickle = os.path.join(
63 os.path.dirname(self.file),
64 '.repopickle_' + os.path.basename(self.file))
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070065
66 def Has(self, name, include_defaults = True):
67 """Return true if this configuration file has the key.
68 """
Shawn O. Pearcef8e32732009-04-17 11:00:31 -070069 if _key(name) in self._cache:
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070070 return True
71 if include_defaults and self.defaults:
72 return self.defaults.Has(name, include_defaults = True)
73 return False
74
75 def GetBoolean(self, name):
76 """Returns a boolean from the configuration file.
77 None : The value was not defined, or is not a boolean.
78 True : The value was set to true or yes.
79 False: The value was set to false or no.
80 """
81 v = self.GetString(name)
82 if v is None:
83 return None
84 v = v.lower()
85 if v in ('true', 'yes'):
86 return True
87 if v in ('false', 'no'):
88 return False
89 return None
90
91 def GetString(self, name, all=False):
92 """Get the first value for a key, or None if it is not defined.
93
94 This configuration file is used first, if the key is not
95 defined or all = True then the defaults are also searched.
96 """
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070097 try:
Shawn O. Pearcef8e32732009-04-17 11:00:31 -070098 v = self._cache[_key(name)]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070099 except KeyError:
100 if self.defaults:
101 return self.defaults.GetString(name, all = all)
102 v = []
103
104 if not all:
105 if v:
106 return v[0]
107 return None
108
109 r = []
110 r.extend(v)
111 if self.defaults:
112 r.extend(self.defaults.GetString(name, all = True))
113 return r
114
115 def SetString(self, name, value):
116 """Set the value(s) for a key.
117 Only this configuration file is modified.
118
119 The supplied value should be either a string,
120 or a list of strings (to store multiple values).
121 """
Shawn O. Pearcef8e32732009-04-17 11:00:31 -0700122 key = _key(name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700123
124 try:
Shawn O. Pearcef8e32732009-04-17 11:00:31 -0700125 old = self._cache[key]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700126 except KeyError:
127 old = []
128
129 if value is None:
130 if old:
Shawn O. Pearcef8e32732009-04-17 11:00:31 -0700131 del self._cache[key]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700132 self._do('--unset-all', name)
133
134 elif isinstance(value, list):
135 if len(value) == 0:
136 self.SetString(name, None)
137
138 elif len(value) == 1:
139 self.SetString(name, value[0])
140
141 elif old != value:
Shawn O. Pearcef8e32732009-04-17 11:00:31 -0700142 self._cache[key] = list(value)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700143 self._do('--replace-all', name, value[0])
144 for i in xrange(1, len(value)):
145 self._do('--add', name, value[i])
146
147 elif len(old) != 1 or old[0] != value:
Shawn O. Pearcef8e32732009-04-17 11:00:31 -0700148 self._cache[key] = [value]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700149 self._do('--replace-all', name, value)
150
151 def GetRemote(self, name):
152 """Get the remote.$name.* configuration values as an object.
153 """
154 try:
155 r = self._remotes[name]
156 except KeyError:
157 r = Remote(self, name)
158 self._remotes[r.name] = r
159 return r
160
161 def GetBranch(self, name):
162 """Get the branch.$name.* configuration values as an object.
163 """
164 try:
165 b = self._branches[name]
166 except KeyError:
167 b = Branch(self, name)
168 self._branches[b.name] = b
169 return b
170
171 @property
172 def _cache(self):
173 if self._cache_dict is None:
174 self._cache_dict = self._Read()
175 return self._cache_dict
176
177 def _Read(self):
Shawn O. Pearcec12c3602009-04-17 21:03:32 -0700178 d = self._ReadPickle()
179 if d is None:
180 d = self._ReadGit()
181 self._SavePickle(d)
182 return d
183
184 def _ReadPickle(self):
185 try:
186 if os.path.getmtime(self._pickle) \
187 <= os.path.getmtime(self.file):
188 os.remove(self._pickle)
189 return None
190 except OSError:
191 return None
192 try:
Shawn O. Pearcead3193a2009-04-18 09:54:51 -0700193 Trace(': unpickle %s', self.file)
Shawn O. Pearcec12c3602009-04-17 21:03:32 -0700194 return cPickle.load(open(self._pickle, 'r'))
195 except IOError:
196 os.remove(self._pickle)
197 return None
198 except cPickle.PickleError:
199 os.remove(self._pickle)
200 return None
201
202 def _SavePickle(self, cache):
203 try:
204 cPickle.dump(cache,
205 open(self._pickle, 'w'),
206 cPickle.HIGHEST_PROTOCOL)
207 except IOError:
208 os.remove(self._pickle)
209 except cPickle.PickleError:
210 os.remove(self._pickle)
211
212 def _ReadGit(self):
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700213 d = self._do('--null', '--list')
214 c = {}
215 while d:
216 lf = d.index('\n')
217 nul = d.index('\0', lf + 1)
218
Shawn O. Pearcef8e32732009-04-17 11:00:31 -0700219 key = _key(d[0:lf])
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700220 val = d[lf + 1:nul]
221
222 if key in c:
223 c[key].append(val)
224 else:
225 c[key] = [val]
226
227 d = d[nul + 1:]
228 return c
229
230 def _do(self, *args):
231 command = ['config', '--file', self.file]
232 command.extend(args)
233
234 p = GitCommand(None,
235 command,
236 capture_stdout = True,
237 capture_stderr = True)
238 if p.Wait() == 0:
239 return p.stdout
240 else:
241 GitError('git config %s: %s' % (str(args), p.stderr))
242
243
244class RefSpec(object):
245 """A Git refspec line, split into its components:
246
247 forced: True if the line starts with '+'
248 src: Left side of the line
249 dst: Right side of the line
250 """
251
252 @classmethod
253 def FromString(cls, rs):
254 lhs, rhs = rs.split(':', 2)
255 if lhs.startswith('+'):
256 lhs = lhs[1:]
257 forced = True
258 else:
259 forced = False
260 return cls(forced, lhs, rhs)
261
262 def __init__(self, forced, lhs, rhs):
263 self.forced = forced
264 self.src = lhs
265 self.dst = rhs
266
267 def SourceMatches(self, rev):
268 if self.src:
269 if rev == self.src:
270 return True
271 if self.src.endswith('/*') and rev.startswith(self.src[:-1]):
272 return True
273 return False
274
275 def DestMatches(self, ref):
276 if self.dst:
277 if ref == self.dst:
278 return True
279 if self.dst.endswith('/*') and ref.startswith(self.dst[:-1]):
280 return True
281 return False
282
283 def MapSource(self, rev):
284 if self.src.endswith('/*'):
285 return self.dst[:-1] + rev[len(self.src) - 1:]
286 return self.dst
287
288 def __str__(self):
289 s = ''
290 if self.forced:
291 s += '+'
292 if self.src:
293 s += self.src
294 if self.dst:
295 s += ':'
296 s += self.dst
297 return s
298
299
300class Remote(object):
301 """Configuration options related to a remote.
302 """
303 def __init__(self, config, name):
304 self._config = config
305 self.name = name
306 self.url = self._Get('url')
307 self.review = self._Get('review')
Shawn O. Pearce339ba9f2008-11-06 09:52:51 -0800308 self.projectname = self._Get('projectname')
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700309 self.fetch = map(lambda x: RefSpec.FromString(x),
310 self._Get('fetch', all=True))
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800311 self._review_protocol = None
312
313 @property
314 def ReviewProtocol(self):
315 if self._review_protocol is None:
316 if self.review is None:
317 return None
318
319 u = self.review
320 if not u.startswith('http:') and not u.startswith('https:'):
321 u = 'http://%s' % u
Shawn O. Pearce13cc3842009-03-25 13:54:54 -0700322 if u.endswith('/Gerrit'):
323 u = u[:len(u) - len('/Gerrit')]
324 if not u.endswith('/ssh_info'):
325 if not u.endswith('/'):
326 u += '/'
327 u += 'ssh_info'
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800328
Shawn O. Pearce146fe902009-03-25 14:06:43 -0700329 if u in REVIEW_CACHE:
330 info = REVIEW_CACHE[u]
331 self._review_protocol = info[0]
332 self._review_host = info[1]
333 self._review_port = info[2]
334 else:
335 try:
336 info = urlopen(u).read()
337 if info == 'NOT_AVAILABLE':
338 raise UploadError('Upload over ssh unavailable')
339 if '<' in info:
340 # Assume the server gave us some sort of HTML
341 # response back, like maybe a login page.
342 #
343 raise UploadError('Cannot read %s:\n%s' % (u, info))
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800344
Shawn O. Pearce146fe902009-03-25 14:06:43 -0700345 self._review_protocol = 'ssh'
346 self._review_host = info.split(" ")[0]
347 self._review_port = info.split(" ")[1]
348 except HTTPError, e:
349 if e.code == 404:
350 self._review_protocol = 'http-post'
351 self._review_host = None
352 self._review_port = None
353 else:
354 raise UploadError('Cannot guess Gerrit version')
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800355
Shawn O. Pearce146fe902009-03-25 14:06:43 -0700356 REVIEW_CACHE[u] = (
357 self._review_protocol,
358 self._review_host,
359 self._review_port)
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800360 return self._review_protocol
361
362 def SshReviewUrl(self, userEmail):
363 if self.ReviewProtocol != 'ssh':
364 return None
365 return 'ssh://%s@%s:%s/%s' % (
366 userEmail.split("@")[0],
367 self._review_host,
368 self._review_port,
369 self.projectname)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700370
371 def ToLocal(self, rev):
372 """Convert a remote revision string to something we have locally.
373 """
374 if IsId(rev):
375 return rev
376 if rev.startswith(R_TAGS):
377 return rev
378
379 if not rev.startswith('refs/'):
380 rev = R_HEADS + rev
381
382 for spec in self.fetch:
383 if spec.SourceMatches(rev):
384 return spec.MapSource(rev)
385 raise GitError('remote %s does not have %s' % (self.name, rev))
386
387 def WritesTo(self, ref):
388 """True if the remote stores to the tracking ref.
389 """
390 for spec in self.fetch:
391 if spec.DestMatches(ref):
392 return True
393 return False
394
Shawn O. Pearcee284ad12008-11-04 07:37:10 -0800395 def ResetFetch(self, mirror=False):
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700396 """Set the fetch refspec to its default value.
397 """
Shawn O. Pearcee284ad12008-11-04 07:37:10 -0800398 if mirror:
399 dst = 'refs/heads/*'
400 else:
401 dst = 'refs/remotes/%s/*' % self.name
402 self.fetch = [RefSpec(True, 'refs/heads/*', dst)]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700403
404 def Save(self):
405 """Save this remote to the configuration.
406 """
407 self._Set('url', self.url)
408 self._Set('review', self.review)
Shawn O. Pearce339ba9f2008-11-06 09:52:51 -0800409 self._Set('projectname', self.projectname)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700410 self._Set('fetch', map(lambda x: str(x), self.fetch))
411
412 def _Set(self, key, value):
413 key = 'remote.%s.%s' % (self.name, key)
414 return self._config.SetString(key, value)
415
416 def _Get(self, key, all=False):
417 key = 'remote.%s.%s' % (self.name, key)
418 return self._config.GetString(key, all = all)
419
420
421class Branch(object):
422 """Configuration options related to a single branch.
423 """
424 def __init__(self, config, name):
425 self._config = config
426 self.name = name
427 self.merge = self._Get('merge')
428
429 r = self._Get('remote')
430 if r:
431 self.remote = self._config.GetRemote(r)
432 else:
433 self.remote = None
434
435 @property
436 def LocalMerge(self):
437 """Convert the merge spec to a local name.
438 """
439 if self.remote and self.merge:
440 return self.remote.ToLocal(self.merge)
441 return None
442
443 def Save(self):
444 """Save this branch back into the configuration.
445 """
446 self._Set('merge', self.merge)
447 if self.remote:
448 self._Set('remote', self.remote.name)
449 else:
450 self._Set('remote', None)
451
452 def _Set(self, key, value):
453 key = 'branch.%s.%s' % (self.name, key)
454 return self._config.SetString(key, value)
455
456 def _Get(self, key, all=False):
457 key = 'branch.%s.%s' % (self.name, key)
458 return self._config.GetString(key, all = all)