blob: 9b63417e2c87a315ba078020f3e67ffe52cbec30 [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
Shawn O. Pearceb54a3922009-01-05 16:18:58 -080019from urllib2 import urlopen, HTTPError
20from error import GitError, UploadError
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070021from git_command import GitCommand
22
23R_HEADS = 'refs/heads/'
24R_TAGS = 'refs/tags/'
25ID_RE = re.compile('^[0-9a-f]{40}$')
26
Shawn O. Pearce146fe902009-03-25 14:06:43 -070027REVIEW_CACHE = dict()
28
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070029def IsId(rev):
30 return ID_RE.match(rev)
31
Shawn O. Pearcef8e32732009-04-17 11:00:31 -070032def _key(name):
33 parts = name.split('.')
34 if len(parts) < 2:
35 return name.lower()
36 parts[ 0] = parts[ 0].lower()
37 parts[-1] = parts[-1].lower()
38 return '.'.join(parts)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070039
40class GitConfig(object):
Shawn O. Pearce90be5c02008-10-29 15:21:24 -070041 _ForUser = None
42
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070043 @classmethod
44 def ForUser(cls):
Shawn O. Pearce90be5c02008-10-29 15:21:24 -070045 if cls._ForUser is None:
46 cls._ForUser = cls(file = os.path.expanduser('~/.gitconfig'))
47 return cls._ForUser
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070048
49 @classmethod
50 def ForRepository(cls, gitdir, defaults=None):
51 return cls(file = os.path.join(gitdir, 'config'),
52 defaults = defaults)
53
54 def __init__(self, file, defaults=None):
55 self.file = file
56 self.defaults = defaults
57 self._cache_dict = None
58 self._remotes = {}
59 self._branches = {}
60
61 def Has(self, name, include_defaults = True):
62 """Return true if this configuration file has the key.
63 """
Shawn O. Pearcef8e32732009-04-17 11:00:31 -070064 if _key(name) in self._cache:
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070065 return True
66 if include_defaults and self.defaults:
67 return self.defaults.Has(name, include_defaults = True)
68 return False
69
70 def GetBoolean(self, name):
71 """Returns a boolean from the configuration file.
72 None : The value was not defined, or is not a boolean.
73 True : The value was set to true or yes.
74 False: The value was set to false or no.
75 """
76 v = self.GetString(name)
77 if v is None:
78 return None
79 v = v.lower()
80 if v in ('true', 'yes'):
81 return True
82 if v in ('false', 'no'):
83 return False
84 return None
85
86 def GetString(self, name, all=False):
87 """Get the first value for a key, or None if it is not defined.
88
89 This configuration file is used first, if the key is not
90 defined or all = True then the defaults are also searched.
91 """
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070092 try:
Shawn O. Pearcef8e32732009-04-17 11:00:31 -070093 v = self._cache[_key(name)]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070094 except KeyError:
95 if self.defaults:
96 return self.defaults.GetString(name, all = all)
97 v = []
98
99 if not all:
100 if v:
101 return v[0]
102 return None
103
104 r = []
105 r.extend(v)
106 if self.defaults:
107 r.extend(self.defaults.GetString(name, all = True))
108 return r
109
110 def SetString(self, name, value):
111 """Set the value(s) for a key.
112 Only this configuration file is modified.
113
114 The supplied value should be either a string,
115 or a list of strings (to store multiple values).
116 """
Shawn O. Pearcef8e32732009-04-17 11:00:31 -0700117 key = _key(name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700118
119 try:
Shawn O. Pearcef8e32732009-04-17 11:00:31 -0700120 old = self._cache[key]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700121 except KeyError:
122 old = []
123
124 if value is None:
125 if old:
Shawn O. Pearcef8e32732009-04-17 11:00:31 -0700126 del self._cache[key]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700127 self._do('--unset-all', name)
128
129 elif isinstance(value, list):
130 if len(value) == 0:
131 self.SetString(name, None)
132
133 elif len(value) == 1:
134 self.SetString(name, value[0])
135
136 elif old != value:
Shawn O. Pearcef8e32732009-04-17 11:00:31 -0700137 self._cache[key] = list(value)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700138 self._do('--replace-all', name, value[0])
139 for i in xrange(1, len(value)):
140 self._do('--add', name, value[i])
141
142 elif len(old) != 1 or old[0] != value:
Shawn O. Pearcef8e32732009-04-17 11:00:31 -0700143 self._cache[key] = [value]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700144 self._do('--replace-all', name, value)
145
146 def GetRemote(self, name):
147 """Get the remote.$name.* configuration values as an object.
148 """
149 try:
150 r = self._remotes[name]
151 except KeyError:
152 r = Remote(self, name)
153 self._remotes[r.name] = r
154 return r
155
156 def GetBranch(self, name):
157 """Get the branch.$name.* configuration values as an object.
158 """
159 try:
160 b = self._branches[name]
161 except KeyError:
162 b = Branch(self, name)
163 self._branches[b.name] = b
164 return b
165
166 @property
167 def _cache(self):
168 if self._cache_dict is None:
169 self._cache_dict = self._Read()
170 return self._cache_dict
171
172 def _Read(self):
173 d = self._do('--null', '--list')
174 c = {}
175 while d:
176 lf = d.index('\n')
177 nul = d.index('\0', lf + 1)
178
Shawn O. Pearcef8e32732009-04-17 11:00:31 -0700179 key = _key(d[0:lf])
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700180 val = d[lf + 1:nul]
181
182 if key in c:
183 c[key].append(val)
184 else:
185 c[key] = [val]
186
187 d = d[nul + 1:]
188 return c
189
190 def _do(self, *args):
191 command = ['config', '--file', self.file]
192 command.extend(args)
193
194 p = GitCommand(None,
195 command,
196 capture_stdout = True,
197 capture_stderr = True)
198 if p.Wait() == 0:
199 return p.stdout
200 else:
201 GitError('git config %s: %s' % (str(args), p.stderr))
202
203
204class RefSpec(object):
205 """A Git refspec line, split into its components:
206
207 forced: True if the line starts with '+'
208 src: Left side of the line
209 dst: Right side of the line
210 """
211
212 @classmethod
213 def FromString(cls, rs):
214 lhs, rhs = rs.split(':', 2)
215 if lhs.startswith('+'):
216 lhs = lhs[1:]
217 forced = True
218 else:
219 forced = False
220 return cls(forced, lhs, rhs)
221
222 def __init__(self, forced, lhs, rhs):
223 self.forced = forced
224 self.src = lhs
225 self.dst = rhs
226
227 def SourceMatches(self, rev):
228 if self.src:
229 if rev == self.src:
230 return True
231 if self.src.endswith('/*') and rev.startswith(self.src[:-1]):
232 return True
233 return False
234
235 def DestMatches(self, ref):
236 if self.dst:
237 if ref == self.dst:
238 return True
239 if self.dst.endswith('/*') and ref.startswith(self.dst[:-1]):
240 return True
241 return False
242
243 def MapSource(self, rev):
244 if self.src.endswith('/*'):
245 return self.dst[:-1] + rev[len(self.src) - 1:]
246 return self.dst
247
248 def __str__(self):
249 s = ''
250 if self.forced:
251 s += '+'
252 if self.src:
253 s += self.src
254 if self.dst:
255 s += ':'
256 s += self.dst
257 return s
258
259
260class Remote(object):
261 """Configuration options related to a remote.
262 """
263 def __init__(self, config, name):
264 self._config = config
265 self.name = name
266 self.url = self._Get('url')
267 self.review = self._Get('review')
Shawn O. Pearce339ba9f2008-11-06 09:52:51 -0800268 self.projectname = self._Get('projectname')
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700269 self.fetch = map(lambda x: RefSpec.FromString(x),
270 self._Get('fetch', all=True))
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800271 self._review_protocol = None
272
273 @property
274 def ReviewProtocol(self):
275 if self._review_protocol is None:
276 if self.review is None:
277 return None
278
279 u = self.review
280 if not u.startswith('http:') and not u.startswith('https:'):
281 u = 'http://%s' % u
Shawn O. Pearce13cc3842009-03-25 13:54:54 -0700282 if u.endswith('/Gerrit'):
283 u = u[:len(u) - len('/Gerrit')]
284 if not u.endswith('/ssh_info'):
285 if not u.endswith('/'):
286 u += '/'
287 u += 'ssh_info'
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800288
Shawn O. Pearce146fe902009-03-25 14:06:43 -0700289 if u in REVIEW_CACHE:
290 info = REVIEW_CACHE[u]
291 self._review_protocol = info[0]
292 self._review_host = info[1]
293 self._review_port = info[2]
294 else:
295 try:
296 info = urlopen(u).read()
297 if info == 'NOT_AVAILABLE':
298 raise UploadError('Upload over ssh unavailable')
299 if '<' in info:
300 # Assume the server gave us some sort of HTML
301 # response back, like maybe a login page.
302 #
303 raise UploadError('Cannot read %s:\n%s' % (u, info))
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800304
Shawn O. Pearce146fe902009-03-25 14:06:43 -0700305 self._review_protocol = 'ssh'
306 self._review_host = info.split(" ")[0]
307 self._review_port = info.split(" ")[1]
308 except HTTPError, e:
309 if e.code == 404:
310 self._review_protocol = 'http-post'
311 self._review_host = None
312 self._review_port = None
313 else:
314 raise UploadError('Cannot guess Gerrit version')
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800315
Shawn O. Pearce146fe902009-03-25 14:06:43 -0700316 REVIEW_CACHE[u] = (
317 self._review_protocol,
318 self._review_host,
319 self._review_port)
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800320 return self._review_protocol
321
322 def SshReviewUrl(self, userEmail):
323 if self.ReviewProtocol != 'ssh':
324 return None
325 return 'ssh://%s@%s:%s/%s' % (
326 userEmail.split("@")[0],
327 self._review_host,
328 self._review_port,
329 self.projectname)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700330
331 def ToLocal(self, rev):
332 """Convert a remote revision string to something we have locally.
333 """
334 if IsId(rev):
335 return rev
336 if rev.startswith(R_TAGS):
337 return rev
338
339 if not rev.startswith('refs/'):
340 rev = R_HEADS + rev
341
342 for spec in self.fetch:
343 if spec.SourceMatches(rev):
344 return spec.MapSource(rev)
345 raise GitError('remote %s does not have %s' % (self.name, rev))
346
347 def WritesTo(self, ref):
348 """True if the remote stores to the tracking ref.
349 """
350 for spec in self.fetch:
351 if spec.DestMatches(ref):
352 return True
353 return False
354
Shawn O. Pearcee284ad12008-11-04 07:37:10 -0800355 def ResetFetch(self, mirror=False):
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700356 """Set the fetch refspec to its default value.
357 """
Shawn O. Pearcee284ad12008-11-04 07:37:10 -0800358 if mirror:
359 dst = 'refs/heads/*'
360 else:
361 dst = 'refs/remotes/%s/*' % self.name
362 self.fetch = [RefSpec(True, 'refs/heads/*', dst)]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700363
364 def Save(self):
365 """Save this remote to the configuration.
366 """
367 self._Set('url', self.url)
368 self._Set('review', self.review)
Shawn O. Pearce339ba9f2008-11-06 09:52:51 -0800369 self._Set('projectname', self.projectname)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700370 self._Set('fetch', map(lambda x: str(x), self.fetch))
371
372 def _Set(self, key, value):
373 key = 'remote.%s.%s' % (self.name, key)
374 return self._config.SetString(key, value)
375
376 def _Get(self, key, all=False):
377 key = 'remote.%s.%s' % (self.name, key)
378 return self._config.GetString(key, all = all)
379
380
381class Branch(object):
382 """Configuration options related to a single branch.
383 """
384 def __init__(self, config, name):
385 self._config = config
386 self.name = name
387 self.merge = self._Get('merge')
388
389 r = self._Get('remote')
390 if r:
391 self.remote = self._config.GetRemote(r)
392 else:
393 self.remote = None
394
395 @property
396 def LocalMerge(self):
397 """Convert the merge spec to a local name.
398 """
399 if self.remote and self.merge:
400 return self.remote.ToLocal(self.merge)
401 return None
402
403 def Save(self):
404 """Save this branch back into the configuration.
405 """
406 self._Set('merge', self.merge)
407 if self.remote:
408 self._Set('remote', self.remote.name)
409 else:
410 self._Set('remote', None)
411
412 def _Set(self, key, value):
413 key = 'branch.%s.%s' % (self.name, key)
414 return self._config.SetString(key, value)
415
416 def _Get(self, key, all=False):
417 key = 'branch.%s.%s' % (self.name, key)
418 return self._config.GetString(key, all = all)