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