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