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