git_config.py 10 KB

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