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