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