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