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