git_config.py 21 KB

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