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