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