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