git_config.py 19 KB

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