git_config.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648
  1. #
  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. import cPickle
  16. import os
  17. import re
  18. import subprocess
  19. import sys
  20. import time
  21. import urllib2
  22. from signal import SIGTERM
  23. from urllib2 import urlopen, HTTPError
  24. from error import GitError, UploadError
  25. from trace import Trace
  26. from git_command import GitCommand, _ssh_sock
  27. R_HEADS = 'refs/heads/'
  28. R_TAGS = 'refs/tags/'
  29. ID_RE = re.compile('^[0-9a-f]{40}$')
  30. REVIEW_CACHE = dict()
  31. def IsId(rev):
  32. return ID_RE.match(rev)
  33. def _key(name):
  34. parts = name.split('.')
  35. if len(parts) < 2:
  36. return name.lower()
  37. parts[ 0] = parts[ 0].lower()
  38. parts[-1] = parts[-1].lower()
  39. return '.'.join(parts)
  40. class GitConfig(object):
  41. _ForUser = None
  42. @classmethod
  43. def ForUser(cls):
  44. if cls._ForUser is None:
  45. cls._ForUser = cls(file = os.path.expanduser('~/.gitconfig'))
  46. return cls._ForUser
  47. @classmethod
  48. def ForRepository(cls, gitdir, defaults=None):
  49. return cls(file = os.path.join(gitdir, 'config'),
  50. defaults = defaults)
  51. def __init__(self, file, defaults=None, pickleFile=None):
  52. self.file = file
  53. self.defaults = defaults
  54. self._cache_dict = None
  55. self._section_dict = None
  56. self._remotes = {}
  57. self._branches = {}
  58. if pickleFile is None:
  59. self._pickle = os.path.join(
  60. os.path.dirname(self.file),
  61. '.repopickle_' + os.path.basename(self.file))
  62. else:
  63. self._pickle = pickleFile
  64. def ClearCache(self):
  65. if os.path.exists(self._pickle):
  66. os.remove(self._pickle)
  67. self._cache_dict = None
  68. self._section_dict = None
  69. self._remotes = {}
  70. self._branches = {}
  71. def Has(self, name, include_defaults = True):
  72. """Return true if this configuration file has the key.
  73. """
  74. if _key(name) in self._cache:
  75. return True
  76. if include_defaults and self.defaults:
  77. return self.defaults.Has(name, include_defaults = True)
  78. return False
  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. def GetString(self, name, all=False):
  95. """Get the first value for a key, or None if it is not defined.
  96. This configuration file is used first, if the key is not
  97. defined or all = True then the defaults are also searched.
  98. """
  99. try:
  100. v = self._cache[_key(name)]
  101. except KeyError:
  102. if self.defaults:
  103. return self.defaults.GetString(name, all = all)
  104. v = []
  105. if not all:
  106. if v:
  107. return v[0]
  108. return None
  109. r = []
  110. r.extend(v)
  111. if self.defaults:
  112. r.extend(self.defaults.GetString(name, all = True))
  113. return r
  114. def SetString(self, name, value):
  115. """Set the value(s) for a key.
  116. Only this configuration file is modified.
  117. The supplied value should be either a string,
  118. or a list of strings (to store multiple values).
  119. """
  120. key = _key(name)
  121. try:
  122. old = self._cache[key]
  123. except KeyError:
  124. old = []
  125. if value is None:
  126. if old:
  127. del self._cache[key]
  128. self._do('--unset-all', name)
  129. elif isinstance(value, list):
  130. if len(value) == 0:
  131. self.SetString(name, None)
  132. elif len(value) == 1:
  133. self.SetString(name, value[0])
  134. elif old != value:
  135. self._cache[key] = list(value)
  136. self._do('--replace-all', name, value[0])
  137. for i in xrange(1, len(value)):
  138. self._do('--add', name, value[i])
  139. elif len(old) != 1 or old[0] != value:
  140. self._cache[key] = [value]
  141. self._do('--replace-all', name, value)
  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. def GetBranch(self, name):
  152. """Get the branch.$name.* configuration values as an object.
  153. """
  154. try:
  155. b = self._branches[name]
  156. except KeyError:
  157. b = Branch(self, name)
  158. self._branches[b.name] = b
  159. return b
  160. def GetSubSections(self, section):
  161. """List all subsection names matching $section.*.*
  162. """
  163. return self._sections.get(section, set())
  164. def HasSection(self, section, subsection = ''):
  165. """Does at least one key in section.subsection exist?
  166. """
  167. try:
  168. return subsection in self._sections[section]
  169. except KeyError:
  170. return False
  171. @property
  172. def _sections(self):
  173. d = self._section_dict
  174. if d is None:
  175. d = {}
  176. for name in self._cache.keys():
  177. p = name.split('.')
  178. if 2 == len(p):
  179. section = p[0]
  180. subsect = ''
  181. else:
  182. section = p[0]
  183. subsect = '.'.join(p[1:-1])
  184. if section not in d:
  185. d[section] = set()
  186. d[section].add(subsect)
  187. self._section_dict = d
  188. return d
  189. @property
  190. def _cache(self):
  191. if self._cache_dict is None:
  192. self._cache_dict = self._Read()
  193. return self._cache_dict
  194. def _Read(self):
  195. d = self._ReadPickle()
  196. if d is None:
  197. d = self._ReadGit()
  198. self._SavePickle(d)
  199. return d
  200. def _ReadPickle(self):
  201. try:
  202. if os.path.getmtime(self._pickle) \
  203. <= os.path.getmtime(self.file):
  204. os.remove(self._pickle)
  205. return None
  206. except OSError:
  207. return None
  208. try:
  209. Trace(': unpickle %s', self.file)
  210. fd = open(self._pickle, 'rb')
  211. try:
  212. return cPickle.load(fd)
  213. finally:
  214. fd.close()
  215. except EOFError:
  216. os.remove(self._pickle)
  217. return None
  218. except IOError:
  219. os.remove(self._pickle)
  220. return None
  221. except cPickle.PickleError:
  222. os.remove(self._pickle)
  223. return None
  224. def _SavePickle(self, cache):
  225. try:
  226. fd = open(self._pickle, 'wb')
  227. try:
  228. cPickle.dump(cache, fd, cPickle.HIGHEST_PROTOCOL)
  229. finally:
  230. fd.close()
  231. except IOError:
  232. if os.path.exists(self._pickle):
  233. os.remove(self._pickle)
  234. except cPickle.PickleError:
  235. if os.path.exists(self._pickle):
  236. os.remove(self._pickle)
  237. def _ReadGit(self):
  238. """
  239. Read configuration data from git.
  240. This internal method populates the GitConfig cache.
  241. """
  242. c = {}
  243. d = self._do('--null', '--list')
  244. if d is None:
  245. return c
  246. for line in d.rstrip('\0').split('\0'):
  247. if '\n' in line:
  248. key, val = line.split('\n', 1)
  249. else:
  250. key = line
  251. val = None
  252. if key in c:
  253. c[key].append(val)
  254. else:
  255. c[key] = [val]
  256. return c
  257. def _do(self, *args):
  258. command = ['config', '--file', self.file]
  259. command.extend(args)
  260. p = GitCommand(None,
  261. command,
  262. capture_stdout = True,
  263. capture_stderr = True)
  264. if p.Wait() == 0:
  265. return p.stdout
  266. else:
  267. GitError('git config %s: %s' % (str(args), p.stderr))
  268. class RefSpec(object):
  269. """A Git refspec line, split into its components:
  270. forced: True if the line starts with '+'
  271. src: Left side of the line
  272. dst: Right side of the line
  273. """
  274. @classmethod
  275. def FromString(cls, rs):
  276. lhs, rhs = rs.split(':', 2)
  277. if lhs.startswith('+'):
  278. lhs = lhs[1:]
  279. forced = True
  280. else:
  281. forced = False
  282. return cls(forced, lhs, rhs)
  283. def __init__(self, forced, lhs, rhs):
  284. self.forced = forced
  285. self.src = lhs
  286. self.dst = rhs
  287. def SourceMatches(self, rev):
  288. if self.src:
  289. if rev == self.src:
  290. return True
  291. if self.src.endswith('/*') and rev.startswith(self.src[:-1]):
  292. return True
  293. return False
  294. def DestMatches(self, ref):
  295. if self.dst:
  296. if ref == self.dst:
  297. return True
  298. if self.dst.endswith('/*') and ref.startswith(self.dst[:-1]):
  299. return True
  300. return False
  301. def MapSource(self, rev):
  302. if self.src.endswith('/*'):
  303. return self.dst[:-1] + rev[len(self.src) - 1:]
  304. return self.dst
  305. def __str__(self):
  306. s = ''
  307. if self.forced:
  308. s += '+'
  309. if self.src:
  310. s += self.src
  311. if self.dst:
  312. s += ':'
  313. s += self.dst
  314. return s
  315. _ssh_cache = {}
  316. _ssh_master = True
  317. def _open_ssh(host, port=None):
  318. global _ssh_master
  319. if port is not None:
  320. key = '%s:%s' % (host, port)
  321. else:
  322. key = host
  323. if key in _ssh_cache:
  324. return True
  325. if not _ssh_master \
  326. or 'GIT_SSH' in os.environ \
  327. or sys.platform in ('win32', 'cygwin'):
  328. # failed earlier, or cygwin ssh can't do this
  329. #
  330. return False
  331. command = ['ssh',
  332. '-o','ControlPath %s' % _ssh_sock(),
  333. '-M',
  334. '-N',
  335. host]
  336. if port is not None:
  337. command[3:3] = ['-p',str(port)]
  338. try:
  339. Trace(': %s', ' '.join(command))
  340. p = subprocess.Popen(command)
  341. except Exception, e:
  342. _ssh_master = False
  343. print >>sys.stderr, \
  344. '\nwarn: cannot enable ssh control master for %s:%s\n%s' \
  345. % (host,port, str(e))
  346. return False
  347. _ssh_cache[key] = p
  348. time.sleep(1)
  349. return True
  350. def close_ssh():
  351. for key,p in _ssh_cache.iteritems():
  352. try:
  353. os.kill(p.pid, SIGTERM)
  354. p.wait()
  355. except OSError:
  356. pass
  357. _ssh_cache.clear()
  358. d = _ssh_sock(create=False)
  359. if d:
  360. try:
  361. os.rmdir(os.path.dirname(d))
  362. except OSError:
  363. pass
  364. URI_SCP = re.compile(r'^([^@:]*@?[^:/]{1,}):')
  365. URI_ALL = re.compile(r'^([a-z][a-z+]*)://([^@/]*@?[^/]*)/')
  366. def _preconnect(url):
  367. m = URI_ALL.match(url)
  368. if m:
  369. scheme = m.group(1)
  370. host = m.group(2)
  371. if ':' in host:
  372. host, port = host.split(':')
  373. else:
  374. port = None
  375. if scheme in ('ssh', 'git+ssh', 'ssh+git'):
  376. return _open_ssh(host, port)
  377. return False
  378. m = URI_SCP.match(url)
  379. if m:
  380. host = m.group(1)
  381. return _open_ssh(host)
  382. return False
  383. class Remote(object):
  384. """Configuration options related to a remote.
  385. """
  386. def __init__(self, config, name):
  387. self._config = config
  388. self.name = name
  389. self.url = self._Get('url')
  390. self.review = self._Get('review')
  391. self.projectname = self._Get('projectname')
  392. self.fetch = map(lambda x: RefSpec.FromString(x),
  393. self._Get('fetch', all=True))
  394. self._review_protocol = None
  395. def _InsteadOf(self):
  396. globCfg = GitConfig.ForUser()
  397. urlList = globCfg.GetSubSections('url')
  398. longest = ""
  399. longestUrl = ""
  400. for url in urlList:
  401. key = "url." + url + ".insteadOf"
  402. insteadOfList = globCfg.GetString(key, all=True)
  403. for insteadOf in insteadOfList:
  404. if self.url.startswith(insteadOf) \
  405. and len(insteadOf) > len(longest):
  406. longest = insteadOf
  407. longestUrl = url
  408. if len(longest) == 0:
  409. return self.url
  410. return self.url.replace(longest, longestUrl, 1)
  411. def PreConnectFetch(self):
  412. connectionUrl = self._InsteadOf()
  413. return _preconnect(connectionUrl)
  414. @property
  415. def ReviewProtocol(self):
  416. if self._review_protocol is None:
  417. if self.review is None:
  418. return None
  419. u = self.review
  420. if not u.startswith('http:') and not u.startswith('https:'):
  421. u = 'http://%s' % u
  422. if u.endswith('/Gerrit'):
  423. u = u[:len(u) - len('/Gerrit')]
  424. if not u.endswith('/ssh_info'):
  425. if not u.endswith('/'):
  426. u += '/'
  427. u += 'ssh_info'
  428. if u in REVIEW_CACHE:
  429. info = REVIEW_CACHE[u]
  430. self._review_protocol = info[0]
  431. self._review_host = info[1]
  432. self._review_port = info[2]
  433. else:
  434. try:
  435. info = urlopen(u).read()
  436. if info == 'NOT_AVAILABLE':
  437. raise UploadError('%s: SSH disabled' % self.review)
  438. if '<' in info:
  439. # Assume the server gave us some sort of HTML
  440. # response back, like maybe a login page.
  441. #
  442. raise UploadError('%s: Cannot parse response' % u)
  443. self._review_protocol = 'ssh'
  444. self._review_host = info.split(" ")[0]
  445. self._review_port = info.split(" ")[1]
  446. except urllib2.URLError, e:
  447. raise UploadError('%s: %s' % (self.review, e.reason[1]))
  448. except HTTPError, e:
  449. if e.code == 404:
  450. self._review_protocol = 'http-post'
  451. self._review_host = None
  452. self._review_port = None
  453. else:
  454. raise UploadError('Upload over ssh unavailable')
  455. REVIEW_CACHE[u] = (
  456. self._review_protocol,
  457. self._review_host,
  458. self._review_port)
  459. return self._review_protocol
  460. def SshReviewUrl(self, userEmail):
  461. if self.ReviewProtocol != 'ssh':
  462. return None
  463. return 'ssh://%s@%s:%s/%s' % (
  464. userEmail.split("@")[0],
  465. self._review_host,
  466. self._review_port,
  467. self.projectname)
  468. def ToLocal(self, rev):
  469. """Convert a remote revision string to something we have locally.
  470. """
  471. if IsId(rev):
  472. return rev
  473. if rev.startswith(R_TAGS):
  474. return rev
  475. if not rev.startswith('refs/'):
  476. rev = R_HEADS + rev
  477. for spec in self.fetch:
  478. if spec.SourceMatches(rev):
  479. return spec.MapSource(rev)
  480. raise GitError('remote %s does not have %s' % (self.name, rev))
  481. def WritesTo(self, ref):
  482. """True if the remote stores to the tracking ref.
  483. """
  484. for spec in self.fetch:
  485. if spec.DestMatches(ref):
  486. return True
  487. return False
  488. def ResetFetch(self, mirror=False):
  489. """Set the fetch refspec to its default value.
  490. """
  491. if mirror:
  492. dst = 'refs/heads/*'
  493. else:
  494. dst = 'refs/remotes/%s/*' % self.name
  495. self.fetch = [RefSpec(True, 'refs/heads/*', dst)]
  496. def Save(self):
  497. """Save this remote to the configuration.
  498. """
  499. self._Set('url', self.url)
  500. self._Set('review', self.review)
  501. self._Set('projectname', self.projectname)
  502. self._Set('fetch', map(lambda x: str(x), self.fetch))
  503. def _Set(self, key, value):
  504. key = 'remote.%s.%s' % (self.name, key)
  505. return self._config.SetString(key, value)
  506. def _Get(self, key, all=False):
  507. key = 'remote.%s.%s' % (self.name, key)
  508. return self._config.GetString(key, all = all)
  509. class Branch(object):
  510. """Configuration options related to a single branch.
  511. """
  512. def __init__(self, config, name):
  513. self._config = config
  514. self.name = name
  515. self.merge = self._Get('merge')
  516. r = self._Get('remote')
  517. if r:
  518. self.remote = self._config.GetRemote(r)
  519. else:
  520. self.remote = None
  521. @property
  522. def LocalMerge(self):
  523. """Convert the merge spec to a local name.
  524. """
  525. if self.remote and self.merge:
  526. return self.remote.ToLocal(self.merge)
  527. return None
  528. def Save(self):
  529. """Save this branch back into the configuration.
  530. """
  531. if self._config.HasSection('branch', self.name):
  532. if self.remote:
  533. self._Set('remote', self.remote.name)
  534. else:
  535. self._Set('remote', None)
  536. self._Set('merge', self.merge)
  537. else:
  538. fd = open(self._config.file, 'ab')
  539. try:
  540. fd.write('[branch "%s"]\n' % self.name)
  541. if self.remote:
  542. fd.write('\tremote = %s\n' % self.remote.name)
  543. if self.merge:
  544. fd.write('\tmerge = %s\n' % self.merge)
  545. finally:
  546. fd.close()
  547. def _Set(self, key, value):
  548. key = 'branch.%s.%s' % (self.name, key)
  549. return self._config.SetString(key, value)
  550. def _Get(self, key, all=False):
  551. key = 'branch.%s.%s' % (self.name, key)
  552. return self._config.GetString(key, all = all)