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