##// END OF EJS Templates
histedit: properly apply bundle2 backups...
Pierre-Yves David -
r26798:38dcb85f default
parent child Browse files
Show More
@@ -1,1236 +1,1248 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
40 40 # d, drop = remove commit from history
41 41 # m, mess = edit commit message without changing commit content
42 42 #
43 43
44 44 In this file, lines beginning with ``#`` are ignored. You must specify a rule
45 45 for each revision in your history. For example, if you had meant to add gamma
46 46 before beta, and then wanted to add delta in the same revision as beta, you
47 47 would reorganize the file to look like this::
48 48
49 49 pick 030b686bedc4 Add gamma
50 50 pick c561b4e977df Add beta
51 51 fold 7c2fd3b9020c Add delta
52 52
53 53 # Edit history between c561b4e977df and 7c2fd3b9020c
54 54 #
55 55 # Commits are listed from least to most recent
56 56 #
57 57 # Commands:
58 58 # p, pick = use commit
59 59 # e, edit = use commit, but stop for amending
60 60 # f, fold = use commit, but combine it with the one above
61 61 # r, roll = like fold, but discard this commit's description
62 62 # d, drop = remove commit from history
63 63 # m, mess = edit commit message without changing commit content
64 64 #
65 65
66 66 At which point you close the editor and ``histedit`` starts working. When you
67 67 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
68 68 those revisions together, offering you a chance to clean up the commit message::
69 69
70 70 Add beta
71 71 ***
72 72 Add delta
73 73
74 74 Edit the commit message to your liking, then close the editor. For
75 75 this example, let's assume that the commit message was changed to
76 76 ``Add beta and delta.`` After histedit has run and had a chance to
77 77 remove any old or temporary revisions it needed, the history looks
78 78 like this::
79 79
80 80 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
81 81 | Add beta and delta.
82 82 |
83 83 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
84 84 | Add gamma
85 85 |
86 86 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
87 87 Add alpha
88 88
89 89 Note that ``histedit`` does *not* remove any revisions (even its own temporary
90 90 ones) until after it has completed all the editing operations, so it will
91 91 probably perform several strip operations when it's done. For the above example,
92 92 it had to run strip twice. Strip can be slow depending on a variety of factors,
93 93 so you might need to be a little patient. You can choose to keep the original
94 94 revisions by passing the ``--keep`` flag.
95 95
96 96 The ``edit`` operation will drop you back to a command prompt,
97 97 allowing you to edit files freely, or even use ``hg record`` to commit
98 98 some changes as a separate commit. When you're done, any remaining
99 99 uncommitted changes will be committed as well. When done, run ``hg
100 100 histedit --continue`` to finish this step. You'll be prompted for a
101 101 new commit message, but the default commit message will be the
102 102 original message for the ``edit`` ed revision.
103 103
104 104 The ``message`` operation will give you a chance to revise a commit
105 105 message without changing the contents. It's a shortcut for doing
106 106 ``edit`` immediately followed by `hg histedit --continue``.
107 107
108 108 If ``histedit`` encounters a conflict when moving a revision (while
109 109 handling ``pick`` or ``fold``), it'll stop in a similar manner to
110 110 ``edit`` with the difference that it won't prompt you for a commit
111 111 message when done. If you decide at this point that you don't like how
112 112 much work it will be to rearrange history, or that you made a mistake,
113 113 you can use ``hg histedit --abort`` to abandon the new changes you
114 114 have made and return to the state before you attempted to edit your
115 115 history.
116 116
117 117 If we clone the histedit-ed example repository above and add four more
118 118 changes, such that we have the following history::
119 119
120 120 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
121 121 | Add theta
122 122 |
123 123 o 5 140988835471 2009-04-27 18:04 -0500 stefan
124 124 | Add eta
125 125 |
126 126 o 4 122930637314 2009-04-27 18:04 -0500 stefan
127 127 | Add zeta
128 128 |
129 129 o 3 836302820282 2009-04-27 18:04 -0500 stefan
130 130 | Add epsilon
131 131 |
132 132 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
133 133 | Add beta and delta.
134 134 |
135 135 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
136 136 | Add gamma
137 137 |
138 138 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
139 139 Add alpha
140 140
141 141 If you run ``hg histedit --outgoing`` on the clone then it is the same
142 142 as running ``hg histedit 836302820282``. If you need plan to push to a
143 143 repository that Mercurial does not detect to be related to the source
144 144 repo, you can add a ``--force`` option.
145 145
146 146 Histedit rule lines are truncated to 80 characters by default. You
147 147 can customize this behavior by setting a different length in your
148 148 configuration file::
149 149
150 150 [histedit]
151 151 linelen = 120 # truncate rule lines at 120 characters
152 152 """
153 153
154 154 try:
155 155 import cPickle as pickle
156 156 pickle.dump # import now
157 157 except ImportError:
158 158 import pickle
159 159 import errno
160 160 import os
161 161 import sys
162 162
163 from mercurial import bundle2
163 164 from mercurial import cmdutil
164 165 from mercurial import discovery
165 166 from mercurial import error
166 167 from mercurial import copies
167 168 from mercurial import context
168 169 from mercurial import exchange
169 170 from mercurial import extensions
170 171 from mercurial import hg
171 172 from mercurial import node
172 173 from mercurial import repair
173 174 from mercurial import scmutil
174 175 from mercurial import util
175 176 from mercurial import obsolete
176 177 from mercurial import merge as mergemod
177 178 from mercurial.lock import release
178 179 from mercurial.i18n import _
179 180
180 181 cmdtable = {}
181 182 command = cmdutil.command(cmdtable)
182 183
183 184 # Note for extension authors: ONLY specify testedwith = 'internal' for
184 185 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
185 186 # be specifying the version(s) of Mercurial they are tested with, or
186 187 # leave the attribute unspecified.
187 188 testedwith = 'internal'
188 189
189 190 # i18n: command names and abbreviations must remain untranslated
190 191 editcomment = _("""# Edit history between %s and %s
191 192 #
192 193 # Commits are listed from least to most recent
193 194 #
194 195 # Commands:
195 196 # p, pick = use commit
196 197 # e, edit = use commit, but stop for amending
197 198 # f, fold = use commit, but combine it with the one above
198 199 # r, roll = like fold, but discard this commit's description
199 200 # d, drop = remove commit from history
200 201 # m, mess = edit commit message without changing commit content
201 202 #
202 203 """)
203 204
204 205 class histeditstate(object):
205 206 def __init__(self, repo, parentctxnode=None, rules=None, keep=None,
206 207 topmost=None, replacements=None, lock=None, wlock=None):
207 208 self.repo = repo
208 209 self.rules = rules
209 210 self.keep = keep
210 211 self.topmost = topmost
211 212 self.parentctxnode = parentctxnode
212 213 self.lock = lock
213 214 self.wlock = wlock
214 215 self.backupfile = None
215 216 if replacements is None:
216 217 self.replacements = []
217 218 else:
218 219 self.replacements = replacements
219 220
220 221 def read(self):
221 222 """Load histedit state from disk and set fields appropriately."""
222 223 try:
223 224 fp = self.repo.vfs('histedit-state', 'r')
224 225 except IOError as err:
225 226 if err.errno != errno.ENOENT:
226 227 raise
227 228 raise error.Abort(_('no histedit in progress'))
228 229
229 230 try:
230 231 data = pickle.load(fp)
231 232 parentctxnode, rules, keep, topmost, replacements = data
232 233 backupfile = None
233 234 except pickle.UnpicklingError:
234 235 data = self._load()
235 236 parentctxnode, rules, keep, topmost, replacements, backupfile = data
236 237
237 238 self.parentctxnode = parentctxnode
238 239 self.rules = rules
239 240 self.keep = keep
240 241 self.topmost = topmost
241 242 self.replacements = replacements
242 243 self.backupfile = backupfile
243 244
244 245 def write(self):
245 246 fp = self.repo.vfs('histedit-state', 'w')
246 247 fp.write('v1\n')
247 248 fp.write('%s\n' % node.hex(self.parentctxnode))
248 249 fp.write('%s\n' % node.hex(self.topmost))
249 250 fp.write('%s\n' % self.keep)
250 251 fp.write('%d\n' % len(self.rules))
251 252 for rule in self.rules:
252 253 fp.write('%s\n' % rule[0]) # action
253 254 fp.write('%s\n' % rule[1]) # remainder
254 255 fp.write('%d\n' % len(self.replacements))
255 256 for replacement in self.replacements:
256 257 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
257 258 for r in replacement[1])))
258 259 backupfile = self.backupfile
259 260 if not backupfile:
260 261 backupfile = ''
261 262 fp.write('%s\n' % backupfile)
262 263 fp.close()
263 264
264 265 def _load(self):
265 266 fp = self.repo.vfs('histedit-state', 'r')
266 267 lines = [l[:-1] for l in fp.readlines()]
267 268
268 269 index = 0
269 270 lines[index] # version number
270 271 index += 1
271 272
272 273 parentctxnode = node.bin(lines[index])
273 274 index += 1
274 275
275 276 topmost = node.bin(lines[index])
276 277 index += 1
277 278
278 279 keep = lines[index] == 'True'
279 280 index += 1
280 281
281 282 # Rules
282 283 rules = []
283 284 rulelen = int(lines[index])
284 285 index += 1
285 286 for i in xrange(rulelen):
286 287 ruleaction = lines[index]
287 288 index += 1
288 289 rule = lines[index]
289 290 index += 1
290 291 rules.append((ruleaction, rule))
291 292
292 293 # Replacements
293 294 replacements = []
294 295 replacementlen = int(lines[index])
295 296 index += 1
296 297 for i in xrange(replacementlen):
297 298 replacement = lines[index]
298 299 original = node.bin(replacement[:40])
299 300 succ = [node.bin(replacement[i:i + 40]) for i in
300 301 range(40, len(replacement), 40)]
301 302 replacements.append((original, succ))
302 303 index += 1
303 304
304 305 backupfile = lines[index]
305 306 index += 1
306 307
307 308 fp.close()
308 309
309 310 return parentctxnode, rules, keep, topmost, replacements, backupfile
310 311
311 312 def clear(self):
312 313 if self.inprogress():
313 314 self.repo.vfs.unlink('histedit-state')
314 315
315 316 def inprogress(self):
316 317 return self.repo.vfs.exists('histedit-state')
317 318
318 319 class histeditaction(object):
319 320 def __init__(self, state, node):
320 321 self.state = state
321 322 self.repo = state.repo
322 323 self.node = node
323 324
324 325 @classmethod
325 326 def fromrule(cls, state, rule):
326 327 """Parses the given rule, returning an instance of the histeditaction.
327 328 """
328 329 repo = state.repo
329 330 rulehash = rule.strip().split(' ', 1)[0]
330 331 try:
331 332 node = repo[rulehash].node()
332 333 except error.RepoError:
333 334 raise error.Abort(_('unknown changeset %s listed') % rulehash[:12])
334 335 return cls(state, node)
335 336
336 337 def run(self):
337 338 """Runs the action. The default behavior is simply apply the action's
338 339 rulectx onto the current parentctx."""
339 340 self.applychange()
340 341 self.continuedirty()
341 342 return self.continueclean()
342 343
343 344 def applychange(self):
344 345 """Applies the changes from this action's rulectx onto the current
345 346 parentctx, but does not commit them."""
346 347 repo = self.repo
347 348 rulectx = repo[self.node]
348 349 hg.update(repo, self.state.parentctxnode)
349 350 stats = applychanges(repo.ui, repo, rulectx, {})
350 351 if stats and stats[3] > 0:
351 352 raise error.InterventionRequired(_('Fix up the change and run '
352 353 'hg histedit --continue'))
353 354
354 355 def continuedirty(self):
355 356 """Continues the action when changes have been applied to the working
356 357 copy. The default behavior is to commit the dirty changes."""
357 358 repo = self.repo
358 359 rulectx = repo[self.node]
359 360
360 361 editor = self.commiteditor()
361 362 commit = commitfuncfor(repo, rulectx)
362 363
363 364 commit(text=rulectx.description(), user=rulectx.user(),
364 365 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
365 366
366 367 def commiteditor(self):
367 368 """The editor to be used to edit the commit message."""
368 369 return False
369 370
370 371 def continueclean(self):
371 372 """Continues the action when the working copy is clean. The default
372 373 behavior is to accept the current commit as the new version of the
373 374 rulectx."""
374 375 ctx = self.repo['.']
375 376 if ctx.node() == self.state.parentctxnode:
376 377 self.repo.ui.warn(_('%s: empty changeset\n') %
377 378 node.short(self.node))
378 379 return ctx, [(self.node, tuple())]
379 380 if ctx.node() == self.node:
380 381 # Nothing changed
381 382 return ctx, []
382 383 return ctx, [(self.node, (ctx.node(),))]
383 384
384 385 def commitfuncfor(repo, src):
385 386 """Build a commit function for the replacement of <src>
386 387
387 388 This function ensure we apply the same treatment to all changesets.
388 389
389 390 - Add a 'histedit_source' entry in extra.
390 391
391 392 Note that fold has its own separated logic because its handling is a bit
392 393 different and not easily factored out of the fold method.
393 394 """
394 395 phasemin = src.phase()
395 396 def commitfunc(**kwargs):
396 397 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
397 398 try:
398 399 repo.ui.setconfig('phases', 'new-commit', phasemin,
399 400 'histedit')
400 401 extra = kwargs.get('extra', {}).copy()
401 402 extra['histedit_source'] = src.hex()
402 403 kwargs['extra'] = extra
403 404 return repo.commit(**kwargs)
404 405 finally:
405 406 repo.ui.restoreconfig(phasebackup)
406 407 return commitfunc
407 408
408 409 def applychanges(ui, repo, ctx, opts):
409 410 """Merge changeset from ctx (only) in the current working directory"""
410 411 wcpar = repo.dirstate.parents()[0]
411 412 if ctx.p1().node() == wcpar:
412 413 # edits are "in place" we do not need to make any merge,
413 414 # just applies changes on parent for edition
414 415 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
415 416 stats = None
416 417 else:
417 418 try:
418 419 # ui.forcemerge is an internal variable, do not document
419 420 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
420 421 'histedit')
421 422 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
422 423 finally:
423 424 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
424 425 return stats
425 426
426 427 def collapse(repo, first, last, commitopts, skipprompt=False):
427 428 """collapse the set of revisions from first to last as new one.
428 429
429 430 Expected commit options are:
430 431 - message
431 432 - date
432 433 - username
433 434 Commit message is edited in all cases.
434 435
435 436 This function works in memory."""
436 437 ctxs = list(repo.set('%d::%d', first, last))
437 438 if not ctxs:
438 439 return None
439 440 for c in ctxs:
440 441 if not c.mutable():
441 442 raise error.Abort(
442 443 _("cannot fold into public change %s") % node.short(c.node()))
443 444 base = first.parents()[0]
444 445
445 446 # commit a new version of the old changeset, including the update
446 447 # collect all files which might be affected
447 448 files = set()
448 449 for ctx in ctxs:
449 450 files.update(ctx.files())
450 451
451 452 # Recompute copies (avoid recording a -> b -> a)
452 453 copied = copies.pathcopies(base, last)
453 454
454 455 # prune files which were reverted by the updates
455 456 def samefile(f):
456 457 if f in last.manifest():
457 458 a = last.filectx(f)
458 459 if f in base.manifest():
459 460 b = base.filectx(f)
460 461 return (a.data() == b.data()
461 462 and a.flags() == b.flags())
462 463 else:
463 464 return False
464 465 else:
465 466 return f not in base.manifest()
466 467 files = [f for f in files if not samefile(f)]
467 468 # commit version of these files as defined by head
468 469 headmf = last.manifest()
469 470 def filectxfn(repo, ctx, path):
470 471 if path in headmf:
471 472 fctx = last[path]
472 473 flags = fctx.flags()
473 474 mctx = context.memfilectx(repo,
474 475 fctx.path(), fctx.data(),
475 476 islink='l' in flags,
476 477 isexec='x' in flags,
477 478 copied=copied.get(path))
478 479 return mctx
479 480 return None
480 481
481 482 if commitopts.get('message'):
482 483 message = commitopts['message']
483 484 else:
484 485 message = first.description()
485 486 user = commitopts.get('user')
486 487 date = commitopts.get('date')
487 488 extra = commitopts.get('extra')
488 489
489 490 parents = (first.p1().node(), first.p2().node())
490 491 editor = None
491 492 if not skipprompt:
492 493 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
493 494 new = context.memctx(repo,
494 495 parents=parents,
495 496 text=message,
496 497 files=files,
497 498 filectxfn=filectxfn,
498 499 user=user,
499 500 date=date,
500 501 extra=extra,
501 502 editor=editor)
502 503 return repo.commitctx(new)
503 504
504 505 class pick(histeditaction):
505 506 def run(self):
506 507 rulectx = self.repo[self.node]
507 508 if rulectx.parents()[0].node() == self.state.parentctxnode:
508 509 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
509 510 return rulectx, []
510 511
511 512 return super(pick, self).run()
512 513
513 514 class edit(histeditaction):
514 515 def run(self):
515 516 repo = self.repo
516 517 rulectx = repo[self.node]
517 518 hg.update(repo, self.state.parentctxnode)
518 519 applychanges(repo.ui, repo, rulectx, {})
519 520 raise error.InterventionRequired(
520 521 _('Make changes as needed, you may commit or record as needed '
521 522 'now.\nWhen you are finished, run hg histedit --continue to '
522 523 'resume.'))
523 524
524 525 def commiteditor(self):
525 526 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
526 527
527 528 class fold(histeditaction):
528 529 def continuedirty(self):
529 530 repo = self.repo
530 531 rulectx = repo[self.node]
531 532
532 533 commit = commitfuncfor(repo, rulectx)
533 534 commit(text='fold-temp-revision %s' % node.short(self.node),
534 535 user=rulectx.user(), date=rulectx.date(),
535 536 extra=rulectx.extra())
536 537
537 538 def continueclean(self):
538 539 repo = self.repo
539 540 ctx = repo['.']
540 541 rulectx = repo[self.node]
541 542 parentctxnode = self.state.parentctxnode
542 543 if ctx.node() == parentctxnode:
543 544 repo.ui.warn(_('%s: empty changeset\n') %
544 545 node.short(self.node))
545 546 return ctx, [(self.node, (parentctxnode,))]
546 547
547 548 parentctx = repo[parentctxnode]
548 549 newcommits = set(c.node() for c in repo.set('(%d::. - %d)', parentctx,
549 550 parentctx))
550 551 if not newcommits:
551 552 repo.ui.warn(_('%s: cannot fold - working copy is not a '
552 553 'descendant of previous commit %s\n') %
553 554 (node.short(self.node), node.short(parentctxnode)))
554 555 return ctx, [(self.node, (ctx.node(),))]
555 556
556 557 middlecommits = newcommits.copy()
557 558 middlecommits.discard(ctx.node())
558 559
559 560 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
560 561 middlecommits)
561 562
562 563 def skipprompt(self):
563 564 """Returns true if the rule should skip the message editor.
564 565
565 566 For example, 'fold' wants to show an editor, but 'rollup'
566 567 doesn't want to.
567 568 """
568 569 return False
569 570
570 571 def mergedescs(self):
571 572 """Returns true if the rule should merge messages of multiple changes.
572 573
573 574 This exists mainly so that 'rollup' rules can be a subclass of
574 575 'fold'.
575 576 """
576 577 return True
577 578
578 579 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
579 580 parent = ctx.parents()[0].node()
580 581 hg.update(repo, parent)
581 582 ### prepare new commit data
582 583 commitopts = {}
583 584 commitopts['user'] = ctx.user()
584 585 # commit message
585 586 if not self.mergedescs():
586 587 newmessage = ctx.description()
587 588 else:
588 589 newmessage = '\n***\n'.join(
589 590 [ctx.description()] +
590 591 [repo[r].description() for r in internalchanges] +
591 592 [oldctx.description()]) + '\n'
592 593 commitopts['message'] = newmessage
593 594 # date
594 595 commitopts['date'] = max(ctx.date(), oldctx.date())
595 596 extra = ctx.extra().copy()
596 597 # histedit_source
597 598 # note: ctx is likely a temporary commit but that the best we can do
598 599 # here. This is sufficient to solve issue3681 anyway.
599 600 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
600 601 commitopts['extra'] = extra
601 602 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
602 603 try:
603 604 phasemin = max(ctx.phase(), oldctx.phase())
604 605 repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit')
605 606 n = collapse(repo, ctx, repo[newnode], commitopts,
606 607 skipprompt=self.skipprompt())
607 608 finally:
608 609 repo.ui.restoreconfig(phasebackup)
609 610 if n is None:
610 611 return ctx, []
611 612 hg.update(repo, n)
612 613 replacements = [(oldctx.node(), (newnode,)),
613 614 (ctx.node(), (n,)),
614 615 (newnode, (n,)),
615 616 ]
616 617 for ich in internalchanges:
617 618 replacements.append((ich, (n,)))
618 619 return repo[n], replacements
619 620
620 621 class _multifold(fold):
621 622 """fold subclass used for when multiple folds happen in a row
622 623
623 624 We only want to fire the editor for the folded message once when
624 625 (say) four changes are folded down into a single change. This is
625 626 similar to rollup, but we should preserve both messages so that
626 627 when the last fold operation runs we can show the user all the
627 628 commit messages in their editor.
628 629 """
629 630 def skipprompt(self):
630 631 return True
631 632
632 633 class rollup(fold):
633 634 def mergedescs(self):
634 635 return False
635 636
636 637 def skipprompt(self):
637 638 return True
638 639
639 640 class drop(histeditaction):
640 641 def run(self):
641 642 parentctx = self.repo[self.state.parentctxnode]
642 643 return parentctx, [(self.node, tuple())]
643 644
644 645 class message(histeditaction):
645 646 def commiteditor(self):
646 647 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
647 648
648 649 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
649 650 """utility function to find the first outgoing changeset
650 651
651 652 Used by initialization code"""
652 653 if opts is None:
653 654 opts = {}
654 655 dest = ui.expandpath(remote or 'default-push', remote or 'default')
655 656 dest, revs = hg.parseurl(dest, None)[:2]
656 657 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
657 658
658 659 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
659 660 other = hg.peer(repo, opts, dest)
660 661
661 662 if revs:
662 663 revs = [repo.lookup(rev) for rev in revs]
663 664
664 665 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
665 666 if not outgoing.missing:
666 667 raise error.Abort(_('no outgoing ancestors'))
667 668 roots = list(repo.revs("roots(%ln)", outgoing.missing))
668 669 if 1 < len(roots):
669 670 msg = _('there are ambiguous outgoing revisions')
670 671 hint = _('see "hg help histedit" for more detail')
671 672 raise error.Abort(msg, hint=hint)
672 673 return repo.lookup(roots[0])
673 674
674 675 actiontable = {'p': pick,
675 676 'pick': pick,
676 677 'e': edit,
677 678 'edit': edit,
678 679 'f': fold,
679 680 'fold': fold,
680 681 '_multifold': _multifold,
681 682 'r': rollup,
682 683 'roll': rollup,
683 684 'd': drop,
684 685 'drop': drop,
685 686 'm': message,
686 687 'mess': message,
687 688 }
688 689
689 690 @command('histedit',
690 691 [('', 'commands', '',
691 692 _('read history edits from the specified file'), _('FILE')),
692 693 ('c', 'continue', False, _('continue an edit already in progress')),
693 694 ('', 'edit-plan', False, _('edit remaining actions list')),
694 695 ('k', 'keep', False,
695 696 _("don't strip old nodes after edit is complete")),
696 697 ('', 'abort', False, _('abort an edit in progress')),
697 698 ('o', 'outgoing', False, _('changesets not found in destination')),
698 699 ('f', 'force', False,
699 700 _('force outgoing even for unrelated repositories')),
700 701 ('r', 'rev', [], _('first revision to be edited'), _('REV'))],
701 702 _("ANCESTOR | --outgoing [URL]"))
702 703 def histedit(ui, repo, *freeargs, **opts):
703 704 """interactively edit changeset history
704 705
705 706 This command edits changesets between ANCESTOR and the parent of
706 707 the working directory.
707 708
708 709 With --outgoing, this edits changesets not found in the
709 710 destination repository. If URL of the destination is omitted, the
710 711 'default-push' (or 'default') path will be used.
711 712
712 713 For safety, this command is also aborted if there are ambiguous
713 714 outgoing revisions which may confuse users: for example, if there
714 715 are multiple branches containing outgoing revisions.
715 716
716 717 Use "min(outgoing() and ::.)" or similar revset specification
717 718 instead of --outgoing to specify edit target revision exactly in
718 719 such ambiguous situation. See :hg:`help revsets` for detail about
719 720 selecting revisions.
720 721
721 722 Returns 0 on success, 1 if user intervention is required (not only
722 723 for intentional "edit" command, but also for resolving unexpected
723 724 conflicts).
724 725 """
725 726 state = histeditstate(repo)
726 727 try:
727 728 state.wlock = repo.wlock()
728 729 state.lock = repo.lock()
729 730 _histedit(ui, repo, state, *freeargs, **opts)
730 731 finally:
731 732 release(state.lock, state.wlock)
732 733
733 734 def _histedit(ui, repo, state, *freeargs, **opts):
734 735 # TODO only abort if we try and histedit mq patches, not just
735 736 # blanket if mq patches are applied somewhere
736 737 mq = getattr(repo, 'mq', None)
737 738 if mq and mq.applied:
738 739 raise error.Abort(_('source has mq patches applied'))
739 740
740 741 # basic argument incompatibility processing
741 742 outg = opts.get('outgoing')
742 743 cont = opts.get('continue')
743 744 editplan = opts.get('edit_plan')
744 745 abort = opts.get('abort')
745 746 force = opts.get('force')
746 747 rules = opts.get('commands', '')
747 748 revs = opts.get('rev', [])
748 749 goal = 'new' # This invocation goal, in new, continue, abort
749 750 if force and not outg:
750 751 raise error.Abort(_('--force only allowed with --outgoing'))
751 752 if cont:
752 753 if any((outg, abort, revs, freeargs, rules, editplan)):
753 754 raise error.Abort(_('no arguments allowed with --continue'))
754 755 goal = 'continue'
755 756 elif abort:
756 757 if any((outg, revs, freeargs, rules, editplan)):
757 758 raise error.Abort(_('no arguments allowed with --abort'))
758 759 goal = 'abort'
759 760 elif editplan:
760 761 if any((outg, revs, freeargs)):
761 762 raise error.Abort(_('only --commands argument allowed with '
762 763 '--edit-plan'))
763 764 goal = 'edit-plan'
764 765 else:
765 766 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
766 767 raise error.Abort(_('history edit already in progress, try '
767 768 '--continue or --abort'))
768 769 if outg:
769 770 if revs:
770 771 raise error.Abort(_('no revisions allowed with --outgoing'))
771 772 if len(freeargs) > 1:
772 773 raise error.Abort(
773 774 _('only one repo argument allowed with --outgoing'))
774 775 else:
775 776 revs.extend(freeargs)
776 777 if len(revs) == 0:
777 778 # experimental config: histedit.defaultrev
778 779 histeditdefault = ui.config('histedit', 'defaultrev')
779 780 if histeditdefault:
780 781 revs.append(histeditdefault)
781 782 if len(revs) != 1:
782 783 raise error.Abort(
783 784 _('histedit requires exactly one ancestor revision'))
784 785
785 786
786 787 replacements = []
787 788 state.keep = opts.get('keep', False)
788 789 supportsmarkers = obsolete.isenabled(repo, obsolete.createmarkersopt)
789 790
790 791 # rebuild state
791 792 if goal == 'continue':
792 793 state.read()
793 794 state = bootstrapcontinue(ui, state, opts)
794 795 elif goal == 'edit-plan':
795 796 state.read()
796 797 if not rules:
797 798 comment = editcomment % (node.short(state.parentctxnode),
798 799 node.short(state.topmost))
799 800 rules = ruleeditor(repo, ui, state.rules, comment)
800 801 else:
801 802 if rules == '-':
802 803 f = sys.stdin
803 804 else:
804 805 f = open(rules)
805 806 rules = f.read()
806 807 f.close()
807 808 rules = [l for l in (r.strip() for r in rules.splitlines())
808 809 if l and not l.startswith('#')]
809 810 rules = verifyrules(rules, repo, [repo[c] for [_a, c] in state.rules])
810 811 state.rules = rules
811 812 state.write()
812 813 return
813 814 elif goal == 'abort':
814 815 try:
815 816 state.read()
816 817 tmpnodes, leafs = newnodestoabort(state)
817 818 ui.debug('restore wc to old parent %s\n'
818 819 % node.short(state.topmost))
819 820
820 821 # Recover our old commits if necessary
821 822 if not state.topmost in repo and state.backupfile:
822 823 backupfile = repo.join(state.backupfile)
823 824 f = hg.openpath(ui, backupfile)
824 825 gen = exchange.readbundle(ui, f, backupfile)
826 tr = repo.transaction('histedit.abort')
827 try:
828 if not isinstance(gen, bundle2.unbundle20):
825 829 gen.apply(repo, 'histedit', 'bundle:' + backupfile)
830 if isinstance(gen, bundle2.unbundle20):
831 bundle2.applybundle(repo, gen, tr,
832 source='histedit',
833 url='bundle:' + backupfile)
834 tr.close()
835 finally:
836 tr.release()
837
826 838 os.remove(backupfile)
827 839
828 840 # check whether we should update away
829 841 if repo.unfiltered().revs('parents() and (%n or %ln::)',
830 842 state.parentctxnode, leafs | tmpnodes):
831 843 hg.clean(repo, state.topmost)
832 844 cleanupnode(ui, repo, 'created', tmpnodes)
833 845 cleanupnode(ui, repo, 'temp', leafs)
834 846 except Exception:
835 847 if state.inprogress():
836 848 ui.warn(_('warning: encountered an exception during histedit '
837 849 '--abort; the repository may not have been completely '
838 850 'cleaned up\n'))
839 851 raise
840 852 finally:
841 853 state.clear()
842 854 return
843 855 else:
844 856 cmdutil.checkunfinished(repo)
845 857 cmdutil.bailifchanged(repo)
846 858
847 859 topmost, empty = repo.dirstate.parents()
848 860 if outg:
849 861 if freeargs:
850 862 remote = freeargs[0]
851 863 else:
852 864 remote = None
853 865 root = findoutgoing(ui, repo, remote, force, opts)
854 866 else:
855 867 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
856 868 if len(rr) != 1:
857 869 raise error.Abort(_('The specified revisions must have '
858 870 'exactly one common root'))
859 871 root = rr[0].node()
860 872
861 873 revs = between(repo, root, topmost, state.keep)
862 874 if not revs:
863 875 raise error.Abort(_('%s is not an ancestor of working directory') %
864 876 node.short(root))
865 877
866 878 ctxs = [repo[r] for r in revs]
867 879 if not rules:
868 880 comment = editcomment % (node.short(root), node.short(topmost))
869 881 rules = ruleeditor(repo, ui, [['pick', c] for c in ctxs], comment)
870 882 else:
871 883 if rules == '-':
872 884 f = sys.stdin
873 885 else:
874 886 f = open(rules)
875 887 rules = f.read()
876 888 f.close()
877 889 rules = [l for l in (r.strip() for r in rules.splitlines())
878 890 if l and not l.startswith('#')]
879 891 rules = verifyrules(rules, repo, ctxs)
880 892
881 893 parentctxnode = repo[root].parents()[0].node()
882 894
883 895 state.parentctxnode = parentctxnode
884 896 state.rules = rules
885 897 state.topmost = topmost
886 898 state.replacements = replacements
887 899
888 900 # Create a backup so we can always abort completely.
889 901 backupfile = None
890 902 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
891 903 backupfile = repair._bundle(repo, [parentctxnode], [topmost], root,
892 904 'histedit')
893 905 state.backupfile = backupfile
894 906
895 907 # preprocess rules so that we can hide inner folds from the user
896 908 # and only show one editor
897 909 rules = state.rules[:]
898 910 for idx, ((action, ha), (nextact, unused)) in enumerate(
899 911 zip(rules, rules[1:] + [(None, None)])):
900 912 if action == 'fold' and nextact == 'fold':
901 913 state.rules[idx] = '_multifold', ha
902 914
903 915 while state.rules:
904 916 state.write()
905 917 action, ha = state.rules.pop(0)
906 918 ui.debug('histedit: processing %s %s\n' % (action, ha[:12]))
907 919 actobj = actiontable[action].fromrule(state, ha)
908 920 parentctx, replacement_ = actobj.run()
909 921 state.parentctxnode = parentctx.node()
910 922 state.replacements.extend(replacement_)
911 923 state.write()
912 924
913 925 hg.update(repo, state.parentctxnode)
914 926
915 927 mapping, tmpnodes, created, ntm = processreplacement(state)
916 928 if mapping:
917 929 for prec, succs in mapping.iteritems():
918 930 if not succs:
919 931 ui.debug('histedit: %s is dropped\n' % node.short(prec))
920 932 else:
921 933 ui.debug('histedit: %s is replaced by %s\n' % (
922 934 node.short(prec), node.short(succs[0])))
923 935 if len(succs) > 1:
924 936 m = 'histedit: %s'
925 937 for n in succs[1:]:
926 938 ui.debug(m % node.short(n))
927 939
928 940 if supportsmarkers:
929 941 # Only create markers if the temp nodes weren't already removed.
930 942 obsolete.createmarkers(repo, ((repo[t],()) for t in sorted(tmpnodes)
931 943 if t in repo))
932 944 else:
933 945 cleanupnode(ui, repo, 'temp', tmpnodes)
934 946
935 947 if not state.keep:
936 948 if mapping:
937 949 movebookmarks(ui, repo, mapping, state.topmost, ntm)
938 950 # TODO update mq state
939 951 if supportsmarkers:
940 952 markers = []
941 953 # sort by revision number because it sound "right"
942 954 for prec in sorted(mapping, key=repo.changelog.rev):
943 955 succs = mapping[prec]
944 956 markers.append((repo[prec],
945 957 tuple(repo[s] for s in succs)))
946 958 if markers:
947 959 obsolete.createmarkers(repo, markers)
948 960 else:
949 961 cleanupnode(ui, repo, 'replaced', mapping)
950 962
951 963 state.clear()
952 964 if os.path.exists(repo.sjoin('undo')):
953 965 os.unlink(repo.sjoin('undo'))
954 966
955 967 def bootstrapcontinue(ui, state, opts):
956 968 repo = state.repo
957 969 if state.rules:
958 970 action, currentnode = state.rules.pop(0)
959 971
960 972 actobj = actiontable[action].fromrule(state, currentnode)
961 973
962 974 s = repo.status()
963 975 if s.modified or s.added or s.removed or s.deleted:
964 976 actobj.continuedirty()
965 977 s = repo.status()
966 978 if s.modified or s.added or s.removed or s.deleted:
967 979 raise error.Abort(_("working copy still dirty"))
968 980
969 981 parentctx, replacements = actobj.continueclean()
970 982
971 983 state.parentctxnode = parentctx.node()
972 984 state.replacements.extend(replacements)
973 985
974 986 return state
975 987
976 988 def between(repo, old, new, keep):
977 989 """select and validate the set of revision to edit
978 990
979 991 When keep is false, the specified set can't have children."""
980 992 ctxs = list(repo.set('%n::%n', old, new))
981 993 if ctxs and not keep:
982 994 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
983 995 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
984 996 raise error.Abort(_('cannot edit history that would orphan nodes'))
985 997 if repo.revs('(%ld) and merge()', ctxs):
986 998 raise error.Abort(_('cannot edit history that contains merges'))
987 999 root = ctxs[0] # list is already sorted by repo.set
988 1000 if not root.mutable():
989 1001 raise error.Abort(_('cannot edit public changeset: %s') % root,
990 1002 hint=_('see "hg help phases" for details'))
991 1003 return [c.node() for c in ctxs]
992 1004
993 1005 def makedesc(repo, action, rev):
994 1006 """build a initial action line for a ctx
995 1007
996 1008 line are in the form:
997 1009
998 1010 <action> <hash> <rev> <summary>
999 1011 """
1000 1012 ctx = repo[rev]
1001 1013 summary = ''
1002 1014 if ctx.description():
1003 1015 summary = ctx.description().splitlines()[0]
1004 1016 line = '%s %s %d %s' % (action, ctx, ctx.rev(), summary)
1005 1017 # trim to 80 columns so it's not stupidly wide in my editor
1006 1018 maxlen = repo.ui.configint('histedit', 'linelen', default=80)
1007 1019 maxlen = max(maxlen, 22) # avoid truncating hash
1008 1020 return util.ellipsis(line, maxlen)
1009 1021
1010 1022 def ruleeditor(repo, ui, rules, editcomment=""):
1011 1023 """open an editor to edit rules
1012 1024
1013 1025 rules are in the format [ [act, ctx], ...] like in state.rules
1014 1026 """
1015 1027 rules = '\n'.join([makedesc(repo, act, rev) for [act, rev] in rules])
1016 1028 rules += '\n\n'
1017 1029 rules += editcomment
1018 1030 rules = ui.edit(rules, ui.username())
1019 1031
1020 1032 # Save edit rules in .hg/histedit-last-edit.txt in case
1021 1033 # the user needs to ask for help after something
1022 1034 # surprising happens.
1023 1035 f = open(repo.join('histedit-last-edit.txt'), 'w')
1024 1036 f.write(rules)
1025 1037 f.close()
1026 1038
1027 1039 return rules
1028 1040
1029 1041 def verifyrules(rules, repo, ctxs):
1030 1042 """Verify that there exists exactly one edit rule per given changeset.
1031 1043
1032 1044 Will abort if there are to many or too few rules, a malformed rule,
1033 1045 or a rule on a changeset outside of the user-given range.
1034 1046 """
1035 1047 parsed = []
1036 1048 expected = set(c.hex() for c in ctxs)
1037 1049 seen = set()
1038 1050 for r in rules:
1039 1051 if ' ' not in r:
1040 1052 raise error.Abort(_('malformed line "%s"') % r)
1041 1053 action, rest = r.split(' ', 1)
1042 1054 ha = rest.strip().split(' ', 1)[0]
1043 1055 try:
1044 1056 ha = repo[ha].hex()
1045 1057 except error.RepoError:
1046 1058 raise error.Abort(_('unknown changeset %s listed') % ha[:12])
1047 1059 if ha not in expected:
1048 1060 raise error.Abort(
1049 1061 _('may not use changesets other than the ones listed'))
1050 1062 if ha in seen:
1051 1063 raise error.Abort(_('duplicated command for changeset %s') %
1052 1064 ha[:12])
1053 1065 seen.add(ha)
1054 1066 if action not in actiontable or action.startswith('_'):
1055 1067 raise error.Abort(_('unknown action "%s"') % action)
1056 1068 parsed.append([action, ha])
1057 1069 missing = sorted(expected - seen) # sort to stabilize output
1058 1070 if missing:
1059 1071 raise error.Abort(_('missing rules for changeset %s') %
1060 1072 missing[0][:12],
1061 1073 hint=_('do you want to use the drop action?'))
1062 1074 return parsed
1063 1075
1064 1076 def newnodestoabort(state):
1065 1077 """process the list of replacements to return
1066 1078
1067 1079 1) the list of final node
1068 1080 2) the list of temporary node
1069 1081
1070 1082 This meant to be used on abort as less data are required in this case.
1071 1083 """
1072 1084 replacements = state.replacements
1073 1085 allsuccs = set()
1074 1086 replaced = set()
1075 1087 for rep in replacements:
1076 1088 allsuccs.update(rep[1])
1077 1089 replaced.add(rep[0])
1078 1090 newnodes = allsuccs - replaced
1079 1091 tmpnodes = allsuccs & replaced
1080 1092 return newnodes, tmpnodes
1081 1093
1082 1094
1083 1095 def processreplacement(state):
1084 1096 """process the list of replacements to return
1085 1097
1086 1098 1) the final mapping between original and created nodes
1087 1099 2) the list of temporary node created by histedit
1088 1100 3) the list of new commit created by histedit"""
1089 1101 replacements = state.replacements
1090 1102 allsuccs = set()
1091 1103 replaced = set()
1092 1104 fullmapping = {}
1093 1105 # initialize basic set
1094 1106 # fullmapping records all operations recorded in replacement
1095 1107 for rep in replacements:
1096 1108 allsuccs.update(rep[1])
1097 1109 replaced.add(rep[0])
1098 1110 fullmapping.setdefault(rep[0], set()).update(rep[1])
1099 1111 new = allsuccs - replaced
1100 1112 tmpnodes = allsuccs & replaced
1101 1113 # Reduce content fullmapping into direct relation between original nodes
1102 1114 # and final node created during history edition
1103 1115 # Dropped changeset are replaced by an empty list
1104 1116 toproceed = set(fullmapping)
1105 1117 final = {}
1106 1118 while toproceed:
1107 1119 for x in list(toproceed):
1108 1120 succs = fullmapping[x]
1109 1121 for s in list(succs):
1110 1122 if s in toproceed:
1111 1123 # non final node with unknown closure
1112 1124 # We can't process this now
1113 1125 break
1114 1126 elif s in final:
1115 1127 # non final node, replace with closure
1116 1128 succs.remove(s)
1117 1129 succs.update(final[s])
1118 1130 else:
1119 1131 final[x] = succs
1120 1132 toproceed.remove(x)
1121 1133 # remove tmpnodes from final mapping
1122 1134 for n in tmpnodes:
1123 1135 del final[n]
1124 1136 # we expect all changes involved in final to exist in the repo
1125 1137 # turn `final` into list (topologically sorted)
1126 1138 nm = state.repo.changelog.nodemap
1127 1139 for prec, succs in final.items():
1128 1140 final[prec] = sorted(succs, key=nm.get)
1129 1141
1130 1142 # computed topmost element (necessary for bookmark)
1131 1143 if new:
1132 1144 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1133 1145 elif not final:
1134 1146 # Nothing rewritten at all. we won't need `newtopmost`
1135 1147 # It is the same as `oldtopmost` and `processreplacement` know it
1136 1148 newtopmost = None
1137 1149 else:
1138 1150 # every body died. The newtopmost is the parent of the root.
1139 1151 r = state.repo.changelog.rev
1140 1152 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1141 1153
1142 1154 return final, tmpnodes, new, newtopmost
1143 1155
1144 1156 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
1145 1157 """Move bookmark from old to newly created node"""
1146 1158 if not mapping:
1147 1159 # if nothing got rewritten there is not purpose for this function
1148 1160 return
1149 1161 moves = []
1150 1162 for bk, old in sorted(repo._bookmarks.iteritems()):
1151 1163 if old == oldtopmost:
1152 1164 # special case ensure bookmark stay on tip.
1153 1165 #
1154 1166 # This is arguably a feature and we may only want that for the
1155 1167 # active bookmark. But the behavior is kept compatible with the old
1156 1168 # version for now.
1157 1169 moves.append((bk, newtopmost))
1158 1170 continue
1159 1171 base = old
1160 1172 new = mapping.get(base, None)
1161 1173 if new is None:
1162 1174 continue
1163 1175 while not new:
1164 1176 # base is killed, trying with parent
1165 1177 base = repo[base].p1().node()
1166 1178 new = mapping.get(base, (base,))
1167 1179 # nothing to move
1168 1180 moves.append((bk, new[-1]))
1169 1181 if moves:
1170 1182 marks = repo._bookmarks
1171 1183 for mark, new in moves:
1172 1184 old = marks[mark]
1173 1185 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
1174 1186 % (mark, node.short(old), node.short(new)))
1175 1187 marks[mark] = new
1176 1188 marks.write()
1177 1189
1178 1190 def cleanupnode(ui, repo, name, nodes):
1179 1191 """strip a group of nodes from the repository
1180 1192
1181 1193 The set of node to strip may contains unknown nodes."""
1182 1194 ui.debug('should strip %s nodes %s\n' %
1183 1195 (name, ', '.join([node.short(n) for n in nodes])))
1184 1196 lock = None
1185 1197 try:
1186 1198 lock = repo.lock()
1187 1199 # do not let filtering get in the way of the cleanse
1188 1200 # we should probably get rid of obsolescence marker created during the
1189 1201 # histedit, but we currently do not have such information.
1190 1202 repo = repo.unfiltered()
1191 1203 # Find all nodes that need to be stripped
1192 1204 # (we use %lr instead of %ln to silently ignore unknown items)
1193 1205 nm = repo.changelog.nodemap
1194 1206 nodes = sorted(n for n in nodes if n in nm)
1195 1207 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1196 1208 for c in roots:
1197 1209 # We should process node in reverse order to strip tip most first.
1198 1210 # but this trigger a bug in changegroup hook.
1199 1211 # This would reduce bundle overhead
1200 1212 repair.strip(ui, repo, c)
1201 1213 finally:
1202 1214 release(lock)
1203 1215
1204 1216 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1205 1217 if isinstance(nodelist, str):
1206 1218 nodelist = [nodelist]
1207 1219 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1208 1220 state = histeditstate(repo)
1209 1221 state.read()
1210 1222 histedit_nodes = set([repo[rulehash].node() for (action, rulehash)
1211 1223 in state.rules if rulehash in repo])
1212 1224 strip_nodes = set([repo[n].node() for n in nodelist])
1213 1225 common_nodes = histedit_nodes & strip_nodes
1214 1226 if common_nodes:
1215 1227 raise error.Abort(_("histedit in progress, can't strip %s")
1216 1228 % ', '.join(node.short(x) for x in common_nodes))
1217 1229 return orig(ui, repo, nodelist, *args, **kwargs)
1218 1230
1219 1231 extensions.wrapfunction(repair, 'strip', stripwrapper)
1220 1232
1221 1233 def summaryhook(ui, repo):
1222 1234 if not os.path.exists(repo.join('histedit-state')):
1223 1235 return
1224 1236 state = histeditstate(repo)
1225 1237 state.read()
1226 1238 if state.rules:
1227 1239 # i18n: column positioning for "hg summary"
1228 1240 ui.write(_('hist: %s (histedit --continue)\n') %
1229 1241 (ui.label(_('%d remaining'), 'histedit.remaining') %
1230 1242 len(state.rules)))
1231 1243
1232 1244 def extsetup(ui):
1233 1245 cmdutil.summaryhooks.add('histedit', summaryhook)
1234 1246 cmdutil.unfinishedstates.append(
1235 1247 ['histedit-state', False, True, _('histedit in progress'),
1236 1248 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
General Comments 0
You need to be logged in to leave comments. Login now