blob: 9dba699acaaf22028eecf74b78af4db49e440e66 [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
Shawn O. Pearce1b34c912009-05-21 18:52:49 -070059 def __init__(self, file, defaults=None, pickleFile=None):
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070060 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. Pearce1b34c912009-05-21 18:52:49 -070066
67 if pickleFile is None:
68 self._pickle = os.path.join(
69 os.path.dirname(self.file),
70 '.repopickle_' + os.path.basename(self.file))
71 else:
72 self._pickle = pickleFile
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070073
Shawn O. Pearce98ea26b2009-06-04 19:19:11 -070074 def ClearCache(self):
75 if os.path.exists(self._pickle):
76 os.remove(self._pickle)
77 self._cache_dict = None
78 self._section_dict = None
79 self._remotes = {}
80 self._branches = {}
81
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070082 def Has(self, name, include_defaults = True):
83 """Return true if this configuration file has the key.
84 """
Shawn O. Pearcef8e32732009-04-17 11:00:31 -070085 if _key(name) in self._cache:
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -070086 return True
87 if include_defaults and self.defaults:
88 return self.defaults.Has(name, include_defaults = True)
89 return False
90
91 def GetBoolean(self, name):
92 """Returns a boolean from the configuration file.
93 None : The value was not defined, or is not a boolean.
94 True : The value was set to true or yes.
95 False: The value was set to false or no.
96 """
97 v = self.GetString(name)
98 if v is None:
99 return None
100 v = v.lower()
101 if v in ('true', 'yes'):
102 return True
103 if v in ('false', 'no'):
104 return False
105 return None
106
107 def GetString(self, name, all=False):
108 """Get the first value for a key, or None if it is not defined.
109
110 This configuration file is used first, if the key is not
111 defined or all = True then the defaults are also searched.
112 """
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700113 try:
Shawn O. Pearcef8e32732009-04-17 11:00:31 -0700114 v = self._cache[_key(name)]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700115 except KeyError:
116 if self.defaults:
117 return self.defaults.GetString(name, all = all)
118 v = []
119
120 if not all:
121 if v:
122 return v[0]
123 return None
124
125 r = []
126 r.extend(v)
127 if self.defaults:
128 r.extend(self.defaults.GetString(name, all = True))
129 return r
130
131 def SetString(self, name, value):
132 """Set the value(s) for a key.
133 Only this configuration file is modified.
134
135 The supplied value should be either a string,
136 or a list of strings (to store multiple values).
137 """
Shawn O. Pearcef8e32732009-04-17 11:00:31 -0700138 key = _key(name)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700139
140 try:
Shawn O. Pearcef8e32732009-04-17 11:00:31 -0700141 old = self._cache[key]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700142 except KeyError:
143 old = []
144
145 if value is None:
146 if old:
Shawn O. Pearcef8e32732009-04-17 11:00:31 -0700147 del self._cache[key]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700148 self._do('--unset-all', name)
149
150 elif isinstance(value, list):
151 if len(value) == 0:
152 self.SetString(name, None)
153
154 elif len(value) == 1:
155 self.SetString(name, value[0])
156
157 elif old != value:
Shawn O. Pearcef8e32732009-04-17 11:00:31 -0700158 self._cache[key] = list(value)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700159 self._do('--replace-all', name, value[0])
160 for i in xrange(1, len(value)):
161 self._do('--add', name, value[i])
162
163 elif len(old) != 1 or old[0] != value:
Shawn O. Pearcef8e32732009-04-17 11:00:31 -0700164 self._cache[key] = [value]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700165 self._do('--replace-all', name, value)
166
167 def GetRemote(self, name):
168 """Get the remote.$name.* configuration values as an object.
169 """
170 try:
171 r = self._remotes[name]
172 except KeyError:
173 r = Remote(self, name)
174 self._remotes[r.name] = r
175 return r
176
177 def GetBranch(self, name):
178 """Get the branch.$name.* configuration values as an object.
179 """
180 try:
181 b = self._branches[name]
182 except KeyError:
183 b = Branch(self, name)
184 self._branches[b.name] = b
185 return b
186
Shawn O. Pearce366ad212009-05-19 12:47:37 -0700187 def GetSubSections(self, section):
188 """List all subsection names matching $section.*.*
189 """
190 return self._sections.get(section, set())
191
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700192 def HasSection(self, section, subsection = ''):
193 """Does at least one key in section.subsection exist?
194 """
195 try:
196 return subsection in self._sections[section]
197 except KeyError:
198 return False
199
200 @property
201 def _sections(self):
202 d = self._section_dict
203 if d is None:
204 d = {}
205 for name in self._cache.keys():
206 p = name.split('.')
207 if 2 == len(p):
208 section = p[0]
209 subsect = ''
210 else:
211 section = p[0]
212 subsect = '.'.join(p[1:-1])
213 if section not in d:
214 d[section] = set()
215 d[section].add(subsect)
216 self._section_dict = d
217 return d
218
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700219 @property
220 def _cache(self):
221 if self._cache_dict is None:
222 self._cache_dict = self._Read()
223 return self._cache_dict
224
225 def _Read(self):
Shawn O. Pearcec12c3602009-04-17 21:03:32 -0700226 d = self._ReadPickle()
227 if d is None:
228 d = self._ReadGit()
229 self._SavePickle(d)
230 return d
231
232 def _ReadPickle(self):
233 try:
234 if os.path.getmtime(self._pickle) \
235 <= os.path.getmtime(self.file):
236 os.remove(self._pickle)
237 return None
238 except OSError:
239 return None
240 try:
Shawn O. Pearcead3193a2009-04-18 09:54:51 -0700241 Trace(': unpickle %s', self.file)
Shawn O. Pearce76ca9f82009-04-18 14:48:03 -0700242 fd = open(self._pickle, 'rb')
243 try:
244 return cPickle.load(fd)
245 finally:
246 fd.close()
Shawn O. Pearce2a3a81b2009-06-12 09:10:07 -0700247 except EOFError:
248 os.remove(self._pickle)
249 return None
Shawn O. Pearcec12c3602009-04-17 21:03:32 -0700250 except IOError:
251 os.remove(self._pickle)
252 return None
253 except cPickle.PickleError:
254 os.remove(self._pickle)
255 return None
256
257 def _SavePickle(self, cache):
258 try:
Shawn O. Pearce76ca9f82009-04-18 14:48:03 -0700259 fd = open(self._pickle, 'wb')
260 try:
261 cPickle.dump(cache, fd, cPickle.HIGHEST_PROTOCOL)
262 finally:
263 fd.close()
Shawn O. Pearcec12c3602009-04-17 21:03:32 -0700264 except IOError:
Shawn O. Pearceb0ca41e2009-07-03 20:01:47 -0700265 if os.path.exists(self._pickle):
266 os.remove(self._pickle)
Shawn O. Pearcec12c3602009-04-17 21:03:32 -0700267 except cPickle.PickleError:
Shawn O. Pearceb0ca41e2009-07-03 20:01:47 -0700268 if os.path.exists(self._pickle):
269 os.remove(self._pickle)
Shawn O. Pearcec12c3602009-04-17 21:03:32 -0700270
271 def _ReadGit(self):
David Aguilar438c5472009-06-28 15:09:16 -0700272 """
273 Read configuration data from git.
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700274
David Aguilar438c5472009-06-28 15:09:16 -0700275 This internal method populates the GitConfig cache.
276
277 """
David Aguilar438c5472009-06-28 15:09:16 -0700278 c = {}
Shawn O. Pearcec24c7202009-07-02 16:12:57 -0700279 d = self._do('--null', '--list')
280 if d is None:
281 return c
282 for line in d.rstrip('\0').split('\0'):
David Aguilar438c5472009-06-28 15:09:16 -0700283 if '\n' in line:
284 key, val = line.split('\n', 1)
285 else:
286 key = line
287 val = None
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700288
289 if key in c:
290 c[key].append(val)
291 else:
292 c[key] = [val]
293
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700294 return c
295
296 def _do(self, *args):
297 command = ['config', '--file', self.file]
298 command.extend(args)
299
300 p = GitCommand(None,
301 command,
302 capture_stdout = True,
303 capture_stderr = True)
304 if p.Wait() == 0:
305 return p.stdout
306 else:
307 GitError('git config %s: %s' % (str(args), p.stderr))
308
309
310class RefSpec(object):
311 """A Git refspec line, split into its components:
312
313 forced: True if the line starts with '+'
314 src: Left side of the line
315 dst: Right side of the line
316 """
317
318 @classmethod
319 def FromString(cls, rs):
320 lhs, rhs = rs.split(':', 2)
321 if lhs.startswith('+'):
322 lhs = lhs[1:]
323 forced = True
324 else:
325 forced = False
326 return cls(forced, lhs, rhs)
327
328 def __init__(self, forced, lhs, rhs):
329 self.forced = forced
330 self.src = lhs
331 self.dst = rhs
332
333 def SourceMatches(self, rev):
334 if self.src:
335 if rev == self.src:
336 return True
337 if self.src.endswith('/*') and rev.startswith(self.src[:-1]):
338 return True
339 return False
340
341 def DestMatches(self, ref):
342 if self.dst:
343 if ref == self.dst:
344 return True
345 if self.dst.endswith('/*') and ref.startswith(self.dst[:-1]):
346 return True
347 return False
348
349 def MapSource(self, rev):
350 if self.src.endswith('/*'):
351 return self.dst[:-1] + rev[len(self.src) - 1:]
352 return self.dst
353
354 def __str__(self):
355 s = ''
356 if self.forced:
357 s += '+'
358 if self.src:
359 s += self.src
360 if self.dst:
361 s += ':'
362 s += self.dst
363 return s
364
365
Shawn O. Pearcefb231612009-04-10 18:53:46 -0700366_ssh_cache = {}
367_ssh_master = True
368
Josh Guilfoyle4c0f6702009-08-16 09:44:40 -0700369def _open_ssh(host, port=None):
Shawn O. Pearcefb231612009-04-10 18:53:46 -0700370 global _ssh_master
371
Josh Guilfoyle4c0f6702009-08-16 09:44:40 -0700372 if port is not None:
373 key = '%s:%s' % (host, port)
374 else:
375 key = host
376
Shawn O. Pearcefb231612009-04-10 18:53:46 -0700377 if key in _ssh_cache:
378 return True
379
380 if not _ssh_master \
381 or 'GIT_SSH' in os.environ \
Shawn O. Pearce2b5b4ac2009-04-23 17:22:18 -0700382 or sys.platform in ('win32', 'cygwin'):
Shawn O. Pearcefb231612009-04-10 18:53:46 -0700383 # failed earlier, or cygwin ssh can't do this
384 #
385 return False
386
387 command = ['ssh',
388 '-o','ControlPath %s' % _ssh_sock(),
Shawn O. Pearcefb231612009-04-10 18:53:46 -0700389 '-M',
390 '-N',
391 host]
Josh Guilfoyle4c0f6702009-08-16 09:44:40 -0700392
393 if port is not None:
394 command[3:3] = ['-p',str(port)]
395
Shawn O. Pearcefb231612009-04-10 18:53:46 -0700396 try:
397 Trace(': %s', ' '.join(command))
398 p = subprocess.Popen(command)
399 except Exception, e:
400 _ssh_master = False
401 print >>sys.stderr, \
402 '\nwarn: cannot enable ssh control master for %s:%s\n%s' \
403 % (host,port, str(e))
404 return False
405
406 _ssh_cache[key] = p
407 time.sleep(1)
408 return True
409
410def close_ssh():
411 for key,p in _ssh_cache.iteritems():
Shawn O. Pearce26120ca2009-06-16 11:49:10 -0700412 try:
413 os.kill(p.pid, SIGTERM)
414 p.wait()
Shawn O. Pearcefb5c8fd2009-06-16 14:57:46 -0700415 except OSError:
Shawn O. Pearce26120ca2009-06-16 11:49:10 -0700416 pass
Shawn O. Pearcefb231612009-04-10 18:53:46 -0700417 _ssh_cache.clear()
418
419 d = _ssh_sock(create=False)
420 if d:
421 try:
422 os.rmdir(os.path.dirname(d))
423 except OSError:
424 pass
425
426URI_SCP = re.compile(r'^([^@:]*@?[^:/]{1,}):')
Shawn O. Pearce2f968c92009-04-30 14:30:28 -0700427URI_ALL = re.compile(r'^([a-z][a-z+]*)://([^@/]*@?[^/]*)/')
Shawn O. Pearcefb231612009-04-10 18:53:46 -0700428
429def _preconnect(url):
430 m = URI_ALL.match(url)
431 if m:
432 scheme = m.group(1)
433 host = m.group(2)
434 if ':' in host:
435 host, port = host.split(':')
Shawn O. Pearce896d5df2009-04-21 14:51:04 -0700436 else:
Josh Guilfoyle4c0f6702009-08-16 09:44:40 -0700437 port = None
Shawn O. Pearcefb231612009-04-10 18:53:46 -0700438 if scheme in ('ssh', 'git+ssh', 'ssh+git'):
439 return _open_ssh(host, port)
440 return False
441
442 m = URI_SCP.match(url)
443 if m:
444 host = m.group(1)
Josh Guilfoyle4c0f6702009-08-16 09:44:40 -0700445 return _open_ssh(host)
Shawn O. Pearcefb231612009-04-10 18:53:46 -0700446
Shawn O. Pearce7b4f4352009-06-12 09:06:35 -0700447 return False
Shawn O. Pearcefb231612009-04-10 18:53:46 -0700448
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700449class Remote(object):
450 """Configuration options related to a remote.
451 """
452 def __init__(self, config, name):
453 self._config = config
454 self.name = name
455 self.url = self._Get('url')
456 self.review = self._Get('review')
Shawn O. Pearce339ba9f2008-11-06 09:52:51 -0800457 self.projectname = self._Get('projectname')
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700458 self.fetch = map(lambda x: RefSpec.FromString(x),
459 self._Get('fetch', all=True))
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800460 self._review_protocol = None
461
Shawn O. Pearcefb231612009-04-10 18:53:46 -0700462 def PreConnectFetch(self):
463 return _preconnect(self.url)
464
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800465 @property
466 def ReviewProtocol(self):
467 if self._review_protocol is None:
468 if self.review is None:
469 return None
470
471 u = self.review
472 if not u.startswith('http:') and not u.startswith('https:'):
473 u = 'http://%s' % u
Shawn O. Pearce13cc3842009-03-25 13:54:54 -0700474 if u.endswith('/Gerrit'):
475 u = u[:len(u) - len('/Gerrit')]
476 if not u.endswith('/ssh_info'):
477 if not u.endswith('/'):
478 u += '/'
479 u += 'ssh_info'
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800480
Shawn O. Pearce146fe902009-03-25 14:06:43 -0700481 if u in REVIEW_CACHE:
482 info = REVIEW_CACHE[u]
483 self._review_protocol = info[0]
484 self._review_host = info[1]
485 self._review_port = info[2]
486 else:
487 try:
488 info = urlopen(u).read()
489 if info == 'NOT_AVAILABLE':
490 raise UploadError('Upload over ssh unavailable')
491 if '<' in info:
492 # Assume the server gave us some sort of HTML
493 # response back, like maybe a login page.
494 #
495 raise UploadError('Cannot read %s:\n%s' % (u, info))
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800496
Shawn O. Pearce146fe902009-03-25 14:06:43 -0700497 self._review_protocol = 'ssh'
498 self._review_host = info.split(" ")[0]
499 self._review_port = info.split(" ")[1]
500 except HTTPError, e:
501 if e.code == 404:
502 self._review_protocol = 'http-post'
503 self._review_host = None
504 self._review_port = None
505 else:
506 raise UploadError('Cannot guess Gerrit version')
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800507
Shawn O. Pearce146fe902009-03-25 14:06:43 -0700508 REVIEW_CACHE[u] = (
509 self._review_protocol,
510 self._review_host,
511 self._review_port)
Shawn O. Pearceb54a3922009-01-05 16:18:58 -0800512 return self._review_protocol
513
514 def SshReviewUrl(self, userEmail):
515 if self.ReviewProtocol != 'ssh':
516 return None
517 return 'ssh://%s@%s:%s/%s' % (
518 userEmail.split("@")[0],
519 self._review_host,
520 self._review_port,
521 self.projectname)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700522
523 def ToLocal(self, rev):
524 """Convert a remote revision string to something we have locally.
525 """
526 if IsId(rev):
527 return rev
528 if rev.startswith(R_TAGS):
529 return rev
530
531 if not rev.startswith('refs/'):
532 rev = R_HEADS + rev
533
534 for spec in self.fetch:
535 if spec.SourceMatches(rev):
536 return spec.MapSource(rev)
537 raise GitError('remote %s does not have %s' % (self.name, rev))
538
539 def WritesTo(self, ref):
540 """True if the remote stores to the tracking ref.
541 """
542 for spec in self.fetch:
543 if spec.DestMatches(ref):
544 return True
545 return False
546
Shawn O. Pearcee284ad12008-11-04 07:37:10 -0800547 def ResetFetch(self, mirror=False):
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700548 """Set the fetch refspec to its default value.
549 """
Shawn O. Pearcee284ad12008-11-04 07:37:10 -0800550 if mirror:
551 dst = 'refs/heads/*'
552 else:
553 dst = 'refs/remotes/%s/*' % self.name
554 self.fetch = [RefSpec(True, 'refs/heads/*', dst)]
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700555
556 def Save(self):
557 """Save this remote to the configuration.
558 """
559 self._Set('url', self.url)
560 self._Set('review', self.review)
Shawn O. Pearce339ba9f2008-11-06 09:52:51 -0800561 self._Set('projectname', self.projectname)
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700562 self._Set('fetch', map(lambda x: str(x), self.fetch))
563
564 def _Set(self, key, value):
565 key = 'remote.%s.%s' % (self.name, key)
566 return self._config.SetString(key, value)
567
568 def _Get(self, key, all=False):
569 key = 'remote.%s.%s' % (self.name, key)
570 return self._config.GetString(key, all = all)
571
572
573class Branch(object):
574 """Configuration options related to a single branch.
575 """
576 def __init__(self, config, name):
577 self._config = config
578 self.name = name
579 self.merge = self._Get('merge')
580
581 r = self._Get('remote')
582 if r:
583 self.remote = self._config.GetRemote(r)
584 else:
585 self.remote = None
586
587 @property
588 def LocalMerge(self):
589 """Convert the merge spec to a local name.
590 """
591 if self.remote and self.merge:
592 return self.remote.ToLocal(self.merge)
593 return None
594
595 def Save(self):
596 """Save this branch back into the configuration.
597 """
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700598 if self._config.HasSection('branch', self.name):
599 if self.remote:
600 self._Set('remote', self.remote.name)
601 else:
602 self._Set('remote', None)
603 self._Set('merge', self.merge)
604
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700605 else:
Shawn O. Pearceaccc56d2009-04-18 14:45:51 -0700606 fd = open(self._config.file, 'ab')
607 try:
608 fd.write('[branch "%s"]\n' % self.name)
609 if self.remote:
610 fd.write('\tremote = %s\n' % self.remote.name)
611 if self.merge:
612 fd.write('\tmerge = %s\n' % self.merge)
613 finally:
614 fd.close()
The Android Open Source Projectcf31fe92008-10-21 07:00:00 -0700615
616 def _Set(self, key, value):
617 key = 'branch.%s.%s' % (self.name, key)
618 return self._config.SetString(key, value)
619
620 def _Get(self, key, all=False):
621 key = 'branch.%s.%s' % (self.name, key)
622 return self._config.GetString(key, all = all)