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