git_config.py 19 KB

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