##// END OF EJS Templates
node: make bin() be a wrapper instead of just an alias...
Augie Fackler -
r36256:f574cc00 default
parent child Browse files
Show More
@@ -1,1651 +1,1650 b''
1 1 # histedit.py - interactive history editing for mercurial
2 2 #
3 3 # Copyright 2009 Augie Fackler <raf@durin42.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 """interactive history editing
8 8
9 9 With this extension installed, Mercurial gains one new command: histedit. Usage
10 10 is as follows, assuming the following history::
11 11
12 12 @ 3[tip] 7c2fd3b9020c 2009-04-27 18:04 -0500 durin42
13 13 | Add delta
14 14 |
15 15 o 2 030b686bedc4 2009-04-27 18:04 -0500 durin42
16 16 | Add gamma
17 17 |
18 18 o 1 c561b4e977df 2009-04-27 18:04 -0500 durin42
19 19 | Add beta
20 20 |
21 21 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
22 22 Add alpha
23 23
24 24 If you were to run ``hg histedit c561b4e977df``, you would see the following
25 25 file open in your editor::
26 26
27 27 pick c561b4e977df Add beta
28 28 pick 030b686bedc4 Add gamma
29 29 pick 7c2fd3b9020c Add delta
30 30
31 31 # Edit history between c561b4e977df and 7c2fd3b9020c
32 32 #
33 33 # Commits are listed from least to most recent
34 34 #
35 35 # Commands:
36 36 # p, pick = use commit
37 37 # e, edit = use commit, but stop for amending
38 38 # f, fold = use commit, but combine it with the one above
39 39 # r, roll = like fold, but discard this commit's description and date
40 40 # d, drop = remove commit from history
41 41 # m, mess = edit commit message without changing commit content
42 42 # b, base = checkout changeset and apply further changesets from there
43 43 #
44 44
45 45 In this file, lines beginning with ``#`` are ignored. You must specify a rule
46 46 for each revision in your history. For example, if you had meant to add gamma
47 47 before beta, and then wanted to add delta in the same revision as beta, you
48 48 would reorganize the file to look like this::
49 49
50 50 pick 030b686bedc4 Add gamma
51 51 pick c561b4e977df Add beta
52 52 fold 7c2fd3b9020c Add delta
53 53
54 54 # Edit history between c561b4e977df and 7c2fd3b9020c
55 55 #
56 56 # Commits are listed from least to most recent
57 57 #
58 58 # Commands:
59 59 # p, pick = use commit
60 60 # e, edit = use commit, but stop for amending
61 61 # f, fold = use commit, but combine it with the one above
62 62 # r, roll = like fold, but discard this commit's description and date
63 63 # d, drop = remove commit from history
64 64 # m, mess = edit commit message without changing commit content
65 65 # b, base = checkout changeset and apply further changesets from there
66 66 #
67 67
68 68 At which point you close the editor and ``histedit`` starts working. When you
69 69 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
70 70 those revisions together, offering you a chance to clean up the commit message::
71 71
72 72 Add beta
73 73 ***
74 74 Add delta
75 75
76 76 Edit the commit message to your liking, then close the editor. The date used
77 77 for the commit will be the later of the two commits' dates. For this example,
78 78 let's assume that the commit message was changed to ``Add beta and delta.``
79 79 After histedit has run and had a chance to remove any old or temporary
80 80 revisions it needed, the history looks like this::
81 81
82 82 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
83 83 | Add beta and delta.
84 84 |
85 85 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
86 86 | Add gamma
87 87 |
88 88 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
89 89 Add alpha
90 90
91 91 Note that ``histedit`` does *not* remove any revisions (even its own temporary
92 92 ones) until after it has completed all the editing operations, so it will
93 93 probably perform several strip operations when it's done. For the above example,
94 94 it had to run strip twice. Strip can be slow depending on a variety of factors,
95 95 so you might need to be a little patient. You can choose to keep the original
96 96 revisions by passing the ``--keep`` flag.
97 97
98 98 The ``edit`` operation will drop you back to a command prompt,
99 99 allowing you to edit files freely, or even use ``hg record`` to commit
100 100 some changes as a separate commit. When you're done, any remaining
101 101 uncommitted changes will be committed as well. When done, run ``hg
102 102 histedit --continue`` to finish this step. If there are uncommitted
103 103 changes, you'll be prompted for a new commit message, but the default
104 104 commit message will be the original message for the ``edit`` ed
105 105 revision, and the date of the original commit will be preserved.
106 106
107 107 The ``message`` operation will give you a chance to revise a commit
108 108 message without changing the contents. It's a shortcut for doing
109 109 ``edit`` immediately followed by `hg histedit --continue``.
110 110
111 111 If ``histedit`` encounters a conflict when moving a revision (while
112 112 handling ``pick`` or ``fold``), it'll stop in a similar manner to
113 113 ``edit`` with the difference that it won't prompt you for a commit
114 114 message when done. If you decide at this point that you don't like how
115 115 much work it will be to rearrange history, or that you made a mistake,
116 116 you can use ``hg histedit --abort`` to abandon the new changes you
117 117 have made and return to the state before you attempted to edit your
118 118 history.
119 119
120 120 If we clone the histedit-ed example repository above and add four more
121 121 changes, such that we have the following history::
122 122
123 123 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
124 124 | Add theta
125 125 |
126 126 o 5 140988835471 2009-04-27 18:04 -0500 stefan
127 127 | Add eta
128 128 |
129 129 o 4 122930637314 2009-04-27 18:04 -0500 stefan
130 130 | Add zeta
131 131 |
132 132 o 3 836302820282 2009-04-27 18:04 -0500 stefan
133 133 | Add epsilon
134 134 |
135 135 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
136 136 | Add beta and delta.
137 137 |
138 138 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
139 139 | Add gamma
140 140 |
141 141 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
142 142 Add alpha
143 143
144 144 If you run ``hg histedit --outgoing`` on the clone then it is the same
145 145 as running ``hg histedit 836302820282``. If you need plan to push to a
146 146 repository that Mercurial does not detect to be related to the source
147 147 repo, you can add a ``--force`` option.
148 148
149 149 Config
150 150 ------
151 151
152 152 Histedit rule lines are truncated to 80 characters by default. You
153 153 can customize this behavior by setting a different length in your
154 154 configuration file::
155 155
156 156 [histedit]
157 157 linelen = 120 # truncate rule lines at 120 characters
158 158
159 159 ``hg histedit`` attempts to automatically choose an appropriate base
160 160 revision to use. To change which base revision is used, define a
161 161 revset in your configuration file::
162 162
163 163 [histedit]
164 164 defaultrev = only(.) & draft()
165 165
166 166 By default each edited revision needs to be present in histedit commands.
167 167 To remove revision you need to use ``drop`` operation. You can configure
168 168 the drop to be implicit for missing commits by adding::
169 169
170 170 [histedit]
171 171 dropmissing = True
172 172
173 173 By default, histedit will close the transaction after each action. For
174 174 performance purposes, you can configure histedit to use a single transaction
175 175 across the entire histedit. WARNING: This setting introduces a significant risk
176 176 of losing the work you've done in a histedit if the histedit aborts
177 177 unexpectedly::
178 178
179 179 [histedit]
180 180 singletransaction = True
181 181
182 182 """
183 183
184 184 from __future__ import absolute_import
185 185
186 import binascii
187 186 import errno
188 187 import os
189 188
190 189 from mercurial.i18n import _
191 190 from mercurial import (
192 191 bundle2,
193 192 cmdutil,
194 193 context,
195 194 copies,
196 195 destutil,
197 196 discovery,
198 197 error,
199 198 exchange,
200 199 extensions,
201 200 hg,
202 201 lock,
203 202 merge as mergemod,
204 203 mergeutil,
205 204 node,
206 205 obsolete,
207 206 pycompat,
208 207 registrar,
209 208 repair,
210 209 scmutil,
211 210 util,
212 211 )
213 212
214 213 pickle = util.pickle
215 214 release = lock.release
216 215 cmdtable = {}
217 216 command = registrar.command(cmdtable)
218 217
219 218 configtable = {}
220 219 configitem = registrar.configitem(configtable)
221 220 configitem('experimental', 'histedit.autoverb',
222 221 default=False,
223 222 )
224 223 configitem('histedit', 'defaultrev',
225 224 default=configitem.dynamicdefault,
226 225 )
227 226 configitem('histedit', 'dropmissing',
228 227 default=False,
229 228 )
230 229 configitem('histedit', 'linelen',
231 230 default=80,
232 231 )
233 232 configitem('histedit', 'singletransaction',
234 233 default=False,
235 234 )
236 235
237 236 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
238 237 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
239 238 # be specifying the version(s) of Mercurial they are tested with, or
240 239 # leave the attribute unspecified.
241 240 testedwith = 'ships-with-hg-core'
242 241
243 242 actiontable = {}
244 243 primaryactions = set()
245 244 secondaryactions = set()
246 245 tertiaryactions = set()
247 246 internalactions = set()
248 247
249 248 def geteditcomment(ui, first, last):
250 249 """ construct the editor comment
251 250 The comment includes::
252 251 - an intro
253 252 - sorted primary commands
254 253 - sorted short commands
255 254 - sorted long commands
256 255 - additional hints
257 256
258 257 Commands are only included once.
259 258 """
260 259 intro = _("""Edit history between %s and %s
261 260
262 261 Commits are listed from least to most recent
263 262
264 263 You can reorder changesets by reordering the lines
265 264
266 265 Commands:
267 266 """)
268 267 actions = []
269 268 def addverb(v):
270 269 a = actiontable[v]
271 270 lines = a.message.split("\n")
272 271 if len(a.verbs):
273 272 v = ', '.join(sorted(a.verbs, key=lambda v: len(v)))
274 273 actions.append(" %s = %s" % (v, lines[0]))
275 274 actions.extend([' %s' for l in lines[1:]])
276 275
277 276 for v in (
278 277 sorted(primaryactions) +
279 278 sorted(secondaryactions) +
280 279 sorted(tertiaryactions)
281 280 ):
282 281 addverb(v)
283 282 actions.append('')
284 283
285 284 hints = []
286 285 if ui.configbool('histedit', 'dropmissing'):
287 286 hints.append("Deleting a changeset from the list "
288 287 "will DISCARD it from the edited history!")
289 288
290 289 lines = (intro % (first, last)).split('\n') + actions + hints
291 290
292 291 return ''.join(['# %s\n' % l if l else '#\n' for l in lines])
293 292
294 293 class histeditstate(object):
295 294 def __init__(self, repo, parentctxnode=None, actions=None, keep=None,
296 295 topmost=None, replacements=None, lock=None, wlock=None):
297 296 self.repo = repo
298 297 self.actions = actions
299 298 self.keep = keep
300 299 self.topmost = topmost
301 300 self.parentctxnode = parentctxnode
302 301 self.lock = lock
303 302 self.wlock = wlock
304 303 self.backupfile = None
305 304 if replacements is None:
306 305 self.replacements = []
307 306 else:
308 307 self.replacements = replacements
309 308
310 309 def read(self):
311 310 """Load histedit state from disk and set fields appropriately."""
312 311 try:
313 312 state = self.repo.vfs.read('histedit-state')
314 313 except IOError as err:
315 314 if err.errno != errno.ENOENT:
316 315 raise
317 316 cmdutil.wrongtooltocontinue(self.repo, _('histedit'))
318 317
319 318 if state.startswith('v1\n'):
320 319 data = self._load()
321 320 parentctxnode, rules, keep, topmost, replacements, backupfile = data
322 321 else:
323 322 data = pickle.loads(state)
324 323 parentctxnode, rules, keep, topmost, replacements = data
325 324 backupfile = None
326 325
327 326 self.parentctxnode = parentctxnode
328 327 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
329 328 actions = parserules(rules, self)
330 329 self.actions = actions
331 330 self.keep = keep
332 331 self.topmost = topmost
333 332 self.replacements = replacements
334 333 self.backupfile = backupfile
335 334
336 335 def write(self, tr=None):
337 336 if tr:
338 337 tr.addfilegenerator('histedit-state', ('histedit-state',),
339 338 self._write, location='plain')
340 339 else:
341 340 with self.repo.vfs("histedit-state", "w") as f:
342 341 self._write(f)
343 342
344 343 def _write(self, fp):
345 344 fp.write('v1\n')
346 345 fp.write('%s\n' % node.hex(self.parentctxnode))
347 346 fp.write('%s\n' % node.hex(self.topmost))
348 347 fp.write('%s\n' % ('True' if self.keep else 'False'))
349 348 fp.write('%d\n' % len(self.actions))
350 349 for action in self.actions:
351 350 fp.write('%s\n' % action.tostate())
352 351 fp.write('%d\n' % len(self.replacements))
353 352 for replacement in self.replacements:
354 353 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
355 354 for r in replacement[1])))
356 355 backupfile = self.backupfile
357 356 if not backupfile:
358 357 backupfile = ''
359 358 fp.write('%s\n' % backupfile)
360 359
361 360 def _load(self):
362 361 fp = self.repo.vfs('histedit-state', 'r')
363 362 lines = [l[:-1] for l in fp.readlines()]
364 363
365 364 index = 0
366 365 lines[index] # version number
367 366 index += 1
368 367
369 368 parentctxnode = node.bin(lines[index])
370 369 index += 1
371 370
372 371 topmost = node.bin(lines[index])
373 372 index += 1
374 373
375 374 keep = lines[index] == 'True'
376 375 index += 1
377 376
378 377 # Rules
379 378 rules = []
380 379 rulelen = int(lines[index])
381 380 index += 1
382 381 for i in xrange(rulelen):
383 382 ruleaction = lines[index]
384 383 index += 1
385 384 rule = lines[index]
386 385 index += 1
387 386 rules.append((ruleaction, rule))
388 387
389 388 # Replacements
390 389 replacements = []
391 390 replacementlen = int(lines[index])
392 391 index += 1
393 392 for i in xrange(replacementlen):
394 393 replacement = lines[index]
395 394 original = node.bin(replacement[:40])
396 395 succ = [node.bin(replacement[i:i + 40]) for i in
397 396 range(40, len(replacement), 40)]
398 397 replacements.append((original, succ))
399 398 index += 1
400 399
401 400 backupfile = lines[index]
402 401 index += 1
403 402
404 403 fp.close()
405 404
406 405 return parentctxnode, rules, keep, topmost, replacements, backupfile
407 406
408 407 def clear(self):
409 408 if self.inprogress():
410 409 self.repo.vfs.unlink('histedit-state')
411 410
412 411 def inprogress(self):
413 412 return self.repo.vfs.exists('histedit-state')
414 413
415 414
416 415 class histeditaction(object):
417 416 def __init__(self, state, node):
418 417 self.state = state
419 418 self.repo = state.repo
420 419 self.node = node
421 420
422 421 @classmethod
423 422 def fromrule(cls, state, rule):
424 423 """Parses the given rule, returning an instance of the histeditaction.
425 424 """
426 425 rulehash = rule.strip().split(' ', 1)[0]
427 426 try:
428 427 rev = node.bin(rulehash)
429 except (TypeError, binascii.Error):
428 except TypeError:
430 429 raise error.ParseError("invalid changeset %s" % rulehash)
431 430 return cls(state, rev)
432 431
433 432 def verify(self, prev, expected, seen):
434 433 """ Verifies semantic correctness of the rule"""
435 434 repo = self.repo
436 435 ha = node.hex(self.node)
437 436 try:
438 437 self.node = repo[ha].node()
439 438 except error.RepoError:
440 439 raise error.ParseError(_('unknown changeset %s listed')
441 440 % ha[:12])
442 441 if self.node is not None:
443 442 self._verifynodeconstraints(prev, expected, seen)
444 443
445 444 def _verifynodeconstraints(self, prev, expected, seen):
446 445 # by default command need a node in the edited list
447 446 if self.node not in expected:
448 447 raise error.ParseError(_('%s "%s" changeset was not a candidate')
449 448 % (self.verb, node.short(self.node)),
450 449 hint=_('only use listed changesets'))
451 450 # and only one command per node
452 451 if self.node in seen:
453 452 raise error.ParseError(_('duplicated command for changeset %s') %
454 453 node.short(self.node))
455 454
456 455 def torule(self):
457 456 """build a histedit rule line for an action
458 457
459 458 by default lines are in the form:
460 459 <hash> <rev> <summary>
461 460 """
462 461 ctx = self.repo[self.node]
463 462 summary = _getsummary(ctx)
464 463 line = '%s %s %d %s' % (self.verb, ctx, ctx.rev(), summary)
465 464 # trim to 75 columns by default so it's not stupidly wide in my editor
466 465 # (the 5 more are left for verb)
467 466 maxlen = self.repo.ui.configint('histedit', 'linelen')
468 467 maxlen = max(maxlen, 22) # avoid truncating hash
469 468 return util.ellipsis(line, maxlen)
470 469
471 470 def tostate(self):
472 471 """Print an action in format used by histedit state files
473 472 (the first line is a verb, the remainder is the second)
474 473 """
475 474 return "%s\n%s" % (self.verb, node.hex(self.node))
476 475
477 476 def run(self):
478 477 """Runs the action. The default behavior is simply apply the action's
479 478 rulectx onto the current parentctx."""
480 479 self.applychange()
481 480 self.continuedirty()
482 481 return self.continueclean()
483 482
484 483 def applychange(self):
485 484 """Applies the changes from this action's rulectx onto the current
486 485 parentctx, but does not commit them."""
487 486 repo = self.repo
488 487 rulectx = repo[self.node]
489 488 repo.ui.pushbuffer(error=True, labeled=True)
490 489 hg.update(repo, self.state.parentctxnode, quietempty=True)
491 490 stats = applychanges(repo.ui, repo, rulectx, {})
492 491 repo.dirstate.setbranch(rulectx.branch())
493 492 if stats and stats[3] > 0:
494 493 buf = repo.ui.popbuffer()
495 494 repo.ui.write(buf)
496 495 raise error.InterventionRequired(
497 496 _('Fix up the change (%s %s)') %
498 497 (self.verb, node.short(self.node)),
499 498 hint=_('hg histedit --continue to resume'))
500 499 else:
501 500 repo.ui.popbuffer()
502 501
503 502 def continuedirty(self):
504 503 """Continues the action when changes have been applied to the working
505 504 copy. The default behavior is to commit the dirty changes."""
506 505 repo = self.repo
507 506 rulectx = repo[self.node]
508 507
509 508 editor = self.commiteditor()
510 509 commit = commitfuncfor(repo, rulectx)
511 510
512 511 commit(text=rulectx.description(), user=rulectx.user(),
513 512 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
514 513
515 514 def commiteditor(self):
516 515 """The editor to be used to edit the commit message."""
517 516 return False
518 517
519 518 def continueclean(self):
520 519 """Continues the action when the working copy is clean. The default
521 520 behavior is to accept the current commit as the new version of the
522 521 rulectx."""
523 522 ctx = self.repo['.']
524 523 if ctx.node() == self.state.parentctxnode:
525 524 self.repo.ui.warn(_('%s: skipping changeset (no changes)\n') %
526 525 node.short(self.node))
527 526 return ctx, [(self.node, tuple())]
528 527 if ctx.node() == self.node:
529 528 # Nothing changed
530 529 return ctx, []
531 530 return ctx, [(self.node, (ctx.node(),))]
532 531
533 532 def commitfuncfor(repo, src):
534 533 """Build a commit function for the replacement of <src>
535 534
536 535 This function ensure we apply the same treatment to all changesets.
537 536
538 537 - Add a 'histedit_source' entry in extra.
539 538
540 539 Note that fold has its own separated logic because its handling is a bit
541 540 different and not easily factored out of the fold method.
542 541 """
543 542 phasemin = src.phase()
544 543 def commitfunc(**kwargs):
545 544 overrides = {('phases', 'new-commit'): phasemin}
546 545 with repo.ui.configoverride(overrides, 'histedit'):
547 546 extra = kwargs.get(r'extra', {}).copy()
548 547 extra['histedit_source'] = src.hex()
549 548 kwargs[r'extra'] = extra
550 549 return repo.commit(**kwargs)
551 550 return commitfunc
552 551
553 552 def applychanges(ui, repo, ctx, opts):
554 553 """Merge changeset from ctx (only) in the current working directory"""
555 554 wcpar = repo.dirstate.parents()[0]
556 555 if ctx.p1().node() == wcpar:
557 556 # edits are "in place" we do not need to make any merge,
558 557 # just applies changes on parent for editing
559 558 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
560 559 stats = None
561 560 else:
562 561 try:
563 562 # ui.forcemerge is an internal variable, do not document
564 563 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
565 564 'histedit')
566 565 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
567 566 finally:
568 567 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
569 568 return stats
570 569
571 570 def collapse(repo, first, last, commitopts, skipprompt=False):
572 571 """collapse the set of revisions from first to last as new one.
573 572
574 573 Expected commit options are:
575 574 - message
576 575 - date
577 576 - username
578 577 Commit message is edited in all cases.
579 578
580 579 This function works in memory."""
581 580 ctxs = list(repo.set('%d::%d', first, last))
582 581 if not ctxs:
583 582 return None
584 583 for c in ctxs:
585 584 if not c.mutable():
586 585 raise error.ParseError(
587 586 _("cannot fold into public change %s") % node.short(c.node()))
588 587 base = first.parents()[0]
589 588
590 589 # commit a new version of the old changeset, including the update
591 590 # collect all files which might be affected
592 591 files = set()
593 592 for ctx in ctxs:
594 593 files.update(ctx.files())
595 594
596 595 # Recompute copies (avoid recording a -> b -> a)
597 596 copied = copies.pathcopies(base, last)
598 597
599 598 # prune files which were reverted by the updates
600 599 files = [f for f in files if not cmdutil.samefile(f, last, base)]
601 600 # commit version of these files as defined by head
602 601 headmf = last.manifest()
603 602 def filectxfn(repo, ctx, path):
604 603 if path in headmf:
605 604 fctx = last[path]
606 605 flags = fctx.flags()
607 606 mctx = context.memfilectx(repo, ctx,
608 607 fctx.path(), fctx.data(),
609 608 islink='l' in flags,
610 609 isexec='x' in flags,
611 610 copied=copied.get(path))
612 611 return mctx
613 612 return None
614 613
615 614 if commitopts.get('message'):
616 615 message = commitopts['message']
617 616 else:
618 617 message = first.description()
619 618 user = commitopts.get('user')
620 619 date = commitopts.get('date')
621 620 extra = commitopts.get('extra')
622 621
623 622 parents = (first.p1().node(), first.p2().node())
624 623 editor = None
625 624 if not skipprompt:
626 625 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
627 626 new = context.memctx(repo,
628 627 parents=parents,
629 628 text=message,
630 629 files=files,
631 630 filectxfn=filectxfn,
632 631 user=user,
633 632 date=date,
634 633 extra=extra,
635 634 editor=editor)
636 635 return repo.commitctx(new)
637 636
638 637 def _isdirtywc(repo):
639 638 return repo[None].dirty(missing=True)
640 639
641 640 def abortdirty():
642 641 raise error.Abort(_('working copy has pending changes'),
643 642 hint=_('amend, commit, or revert them and run histedit '
644 643 '--continue, or abort with histedit --abort'))
645 644
646 645 def action(verbs, message, priority=False, internal=False):
647 646 def wrap(cls):
648 647 assert not priority or not internal
649 648 verb = verbs[0]
650 649 if priority:
651 650 primaryactions.add(verb)
652 651 elif internal:
653 652 internalactions.add(verb)
654 653 elif len(verbs) > 1:
655 654 secondaryactions.add(verb)
656 655 else:
657 656 tertiaryactions.add(verb)
658 657
659 658 cls.verb = verb
660 659 cls.verbs = verbs
661 660 cls.message = message
662 661 for verb in verbs:
663 662 actiontable[verb] = cls
664 663 return cls
665 664 return wrap
666 665
667 666 @action(['pick', 'p'],
668 667 _('use commit'),
669 668 priority=True)
670 669 class pick(histeditaction):
671 670 def run(self):
672 671 rulectx = self.repo[self.node]
673 672 if rulectx.parents()[0].node() == self.state.parentctxnode:
674 673 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
675 674 return rulectx, []
676 675
677 676 return super(pick, self).run()
678 677
679 678 @action(['edit', 'e'],
680 679 _('use commit, but stop for amending'),
681 680 priority=True)
682 681 class edit(histeditaction):
683 682 def run(self):
684 683 repo = self.repo
685 684 rulectx = repo[self.node]
686 685 hg.update(repo, self.state.parentctxnode, quietempty=True)
687 686 applychanges(repo.ui, repo, rulectx, {})
688 687 raise error.InterventionRequired(
689 688 _('Editing (%s), you may commit or record as needed now.')
690 689 % node.short(self.node),
691 690 hint=_('hg histedit --continue to resume'))
692 691
693 692 def commiteditor(self):
694 693 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
695 694
696 695 @action(['fold', 'f'],
697 696 _('use commit, but combine it with the one above'))
698 697 class fold(histeditaction):
699 698 def verify(self, prev, expected, seen):
700 699 """ Verifies semantic correctness of the fold rule"""
701 700 super(fold, self).verify(prev, expected, seen)
702 701 repo = self.repo
703 702 if not prev:
704 703 c = repo[self.node].parents()[0]
705 704 elif not prev.verb in ('pick', 'base'):
706 705 return
707 706 else:
708 707 c = repo[prev.node]
709 708 if not c.mutable():
710 709 raise error.ParseError(
711 710 _("cannot fold into public change %s") % node.short(c.node()))
712 711
713 712
714 713 def continuedirty(self):
715 714 repo = self.repo
716 715 rulectx = repo[self.node]
717 716
718 717 commit = commitfuncfor(repo, rulectx)
719 718 commit(text='fold-temp-revision %s' % node.short(self.node),
720 719 user=rulectx.user(), date=rulectx.date(),
721 720 extra=rulectx.extra())
722 721
723 722 def continueclean(self):
724 723 repo = self.repo
725 724 ctx = repo['.']
726 725 rulectx = repo[self.node]
727 726 parentctxnode = self.state.parentctxnode
728 727 if ctx.node() == parentctxnode:
729 728 repo.ui.warn(_('%s: empty changeset\n') %
730 729 node.short(self.node))
731 730 return ctx, [(self.node, (parentctxnode,))]
732 731
733 732 parentctx = repo[parentctxnode]
734 733 newcommits = set(c.node() for c in repo.set('(%d::. - %d)', parentctx,
735 734 parentctx))
736 735 if not newcommits:
737 736 repo.ui.warn(_('%s: cannot fold - working copy is not a '
738 737 'descendant of previous commit %s\n') %
739 738 (node.short(self.node), node.short(parentctxnode)))
740 739 return ctx, [(self.node, (ctx.node(),))]
741 740
742 741 middlecommits = newcommits.copy()
743 742 middlecommits.discard(ctx.node())
744 743
745 744 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
746 745 middlecommits)
747 746
748 747 def skipprompt(self):
749 748 """Returns true if the rule should skip the message editor.
750 749
751 750 For example, 'fold' wants to show an editor, but 'rollup'
752 751 doesn't want to.
753 752 """
754 753 return False
755 754
756 755 def mergedescs(self):
757 756 """Returns true if the rule should merge messages of multiple changes.
758 757
759 758 This exists mainly so that 'rollup' rules can be a subclass of
760 759 'fold'.
761 760 """
762 761 return True
763 762
764 763 def firstdate(self):
765 764 """Returns true if the rule should preserve the date of the first
766 765 change.
767 766
768 767 This exists mainly so that 'rollup' rules can be a subclass of
769 768 'fold'.
770 769 """
771 770 return False
772 771
773 772 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
774 773 parent = ctx.parents()[0].node()
775 774 repo.ui.pushbuffer()
776 775 hg.update(repo, parent)
777 776 repo.ui.popbuffer()
778 777 ### prepare new commit data
779 778 commitopts = {}
780 779 commitopts['user'] = ctx.user()
781 780 # commit message
782 781 if not self.mergedescs():
783 782 newmessage = ctx.description()
784 783 else:
785 784 newmessage = '\n***\n'.join(
786 785 [ctx.description()] +
787 786 [repo[r].description() for r in internalchanges] +
788 787 [oldctx.description()]) + '\n'
789 788 commitopts['message'] = newmessage
790 789 # date
791 790 if self.firstdate():
792 791 commitopts['date'] = ctx.date()
793 792 else:
794 793 commitopts['date'] = max(ctx.date(), oldctx.date())
795 794 extra = ctx.extra().copy()
796 795 # histedit_source
797 796 # note: ctx is likely a temporary commit but that the best we can do
798 797 # here. This is sufficient to solve issue3681 anyway.
799 798 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
800 799 commitopts['extra'] = extra
801 800 phasemin = max(ctx.phase(), oldctx.phase())
802 801 overrides = {('phases', 'new-commit'): phasemin}
803 802 with repo.ui.configoverride(overrides, 'histedit'):
804 803 n = collapse(repo, ctx, repo[newnode], commitopts,
805 804 skipprompt=self.skipprompt())
806 805 if n is None:
807 806 return ctx, []
808 807 repo.ui.pushbuffer()
809 808 hg.update(repo, n)
810 809 repo.ui.popbuffer()
811 810 replacements = [(oldctx.node(), (newnode,)),
812 811 (ctx.node(), (n,)),
813 812 (newnode, (n,)),
814 813 ]
815 814 for ich in internalchanges:
816 815 replacements.append((ich, (n,)))
817 816 return repo[n], replacements
818 817
819 818 @action(['base', 'b'],
820 819 _('checkout changeset and apply further changesets from there'))
821 820 class base(histeditaction):
822 821
823 822 def run(self):
824 823 if self.repo['.'].node() != self.node:
825 824 mergemod.update(self.repo, self.node, False, True)
826 825 # branchmerge, force)
827 826 return self.continueclean()
828 827
829 828 def continuedirty(self):
830 829 abortdirty()
831 830
832 831 def continueclean(self):
833 832 basectx = self.repo['.']
834 833 return basectx, []
835 834
836 835 def _verifynodeconstraints(self, prev, expected, seen):
837 836 # base can only be use with a node not in the edited set
838 837 if self.node in expected:
839 838 msg = _('%s "%s" changeset was an edited list candidate')
840 839 raise error.ParseError(
841 840 msg % (self.verb, node.short(self.node)),
842 841 hint=_('base must only use unlisted changesets'))
843 842
844 843 @action(['_multifold'],
845 844 _(
846 845 """fold subclass used for when multiple folds happen in a row
847 846
848 847 We only want to fire the editor for the folded message once when
849 848 (say) four changes are folded down into a single change. This is
850 849 similar to rollup, but we should preserve both messages so that
851 850 when the last fold operation runs we can show the user all the
852 851 commit messages in their editor.
853 852 """),
854 853 internal=True)
855 854 class _multifold(fold):
856 855 def skipprompt(self):
857 856 return True
858 857
859 858 @action(["roll", "r"],
860 859 _("like fold, but discard this commit's description and date"))
861 860 class rollup(fold):
862 861 def mergedescs(self):
863 862 return False
864 863
865 864 def skipprompt(self):
866 865 return True
867 866
868 867 def firstdate(self):
869 868 return True
870 869
871 870 @action(["drop", "d"],
872 871 _('remove commit from history'))
873 872 class drop(histeditaction):
874 873 def run(self):
875 874 parentctx = self.repo[self.state.parentctxnode]
876 875 return parentctx, [(self.node, tuple())]
877 876
878 877 @action(["mess", "m"],
879 878 _('edit commit message without changing commit content'),
880 879 priority=True)
881 880 class message(histeditaction):
882 881 def commiteditor(self):
883 882 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
884 883
885 884 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
886 885 """utility function to find the first outgoing changeset
887 886
888 887 Used by initialization code"""
889 888 if opts is None:
890 889 opts = {}
891 890 dest = ui.expandpath(remote or 'default-push', remote or 'default')
892 891 dest, revs = hg.parseurl(dest, None)[:2]
893 892 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
894 893
895 894 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
896 895 other = hg.peer(repo, opts, dest)
897 896
898 897 if revs:
899 898 revs = [repo.lookup(rev) for rev in revs]
900 899
901 900 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
902 901 if not outgoing.missing:
903 902 raise error.Abort(_('no outgoing ancestors'))
904 903 roots = list(repo.revs("roots(%ln)", outgoing.missing))
905 904 if 1 < len(roots):
906 905 msg = _('there are ambiguous outgoing revisions')
907 906 hint = _("see 'hg help histedit' for more detail")
908 907 raise error.Abort(msg, hint=hint)
909 908 return repo.lookup(roots[0])
910 909
911 910 @command('histedit',
912 911 [('', 'commands', '',
913 912 _('read history edits from the specified file'), _('FILE')),
914 913 ('c', 'continue', False, _('continue an edit already in progress')),
915 914 ('', 'edit-plan', False, _('edit remaining actions list')),
916 915 ('k', 'keep', False,
917 916 _("don't strip old nodes after edit is complete")),
918 917 ('', 'abort', False, _('abort an edit in progress')),
919 918 ('o', 'outgoing', False, _('changesets not found in destination')),
920 919 ('f', 'force', False,
921 920 _('force outgoing even for unrelated repositories')),
922 921 ('r', 'rev', [], _('first revision to be edited'), _('REV'))] +
923 922 cmdutil.formatteropts,
924 923 _("[OPTIONS] ([ANCESTOR] | --outgoing [URL])"))
925 924 def histedit(ui, repo, *freeargs, **opts):
926 925 """interactively edit changeset history
927 926
928 927 This command lets you edit a linear series of changesets (up to
929 928 and including the working directory, which should be clean).
930 929 You can:
931 930
932 931 - `pick` to [re]order a changeset
933 932
934 933 - `drop` to omit changeset
935 934
936 935 - `mess` to reword the changeset commit message
937 936
938 937 - `fold` to combine it with the preceding changeset (using the later date)
939 938
940 939 - `roll` like fold, but discarding this commit's description and date
941 940
942 941 - `edit` to edit this changeset (preserving date)
943 942
944 943 - `base` to checkout changeset and apply further changesets from there
945 944
946 945 There are a number of ways to select the root changeset:
947 946
948 947 - Specify ANCESTOR directly
949 948
950 949 - Use --outgoing -- it will be the first linear changeset not
951 950 included in destination. (See :hg:`help config.paths.default-push`)
952 951
953 952 - Otherwise, the value from the "histedit.defaultrev" config option
954 953 is used as a revset to select the base revision when ANCESTOR is not
955 954 specified. The first revision returned by the revset is used. By
956 955 default, this selects the editable history that is unique to the
957 956 ancestry of the working directory.
958 957
959 958 .. container:: verbose
960 959
961 960 If you use --outgoing, this command will abort if there are ambiguous
962 961 outgoing revisions. For example, if there are multiple branches
963 962 containing outgoing revisions.
964 963
965 964 Use "min(outgoing() and ::.)" or similar revset specification
966 965 instead of --outgoing to specify edit target revision exactly in
967 966 such ambiguous situation. See :hg:`help revsets` for detail about
968 967 selecting revisions.
969 968
970 969 .. container:: verbose
971 970
972 971 Examples:
973 972
974 973 - A number of changes have been made.
975 974 Revision 3 is no longer needed.
976 975
977 976 Start history editing from revision 3::
978 977
979 978 hg histedit -r 3
980 979
981 980 An editor opens, containing the list of revisions,
982 981 with specific actions specified::
983 982
984 983 pick 5339bf82f0ca 3 Zworgle the foobar
985 984 pick 8ef592ce7cc4 4 Bedazzle the zerlog
986 985 pick 0a9639fcda9d 5 Morgify the cromulancy
987 986
988 987 Additional information about the possible actions
989 988 to take appears below the list of revisions.
990 989
991 990 To remove revision 3 from the history,
992 991 its action (at the beginning of the relevant line)
993 992 is changed to 'drop'::
994 993
995 994 drop 5339bf82f0ca 3 Zworgle the foobar
996 995 pick 8ef592ce7cc4 4 Bedazzle the zerlog
997 996 pick 0a9639fcda9d 5 Morgify the cromulancy
998 997
999 998 - A number of changes have been made.
1000 999 Revision 2 and 4 need to be swapped.
1001 1000
1002 1001 Start history editing from revision 2::
1003 1002
1004 1003 hg histedit -r 2
1005 1004
1006 1005 An editor opens, containing the list of revisions,
1007 1006 with specific actions specified::
1008 1007
1009 1008 pick 252a1af424ad 2 Blorb a morgwazzle
1010 1009 pick 5339bf82f0ca 3 Zworgle the foobar
1011 1010 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1012 1011
1013 1012 To swap revision 2 and 4, its lines are swapped
1014 1013 in the editor::
1015 1014
1016 1015 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1017 1016 pick 5339bf82f0ca 3 Zworgle the foobar
1018 1017 pick 252a1af424ad 2 Blorb a morgwazzle
1019 1018
1020 1019 Returns 0 on success, 1 if user intervention is required (not only
1021 1020 for intentional "edit" command, but also for resolving unexpected
1022 1021 conflicts).
1023 1022 """
1024 1023 state = histeditstate(repo)
1025 1024 try:
1026 1025 state.wlock = repo.wlock()
1027 1026 state.lock = repo.lock()
1028 1027 _histedit(ui, repo, state, *freeargs, **opts)
1029 1028 finally:
1030 1029 release(state.lock, state.wlock)
1031 1030
1032 1031 goalcontinue = 'continue'
1033 1032 goalabort = 'abort'
1034 1033 goaleditplan = 'edit-plan'
1035 1034 goalnew = 'new'
1036 1035
1037 1036 def _getgoal(opts):
1038 1037 if opts.get('continue'):
1039 1038 return goalcontinue
1040 1039 if opts.get('abort'):
1041 1040 return goalabort
1042 1041 if opts.get('edit_plan'):
1043 1042 return goaleditplan
1044 1043 return goalnew
1045 1044
1046 1045 def _readfile(ui, path):
1047 1046 if path == '-':
1048 1047 with ui.timeblockedsection('histedit'):
1049 1048 return ui.fin.read()
1050 1049 else:
1051 1050 with open(path, 'rb') as f:
1052 1051 return f.read()
1053 1052
1054 1053 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1055 1054 # TODO only abort if we try to histedit mq patches, not just
1056 1055 # blanket if mq patches are applied somewhere
1057 1056 mq = getattr(repo, 'mq', None)
1058 1057 if mq and mq.applied:
1059 1058 raise error.Abort(_('source has mq patches applied'))
1060 1059
1061 1060 # basic argument incompatibility processing
1062 1061 outg = opts.get('outgoing')
1063 1062 editplan = opts.get('edit_plan')
1064 1063 abort = opts.get('abort')
1065 1064 force = opts.get('force')
1066 1065 if force and not outg:
1067 1066 raise error.Abort(_('--force only allowed with --outgoing'))
1068 1067 if goal == 'continue':
1069 1068 if any((outg, abort, revs, freeargs, rules, editplan)):
1070 1069 raise error.Abort(_('no arguments allowed with --continue'))
1071 1070 elif goal == 'abort':
1072 1071 if any((outg, revs, freeargs, rules, editplan)):
1073 1072 raise error.Abort(_('no arguments allowed with --abort'))
1074 1073 elif goal == 'edit-plan':
1075 1074 if any((outg, revs, freeargs)):
1076 1075 raise error.Abort(_('only --commands argument allowed with '
1077 1076 '--edit-plan'))
1078 1077 else:
1079 1078 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1080 1079 raise error.Abort(_('history edit already in progress, try '
1081 1080 '--continue or --abort'))
1082 1081 if outg:
1083 1082 if revs:
1084 1083 raise error.Abort(_('no revisions allowed with --outgoing'))
1085 1084 if len(freeargs) > 1:
1086 1085 raise error.Abort(
1087 1086 _('only one repo argument allowed with --outgoing'))
1088 1087 else:
1089 1088 revs.extend(freeargs)
1090 1089 if len(revs) == 0:
1091 1090 defaultrev = destutil.desthistedit(ui, repo)
1092 1091 if defaultrev is not None:
1093 1092 revs.append(defaultrev)
1094 1093
1095 1094 if len(revs) != 1:
1096 1095 raise error.Abort(
1097 1096 _('histedit requires exactly one ancestor revision'))
1098 1097
1099 1098 def _histedit(ui, repo, state, *freeargs, **opts):
1100 1099 opts = pycompat.byteskwargs(opts)
1101 1100 fm = ui.formatter('histedit', opts)
1102 1101 fm.startitem()
1103 1102 goal = _getgoal(opts)
1104 1103 revs = opts.get('rev', [])
1105 1104 rules = opts.get('commands', '')
1106 1105 state.keep = opts.get('keep', False)
1107 1106
1108 1107 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1109 1108
1110 1109 # rebuild state
1111 1110 if goal == goalcontinue:
1112 1111 state.read()
1113 1112 state = bootstrapcontinue(ui, state, opts)
1114 1113 elif goal == goaleditplan:
1115 1114 _edithisteditplan(ui, repo, state, rules)
1116 1115 return
1117 1116 elif goal == goalabort:
1118 1117 _aborthistedit(ui, repo, state)
1119 1118 return
1120 1119 else:
1121 1120 # goal == goalnew
1122 1121 _newhistedit(ui, repo, state, revs, freeargs, opts)
1123 1122
1124 1123 _continuehistedit(ui, repo, state)
1125 1124 _finishhistedit(ui, repo, state, fm)
1126 1125 fm.end()
1127 1126
1128 1127 def _continuehistedit(ui, repo, state):
1129 1128 """This function runs after either:
1130 1129 - bootstrapcontinue (if the goal is 'continue')
1131 1130 - _newhistedit (if the goal is 'new')
1132 1131 """
1133 1132 # preprocess rules so that we can hide inner folds from the user
1134 1133 # and only show one editor
1135 1134 actions = state.actions[:]
1136 1135 for idx, (action, nextact) in enumerate(
1137 1136 zip(actions, actions[1:] + [None])):
1138 1137 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1139 1138 state.actions[idx].__class__ = _multifold
1140 1139
1141 1140 # Force an initial state file write, so the user can run --abort/continue
1142 1141 # even if there's an exception before the first transaction serialize.
1143 1142 state.write()
1144 1143
1145 1144 total = len(state.actions)
1146 1145 pos = 0
1147 1146 tr = None
1148 1147 # Don't use singletransaction by default since it rolls the entire
1149 1148 # transaction back if an unexpected exception happens (like a
1150 1149 # pretxncommit hook throws, or the user aborts the commit msg editor).
1151 1150 if ui.configbool("histedit", "singletransaction"):
1152 1151 # Don't use a 'with' for the transaction, since actions may close
1153 1152 # and reopen a transaction. For example, if the action executes an
1154 1153 # external process it may choose to commit the transaction first.
1155 1154 tr = repo.transaction('histedit')
1156 1155 with util.acceptintervention(tr):
1157 1156 while state.actions:
1158 1157 state.write(tr=tr)
1159 1158 actobj = state.actions[0]
1160 1159 pos += 1
1161 1160 ui.progress(_("editing"), pos, actobj.torule(),
1162 1161 _('changes'), total)
1163 1162 ui.debug('histedit: processing %s %s\n' % (actobj.verb,\
1164 1163 actobj.torule()))
1165 1164 parentctx, replacement_ = actobj.run()
1166 1165 state.parentctxnode = parentctx.node()
1167 1166 state.replacements.extend(replacement_)
1168 1167 state.actions.pop(0)
1169 1168
1170 1169 state.write()
1171 1170 ui.progress(_("editing"), None)
1172 1171
1173 1172 def _finishhistedit(ui, repo, state, fm):
1174 1173 """This action runs when histedit is finishing its session"""
1175 1174 repo.ui.pushbuffer()
1176 1175 hg.update(repo, state.parentctxnode, quietempty=True)
1177 1176 repo.ui.popbuffer()
1178 1177
1179 1178 mapping, tmpnodes, created, ntm = processreplacement(state)
1180 1179 if mapping:
1181 1180 for prec, succs in mapping.iteritems():
1182 1181 if not succs:
1183 1182 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1184 1183 else:
1185 1184 ui.debug('histedit: %s is replaced by %s\n' % (
1186 1185 node.short(prec), node.short(succs[0])))
1187 1186 if len(succs) > 1:
1188 1187 m = 'histedit: %s'
1189 1188 for n in succs[1:]:
1190 1189 ui.debug(m % node.short(n))
1191 1190
1192 1191 if not state.keep:
1193 1192 if mapping:
1194 1193 movetopmostbookmarks(repo, state.topmost, ntm)
1195 1194 # TODO update mq state
1196 1195 else:
1197 1196 mapping = {}
1198 1197
1199 1198 for n in tmpnodes:
1200 1199 mapping[n] = ()
1201 1200
1202 1201 # remove entries about unknown nodes
1203 1202 nodemap = repo.unfiltered().changelog.nodemap
1204 1203 mapping = {k: v for k, v in mapping.items()
1205 1204 if k in nodemap and all(n in nodemap for n in v)}
1206 1205 scmutil.cleanupnodes(repo, mapping, 'histedit')
1207 1206 hf = fm.hexfunc
1208 1207 fl = fm.formatlist
1209 1208 fd = fm.formatdict
1210 1209 nodechanges = fd({hf(oldn): fl([hf(n) for n in newn], name='node')
1211 1210 for oldn, newn in mapping.iteritems()},
1212 1211 key="oldnode", value="newnodes")
1213 1212 fm.data(nodechanges=nodechanges)
1214 1213
1215 1214 state.clear()
1216 1215 if os.path.exists(repo.sjoin('undo')):
1217 1216 os.unlink(repo.sjoin('undo'))
1218 1217 if repo.vfs.exists('histedit-last-edit.txt'):
1219 1218 repo.vfs.unlink('histedit-last-edit.txt')
1220 1219
1221 1220 def _aborthistedit(ui, repo, state):
1222 1221 try:
1223 1222 state.read()
1224 1223 __, leafs, tmpnodes, __ = processreplacement(state)
1225 1224 ui.debug('restore wc to old parent %s\n'
1226 1225 % node.short(state.topmost))
1227 1226
1228 1227 # Recover our old commits if necessary
1229 1228 if not state.topmost in repo and state.backupfile:
1230 1229 backupfile = repo.vfs.join(state.backupfile)
1231 1230 f = hg.openpath(ui, backupfile)
1232 1231 gen = exchange.readbundle(ui, f, backupfile)
1233 1232 with repo.transaction('histedit.abort') as tr:
1234 1233 bundle2.applybundle(repo, gen, tr, source='histedit',
1235 1234 url='bundle:' + backupfile)
1236 1235
1237 1236 os.remove(backupfile)
1238 1237
1239 1238 # check whether we should update away
1240 1239 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1241 1240 state.parentctxnode, leafs | tmpnodes):
1242 1241 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1243 1242 cleanupnode(ui, repo, tmpnodes)
1244 1243 cleanupnode(ui, repo, leafs)
1245 1244 except Exception:
1246 1245 if state.inprogress():
1247 1246 ui.warn(_('warning: encountered an exception during histedit '
1248 1247 '--abort; the repository may not have been completely '
1249 1248 'cleaned up\n'))
1250 1249 raise
1251 1250 finally:
1252 1251 state.clear()
1253 1252
1254 1253 def _edithisteditplan(ui, repo, state, rules):
1255 1254 state.read()
1256 1255 if not rules:
1257 1256 comment = geteditcomment(ui,
1258 1257 node.short(state.parentctxnode),
1259 1258 node.short(state.topmost))
1260 1259 rules = ruleeditor(repo, ui, state.actions, comment)
1261 1260 else:
1262 1261 rules = _readfile(ui, rules)
1263 1262 actions = parserules(rules, state)
1264 1263 ctxs = [repo[act.node] \
1265 1264 for act in state.actions if act.node]
1266 1265 warnverifyactions(ui, repo, actions, state, ctxs)
1267 1266 state.actions = actions
1268 1267 state.write()
1269 1268
1270 1269 def _newhistedit(ui, repo, state, revs, freeargs, opts):
1271 1270 outg = opts.get('outgoing')
1272 1271 rules = opts.get('commands', '')
1273 1272 force = opts.get('force')
1274 1273
1275 1274 cmdutil.checkunfinished(repo)
1276 1275 cmdutil.bailifchanged(repo)
1277 1276
1278 1277 topmost, empty = repo.dirstate.parents()
1279 1278 if outg:
1280 1279 if freeargs:
1281 1280 remote = freeargs[0]
1282 1281 else:
1283 1282 remote = None
1284 1283 root = findoutgoing(ui, repo, remote, force, opts)
1285 1284 else:
1286 1285 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1287 1286 if len(rr) != 1:
1288 1287 raise error.Abort(_('The specified revisions must have '
1289 1288 'exactly one common root'))
1290 1289 root = rr[0].node()
1291 1290
1292 1291 revs = between(repo, root, topmost, state.keep)
1293 1292 if not revs:
1294 1293 raise error.Abort(_('%s is not an ancestor of working directory') %
1295 1294 node.short(root))
1296 1295
1297 1296 ctxs = [repo[r] for r in revs]
1298 1297 if not rules:
1299 1298 comment = geteditcomment(ui, node.short(root), node.short(topmost))
1300 1299 actions = [pick(state, r) for r in revs]
1301 1300 rules = ruleeditor(repo, ui, actions, comment)
1302 1301 else:
1303 1302 rules = _readfile(ui, rules)
1304 1303 actions = parserules(rules, state)
1305 1304 warnverifyactions(ui, repo, actions, state, ctxs)
1306 1305
1307 1306 parentctxnode = repo[root].parents()[0].node()
1308 1307
1309 1308 state.parentctxnode = parentctxnode
1310 1309 state.actions = actions
1311 1310 state.topmost = topmost
1312 1311 state.replacements = []
1313 1312
1314 1313 ui.log("histedit", "%d actions to histedit", len(actions),
1315 1314 histedit_num_actions=len(actions))
1316 1315
1317 1316 # Create a backup so we can always abort completely.
1318 1317 backupfile = None
1319 1318 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1320 1319 backupfile = repair._bundle(repo, [parentctxnode], [topmost], root,
1321 1320 'histedit')
1322 1321 state.backupfile = backupfile
1323 1322
1324 1323 def _getsummary(ctx):
1325 1324 # a common pattern is to extract the summary but default to the empty
1326 1325 # string
1327 1326 summary = ctx.description() or ''
1328 1327 if summary:
1329 1328 summary = summary.splitlines()[0]
1330 1329 return summary
1331 1330
1332 1331 def bootstrapcontinue(ui, state, opts):
1333 1332 repo = state.repo
1334 1333
1335 1334 ms = mergemod.mergestate.read(repo)
1336 1335 mergeutil.checkunresolved(ms)
1337 1336
1338 1337 if state.actions:
1339 1338 actobj = state.actions.pop(0)
1340 1339
1341 1340 if _isdirtywc(repo):
1342 1341 actobj.continuedirty()
1343 1342 if _isdirtywc(repo):
1344 1343 abortdirty()
1345 1344
1346 1345 parentctx, replacements = actobj.continueclean()
1347 1346
1348 1347 state.parentctxnode = parentctx.node()
1349 1348 state.replacements.extend(replacements)
1350 1349
1351 1350 return state
1352 1351
1353 1352 def between(repo, old, new, keep):
1354 1353 """select and validate the set of revision to edit
1355 1354
1356 1355 When keep is false, the specified set can't have children."""
1357 1356 ctxs = list(repo.set('%n::%n', old, new))
1358 1357 if ctxs and not keep:
1359 1358 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1360 1359 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
1361 1360 raise error.Abort(_('can only histedit a changeset together '
1362 1361 'with all its descendants'))
1363 1362 if repo.revs('(%ld) and merge()', ctxs):
1364 1363 raise error.Abort(_('cannot edit history that contains merges'))
1365 1364 root = ctxs[0] # list is already sorted by repo.set
1366 1365 if not root.mutable():
1367 1366 raise error.Abort(_('cannot edit public changeset: %s') % root,
1368 1367 hint=_("see 'hg help phases' for details"))
1369 1368 return [c.node() for c in ctxs]
1370 1369
1371 1370 def ruleeditor(repo, ui, actions, editcomment=""):
1372 1371 """open an editor to edit rules
1373 1372
1374 1373 rules are in the format [ [act, ctx], ...] like in state.rules
1375 1374 """
1376 1375 if repo.ui.configbool("experimental", "histedit.autoverb"):
1377 1376 newact = util.sortdict()
1378 1377 for act in actions:
1379 1378 ctx = repo[act.node]
1380 1379 summary = _getsummary(ctx)
1381 1380 fword = summary.split(' ', 1)[0].lower()
1382 1381 added = False
1383 1382
1384 1383 # if it doesn't end with the special character '!' just skip this
1385 1384 if fword.endswith('!'):
1386 1385 fword = fword[:-1]
1387 1386 if fword in primaryactions | secondaryactions | tertiaryactions:
1388 1387 act.verb = fword
1389 1388 # get the target summary
1390 1389 tsum = summary[len(fword) + 1:].lstrip()
1391 1390 # safe but slow: reverse iterate over the actions so we
1392 1391 # don't clash on two commits having the same summary
1393 1392 for na, l in reversed(list(newact.iteritems())):
1394 1393 actx = repo[na.node]
1395 1394 asum = _getsummary(actx)
1396 1395 if asum == tsum:
1397 1396 added = True
1398 1397 l.append(act)
1399 1398 break
1400 1399
1401 1400 if not added:
1402 1401 newact[act] = []
1403 1402
1404 1403 # copy over and flatten the new list
1405 1404 actions = []
1406 1405 for na, l in newact.iteritems():
1407 1406 actions.append(na)
1408 1407 actions += l
1409 1408
1410 1409 rules = '\n'.join([act.torule() for act in actions])
1411 1410 rules += '\n\n'
1412 1411 rules += editcomment
1413 1412 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'},
1414 1413 repopath=repo.path, action='histedit')
1415 1414
1416 1415 # Save edit rules in .hg/histedit-last-edit.txt in case
1417 1416 # the user needs to ask for help after something
1418 1417 # surprising happens.
1419 1418 with repo.vfs('histedit-last-edit.txt', 'wb') as f:
1420 1419 f.write(rules)
1421 1420
1422 1421 return rules
1423 1422
1424 1423 def parserules(rules, state):
1425 1424 """Read the histedit rules string and return list of action objects """
1426 1425 rules = [l for l in (r.strip() for r in rules.splitlines())
1427 1426 if l and not l.startswith('#')]
1428 1427 actions = []
1429 1428 for r in rules:
1430 1429 if ' ' not in r:
1431 1430 raise error.ParseError(_('malformed line "%s"') % r)
1432 1431 verb, rest = r.split(' ', 1)
1433 1432
1434 1433 if verb not in actiontable:
1435 1434 raise error.ParseError(_('unknown action "%s"') % verb)
1436 1435
1437 1436 action = actiontable[verb].fromrule(state, rest)
1438 1437 actions.append(action)
1439 1438 return actions
1440 1439
1441 1440 def warnverifyactions(ui, repo, actions, state, ctxs):
1442 1441 try:
1443 1442 verifyactions(actions, state, ctxs)
1444 1443 except error.ParseError:
1445 1444 if repo.vfs.exists('histedit-last-edit.txt'):
1446 1445 ui.warn(_('warning: histedit rules saved '
1447 1446 'to: .hg/histedit-last-edit.txt\n'))
1448 1447 raise
1449 1448
1450 1449 def verifyactions(actions, state, ctxs):
1451 1450 """Verify that there exists exactly one action per given changeset and
1452 1451 other constraints.
1453 1452
1454 1453 Will abort if there are to many or too few rules, a malformed rule,
1455 1454 or a rule on a changeset outside of the user-given range.
1456 1455 """
1457 1456 expected = set(c.node() for c in ctxs)
1458 1457 seen = set()
1459 1458 prev = None
1460 1459
1461 1460 if actions and actions[0].verb in ['roll', 'fold']:
1462 1461 raise error.ParseError(_('first changeset cannot use verb "%s"') %
1463 1462 actions[0].verb)
1464 1463
1465 1464 for action in actions:
1466 1465 action.verify(prev, expected, seen)
1467 1466 prev = action
1468 1467 if action.node is not None:
1469 1468 seen.add(action.node)
1470 1469 missing = sorted(expected - seen) # sort to stabilize output
1471 1470
1472 1471 if state.repo.ui.configbool('histedit', 'dropmissing'):
1473 1472 if len(actions) == 0:
1474 1473 raise error.ParseError(_('no rules provided'),
1475 1474 hint=_('use strip extension to remove commits'))
1476 1475
1477 1476 drops = [drop(state, n) for n in missing]
1478 1477 # put the in the beginning so they execute immediately and
1479 1478 # don't show in the edit-plan in the future
1480 1479 actions[:0] = drops
1481 1480 elif missing:
1482 1481 raise error.ParseError(_('missing rules for changeset %s') %
1483 1482 node.short(missing[0]),
1484 1483 hint=_('use "drop %s" to discard, see also: '
1485 1484 "'hg help -e histedit.config'")
1486 1485 % node.short(missing[0]))
1487 1486
1488 1487 def adjustreplacementsfrommarkers(repo, oldreplacements):
1489 1488 """Adjust replacements from obsolescence markers
1490 1489
1491 1490 Replacements structure is originally generated based on
1492 1491 histedit's state and does not account for changes that are
1493 1492 not recorded there. This function fixes that by adding
1494 1493 data read from obsolescence markers"""
1495 1494 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1496 1495 return oldreplacements
1497 1496
1498 1497 unfi = repo.unfiltered()
1499 1498 nm = unfi.changelog.nodemap
1500 1499 obsstore = repo.obsstore
1501 1500 newreplacements = list(oldreplacements)
1502 1501 oldsuccs = [r[1] for r in oldreplacements]
1503 1502 # successors that have already been added to succstocheck once
1504 1503 seensuccs = set().union(*oldsuccs) # create a set from an iterable of tuples
1505 1504 succstocheck = list(seensuccs)
1506 1505 while succstocheck:
1507 1506 n = succstocheck.pop()
1508 1507 missing = nm.get(n) is None
1509 1508 markers = obsstore.successors.get(n, ())
1510 1509 if missing and not markers:
1511 1510 # dead end, mark it as such
1512 1511 newreplacements.append((n, ()))
1513 1512 for marker in markers:
1514 1513 nsuccs = marker[1]
1515 1514 newreplacements.append((n, nsuccs))
1516 1515 for nsucc in nsuccs:
1517 1516 if nsucc not in seensuccs:
1518 1517 seensuccs.add(nsucc)
1519 1518 succstocheck.append(nsucc)
1520 1519
1521 1520 return newreplacements
1522 1521
1523 1522 def processreplacement(state):
1524 1523 """process the list of replacements to return
1525 1524
1526 1525 1) the final mapping between original and created nodes
1527 1526 2) the list of temporary node created by histedit
1528 1527 3) the list of new commit created by histedit"""
1529 1528 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
1530 1529 allsuccs = set()
1531 1530 replaced = set()
1532 1531 fullmapping = {}
1533 1532 # initialize basic set
1534 1533 # fullmapping records all operations recorded in replacement
1535 1534 for rep in replacements:
1536 1535 allsuccs.update(rep[1])
1537 1536 replaced.add(rep[0])
1538 1537 fullmapping.setdefault(rep[0], set()).update(rep[1])
1539 1538 new = allsuccs - replaced
1540 1539 tmpnodes = allsuccs & replaced
1541 1540 # Reduce content fullmapping into direct relation between original nodes
1542 1541 # and final node created during history edition
1543 1542 # Dropped changeset are replaced by an empty list
1544 1543 toproceed = set(fullmapping)
1545 1544 final = {}
1546 1545 while toproceed:
1547 1546 for x in list(toproceed):
1548 1547 succs = fullmapping[x]
1549 1548 for s in list(succs):
1550 1549 if s in toproceed:
1551 1550 # non final node with unknown closure
1552 1551 # We can't process this now
1553 1552 break
1554 1553 elif s in final:
1555 1554 # non final node, replace with closure
1556 1555 succs.remove(s)
1557 1556 succs.update(final[s])
1558 1557 else:
1559 1558 final[x] = succs
1560 1559 toproceed.remove(x)
1561 1560 # remove tmpnodes from final mapping
1562 1561 for n in tmpnodes:
1563 1562 del final[n]
1564 1563 # we expect all changes involved in final to exist in the repo
1565 1564 # turn `final` into list (topologically sorted)
1566 1565 nm = state.repo.changelog.nodemap
1567 1566 for prec, succs in final.items():
1568 1567 final[prec] = sorted(succs, key=nm.get)
1569 1568
1570 1569 # computed topmost element (necessary for bookmark)
1571 1570 if new:
1572 1571 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1573 1572 elif not final:
1574 1573 # Nothing rewritten at all. we won't need `newtopmost`
1575 1574 # It is the same as `oldtopmost` and `processreplacement` know it
1576 1575 newtopmost = None
1577 1576 else:
1578 1577 # every body died. The newtopmost is the parent of the root.
1579 1578 r = state.repo.changelog.rev
1580 1579 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1581 1580
1582 1581 return final, tmpnodes, new, newtopmost
1583 1582
1584 1583 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
1585 1584 """Move bookmark from oldtopmost to newly created topmost
1586 1585
1587 1586 This is arguably a feature and we may only want that for the active
1588 1587 bookmark. But the behavior is kept compatible with the old version for now.
1589 1588 """
1590 1589 if not oldtopmost or not newtopmost:
1591 1590 return
1592 1591 oldbmarks = repo.nodebookmarks(oldtopmost)
1593 1592 if oldbmarks:
1594 1593 with repo.lock(), repo.transaction('histedit') as tr:
1595 1594 marks = repo._bookmarks
1596 1595 changes = []
1597 1596 for name in oldbmarks:
1598 1597 changes.append((name, newtopmost))
1599 1598 marks.applychanges(repo, tr, changes)
1600 1599
1601 1600 def cleanupnode(ui, repo, nodes):
1602 1601 """strip a group of nodes from the repository
1603 1602
1604 1603 The set of node to strip may contains unknown nodes."""
1605 1604 with repo.lock():
1606 1605 # do not let filtering get in the way of the cleanse
1607 1606 # we should probably get rid of obsolescence marker created during the
1608 1607 # histedit, but we currently do not have such information.
1609 1608 repo = repo.unfiltered()
1610 1609 # Find all nodes that need to be stripped
1611 1610 # (we use %lr instead of %ln to silently ignore unknown items)
1612 1611 nm = repo.changelog.nodemap
1613 1612 nodes = sorted(n for n in nodes if n in nm)
1614 1613 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1615 1614 if roots:
1616 1615 repair.strip(ui, repo, roots)
1617 1616
1618 1617 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1619 1618 if isinstance(nodelist, str):
1620 1619 nodelist = [nodelist]
1621 1620 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1622 1621 state = histeditstate(repo)
1623 1622 state.read()
1624 1623 histedit_nodes = {action.node for action
1625 1624 in state.actions if action.node}
1626 1625 common_nodes = histedit_nodes & set(nodelist)
1627 1626 if common_nodes:
1628 1627 raise error.Abort(_("histedit in progress, can't strip %s")
1629 1628 % ', '.join(node.short(x) for x in common_nodes))
1630 1629 return orig(ui, repo, nodelist, *args, **kwargs)
1631 1630
1632 1631 extensions.wrapfunction(repair, 'strip', stripwrapper)
1633 1632
1634 1633 def summaryhook(ui, repo):
1635 1634 if not os.path.exists(repo.vfs.join('histedit-state')):
1636 1635 return
1637 1636 state = histeditstate(repo)
1638 1637 state.read()
1639 1638 if state.actions:
1640 1639 # i18n: column positioning for "hg summary"
1641 1640 ui.write(_('hist: %s (histedit --continue)\n') %
1642 1641 (ui.label(_('%d remaining'), 'histedit.remaining') %
1643 1642 len(state.actions)))
1644 1643
1645 1644 def extsetup(ui):
1646 1645 cmdutil.summaryhooks.add('histedit', summaryhook)
1647 1646 cmdutil.unfinishedstates.append(
1648 1647 ['histedit-state', False, True, _('histedit in progress'),
1649 1648 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
1650 1649 cmdutil.afterresolvedstates.append(
1651 1650 ['histedit-state', _('hg histedit --continue')])
@@ -1,35 +1,42 b''
1 1 # node.py - basic nodeid manipulation for mercurial
2 2 #
3 3 # Copyright 2005, 2006 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 binascii
11 11
12 12 # This ugly style has a noticeable effect in manifest parsing
13 13 hex = binascii.hexlify
14 bin = binascii.unhexlify
14 # Adapt to Python 3 API changes. If this ends up showing up in
15 # profiles, we can use this version only on Python 3, and forward
16 # binascii.unhexlify like we used to on Python 2.
17 def bin(s):
18 try:
19 return binascii.unhexlify(s)
20 except binascii.Error as e:
21 raise TypeError(e)
15 22
16 23 nullrev = -1
17 24 nullid = b"\0" * 20
18 25 nullhex = hex(nullid)
19 26
20 27 # Phony node value to stand-in for new files in some uses of
21 28 # manifests.
22 29 newnodeid = '!' * 20
23 30 addednodeid = ('0' * 15) + 'added'
24 31 modifiednodeid = ('0' * 12) + 'modified'
25 32
26 33 wdirnodes = {newnodeid, addednodeid, modifiednodeid}
27 34
28 35 # pseudo identifiers for working directory
29 36 # (they are experimental, so don't add too many dependencies on them)
30 37 wdirrev = 0x7fffffff
31 38 wdirid = b"\xff" * 20
32 39 wdirhex = hex(wdirid)
33 40
34 41 def short(node):
35 42 return hex(node[:6])
@@ -1,2501 +1,2500 b''
1 1 # revlog.py - storage back-end 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 """Storage back-end for Mercurial.
9 9
10 10 This provides efficient delta storage with O(1) retrieve and append
11 11 and O(changes) merge between branches.
12 12 """
13 13
14 14 from __future__ import absolute_import
15 15
16 import binascii
17 16 import collections
18 17 import contextlib
19 18 import errno
20 19 import hashlib
21 20 import heapq
22 21 import os
23 22 import struct
24 23 import zlib
25 24
26 25 # import stuff from node for others to import from revlog
27 26 from .node import (
28 27 bin,
29 28 hex,
30 29 nullid,
31 30 nullrev,
32 31 wdirhex,
33 32 wdirid,
34 33 wdirrev,
35 34 )
36 35 from .i18n import _
37 36 from .thirdparty import (
38 37 attr,
39 38 )
40 39 from . import (
41 40 ancestor,
42 41 error,
43 42 mdiff,
44 43 policy,
45 44 pycompat,
46 45 templatefilters,
47 46 util,
48 47 )
49 48
50 49 parsers = policy.importmod(r'parsers')
51 50
52 51 # Aliased for performance.
53 52 _zlibdecompress = zlib.decompress
54 53
55 54 # revlog header flags
56 55 REVLOGV0 = 0
57 56 REVLOGV1 = 1
58 57 # Dummy value until file format is finalized.
59 58 # Reminder: change the bounds check in revlog.__init__ when this is changed.
60 59 REVLOGV2 = 0xDEAD
61 60 FLAG_INLINE_DATA = (1 << 16)
62 61 FLAG_GENERALDELTA = (1 << 17)
63 62 REVLOG_DEFAULT_FLAGS = FLAG_INLINE_DATA
64 63 REVLOG_DEFAULT_FORMAT = REVLOGV1
65 64 REVLOG_DEFAULT_VERSION = REVLOG_DEFAULT_FORMAT | REVLOG_DEFAULT_FLAGS
66 65 REVLOGV1_FLAGS = FLAG_INLINE_DATA | FLAG_GENERALDELTA
67 66 REVLOGV2_FLAGS = REVLOGV1_FLAGS
68 67
69 68 # revlog index flags
70 69 REVIDX_ISCENSORED = (1 << 15) # revision has censor metadata, must be verified
71 70 REVIDX_ELLIPSIS = (1 << 14) # revision hash does not match data (narrowhg)
72 71 REVIDX_EXTSTORED = (1 << 13) # revision data is stored externally
73 72 REVIDX_DEFAULT_FLAGS = 0
74 73 # stable order in which flags need to be processed and their processors applied
75 74 REVIDX_FLAGS_ORDER = [
76 75 REVIDX_ISCENSORED,
77 76 REVIDX_ELLIPSIS,
78 77 REVIDX_EXTSTORED,
79 78 ]
80 79 REVIDX_KNOWN_FLAGS = util.bitsfrom(REVIDX_FLAGS_ORDER)
81 80
82 81 # max size of revlog with inline data
83 82 _maxinline = 131072
84 83 _chunksize = 1048576
85 84
86 85 RevlogError = error.RevlogError
87 86 LookupError = error.LookupError
88 87 CensoredNodeError = error.CensoredNodeError
89 88 ProgrammingError = error.ProgrammingError
90 89
91 90 # Store flag processors (cf. 'addflagprocessor()' to register)
92 91 _flagprocessors = {
93 92 REVIDX_ISCENSORED: None,
94 93 }
95 94
96 95 def addflagprocessor(flag, processor):
97 96 """Register a flag processor on a revision data flag.
98 97
99 98 Invariant:
100 99 - Flags need to be defined in REVIDX_KNOWN_FLAGS and REVIDX_FLAGS_ORDER.
101 100 - Only one flag processor can be registered on a specific flag.
102 101 - flagprocessors must be 3-tuples of functions (read, write, raw) with the
103 102 following signatures:
104 103 - (read) f(self, rawtext) -> text, bool
105 104 - (write) f(self, text) -> rawtext, bool
106 105 - (raw) f(self, rawtext) -> bool
107 106 "text" is presented to the user. "rawtext" is stored in revlog data, not
108 107 directly visible to the user.
109 108 The boolean returned by these transforms is used to determine whether
110 109 the returned text can be used for hash integrity checking. For example,
111 110 if "write" returns False, then "text" is used to generate hash. If
112 111 "write" returns True, that basically means "rawtext" returned by "write"
113 112 should be used to generate hash. Usually, "write" and "read" return
114 113 different booleans. And "raw" returns a same boolean as "write".
115 114
116 115 Note: The 'raw' transform is used for changegroup generation and in some
117 116 debug commands. In this case the transform only indicates whether the
118 117 contents can be used for hash integrity checks.
119 118 """
120 119 if not flag & REVIDX_KNOWN_FLAGS:
121 120 msg = _("cannot register processor on unknown flag '%#x'.") % (flag)
122 121 raise ProgrammingError(msg)
123 122 if flag not in REVIDX_FLAGS_ORDER:
124 123 msg = _("flag '%#x' undefined in REVIDX_FLAGS_ORDER.") % (flag)
125 124 raise ProgrammingError(msg)
126 125 if flag in _flagprocessors:
127 126 msg = _("cannot register multiple processors on flag '%#x'.") % (flag)
128 127 raise error.Abort(msg)
129 128 _flagprocessors[flag] = processor
130 129
131 130 def getoffset(q):
132 131 return int(q >> 16)
133 132
134 133 def gettype(q):
135 134 return int(q & 0xFFFF)
136 135
137 136 def offset_type(offset, type):
138 137 if (type & ~REVIDX_KNOWN_FLAGS) != 0:
139 138 raise ValueError('unknown revlog index flags')
140 139 return int(int(offset) << 16 | type)
141 140
142 141 _nullhash = hashlib.sha1(nullid)
143 142
144 143 def hash(text, p1, p2):
145 144 """generate a hash from the given text and its parent hashes
146 145
147 146 This hash combines both the current file contents and its history
148 147 in a manner that makes it easy to distinguish nodes with the same
149 148 content in the revision graph.
150 149 """
151 150 # As of now, if one of the parent node is null, p2 is null
152 151 if p2 == nullid:
153 152 # deep copy of a hash is faster than creating one
154 153 s = _nullhash.copy()
155 154 s.update(p1)
156 155 else:
157 156 # none of the parent nodes are nullid
158 157 if p1 < p2:
159 158 a = p1
160 159 b = p2
161 160 else:
162 161 a = p2
163 162 b = p1
164 163 s = hashlib.sha1(a)
165 164 s.update(b)
166 165 s.update(text)
167 166 return s.digest()
168 167
169 168 def _trimchunk(revlog, revs, startidx, endidx=None):
170 169 """returns revs[startidx:endidx] without empty trailing revs
171 170 """
172 171 length = revlog.length
173 172
174 173 if endidx is None:
175 174 endidx = len(revs)
176 175
177 176 # Trim empty revs at the end, but never the very first revision of a chain
178 177 while endidx > 1 and endidx > startidx and length(revs[endidx - 1]) == 0:
179 178 endidx -= 1
180 179
181 180 return revs[startidx:endidx]
182 181
183 182 def _slicechunk(revlog, revs):
184 183 """slice revs to reduce the amount of unrelated data to be read from disk.
185 184
186 185 ``revs`` is sliced into groups that should be read in one time.
187 186 Assume that revs are sorted.
188 187 """
189 188 start = revlog.start
190 189 length = revlog.length
191 190
192 191 if len(revs) <= 1:
193 192 yield revs
194 193 return
195 194
196 195 startbyte = start(revs[0])
197 196 endbyte = start(revs[-1]) + length(revs[-1])
198 197 readdata = deltachainspan = endbyte - startbyte
199 198
200 199 chainpayload = sum(length(r) for r in revs)
201 200
202 201 if deltachainspan:
203 202 density = chainpayload / float(deltachainspan)
204 203 else:
205 204 density = 1.0
206 205
207 206 # Store the gaps in a heap to have them sorted by decreasing size
208 207 gapsheap = []
209 208 heapq.heapify(gapsheap)
210 209 prevend = None
211 210 for i, rev in enumerate(revs):
212 211 revstart = start(rev)
213 212 revlen = length(rev)
214 213
215 214 # Skip empty revisions to form larger holes
216 215 if revlen == 0:
217 216 continue
218 217
219 218 if prevend is not None:
220 219 gapsize = revstart - prevend
221 220 # only consider holes that are large enough
222 221 if gapsize > revlog._srmingapsize:
223 222 heapq.heappush(gapsheap, (-gapsize, i))
224 223
225 224 prevend = revstart + revlen
226 225
227 226 # Collect the indices of the largest holes until the density is acceptable
228 227 indicesheap = []
229 228 heapq.heapify(indicesheap)
230 229 while gapsheap and density < revlog._srdensitythreshold:
231 230 oppgapsize, gapidx = heapq.heappop(gapsheap)
232 231
233 232 heapq.heappush(indicesheap, gapidx)
234 233
235 234 # the gap sizes are stored as negatives to be sorted decreasingly
236 235 # by the heap
237 236 readdata -= (-oppgapsize)
238 237 if readdata > 0:
239 238 density = chainpayload / float(readdata)
240 239 else:
241 240 density = 1.0
242 241
243 242 # Cut the revs at collected indices
244 243 previdx = 0
245 244 while indicesheap:
246 245 idx = heapq.heappop(indicesheap)
247 246
248 247 chunk = _trimchunk(revlog, revs, previdx, idx)
249 248 if chunk:
250 249 yield chunk
251 250
252 251 previdx = idx
253 252
254 253 chunk = _trimchunk(revlog, revs, previdx)
255 254 if chunk:
256 255 yield chunk
257 256
258 257 @attr.s(slots=True, frozen=True)
259 258 class _deltainfo(object):
260 259 distance = attr.ib()
261 260 deltalen = attr.ib()
262 261 data = attr.ib()
263 262 base = attr.ib()
264 263 chainbase = attr.ib()
265 264 chainlen = attr.ib()
266 265 compresseddeltalen = attr.ib()
267 266
268 267 class _deltacomputer(object):
269 268 def __init__(self, revlog):
270 269 self.revlog = revlog
271 270
272 271 def _getcandidaterevs(self, p1, p2, cachedelta):
273 272 """
274 273 Provides revisions that present an interest to be diffed against,
275 274 grouped by level of easiness.
276 275 """
277 276 revlog = self.revlog
278 277 curr = len(revlog)
279 278 prev = curr - 1
280 279 p1r, p2r = revlog.rev(p1), revlog.rev(p2)
281 280
282 281 # should we try to build a delta?
283 282 if prev != nullrev and revlog.storedeltachains:
284 283 tested = set()
285 284 # This condition is true most of the time when processing
286 285 # changegroup data into a generaldelta repo. The only time it
287 286 # isn't true is if this is the first revision in a delta chain
288 287 # or if ``format.generaldelta=true`` disabled ``lazydeltabase``.
289 288 if cachedelta and revlog._generaldelta and revlog._lazydeltabase:
290 289 # Assume what we received from the server is a good choice
291 290 # build delta will reuse the cache
292 291 yield (cachedelta[0],)
293 292 tested.add(cachedelta[0])
294 293
295 294 if revlog._generaldelta:
296 295 # exclude already lazy tested base if any
297 296 parents = [p for p in (p1r, p2r)
298 297 if p != nullrev and p not in tested]
299 298 if parents and not revlog._aggressivemergedeltas:
300 299 # Pick whichever parent is closer to us (to minimize the
301 300 # chance of having to build a fulltext).
302 301 parents = [max(parents)]
303 302 tested.update(parents)
304 303 yield parents
305 304
306 305 if prev not in tested:
307 306 # other approach failed try against prev to hopefully save us a
308 307 # fulltext.
309 308 yield (prev,)
310 309
311 310 def buildtext(self, revinfo, fh):
312 311 """Builds a fulltext version of a revision
313 312
314 313 revinfo: _revisioninfo instance that contains all needed info
315 314 fh: file handle to either the .i or the .d revlog file,
316 315 depending on whether it is inlined or not
317 316 """
318 317 btext = revinfo.btext
319 318 if btext[0] is not None:
320 319 return btext[0]
321 320
322 321 revlog = self.revlog
323 322 cachedelta = revinfo.cachedelta
324 323 flags = revinfo.flags
325 324 node = revinfo.node
326 325
327 326 baserev = cachedelta[0]
328 327 delta = cachedelta[1]
329 328 # special case deltas which replace entire base; no need to decode
330 329 # base revision. this neatly avoids censored bases, which throw when
331 330 # they're decoded.
332 331 hlen = struct.calcsize(">lll")
333 332 if delta[:hlen] == mdiff.replacediffheader(revlog.rawsize(baserev),
334 333 len(delta) - hlen):
335 334 btext[0] = delta[hlen:]
336 335 else:
337 336 basetext = revlog.revision(baserev, _df=fh, raw=True)
338 337 btext[0] = mdiff.patch(basetext, delta)
339 338
340 339 try:
341 340 res = revlog._processflags(btext[0], flags, 'read', raw=True)
342 341 btext[0], validatehash = res
343 342 if validatehash:
344 343 revlog.checkhash(btext[0], node, p1=revinfo.p1, p2=revinfo.p2)
345 344 if flags & REVIDX_ISCENSORED:
346 345 raise RevlogError(_('node %s is not censored') % node)
347 346 except CensoredNodeError:
348 347 # must pass the censored index flag to add censored revisions
349 348 if not flags & REVIDX_ISCENSORED:
350 349 raise
351 350 return btext[0]
352 351
353 352 def _builddeltadiff(self, base, revinfo, fh):
354 353 revlog = self.revlog
355 354 t = self.buildtext(revinfo, fh)
356 355 if revlog.iscensored(base):
357 356 # deltas based on a censored revision must replace the
358 357 # full content in one patch, so delta works everywhere
359 358 header = mdiff.replacediffheader(revlog.rawsize(base), len(t))
360 359 delta = header + t
361 360 else:
362 361 ptext = revlog.revision(base, _df=fh, raw=True)
363 362 delta = mdiff.textdiff(ptext, t)
364 363
365 364 return delta
366 365
367 366 def _builddeltainfo(self, revinfo, base, fh):
368 367 # can we use the cached delta?
369 368 if revinfo.cachedelta and revinfo.cachedelta[0] == base:
370 369 delta = revinfo.cachedelta[1]
371 370 else:
372 371 delta = self._builddeltadiff(base, revinfo, fh)
373 372 revlog = self.revlog
374 373 header, data = revlog.compress(delta)
375 374 deltalen = len(header) + len(data)
376 375 chainbase = revlog.chainbase(base)
377 376 offset = revlog.end(len(revlog) - 1)
378 377 dist = deltalen + offset - revlog.start(chainbase)
379 378 if revlog._generaldelta:
380 379 deltabase = base
381 380 else:
382 381 deltabase = chainbase
383 382 chainlen, compresseddeltalen = revlog._chaininfo(base)
384 383 chainlen += 1
385 384 compresseddeltalen += deltalen
386 385 return _deltainfo(dist, deltalen, (header, data), deltabase,
387 386 chainbase, chainlen, compresseddeltalen)
388 387
389 388 def finddeltainfo(self, revinfo, fh):
390 389 """Find an acceptable delta against a candidate revision
391 390
392 391 revinfo: information about the revision (instance of _revisioninfo)
393 392 fh: file handle to either the .i or the .d revlog file,
394 393 depending on whether it is inlined or not
395 394
396 395 Returns the first acceptable candidate revision, as ordered by
397 396 _getcandidaterevs
398 397 """
399 398 cachedelta = revinfo.cachedelta
400 399 p1 = revinfo.p1
401 400 p2 = revinfo.p2
402 401 revlog = self.revlog
403 402
404 403 deltainfo = None
405 404 for candidaterevs in self._getcandidaterevs(p1, p2, cachedelta):
406 405 nominateddeltas = []
407 406 for candidaterev in candidaterevs:
408 407 candidatedelta = self._builddeltainfo(revinfo, candidaterev, fh)
409 408 if revlog._isgooddeltainfo(candidatedelta, revinfo.textlen):
410 409 nominateddeltas.append(candidatedelta)
411 410 if nominateddeltas:
412 411 deltainfo = min(nominateddeltas, key=lambda x: x.deltalen)
413 412 break
414 413
415 414 return deltainfo
416 415
417 416 @attr.s(slots=True, frozen=True)
418 417 class _revisioninfo(object):
419 418 """Information about a revision that allows building its fulltext
420 419 node: expected hash of the revision
421 420 p1, p2: parent revs of the revision
422 421 btext: built text cache consisting of a one-element list
423 422 cachedelta: (baserev, uncompressed_delta) or None
424 423 flags: flags associated to the revision storage
425 424
426 425 One of btext[0] or cachedelta must be set.
427 426 """
428 427 node = attr.ib()
429 428 p1 = attr.ib()
430 429 p2 = attr.ib()
431 430 btext = attr.ib()
432 431 textlen = attr.ib()
433 432 cachedelta = attr.ib()
434 433 flags = attr.ib()
435 434
436 435 # index v0:
437 436 # 4 bytes: offset
438 437 # 4 bytes: compressed length
439 438 # 4 bytes: base rev
440 439 # 4 bytes: link rev
441 440 # 20 bytes: parent 1 nodeid
442 441 # 20 bytes: parent 2 nodeid
443 442 # 20 bytes: nodeid
444 443 indexformatv0 = struct.Struct(">4l20s20s20s")
445 444 indexformatv0_pack = indexformatv0.pack
446 445 indexformatv0_unpack = indexformatv0.unpack
447 446
448 447 class revlogoldio(object):
449 448 def __init__(self):
450 449 self.size = indexformatv0.size
451 450
452 451 def parseindex(self, data, inline):
453 452 s = self.size
454 453 index = []
455 454 nodemap = {nullid: nullrev}
456 455 n = off = 0
457 456 l = len(data)
458 457 while off + s <= l:
459 458 cur = data[off:off + s]
460 459 off += s
461 460 e = indexformatv0_unpack(cur)
462 461 # transform to revlogv1 format
463 462 e2 = (offset_type(e[0], 0), e[1], -1, e[2], e[3],
464 463 nodemap.get(e[4], nullrev), nodemap.get(e[5], nullrev), e[6])
465 464 index.append(e2)
466 465 nodemap[e[6]] = n
467 466 n += 1
468 467
469 468 # add the magic null revision at -1
470 469 index.append((0, 0, 0, -1, -1, -1, -1, nullid))
471 470
472 471 return index, nodemap, None
473 472
474 473 def packentry(self, entry, node, version, rev):
475 474 if gettype(entry[0]):
476 475 raise RevlogError(_('index entry flags need revlog version 1'))
477 476 e2 = (getoffset(entry[0]), entry[1], entry[3], entry[4],
478 477 node(entry[5]), node(entry[6]), entry[7])
479 478 return indexformatv0_pack(*e2)
480 479
481 480 # index ng:
482 481 # 6 bytes: offset
483 482 # 2 bytes: flags
484 483 # 4 bytes: compressed length
485 484 # 4 bytes: uncompressed length
486 485 # 4 bytes: base rev
487 486 # 4 bytes: link rev
488 487 # 4 bytes: parent 1 rev
489 488 # 4 bytes: parent 2 rev
490 489 # 32 bytes: nodeid
491 490 indexformatng = struct.Struct(">Qiiiiii20s12x")
492 491 indexformatng_pack = indexformatng.pack
493 492 versionformat = struct.Struct(">I")
494 493 versionformat_pack = versionformat.pack
495 494 versionformat_unpack = versionformat.unpack
496 495
497 496 # corresponds to uncompressed length of indexformatng (2 gigs, 4-byte
498 497 # signed integer)
499 498 _maxentrysize = 0x7fffffff
500 499
501 500 class revlogio(object):
502 501 def __init__(self):
503 502 self.size = indexformatng.size
504 503
505 504 def parseindex(self, data, inline):
506 505 # call the C implementation to parse the index data
507 506 index, cache = parsers.parse_index2(data, inline)
508 507 return index, getattr(index, 'nodemap', None), cache
509 508
510 509 def packentry(self, entry, node, version, rev):
511 510 p = indexformatng_pack(*entry)
512 511 if rev == 0:
513 512 p = versionformat_pack(version) + p[4:]
514 513 return p
515 514
516 515 class revlog(object):
517 516 """
518 517 the underlying revision storage object
519 518
520 519 A revlog consists of two parts, an index and the revision data.
521 520
522 521 The index is a file with a fixed record size containing
523 522 information on each revision, including its nodeid (hash), the
524 523 nodeids of its parents, the position and offset of its data within
525 524 the data file, and the revision it's based on. Finally, each entry
526 525 contains a linkrev entry that can serve as a pointer to external
527 526 data.
528 527
529 528 The revision data itself is a linear collection of data chunks.
530 529 Each chunk represents a revision and is usually represented as a
531 530 delta against the previous chunk. To bound lookup time, runs of
532 531 deltas are limited to about 2 times the length of the original
533 532 version data. This makes retrieval of a version proportional to
534 533 its size, or O(1) relative to the number of revisions.
535 534
536 535 Both pieces of the revlog are written to in an append-only
537 536 fashion, which means we never need to rewrite a file to insert or
538 537 remove data, and can use some simple techniques to avoid the need
539 538 for locking while reading.
540 539
541 540 If checkambig, indexfile is opened with checkambig=True at
542 541 writing, to avoid file stat ambiguity.
543 542
544 543 If mmaplargeindex is True, and an mmapindexthreshold is set, the
545 544 index will be mmapped rather than read if it is larger than the
546 545 configured threshold.
547 546 """
548 547 def __init__(self, opener, indexfile, datafile=None, checkambig=False,
549 548 mmaplargeindex=False):
550 549 """
551 550 create a revlog object
552 551
553 552 opener is a function that abstracts the file opening operation
554 553 and can be used to implement COW semantics or the like.
555 554 """
556 555 self.indexfile = indexfile
557 556 self.datafile = datafile or (indexfile[:-2] + ".d")
558 557 self.opener = opener
559 558 # When True, indexfile is opened with checkambig=True at writing, to
560 559 # avoid file stat ambiguity.
561 560 self._checkambig = checkambig
562 561 # 3-tuple of (node, rev, text) for a raw revision.
563 562 self._cache = None
564 563 # Maps rev to chain base rev.
565 564 self._chainbasecache = util.lrucachedict(100)
566 565 # 2-tuple of (offset, data) of raw data from the revlog at an offset.
567 566 self._chunkcache = (0, '')
568 567 # How much data to read and cache into the raw revlog data cache.
569 568 self._chunkcachesize = 65536
570 569 self._maxchainlen = None
571 570 self._aggressivemergedeltas = False
572 571 self.index = []
573 572 # Mapping of partial identifiers to full nodes.
574 573 self._pcache = {}
575 574 # Mapping of revision integer to full node.
576 575 self._nodecache = {nullid: nullrev}
577 576 self._nodepos = None
578 577 self._compengine = 'zlib'
579 578 self._maxdeltachainspan = -1
580 579 self._withsparseread = False
581 580 self._srdensitythreshold = 0.25
582 581 self._srmingapsize = 262144
583 582
584 583 mmapindexthreshold = None
585 584 v = REVLOG_DEFAULT_VERSION
586 585 opts = getattr(opener, 'options', None)
587 586 if opts is not None:
588 587 if 'revlogv2' in opts:
589 588 # version 2 revlogs always use generaldelta.
590 589 v = REVLOGV2 | FLAG_GENERALDELTA | FLAG_INLINE_DATA
591 590 elif 'revlogv1' in opts:
592 591 if 'generaldelta' in opts:
593 592 v |= FLAG_GENERALDELTA
594 593 else:
595 594 v = 0
596 595 if 'chunkcachesize' in opts:
597 596 self._chunkcachesize = opts['chunkcachesize']
598 597 if 'maxchainlen' in opts:
599 598 self._maxchainlen = opts['maxchainlen']
600 599 if 'aggressivemergedeltas' in opts:
601 600 self._aggressivemergedeltas = opts['aggressivemergedeltas']
602 601 self._lazydeltabase = bool(opts.get('lazydeltabase', False))
603 602 if 'compengine' in opts:
604 603 self._compengine = opts['compengine']
605 604 if 'maxdeltachainspan' in opts:
606 605 self._maxdeltachainspan = opts['maxdeltachainspan']
607 606 if mmaplargeindex and 'mmapindexthreshold' in opts:
608 607 mmapindexthreshold = opts['mmapindexthreshold']
609 608 self._withsparseread = bool(opts.get('with-sparse-read', False))
610 609 if 'sparse-read-density-threshold' in opts:
611 610 self._srdensitythreshold = opts['sparse-read-density-threshold']
612 611 if 'sparse-read-min-gap-size' in opts:
613 612 self._srmingapsize = opts['sparse-read-min-gap-size']
614 613
615 614 if self._chunkcachesize <= 0:
616 615 raise RevlogError(_('revlog chunk cache size %r is not greater '
617 616 'than 0') % self._chunkcachesize)
618 617 elif self._chunkcachesize & (self._chunkcachesize - 1):
619 618 raise RevlogError(_('revlog chunk cache size %r is not a power '
620 619 'of 2') % self._chunkcachesize)
621 620
622 621 indexdata = ''
623 622 self._initempty = True
624 623 try:
625 624 with self._indexfp() as f:
626 625 if (mmapindexthreshold is not None and
627 626 self.opener.fstat(f).st_size >= mmapindexthreshold):
628 627 indexdata = util.buffer(util.mmapread(f))
629 628 else:
630 629 indexdata = f.read()
631 630 if len(indexdata) > 0:
632 631 v = versionformat_unpack(indexdata[:4])[0]
633 632 self._initempty = False
634 633 except IOError as inst:
635 634 if inst.errno != errno.ENOENT:
636 635 raise
637 636
638 637 self.version = v
639 638 self._inline = v & FLAG_INLINE_DATA
640 639 self._generaldelta = v & FLAG_GENERALDELTA
641 640 flags = v & ~0xFFFF
642 641 fmt = v & 0xFFFF
643 642 if fmt == REVLOGV0:
644 643 if flags:
645 644 raise RevlogError(_('unknown flags (%#04x) in version %d '
646 645 'revlog %s') %
647 646 (flags >> 16, fmt, self.indexfile))
648 647 elif fmt == REVLOGV1:
649 648 if flags & ~REVLOGV1_FLAGS:
650 649 raise RevlogError(_('unknown flags (%#04x) in version %d '
651 650 'revlog %s') %
652 651 (flags >> 16, fmt, self.indexfile))
653 652 elif fmt == REVLOGV2:
654 653 if flags & ~REVLOGV2_FLAGS:
655 654 raise RevlogError(_('unknown flags (%#04x) in version %d '
656 655 'revlog %s') %
657 656 (flags >> 16, fmt, self.indexfile))
658 657 else:
659 658 raise RevlogError(_('unknown version (%d) in revlog %s') %
660 659 (fmt, self.indexfile))
661 660
662 661 self.storedeltachains = True
663 662
664 663 self._io = revlogio()
665 664 if self.version == REVLOGV0:
666 665 self._io = revlogoldio()
667 666 try:
668 667 d = self._io.parseindex(indexdata, self._inline)
669 668 except (ValueError, IndexError):
670 669 raise RevlogError(_("index %s is corrupted") % (self.indexfile))
671 670 self.index, nodemap, self._chunkcache = d
672 671 if nodemap is not None:
673 672 self.nodemap = self._nodecache = nodemap
674 673 if not self._chunkcache:
675 674 self._chunkclear()
676 675 # revnum -> (chain-length, sum-delta-length)
677 676 self._chaininfocache = {}
678 677 # revlog header -> revlog compressor
679 678 self._decompressors = {}
680 679
681 680 @util.propertycache
682 681 def _compressor(self):
683 682 return util.compengines[self._compengine].revlogcompressor()
684 683
685 684 def _indexfp(self, mode='r'):
686 685 """file object for the revlog's index file"""
687 686 args = {r'mode': mode}
688 687 if mode != 'r':
689 688 args[r'checkambig'] = self._checkambig
690 689 if mode == 'w':
691 690 args[r'atomictemp'] = True
692 691 return self.opener(self.indexfile, **args)
693 692
694 693 def _datafp(self, mode='r'):
695 694 """file object for the revlog's data file"""
696 695 return self.opener(self.datafile, mode=mode)
697 696
698 697 @contextlib.contextmanager
699 698 def _datareadfp(self, existingfp=None):
700 699 """file object suitable to read data"""
701 700 if existingfp is not None:
702 701 yield existingfp
703 702 else:
704 703 if self._inline:
705 704 func = self._indexfp
706 705 else:
707 706 func = self._datafp
708 707 with func() as fp:
709 708 yield fp
710 709
711 710 def tip(self):
712 711 return self.node(len(self.index) - 2)
713 712 def __contains__(self, rev):
714 713 return 0 <= rev < len(self)
715 714 def __len__(self):
716 715 return len(self.index) - 1
717 716 def __iter__(self):
718 717 return iter(xrange(len(self)))
719 718 def revs(self, start=0, stop=None):
720 719 """iterate over all rev in this revlog (from start to stop)"""
721 720 step = 1
722 721 if stop is not None:
723 722 if start > stop:
724 723 step = -1
725 724 stop += step
726 725 else:
727 726 stop = len(self)
728 727 return xrange(start, stop, step)
729 728
730 729 @util.propertycache
731 730 def nodemap(self):
732 731 self.rev(self.node(0))
733 732 return self._nodecache
734 733
735 734 def hasnode(self, node):
736 735 try:
737 736 self.rev(node)
738 737 return True
739 738 except KeyError:
740 739 return False
741 740
742 741 def clearcaches(self):
743 742 self._cache = None
744 743 self._chainbasecache.clear()
745 744 self._chunkcache = (0, '')
746 745 self._pcache = {}
747 746
748 747 try:
749 748 self._nodecache.clearcaches()
750 749 except AttributeError:
751 750 self._nodecache = {nullid: nullrev}
752 751 self._nodepos = None
753 752
754 753 def rev(self, node):
755 754 try:
756 755 return self._nodecache[node]
757 756 except TypeError:
758 757 raise
759 758 except RevlogError:
760 759 # parsers.c radix tree lookup failed
761 760 if node == wdirid:
762 761 raise error.WdirUnsupported
763 762 raise LookupError(node, self.indexfile, _('no node'))
764 763 except KeyError:
765 764 # pure python cache lookup failed
766 765 n = self._nodecache
767 766 i = self.index
768 767 p = self._nodepos
769 768 if p is None:
770 769 p = len(i) - 2
771 770 for r in xrange(p, -1, -1):
772 771 v = i[r][7]
773 772 n[v] = r
774 773 if v == node:
775 774 self._nodepos = r - 1
776 775 return r
777 776 if node == wdirid:
778 777 raise error.WdirUnsupported
779 778 raise LookupError(node, self.indexfile, _('no node'))
780 779
781 780 # Accessors for index entries.
782 781
783 782 # First tuple entry is 8 bytes. First 6 bytes are offset. Last 2 bytes
784 783 # are flags.
785 784 def start(self, rev):
786 785 return int(self.index[rev][0] >> 16)
787 786
788 787 def flags(self, rev):
789 788 return self.index[rev][0] & 0xFFFF
790 789
791 790 def length(self, rev):
792 791 return self.index[rev][1]
793 792
794 793 def rawsize(self, rev):
795 794 """return the length of the uncompressed text for a given revision"""
796 795 l = self.index[rev][2]
797 796 if l >= 0:
798 797 return l
799 798
800 799 t = self.revision(rev, raw=True)
801 800 return len(t)
802 801
803 802 def size(self, rev):
804 803 """length of non-raw text (processed by a "read" flag processor)"""
805 804 # fast path: if no "read" flag processor could change the content,
806 805 # size is rawsize. note: ELLIPSIS is known to not change the content.
807 806 flags = self.flags(rev)
808 807 if flags & (REVIDX_KNOWN_FLAGS ^ REVIDX_ELLIPSIS) == 0:
809 808 return self.rawsize(rev)
810 809
811 810 return len(self.revision(rev, raw=False))
812 811
813 812 def chainbase(self, rev):
814 813 base = self._chainbasecache.get(rev)
815 814 if base is not None:
816 815 return base
817 816
818 817 index = self.index
819 818 base = index[rev][3]
820 819 while base != rev:
821 820 rev = base
822 821 base = index[rev][3]
823 822
824 823 self._chainbasecache[rev] = base
825 824 return base
826 825
827 826 def linkrev(self, rev):
828 827 return self.index[rev][4]
829 828
830 829 def parentrevs(self, rev):
831 830 try:
832 831 entry = self.index[rev]
833 832 except IndexError:
834 833 if rev == wdirrev:
835 834 raise error.WdirUnsupported
836 835 raise
837 836
838 837 return entry[5], entry[6]
839 838
840 839 def node(self, rev):
841 840 try:
842 841 return self.index[rev][7]
843 842 except IndexError:
844 843 if rev == wdirrev:
845 844 raise error.WdirUnsupported
846 845 raise
847 846
848 847 # Derived from index values.
849 848
850 849 def end(self, rev):
851 850 return self.start(rev) + self.length(rev)
852 851
853 852 def parents(self, node):
854 853 i = self.index
855 854 d = i[self.rev(node)]
856 855 return i[d[5]][7], i[d[6]][7] # map revisions to nodes inline
857 856
858 857 def chainlen(self, rev):
859 858 return self._chaininfo(rev)[0]
860 859
861 860 def _chaininfo(self, rev):
862 861 chaininfocache = self._chaininfocache
863 862 if rev in chaininfocache:
864 863 return chaininfocache[rev]
865 864 index = self.index
866 865 generaldelta = self._generaldelta
867 866 iterrev = rev
868 867 e = index[iterrev]
869 868 clen = 0
870 869 compresseddeltalen = 0
871 870 while iterrev != e[3]:
872 871 clen += 1
873 872 compresseddeltalen += e[1]
874 873 if generaldelta:
875 874 iterrev = e[3]
876 875 else:
877 876 iterrev -= 1
878 877 if iterrev in chaininfocache:
879 878 t = chaininfocache[iterrev]
880 879 clen += t[0]
881 880 compresseddeltalen += t[1]
882 881 break
883 882 e = index[iterrev]
884 883 else:
885 884 # Add text length of base since decompressing that also takes
886 885 # work. For cache hits the length is already included.
887 886 compresseddeltalen += e[1]
888 887 r = (clen, compresseddeltalen)
889 888 chaininfocache[rev] = r
890 889 return r
891 890
892 891 def _deltachain(self, rev, stoprev=None):
893 892 """Obtain the delta chain for a revision.
894 893
895 894 ``stoprev`` specifies a revision to stop at. If not specified, we
896 895 stop at the base of the chain.
897 896
898 897 Returns a 2-tuple of (chain, stopped) where ``chain`` is a list of
899 898 revs in ascending order and ``stopped`` is a bool indicating whether
900 899 ``stoprev`` was hit.
901 900 """
902 901 # Try C implementation.
903 902 try:
904 903 return self.index.deltachain(rev, stoprev, self._generaldelta)
905 904 except AttributeError:
906 905 pass
907 906
908 907 chain = []
909 908
910 909 # Alias to prevent attribute lookup in tight loop.
911 910 index = self.index
912 911 generaldelta = self._generaldelta
913 912
914 913 iterrev = rev
915 914 e = index[iterrev]
916 915 while iterrev != e[3] and iterrev != stoprev:
917 916 chain.append(iterrev)
918 917 if generaldelta:
919 918 iterrev = e[3]
920 919 else:
921 920 iterrev -= 1
922 921 e = index[iterrev]
923 922
924 923 if iterrev == stoprev:
925 924 stopped = True
926 925 else:
927 926 chain.append(iterrev)
928 927 stopped = False
929 928
930 929 chain.reverse()
931 930 return chain, stopped
932 931
933 932 def ancestors(self, revs, stoprev=0, inclusive=False):
934 933 """Generate the ancestors of 'revs' in reverse topological order.
935 934 Does not generate revs lower than stoprev.
936 935
937 936 See the documentation for ancestor.lazyancestors for more details."""
938 937
939 938 return ancestor.lazyancestors(self.parentrevs, revs, stoprev=stoprev,
940 939 inclusive=inclusive)
941 940
942 941 def descendants(self, revs):
943 942 """Generate the descendants of 'revs' in revision order.
944 943
945 944 Yield a sequence of revision numbers starting with a child of
946 945 some rev in revs, i.e., each revision is *not* considered a
947 946 descendant of itself. Results are ordered by revision number (a
948 947 topological sort)."""
949 948 first = min(revs)
950 949 if first == nullrev:
951 950 for i in self:
952 951 yield i
953 952 return
954 953
955 954 seen = set(revs)
956 955 for i in self.revs(start=first + 1):
957 956 for x in self.parentrevs(i):
958 957 if x != nullrev and x in seen:
959 958 seen.add(i)
960 959 yield i
961 960 break
962 961
963 962 def findcommonmissing(self, common=None, heads=None):
964 963 """Return a tuple of the ancestors of common and the ancestors of heads
965 964 that are not ancestors of common. In revset terminology, we return the
966 965 tuple:
967 966
968 967 ::common, (::heads) - (::common)
969 968
970 969 The list is sorted by revision number, meaning it is
971 970 topologically sorted.
972 971
973 972 'heads' and 'common' are both lists of node IDs. If heads is
974 973 not supplied, uses all of the revlog's heads. If common is not
975 974 supplied, uses nullid."""
976 975 if common is None:
977 976 common = [nullid]
978 977 if heads is None:
979 978 heads = self.heads()
980 979
981 980 common = [self.rev(n) for n in common]
982 981 heads = [self.rev(n) for n in heads]
983 982
984 983 # we want the ancestors, but inclusive
985 984 class lazyset(object):
986 985 def __init__(self, lazyvalues):
987 986 self.addedvalues = set()
988 987 self.lazyvalues = lazyvalues
989 988
990 989 def __contains__(self, value):
991 990 return value in self.addedvalues or value in self.lazyvalues
992 991
993 992 def __iter__(self):
994 993 added = self.addedvalues
995 994 for r in added:
996 995 yield r
997 996 for r in self.lazyvalues:
998 997 if not r in added:
999 998 yield r
1000 999
1001 1000 def add(self, value):
1002 1001 self.addedvalues.add(value)
1003 1002
1004 1003 def update(self, values):
1005 1004 self.addedvalues.update(values)
1006 1005
1007 1006 has = lazyset(self.ancestors(common))
1008 1007 has.add(nullrev)
1009 1008 has.update(common)
1010 1009
1011 1010 # take all ancestors from heads that aren't in has
1012 1011 missing = set()
1013 1012 visit = collections.deque(r for r in heads if r not in has)
1014 1013 while visit:
1015 1014 r = visit.popleft()
1016 1015 if r in missing:
1017 1016 continue
1018 1017 else:
1019 1018 missing.add(r)
1020 1019 for p in self.parentrevs(r):
1021 1020 if p not in has:
1022 1021 visit.append(p)
1023 1022 missing = list(missing)
1024 1023 missing.sort()
1025 1024 return has, [self.node(miss) for miss in missing]
1026 1025
1027 1026 def incrementalmissingrevs(self, common=None):
1028 1027 """Return an object that can be used to incrementally compute the
1029 1028 revision numbers of the ancestors of arbitrary sets that are not
1030 1029 ancestors of common. This is an ancestor.incrementalmissingancestors
1031 1030 object.
1032 1031
1033 1032 'common' is a list of revision numbers. If common is not supplied, uses
1034 1033 nullrev.
1035 1034 """
1036 1035 if common is None:
1037 1036 common = [nullrev]
1038 1037
1039 1038 return ancestor.incrementalmissingancestors(self.parentrevs, common)
1040 1039
1041 1040 def findmissingrevs(self, common=None, heads=None):
1042 1041 """Return the revision numbers of the ancestors of heads that
1043 1042 are not ancestors of common.
1044 1043
1045 1044 More specifically, return a list of revision numbers corresponding to
1046 1045 nodes N such that every N satisfies the following constraints:
1047 1046
1048 1047 1. N is an ancestor of some node in 'heads'
1049 1048 2. N is not an ancestor of any node in 'common'
1050 1049
1051 1050 The list is sorted by revision number, meaning it is
1052 1051 topologically sorted.
1053 1052
1054 1053 'heads' and 'common' are both lists of revision numbers. If heads is
1055 1054 not supplied, uses all of the revlog's heads. If common is not
1056 1055 supplied, uses nullid."""
1057 1056 if common is None:
1058 1057 common = [nullrev]
1059 1058 if heads is None:
1060 1059 heads = self.headrevs()
1061 1060
1062 1061 inc = self.incrementalmissingrevs(common=common)
1063 1062 return inc.missingancestors(heads)
1064 1063
1065 1064 def findmissing(self, common=None, heads=None):
1066 1065 """Return the ancestors of heads that are not ancestors of common.
1067 1066
1068 1067 More specifically, return a list of nodes N such that every N
1069 1068 satisfies the following constraints:
1070 1069
1071 1070 1. N is an ancestor of some node in 'heads'
1072 1071 2. N is not an ancestor of any node in 'common'
1073 1072
1074 1073 The list is sorted by revision number, meaning it is
1075 1074 topologically sorted.
1076 1075
1077 1076 'heads' and 'common' are both lists of node IDs. If heads is
1078 1077 not supplied, uses all of the revlog's heads. If common is not
1079 1078 supplied, uses nullid."""
1080 1079 if common is None:
1081 1080 common = [nullid]
1082 1081 if heads is None:
1083 1082 heads = self.heads()
1084 1083
1085 1084 common = [self.rev(n) for n in common]
1086 1085 heads = [self.rev(n) for n in heads]
1087 1086
1088 1087 inc = self.incrementalmissingrevs(common=common)
1089 1088 return [self.node(r) for r in inc.missingancestors(heads)]
1090 1089
1091 1090 def nodesbetween(self, roots=None, heads=None):
1092 1091 """Return a topological path from 'roots' to 'heads'.
1093 1092
1094 1093 Return a tuple (nodes, outroots, outheads) where 'nodes' is a
1095 1094 topologically sorted list of all nodes N that satisfy both of
1096 1095 these constraints:
1097 1096
1098 1097 1. N is a descendant of some node in 'roots'
1099 1098 2. N is an ancestor of some node in 'heads'
1100 1099
1101 1100 Every node is considered to be both a descendant and an ancestor
1102 1101 of itself, so every reachable node in 'roots' and 'heads' will be
1103 1102 included in 'nodes'.
1104 1103
1105 1104 'outroots' is the list of reachable nodes in 'roots', i.e., the
1106 1105 subset of 'roots' that is returned in 'nodes'. Likewise,
1107 1106 'outheads' is the subset of 'heads' that is also in 'nodes'.
1108 1107
1109 1108 'roots' and 'heads' are both lists of node IDs. If 'roots' is
1110 1109 unspecified, uses nullid as the only root. If 'heads' is
1111 1110 unspecified, uses list of all of the revlog's heads."""
1112 1111 nonodes = ([], [], [])
1113 1112 if roots is not None:
1114 1113 roots = list(roots)
1115 1114 if not roots:
1116 1115 return nonodes
1117 1116 lowestrev = min([self.rev(n) for n in roots])
1118 1117 else:
1119 1118 roots = [nullid] # Everybody's a descendant of nullid
1120 1119 lowestrev = nullrev
1121 1120 if (lowestrev == nullrev) and (heads is None):
1122 1121 # We want _all_ the nodes!
1123 1122 return ([self.node(r) for r in self], [nullid], list(self.heads()))
1124 1123 if heads is None:
1125 1124 # All nodes are ancestors, so the latest ancestor is the last
1126 1125 # node.
1127 1126 highestrev = len(self) - 1
1128 1127 # Set ancestors to None to signal that every node is an ancestor.
1129 1128 ancestors = None
1130 1129 # Set heads to an empty dictionary for later discovery of heads
1131 1130 heads = {}
1132 1131 else:
1133 1132 heads = list(heads)
1134 1133 if not heads:
1135 1134 return nonodes
1136 1135 ancestors = set()
1137 1136 # Turn heads into a dictionary so we can remove 'fake' heads.
1138 1137 # Also, later we will be using it to filter out the heads we can't
1139 1138 # find from roots.
1140 1139 heads = dict.fromkeys(heads, False)
1141 1140 # Start at the top and keep marking parents until we're done.
1142 1141 nodestotag = set(heads)
1143 1142 # Remember where the top was so we can use it as a limit later.
1144 1143 highestrev = max([self.rev(n) for n in nodestotag])
1145 1144 while nodestotag:
1146 1145 # grab a node to tag
1147 1146 n = nodestotag.pop()
1148 1147 # Never tag nullid
1149 1148 if n == nullid:
1150 1149 continue
1151 1150 # A node's revision number represents its place in a
1152 1151 # topologically sorted list of nodes.
1153 1152 r = self.rev(n)
1154 1153 if r >= lowestrev:
1155 1154 if n not in ancestors:
1156 1155 # If we are possibly a descendant of one of the roots
1157 1156 # and we haven't already been marked as an ancestor
1158 1157 ancestors.add(n) # Mark as ancestor
1159 1158 # Add non-nullid parents to list of nodes to tag.
1160 1159 nodestotag.update([p for p in self.parents(n) if
1161 1160 p != nullid])
1162 1161 elif n in heads: # We've seen it before, is it a fake head?
1163 1162 # So it is, real heads should not be the ancestors of
1164 1163 # any other heads.
1165 1164 heads.pop(n)
1166 1165 if not ancestors:
1167 1166 return nonodes
1168 1167 # Now that we have our set of ancestors, we want to remove any
1169 1168 # roots that are not ancestors.
1170 1169
1171 1170 # If one of the roots was nullid, everything is included anyway.
1172 1171 if lowestrev > nullrev:
1173 1172 # But, since we weren't, let's recompute the lowest rev to not
1174 1173 # include roots that aren't ancestors.
1175 1174
1176 1175 # Filter out roots that aren't ancestors of heads
1177 1176 roots = [root for root in roots if root in ancestors]
1178 1177 # Recompute the lowest revision
1179 1178 if roots:
1180 1179 lowestrev = min([self.rev(root) for root in roots])
1181 1180 else:
1182 1181 # No more roots? Return empty list
1183 1182 return nonodes
1184 1183 else:
1185 1184 # We are descending from nullid, and don't need to care about
1186 1185 # any other roots.
1187 1186 lowestrev = nullrev
1188 1187 roots = [nullid]
1189 1188 # Transform our roots list into a set.
1190 1189 descendants = set(roots)
1191 1190 # Also, keep the original roots so we can filter out roots that aren't
1192 1191 # 'real' roots (i.e. are descended from other roots).
1193 1192 roots = descendants.copy()
1194 1193 # Our topologically sorted list of output nodes.
1195 1194 orderedout = []
1196 1195 # Don't start at nullid since we don't want nullid in our output list,
1197 1196 # and if nullid shows up in descendants, empty parents will look like
1198 1197 # they're descendants.
1199 1198 for r in self.revs(start=max(lowestrev, 0), stop=highestrev + 1):
1200 1199 n = self.node(r)
1201 1200 isdescendant = False
1202 1201 if lowestrev == nullrev: # Everybody is a descendant of nullid
1203 1202 isdescendant = True
1204 1203 elif n in descendants:
1205 1204 # n is already a descendant
1206 1205 isdescendant = True
1207 1206 # This check only needs to be done here because all the roots
1208 1207 # will start being marked is descendants before the loop.
1209 1208 if n in roots:
1210 1209 # If n was a root, check if it's a 'real' root.
1211 1210 p = tuple(self.parents(n))
1212 1211 # If any of its parents are descendants, it's not a root.
1213 1212 if (p[0] in descendants) or (p[1] in descendants):
1214 1213 roots.remove(n)
1215 1214 else:
1216 1215 p = tuple(self.parents(n))
1217 1216 # A node is a descendant if either of its parents are
1218 1217 # descendants. (We seeded the dependents list with the roots
1219 1218 # up there, remember?)
1220 1219 if (p[0] in descendants) or (p[1] in descendants):
1221 1220 descendants.add(n)
1222 1221 isdescendant = True
1223 1222 if isdescendant and ((ancestors is None) or (n in ancestors)):
1224 1223 # Only include nodes that are both descendants and ancestors.
1225 1224 orderedout.append(n)
1226 1225 if (ancestors is not None) and (n in heads):
1227 1226 # We're trying to figure out which heads are reachable
1228 1227 # from roots.
1229 1228 # Mark this head as having been reached
1230 1229 heads[n] = True
1231 1230 elif ancestors is None:
1232 1231 # Otherwise, we're trying to discover the heads.
1233 1232 # Assume this is a head because if it isn't, the next step
1234 1233 # will eventually remove it.
1235 1234 heads[n] = True
1236 1235 # But, obviously its parents aren't.
1237 1236 for p in self.parents(n):
1238 1237 heads.pop(p, None)
1239 1238 heads = [head for head, flag in heads.iteritems() if flag]
1240 1239 roots = list(roots)
1241 1240 assert orderedout
1242 1241 assert roots
1243 1242 assert heads
1244 1243 return (orderedout, roots, heads)
1245 1244
1246 1245 def headrevs(self):
1247 1246 try:
1248 1247 return self.index.headrevs()
1249 1248 except AttributeError:
1250 1249 return self._headrevs()
1251 1250
1252 1251 def computephases(self, roots):
1253 1252 return self.index.computephasesmapsets(roots)
1254 1253
1255 1254 def _headrevs(self):
1256 1255 count = len(self)
1257 1256 if not count:
1258 1257 return [nullrev]
1259 1258 # we won't iter over filtered rev so nobody is a head at start
1260 1259 ishead = [0] * (count + 1)
1261 1260 index = self.index
1262 1261 for r in self:
1263 1262 ishead[r] = 1 # I may be an head
1264 1263 e = index[r]
1265 1264 ishead[e[5]] = ishead[e[6]] = 0 # my parent are not
1266 1265 return [r for r, val in enumerate(ishead) if val]
1267 1266
1268 1267 def heads(self, start=None, stop=None):
1269 1268 """return the list of all nodes that have no children
1270 1269
1271 1270 if start is specified, only heads that are descendants of
1272 1271 start will be returned
1273 1272 if stop is specified, it will consider all the revs from stop
1274 1273 as if they had no children
1275 1274 """
1276 1275 if start is None and stop is None:
1277 1276 if not len(self):
1278 1277 return [nullid]
1279 1278 return [self.node(r) for r in self.headrevs()]
1280 1279
1281 1280 if start is None:
1282 1281 start = nullid
1283 1282 if stop is None:
1284 1283 stop = []
1285 1284 stoprevs = set([self.rev(n) for n in stop])
1286 1285 startrev = self.rev(start)
1287 1286 reachable = {startrev}
1288 1287 heads = {startrev}
1289 1288
1290 1289 parentrevs = self.parentrevs
1291 1290 for r in self.revs(start=startrev + 1):
1292 1291 for p in parentrevs(r):
1293 1292 if p in reachable:
1294 1293 if r not in stoprevs:
1295 1294 reachable.add(r)
1296 1295 heads.add(r)
1297 1296 if p in heads and p not in stoprevs:
1298 1297 heads.remove(p)
1299 1298
1300 1299 return [self.node(r) for r in heads]
1301 1300
1302 1301 def children(self, node):
1303 1302 """find the children of a given node"""
1304 1303 c = []
1305 1304 p = self.rev(node)
1306 1305 for r in self.revs(start=p + 1):
1307 1306 prevs = [pr for pr in self.parentrevs(r) if pr != nullrev]
1308 1307 if prevs:
1309 1308 for pr in prevs:
1310 1309 if pr == p:
1311 1310 c.append(self.node(r))
1312 1311 elif p == nullrev:
1313 1312 c.append(self.node(r))
1314 1313 return c
1315 1314
1316 1315 def descendant(self, start, end):
1317 1316 if start == nullrev:
1318 1317 return True
1319 1318 for i in self.descendants([start]):
1320 1319 if i == end:
1321 1320 return True
1322 1321 elif i > end:
1323 1322 break
1324 1323 return False
1325 1324
1326 1325 def commonancestorsheads(self, a, b):
1327 1326 """calculate all the heads of the common ancestors of nodes a and b"""
1328 1327 a, b = self.rev(a), self.rev(b)
1329 1328 try:
1330 1329 ancs = self.index.commonancestorsheads(a, b)
1331 1330 except (AttributeError, OverflowError): # C implementation failed
1332 1331 ancs = ancestor.commonancestorsheads(self.parentrevs, a, b)
1333 1332 return pycompat.maplist(self.node, ancs)
1334 1333
1335 1334 def isancestor(self, a, b):
1336 1335 """return True if node a is an ancestor of node b
1337 1336
1338 1337 The implementation of this is trivial but the use of
1339 1338 commonancestorsheads is not."""
1340 1339 return a in self.commonancestorsheads(a, b)
1341 1340
1342 1341 def ancestor(self, a, b):
1343 1342 """calculate the "best" common ancestor of nodes a and b"""
1344 1343
1345 1344 a, b = self.rev(a), self.rev(b)
1346 1345 try:
1347 1346 ancs = self.index.ancestors(a, b)
1348 1347 except (AttributeError, OverflowError):
1349 1348 ancs = ancestor.ancestors(self.parentrevs, a, b)
1350 1349 if ancs:
1351 1350 # choose a consistent winner when there's a tie
1352 1351 return min(map(self.node, ancs))
1353 1352 return nullid
1354 1353
1355 1354 def _match(self, id):
1356 1355 if isinstance(id, int):
1357 1356 # rev
1358 1357 return self.node(id)
1359 1358 if len(id) == 20:
1360 1359 # possibly a binary node
1361 1360 # odds of a binary node being all hex in ASCII are 1 in 10**25
1362 1361 try:
1363 1362 node = id
1364 1363 self.rev(node) # quick search the index
1365 1364 return node
1366 1365 except LookupError:
1367 1366 pass # may be partial hex id
1368 1367 try:
1369 1368 # str(rev)
1370 1369 rev = int(id)
1371 1370 if str(rev) != id:
1372 1371 raise ValueError
1373 1372 if rev < 0:
1374 1373 rev = len(self) + rev
1375 1374 if rev < 0 or rev >= len(self):
1376 1375 raise ValueError
1377 1376 return self.node(rev)
1378 1377 except (ValueError, OverflowError):
1379 1378 pass
1380 1379 if len(id) == 40:
1381 1380 try:
1382 1381 # a full hex nodeid?
1383 1382 node = bin(id)
1384 1383 self.rev(node)
1385 1384 return node
1386 1385 except (TypeError, LookupError):
1387 1386 pass
1388 1387
1389 1388 def _partialmatch(self, id):
1390 1389 maybewdir = wdirhex.startswith(id)
1391 1390 try:
1392 1391 partial = self.index.partialmatch(id)
1393 1392 if partial and self.hasnode(partial):
1394 1393 if maybewdir:
1395 1394 # single 'ff...' match in radix tree, ambiguous with wdir
1396 1395 raise RevlogError
1397 1396 return partial
1398 1397 if maybewdir:
1399 1398 # no 'ff...' match in radix tree, wdir identified
1400 1399 raise error.WdirUnsupported
1401 1400 return None
1402 1401 except RevlogError:
1403 1402 # parsers.c radix tree lookup gave multiple matches
1404 1403 # fast path: for unfiltered changelog, radix tree is accurate
1405 1404 if not getattr(self, 'filteredrevs', None):
1406 1405 raise LookupError(id, self.indexfile,
1407 1406 _('ambiguous identifier'))
1408 1407 # fall through to slow path that filters hidden revisions
1409 1408 except (AttributeError, ValueError):
1410 1409 # we are pure python, or key was too short to search radix tree
1411 1410 pass
1412 1411
1413 1412 if id in self._pcache:
1414 1413 return self._pcache[id]
1415 1414
1416 1415 if len(id) < 40:
1417 1416 try:
1418 1417 # hex(node)[:...]
1419 1418 l = len(id) // 2 # grab an even number of digits
1420 1419 prefix = bin(id[:l * 2])
1421 1420 nl = [e[7] for e in self.index if e[7].startswith(prefix)]
1422 1421 nl = [n for n in nl if hex(n).startswith(id) and
1423 1422 self.hasnode(n)]
1424 1423 if len(nl) > 0:
1425 1424 if len(nl) == 1 and not maybewdir:
1426 1425 self._pcache[id] = nl[0]
1427 1426 return nl[0]
1428 1427 raise LookupError(id, self.indexfile,
1429 1428 _('ambiguous identifier'))
1430 1429 if maybewdir:
1431 1430 raise error.WdirUnsupported
1432 1431 return None
1433 except (TypeError, binascii.Error):
1432 except TypeError:
1434 1433 pass
1435 1434
1436 1435 def lookup(self, id):
1437 1436 """locate a node based on:
1438 1437 - revision number or str(revision number)
1439 1438 - nodeid or subset of hex nodeid
1440 1439 """
1441 1440 n = self._match(id)
1442 1441 if n is not None:
1443 1442 return n
1444 1443 n = self._partialmatch(id)
1445 1444 if n:
1446 1445 return n
1447 1446
1448 1447 raise LookupError(id, self.indexfile, _('no match found'))
1449 1448
1450 1449 def shortest(self, hexnode, minlength=1):
1451 1450 """Find the shortest unambiguous prefix that matches hexnode."""
1452 1451 def isvalid(test):
1453 1452 try:
1454 1453 if self._partialmatch(test) is None:
1455 1454 return False
1456 1455
1457 1456 try:
1458 1457 i = int(test)
1459 1458 # if we are a pure int, then starting with zero will not be
1460 1459 # confused as a rev; or, obviously, if the int is larger
1461 1460 # than the value of the tip rev
1462 1461 if test[0] == '0' or i > len(self):
1463 1462 return True
1464 1463 return False
1465 1464 except ValueError:
1466 1465 return True
1467 1466 except error.RevlogError:
1468 1467 return False
1469 1468 except error.WdirUnsupported:
1470 1469 # single 'ff...' match
1471 1470 return True
1472 1471
1473 1472 shortest = hexnode
1474 1473 startlength = max(6, minlength)
1475 1474 length = startlength
1476 1475 while True:
1477 1476 test = hexnode[:length]
1478 1477 if isvalid(test):
1479 1478 shortest = test
1480 1479 if length == minlength or length > startlength:
1481 1480 return shortest
1482 1481 length -= 1
1483 1482 else:
1484 1483 length += 1
1485 1484 if len(shortest) <= length:
1486 1485 return shortest
1487 1486
1488 1487 def cmp(self, node, text):
1489 1488 """compare text with a given file revision
1490 1489
1491 1490 returns True if text is different than what is stored.
1492 1491 """
1493 1492 p1, p2 = self.parents(node)
1494 1493 return hash(text, p1, p2) != node
1495 1494
1496 1495 def _cachesegment(self, offset, data):
1497 1496 """Add a segment to the revlog cache.
1498 1497
1499 1498 Accepts an absolute offset and the data that is at that location.
1500 1499 """
1501 1500 o, d = self._chunkcache
1502 1501 # try to add to existing cache
1503 1502 if o + len(d) == offset and len(d) + len(data) < _chunksize:
1504 1503 self._chunkcache = o, d + data
1505 1504 else:
1506 1505 self._chunkcache = offset, data
1507 1506
1508 1507 def _readsegment(self, offset, length, df=None):
1509 1508 """Load a segment of raw data from the revlog.
1510 1509
1511 1510 Accepts an absolute offset, length to read, and an optional existing
1512 1511 file handle to read from.
1513 1512
1514 1513 If an existing file handle is passed, it will be seeked and the
1515 1514 original seek position will NOT be restored.
1516 1515
1517 1516 Returns a str or buffer of raw byte data.
1518 1517 """
1519 1518 # Cache data both forward and backward around the requested
1520 1519 # data, in a fixed size window. This helps speed up operations
1521 1520 # involving reading the revlog backwards.
1522 1521 cachesize = self._chunkcachesize
1523 1522 realoffset = offset & ~(cachesize - 1)
1524 1523 reallength = (((offset + length + cachesize) & ~(cachesize - 1))
1525 1524 - realoffset)
1526 1525 with self._datareadfp(df) as df:
1527 1526 df.seek(realoffset)
1528 1527 d = df.read(reallength)
1529 1528 self._cachesegment(realoffset, d)
1530 1529 if offset != realoffset or reallength != length:
1531 1530 return util.buffer(d, offset - realoffset, length)
1532 1531 return d
1533 1532
1534 1533 def _getsegment(self, offset, length, df=None):
1535 1534 """Obtain a segment of raw data from the revlog.
1536 1535
1537 1536 Accepts an absolute offset, length of bytes to obtain, and an
1538 1537 optional file handle to the already-opened revlog. If the file
1539 1538 handle is used, it's original seek position will not be preserved.
1540 1539
1541 1540 Requests for data may be returned from a cache.
1542 1541
1543 1542 Returns a str or a buffer instance of raw byte data.
1544 1543 """
1545 1544 o, d = self._chunkcache
1546 1545 l = len(d)
1547 1546
1548 1547 # is it in the cache?
1549 1548 cachestart = offset - o
1550 1549 cacheend = cachestart + length
1551 1550 if cachestart >= 0 and cacheend <= l:
1552 1551 if cachestart == 0 and cacheend == l:
1553 1552 return d # avoid a copy
1554 1553 return util.buffer(d, cachestart, cacheend - cachestart)
1555 1554
1556 1555 return self._readsegment(offset, length, df=df)
1557 1556
1558 1557 def _getsegmentforrevs(self, startrev, endrev, df=None):
1559 1558 """Obtain a segment of raw data corresponding to a range of revisions.
1560 1559
1561 1560 Accepts the start and end revisions and an optional already-open
1562 1561 file handle to be used for reading. If the file handle is read, its
1563 1562 seek position will not be preserved.
1564 1563
1565 1564 Requests for data may be satisfied by a cache.
1566 1565
1567 1566 Returns a 2-tuple of (offset, data) for the requested range of
1568 1567 revisions. Offset is the integer offset from the beginning of the
1569 1568 revlog and data is a str or buffer of the raw byte data.
1570 1569
1571 1570 Callers will need to call ``self.start(rev)`` and ``self.length(rev)``
1572 1571 to determine where each revision's data begins and ends.
1573 1572 """
1574 1573 # Inlined self.start(startrev) & self.end(endrev) for perf reasons
1575 1574 # (functions are expensive).
1576 1575 index = self.index
1577 1576 istart = index[startrev]
1578 1577 start = int(istart[0] >> 16)
1579 1578 if startrev == endrev:
1580 1579 end = start + istart[1]
1581 1580 else:
1582 1581 iend = index[endrev]
1583 1582 end = int(iend[0] >> 16) + iend[1]
1584 1583
1585 1584 if self._inline:
1586 1585 start += (startrev + 1) * self._io.size
1587 1586 end += (endrev + 1) * self._io.size
1588 1587 length = end - start
1589 1588
1590 1589 return start, self._getsegment(start, length, df=df)
1591 1590
1592 1591 def _chunk(self, rev, df=None):
1593 1592 """Obtain a single decompressed chunk for a revision.
1594 1593
1595 1594 Accepts an integer revision and an optional already-open file handle
1596 1595 to be used for reading. If used, the seek position of the file will not
1597 1596 be preserved.
1598 1597
1599 1598 Returns a str holding uncompressed data for the requested revision.
1600 1599 """
1601 1600 return self.decompress(self._getsegmentforrevs(rev, rev, df=df)[1])
1602 1601
1603 1602 def _chunks(self, revs, df=None):
1604 1603 """Obtain decompressed chunks for the specified revisions.
1605 1604
1606 1605 Accepts an iterable of numeric revisions that are assumed to be in
1607 1606 ascending order. Also accepts an optional already-open file handle
1608 1607 to be used for reading. If used, the seek position of the file will
1609 1608 not be preserved.
1610 1609
1611 1610 This function is similar to calling ``self._chunk()`` multiple times,
1612 1611 but is faster.
1613 1612
1614 1613 Returns a list with decompressed data for each requested revision.
1615 1614 """
1616 1615 if not revs:
1617 1616 return []
1618 1617 start = self.start
1619 1618 length = self.length
1620 1619 inline = self._inline
1621 1620 iosize = self._io.size
1622 1621 buffer = util.buffer
1623 1622
1624 1623 l = []
1625 1624 ladd = l.append
1626 1625
1627 1626 if not self._withsparseread:
1628 1627 slicedchunks = (revs,)
1629 1628 else:
1630 1629 slicedchunks = _slicechunk(self, revs)
1631 1630
1632 1631 for revschunk in slicedchunks:
1633 1632 firstrev = revschunk[0]
1634 1633 # Skip trailing revisions with empty diff
1635 1634 for lastrev in revschunk[::-1]:
1636 1635 if length(lastrev) != 0:
1637 1636 break
1638 1637
1639 1638 try:
1640 1639 offset, data = self._getsegmentforrevs(firstrev, lastrev, df=df)
1641 1640 except OverflowError:
1642 1641 # issue4215 - we can't cache a run of chunks greater than
1643 1642 # 2G on Windows
1644 1643 return [self._chunk(rev, df=df) for rev in revschunk]
1645 1644
1646 1645 decomp = self.decompress
1647 1646 for rev in revschunk:
1648 1647 chunkstart = start(rev)
1649 1648 if inline:
1650 1649 chunkstart += (rev + 1) * iosize
1651 1650 chunklength = length(rev)
1652 1651 ladd(decomp(buffer(data, chunkstart - offset, chunklength)))
1653 1652
1654 1653 return l
1655 1654
1656 1655 def _chunkclear(self):
1657 1656 """Clear the raw chunk cache."""
1658 1657 self._chunkcache = (0, '')
1659 1658
1660 1659 def deltaparent(self, rev):
1661 1660 """return deltaparent of the given revision"""
1662 1661 base = self.index[rev][3]
1663 1662 if base == rev:
1664 1663 return nullrev
1665 1664 elif self._generaldelta:
1666 1665 return base
1667 1666 else:
1668 1667 return rev - 1
1669 1668
1670 1669 def revdiff(self, rev1, rev2):
1671 1670 """return or calculate a delta between two revisions
1672 1671
1673 1672 The delta calculated is in binary form and is intended to be written to
1674 1673 revlog data directly. So this function needs raw revision data.
1675 1674 """
1676 1675 if rev1 != nullrev and self.deltaparent(rev2) == rev1:
1677 1676 return bytes(self._chunk(rev2))
1678 1677
1679 1678 return mdiff.textdiff(self.revision(rev1, raw=True),
1680 1679 self.revision(rev2, raw=True))
1681 1680
1682 1681 def revision(self, nodeorrev, _df=None, raw=False):
1683 1682 """return an uncompressed revision of a given node or revision
1684 1683 number.
1685 1684
1686 1685 _df - an existing file handle to read from. (internal-only)
1687 1686 raw - an optional argument specifying if the revision data is to be
1688 1687 treated as raw data when applying flag transforms. 'raw' should be set
1689 1688 to True when generating changegroups or in debug commands.
1690 1689 """
1691 1690 if isinstance(nodeorrev, int):
1692 1691 rev = nodeorrev
1693 1692 node = self.node(rev)
1694 1693 else:
1695 1694 node = nodeorrev
1696 1695 rev = None
1697 1696
1698 1697 cachedrev = None
1699 1698 flags = None
1700 1699 rawtext = None
1701 1700 if node == nullid:
1702 1701 return ""
1703 1702 if self._cache:
1704 1703 if self._cache[0] == node:
1705 1704 # _cache only stores rawtext
1706 1705 if raw:
1707 1706 return self._cache[2]
1708 1707 # duplicated, but good for perf
1709 1708 if rev is None:
1710 1709 rev = self.rev(node)
1711 1710 if flags is None:
1712 1711 flags = self.flags(rev)
1713 1712 # no extra flags set, no flag processor runs, text = rawtext
1714 1713 if flags == REVIDX_DEFAULT_FLAGS:
1715 1714 return self._cache[2]
1716 1715 # rawtext is reusable. need to run flag processor
1717 1716 rawtext = self._cache[2]
1718 1717
1719 1718 cachedrev = self._cache[1]
1720 1719
1721 1720 # look up what we need to read
1722 1721 if rawtext is None:
1723 1722 if rev is None:
1724 1723 rev = self.rev(node)
1725 1724
1726 1725 chain, stopped = self._deltachain(rev, stoprev=cachedrev)
1727 1726 if stopped:
1728 1727 rawtext = self._cache[2]
1729 1728
1730 1729 # drop cache to save memory
1731 1730 self._cache = None
1732 1731
1733 1732 bins = self._chunks(chain, df=_df)
1734 1733 if rawtext is None:
1735 1734 rawtext = bytes(bins[0])
1736 1735 bins = bins[1:]
1737 1736
1738 1737 rawtext = mdiff.patches(rawtext, bins)
1739 1738 self._cache = (node, rev, rawtext)
1740 1739
1741 1740 if flags is None:
1742 1741 if rev is None:
1743 1742 rev = self.rev(node)
1744 1743 flags = self.flags(rev)
1745 1744
1746 1745 text, validatehash = self._processflags(rawtext, flags, 'read', raw=raw)
1747 1746 if validatehash:
1748 1747 self.checkhash(text, node, rev=rev)
1749 1748
1750 1749 return text
1751 1750
1752 1751 def hash(self, text, p1, p2):
1753 1752 """Compute a node hash.
1754 1753
1755 1754 Available as a function so that subclasses can replace the hash
1756 1755 as needed.
1757 1756 """
1758 1757 return hash(text, p1, p2)
1759 1758
1760 1759 def _processflags(self, text, flags, operation, raw=False):
1761 1760 """Inspect revision data flags and applies transforms defined by
1762 1761 registered flag processors.
1763 1762
1764 1763 ``text`` - the revision data to process
1765 1764 ``flags`` - the revision flags
1766 1765 ``operation`` - the operation being performed (read or write)
1767 1766 ``raw`` - an optional argument describing if the raw transform should be
1768 1767 applied.
1769 1768
1770 1769 This method processes the flags in the order (or reverse order if
1771 1770 ``operation`` is 'write') defined by REVIDX_FLAGS_ORDER, applying the
1772 1771 flag processors registered for present flags. The order of flags defined
1773 1772 in REVIDX_FLAGS_ORDER needs to be stable to allow non-commutativity.
1774 1773
1775 1774 Returns a 2-tuple of ``(text, validatehash)`` where ``text`` is the
1776 1775 processed text and ``validatehash`` is a bool indicating whether the
1777 1776 returned text should be checked for hash integrity.
1778 1777
1779 1778 Note: If the ``raw`` argument is set, it has precedence over the
1780 1779 operation and will only update the value of ``validatehash``.
1781 1780 """
1782 1781 # fast path: no flag processors will run
1783 1782 if flags == 0:
1784 1783 return text, True
1785 1784 if not operation in ('read', 'write'):
1786 1785 raise ProgrammingError(_("invalid '%s' operation ") % (operation))
1787 1786 # Check all flags are known.
1788 1787 if flags & ~REVIDX_KNOWN_FLAGS:
1789 1788 raise RevlogError(_("incompatible revision flag '%#x'") %
1790 1789 (flags & ~REVIDX_KNOWN_FLAGS))
1791 1790 validatehash = True
1792 1791 # Depending on the operation (read or write), the order might be
1793 1792 # reversed due to non-commutative transforms.
1794 1793 orderedflags = REVIDX_FLAGS_ORDER
1795 1794 if operation == 'write':
1796 1795 orderedflags = reversed(orderedflags)
1797 1796
1798 1797 for flag in orderedflags:
1799 1798 # If a flagprocessor has been registered for a known flag, apply the
1800 1799 # related operation transform and update result tuple.
1801 1800 if flag & flags:
1802 1801 vhash = True
1803 1802
1804 1803 if flag not in _flagprocessors:
1805 1804 message = _("missing processor for flag '%#x'") % (flag)
1806 1805 raise RevlogError(message)
1807 1806
1808 1807 processor = _flagprocessors[flag]
1809 1808 if processor is not None:
1810 1809 readtransform, writetransform, rawtransform = processor
1811 1810
1812 1811 if raw:
1813 1812 vhash = rawtransform(self, text)
1814 1813 elif operation == 'read':
1815 1814 text, vhash = readtransform(self, text)
1816 1815 else: # write operation
1817 1816 text, vhash = writetransform(self, text)
1818 1817 validatehash = validatehash and vhash
1819 1818
1820 1819 return text, validatehash
1821 1820
1822 1821 def checkhash(self, text, node, p1=None, p2=None, rev=None):
1823 1822 """Check node hash integrity.
1824 1823
1825 1824 Available as a function so that subclasses can extend hash mismatch
1826 1825 behaviors as needed.
1827 1826 """
1828 1827 if p1 is None and p2 is None:
1829 1828 p1, p2 = self.parents(node)
1830 1829 if node != self.hash(text, p1, p2):
1831 1830 revornode = rev
1832 1831 if revornode is None:
1833 1832 revornode = templatefilters.short(hex(node))
1834 1833 raise RevlogError(_("integrity check failed on %s:%s")
1835 1834 % (self.indexfile, pycompat.bytestr(revornode)))
1836 1835
1837 1836 def _enforceinlinesize(self, tr, fp=None):
1838 1837 """Check if the revlog is too big for inline and convert if so.
1839 1838
1840 1839 This should be called after revisions are added to the revlog. If the
1841 1840 revlog has grown too large to be an inline revlog, it will convert it
1842 1841 to use multiple index and data files.
1843 1842 """
1844 1843 if not self._inline or (self.start(-2) + self.length(-2)) < _maxinline:
1845 1844 return
1846 1845
1847 1846 trinfo = tr.find(self.indexfile)
1848 1847 if trinfo is None:
1849 1848 raise RevlogError(_("%s not found in the transaction")
1850 1849 % self.indexfile)
1851 1850
1852 1851 trindex = trinfo[2]
1853 1852 if trindex is not None:
1854 1853 dataoff = self.start(trindex)
1855 1854 else:
1856 1855 # revlog was stripped at start of transaction, use all leftover data
1857 1856 trindex = len(self) - 1
1858 1857 dataoff = self.end(-2)
1859 1858
1860 1859 tr.add(self.datafile, dataoff)
1861 1860
1862 1861 if fp:
1863 1862 fp.flush()
1864 1863 fp.close()
1865 1864
1866 1865 with self._datafp('w') as df:
1867 1866 for r in self:
1868 1867 df.write(self._getsegmentforrevs(r, r)[1])
1869 1868
1870 1869 with self._indexfp('w') as fp:
1871 1870 self.version &= ~FLAG_INLINE_DATA
1872 1871 self._inline = False
1873 1872 io = self._io
1874 1873 for i in self:
1875 1874 e = io.packentry(self.index[i], self.node, self.version, i)
1876 1875 fp.write(e)
1877 1876
1878 1877 # the temp file replace the real index when we exit the context
1879 1878 # manager
1880 1879
1881 1880 tr.replace(self.indexfile, trindex * self._io.size)
1882 1881 self._chunkclear()
1883 1882
1884 1883 def addrevision(self, text, transaction, link, p1, p2, cachedelta=None,
1885 1884 node=None, flags=REVIDX_DEFAULT_FLAGS, deltacomputer=None):
1886 1885 """add a revision to the log
1887 1886
1888 1887 text - the revision data to add
1889 1888 transaction - the transaction object used for rollback
1890 1889 link - the linkrev data to add
1891 1890 p1, p2 - the parent nodeids of the revision
1892 1891 cachedelta - an optional precomputed delta
1893 1892 node - nodeid of revision; typically node is not specified, and it is
1894 1893 computed by default as hash(text, p1, p2), however subclasses might
1895 1894 use different hashing method (and override checkhash() in such case)
1896 1895 flags - the known flags to set on the revision
1897 1896 deltacomputer - an optional _deltacomputer instance shared between
1898 1897 multiple calls
1899 1898 """
1900 1899 if link == nullrev:
1901 1900 raise RevlogError(_("attempted to add linkrev -1 to %s")
1902 1901 % self.indexfile)
1903 1902
1904 1903 if flags:
1905 1904 node = node or self.hash(text, p1, p2)
1906 1905
1907 1906 rawtext, validatehash = self._processflags(text, flags, 'write')
1908 1907
1909 1908 # If the flag processor modifies the revision data, ignore any provided
1910 1909 # cachedelta.
1911 1910 if rawtext != text:
1912 1911 cachedelta = None
1913 1912
1914 1913 if len(rawtext) > _maxentrysize:
1915 1914 raise RevlogError(
1916 1915 _("%s: size of %d bytes exceeds maximum revlog storage of 2GiB")
1917 1916 % (self.indexfile, len(rawtext)))
1918 1917
1919 1918 node = node or self.hash(rawtext, p1, p2)
1920 1919 if node in self.nodemap:
1921 1920 return node
1922 1921
1923 1922 if validatehash:
1924 1923 self.checkhash(rawtext, node, p1=p1, p2=p2)
1925 1924
1926 1925 return self.addrawrevision(rawtext, transaction, link, p1, p2, node,
1927 1926 flags, cachedelta=cachedelta,
1928 1927 deltacomputer=deltacomputer)
1929 1928
1930 1929 def addrawrevision(self, rawtext, transaction, link, p1, p2, node, flags,
1931 1930 cachedelta=None, deltacomputer=None):
1932 1931 """add a raw revision with known flags, node and parents
1933 1932 useful when reusing a revision not stored in this revlog (ex: received
1934 1933 over wire, or read from an external bundle).
1935 1934 """
1936 1935 dfh = None
1937 1936 if not self._inline:
1938 1937 dfh = self._datafp("a+")
1939 1938 ifh = self._indexfp("a+")
1940 1939 try:
1941 1940 return self._addrevision(node, rawtext, transaction, link, p1, p2,
1942 1941 flags, cachedelta, ifh, dfh,
1943 1942 deltacomputer=deltacomputer)
1944 1943 finally:
1945 1944 if dfh:
1946 1945 dfh.close()
1947 1946 ifh.close()
1948 1947
1949 1948 def compress(self, data):
1950 1949 """Generate a possibly-compressed representation of data."""
1951 1950 if not data:
1952 1951 return '', data
1953 1952
1954 1953 compressed = self._compressor.compress(data)
1955 1954
1956 1955 if compressed:
1957 1956 # The revlog compressor added the header in the returned data.
1958 1957 return '', compressed
1959 1958
1960 1959 if data[0:1] == '\0':
1961 1960 return '', data
1962 1961 return 'u', data
1963 1962
1964 1963 def decompress(self, data):
1965 1964 """Decompress a revlog chunk.
1966 1965
1967 1966 The chunk is expected to begin with a header identifying the
1968 1967 format type so it can be routed to an appropriate decompressor.
1969 1968 """
1970 1969 if not data:
1971 1970 return data
1972 1971
1973 1972 # Revlogs are read much more frequently than they are written and many
1974 1973 # chunks only take microseconds to decompress, so performance is
1975 1974 # important here.
1976 1975 #
1977 1976 # We can make a few assumptions about revlogs:
1978 1977 #
1979 1978 # 1) the majority of chunks will be compressed (as opposed to inline
1980 1979 # raw data).
1981 1980 # 2) decompressing *any* data will likely by at least 10x slower than
1982 1981 # returning raw inline data.
1983 1982 # 3) we want to prioritize common and officially supported compression
1984 1983 # engines
1985 1984 #
1986 1985 # It follows that we want to optimize for "decompress compressed data
1987 1986 # when encoded with common and officially supported compression engines"
1988 1987 # case over "raw data" and "data encoded by less common or non-official
1989 1988 # compression engines." That is why we have the inline lookup first
1990 1989 # followed by the compengines lookup.
1991 1990 #
1992 1991 # According to `hg perfrevlogchunks`, this is ~0.5% faster for zlib
1993 1992 # compressed chunks. And this matters for changelog and manifest reads.
1994 1993 t = data[0:1]
1995 1994
1996 1995 if t == 'x':
1997 1996 try:
1998 1997 return _zlibdecompress(data)
1999 1998 except zlib.error as e:
2000 1999 raise RevlogError(_('revlog decompress error: %s') % str(e))
2001 2000 # '\0' is more common than 'u' so it goes first.
2002 2001 elif t == '\0':
2003 2002 return data
2004 2003 elif t == 'u':
2005 2004 return util.buffer(data, 1)
2006 2005
2007 2006 try:
2008 2007 compressor = self._decompressors[t]
2009 2008 except KeyError:
2010 2009 try:
2011 2010 engine = util.compengines.forrevlogheader(t)
2012 2011 compressor = engine.revlogcompressor()
2013 2012 self._decompressors[t] = compressor
2014 2013 except KeyError:
2015 2014 raise RevlogError(_('unknown compression type %r') % t)
2016 2015
2017 2016 return compressor.decompress(data)
2018 2017
2019 2018 def _isgooddeltainfo(self, d, textlen):
2020 2019 """Returns True if the given delta is good. Good means that it is within
2021 2020 the disk span, disk size, and chain length bounds that we know to be
2022 2021 performant."""
2023 2022 if d is None:
2024 2023 return False
2025 2024
2026 2025 # - 'd.distance' is the distance from the base revision -- bounding it
2027 2026 # limits the amount of I/O we need to do.
2028 2027 # - 'd.compresseddeltalen' is the sum of the total size of deltas we
2029 2028 # need to apply -- bounding it limits the amount of CPU we consume.
2030 2029
2031 2030 defaultmax = textlen * 4
2032 2031 maxdist = self._maxdeltachainspan
2033 2032 if not maxdist:
2034 2033 maxdist = d.distance # ensure the conditional pass
2035 2034 maxdist = max(maxdist, defaultmax)
2036 2035 if (d.distance > maxdist or d.deltalen > textlen or
2037 2036 d.compresseddeltalen > textlen * 2 or
2038 2037 (self._maxchainlen and d.chainlen > self._maxchainlen)):
2039 2038 return False
2040 2039
2041 2040 return True
2042 2041
2043 2042 def _addrevision(self, node, rawtext, transaction, link, p1, p2, flags,
2044 2043 cachedelta, ifh, dfh, alwayscache=False,
2045 2044 deltacomputer=None):
2046 2045 """internal function to add revisions to the log
2047 2046
2048 2047 see addrevision for argument descriptions.
2049 2048
2050 2049 note: "addrevision" takes non-raw text, "_addrevision" takes raw text.
2051 2050
2052 2051 if "deltacomputer" is not provided or None, a defaultdeltacomputer will
2053 2052 be used.
2054 2053
2055 2054 invariants:
2056 2055 - rawtext is optional (can be None); if not set, cachedelta must be set.
2057 2056 if both are set, they must correspond to each other.
2058 2057 """
2059 2058 if node == nullid:
2060 2059 raise RevlogError(_("%s: attempt to add null revision") %
2061 2060 (self.indexfile))
2062 2061 if node == wdirid:
2063 2062 raise RevlogError(_("%s: attempt to add wdir revision") %
2064 2063 (self.indexfile))
2065 2064
2066 2065 if self._inline:
2067 2066 fh = ifh
2068 2067 else:
2069 2068 fh = dfh
2070 2069
2071 2070 btext = [rawtext]
2072 2071
2073 2072 curr = len(self)
2074 2073 prev = curr - 1
2075 2074 offset = self.end(prev)
2076 2075 p1r, p2r = self.rev(p1), self.rev(p2)
2077 2076
2078 2077 # full versions are inserted when the needed deltas
2079 2078 # become comparable to the uncompressed text
2080 2079 if rawtext is None:
2081 2080 textlen = mdiff.patchedsize(self.rawsize(cachedelta[0]),
2082 2081 cachedelta[1])
2083 2082 else:
2084 2083 textlen = len(rawtext)
2085 2084
2086 2085 if deltacomputer is None:
2087 2086 deltacomputer = _deltacomputer(self)
2088 2087
2089 2088 revinfo = _revisioninfo(node, p1, p2, btext, textlen, cachedelta, flags)
2090 2089 deltainfo = deltacomputer.finddeltainfo(revinfo, fh)
2091 2090
2092 2091 if deltainfo is not None:
2093 2092 base = deltainfo.base
2094 2093 chainbase = deltainfo.chainbase
2095 2094 data = deltainfo.data
2096 2095 l = deltainfo.deltalen
2097 2096 else:
2098 2097 rawtext = deltacomputer.buildtext(revinfo, fh)
2099 2098 data = self.compress(rawtext)
2100 2099 l = len(data[1]) + len(data[0])
2101 2100 base = chainbase = curr
2102 2101
2103 2102 e = (offset_type(offset, flags), l, textlen,
2104 2103 base, link, p1r, p2r, node)
2105 2104 self.index.insert(-1, e)
2106 2105 self.nodemap[node] = curr
2107 2106
2108 2107 entry = self._io.packentry(e, self.node, self.version, curr)
2109 2108 self._writeentry(transaction, ifh, dfh, entry, data, link, offset)
2110 2109
2111 2110 if alwayscache and rawtext is None:
2112 2111 rawtext = deltacomputer._buildtext(revinfo, fh)
2113 2112
2114 2113 if type(rawtext) == bytes: # only accept immutable objects
2115 2114 self._cache = (node, curr, rawtext)
2116 2115 self._chainbasecache[curr] = chainbase
2117 2116 return node
2118 2117
2119 2118 def _writeentry(self, transaction, ifh, dfh, entry, data, link, offset):
2120 2119 # Files opened in a+ mode have inconsistent behavior on various
2121 2120 # platforms. Windows requires that a file positioning call be made
2122 2121 # when the file handle transitions between reads and writes. See
2123 2122 # 3686fa2b8eee and the mixedfilemodewrapper in windows.py. On other
2124 2123 # platforms, Python or the platform itself can be buggy. Some versions
2125 2124 # of Solaris have been observed to not append at the end of the file
2126 2125 # if the file was seeked to before the end. See issue4943 for more.
2127 2126 #
2128 2127 # We work around this issue by inserting a seek() before writing.
2129 2128 # Note: This is likely not necessary on Python 3.
2130 2129 ifh.seek(0, os.SEEK_END)
2131 2130 if dfh:
2132 2131 dfh.seek(0, os.SEEK_END)
2133 2132
2134 2133 curr = len(self) - 1
2135 2134 if not self._inline:
2136 2135 transaction.add(self.datafile, offset)
2137 2136 transaction.add(self.indexfile, curr * len(entry))
2138 2137 if data[0]:
2139 2138 dfh.write(data[0])
2140 2139 dfh.write(data[1])
2141 2140 ifh.write(entry)
2142 2141 else:
2143 2142 offset += curr * self._io.size
2144 2143 transaction.add(self.indexfile, offset, curr)
2145 2144 ifh.write(entry)
2146 2145 ifh.write(data[0])
2147 2146 ifh.write(data[1])
2148 2147 self._enforceinlinesize(transaction, ifh)
2149 2148
2150 2149 def addgroup(self, deltas, linkmapper, transaction, addrevisioncb=None):
2151 2150 """
2152 2151 add a delta group
2153 2152
2154 2153 given a set of deltas, add them to the revision log. the
2155 2154 first delta is against its parent, which should be in our
2156 2155 log, the rest are against the previous delta.
2157 2156
2158 2157 If ``addrevisioncb`` is defined, it will be called with arguments of
2159 2158 this revlog and the node that was added.
2160 2159 """
2161 2160
2162 2161 nodes = []
2163 2162
2164 2163 r = len(self)
2165 2164 end = 0
2166 2165 if r:
2167 2166 end = self.end(r - 1)
2168 2167 ifh = self._indexfp("a+")
2169 2168 isize = r * self._io.size
2170 2169 if self._inline:
2171 2170 transaction.add(self.indexfile, end + isize, r)
2172 2171 dfh = None
2173 2172 else:
2174 2173 transaction.add(self.indexfile, isize, r)
2175 2174 transaction.add(self.datafile, end)
2176 2175 dfh = self._datafp("a+")
2177 2176 def flush():
2178 2177 if dfh:
2179 2178 dfh.flush()
2180 2179 ifh.flush()
2181 2180 try:
2182 2181 deltacomputer = _deltacomputer(self)
2183 2182 # loop through our set of deltas
2184 2183 for data in deltas:
2185 2184 node, p1, p2, linknode, deltabase, delta, flags = data
2186 2185 link = linkmapper(linknode)
2187 2186 flags = flags or REVIDX_DEFAULT_FLAGS
2188 2187
2189 2188 nodes.append(node)
2190 2189
2191 2190 if node in self.nodemap:
2192 2191 # this can happen if two branches make the same change
2193 2192 continue
2194 2193
2195 2194 for p in (p1, p2):
2196 2195 if p not in self.nodemap:
2197 2196 raise LookupError(p, self.indexfile,
2198 2197 _('unknown parent'))
2199 2198
2200 2199 if deltabase not in self.nodemap:
2201 2200 raise LookupError(deltabase, self.indexfile,
2202 2201 _('unknown delta base'))
2203 2202
2204 2203 baserev = self.rev(deltabase)
2205 2204
2206 2205 if baserev != nullrev and self.iscensored(baserev):
2207 2206 # if base is censored, delta must be full replacement in a
2208 2207 # single patch operation
2209 2208 hlen = struct.calcsize(">lll")
2210 2209 oldlen = self.rawsize(baserev)
2211 2210 newlen = len(delta) - hlen
2212 2211 if delta[:hlen] != mdiff.replacediffheader(oldlen, newlen):
2213 2212 raise error.CensoredBaseError(self.indexfile,
2214 2213 self.node(baserev))
2215 2214
2216 2215 if not flags and self._peek_iscensored(baserev, delta, flush):
2217 2216 flags |= REVIDX_ISCENSORED
2218 2217
2219 2218 # We assume consumers of addrevisioncb will want to retrieve
2220 2219 # the added revision, which will require a call to
2221 2220 # revision(). revision() will fast path if there is a cache
2222 2221 # hit. So, we tell _addrevision() to always cache in this case.
2223 2222 # We're only using addgroup() in the context of changegroup
2224 2223 # generation so the revision data can always be handled as raw
2225 2224 # by the flagprocessor.
2226 2225 self._addrevision(node, None, transaction, link,
2227 2226 p1, p2, flags, (baserev, delta),
2228 2227 ifh, dfh,
2229 2228 alwayscache=bool(addrevisioncb),
2230 2229 deltacomputer=deltacomputer)
2231 2230
2232 2231 if addrevisioncb:
2233 2232 addrevisioncb(self, node)
2234 2233
2235 2234 if not dfh and not self._inline:
2236 2235 # addrevision switched from inline to conventional
2237 2236 # reopen the index
2238 2237 ifh.close()
2239 2238 dfh = self._datafp("a+")
2240 2239 ifh = self._indexfp("a+")
2241 2240 finally:
2242 2241 if dfh:
2243 2242 dfh.close()
2244 2243 ifh.close()
2245 2244
2246 2245 return nodes
2247 2246
2248 2247 def iscensored(self, rev):
2249 2248 """Check if a file revision is censored."""
2250 2249 return False
2251 2250
2252 2251 def _peek_iscensored(self, baserev, delta, flush):
2253 2252 """Quickly check if a delta produces a censored revision."""
2254 2253 return False
2255 2254
2256 2255 def getstrippoint(self, minlink):
2257 2256 """find the minimum rev that must be stripped to strip the linkrev
2258 2257
2259 2258 Returns a tuple containing the minimum rev and a set of all revs that
2260 2259 have linkrevs that will be broken by this strip.
2261 2260 """
2262 2261 brokenrevs = set()
2263 2262 strippoint = len(self)
2264 2263
2265 2264 heads = {}
2266 2265 futurelargelinkrevs = set()
2267 2266 for head in self.headrevs():
2268 2267 headlinkrev = self.linkrev(head)
2269 2268 heads[head] = headlinkrev
2270 2269 if headlinkrev >= minlink:
2271 2270 futurelargelinkrevs.add(headlinkrev)
2272 2271
2273 2272 # This algorithm involves walking down the rev graph, starting at the
2274 2273 # heads. Since the revs are topologically sorted according to linkrev,
2275 2274 # once all head linkrevs are below the minlink, we know there are
2276 2275 # no more revs that could have a linkrev greater than minlink.
2277 2276 # So we can stop walking.
2278 2277 while futurelargelinkrevs:
2279 2278 strippoint -= 1
2280 2279 linkrev = heads.pop(strippoint)
2281 2280
2282 2281 if linkrev < minlink:
2283 2282 brokenrevs.add(strippoint)
2284 2283 else:
2285 2284 futurelargelinkrevs.remove(linkrev)
2286 2285
2287 2286 for p in self.parentrevs(strippoint):
2288 2287 if p != nullrev:
2289 2288 plinkrev = self.linkrev(p)
2290 2289 heads[p] = plinkrev
2291 2290 if plinkrev >= minlink:
2292 2291 futurelargelinkrevs.add(plinkrev)
2293 2292
2294 2293 return strippoint, brokenrevs
2295 2294
2296 2295 def strip(self, minlink, transaction):
2297 2296 """truncate the revlog on the first revision with a linkrev >= minlink
2298 2297
2299 2298 This function is called when we're stripping revision minlink and
2300 2299 its descendants from the repository.
2301 2300
2302 2301 We have to remove all revisions with linkrev >= minlink, because
2303 2302 the equivalent changelog revisions will be renumbered after the
2304 2303 strip.
2305 2304
2306 2305 So we truncate the revlog on the first of these revisions, and
2307 2306 trust that the caller has saved the revisions that shouldn't be
2308 2307 removed and that it'll re-add them after this truncation.
2309 2308 """
2310 2309 if len(self) == 0:
2311 2310 return
2312 2311
2313 2312 rev, _ = self.getstrippoint(minlink)
2314 2313 if rev == len(self):
2315 2314 return
2316 2315
2317 2316 # first truncate the files on disk
2318 2317 end = self.start(rev)
2319 2318 if not self._inline:
2320 2319 transaction.add(self.datafile, end)
2321 2320 end = rev * self._io.size
2322 2321 else:
2323 2322 end += rev * self._io.size
2324 2323
2325 2324 transaction.add(self.indexfile, end)
2326 2325
2327 2326 # then reset internal state in memory to forget those revisions
2328 2327 self._cache = None
2329 2328 self._chaininfocache = {}
2330 2329 self._chunkclear()
2331 2330 for x in xrange(rev, len(self)):
2332 2331 del self.nodemap[self.node(x)]
2333 2332
2334 2333 del self.index[rev:-1]
2335 2334
2336 2335 def checksize(self):
2337 2336 expected = 0
2338 2337 if len(self):
2339 2338 expected = max(0, self.end(len(self) - 1))
2340 2339
2341 2340 try:
2342 2341 with self._datafp() as f:
2343 2342 f.seek(0, 2)
2344 2343 actual = f.tell()
2345 2344 dd = actual - expected
2346 2345 except IOError as inst:
2347 2346 if inst.errno != errno.ENOENT:
2348 2347 raise
2349 2348 dd = 0
2350 2349
2351 2350 try:
2352 2351 f = self.opener(self.indexfile)
2353 2352 f.seek(0, 2)
2354 2353 actual = f.tell()
2355 2354 f.close()
2356 2355 s = self._io.size
2357 2356 i = max(0, actual // s)
2358 2357 di = actual - (i * s)
2359 2358 if self._inline:
2360 2359 databytes = 0
2361 2360 for r in self:
2362 2361 databytes += max(0, self.length(r))
2363 2362 dd = 0
2364 2363 di = actual - len(self) * s - databytes
2365 2364 except IOError as inst:
2366 2365 if inst.errno != errno.ENOENT:
2367 2366 raise
2368 2367 di = 0
2369 2368
2370 2369 return (dd, di)
2371 2370
2372 2371 def files(self):
2373 2372 res = [self.indexfile]
2374 2373 if not self._inline:
2375 2374 res.append(self.datafile)
2376 2375 return res
2377 2376
2378 2377 DELTAREUSEALWAYS = 'always'
2379 2378 DELTAREUSESAMEREVS = 'samerevs'
2380 2379 DELTAREUSENEVER = 'never'
2381 2380
2382 2381 DELTAREUSEFULLADD = 'fulladd'
2383 2382
2384 2383 DELTAREUSEALL = {'always', 'samerevs', 'never', 'fulladd'}
2385 2384
2386 2385 def clone(self, tr, destrevlog, addrevisioncb=None,
2387 2386 deltareuse=DELTAREUSESAMEREVS, aggressivemergedeltas=None):
2388 2387 """Copy this revlog to another, possibly with format changes.
2389 2388
2390 2389 The destination revlog will contain the same revisions and nodes.
2391 2390 However, it may not be bit-for-bit identical due to e.g. delta encoding
2392 2391 differences.
2393 2392
2394 2393 The ``deltareuse`` argument control how deltas from the existing revlog
2395 2394 are preserved in the destination revlog. The argument can have the
2396 2395 following values:
2397 2396
2398 2397 DELTAREUSEALWAYS
2399 2398 Deltas will always be reused (if possible), even if the destination
2400 2399 revlog would not select the same revisions for the delta. This is the
2401 2400 fastest mode of operation.
2402 2401 DELTAREUSESAMEREVS
2403 2402 Deltas will be reused if the destination revlog would pick the same
2404 2403 revisions for the delta. This mode strikes a balance between speed
2405 2404 and optimization.
2406 2405 DELTAREUSENEVER
2407 2406 Deltas will never be reused. This is the slowest mode of execution.
2408 2407 This mode can be used to recompute deltas (e.g. if the diff/delta
2409 2408 algorithm changes).
2410 2409
2411 2410 Delta computation can be slow, so the choice of delta reuse policy can
2412 2411 significantly affect run time.
2413 2412
2414 2413 The default policy (``DELTAREUSESAMEREVS``) strikes a balance between
2415 2414 two extremes. Deltas will be reused if they are appropriate. But if the
2416 2415 delta could choose a better revision, it will do so. This means if you
2417 2416 are converting a non-generaldelta revlog to a generaldelta revlog,
2418 2417 deltas will be recomputed if the delta's parent isn't a parent of the
2419 2418 revision.
2420 2419
2421 2420 In addition to the delta policy, the ``aggressivemergedeltas`` argument
2422 2421 controls whether to compute deltas against both parents for merges.
2423 2422 By default, the current default is used.
2424 2423 """
2425 2424 if deltareuse not in self.DELTAREUSEALL:
2426 2425 raise ValueError(_('value for deltareuse invalid: %s') % deltareuse)
2427 2426
2428 2427 if len(destrevlog):
2429 2428 raise ValueError(_('destination revlog is not empty'))
2430 2429
2431 2430 if getattr(self, 'filteredrevs', None):
2432 2431 raise ValueError(_('source revlog has filtered revisions'))
2433 2432 if getattr(destrevlog, 'filteredrevs', None):
2434 2433 raise ValueError(_('destination revlog has filtered revisions'))
2435 2434
2436 2435 # lazydeltabase controls whether to reuse a cached delta, if possible.
2437 2436 oldlazydeltabase = destrevlog._lazydeltabase
2438 2437 oldamd = destrevlog._aggressivemergedeltas
2439 2438
2440 2439 try:
2441 2440 if deltareuse == self.DELTAREUSEALWAYS:
2442 2441 destrevlog._lazydeltabase = True
2443 2442 elif deltareuse == self.DELTAREUSESAMEREVS:
2444 2443 destrevlog._lazydeltabase = False
2445 2444
2446 2445 destrevlog._aggressivemergedeltas = aggressivemergedeltas or oldamd
2447 2446
2448 2447 populatecachedelta = deltareuse in (self.DELTAREUSEALWAYS,
2449 2448 self.DELTAREUSESAMEREVS)
2450 2449
2451 2450 deltacomputer = _deltacomputer(destrevlog)
2452 2451 index = self.index
2453 2452 for rev in self:
2454 2453 entry = index[rev]
2455 2454
2456 2455 # Some classes override linkrev to take filtered revs into
2457 2456 # account. Use raw entry from index.
2458 2457 flags = entry[0] & 0xffff
2459 2458 linkrev = entry[4]
2460 2459 p1 = index[entry[5]][7]
2461 2460 p2 = index[entry[6]][7]
2462 2461 node = entry[7]
2463 2462
2464 2463 # (Possibly) reuse the delta from the revlog if allowed and
2465 2464 # the revlog chunk is a delta.
2466 2465 cachedelta = None
2467 2466 rawtext = None
2468 2467 if populatecachedelta:
2469 2468 dp = self.deltaparent(rev)
2470 2469 if dp != nullrev:
2471 2470 cachedelta = (dp, str(self._chunk(rev)))
2472 2471
2473 2472 if not cachedelta:
2474 2473 rawtext = self.revision(rev, raw=True)
2475 2474
2476 2475
2477 2476 if deltareuse == self.DELTAREUSEFULLADD:
2478 2477 destrevlog.addrevision(rawtext, tr, linkrev, p1, p2,
2479 2478 cachedelta=cachedelta,
2480 2479 node=node, flags=flags,
2481 2480 deltacomputer=deltacomputer)
2482 2481 else:
2483 2482 ifh = destrevlog.opener(destrevlog.indexfile, 'a+',
2484 2483 checkambig=False)
2485 2484 dfh = None
2486 2485 if not destrevlog._inline:
2487 2486 dfh = destrevlog.opener(destrevlog.datafile, 'a+')
2488 2487 try:
2489 2488 destrevlog._addrevision(node, rawtext, tr, linkrev, p1,
2490 2489 p2, flags, cachedelta, ifh, dfh,
2491 2490 deltacomputer=deltacomputer)
2492 2491 finally:
2493 2492 if dfh:
2494 2493 dfh.close()
2495 2494 ifh.close()
2496 2495
2497 2496 if addrevisioncb:
2498 2497 addrevisioncb(self, rev, node)
2499 2498 finally:
2500 2499 destrevlog._lazydeltabase = oldlazydeltabase
2501 2500 destrevlog._aggressivemergedeltas = oldamd
General Comments 0
You need to be logged in to leave comments. Login now