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