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