git_config.py 22 KB

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