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