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