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