blob: bc7ed4592a5736331198d4d058daa046dd29e81d [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
Shawn O. Pearcefb231612009-04-10 18:53:46 -070019import subprocess
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070020import sys
Shawn O. Pearcefb231612009-04-10 18:53:46 -070021import time
22from signal import SIGTERM
Shawn O. Pearceb54a3922009-01-05 16:18:58 -080023from urllib2 import urlopen, HTTPError
24from error import GitError, UploadError
Shawn O. Pearcead3193a2009-04-18 09:54:51 -070025from trace import Trace
Shawn O. Pearcefb231612009-04-10 18:53:46 -070026from git_command import GitCommand, _ssh_sock
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070027
28R_HEADS = 'refs/heads/'
29R_TAGS = 'refs/tags/'
30ID_RE = re.compile('^[0-9a-f]{40}$')
31
Shawn O. Pearce146fe902009-03-25 14:06:43 -070032REVIEW_CACHE = dict()
33
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070034def IsId(rev):
35 return ID_RE.match(rev)
36
Shawn O. Pearcef8e32732009-04-17 11:00:31 -070037def _key(name):
38 parts = name.split('.')
39 if len(parts) < 2:
40 return name.lower()
41 parts[ 0] = parts[ 0].lower()
42 parts[-1] = parts[-1].lower()
43 return '.'.join(parts)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070044
45class GitConfig(object):
Shawn O. Pearce90be5c02008-10-29 15:21:24 -070046 _ForUser = None
47
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070048 @classmethod
49 def ForUser(cls):
Shawn O. Pearce90be5c02008-10-29 15:21:24 -070050 if cls._ForUser is None:
51 cls._ForUser = cls(file = os.path.expanduser('~/.gitconfig'))
52 return cls._ForUser
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070053
54 @classmethod
55 def ForRepository(cls, gitdir, defaults=None):
56 return cls(file = os.path.join(gitdir, 'config'),
57 defaults = defaults)
58
59 def __init__(self, file, defaults=None):
60 self.file = file
61 self.defaults = defaults
62 self._cache_dict = None
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -070063 self._section_dict = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070064 self._remotes = {}
65 self._branches = {}
Shawn O. Pearcec12c3602009-04-17 21:03:32 -070066 self._pickle = os.path.join(
67 os.path.dirname(self.file),
68 '.repopickle_' + os.path.basename(self.file))
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070069
70 def Has(self, name, include_defaults = True):
71 """Return true if this configuration file has the key.
72 """
Shawn O. Pearcef8e32732009-04-17 11:00:31 -070073 if _key(name) in self._cache:
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070074 return True
75 if include_defaults and self.defaults:
76 return self.defaults.Has(name, include_defaults = True)
77 return False
78
79 def GetBoolean(self, name):
80 """Returns a boolean from the configuration file.
81 None : The value was not defined, or is not a boolean.
82 True : The value was set to true or yes.
83 False: The value was set to false or no.
84 """
85 v = self.GetString(name)
86 if v is None:
87 return None
88 v = v.lower()
89 if v in ('true', 'yes'):
90 return True
91 if v in ('false', 'no'):
92 return False
93 return None
94
95 def GetString(self, name, all=False):
96 """Get the first value for a key, or None if it is not defined.
97
98 This configuration file is used first, if the key is not
99 defined or all = True then the defaults are also searched.
100 """
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700101 try:
Shawn O. Pearcef8e32732009-04-17 11:00:31 -0700102 v = self._cache[_key(name)]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700103 except KeyError:
104 if self.defaults:
105 return self.defaults.GetString(name, all = all)
106 v = []
107
108 if not all:
109 if v:
110 return v[0]
111 return None
112
113 r = []
114 r.extend(v)
115 if self.defaults:
116 r.extend(self.defaults.GetString(name, all = True))
117 return r
118
119 def SetString(self, name, value):
120 """Set the value(s) for a key.
121 Only this configuration file is modified.
122
123 The supplied value should be either a string,
124 or a list of strings (to store multiple values).
125 """
Shawn O. Pearcef8e32732009-04-17 11:00:31 -0700126 key = _key(name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700127
128 try:
Shawn O. Pearcef8e32732009-04-17 11:00:31 -0700129 old = self._cache[key]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700130 except KeyError:
131 old = []
132
133 if value is None:
134 if old:
Shawn O. Pearcef8e32732009-04-17 11:00:31 -0700135 del self._cache[key]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700136 self._do('--unset-all', name)
137
138 elif isinstance(value, list):
139 if len(value) == 0:
140 self.SetString(name, None)
141
142 elif len(value) == 1:
143 self.SetString(name, value[0])
144
145 elif old != value:
Shawn O. Pearcef8e32732009-04-17 11:00:31 -0700146 self._cache[key] = list(value)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700147 self._do('--replace-all', name, value[0])
148 for i in xrange(1, len(value)):
149 self._do('--add', name, value[i])
150
151 elif len(old) != 1 or old[0] != value:
Shawn O. Pearcef8e32732009-04-17 11:00:31 -0700152 self._cache[key] = [value]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700153 self._do('--replace-all', name, value)
154
155 def GetRemote(self, name):
156 """Get the remote.$name.* configuration values as an object.
157 """
158 try:
159 r = self._remotes[name]
160 except KeyError:
161 r = Remote(self, name)
162 self._remotes[r.name] = r
163 return r
164
165 def GetBranch(self, name):
166 """Get the branch.$name.* configuration values as an object.
167 """
168 try:
169 b = self._branches[name]
170 except KeyError:
171 b = Branch(self, name)
172 self._branches[b.name] = b
173 return b
174
Shawn O. Pearce366ad212009-05-19 12:47:37 -0700175 def GetSubSections(self, section):
176 """List all subsection names matching $section.*.*
177 """
178 return self._sections.get(section, set())
179
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700180 def HasSection(self, section, subsection = ''):
181 """Does at least one key in section.subsection exist?
182 """
183 try:
184 return subsection in self._sections[section]
185 except KeyError:
186 return False
187
188 @property
189 def _sections(self):
190 d = self._section_dict
191 if d is None:
192 d = {}
193 for name in self._cache.keys():
194 p = name.split('.')
195 if 2 == len(p):
196 section = p[0]
197 subsect = ''
198 else:
199 section = p[0]
200 subsect = '.'.join(p[1:-1])
201 if section not in d:
202 d[section] = set()
203 d[section].add(subsect)
204 self._section_dict = d
205 return d
206
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700207 @property
208 def _cache(self):
209 if self._cache_dict is None:
210 self._cache_dict = self._Read()
211 return self._cache_dict
212
213 def _Read(self):
Shawn O. Pearcec12c3602009-04-17 21:03:32 -0700214 d = self._ReadPickle()
215 if d is None:
216 d = self._ReadGit()
217 self._SavePickle(d)
218 return d
219
220 def _ReadPickle(self):
221 try:
222 if os.path.getmtime(self._pickle) \
223 <= os.path.getmtime(self.file):
224 os.remove(self._pickle)
225 return None
226 except OSError:
227 return None
228 try:
Shawn O. Pearcead3193a2009-04-18 09:54:51 -0700229 Trace(': unpickle %s', self.file)
Shawn O. Pearce76ca9f82009-04-18 14:48:03 -0700230 fd = open(self._pickle, 'rb')
231 try:
232 return cPickle.load(fd)
233 finally:
234 fd.close()
Shawn O. Pearcec12c3602009-04-17 21:03:32 -0700235 except IOError:
236 os.remove(self._pickle)
237 return None
238 except cPickle.PickleError:
239 os.remove(self._pickle)
240 return None
241
242 def _SavePickle(self, cache):
243 try:
Shawn O. Pearce76ca9f82009-04-18 14:48:03 -0700244 fd = open(self._pickle, 'wb')
245 try:
246 cPickle.dump(cache, fd, cPickle.HIGHEST_PROTOCOL)
247 finally:
248 fd.close()
Shawn O. Pearcec12c3602009-04-17 21:03:32 -0700249 except IOError:
250 os.remove(self._pickle)
251 except cPickle.PickleError:
252 os.remove(self._pickle)
253
254 def _ReadGit(self):
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700255 d = self._do('--null', '--list')
256 c = {}
257 while d:
258 lf = d.index('\n')
259 nul = d.index('\0', lf + 1)
260
Shawn O. Pearcef8e32732009-04-17 11:00:31 -0700261 key = _key(d[0:lf])
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700262 val = d[lf + 1:nul]
263
264 if key in c:
265 c[key].append(val)
266 else:
267 c[key] = [val]
268
269 d = d[nul + 1:]
270 return c
271
272 def _do(self, *args):
273 command = ['config', '--file', self.file]
274 command.extend(args)
275
276 p = GitCommand(None,
277 command,
278 capture_stdout = True,
279 capture_stderr = True)
280 if p.Wait() == 0:
281 return p.stdout
282 else:
283 GitError('git config %s: %s' % (str(args), p.stderr))
284
285
286class RefSpec(object):
287 """A Git refspec line, split into its components:
288
289 forced: True if the line starts with '+'
290 src: Left side of the line
291 dst: Right side of the line
292 """
293
294 @classmethod
295 def FromString(cls, rs):
296 lhs, rhs = rs.split(':', 2)
297 if lhs.startswith('+'):
298 lhs = lhs[1:]
299 forced = True
300 else:
301 forced = False
302 return cls(forced, lhs, rhs)
303
304 def __init__(self, forced, lhs, rhs):
305 self.forced = forced
306 self.src = lhs
307 self.dst = rhs
308
309 def SourceMatches(self, rev):
310 if self.src:
311 if rev == self.src:
312 return True
313 if self.src.endswith('/*') and rev.startswith(self.src[:-1]):
314 return True
315 return False
316
317 def DestMatches(self, ref):
318 if self.dst:
319 if ref == self.dst:
320 return True
321 if self.dst.endswith('/*') and ref.startswith(self.dst[:-1]):
322 return True
323 return False
324
325 def MapSource(self, rev):
326 if self.src.endswith('/*'):
327 return self.dst[:-1] + rev[len(self.src) - 1:]
328 return self.dst
329
330 def __str__(self):
331 s = ''
332 if self.forced:
333 s += '+'
334 if self.src:
335 s += self.src
336 if self.dst:
337 s += ':'
338 s += self.dst
339 return s
340
341
Shawn O. Pearcefb231612009-04-10 18:53:46 -0700342_ssh_cache = {}
343_ssh_master = True
344
Shawn O. Pearce896d5df2009-04-21 14:51:04 -0700345def _open_ssh(host, port):
Shawn O. Pearcefb231612009-04-10 18:53:46 -0700346 global _ssh_master
347
Shawn O. Pearcefb231612009-04-10 18:53:46 -0700348 key = '%s:%s' % (host, port)
349 if key in _ssh_cache:
350 return True
351
352 if not _ssh_master \
353 or 'GIT_SSH' in os.environ \
Shawn O. Pearce2b5b4ac2009-04-23 17:22:18 -0700354 or sys.platform in ('win32', 'cygwin'):
Shawn O. Pearcefb231612009-04-10 18:53:46 -0700355 # failed earlier, or cygwin ssh can't do this
356 #
357 return False
358
359 command = ['ssh',
360 '-o','ControlPath %s' % _ssh_sock(),
361 '-p',str(port),
362 '-M',
363 '-N',
364 host]
365 try:
366 Trace(': %s', ' '.join(command))
367 p = subprocess.Popen(command)
368 except Exception, e:
369 _ssh_master = False
370 print >>sys.stderr, \
371 '\nwarn: cannot enable ssh control master for %s:%s\n%s' \
372 % (host,port, str(e))
373 return False
374
375 _ssh_cache[key] = p
376 time.sleep(1)
377 return True
378
379def close_ssh():
380 for key,p in _ssh_cache.iteritems():
381 os.kill(p.pid, SIGTERM)
382 p.wait()
383 _ssh_cache.clear()
384
385 d = _ssh_sock(create=False)
386 if d:
387 try:
388 os.rmdir(os.path.dirname(d))
389 except OSError:
390 pass
391
392URI_SCP = re.compile(r'^([^@:]*@?[^:/]{1,}):')
Shawn O. Pearce2f968c92009-04-30 14:30:28 -0700393URI_ALL = re.compile(r'^([a-z][a-z+]*)://([^@/]*@?[^/]*)/')
Shawn O. Pearcefb231612009-04-10 18:53:46 -0700394
395def _preconnect(url):
396 m = URI_ALL.match(url)
397 if m:
398 scheme = m.group(1)
399 host = m.group(2)
400 if ':' in host:
401 host, port = host.split(':')
Shawn O. Pearce896d5df2009-04-21 14:51:04 -0700402 else:
403 port = 22
Shawn O. Pearcefb231612009-04-10 18:53:46 -0700404 if scheme in ('ssh', 'git+ssh', 'ssh+git'):
405 return _open_ssh(host, port)
406 return False
407
408 m = URI_SCP.match(url)
409 if m:
410 host = m.group(1)
Shawn O. Pearce896d5df2009-04-21 14:51:04 -0700411 return _open_ssh(host, 22)
Shawn O. Pearcefb231612009-04-10 18:53:46 -0700412
413
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700414class Remote(object):
415 """Configuration options related to a remote.
416 """
417 def __init__(self, config, name):
418 self._config = config
419 self.name = name
420 self.url = self._Get('url')
421 self.review = self._Get('review')
Shawn O. Pearce339ba9f2008-11-06 09:52:51 -0800422 self.projectname = self._Get('projectname')
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700423 self.fetch = map(lambda x: RefSpec.FromString(x),
424 self._Get('fetch', all=True))
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800425 self._review_protocol = None
426
Shawn O. Pearcefb231612009-04-10 18:53:46 -0700427 def PreConnectFetch(self):
428 return _preconnect(self.url)
429
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800430 @property
431 def ReviewProtocol(self):
432 if self._review_protocol is None:
433 if self.review is None:
434 return None
435
436 u = self.review
437 if not u.startswith('http:') and not u.startswith('https:'):
438 u = 'http://%s' % u
Shawn O. Pearce13cc3842009-03-25 13:54:54 -0700439 if u.endswith('/Gerrit'):
440 u = u[:len(u) - len('/Gerrit')]
441 if not u.endswith('/ssh_info'):
442 if not u.endswith('/'):
443 u += '/'
444 u += 'ssh_info'
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800445
Shawn O. Pearce146fe902009-03-25 14:06:43 -0700446 if u in REVIEW_CACHE:
447 info = REVIEW_CACHE[u]
448 self._review_protocol = info[0]
449 self._review_host = info[1]
450 self._review_port = info[2]
451 else:
452 try:
453 info = urlopen(u).read()
454 if info == 'NOT_AVAILABLE':
455 raise UploadError('Upload over ssh unavailable')
456 if '<' in info:
457 # Assume the server gave us some sort of HTML
458 # response back, like maybe a login page.
459 #
460 raise UploadError('Cannot read %s:\n%s' % (u, info))
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800461
Shawn O. Pearce146fe902009-03-25 14:06:43 -0700462 self._review_protocol = 'ssh'
463 self._review_host = info.split(" ")[0]
464 self._review_port = info.split(" ")[1]
465 except HTTPError, e:
466 if e.code == 404:
467 self._review_protocol = 'http-post'
468 self._review_host = None
469 self._review_port = None
470 else:
471 raise UploadError('Cannot guess Gerrit version')
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800472
Shawn O. Pearce146fe902009-03-25 14:06:43 -0700473 REVIEW_CACHE[u] = (
474 self._review_protocol,
475 self._review_host,
476 self._review_port)
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800477 return self._review_protocol
478
479 def SshReviewUrl(self, userEmail):
480 if self.ReviewProtocol != 'ssh':
481 return None
482 return 'ssh://%s@%s:%s/%s' % (
483 userEmail.split("@")[0],
484 self._review_host,
485 self._review_port,
486 self.projectname)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700487
488 def ToLocal(self, rev):
489 """Convert a remote revision string to something we have locally.
490 """
491 if IsId(rev):
492 return rev
493 if rev.startswith(R_TAGS):
494 return rev
495
496 if not rev.startswith('refs/'):
497 rev = R_HEADS + rev
498
499 for spec in self.fetch:
500 if spec.SourceMatches(rev):
501 return spec.MapSource(rev)
502 raise GitError('remote %s does not have %s' % (self.name, rev))
503
504 def WritesTo(self, ref):
505 """True if the remote stores to the tracking ref.
506 """
507 for spec in self.fetch:
508 if spec.DestMatches(ref):
509 return True
510 return False
511
Shawn O. Pearcee284ad12008-11-04 07:37:10 -0800512 def ResetFetch(self, mirror=False):
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700513 """Set the fetch refspec to its default value.
514 """
Shawn O. Pearcee284ad12008-11-04 07:37:10 -0800515 if mirror:
516 dst = 'refs/heads/*'
517 else:
518 dst = 'refs/remotes/%s/*' % self.name
519 self.fetch = [RefSpec(True, 'refs/heads/*', dst)]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700520
521 def Save(self):
522 """Save this remote to the configuration.
523 """
524 self._Set('url', self.url)
525 self._Set('review', self.review)
Shawn O. Pearce339ba9f2008-11-06 09:52:51 -0800526 self._Set('projectname', self.projectname)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700527 self._Set('fetch', map(lambda x: str(x), self.fetch))
528
529 def _Set(self, key, value):
530 key = 'remote.%s.%s' % (self.name, key)
531 return self._config.SetString(key, value)
532
533 def _Get(self, key, all=False):
534 key = 'remote.%s.%s' % (self.name, key)
535 return self._config.GetString(key, all = all)
536
537
538class Branch(object):
539 """Configuration options related to a single branch.
540 """
541 def __init__(self, config, name):
542 self._config = config
543 self.name = name
544 self.merge = self._Get('merge')
545
546 r = self._Get('remote')
547 if r:
548 self.remote = self._config.GetRemote(r)
549 else:
550 self.remote = None
551
552 @property
553 def LocalMerge(self):
554 """Convert the merge spec to a local name.
555 """
556 if self.remote and self.merge:
557 return self.remote.ToLocal(self.merge)
558 return None
559
560 def Save(self):
561 """Save this branch back into the configuration.
562 """
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700563 if self._config.HasSection('branch', self.name):
564 if self.remote:
565 self._Set('remote', self.remote.name)
566 else:
567 self._Set('remote', None)
568 self._Set('merge', self.merge)
569
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700570 else:
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700571 fd = open(self._config.file, 'ab')
572 try:
573 fd.write('[branch "%s"]\n' % self.name)
574 if self.remote:
575 fd.write('\tremote = %s\n' % self.remote.name)
576 if self.merge:
577 fd.write('\tmerge = %s\n' % self.merge)
578 finally:
579 fd.close()
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700580
581 def _Set(self, key, value):
582 key = 'branch.%s.%s' % (self.name, key)
583 return self._config.SetString(key, value)
584
585 def _Get(self, key, all=False):
586 key = 'branch.%s.%s' % (self.name, key)
587 return self._config.GetString(key, all = all)