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