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