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