##// END OF EJS Templates
repoview: fix changelog.__contains__ method...
marmoute -
r52351:ec8c1d0f default
parent child Browse files
Show More
@@ -1,507 +1,510 b''
1 1 # changelog.py - changelog class for mercurial
2 2 #
3 3 # Copyright 2005-2007 Olivia Mackall <olivia@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
9 9 from .i18n import _
10 10 from .node import (
11 11 bin,
12 12 hex,
13 13 )
14 14 from .thirdparty import attr
15 15
16 16 from . import (
17 17 encoding,
18 18 error,
19 19 metadata,
20 20 pycompat,
21 21 revlog,
22 22 )
23 23 from .utils import (
24 24 dateutil,
25 25 stringutil,
26 26 )
27 27 from .revlogutils import (
28 28 constants as revlog_constants,
29 29 flagutil,
30 30 )
31 31
32 32 _defaultextra = {b'branch': b'default'}
33 33
34 34
35 35 def _string_escape(text):
36 36 """
37 37 >>> from .pycompat import bytechr as chr
38 38 >>> d = {b'nl': chr(10), b'bs': chr(92), b'cr': chr(13), b'nul': chr(0)}
39 39 >>> s = b"ab%(nl)scd%(bs)s%(bs)sn%(nul)s12ab%(cr)scd%(bs)s%(nl)s" % d
40 40 >>> s
41 41 'ab\\ncd\\\\\\\\n\\x0012ab\\rcd\\\\\\n'
42 42 >>> res = _string_escape(s)
43 43 >>> s == _string_unescape(res)
44 44 True
45 45 """
46 46 # subset of the string_escape codec
47 47 text = (
48 48 text.replace(b'\\', b'\\\\')
49 49 .replace(b'\n', b'\\n')
50 50 .replace(b'\r', b'\\r')
51 51 )
52 52 return text.replace(b'\0', b'\\0')
53 53
54 54
55 55 def _string_unescape(text):
56 56 if b'\\0' in text:
57 57 # fix up \0 without getting into trouble with \\0
58 58 text = text.replace(b'\\\\', b'\\\\\n')
59 59 text = text.replace(b'\\0', b'\0')
60 60 text = text.replace(b'\n', b'')
61 61 return stringutil.unescapestr(text)
62 62
63 63
64 64 def decodeextra(text):
65 65 """
66 66 >>> from .pycompat import bytechr as chr
67 67 >>> sorted(decodeextra(encodeextra({b'foo': b'bar', b'baz': chr(0) + b'2'})
68 68 ... ).items())
69 69 [('baz', '\\x002'), ('branch', 'default'), ('foo', 'bar')]
70 70 >>> sorted(decodeextra(encodeextra({b'foo': b'bar',
71 71 ... b'baz': chr(92) + chr(0) + b'2'})
72 72 ... ).items())
73 73 [('baz', '\\\\\\x002'), ('branch', 'default'), ('foo', 'bar')]
74 74 """
75 75 extra = _defaultextra.copy()
76 76 for l in text.split(b'\0'):
77 77 if l:
78 78 k, v = _string_unescape(l).split(b':', 1)
79 79 extra[k] = v
80 80 return extra
81 81
82 82
83 83 def encodeextra(d):
84 84 # keys must be sorted to produce a deterministic changelog entry
85 85 items = [_string_escape(b'%s:%s' % (k, d[k])) for k in sorted(d)]
86 86 return b"\0".join(items)
87 87
88 88
89 89 def stripdesc(desc):
90 90 """strip trailing whitespace and leading and trailing empty lines"""
91 91 return b'\n'.join([l.rstrip() for l in desc.splitlines()]).strip(b'\n')
92 92
93 93
94 94 @attr.s
95 95 class _changelogrevision:
96 96 # Extensions might modify _defaultextra, so let the constructor below pass
97 97 # it in
98 98 extra = attr.ib()
99 99 manifest = attr.ib()
100 100 user = attr.ib(default=b'')
101 101 date = attr.ib(default=(0, 0))
102 102 files = attr.ib(default=attr.Factory(list))
103 103 filesadded = attr.ib(default=None)
104 104 filesremoved = attr.ib(default=None)
105 105 p1copies = attr.ib(default=None)
106 106 p2copies = attr.ib(default=None)
107 107 description = attr.ib(default=b'')
108 108 branchinfo = attr.ib(default=(_defaultextra[b'branch'], False))
109 109
110 110
111 111 class changelogrevision:
112 112 """Holds results of a parsed changelog revision.
113 113
114 114 Changelog revisions consist of multiple pieces of data, including
115 115 the manifest node, user, and date. This object exposes a view into
116 116 the parsed object.
117 117 """
118 118
119 119 __slots__ = (
120 120 '_offsets',
121 121 '_text',
122 122 '_sidedata',
123 123 '_cpsd',
124 124 '_changes',
125 125 )
126 126
127 127 def __new__(cls, cl, text, sidedata, cpsd):
128 128 if not text:
129 129 return _changelogrevision(extra=_defaultextra, manifest=cl.nullid)
130 130
131 131 self = super(changelogrevision, cls).__new__(cls)
132 132 # We could return here and implement the following as an __init__.
133 133 # But doing it here is equivalent and saves an extra function call.
134 134
135 135 # format used:
136 136 # nodeid\n : manifest node in ascii
137 137 # user\n : user, no \n or \r allowed
138 138 # time tz extra\n : date (time is int or float, timezone is int)
139 139 # : extra is metadata, encoded and separated by '\0'
140 140 # : older versions ignore it
141 141 # files\n\n : files modified by the cset, no \n or \r allowed
142 142 # (.*) : comment (free text, ideally utf-8)
143 143 #
144 144 # changelog v0 doesn't use extra
145 145
146 146 nl1 = text.index(b'\n')
147 147 nl2 = text.index(b'\n', nl1 + 1)
148 148 nl3 = text.index(b'\n', nl2 + 1)
149 149
150 150 # The list of files may be empty. Which means nl3 is the first of the
151 151 # double newline that precedes the description.
152 152 if text[nl3 + 1 : nl3 + 2] == b'\n':
153 153 doublenl = nl3
154 154 else:
155 155 doublenl = text.index(b'\n\n', nl3 + 1)
156 156
157 157 self._offsets = (nl1, nl2, nl3, doublenl)
158 158 self._text = text
159 159 self._sidedata = sidedata
160 160 self._cpsd = cpsd
161 161 self._changes = None
162 162
163 163 return self
164 164
165 165 @property
166 166 def manifest(self):
167 167 return bin(self._text[0 : self._offsets[0]])
168 168
169 169 @property
170 170 def user(self):
171 171 off = self._offsets
172 172 return encoding.tolocal(self._text[off[0] + 1 : off[1]])
173 173
174 174 @property
175 175 def _rawdate(self):
176 176 off = self._offsets
177 177 dateextra = self._text[off[1] + 1 : off[2]]
178 178 return dateextra.split(b' ', 2)[0:2]
179 179
180 180 @property
181 181 def _rawextra(self):
182 182 off = self._offsets
183 183 dateextra = self._text[off[1] + 1 : off[2]]
184 184 fields = dateextra.split(b' ', 2)
185 185 if len(fields) != 3:
186 186 return None
187 187
188 188 return fields[2]
189 189
190 190 @property
191 191 def date(self):
192 192 raw = self._rawdate
193 193 time = float(raw[0])
194 194 # Various tools did silly things with the timezone.
195 195 try:
196 196 timezone = int(raw[1])
197 197 except ValueError:
198 198 timezone = 0
199 199
200 200 return time, timezone
201 201
202 202 @property
203 203 def extra(self):
204 204 raw = self._rawextra
205 205 if raw is None:
206 206 return _defaultextra
207 207
208 208 return decodeextra(raw)
209 209
210 210 @property
211 211 def changes(self):
212 212 if self._changes is not None:
213 213 return self._changes
214 214 if self._cpsd:
215 215 changes = metadata.decode_files_sidedata(self._sidedata)
216 216 else:
217 217 changes = metadata.ChangingFiles(
218 218 touched=self.files or (),
219 219 added=self.filesadded or (),
220 220 removed=self.filesremoved or (),
221 221 p1_copies=self.p1copies or {},
222 222 p2_copies=self.p2copies or {},
223 223 )
224 224 self._changes = changes
225 225 return changes
226 226
227 227 @property
228 228 def files(self):
229 229 if self._cpsd:
230 230 return sorted(self.changes.touched)
231 231 off = self._offsets
232 232 if off[2] == off[3]:
233 233 return []
234 234
235 235 return self._text[off[2] + 1 : off[3]].split(b'\n')
236 236
237 237 @property
238 238 def filesadded(self):
239 239 if self._cpsd:
240 240 return self.changes.added
241 241 else:
242 242 rawindices = self.extra.get(b'filesadded')
243 243 if rawindices is None:
244 244 return None
245 245 return metadata.decodefileindices(self.files, rawindices)
246 246
247 247 @property
248 248 def filesremoved(self):
249 249 if self._cpsd:
250 250 return self.changes.removed
251 251 else:
252 252 rawindices = self.extra.get(b'filesremoved')
253 253 if rawindices is None:
254 254 return None
255 255 return metadata.decodefileindices(self.files, rawindices)
256 256
257 257 @property
258 258 def p1copies(self):
259 259 if self._cpsd:
260 260 return self.changes.copied_from_p1
261 261 else:
262 262 rawcopies = self.extra.get(b'p1copies')
263 263 if rawcopies is None:
264 264 return None
265 265 return metadata.decodecopies(self.files, rawcopies)
266 266
267 267 @property
268 268 def p2copies(self):
269 269 if self._cpsd:
270 270 return self.changes.copied_from_p2
271 271 else:
272 272 rawcopies = self.extra.get(b'p2copies')
273 273 if rawcopies is None:
274 274 return None
275 275 return metadata.decodecopies(self.files, rawcopies)
276 276
277 277 @property
278 278 def description(self):
279 279 return encoding.tolocal(self._text[self._offsets[3] + 2 :])
280 280
281 281 @property
282 282 def branchinfo(self):
283 283 extra = self.extra
284 284 return encoding.tolocal(extra.get(b"branch")), b'close' in extra
285 285
286 286
287 287 class changelog(revlog.revlog):
288 288 def __init__(self, opener, trypending=False, concurrencychecker=None):
289 289 """Load a changelog revlog using an opener.
290 290
291 291 If ``trypending`` is true, we attempt to load the index from a
292 292 ``00changelog.i.a`` file instead of the default ``00changelog.i``.
293 293 The ``00changelog.i.a`` file contains index (and possibly inline
294 294 revision) data for a transaction that hasn't been finalized yet.
295 295 It exists in a separate file to facilitate readers (such as
296 296 hooks processes) accessing data before a transaction is finalized.
297 297
298 298 ``concurrencychecker`` will be passed to the revlog init function, see
299 299 the documentation there.
300 300 """
301 301 revlog.revlog.__init__(
302 302 self,
303 303 opener,
304 304 target=(revlog_constants.KIND_CHANGELOG, None),
305 305 radix=b'00changelog',
306 306 checkambig=True,
307 307 mmaplargeindex=True,
308 308 persistentnodemap=opener.options.get(b'persistent-nodemap', False),
309 309 concurrencychecker=concurrencychecker,
310 310 trypending=trypending,
311 311 may_inline=False,
312 312 )
313 313
314 314 if self._initempty and (self._format_version == revlog.REVLOGV1):
315 315 # changelogs don't benefit from generaldelta.
316 316
317 317 self._format_flags &= ~revlog.FLAG_GENERALDELTA
318 318 self.delta_config.general_delta = False
319 319
320 320 # Delta chains for changelogs tend to be very small because entries
321 321 # tend to be small and don't delta well with each. So disable delta
322 322 # chains.
323 323 self._storedeltachains = False
324 324
325 325 self._v2_delayed = False
326 326 self._filteredrevs = frozenset()
327 327 self._filteredrevs_hashcache = {}
328 328 self._copiesstorage = opener.options.get(b'copies-storage')
329 329
330 def __contains__(self, rev):
331 return (0 <= rev < len(self)) and rev not in self._filteredrevs
332
330 333 @property
331 334 def filteredrevs(self):
332 335 return self._filteredrevs
333 336
334 337 @filteredrevs.setter
335 338 def filteredrevs(self, val):
336 339 # Ensure all updates go through this function
337 340 assert isinstance(val, frozenset)
338 341 self._filteredrevs = val
339 342 self._filteredrevs_hashcache = {}
340 343
341 344 def _write_docket(self, tr):
342 345 if not self._v2_delayed:
343 346 super(changelog, self)._write_docket(tr)
344 347
345 348 def delayupdate(self, tr):
346 349 """delay visibility of index updates to other readers"""
347 350 assert not self._inner.is_open
348 351 assert not self._may_inline
349 352 # enforce that older changelog that are still inline are split at the
350 353 # first opportunity.
351 354 if self._inline:
352 355 self._enforceinlinesize(tr)
353 356 if self._docket is not None:
354 357 self._v2_delayed = True
355 358 else:
356 359 new_index = self._inner.delay()
357 360 if new_index is not None:
358 361 self._indexfile = new_index
359 362 tr.registertmp(new_index)
360 363 tr.addpending(b'cl-%i' % id(self), self._writepending)
361 364 tr.addfinalize(b'cl-%i' % id(self), self._finalize)
362 365
363 366 def _finalize(self, tr):
364 367 """finalize index updates"""
365 368 assert not self._inner.is_open
366 369 if self._docket is not None:
367 370 self._docket.write(tr)
368 371 self._v2_delayed = False
369 372 else:
370 373 new_index_file = self._inner.finalize_pending()
371 374 self._indexfile = new_index_file
372 375 if self._inline:
373 376 msg = 'changelog should not be inline at that point'
374 377 raise error.ProgrammingError(msg)
375 378
376 379 def _writepending(self, tr):
377 380 """create a file containing the unfinalized state for
378 381 pretxnchangegroup"""
379 382 assert not self._inner.is_open
380 383 if self._docket:
381 384 any_pending = self._docket.write(tr, pending=True)
382 385 self._v2_delayed = False
383 386 else:
384 387 new_index, any_pending = self._inner.write_pending()
385 388 if new_index is not None:
386 389 self._indexfile = new_index
387 390 tr.registertmp(new_index)
388 391 return any_pending
389 392
390 393 def _enforceinlinesize(self, tr):
391 394 if not self.is_delaying:
392 395 revlog.revlog._enforceinlinesize(self, tr)
393 396
394 397 def read(self, nodeorrev):
395 398 """Obtain data from a parsed changelog revision.
396 399
397 400 Returns a 6-tuple of:
398 401
399 402 - manifest node in binary
400 403 - author/user as a localstr
401 404 - date as a 2-tuple of (time, timezone)
402 405 - list of files
403 406 - commit message as a localstr
404 407 - dict of extra metadata
405 408
406 409 Unless you need to access all fields, consider calling
407 410 ``changelogrevision`` instead, as it is faster for partial object
408 411 access.
409 412 """
410 413 d = self._revisiondata(nodeorrev)
411 414 sidedata = self.sidedata(nodeorrev)
412 415 copy_sd = self._copiesstorage == b'changeset-sidedata'
413 416 c = changelogrevision(self, d, sidedata, copy_sd)
414 417 return (c.manifest, c.user, c.date, c.files, c.description, c.extra)
415 418
416 419 def changelogrevision(self, nodeorrev):
417 420 """Obtain a ``changelogrevision`` for a node or revision."""
418 421 text = self._revisiondata(nodeorrev)
419 422 sidedata = self.sidedata(nodeorrev)
420 423 return changelogrevision(
421 424 self, text, sidedata, self._copiesstorage == b'changeset-sidedata'
422 425 )
423 426
424 427 def readfiles(self, nodeorrev):
425 428 """
426 429 short version of read that only returns the files modified by the cset
427 430 """
428 431 text = self.revision(nodeorrev)
429 432 if not text:
430 433 return []
431 434 last = text.index(b"\n\n")
432 435 l = text[:last].split(b'\n')
433 436 return l[3:]
434 437
435 438 def add(
436 439 self,
437 440 manifest,
438 441 files,
439 442 desc,
440 443 transaction,
441 444 p1,
442 445 p2,
443 446 user,
444 447 date=None,
445 448 extra=None,
446 449 ):
447 450 # Convert to UTF-8 encoded bytestrings as the very first
448 451 # thing: calling any method on a localstr object will turn it
449 452 # into a str object and the cached UTF-8 string is thus lost.
450 453 user, desc = encoding.fromlocal(user), encoding.fromlocal(desc)
451 454
452 455 user = user.strip()
453 456 # An empty username or a username with a "\n" will make the
454 457 # revision text contain two "\n\n" sequences -> corrupt
455 458 # repository since read cannot unpack the revision.
456 459 if not user:
457 460 raise error.StorageError(_(b"empty username"))
458 461 if b"\n" in user:
459 462 raise error.StorageError(
460 463 _(b"username %r contains a newline") % pycompat.bytestr(user)
461 464 )
462 465
463 466 desc = stripdesc(desc)
464 467
465 468 if date:
466 469 parseddate = b"%d %d" % dateutil.parsedate(date)
467 470 else:
468 471 parseddate = b"%d %d" % dateutil.makedate()
469 472 if extra:
470 473 branch = extra.get(b"branch")
471 474 if branch in (b"default", b""):
472 475 del extra[b"branch"]
473 476 elif branch in (b".", b"null", b"tip"):
474 477 raise error.StorageError(
475 478 _(b'the name \'%s\' is reserved') % branch
476 479 )
477 480 sortedfiles = sorted(files.touched)
478 481 flags = 0
479 482 sidedata = None
480 483 if self._copiesstorage == b'changeset-sidedata':
481 484 if files.has_copies_info:
482 485 flags |= flagutil.REVIDX_HASCOPIESINFO
483 486 sidedata = metadata.encode_files_sidedata(files)
484 487
485 488 if extra:
486 489 extra = encodeextra(extra)
487 490 parseddate = b"%s %s" % (parseddate, extra)
488 491 l = [hex(manifest), user, parseddate] + sortedfiles + [b"", desc]
489 492 text = b"\n".join(l)
490 493 rev = self.addrevision(
491 494 text, transaction, len(self), p1, p2, sidedata=sidedata, flags=flags
492 495 )
493 496 return self.node(rev)
494 497
495 498 def branchinfo(self, rev):
496 499 """return the branch name and open/close state of a revision
497 500
498 501 This function exists because creating a changectx object
499 502 just to access this is costly."""
500 503 return self.changelogrevision(rev).branchinfo
501 504
502 505 def _nodeduplicatecallback(self, transaction, rev):
503 506 # keep track of revisions that got "re-added", eg: unbunde of know rev.
504 507 #
505 508 # We track them in a list to preserve their order from the source bundle
506 509 duplicates = transaction.changes.setdefault(b'revduplicates', [])
507 510 duplicates.append(rev)
General Comments 0
You need to be logged in to leave comments. Login now