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