git_config.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729
  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. try:
  21. import threading as _threading
  22. except ImportError:
  23. import dummy_threading as _threading
  24. import time
  25. import urllib2
  26. from signal import SIGTERM
  27. from error import GitError, UploadError
  28. from trace import Trace
  29. from git_command import GitCommand
  30. from git_command import ssh_sock
  31. from git_command import terminate_ssh_clients
  32. R_HEADS = 'refs/heads/'
  33. R_TAGS = 'refs/tags/'
  34. ID_RE = re.compile('^[0-9a-f]{40}$')
  35. REVIEW_CACHE = dict()
  36. def IsId(rev):
  37. return ID_RE.match(rev)
  38. def _key(name):
  39. parts = name.split('.')
  40. if len(parts) < 2:
  41. return name.lower()
  42. parts[ 0] = parts[ 0].lower()
  43. parts[-1] = parts[-1].lower()
  44. return '.'.join(parts)
  45. class GitConfig(object):
  46. _ForUser = None
  47. @classmethod
  48. def ForUser(cls):
  49. if cls._ForUser is None:
  50. cls._ForUser = cls(file = os.path.expanduser('~/.gitconfig'))
  51. return cls._ForUser
  52. @classmethod
  53. def ForRepository(cls, gitdir, defaults=None):
  54. return cls(file = os.path.join(gitdir, 'config'),
  55. defaults = defaults)
  56. def __init__(self, file, defaults=None, pickleFile=None):
  57. self.file = file
  58. self.defaults = defaults
  59. self._cache_dict = None
  60. self._section_dict = None
  61. self._remotes = {}
  62. self._branches = {}
  63. if pickleFile is None:
  64. self._pickle = os.path.join(
  65. os.path.dirname(self.file),
  66. '.repopickle_' + os.path.basename(self.file))
  67. else:
  68. self._pickle = pickleFile
  69. def Has(self, name, include_defaults = True):
  70. """Return true if this configuration file has the key.
  71. """
  72. if _key(name) in self._cache:
  73. return True
  74. if include_defaults and self.defaults:
  75. return self.defaults.Has(name, include_defaults = True)
  76. return False
  77. def GetBoolean(self, name):
  78. """Returns a boolean from the configuration file.
  79. None : The value was not defined, or is not a boolean.
  80. True : The value was set to true or yes.
  81. False: The value was set to false or no.
  82. """
  83. v = self.GetString(name)
  84. if v is None:
  85. return None
  86. v = v.lower()
  87. if v in ('true', 'yes'):
  88. return True
  89. if v in ('false', 'no'):
  90. return False
  91. return None
  92. def GetString(self, name, all=False):
  93. """Get the first value for a key, or None if it is not defined.
  94. This configuration file is used first, if the key is not
  95. defined or all = True then the defaults are also searched.
  96. """
  97. try:
  98. v = self._cache[_key(name)]
  99. except KeyError:
  100. if self.defaults:
  101. return self.defaults.GetString(name, all = all)
  102. v = []
  103. if not all:
  104. if v:
  105. return v[0]
  106. return None
  107. r = []
  108. r.extend(v)
  109. if self.defaults:
  110. r.extend(self.defaults.GetString(name, all = True))
  111. return r
  112. def SetString(self, name, value):
  113. """Set the value(s) for a key.
  114. Only this configuration file is modified.
  115. The supplied value should be either a string,
  116. or a list of strings (to store multiple values).
  117. """
  118. key = _key(name)
  119. try:
  120. old = self._cache[key]
  121. except KeyError:
  122. old = []
  123. if value is None:
  124. if old:
  125. del self._cache[key]
  126. self._do('--unset-all', name)
  127. elif isinstance(value, list):
  128. if len(value) == 0:
  129. self.SetString(name, None)
  130. elif len(value) == 1:
  131. self.SetString(name, value[0])
  132. elif old != value:
  133. self._cache[key] = 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. elif len(old) != 1 or old[0] != value:
  138. self._cache[key] = [value]
  139. self._do('--replace-all', name, value)
  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. def GetBranch(self, name):
  150. """Get the branch.$name.* configuration values as an object.
  151. """
  152. try:
  153. b = self._branches[name]
  154. except KeyError:
  155. b = Branch(self, name)
  156. self._branches[b.name] = b
  157. return b
  158. def GetSubSections(self, section):
  159. """List all subsection names matching $section.*.*
  160. """
  161. return self._sections.get(section, set())
  162. def HasSection(self, section, subsection = ''):
  163. """Does at least one key in section.subsection exist?
  164. """
  165. try:
  166. return subsection in self._sections[section]
  167. except KeyError:
  168. return False
  169. def UrlInsteadOf(self, url):
  170. """Resolve any url.*.insteadof references.
  171. """
  172. for new_url in self.GetSubSections('url'):
  173. old_url = self.GetString('url.%s.insteadof' % new_url)
  174. if old_url is not None and url.startswith(old_url):
  175. return new_url + url[len(old_url):]
  176. return url
  177. @property
  178. def _sections(self):
  179. d = self._section_dict
  180. if d is None:
  181. d = {}
  182. for name in self._cache.keys():
  183. p = name.split('.')
  184. if 2 == len(p):
  185. section = p[0]
  186. subsect = ''
  187. else:
  188. section = p[0]
  189. subsect = '.'.join(p[1:-1])
  190. if section not in d:
  191. d[section] = set()
  192. d[section].add(subsect)
  193. self._section_dict = d
  194. return d
  195. @property
  196. def _cache(self):
  197. if self._cache_dict is None:
  198. self._cache_dict = self._Read()
  199. return self._cache_dict
  200. def _Read(self):
  201. d = self._ReadPickle()
  202. if d is None:
  203. d = self._ReadGit()
  204. self._SavePickle(d)
  205. return d
  206. def _ReadPickle(self):
  207. try:
  208. if os.path.getmtime(self._pickle) \
  209. <= os.path.getmtime(self.file):
  210. os.remove(self._pickle)
  211. return None
  212. except OSError:
  213. return None
  214. try:
  215. Trace(': unpickle %s', self.file)
  216. fd = open(self._pickle, 'rb')
  217. try:
  218. return cPickle.load(fd)
  219. finally:
  220. fd.close()
  221. except EOFError:
  222. os.remove(self._pickle)
  223. return None
  224. except IOError:
  225. os.remove(self._pickle)
  226. return None
  227. except cPickle.PickleError:
  228. os.remove(self._pickle)
  229. return None
  230. def _SavePickle(self, cache):
  231. try:
  232. fd = open(self._pickle, 'wb')
  233. try:
  234. cPickle.dump(cache, fd, cPickle.HIGHEST_PROTOCOL)
  235. finally:
  236. fd.close()
  237. except IOError:
  238. if os.path.exists(self._pickle):
  239. os.remove(self._pickle)
  240. except cPickle.PickleError:
  241. if os.path.exists(self._pickle):
  242. os.remove(self._pickle)
  243. def _ReadGit(self):
  244. """
  245. Read configuration data from git.
  246. This internal method populates the GitConfig cache.
  247. """
  248. c = {}
  249. d = self._do('--null', '--list')
  250. if d is None:
  251. return c
  252. for line in d.rstrip('\0').split('\0'):
  253. if '\n' in line:
  254. key, val = line.split('\n', 1)
  255. else:
  256. key = line
  257. val = None
  258. if key in c:
  259. c[key].append(val)
  260. else:
  261. c[key] = [val]
  262. return c
  263. def _do(self, *args):
  264. command = ['config', '--file', self.file]
  265. command.extend(args)
  266. p = GitCommand(None,
  267. command,
  268. capture_stdout = True,
  269. capture_stderr = True)
  270. if p.Wait() == 0:
  271. return p.stdout
  272. else:
  273. GitError('git config %s: %s' % (str(args), p.stderr))
  274. class RefSpec(object):
  275. """A Git refspec line, split into its components:
  276. forced: True if the line starts with '+'
  277. src: Left side of the line
  278. dst: Right side of the line
  279. """
  280. @classmethod
  281. def FromString(cls, rs):
  282. lhs, rhs = rs.split(':', 2)
  283. if lhs.startswith('+'):
  284. lhs = lhs[1:]
  285. forced = True
  286. else:
  287. forced = False
  288. return cls(forced, lhs, rhs)
  289. def __init__(self, forced, lhs, rhs):
  290. self.forced = forced
  291. self.src = lhs
  292. self.dst = rhs
  293. def SourceMatches(self, rev):
  294. if self.src:
  295. if rev == self.src:
  296. return True
  297. if self.src.endswith('/*') and rev.startswith(self.src[:-1]):
  298. return True
  299. return False
  300. def DestMatches(self, ref):
  301. if self.dst:
  302. if ref == self.dst:
  303. return True
  304. if self.dst.endswith('/*') and ref.startswith(self.dst[:-1]):
  305. return True
  306. return False
  307. def MapSource(self, rev):
  308. if self.src.endswith('/*'):
  309. return self.dst[:-1] + rev[len(self.src) - 1:]
  310. return self.dst
  311. def __str__(self):
  312. s = ''
  313. if self.forced:
  314. s += '+'
  315. if self.src:
  316. s += self.src
  317. if self.dst:
  318. s += ':'
  319. s += self.dst
  320. return s
  321. _master_processes = []
  322. _master_keys = set()
  323. _ssh_master = True
  324. _master_keys_lock = None
  325. def init_ssh():
  326. """Should be called once at the start of repo to init ssh master handling.
  327. At the moment, all we do is to create our lock.
  328. """
  329. global _master_keys_lock
  330. assert _master_keys_lock is None, "Should only call init_ssh once"
  331. _master_keys_lock = _threading.Lock()
  332. def _open_ssh(host, port=None):
  333. global _ssh_master
  334. # Acquire the lock. This is needed to prevent opening multiple masters for
  335. # the same host when we're running "repo sync -jN" (for N > 1) _and_ the
  336. # manifest <remote fetch="ssh://xyz"> specifies a different host from the
  337. # one that was passed to repo init.
  338. _master_keys_lock.acquire()
  339. try:
  340. # Check to see whether we already think that the master is running; if we
  341. # think it's already running, return right away.
  342. if port is not None:
  343. key = '%s:%s' % (host, port)
  344. else:
  345. key = host
  346. if key in _master_keys:
  347. return True
  348. if not _ssh_master \
  349. or 'GIT_SSH' in os.environ \
  350. or sys.platform in ('win32', 'cygwin'):
  351. # failed earlier, or cygwin ssh can't do this
  352. #
  353. return False
  354. # We will make two calls to ssh; this is the common part of both calls.
  355. command_base = ['ssh',
  356. '-o','ControlPath %s' % ssh_sock(),
  357. host]
  358. if port is not None:
  359. command_base[1:1] = ['-p',str(port)]
  360. # Since the key wasn't in _master_keys, we think that master isn't running.
  361. # ...but before actually starting a master, we'll double-check. This can
  362. # be important because we can't tell that that 'git@myhost.com' is the same
  363. # as 'myhost.com' where "User git" is setup in the user's ~/.ssh/config file.
  364. check_command = command_base + ['-O','check']
  365. try:
  366. Trace(': %s', ' '.join(check_command))
  367. check_process = subprocess.Popen(check_command,
  368. stdout=subprocess.PIPE,
  369. stderr=subprocess.PIPE)
  370. check_process.communicate() # read output, but ignore it...
  371. isnt_running = check_process.wait()
  372. if not isnt_running:
  373. # Our double-check found that the master _was_ infact running. Add to
  374. # the list of keys.
  375. _master_keys.add(key)
  376. return True
  377. except Exception:
  378. # Ignore excpetions. We we will fall back to the normal command and print
  379. # to the log there.
  380. pass
  381. command = command_base[:1] + \
  382. ['-M', '-N'] + \
  383. command_base[1:]
  384. try:
  385. Trace(': %s', ' '.join(command))
  386. p = subprocess.Popen(command)
  387. except Exception, e:
  388. _ssh_master = False
  389. print >>sys.stderr, \
  390. '\nwarn: cannot enable ssh control master for %s:%s\n%s' \
  391. % (host,port, str(e))
  392. return False
  393. _master_processes.append(p)
  394. _master_keys.add(key)
  395. time.sleep(1)
  396. return True
  397. finally:
  398. _master_keys_lock.release()
  399. def close_ssh():
  400. global _master_keys_lock
  401. terminate_ssh_clients()
  402. for p in _master_processes:
  403. try:
  404. os.kill(p.pid, SIGTERM)
  405. p.wait()
  406. except OSError:
  407. pass
  408. del _master_processes[:]
  409. _master_keys.clear()
  410. d = ssh_sock(create=False)
  411. if d:
  412. try:
  413. os.rmdir(os.path.dirname(d))
  414. except OSError:
  415. pass
  416. # We're done with the lock, so we can delete it.
  417. _master_keys_lock = None
  418. URI_SCP = re.compile(r'^([^@:]*@?[^:/]{1,}):')
  419. URI_ALL = re.compile(r'^([a-z][a-z+]*)://([^@/]*@?[^/]*)/')
  420. def GetSchemeFromUrl(url):
  421. m = URI_ALL.match(url)
  422. if m:
  423. return m.group(1)
  424. return None
  425. def _preconnect(url):
  426. m = URI_ALL.match(url)
  427. if m:
  428. scheme = m.group(1)
  429. host = m.group(2)
  430. if ':' in host:
  431. host, port = host.split(':')
  432. else:
  433. port = None
  434. if scheme in ('ssh', 'git+ssh', 'ssh+git'):
  435. return _open_ssh(host, port)
  436. return False
  437. m = URI_SCP.match(url)
  438. if m:
  439. host = m.group(1)
  440. return _open_ssh(host)
  441. return False
  442. class Remote(object):
  443. """Configuration options related to a remote.
  444. """
  445. def __init__(self, config, name):
  446. self._config = config
  447. self.name = name
  448. self.url = self._Get('url')
  449. self.review = self._Get('review')
  450. self.projectname = self._Get('projectname')
  451. self.fetch = map(lambda x: RefSpec.FromString(x),
  452. self._Get('fetch', all=True))
  453. self._review_protocol = None
  454. def _InsteadOf(self):
  455. globCfg = GitConfig.ForUser()
  456. urlList = globCfg.GetSubSections('url')
  457. longest = ""
  458. longestUrl = ""
  459. for url in urlList:
  460. key = "url." + url + ".insteadOf"
  461. insteadOfList = globCfg.GetString(key, all=True)
  462. for insteadOf in insteadOfList:
  463. if self.url.startswith(insteadOf) \
  464. and len(insteadOf) > len(longest):
  465. longest = insteadOf
  466. longestUrl = url
  467. if len(longest) == 0:
  468. return self.url
  469. return self.url.replace(longest, longestUrl, 1)
  470. def PreConnectFetch(self):
  471. connectionUrl = self._InsteadOf()
  472. return _preconnect(connectionUrl)
  473. @property
  474. def ReviewProtocol(self):
  475. if self._review_protocol is None:
  476. if self.review is None:
  477. return None
  478. u = self.review
  479. if not u.startswith('http:') and not u.startswith('https:'):
  480. u = 'http://%s' % u
  481. if u.endswith('/Gerrit'):
  482. u = u[:len(u) - len('/Gerrit')]
  483. if not u.endswith('/ssh_info'):
  484. if not u.endswith('/'):
  485. u += '/'
  486. u += 'ssh_info'
  487. if u in REVIEW_CACHE:
  488. info = REVIEW_CACHE[u]
  489. self._review_protocol = info[0]
  490. self._review_host = info[1]
  491. self._review_port = info[2]
  492. elif 'REPO_HOST_PORT_INFO' in os.environ:
  493. info = os.environ['REPO_HOST_PORT_INFO']
  494. self._review_protocol = 'ssh'
  495. self._review_host = info.split(" ")[0]
  496. self._review_port = info.split(" ")[1]
  497. REVIEW_CACHE[u] = (
  498. self._review_protocol,
  499. self._review_host,
  500. self._review_port)
  501. else:
  502. try:
  503. info = urllib2.urlopen(u).read()
  504. if info == 'NOT_AVAILABLE':
  505. raise UploadError('%s: SSH disabled' % self.review)
  506. if '<' in info:
  507. # Assume the server gave us some sort of HTML
  508. # response back, like maybe a login page.
  509. #
  510. raise UploadError('%s: Cannot parse response' % u)
  511. self._review_protocol = 'ssh'
  512. self._review_host = info.split(" ")[0]
  513. self._review_port = info.split(" ")[1]
  514. except urllib2.HTTPError, e:
  515. if e.code == 404:
  516. self._review_protocol = 'http-post'
  517. self._review_host = None
  518. self._review_port = None
  519. else:
  520. raise UploadError('Upload over SSH unavailable')
  521. except urllib2.URLError, e:
  522. raise UploadError('%s: %s' % (self.review, str(e)))
  523. REVIEW_CACHE[u] = (
  524. self._review_protocol,
  525. self._review_host,
  526. self._review_port)
  527. return self._review_protocol
  528. def SshReviewUrl(self, userEmail):
  529. if self.ReviewProtocol != 'ssh':
  530. return None
  531. username = self._config.GetString('review.%s.username' % self.review)
  532. if username is None:
  533. username = userEmail.split("@")[0]
  534. return 'ssh://%s@%s:%s/%s' % (
  535. username,
  536. self._review_host,
  537. self._review_port,
  538. self.projectname)
  539. def ToLocal(self, rev):
  540. """Convert a remote revision string to something we have locally.
  541. """
  542. if IsId(rev):
  543. return rev
  544. if rev.startswith(R_TAGS):
  545. return rev
  546. if not rev.startswith('refs/'):
  547. rev = R_HEADS + rev
  548. for spec in self.fetch:
  549. if spec.SourceMatches(rev):
  550. return spec.MapSource(rev)
  551. raise GitError('remote %s does not have %s' % (self.name, rev))
  552. def WritesTo(self, ref):
  553. """True if the remote stores to the tracking ref.
  554. """
  555. for spec in self.fetch:
  556. if spec.DestMatches(ref):
  557. return True
  558. return False
  559. def ResetFetch(self, mirror=False):
  560. """Set the fetch refspec to its default value.
  561. """
  562. if mirror:
  563. dst = 'refs/heads/*'
  564. else:
  565. dst = 'refs/remotes/%s/*' % self.name
  566. self.fetch = [RefSpec(True, 'refs/heads/*', dst)]
  567. def Save(self):
  568. """Save this remote to the configuration.
  569. """
  570. self._Set('url', self.url)
  571. self._Set('review', self.review)
  572. self._Set('projectname', self.projectname)
  573. self._Set('fetch', map(lambda x: str(x), self.fetch))
  574. def _Set(self, key, value):
  575. key = 'remote.%s.%s' % (self.name, key)
  576. return self._config.SetString(key, value)
  577. def _Get(self, key, all=False):
  578. key = 'remote.%s.%s' % (self.name, key)
  579. return self._config.GetString(key, all = all)
  580. class Branch(object):
  581. """Configuration options related to a single branch.
  582. """
  583. def __init__(self, config, name):
  584. self._config = config
  585. self.name = name
  586. self.merge = self._Get('merge')
  587. r = self._Get('remote')
  588. if r:
  589. self.remote = self._config.GetRemote(r)
  590. else:
  591. self.remote = None
  592. @property
  593. def LocalMerge(self):
  594. """Convert the merge spec to a local name.
  595. """
  596. if self.remote and self.merge:
  597. return self.remote.ToLocal(self.merge)
  598. return None
  599. def Save(self):
  600. """Save this branch back into the configuration.
  601. """
  602. if self._config.HasSection('branch', self.name):
  603. if self.remote:
  604. self._Set('remote', self.remote.name)
  605. else:
  606. self._Set('remote', None)
  607. self._Set('merge', self.merge)
  608. else:
  609. fd = open(self._config.file, 'ab')
  610. try:
  611. fd.write('[branch "%s"]\n' % self.name)
  612. if self.remote:
  613. fd.write('\tremote = %s\n' % self.remote.name)
  614. if self.merge:
  615. fd.write('\tmerge = %s\n' % self.merge)
  616. finally:
  617. fd.close()
  618. def _Set(self, key, value):
  619. key = 'branch.%s.%s' % (self.name, key)
  620. return self._config.SetString(key, value)
  621. def _Get(self, key, all=False):
  622. key = 'branch.%s.%s' % (self.name, key)
  623. return self._config.GetString(key, all = all)