git_config.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  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. import os
  16. import re
  17. import sys
  18. from error import GitError
  19. from git_command import GitCommand
  20. R_HEADS = 'refs/heads/'
  21. R_TAGS = 'refs/tags/'
  22. ID_RE = re.compile('^[0-9a-f]{40}$')
  23. def IsId(rev):
  24. return ID_RE.match(rev)
  25. class GitConfig(object):
  26. _ForUser = None
  27. @classmethod
  28. def ForUser(cls):
  29. if cls._ForUser is None:
  30. cls._ForUser = cls(file = os.path.expanduser('~/.gitconfig'))
  31. return cls._ForUser
  32. @classmethod
  33. def ForRepository(cls, gitdir, defaults=None):
  34. return cls(file = os.path.join(gitdir, 'config'),
  35. defaults = defaults)
  36. def __init__(self, file, defaults=None):
  37. self.file = file
  38. self.defaults = defaults
  39. self._cache_dict = None
  40. self._remotes = {}
  41. self._branches = {}
  42. def Has(self, name, include_defaults = True):
  43. """Return true if this configuration file has the key.
  44. """
  45. name = name.lower()
  46. if name in self._cache:
  47. return True
  48. if include_defaults and self.defaults:
  49. return self.defaults.Has(name, include_defaults = True)
  50. return False
  51. def GetBoolean(self, name):
  52. """Returns a boolean from the configuration file.
  53. None : The value was not defined, or is not a boolean.
  54. True : The value was set to true or yes.
  55. False: The value was set to false or no.
  56. """
  57. v = self.GetString(name)
  58. if v is None:
  59. return None
  60. v = v.lower()
  61. if v in ('true', 'yes'):
  62. return True
  63. if v in ('false', 'no'):
  64. return False
  65. return None
  66. def GetString(self, name, all=False):
  67. """Get the first value for a key, or None if it is not defined.
  68. This configuration file is used first, if the key is not
  69. defined or all = True then the defaults are also searched.
  70. """
  71. name = name.lower()
  72. try:
  73. v = self._cache[name]
  74. except KeyError:
  75. if self.defaults:
  76. return self.defaults.GetString(name, all = all)
  77. v = []
  78. if not all:
  79. if v:
  80. return v[0]
  81. return None
  82. r = []
  83. r.extend(v)
  84. if self.defaults:
  85. r.extend(self.defaults.GetString(name, all = True))
  86. return r
  87. def SetString(self, name, value):
  88. """Set the value(s) for a key.
  89. Only this configuration file is modified.
  90. The supplied value should be either a string,
  91. or a list of strings (to store multiple values).
  92. """
  93. name = name.lower()
  94. try:
  95. old = self._cache[name]
  96. except KeyError:
  97. old = []
  98. if value is None:
  99. if old:
  100. del self._cache[name]
  101. self._do('--unset-all', name)
  102. elif isinstance(value, list):
  103. if len(value) == 0:
  104. self.SetString(name, None)
  105. elif len(value) == 1:
  106. self.SetString(name, value[0])
  107. elif old != value:
  108. self._cache[name] = list(value)
  109. self._do('--replace-all', name, value[0])
  110. for i in xrange(1, len(value)):
  111. self._do('--add', name, value[i])
  112. elif len(old) != 1 or old[0] != value:
  113. self._cache[name] = [value]
  114. self._do('--replace-all', name, value)
  115. def GetRemote(self, name):
  116. """Get the remote.$name.* configuration values as an object.
  117. """
  118. try:
  119. r = self._remotes[name]
  120. except KeyError:
  121. r = Remote(self, name)
  122. self._remotes[r.name] = r
  123. return r
  124. def GetBranch(self, name):
  125. """Get the branch.$name.* configuration values as an object.
  126. """
  127. try:
  128. b = self._branches[name]
  129. except KeyError:
  130. b = Branch(self, name)
  131. self._branches[b.name] = b
  132. return b
  133. @property
  134. def _cache(self):
  135. if self._cache_dict is None:
  136. self._cache_dict = self._Read()
  137. return self._cache_dict
  138. def _Read(self):
  139. d = self._do('--null', '--list')
  140. c = {}
  141. while d:
  142. lf = d.index('\n')
  143. nul = d.index('\0', lf + 1)
  144. key = d[0:lf]
  145. val = d[lf + 1:nul]
  146. if key in c:
  147. c[key].append(val)
  148. else:
  149. c[key] = [val]
  150. d = d[nul + 1:]
  151. return c
  152. def _do(self, *args):
  153. command = ['config', '--file', self.file]
  154. command.extend(args)
  155. p = GitCommand(None,
  156. command,
  157. capture_stdout = True,
  158. capture_stderr = True)
  159. if p.Wait() == 0:
  160. return p.stdout
  161. else:
  162. GitError('git config %s: %s' % (str(args), p.stderr))
  163. class RefSpec(object):
  164. """A Git refspec line, split into its components:
  165. forced: True if the line starts with '+'
  166. src: Left side of the line
  167. dst: Right side of the line
  168. """
  169. @classmethod
  170. def FromString(cls, rs):
  171. lhs, rhs = rs.split(':', 2)
  172. if lhs.startswith('+'):
  173. lhs = lhs[1:]
  174. forced = True
  175. else:
  176. forced = False
  177. return cls(forced, lhs, rhs)
  178. def __init__(self, forced, lhs, rhs):
  179. self.forced = forced
  180. self.src = lhs
  181. self.dst = rhs
  182. def SourceMatches(self, rev):
  183. if self.src:
  184. if rev == self.src:
  185. return True
  186. if self.src.endswith('/*') and rev.startswith(self.src[:-1]):
  187. return True
  188. return False
  189. def DestMatches(self, ref):
  190. if self.dst:
  191. if ref == self.dst:
  192. return True
  193. if self.dst.endswith('/*') and ref.startswith(self.dst[:-1]):
  194. return True
  195. return False
  196. def MapSource(self, rev):
  197. if self.src.endswith('/*'):
  198. return self.dst[:-1] + rev[len(self.src) - 1:]
  199. return self.dst
  200. def __str__(self):
  201. s = ''
  202. if self.forced:
  203. s += '+'
  204. if self.src:
  205. s += self.src
  206. if self.dst:
  207. s += ':'
  208. s += self.dst
  209. return s
  210. class Remote(object):
  211. """Configuration options related to a remote.
  212. """
  213. def __init__(self, config, name):
  214. self._config = config
  215. self.name = name
  216. self.url = self._Get('url')
  217. self.review = self._Get('review')
  218. self.fetch = map(lambda x: RefSpec.FromString(x),
  219. self._Get('fetch', all=True))
  220. def ToLocal(self, rev):
  221. """Convert a remote revision string to something we have locally.
  222. """
  223. if IsId(rev):
  224. return rev
  225. if rev.startswith(R_TAGS):
  226. return rev
  227. if not rev.startswith('refs/'):
  228. rev = R_HEADS + rev
  229. for spec in self.fetch:
  230. if spec.SourceMatches(rev):
  231. return spec.MapSource(rev)
  232. raise GitError('remote %s does not have %s' % (self.name, rev))
  233. def WritesTo(self, ref):
  234. """True if the remote stores to the tracking ref.
  235. """
  236. for spec in self.fetch:
  237. if spec.DestMatches(ref):
  238. return True
  239. return False
  240. def ResetFetch(self):
  241. """Set the fetch refspec to its default value.
  242. """
  243. self.fetch = [RefSpec(True,
  244. 'refs/heads/*',
  245. 'refs/remotes/%s/*' % self.name)]
  246. def Save(self):
  247. """Save this remote to the configuration.
  248. """
  249. self._Set('url', self.url)
  250. self._Set('review', self.review)
  251. self._Set('fetch', map(lambda x: str(x), self.fetch))
  252. def _Set(self, key, value):
  253. key = 'remote.%s.%s' % (self.name, key)
  254. return self._config.SetString(key, value)
  255. def _Get(self, key, all=False):
  256. key = 'remote.%s.%s' % (self.name, key)
  257. return self._config.GetString(key, all = all)
  258. class Branch(object):
  259. """Configuration options related to a single branch.
  260. """
  261. def __init__(self, config, name):
  262. self._config = config
  263. self.name = name
  264. self.merge = self._Get('merge')
  265. r = self._Get('remote')
  266. if r:
  267. self.remote = self._config.GetRemote(r)
  268. else:
  269. self.remote = None
  270. @property
  271. def LocalMerge(self):
  272. """Convert the merge spec to a local name.
  273. """
  274. if self.remote and self.merge:
  275. return self.remote.ToLocal(self.merge)
  276. return None
  277. def Save(self):
  278. """Save this branch back into the configuration.
  279. """
  280. self._Set('merge', self.merge)
  281. if self.remote:
  282. self._Set('remote', self.remote.name)
  283. else:
  284. self._Set('remote', None)
  285. def _Set(self, key, value):
  286. key = 'branch.%s.%s' % (self.name, key)
  287. return self._config.SetString(key, value)
  288. def _Get(self, key, all=False):
  289. key = 'branch.%s.%s' % (self.name, key)
  290. return self._config.GetString(key, all = all)