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