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