PageRenderTime 52ms CodeModel.GetById 11ms RepoModel.GetById 1ms app.codeStats 0ms

/mercurial/changelog.py

https://bitbucket.org/mirror/mercurial/
Python | 352 lines | 312 code | 19 blank | 21 comment | 18 complexity | a93a3556bf61e7293f7a2620f9603f00 MD5 | raw file
Possible License(s): GPL-2.0
  1. # changelog.py - changelog class for mercurial
  2. #
  3. # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
  4. #
  5. # This software may be used and distributed according to the terms of the
  6. # GNU General Public License version 2 or any later version.
  7. from node import bin, hex, nullid
  8. from i18n import _
  9. import util, error, revlog, encoding
  10. _defaultextra = {'branch': 'default'}
  11. def _string_escape(text):
  12. """
  13. >>> d = {'nl': chr(10), 'bs': chr(92), 'cr': chr(13), 'nul': chr(0)}
  14. >>> s = "ab%(nl)scd%(bs)s%(bs)sn%(nul)sab%(cr)scd%(bs)s%(nl)s" % d
  15. >>> s
  16. 'ab\\ncd\\\\\\\\n\\x00ab\\rcd\\\\\\n'
  17. >>> res = _string_escape(s)
  18. >>> s == res.decode('string_escape')
  19. True
  20. """
  21. # subset of the string_escape codec
  22. text = text.replace('\\', '\\\\').replace('\n', '\\n').replace('\r', '\\r')
  23. return text.replace('\0', '\\0')
  24. def decodeextra(text):
  25. """
  26. >>> sorted(decodeextra(encodeextra({'foo': 'bar', 'baz': chr(0) + '2'})
  27. ... ).iteritems())
  28. [('baz', '\\x002'), ('branch', 'default'), ('foo', 'bar')]
  29. >>> sorted(decodeextra(encodeextra({'foo': 'bar',
  30. ... 'baz': chr(92) + chr(0) + '2'})
  31. ... ).iteritems())
  32. [('baz', '\\\\\\x002'), ('branch', 'default'), ('foo', 'bar')]
  33. """
  34. extra = _defaultextra.copy()
  35. for l in text.split('\0'):
  36. if l:
  37. if '\\0' in l:
  38. # fix up \0 without getting into trouble with \\0
  39. l = l.replace('\\\\', '\\\\\n')
  40. l = l.replace('\\0', '\0')
  41. l = l.replace('\n', '')
  42. k, v = l.decode('string_escape').split(':', 1)
  43. extra[k] = v
  44. return extra
  45. def encodeextra(d):
  46. # keys must be sorted to produce a deterministic changelog entry
  47. items = [_string_escape('%s:%s' % (k, d[k])) for k in sorted(d)]
  48. return "\0".join(items)
  49. def stripdesc(desc):
  50. """strip trailing whitespace and leading and trailing empty lines"""
  51. return '\n'.join([l.rstrip() for l in desc.splitlines()]).strip('\n')
  52. class appender(object):
  53. '''the changelog index must be updated last on disk, so we use this class
  54. to delay writes to it'''
  55. def __init__(self, vfs, name, mode, buf):
  56. self.data = buf
  57. fp = vfs(name, mode)
  58. self.fp = fp
  59. self.offset = fp.tell()
  60. self.size = vfs.fstat(fp).st_size
  61. def end(self):
  62. return self.size + len("".join(self.data))
  63. def tell(self):
  64. return self.offset
  65. def flush(self):
  66. pass
  67. def close(self):
  68. self.fp.close()
  69. def seek(self, offset, whence=0):
  70. '''virtual file offset spans real file and data'''
  71. if whence == 0:
  72. self.offset = offset
  73. elif whence == 1:
  74. self.offset += offset
  75. elif whence == 2:
  76. self.offset = self.end() + offset
  77. if self.offset < self.size:
  78. self.fp.seek(self.offset)
  79. def read(self, count=-1):
  80. '''only trick here is reads that span real file and data'''
  81. ret = ""
  82. if self.offset < self.size:
  83. s = self.fp.read(count)
  84. ret = s
  85. self.offset += len(s)
  86. if count > 0:
  87. count -= len(s)
  88. if count != 0:
  89. doff = self.offset - self.size
  90. self.data.insert(0, "".join(self.data))
  91. del self.data[1:]
  92. s = self.data[0][doff:doff + count]
  93. self.offset += len(s)
  94. ret += s
  95. return ret
  96. def write(self, s):
  97. self.data.append(str(s))
  98. self.offset += len(s)
  99. def delayopener(opener, target, divert, buf):
  100. def o(name, mode='r'):
  101. if name != target:
  102. return opener(name, mode)
  103. if divert:
  104. return opener(name + ".a", mode.replace('a', 'w'))
  105. # otherwise, divert to memory
  106. return appender(opener, name, mode, buf)
  107. return o
  108. class changelog(revlog.revlog):
  109. def __init__(self, opener):
  110. revlog.revlog.__init__(self, opener, "00changelog.i")
  111. if self._initempty:
  112. # changelogs don't benefit from generaldelta
  113. self.version &= ~revlog.REVLOGGENERALDELTA
  114. self._generaldelta = False
  115. self._realopener = opener
  116. self._delayed = False
  117. self._delaybuf = []
  118. self._divert = False
  119. self.filteredrevs = frozenset()
  120. def tip(self):
  121. """filtered version of revlog.tip"""
  122. for i in xrange(len(self) -1, -2, -1):
  123. if i not in self.filteredrevs:
  124. return self.node(i)
  125. def __iter__(self):
  126. """filtered version of revlog.__iter__"""
  127. if len(self.filteredrevs) == 0:
  128. return revlog.revlog.__iter__(self)
  129. def filterediter():
  130. for i in xrange(len(self)):
  131. if i not in self.filteredrevs:
  132. yield i
  133. return filterediter()
  134. def revs(self, start=0, stop=None):
  135. """filtered version of revlog.revs"""
  136. for i in super(changelog, self).revs(start, stop):
  137. if i not in self.filteredrevs:
  138. yield i
  139. @util.propertycache
  140. def nodemap(self):
  141. # XXX need filtering too
  142. self.rev(self.node(0))
  143. return self._nodecache
  144. def hasnode(self, node):
  145. """filtered version of revlog.hasnode"""
  146. try:
  147. i = self.rev(node)
  148. return i not in self.filteredrevs
  149. except KeyError:
  150. return False
  151. def headrevs(self):
  152. if self.filteredrevs:
  153. # XXX we should fix and use the C version
  154. return self._headrevs()
  155. return super(changelog, self).headrevs()
  156. def strip(self, *args, **kwargs):
  157. # XXX make something better than assert
  158. # We can't expect proper strip behavior if we are filtered.
  159. assert not self.filteredrevs
  160. super(changelog, self).strip(*args, **kwargs)
  161. def rev(self, node):
  162. """filtered version of revlog.rev"""
  163. r = super(changelog, self).rev(node)
  164. if r in self.filteredrevs:
  165. raise error.LookupError(hex(node), self.indexfile, _('no node'))
  166. return r
  167. def node(self, rev):
  168. """filtered version of revlog.node"""
  169. if rev in self.filteredrevs:
  170. raise IndexError(rev)
  171. return super(changelog, self).node(rev)
  172. def linkrev(self, rev):
  173. """filtered version of revlog.linkrev"""
  174. if rev in self.filteredrevs:
  175. raise IndexError(rev)
  176. return super(changelog, self).linkrev(rev)
  177. def parentrevs(self, rev):
  178. """filtered version of revlog.parentrevs"""
  179. if rev in self.filteredrevs:
  180. raise IndexError(rev)
  181. return super(changelog, self).parentrevs(rev)
  182. def flags(self, rev):
  183. """filtered version of revlog.flags"""
  184. if rev in self.filteredrevs:
  185. raise IndexError(rev)
  186. return super(changelog, self).flags(rev)
  187. def delayupdate(self):
  188. "delay visibility of index updates to other readers"
  189. self._delayed = True
  190. self._divert = (len(self) == 0)
  191. self._delaybuf = []
  192. self.opener = delayopener(self._realopener, self.indexfile,
  193. self._divert, self._delaybuf)
  194. def finalize(self, tr):
  195. "finalize index updates"
  196. self._delayed = False
  197. self.opener = self._realopener
  198. # move redirected index data back into place
  199. if self._divert:
  200. tmpname = self.indexfile + ".a"
  201. nfile = self.opener.open(tmpname)
  202. nfile.close()
  203. self.opener.rename(tmpname, self.indexfile)
  204. elif self._delaybuf:
  205. fp = self.opener(self.indexfile, 'a')
  206. fp.write("".join(self._delaybuf))
  207. fp.close()
  208. self._delaybuf = []
  209. # split when we're done
  210. self.checkinlinesize(tr)
  211. def readpending(self, file):
  212. r = revlog.revlog(self.opener, file)
  213. self.index = r.index
  214. self.nodemap = r.nodemap
  215. self._nodecache = r._nodecache
  216. self._chunkcache = r._chunkcache
  217. def writepending(self):
  218. "create a file containing the unfinalized state for pretxnchangegroup"
  219. if self._delaybuf:
  220. # make a temporary copy of the index
  221. fp1 = self._realopener(self.indexfile)
  222. fp2 = self._realopener(self.indexfile + ".a", "w")
  223. fp2.write(fp1.read())
  224. # add pending data
  225. fp2.write("".join(self._delaybuf))
  226. fp2.close()
  227. # switch modes so finalize can simply rename
  228. self._delaybuf = []
  229. self._divert = True
  230. if self._divert:
  231. return True
  232. return False
  233. def checkinlinesize(self, tr, fp=None):
  234. if not self._delayed:
  235. revlog.revlog.checkinlinesize(self, tr, fp)
  236. def read(self, node):
  237. """
  238. format used:
  239. nodeid\n : manifest node in ascii
  240. user\n : user, no \n or \r allowed
  241. time tz extra\n : date (time is int or float, timezone is int)
  242. : extra is metadata, encoded and separated by '\0'
  243. : older versions ignore it
  244. files\n\n : files modified by the cset, no \n or \r allowed
  245. (.*) : comment (free text, ideally utf-8)
  246. changelog v0 doesn't use extra
  247. """
  248. text = self.revision(node)
  249. if not text:
  250. return (nullid, "", (0, 0), [], "", _defaultextra)
  251. last = text.index("\n\n")
  252. desc = encoding.tolocal(text[last + 2:])
  253. l = text[:last].split('\n')
  254. manifest = bin(l[0])
  255. user = encoding.tolocal(l[1])
  256. tdata = l[2].split(' ', 2)
  257. if len(tdata) != 3:
  258. time = float(tdata[0])
  259. try:
  260. # various tools did silly things with the time zone field.
  261. timezone = int(tdata[1])
  262. except ValueError:
  263. timezone = 0
  264. extra = _defaultextra
  265. else:
  266. time, timezone = float(tdata[0]), int(tdata[1])
  267. extra = decodeextra(tdata[2])
  268. files = l[3:]
  269. return (manifest, user, (time, timezone), files, desc, extra)
  270. def add(self, manifest, files, desc, transaction, p1, p2,
  271. user, date=None, extra=None):
  272. # Convert to UTF-8 encoded bytestrings as the very first
  273. # thing: calling any method on a localstr object will turn it
  274. # into a str object and the cached UTF-8 string is thus lost.
  275. user, desc = encoding.fromlocal(user), encoding.fromlocal(desc)
  276. user = user.strip()
  277. # An empty username or a username with a "\n" will make the
  278. # revision text contain two "\n\n" sequences -> corrupt
  279. # repository since read cannot unpack the revision.
  280. if not user:
  281. raise error.RevlogError(_("empty username"))
  282. if "\n" in user:
  283. raise error.RevlogError(_("username %s contains a newline")
  284. % repr(user))
  285. desc = stripdesc(desc)
  286. if date:
  287. parseddate = "%d %d" % util.parsedate(date)
  288. else:
  289. parseddate = "%d %d" % util.makedate()
  290. if extra:
  291. branch = extra.get("branch")
  292. if branch in ("default", ""):
  293. del extra["branch"]
  294. elif branch in (".", "null", "tip"):
  295. raise error.RevlogError(_('the name \'%s\' is reserved')
  296. % branch)
  297. if extra:
  298. extra = encodeextra(extra)
  299. parseddate = "%s %s" % (parseddate, extra)
  300. l = [hex(manifest), user, parseddate] + sorted(files) + ["", desc]
  301. text = "\n".join(l)
  302. return self.addrevision(text, transaction, len(self), p1, p2)
  303. def branchinfo(self, rev):
  304. """return the branch name and open/close state of a revision
  305. This function exists because creating a changectx object
  306. just to access this is costly."""
  307. extra = self.read(rev)[5]
  308. return encoding.tolocal(extra.get("branch")), 'close' in extra