git_config.py 9.7 KB

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