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