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