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