##// END OF EJS Templates
changelog: add class to represent parsed changelog revisions...
Gregory Szorc -
r28487:98d98a64 default
parent child Browse files
Show More
@@ -1,417 +1,482 b''
1 1 # changelog.py - changelog class for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 import collections
11
10 12 from .i18n import _
11 13 from .node import (
12 14 bin,
13 15 hex,
14 16 nullid,
15 17 )
16 18
17 19 from . import (
18 20 encoding,
19 21 error,
20 22 revlog,
21 23 util,
22 24 )
23 25
24 26 _defaultextra = {'branch': 'default'}
25 27
26 28 def _string_escape(text):
27 29 """
28 30 >>> d = {'nl': chr(10), 'bs': chr(92), 'cr': chr(13), 'nul': chr(0)}
29 31 >>> s = "ab%(nl)scd%(bs)s%(bs)sn%(nul)sab%(cr)scd%(bs)s%(nl)s" % d
30 32 >>> s
31 33 'ab\\ncd\\\\\\\\n\\x00ab\\rcd\\\\\\n'
32 34 >>> res = _string_escape(s)
33 35 >>> s == res.decode('string_escape')
34 36 True
35 37 """
36 38 # subset of the string_escape codec
37 39 text = text.replace('\\', '\\\\').replace('\n', '\\n').replace('\r', '\\r')
38 40 return text.replace('\0', '\\0')
39 41
40 42 def decodeextra(text):
41 43 """
42 44 >>> sorted(decodeextra(encodeextra({'foo': 'bar', 'baz': chr(0) + '2'})
43 45 ... ).iteritems())
44 46 [('baz', '\\x002'), ('branch', 'default'), ('foo', 'bar')]
45 47 >>> sorted(decodeextra(encodeextra({'foo': 'bar',
46 48 ... 'baz': chr(92) + chr(0) + '2'})
47 49 ... ).iteritems())
48 50 [('baz', '\\\\\\x002'), ('branch', 'default'), ('foo', 'bar')]
49 51 """
50 52 extra = _defaultextra.copy()
51 53 for l in text.split('\0'):
52 54 if l:
53 55 if '\\0' in l:
54 56 # fix up \0 without getting into trouble with \\0
55 57 l = l.replace('\\\\', '\\\\\n')
56 58 l = l.replace('\\0', '\0')
57 59 l = l.replace('\n', '')
58 60 k, v = l.decode('string_escape').split(':', 1)
59 61 extra[k] = v
60 62 return extra
61 63
62 64 def encodeextra(d):
63 65 # keys must be sorted to produce a deterministic changelog entry
64 66 items = [_string_escape('%s:%s' % (k, d[k])) for k in sorted(d)]
65 67 return "\0".join(items)
66 68
67 69 def stripdesc(desc):
68 70 """strip trailing whitespace and leading and trailing empty lines"""
69 71 return '\n'.join([l.rstrip() for l in desc.splitlines()]).strip('\n')
70 72
71 73 class appender(object):
72 74 '''the changelog index must be updated last on disk, so we use this class
73 75 to delay writes to it'''
74 76 def __init__(self, vfs, name, mode, buf):
75 77 self.data = buf
76 78 fp = vfs(name, mode)
77 79 self.fp = fp
78 80 self.offset = fp.tell()
79 81 self.size = vfs.fstat(fp).st_size
80 82
81 83 def end(self):
82 84 return self.size + len("".join(self.data))
83 85 def tell(self):
84 86 return self.offset
85 87 def flush(self):
86 88 pass
87 89 def close(self):
88 90 self.fp.close()
89 91
90 92 def seek(self, offset, whence=0):
91 93 '''virtual file offset spans real file and data'''
92 94 if whence == 0:
93 95 self.offset = offset
94 96 elif whence == 1:
95 97 self.offset += offset
96 98 elif whence == 2:
97 99 self.offset = self.end() + offset
98 100 if self.offset < self.size:
99 101 self.fp.seek(self.offset)
100 102
101 103 def read(self, count=-1):
102 104 '''only trick here is reads that span real file and data'''
103 105 ret = ""
104 106 if self.offset < self.size:
105 107 s = self.fp.read(count)
106 108 ret = s
107 109 self.offset += len(s)
108 110 if count > 0:
109 111 count -= len(s)
110 112 if count != 0:
111 113 doff = self.offset - self.size
112 114 self.data.insert(0, "".join(self.data))
113 115 del self.data[1:]
114 116 s = self.data[0][doff:doff + count]
115 117 self.offset += len(s)
116 118 ret += s
117 119 return ret
118 120
119 121 def write(self, s):
120 122 self.data.append(str(s))
121 123 self.offset += len(s)
122 124
123 125 def _divertopener(opener, target):
124 126 """build an opener that writes in 'target.a' instead of 'target'"""
125 127 def _divert(name, mode='r'):
126 128 if name != target:
127 129 return opener(name, mode)
128 130 return opener(name + ".a", mode)
129 131 return _divert
130 132
131 133 def _delayopener(opener, target, buf):
132 134 """build an opener that stores chunks in 'buf' instead of 'target'"""
133 135 def _delay(name, mode='r'):
134 136 if name != target:
135 137 return opener(name, mode)
136 138 return appender(opener, name, mode, buf)
137 139 return _delay
138 140
141 _changelogrevision = collections.namedtuple('changelogrevision',
142 ('manifest', 'user', 'date',
143 'files', 'description', 'extra'))
144
145 class changelogrevision(object):
146 """Holds results of a parsed changelog revision.
147
148 Changelog revisions consist of multiple pieces of data, including
149 the manifest node, user, and date. This object exposes a view into
150 the parsed object.
151 """
152
153 __slots__ = (
154 'date',
155 'description',
156 'extra',
157 'files',
158 'manifest',
159 'user',
160 )
161
162 def __new__(cls, text):
163 if not text:
164 return _changelogrevision(
165 manifest=nullid,
166 user='',
167 date=(0, 0),
168 files=[],
169 description='',
170 extra=_defaultextra,
171 )
172
173 self = super(changelogrevision, cls).__new__(cls)
174 # We could return here and implement the following as an __init__.
175 # But doing it here is equivalent and saves an extra function call.
176
177 # format used:
178 # nodeid\n : manifest node in ascii
179 # user\n : user, no \n or \r allowed
180 # time tz extra\n : date (time is int or float, timezone is int)
181 # : extra is metadata, encoded and separated by '\0'
182 # : older versions ignore it
183 # files\n\n : files modified by the cset, no \n or \r allowed
184 # (.*) : comment (free text, ideally utf-8)
185 #
186 # changelog v0 doesn't use extra
187
188 last = text.index("\n\n")
189 self.description = encoding.tolocal(text[last + 2:])
190 l = text[:last].split('\n')
191 self.manifest = bin(l[0])
192 self.user = encoding.tolocal(l[1])
193
194 tdata = l[2].split(' ', 2)
195 if len(tdata) != 3:
196 time = float(tdata[0])
197 try:
198 # various tools did silly things with the time zone field.
199 timezone = int(tdata[1])
200 except ValueError:
201 timezone = 0
202 self.extra = _defaultextra
203 else:
204 time, timezone = float(tdata[0]), int(tdata[1])
205 self.extra = decodeextra(tdata[2])
206
207 self.date = (time, timezone)
208 self.files = l[3:]
209
210 return self
211
139 212 class changelog(revlog.revlog):
140 213 def __init__(self, opener):
141 214 revlog.revlog.__init__(self, opener, "00changelog.i")
142 215 if self._initempty:
143 216 # changelogs don't benefit from generaldelta
144 217 self.version &= ~revlog.REVLOGGENERALDELTA
145 218 self._generaldelta = False
146 219 self._realopener = opener
147 220 self._delayed = False
148 221 self._delaybuf = None
149 222 self._divert = False
150 223 self.filteredrevs = frozenset()
151 224
152 225 def tip(self):
153 226 """filtered version of revlog.tip"""
154 227 for i in xrange(len(self) -1, -2, -1):
155 228 if i not in self.filteredrevs:
156 229 return self.node(i)
157 230
158 231 def __contains__(self, rev):
159 232 """filtered version of revlog.__contains__"""
160 233 return (0 <= rev < len(self)
161 234 and rev not in self.filteredrevs)
162 235
163 236 def __iter__(self):
164 237 """filtered version of revlog.__iter__"""
165 238 if len(self.filteredrevs) == 0:
166 239 return revlog.revlog.__iter__(self)
167 240
168 241 def filterediter():
169 242 for i in xrange(len(self)):
170 243 if i not in self.filteredrevs:
171 244 yield i
172 245
173 246 return filterediter()
174 247
175 248 def revs(self, start=0, stop=None):
176 249 """filtered version of revlog.revs"""
177 250 for i in super(changelog, self).revs(start, stop):
178 251 if i not in self.filteredrevs:
179 252 yield i
180 253
181 254 @util.propertycache
182 255 def nodemap(self):
183 256 # XXX need filtering too
184 257 self.rev(self.node(0))
185 258 return self._nodecache
186 259
187 260 def reachableroots(self, minroot, heads, roots, includepath=False):
188 261 return self.index.reachableroots2(minroot, heads, roots, includepath)
189 262
190 263 def headrevs(self):
191 264 if self.filteredrevs:
192 265 try:
193 266 return self.index.headrevsfiltered(self.filteredrevs)
194 267 # AttributeError covers non-c-extension environments and
195 268 # old c extensions without filter handling.
196 269 except AttributeError:
197 270 return self._headrevs()
198 271
199 272 return super(changelog, self).headrevs()
200 273
201 274 def strip(self, *args, **kwargs):
202 275 # XXX make something better than assert
203 276 # We can't expect proper strip behavior if we are filtered.
204 277 assert not self.filteredrevs
205 278 super(changelog, self).strip(*args, **kwargs)
206 279
207 280 def rev(self, node):
208 281 """filtered version of revlog.rev"""
209 282 r = super(changelog, self).rev(node)
210 283 if r in self.filteredrevs:
211 284 raise error.FilteredLookupError(hex(node), self.indexfile,
212 285 _('filtered node'))
213 286 return r
214 287
215 288 def node(self, rev):
216 289 """filtered version of revlog.node"""
217 290 if rev in self.filteredrevs:
218 291 raise error.FilteredIndexError(rev)
219 292 return super(changelog, self).node(rev)
220 293
221 294 def linkrev(self, rev):
222 295 """filtered version of revlog.linkrev"""
223 296 if rev in self.filteredrevs:
224 297 raise error.FilteredIndexError(rev)
225 298 return super(changelog, self).linkrev(rev)
226 299
227 300 def parentrevs(self, rev):
228 301 """filtered version of revlog.parentrevs"""
229 302 if rev in self.filteredrevs:
230 303 raise error.FilteredIndexError(rev)
231 304 return super(changelog, self).parentrevs(rev)
232 305
233 306 def flags(self, rev):
234 307 """filtered version of revlog.flags"""
235 308 if rev in self.filteredrevs:
236 309 raise error.FilteredIndexError(rev)
237 310 return super(changelog, self).flags(rev)
238 311
239 312 def delayupdate(self, tr):
240 313 "delay visibility of index updates to other readers"
241 314
242 315 if not self._delayed:
243 316 if len(self) == 0:
244 317 self._divert = True
245 318 if self._realopener.exists(self.indexfile + '.a'):
246 319 self._realopener.unlink(self.indexfile + '.a')
247 320 self.opener = _divertopener(self._realopener, self.indexfile)
248 321 else:
249 322 self._delaybuf = []
250 323 self.opener = _delayopener(self._realopener, self.indexfile,
251 324 self._delaybuf)
252 325 self._delayed = True
253 326 tr.addpending('cl-%i' % id(self), self._writepending)
254 327 tr.addfinalize('cl-%i' % id(self), self._finalize)
255 328
256 329 def _finalize(self, tr):
257 330 "finalize index updates"
258 331 self._delayed = False
259 332 self.opener = self._realopener
260 333 # move redirected index data back into place
261 334 if self._divert:
262 335 assert not self._delaybuf
263 336 tmpname = self.indexfile + ".a"
264 337 nfile = self.opener.open(tmpname)
265 338 nfile.close()
266 339 self.opener.rename(tmpname, self.indexfile)
267 340 elif self._delaybuf:
268 341 fp = self.opener(self.indexfile, 'a')
269 342 fp.write("".join(self._delaybuf))
270 343 fp.close()
271 344 self._delaybuf = None
272 345 self._divert = False
273 346 # split when we're done
274 347 self.checkinlinesize(tr)
275 348
276 349 def readpending(self, file):
277 350 """read index data from a "pending" file
278 351
279 352 During a transaction, the actual changeset data is already stored in the
280 353 main file, but not yet finalized in the on-disk index. Instead, a
281 354 "pending" index is written by the transaction logic. If this function
282 355 is running, we are likely in a subprocess invoked in a hook. The
283 356 subprocess is informed that it is within a transaction and needs to
284 357 access its content.
285 358
286 359 This function will read all the index data out of the pending file and
287 360 overwrite the main index."""
288 361
289 362 if not self.opener.exists(file):
290 363 return # no pending data for changelog
291 364 r = revlog.revlog(self.opener, file)
292 365 self.index = r.index
293 366 self.nodemap = r.nodemap
294 367 self._nodecache = r._nodecache
295 368 self._chunkcache = r._chunkcache
296 369
297 370 def _writepending(self, tr):
298 371 "create a file containing the unfinalized state for pretxnchangegroup"
299 372 if self._delaybuf:
300 373 # make a temporary copy of the index
301 374 fp1 = self._realopener(self.indexfile)
302 375 pendingfilename = self.indexfile + ".a"
303 376 # register as a temp file to ensure cleanup on failure
304 377 tr.registertmp(pendingfilename)
305 378 # write existing data
306 379 fp2 = self._realopener(pendingfilename, "w")
307 380 fp2.write(fp1.read())
308 381 # add pending data
309 382 fp2.write("".join(self._delaybuf))
310 383 fp2.close()
311 384 # switch modes so finalize can simply rename
312 385 self._delaybuf = None
313 386 self._divert = True
314 387 self.opener = _divertopener(self._realopener, self.indexfile)
315 388
316 389 if self._divert:
317 390 return True
318 391
319 392 return False
320 393
321 394 def checkinlinesize(self, tr, fp=None):
322 395 if not self._delayed:
323 396 revlog.revlog.checkinlinesize(self, tr, fp)
324 397
325 398 def read(self, node):
326 """
327 format used:
328 nodeid\n : manifest node in ascii
329 user\n : user, no \n or \r allowed
330 time tz extra\n : date (time is int or float, timezone is int)
331 : extra is metadata, encoded and separated by '\0'
332 : older versions ignore it
333 files\n\n : files modified by the cset, no \n or \r allowed
334 (.*) : comment (free text, ideally utf-8)
399 """Obtain data from a parsed changelog revision.
400
401 Returns a 6-tuple of:
335 402
336 changelog v0 doesn't use extra
403 - manifest node in binary
404 - author/user as a localstr
405 - date as a 2-tuple of (time, timezone)
406 - list of files
407 - commit message as a localstr
408 - dict of extra metadata
409
410 Unless you need to access all fields, consider calling
411 ``changelogrevision`` instead, as it is faster for partial object
412 access.
337 413 """
338 text = self.revision(node)
339 if not text:
340 return nullid, "", (0, 0), [], "", _defaultextra
341 last = text.index("\n\n")
342 desc = encoding.tolocal(text[last + 2:])
343 l = text[:last].split('\n')
344 manifest = bin(l[0])
345 user = encoding.tolocal(l[1])
414 c = changelogrevision(self.revision(node))
415 return (
416 c.manifest,
417 c.user,
418 c.date,
419 c.files,
420 c.description,
421 c.extra
422 )
346 423
347 tdata = l[2].split(' ', 2)
348 if len(tdata) != 3:
349 time = float(tdata[0])
350 try:
351 # various tools did silly things with the time zone field.
352 timezone = int(tdata[1])
353 except ValueError:
354 timezone = 0
355 extra = _defaultextra
356 else:
357 time, timezone = float(tdata[0]), int(tdata[1])
358 extra = decodeextra(tdata[2])
359
360 files = l[3:]
361 return manifest, user, (time, timezone), files, desc, extra
424 def changelogrevision(self, nodeorrev):
425 """Obtain a ``changelogrevision`` for a node or revision."""
426 return changelogrevision(self.revision(nodeorrev))
362 427
363 428 def readfiles(self, node):
364 429 """
365 430 short version of read that only returns the files modified by the cset
366 431 """
367 432 text = self.revision(node)
368 433 if not text:
369 434 return []
370 435 last = text.index("\n\n")
371 436 l = text[:last].split('\n')
372 437 return l[3:]
373 438
374 439 def add(self, manifest, files, desc, transaction, p1, p2,
375 440 user, date=None, extra=None):
376 441 # Convert to UTF-8 encoded bytestrings as the very first
377 442 # thing: calling any method on a localstr object will turn it
378 443 # into a str object and the cached UTF-8 string is thus lost.
379 444 user, desc = encoding.fromlocal(user), encoding.fromlocal(desc)
380 445
381 446 user = user.strip()
382 447 # An empty username or a username with a "\n" will make the
383 448 # revision text contain two "\n\n" sequences -> corrupt
384 449 # repository since read cannot unpack the revision.
385 450 if not user:
386 451 raise error.RevlogError(_("empty username"))
387 452 if "\n" in user:
388 453 raise error.RevlogError(_("username %s contains a newline")
389 454 % repr(user))
390 455
391 456 desc = stripdesc(desc)
392 457
393 458 if date:
394 459 parseddate = "%d %d" % util.parsedate(date)
395 460 else:
396 461 parseddate = "%d %d" % util.makedate()
397 462 if extra:
398 463 branch = extra.get("branch")
399 464 if branch in ("default", ""):
400 465 del extra["branch"]
401 466 elif branch in (".", "null", "tip"):
402 467 raise error.RevlogError(_('the name \'%s\' is reserved')
403 468 % branch)
404 469 if extra:
405 470 extra = encodeextra(extra)
406 471 parseddate = "%s %s" % (parseddate, extra)
407 472 l = [hex(manifest), user, parseddate] + sorted(files) + ["", desc]
408 473 text = "\n".join(l)
409 474 return self.addrevision(text, transaction, len(self), p1, p2)
410 475
411 476 def branchinfo(self, rev):
412 477 """return the branch name and open/close state of a revision
413 478
414 479 This function exists because creating a changectx object
415 480 just to access this is costly."""
416 481 extra = self.read(rev)[5]
417 482 return encoding.tolocal(extra.get("branch")), 'close' in extra
General Comments 0
You need to be logged in to leave comments. Login now