git_config.py 22 KB

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