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