git_config.py 11 KB

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