##// END OF EJS Templates
histedit: omit useless message from update (edit)...
timeless -
r27407:bf4d5d8d default
parent child Browse files
Show More
@@ -1,1414 +1,1414
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 Histedit rule lines are truncated to 80 characters by default. You
147 147 can customize this behavior by setting a different length in your
148 148 configuration file::
149 149
150 150 [histedit]
151 151 linelen = 120 # truncate rule lines at 120 characters
152 152
153 153 ``hg histedit`` attempts to automatically choose an appropriate base
154 154 revision to use. To change which base revision is used, define a
155 155 revset in your configuration file::
156 156
157 157 [histedit]
158 158 defaultrev = only(.) & draft()
159 159 """
160 160
161 161 try:
162 162 import cPickle as pickle
163 163 pickle.dump # import now
164 164 except ImportError:
165 165 import pickle
166 166 import errno
167 167 import os
168 168 import sys
169 169
170 170 from mercurial import bundle2
171 171 from mercurial import cmdutil
172 172 from mercurial import discovery
173 173 from mercurial import error
174 174 from mercurial import copies
175 175 from mercurial import context
176 176 from mercurial import destutil
177 177 from mercurial import exchange
178 178 from mercurial import extensions
179 179 from mercurial import hg
180 180 from mercurial import node
181 181 from mercurial import repair
182 182 from mercurial import scmutil
183 183 from mercurial import util
184 184 from mercurial import obsolete
185 185 from mercurial import merge as mergemod
186 186 from mercurial.lock import release
187 187 from mercurial.i18n import _
188 188
189 189 cmdtable = {}
190 190 command = cmdutil.command(cmdtable)
191 191
192 192 class _constraints(object):
193 193 # aborts if there are multiple rules for one node
194 194 noduplicates = 'noduplicates'
195 195 # abort if the node does belong to edited stack
196 196 forceother = 'forceother'
197 197 # abort if the node doesn't belong to edited stack
198 198 noother = 'noother'
199 199
200 200 @classmethod
201 201 def known(cls):
202 202 return set([v for k, v in cls.__dict__.items() if k[0] != '_'])
203 203
204 204 # Note for extension authors: ONLY specify testedwith = 'internal' for
205 205 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
206 206 # be specifying the version(s) of Mercurial they are tested with, or
207 207 # leave the attribute unspecified.
208 208 testedwith = 'internal'
209 209
210 210 # i18n: command names and abbreviations must remain untranslated
211 211 editcomment = _("""# Edit history between %s and %s
212 212 #
213 213 # Commits are listed from least to most recent
214 214 #
215 215 # Commands:
216 216 # p, pick = use commit
217 217 # e, edit = use commit, but stop for amending
218 218 # f, fold = use commit, but combine it with the one above
219 219 # r, roll = like fold, but discard this commit's description
220 220 # d, drop = remove commit from history
221 221 # m, mess = edit commit message without changing commit content
222 222 #
223 223 """)
224 224
225 225 class histeditstate(object):
226 226 def __init__(self, repo, parentctxnode=None, actions=None, keep=None,
227 227 topmost=None, replacements=None, lock=None, wlock=None):
228 228 self.repo = repo
229 229 self.actions = actions
230 230 self.keep = keep
231 231 self.topmost = topmost
232 232 self.parentctxnode = parentctxnode
233 233 self.lock = lock
234 234 self.wlock = wlock
235 235 self.backupfile = None
236 236 if replacements is None:
237 237 self.replacements = []
238 238 else:
239 239 self.replacements = replacements
240 240
241 241 def read(self):
242 242 """Load histedit state from disk and set fields appropriately."""
243 243 try:
244 244 fp = self.repo.vfs('histedit-state', 'r')
245 245 except IOError as err:
246 246 if err.errno != errno.ENOENT:
247 247 raise
248 248 raise error.Abort(_('no histedit in progress'))
249 249
250 250 try:
251 251 data = pickle.load(fp)
252 252 parentctxnode, rules, keep, topmost, replacements = data
253 253 backupfile = None
254 254 except pickle.UnpicklingError:
255 255 data = self._load()
256 256 parentctxnode, rules, keep, topmost, replacements, backupfile = data
257 257
258 258 self.parentctxnode = parentctxnode
259 259 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
260 260 actions = parserules(rules, self)
261 261 self.actions = actions
262 262 self.keep = keep
263 263 self.topmost = topmost
264 264 self.replacements = replacements
265 265 self.backupfile = backupfile
266 266
267 267 def write(self):
268 268 fp = self.repo.vfs('histedit-state', 'w')
269 269 fp.write('v1\n')
270 270 fp.write('%s\n' % node.hex(self.parentctxnode))
271 271 fp.write('%s\n' % node.hex(self.topmost))
272 272 fp.write('%s\n' % self.keep)
273 273 fp.write('%d\n' % len(self.actions))
274 274 for action in self.actions:
275 275 fp.write('%s\n' % action.tostate())
276 276 fp.write('%d\n' % len(self.replacements))
277 277 for replacement in self.replacements:
278 278 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
279 279 for r in replacement[1])))
280 280 backupfile = self.backupfile
281 281 if not backupfile:
282 282 backupfile = ''
283 283 fp.write('%s\n' % backupfile)
284 284 fp.close()
285 285
286 286 def _load(self):
287 287 fp = self.repo.vfs('histedit-state', 'r')
288 288 lines = [l[:-1] for l in fp.readlines()]
289 289
290 290 index = 0
291 291 lines[index] # version number
292 292 index += 1
293 293
294 294 parentctxnode = node.bin(lines[index])
295 295 index += 1
296 296
297 297 topmost = node.bin(lines[index])
298 298 index += 1
299 299
300 300 keep = lines[index] == 'True'
301 301 index += 1
302 302
303 303 # Rules
304 304 rules = []
305 305 rulelen = int(lines[index])
306 306 index += 1
307 307 for i in xrange(rulelen):
308 308 ruleaction = lines[index]
309 309 index += 1
310 310 rule = lines[index]
311 311 index += 1
312 312 rules.append((ruleaction, rule))
313 313
314 314 # Replacements
315 315 replacements = []
316 316 replacementlen = int(lines[index])
317 317 index += 1
318 318 for i in xrange(replacementlen):
319 319 replacement = lines[index]
320 320 original = node.bin(replacement[:40])
321 321 succ = [node.bin(replacement[i:i + 40]) for i in
322 322 range(40, len(replacement), 40)]
323 323 replacements.append((original, succ))
324 324 index += 1
325 325
326 326 backupfile = lines[index]
327 327 index += 1
328 328
329 329 fp.close()
330 330
331 331 return parentctxnode, rules, keep, topmost, replacements, backupfile
332 332
333 333 def clear(self):
334 334 if self.inprogress():
335 335 self.repo.vfs.unlink('histedit-state')
336 336
337 337 def inprogress(self):
338 338 return self.repo.vfs.exists('histedit-state')
339 339
340 340
341 341 class histeditaction(object):
342 342 def __init__(self, state, node):
343 343 self.state = state
344 344 self.repo = state.repo
345 345 self.node = node
346 346
347 347 @classmethod
348 348 def fromrule(cls, state, rule):
349 349 """Parses the given rule, returning an instance of the histeditaction.
350 350 """
351 351 rulehash = rule.strip().split(' ', 1)[0]
352 352 return cls(state, node.bin(rulehash))
353 353
354 354 def verify(self):
355 355 """ Verifies semantic correctness of the rule"""
356 356 repo = self.repo
357 357 ha = node.hex(self.node)
358 358 try:
359 359 self.node = repo[ha].node()
360 360 except error.RepoError:
361 361 raise error.Abort(_('unknown changeset %s listed')
362 362 % ha[:12])
363 363
364 364 def torule(self):
365 365 """build a histedit rule line for an action
366 366
367 367 by default lines are in the form:
368 368 <hash> <rev> <summary>
369 369 """
370 370 ctx = self.repo[self.node]
371 371 summary = ''
372 372 if ctx.description():
373 373 summary = ctx.description().splitlines()[0]
374 374 line = '%s %s %d %s' % (self.verb, ctx, ctx.rev(), summary)
375 375 # trim to 75 columns by default so it's not stupidly wide in my editor
376 376 # (the 5 more are left for verb)
377 377 maxlen = self.repo.ui.configint('histedit', 'linelen', default=80)
378 378 maxlen = max(maxlen, 22) # avoid truncating hash
379 379 return util.ellipsis(line, maxlen)
380 380
381 381 def tostate(self):
382 382 """Print an action in format used by histedit state files
383 383 (the first line is a verb, the remainder is the second)
384 384 """
385 385 return "%s\n%s" % (self.verb, node.hex(self.node))
386 386
387 387 def constraints(self):
388 388 """Return a set of constrains that this action should be verified for
389 389 """
390 390 return set([_constraints.noduplicates, _constraints.noother])
391 391
392 392 def nodetoverify(self):
393 393 """Returns a node associated with the action that will be used for
394 394 verification purposes.
395 395
396 396 If the action doesn't correspond to node it should return None
397 397 """
398 398 return self.node
399 399
400 400 def run(self):
401 401 """Runs the action. The default behavior is simply apply the action's
402 402 rulectx onto the current parentctx."""
403 403 self.applychange()
404 404 self.continuedirty()
405 405 return self.continueclean()
406 406
407 407 def applychange(self):
408 408 """Applies the changes from this action's rulectx onto the current
409 409 parentctx, but does not commit them."""
410 410 repo = self.repo
411 411 rulectx = repo[self.node]
412 412 hg.update(repo, self.state.parentctxnode, quietempty=True)
413 413 stats = applychanges(repo.ui, repo, rulectx, {})
414 414 if stats and stats[3] > 0:
415 415 raise error.InterventionRequired(_('Fix up the change and run '
416 416 'hg histedit --continue'))
417 417
418 418 def continuedirty(self):
419 419 """Continues the action when changes have been applied to the working
420 420 copy. The default behavior is to commit the dirty changes."""
421 421 repo = self.repo
422 422 rulectx = repo[self.node]
423 423
424 424 editor = self.commiteditor()
425 425 commit = commitfuncfor(repo, rulectx)
426 426
427 427 commit(text=rulectx.description(), user=rulectx.user(),
428 428 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
429 429
430 430 def commiteditor(self):
431 431 """The editor to be used to edit the commit message."""
432 432 return False
433 433
434 434 def continueclean(self):
435 435 """Continues the action when the working copy is clean. The default
436 436 behavior is to accept the current commit as the new version of the
437 437 rulectx."""
438 438 ctx = self.repo['.']
439 439 if ctx.node() == self.state.parentctxnode:
440 440 self.repo.ui.warn(_('%s: empty changeset\n') %
441 441 node.short(self.node))
442 442 return ctx, [(self.node, tuple())]
443 443 if ctx.node() == self.node:
444 444 # Nothing changed
445 445 return ctx, []
446 446 return ctx, [(self.node, (ctx.node(),))]
447 447
448 448 def commitfuncfor(repo, src):
449 449 """Build a commit function for the replacement of <src>
450 450
451 451 This function ensure we apply the same treatment to all changesets.
452 452
453 453 - Add a 'histedit_source' entry in extra.
454 454
455 455 Note that fold has its own separated logic because its handling is a bit
456 456 different and not easily factored out of the fold method.
457 457 """
458 458 phasemin = src.phase()
459 459 def commitfunc(**kwargs):
460 460 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
461 461 try:
462 462 repo.ui.setconfig('phases', 'new-commit', phasemin,
463 463 'histedit')
464 464 extra = kwargs.get('extra', {}).copy()
465 465 extra['histedit_source'] = src.hex()
466 466 kwargs['extra'] = extra
467 467 return repo.commit(**kwargs)
468 468 finally:
469 469 repo.ui.restoreconfig(phasebackup)
470 470 return commitfunc
471 471
472 472 def applychanges(ui, repo, ctx, opts):
473 473 """Merge changeset from ctx (only) in the current working directory"""
474 474 wcpar = repo.dirstate.parents()[0]
475 475 if ctx.p1().node() == wcpar:
476 476 # edits are "in place" we do not need to make any merge,
477 477 # just applies changes on parent for edition
478 478 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
479 479 stats = None
480 480 else:
481 481 try:
482 482 # ui.forcemerge is an internal variable, do not document
483 483 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
484 484 'histedit')
485 485 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
486 486 finally:
487 487 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
488 488 return stats
489 489
490 490 def collapse(repo, first, last, commitopts, skipprompt=False):
491 491 """collapse the set of revisions from first to last as new one.
492 492
493 493 Expected commit options are:
494 494 - message
495 495 - date
496 496 - username
497 497 Commit message is edited in all cases.
498 498
499 499 This function works in memory."""
500 500 ctxs = list(repo.set('%d::%d', first, last))
501 501 if not ctxs:
502 502 return None
503 503 for c in ctxs:
504 504 if not c.mutable():
505 505 raise error.Abort(
506 506 _("cannot fold into public change %s") % node.short(c.node()))
507 507 base = first.parents()[0]
508 508
509 509 # commit a new version of the old changeset, including the update
510 510 # collect all files which might be affected
511 511 files = set()
512 512 for ctx in ctxs:
513 513 files.update(ctx.files())
514 514
515 515 # Recompute copies (avoid recording a -> b -> a)
516 516 copied = copies.pathcopies(base, last)
517 517
518 518 # prune files which were reverted by the updates
519 519 def samefile(f):
520 520 if f in last.manifest():
521 521 a = last.filectx(f)
522 522 if f in base.manifest():
523 523 b = base.filectx(f)
524 524 return (a.data() == b.data()
525 525 and a.flags() == b.flags())
526 526 else:
527 527 return False
528 528 else:
529 529 return f not in base.manifest()
530 530 files = [f for f in files if not samefile(f)]
531 531 # commit version of these files as defined by head
532 532 headmf = last.manifest()
533 533 def filectxfn(repo, ctx, path):
534 534 if path in headmf:
535 535 fctx = last[path]
536 536 flags = fctx.flags()
537 537 mctx = context.memfilectx(repo,
538 538 fctx.path(), fctx.data(),
539 539 islink='l' in flags,
540 540 isexec='x' in flags,
541 541 copied=copied.get(path))
542 542 return mctx
543 543 return None
544 544
545 545 if commitopts.get('message'):
546 546 message = commitopts['message']
547 547 else:
548 548 message = first.description()
549 549 user = commitopts.get('user')
550 550 date = commitopts.get('date')
551 551 extra = commitopts.get('extra')
552 552
553 553 parents = (first.p1().node(), first.p2().node())
554 554 editor = None
555 555 if not skipprompt:
556 556 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
557 557 new = context.memctx(repo,
558 558 parents=parents,
559 559 text=message,
560 560 files=files,
561 561 filectxfn=filectxfn,
562 562 user=user,
563 563 date=date,
564 564 extra=extra,
565 565 editor=editor)
566 566 return repo.commitctx(new)
567 567
568 568 def _isdirtywc(repo):
569 569 return repo[None].dirty(missing=True)
570 570
571 571 def abortdirty():
572 572 raise error.Abort(_('working copy has pending changes'),
573 573 hint=_('amend, commit, or revert them and run histedit '
574 574 '--continue, or abort with histedit --abort'))
575 575
576 576
577 577 actiontable = {}
578 578 actionlist = []
579 579
580 580 def addhisteditaction(verbs):
581 581 def wrap(cls):
582 582 cls.verb = verbs[0]
583 583 for verb in verbs:
584 584 actiontable[verb] = cls
585 585 actionlist.append(cls)
586 586 return cls
587 587 return wrap
588 588
589 589
590 590 @addhisteditaction(['pick', 'p'])
591 591 class pick(histeditaction):
592 592 def run(self):
593 593 rulectx = self.repo[self.node]
594 594 if rulectx.parents()[0].node() == self.state.parentctxnode:
595 595 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
596 596 return rulectx, []
597 597
598 598 return super(pick, self).run()
599 599
600 600 @addhisteditaction(['edit', 'e'])
601 601 class edit(histeditaction):
602 602 def run(self):
603 603 repo = self.repo
604 604 rulectx = repo[self.node]
605 hg.update(repo, self.state.parentctxnode)
605 hg.update(repo, self.state.parentctxnode, quietempty=True)
606 606 applychanges(repo.ui, repo, rulectx, {})
607 607 raise error.InterventionRequired(
608 608 _('Make changes as needed, you may commit or record as needed '
609 609 'now.\nWhen you are finished, run hg histedit --continue to '
610 610 'resume.'))
611 611
612 612 def commiteditor(self):
613 613 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
614 614
615 615 @addhisteditaction(['fold', 'f'])
616 616 class fold(histeditaction):
617 617 def continuedirty(self):
618 618 repo = self.repo
619 619 rulectx = repo[self.node]
620 620
621 621 commit = commitfuncfor(repo, rulectx)
622 622 commit(text='fold-temp-revision %s' % node.short(self.node),
623 623 user=rulectx.user(), date=rulectx.date(),
624 624 extra=rulectx.extra())
625 625
626 626 def continueclean(self):
627 627 repo = self.repo
628 628 ctx = repo['.']
629 629 rulectx = repo[self.node]
630 630 parentctxnode = self.state.parentctxnode
631 631 if ctx.node() == parentctxnode:
632 632 repo.ui.warn(_('%s: empty changeset\n') %
633 633 node.short(self.node))
634 634 return ctx, [(self.node, (parentctxnode,))]
635 635
636 636 parentctx = repo[parentctxnode]
637 637 newcommits = set(c.node() for c in repo.set('(%d::. - %d)', parentctx,
638 638 parentctx))
639 639 if not newcommits:
640 640 repo.ui.warn(_('%s: cannot fold - working copy is not a '
641 641 'descendant of previous commit %s\n') %
642 642 (node.short(self.node), node.short(parentctxnode)))
643 643 return ctx, [(self.node, (ctx.node(),))]
644 644
645 645 middlecommits = newcommits.copy()
646 646 middlecommits.discard(ctx.node())
647 647
648 648 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
649 649 middlecommits)
650 650
651 651 def skipprompt(self):
652 652 """Returns true if the rule should skip the message editor.
653 653
654 654 For example, 'fold' wants to show an editor, but 'rollup'
655 655 doesn't want to.
656 656 """
657 657 return False
658 658
659 659 def mergedescs(self):
660 660 """Returns true if the rule should merge messages of multiple changes.
661 661
662 662 This exists mainly so that 'rollup' rules can be a subclass of
663 663 'fold'.
664 664 """
665 665 return True
666 666
667 667 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
668 668 parent = ctx.parents()[0].node()
669 669 hg.update(repo, parent)
670 670 ### prepare new commit data
671 671 commitopts = {}
672 672 commitopts['user'] = ctx.user()
673 673 # commit message
674 674 if not self.mergedescs():
675 675 newmessage = ctx.description()
676 676 else:
677 677 newmessage = '\n***\n'.join(
678 678 [ctx.description()] +
679 679 [repo[r].description() for r in internalchanges] +
680 680 [oldctx.description()]) + '\n'
681 681 commitopts['message'] = newmessage
682 682 # date
683 683 commitopts['date'] = max(ctx.date(), oldctx.date())
684 684 extra = ctx.extra().copy()
685 685 # histedit_source
686 686 # note: ctx is likely a temporary commit but that the best we can do
687 687 # here. This is sufficient to solve issue3681 anyway.
688 688 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
689 689 commitopts['extra'] = extra
690 690 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
691 691 try:
692 692 phasemin = max(ctx.phase(), oldctx.phase())
693 693 repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit')
694 694 n = collapse(repo, ctx, repo[newnode], commitopts,
695 695 skipprompt=self.skipprompt())
696 696 finally:
697 697 repo.ui.restoreconfig(phasebackup)
698 698 if n is None:
699 699 return ctx, []
700 700 hg.update(repo, n)
701 701 replacements = [(oldctx.node(), (newnode,)),
702 702 (ctx.node(), (n,)),
703 703 (newnode, (n,)),
704 704 ]
705 705 for ich in internalchanges:
706 706 replacements.append((ich, (n,)))
707 707 return repo[n], replacements
708 708
709 709 class base(histeditaction):
710 710 def constraints(self):
711 711 return set([_constraints.forceother])
712 712
713 713 def run(self):
714 714 if self.repo['.'].node() != self.node:
715 715 mergemod.update(self.repo, self.node, False, True)
716 716 # branchmerge, force)
717 717 return self.continueclean()
718 718
719 719 def continuedirty(self):
720 720 abortdirty()
721 721
722 722 def continueclean(self):
723 723 basectx = self.repo['.']
724 724 return basectx, []
725 725
726 726 @addhisteditaction(['_multifold'])
727 727 class _multifold(fold):
728 728 """fold subclass used for when multiple folds happen in a row
729 729
730 730 We only want to fire the editor for the folded message once when
731 731 (say) four changes are folded down into a single change. This is
732 732 similar to rollup, but we should preserve both messages so that
733 733 when the last fold operation runs we can show the user all the
734 734 commit messages in their editor.
735 735 """
736 736 def skipprompt(self):
737 737 return True
738 738
739 739 @addhisteditaction(["roll", "r"])
740 740 class rollup(fold):
741 741 def mergedescs(self):
742 742 return False
743 743
744 744 def skipprompt(self):
745 745 return True
746 746
747 747 @addhisteditaction(["drop", "d"])
748 748 class drop(histeditaction):
749 749 def run(self):
750 750 parentctx = self.repo[self.state.parentctxnode]
751 751 return parentctx, [(self.node, tuple())]
752 752
753 753 @addhisteditaction(["mess", "m"])
754 754 class message(histeditaction):
755 755 def commiteditor(self):
756 756 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
757 757
758 758 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
759 759 """utility function to find the first outgoing changeset
760 760
761 761 Used by initialization code"""
762 762 if opts is None:
763 763 opts = {}
764 764 dest = ui.expandpath(remote or 'default-push', remote or 'default')
765 765 dest, revs = hg.parseurl(dest, None)[:2]
766 766 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
767 767
768 768 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
769 769 other = hg.peer(repo, opts, dest)
770 770
771 771 if revs:
772 772 revs = [repo.lookup(rev) for rev in revs]
773 773
774 774 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
775 775 if not outgoing.missing:
776 776 raise error.Abort(_('no outgoing ancestors'))
777 777 roots = list(repo.revs("roots(%ln)", outgoing.missing))
778 778 if 1 < len(roots):
779 779 msg = _('there are ambiguous outgoing revisions')
780 780 hint = _('see "hg help histedit" for more detail')
781 781 raise error.Abort(msg, hint=hint)
782 782 return repo.lookup(roots[0])
783 783
784 784
785 785 @command('histedit',
786 786 [('', 'commands', '',
787 787 _('read history edits from the specified file'), _('FILE')),
788 788 ('c', 'continue', False, _('continue an edit already in progress')),
789 789 ('', 'edit-plan', False, _('edit remaining actions list')),
790 790 ('k', 'keep', False,
791 791 _("don't strip old nodes after edit is complete")),
792 792 ('', 'abort', False, _('abort an edit in progress')),
793 793 ('o', 'outgoing', False, _('changesets not found in destination')),
794 794 ('f', 'force', False,
795 795 _('force outgoing even for unrelated repositories')),
796 796 ('r', 'rev', [], _('first revision to be edited'), _('REV'))],
797 797 _("[ANCESTOR] | --outgoing [URL]"))
798 798 def histedit(ui, repo, *freeargs, **opts):
799 799 """interactively edit changeset history
800 800
801 801 This command edits changesets between an ANCESTOR and the parent of
802 802 the working directory.
803 803
804 804 The value from the "histedit.defaultrev" config option is used as a
805 805 revset to select the base revision when ANCESTOR is not specified.
806 806 The first revision returned by the revset is used. By default, this
807 807 selects the editable history that is unique to the ancestry of the
808 808 working directory.
809 809
810 810 With --outgoing, this edits changesets not found in the
811 811 destination repository. If URL of the destination is omitted, the
812 812 'default-push' (or 'default') path will be used.
813 813
814 814 For safety, this command is also aborted if there are ambiguous
815 815 outgoing revisions which may confuse users: for example, if there
816 816 are multiple branches containing outgoing revisions.
817 817
818 818 Use "min(outgoing() and ::.)" or similar revset specification
819 819 instead of --outgoing to specify edit target revision exactly in
820 820 such ambiguous situation. See :hg:`help revsets` for detail about
821 821 selecting revisions.
822 822
823 823 .. container:: verbose
824 824
825 825 Examples:
826 826
827 827 - A number of changes have been made.
828 828 Revision 3 is no longer needed.
829 829
830 830 Start history editing from revision 3::
831 831
832 832 hg histedit -r 3
833 833
834 834 An editor opens, containing the list of revisions,
835 835 with specific actions specified::
836 836
837 837 pick 5339bf82f0ca 3 Zworgle the foobar
838 838 pick 8ef592ce7cc4 4 Bedazzle the zerlog
839 839 pick 0a9639fcda9d 5 Morgify the cromulancy
840 840
841 841 Additional information about the possible actions
842 842 to take appears below the list of revisions.
843 843
844 844 To remove revision 3 from the history,
845 845 its action (at the beginning of the relevant line)
846 846 is changed to 'drop'::
847 847
848 848 drop 5339bf82f0ca 3 Zworgle the foobar
849 849 pick 8ef592ce7cc4 4 Bedazzle the zerlog
850 850 pick 0a9639fcda9d 5 Morgify the cromulancy
851 851
852 852 - A number of changes have been made.
853 853 Revision 2 and 4 need to be swapped.
854 854
855 855 Start history editing from revision 2::
856 856
857 857 hg histedit -r 2
858 858
859 859 An editor opens, containing the list of revisions,
860 860 with specific actions specified::
861 861
862 862 pick 252a1af424ad 2 Blorb a morgwazzle
863 863 pick 5339bf82f0ca 3 Zworgle the foobar
864 864 pick 8ef592ce7cc4 4 Bedazzle the zerlog
865 865
866 866 To swap revision 2 and 4, its lines are swapped
867 867 in the editor::
868 868
869 869 pick 8ef592ce7cc4 4 Bedazzle the zerlog
870 870 pick 5339bf82f0ca 3 Zworgle the foobar
871 871 pick 252a1af424ad 2 Blorb a morgwazzle
872 872
873 873 Returns 0 on success, 1 if user intervention is required (not only
874 874 for intentional "edit" command, but also for resolving unexpected
875 875 conflicts).
876 876 """
877 877 state = histeditstate(repo)
878 878 try:
879 879 state.wlock = repo.wlock()
880 880 state.lock = repo.lock()
881 881 _histedit(ui, repo, state, *freeargs, **opts)
882 882 except error.Abort:
883 883 if repo.vfs.exists('histedit-last-edit.txt'):
884 884 ui.warn(_('warning: histedit rules saved '
885 885 'to: .hg/histedit-last-edit.txt\n'))
886 886 raise
887 887 finally:
888 888 release(state.lock, state.wlock)
889 889
890 890 def _histedit(ui, repo, state, *freeargs, **opts):
891 891 # TODO only abort if we try to histedit mq patches, not just
892 892 # blanket if mq patches are applied somewhere
893 893 mq = getattr(repo, 'mq', None)
894 894 if mq and mq.applied:
895 895 raise error.Abort(_('source has mq patches applied'))
896 896
897 897 # basic argument incompatibility processing
898 898 outg = opts.get('outgoing')
899 899 cont = opts.get('continue')
900 900 editplan = opts.get('edit_plan')
901 901 abort = opts.get('abort')
902 902 force = opts.get('force')
903 903 rules = opts.get('commands', '')
904 904 revs = opts.get('rev', [])
905 905 goal = 'new' # This invocation goal, in new, continue, abort
906 906 if force and not outg:
907 907 raise error.Abort(_('--force only allowed with --outgoing'))
908 908 if cont:
909 909 if any((outg, abort, revs, freeargs, rules, editplan)):
910 910 raise error.Abort(_('no arguments allowed with --continue'))
911 911 goal = 'continue'
912 912 elif abort:
913 913 if any((outg, revs, freeargs, rules, editplan)):
914 914 raise error.Abort(_('no arguments allowed with --abort'))
915 915 goal = 'abort'
916 916 elif editplan:
917 917 if any((outg, revs, freeargs)):
918 918 raise error.Abort(_('only --commands argument allowed with '
919 919 '--edit-plan'))
920 920 goal = 'edit-plan'
921 921 else:
922 922 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
923 923 raise error.Abort(_('history edit already in progress, try '
924 924 '--continue or --abort'))
925 925 if outg:
926 926 if revs:
927 927 raise error.Abort(_('no revisions allowed with --outgoing'))
928 928 if len(freeargs) > 1:
929 929 raise error.Abort(
930 930 _('only one repo argument allowed with --outgoing'))
931 931 else:
932 932 revs.extend(freeargs)
933 933 if len(revs) == 0:
934 934 defaultrev = destutil.desthistedit(ui, repo)
935 935 if defaultrev is not None:
936 936 revs.append(defaultrev)
937 937
938 938 if len(revs) != 1:
939 939 raise error.Abort(
940 940 _('histedit requires exactly one ancestor revision'))
941 941
942 942
943 943 replacements = []
944 944 state.keep = opts.get('keep', False)
945 945 supportsmarkers = obsolete.isenabled(repo, obsolete.createmarkersopt)
946 946
947 947 # rebuild state
948 948 if goal == 'continue':
949 949 state.read()
950 950 state = bootstrapcontinue(ui, state, opts)
951 951 elif goal == 'edit-plan':
952 952 state.read()
953 953 if not rules:
954 954 comment = editcomment % (node.short(state.parentctxnode),
955 955 node.short(state.topmost))
956 956 rules = ruleeditor(repo, ui, state.actions, comment)
957 957 else:
958 958 if rules == '-':
959 959 f = sys.stdin
960 960 else:
961 961 f = open(rules)
962 962 rules = f.read()
963 963 f.close()
964 964 actions = parserules(rules, state)
965 965 ctxs = [repo[act.nodetoverify()] \
966 966 for act in state.actions if act.nodetoverify()]
967 967 verifyactions(actions, state, ctxs)
968 968 state.actions = actions
969 969 state.write()
970 970 return
971 971 elif goal == 'abort':
972 972 try:
973 973 state.read()
974 974 tmpnodes, leafs = newnodestoabort(state)
975 975 ui.debug('restore wc to old parent %s\n'
976 976 % node.short(state.topmost))
977 977
978 978 # Recover our old commits if necessary
979 979 if not state.topmost in repo and state.backupfile:
980 980 backupfile = repo.join(state.backupfile)
981 981 f = hg.openpath(ui, backupfile)
982 982 gen = exchange.readbundle(ui, f, backupfile)
983 983 tr = repo.transaction('histedit.abort')
984 984 try:
985 985 if not isinstance(gen, bundle2.unbundle20):
986 986 gen.apply(repo, 'histedit', 'bundle:' + backupfile)
987 987 if isinstance(gen, bundle2.unbundle20):
988 988 bundle2.applybundle(repo, gen, tr,
989 989 source='histedit',
990 990 url='bundle:' + backupfile)
991 991 tr.close()
992 992 finally:
993 993 tr.release()
994 994
995 995 os.remove(backupfile)
996 996
997 997 # check whether we should update away
998 998 if repo.unfiltered().revs('parents() and (%n or %ln::)',
999 999 state.parentctxnode, leafs | tmpnodes):
1000 1000 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1001 1001 cleanupnode(ui, repo, 'created', tmpnodes)
1002 1002 cleanupnode(ui, repo, 'temp', leafs)
1003 1003 except Exception:
1004 1004 if state.inprogress():
1005 1005 ui.warn(_('warning: encountered an exception during histedit '
1006 1006 '--abort; the repository may not have been completely '
1007 1007 'cleaned up\n'))
1008 1008 raise
1009 1009 finally:
1010 1010 state.clear()
1011 1011 return
1012 1012 else:
1013 1013 cmdutil.checkunfinished(repo)
1014 1014 cmdutil.bailifchanged(repo)
1015 1015
1016 1016 if repo.vfs.exists('histedit-last-edit.txt'):
1017 1017 repo.vfs.unlink('histedit-last-edit.txt')
1018 1018 topmost, empty = repo.dirstate.parents()
1019 1019 if outg:
1020 1020 if freeargs:
1021 1021 remote = freeargs[0]
1022 1022 else:
1023 1023 remote = None
1024 1024 root = findoutgoing(ui, repo, remote, force, opts)
1025 1025 else:
1026 1026 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1027 1027 if len(rr) != 1:
1028 1028 raise error.Abort(_('The specified revisions must have '
1029 1029 'exactly one common root'))
1030 1030 root = rr[0].node()
1031 1031
1032 1032 revs = between(repo, root, topmost, state.keep)
1033 1033 if not revs:
1034 1034 raise error.Abort(_('%s is not an ancestor of working directory') %
1035 1035 node.short(root))
1036 1036
1037 1037 ctxs = [repo[r] for r in revs]
1038 1038 if not rules:
1039 1039 comment = editcomment % (node.short(root), node.short(topmost))
1040 1040 actions = [pick(state, r) for r in revs]
1041 1041 rules = ruleeditor(repo, ui, actions, comment)
1042 1042 else:
1043 1043 if rules == '-':
1044 1044 f = sys.stdin
1045 1045 else:
1046 1046 f = open(rules)
1047 1047 rules = f.read()
1048 1048 f.close()
1049 1049 actions = parserules(rules, state)
1050 1050 verifyactions(actions, state, ctxs)
1051 1051
1052 1052 parentctxnode = repo[root].parents()[0].node()
1053 1053
1054 1054 state.parentctxnode = parentctxnode
1055 1055 state.actions = actions
1056 1056 state.topmost = topmost
1057 1057 state.replacements = replacements
1058 1058
1059 1059 # Create a backup so we can always abort completely.
1060 1060 backupfile = None
1061 1061 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1062 1062 backupfile = repair._bundle(repo, [parentctxnode], [topmost], root,
1063 1063 'histedit')
1064 1064 state.backupfile = backupfile
1065 1065
1066 1066 # preprocess rules so that we can hide inner folds from the user
1067 1067 # and only show one editor
1068 1068 actions = state.actions[:]
1069 1069 for idx, (action, nextact) in enumerate(
1070 1070 zip(actions, actions[1:] + [None])):
1071 1071 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1072 1072 state.actions[idx].__class__ = _multifold
1073 1073
1074 1074 while state.actions:
1075 1075 state.write()
1076 1076 actobj = state.actions.pop(0)
1077 1077 ui.debug('histedit: processing %s %s\n' % (actobj.verb,\
1078 1078 actobj.torule()))
1079 1079 parentctx, replacement_ = actobj.run()
1080 1080 state.parentctxnode = parentctx.node()
1081 1081 state.replacements.extend(replacement_)
1082 1082 state.write()
1083 1083
1084 1084 hg.update(repo, state.parentctxnode, quietempty=True)
1085 1085
1086 1086 mapping, tmpnodes, created, ntm = processreplacement(state)
1087 1087 if mapping:
1088 1088 for prec, succs in mapping.iteritems():
1089 1089 if not succs:
1090 1090 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1091 1091 else:
1092 1092 ui.debug('histedit: %s is replaced by %s\n' % (
1093 1093 node.short(prec), node.short(succs[0])))
1094 1094 if len(succs) > 1:
1095 1095 m = 'histedit: %s'
1096 1096 for n in succs[1:]:
1097 1097 ui.debug(m % node.short(n))
1098 1098
1099 1099 if supportsmarkers:
1100 1100 # Only create markers if the temp nodes weren't already removed.
1101 1101 obsolete.createmarkers(repo, ((repo[t],()) for t in sorted(tmpnodes)
1102 1102 if t in repo))
1103 1103 else:
1104 1104 cleanupnode(ui, repo, 'temp', tmpnodes)
1105 1105
1106 1106 if not state.keep:
1107 1107 if mapping:
1108 1108 movebookmarks(ui, repo, mapping, state.topmost, ntm)
1109 1109 # TODO update mq state
1110 1110 if supportsmarkers:
1111 1111 markers = []
1112 1112 # sort by revision number because it sound "right"
1113 1113 for prec in sorted(mapping, key=repo.changelog.rev):
1114 1114 succs = mapping[prec]
1115 1115 markers.append((repo[prec],
1116 1116 tuple(repo[s] for s in succs)))
1117 1117 if markers:
1118 1118 obsolete.createmarkers(repo, markers)
1119 1119 else:
1120 1120 cleanupnode(ui, repo, 'replaced', mapping)
1121 1121
1122 1122 state.clear()
1123 1123 if os.path.exists(repo.sjoin('undo')):
1124 1124 os.unlink(repo.sjoin('undo'))
1125 1125
1126 1126 def bootstrapcontinue(ui, state, opts):
1127 1127 repo = state.repo
1128 1128 if state.actions:
1129 1129 actobj = state.actions.pop(0)
1130 1130
1131 1131 if _isdirtywc(repo):
1132 1132 actobj.continuedirty()
1133 1133 if _isdirtywc(repo):
1134 1134 abortdirty()
1135 1135
1136 1136 parentctx, replacements = actobj.continueclean()
1137 1137
1138 1138 state.parentctxnode = parentctx.node()
1139 1139 state.replacements.extend(replacements)
1140 1140
1141 1141 return state
1142 1142
1143 1143 def between(repo, old, new, keep):
1144 1144 """select and validate the set of revision to edit
1145 1145
1146 1146 When keep is false, the specified set can't have children."""
1147 1147 ctxs = list(repo.set('%n::%n', old, new))
1148 1148 if ctxs and not keep:
1149 1149 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1150 1150 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
1151 1151 raise error.Abort(_('cannot edit history that would orphan nodes'))
1152 1152 if repo.revs('(%ld) and merge()', ctxs):
1153 1153 raise error.Abort(_('cannot edit history that contains merges'))
1154 1154 root = ctxs[0] # list is already sorted by repo.set
1155 1155 if not root.mutable():
1156 1156 raise error.Abort(_('cannot edit public changeset: %s') % root,
1157 1157 hint=_('see "hg help phases" for details'))
1158 1158 return [c.node() for c in ctxs]
1159 1159
1160 1160 def ruleeditor(repo, ui, actions, editcomment=""):
1161 1161 """open an editor to edit rules
1162 1162
1163 1163 rules are in the format [ [act, ctx], ...] like in state.rules
1164 1164 """
1165 1165 rules = '\n'.join([act.torule() for act in actions])
1166 1166 rules += '\n\n'
1167 1167 rules += editcomment
1168 1168 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'})
1169 1169
1170 1170 # Save edit rules in .hg/histedit-last-edit.txt in case
1171 1171 # the user needs to ask for help after something
1172 1172 # surprising happens.
1173 1173 f = open(repo.join('histedit-last-edit.txt'), 'w')
1174 1174 f.write(rules)
1175 1175 f.close()
1176 1176
1177 1177 return rules
1178 1178
1179 1179 def parserules(rules, state):
1180 1180 """Read the histedit rules string and return list of action objects """
1181 1181 rules = [l for l in (r.strip() for r in rules.splitlines())
1182 1182 if l and not l.startswith('#')]
1183 1183 actions = []
1184 1184 for r in rules:
1185 1185 if ' ' not in r:
1186 1186 raise error.Abort(_('malformed line "%s"') % r)
1187 1187 verb, rest = r.split(' ', 1)
1188 1188
1189 1189 if verb not in actiontable:
1190 1190 raise error.Abort(_('unknown action "%s"') % verb)
1191 1191
1192 1192 action = actiontable[verb].fromrule(state, rest)
1193 1193 actions.append(action)
1194 1194 return actions
1195 1195
1196 1196 def verifyactions(actions, state, ctxs):
1197 1197 """Verify that there exists exactly one action per given changeset and
1198 1198 other constraints.
1199 1199
1200 1200 Will abort if there are to many or too few rules, a malformed rule,
1201 1201 or a rule on a changeset outside of the user-given range.
1202 1202 """
1203 1203 expected = set(c.hex() for c in ctxs)
1204 1204 seen = set()
1205 1205 for action in actions:
1206 1206 action.verify()
1207 1207 constraints = action.constraints()
1208 1208 for constraint in constraints:
1209 1209 if constraint not in _constraints.known():
1210 1210 raise error.Abort(_('unknown constraint "%s"') % constraint)
1211 1211
1212 1212 nodetoverify = action.nodetoverify()
1213 1213 if nodetoverify is not None:
1214 1214 ha = node.hex(nodetoverify)
1215 1215 if _constraints.noother in constraints and ha not in expected:
1216 1216 raise error.Abort(
1217 1217 _('may not use "%s" with changesets '
1218 1218 'other than the ones listed') % action.verb)
1219 1219 if _constraints.forceother in constraints and ha in expected:
1220 1220 raise error.Abort(
1221 1221 _('may not use "%s" with changesets '
1222 1222 'within the edited list') % action.verb)
1223 1223 if _constraints.noduplicates in constraints and ha in seen:
1224 1224 raise error.Abort(_('duplicated command for changeset %s') %
1225 1225 ha[:12])
1226 1226 seen.add(ha)
1227 1227 missing = sorted(expected - seen) # sort to stabilize output
1228 1228 if missing:
1229 1229 raise error.Abort(_('missing rules for changeset %s') %
1230 1230 missing[0][:12],
1231 1231 hint=_('use "drop %s" to discard the change') % missing[0][:12])
1232 1232
1233 1233 def newnodestoabort(state):
1234 1234 """process the list of replacements to return
1235 1235
1236 1236 1) the list of final node
1237 1237 2) the list of temporary node
1238 1238
1239 1239 This meant to be used on abort as less data are required in this case.
1240 1240 """
1241 1241 replacements = state.replacements
1242 1242 allsuccs = set()
1243 1243 replaced = set()
1244 1244 for rep in replacements:
1245 1245 allsuccs.update(rep[1])
1246 1246 replaced.add(rep[0])
1247 1247 newnodes = allsuccs - replaced
1248 1248 tmpnodes = allsuccs & replaced
1249 1249 return newnodes, tmpnodes
1250 1250
1251 1251
1252 1252 def processreplacement(state):
1253 1253 """process the list of replacements to return
1254 1254
1255 1255 1) the final mapping between original and created nodes
1256 1256 2) the list of temporary node created by histedit
1257 1257 3) the list of new commit created by histedit"""
1258 1258 replacements = state.replacements
1259 1259 allsuccs = set()
1260 1260 replaced = set()
1261 1261 fullmapping = {}
1262 1262 # initialize basic set
1263 1263 # fullmapping records all operations recorded in replacement
1264 1264 for rep in replacements:
1265 1265 allsuccs.update(rep[1])
1266 1266 replaced.add(rep[0])
1267 1267 fullmapping.setdefault(rep[0], set()).update(rep[1])
1268 1268 new = allsuccs - replaced
1269 1269 tmpnodes = allsuccs & replaced
1270 1270 # Reduce content fullmapping into direct relation between original nodes
1271 1271 # and final node created during history edition
1272 1272 # Dropped changeset are replaced by an empty list
1273 1273 toproceed = set(fullmapping)
1274 1274 final = {}
1275 1275 while toproceed:
1276 1276 for x in list(toproceed):
1277 1277 succs = fullmapping[x]
1278 1278 for s in list(succs):
1279 1279 if s in toproceed:
1280 1280 # non final node with unknown closure
1281 1281 # We can't process this now
1282 1282 break
1283 1283 elif s in final:
1284 1284 # non final node, replace with closure
1285 1285 succs.remove(s)
1286 1286 succs.update(final[s])
1287 1287 else:
1288 1288 final[x] = succs
1289 1289 toproceed.remove(x)
1290 1290 # remove tmpnodes from final mapping
1291 1291 for n in tmpnodes:
1292 1292 del final[n]
1293 1293 # we expect all changes involved in final to exist in the repo
1294 1294 # turn `final` into list (topologically sorted)
1295 1295 nm = state.repo.changelog.nodemap
1296 1296 for prec, succs in final.items():
1297 1297 final[prec] = sorted(succs, key=nm.get)
1298 1298
1299 1299 # computed topmost element (necessary for bookmark)
1300 1300 if new:
1301 1301 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1302 1302 elif not final:
1303 1303 # Nothing rewritten at all. we won't need `newtopmost`
1304 1304 # It is the same as `oldtopmost` and `processreplacement` know it
1305 1305 newtopmost = None
1306 1306 else:
1307 1307 # every body died. The newtopmost is the parent of the root.
1308 1308 r = state.repo.changelog.rev
1309 1309 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1310 1310
1311 1311 return final, tmpnodes, new, newtopmost
1312 1312
1313 1313 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
1314 1314 """Move bookmark from old to newly created node"""
1315 1315 if not mapping:
1316 1316 # if nothing got rewritten there is not purpose for this function
1317 1317 return
1318 1318 moves = []
1319 1319 for bk, old in sorted(repo._bookmarks.iteritems()):
1320 1320 if old == oldtopmost:
1321 1321 # special case ensure bookmark stay on tip.
1322 1322 #
1323 1323 # This is arguably a feature and we may only want that for the
1324 1324 # active bookmark. But the behavior is kept compatible with the old
1325 1325 # version for now.
1326 1326 moves.append((bk, newtopmost))
1327 1327 continue
1328 1328 base = old
1329 1329 new = mapping.get(base, None)
1330 1330 if new is None:
1331 1331 continue
1332 1332 while not new:
1333 1333 # base is killed, trying with parent
1334 1334 base = repo[base].p1().node()
1335 1335 new = mapping.get(base, (base,))
1336 1336 # nothing to move
1337 1337 moves.append((bk, new[-1]))
1338 1338 if moves:
1339 1339 lock = tr = None
1340 1340 try:
1341 1341 lock = repo.lock()
1342 1342 tr = repo.transaction('histedit')
1343 1343 marks = repo._bookmarks
1344 1344 for mark, new in moves:
1345 1345 old = marks[mark]
1346 1346 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
1347 1347 % (mark, node.short(old), node.short(new)))
1348 1348 marks[mark] = new
1349 1349 marks.recordchange(tr)
1350 1350 tr.close()
1351 1351 finally:
1352 1352 release(tr, lock)
1353 1353
1354 1354 def cleanupnode(ui, repo, name, nodes):
1355 1355 """strip a group of nodes from the repository
1356 1356
1357 1357 The set of node to strip may contains unknown nodes."""
1358 1358 ui.debug('should strip %s nodes %s\n' %
1359 1359 (name, ', '.join([node.short(n) for n in nodes])))
1360 1360 lock = None
1361 1361 try:
1362 1362 lock = repo.lock()
1363 1363 # do not let filtering get in the way of the cleanse
1364 1364 # we should probably get rid of obsolescence marker created during the
1365 1365 # histedit, but we currently do not have such information.
1366 1366 repo = repo.unfiltered()
1367 1367 # Find all nodes that need to be stripped
1368 1368 # (we use %lr instead of %ln to silently ignore unknown items)
1369 1369 nm = repo.changelog.nodemap
1370 1370 nodes = sorted(n for n in nodes if n in nm)
1371 1371 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1372 1372 for c in roots:
1373 1373 # We should process node in reverse order to strip tip most first.
1374 1374 # but this trigger a bug in changegroup hook.
1375 1375 # This would reduce bundle overhead
1376 1376 repair.strip(ui, repo, c)
1377 1377 finally:
1378 1378 release(lock)
1379 1379
1380 1380 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1381 1381 if isinstance(nodelist, str):
1382 1382 nodelist = [nodelist]
1383 1383 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1384 1384 state = histeditstate(repo)
1385 1385 state.read()
1386 1386 histedit_nodes = set([action.nodetoverify() for action
1387 1387 in state.actions if action.nodetoverify()])
1388 1388 strip_nodes = set([repo[n].node() for n in nodelist])
1389 1389 common_nodes = histedit_nodes & strip_nodes
1390 1390 if common_nodes:
1391 1391 raise error.Abort(_("histedit in progress, can't strip %s")
1392 1392 % ', '.join(node.short(x) for x in common_nodes))
1393 1393 return orig(ui, repo, nodelist, *args, **kwargs)
1394 1394
1395 1395 extensions.wrapfunction(repair, 'strip', stripwrapper)
1396 1396
1397 1397 def summaryhook(ui, repo):
1398 1398 if not os.path.exists(repo.join('histedit-state')):
1399 1399 return
1400 1400 state = histeditstate(repo)
1401 1401 state.read()
1402 1402 if state.actions:
1403 1403 # i18n: column positioning for "hg summary"
1404 1404 ui.write(_('hist: %s (histedit --continue)\n') %
1405 1405 (ui.label(_('%d remaining'), 'histedit.remaining') %
1406 1406 len(state.actions)))
1407 1407
1408 1408 def extsetup(ui):
1409 1409 cmdutil.summaryhooks.add('histedit', summaryhook)
1410 1410 cmdutil.unfinishedstates.append(
1411 1411 ['histedit-state', False, True, _('histedit in progress'),
1412 1412 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
1413 1413 if ui.configbool("experimental", "histeditng"):
1414 1414 globals()['base'] = addhisteditaction(['base', 'b'])(base)
@@ -1,220 +1,218
1 1 test for old histedit issue #6:
2 2 editing a changeset without any actual change would corrupt the repository
3 3
4 4 $ . "$TESTDIR/histedit-helpers.sh"
5 5
6 6 $ cat >> $HGRCPATH <<EOF
7 7 > [extensions]
8 8 > histedit=
9 9 > EOF
10 10
11 11 $ initrepo ()
12 12 > {
13 13 > dir="$1"
14 14 > comment="$2"
15 15 > if [ -n "${comment}" ]; then
16 16 > echo % ${comment}
17 17 > echo % ${comment} | sed 's:.:-:g'
18 18 > fi
19 19 > hg init ${dir}
20 20 > cd ${dir}
21 21 > for x in a b c d e f ; do
22 22 > echo $x > $x
23 23 > hg add $x
24 24 > hg ci -m $x
25 25 > done
26 26 > cd ..
27 27 > }
28 28
29 29 $ geneditor ()
30 30 > {
31 31 > # generate an editor script for selecting changesets to be edited
32 32 > choice=$1 # changesets that should be edited (using sed line ranges)
33 33 > cat <<EOF | sed 's:^....::'
34 34 > # editing the rules, replacing 'pick' with 'edit' for the chosen lines
35 35 > sed '${choice}s:^pick:edit:' "\$1" > "\${1}.tmp"
36 36 > mv "\${1}.tmp" "\$1"
37 37 > # displaying the resulting rules, minus comments and empty lines
38 38 > sed '/^#/d;/^$/d;s:^:| :' "\$1" >&2
39 39 > EOF
40 40 > }
41 41
42 42 $ startediting ()
43 43 > {
44 44 > # begin an editing session
45 45 > choice="$1" # changesets that should be edited
46 46 > number="$2" # number of changesets considered (from tip)
47 47 > comment="$3"
48 48 > geneditor "${choice}" > edit.sh
49 49 > echo % start editing the history ${comment}
50 50 > HGEDITOR="sh ./edit.sh" hg histedit -- -${number} 2>&1 | fixbundle
51 51 > }
52 52
53 53 $ continueediting ()
54 54 > {
55 55 > # continue an edit already in progress
56 56 > editor="$1" # message editor when finalizing editing
57 57 > comment="$2"
58 58 > echo % finalize changeset editing ${comment}
59 59 > HGEDITOR=${editor} hg histedit --continue 2>&1 | fixbundle
60 60 > }
61 61
62 62 $ graphlog ()
63 63 > {
64 64 > comment="${1:-log}"
65 65 > echo % "${comment}"
66 66 > hg log -G --template '{rev} {node} \"{desc|firstline}\"\n'
67 67 > }
68 68
69 69
70 70 $ initrepo r1 "test editing with no change"
71 71 % test editing with no change
72 72 -----------------------------
73 73 $ cd r1
74 74 $ graphlog "log before editing"
75 75 % log before editing
76 76 @ 5 652413bf663ef2a641cab26574e46d5f5a64a55a "f"
77 77 |
78 78 o 4 e860deea161a2f77de56603b340ebbb4536308ae "e"
79 79 |
80 80 o 3 055a42cdd88768532f9cf79daa407fc8d138de9b "d"
81 81 |
82 82 o 2 177f92b773850b59254aa5e923436f921b55483b "c"
83 83 |
84 84 o 1 d2ae7f538514cd87c17547b0de4cea71fe1af9fb "b"
85 85 |
86 86 o 0 cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b "a"
87 87
88 88 $ startediting 2 3 "(not changing anything)" # edit the 2nd of 3 changesets
89 89 % start editing the history (not changing anything)
90 90 | pick 055a42cdd887 3 d
91 91 | edit e860deea161a 4 e
92 92 | pick 652413bf663e 5 f
93 93 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
94 94 Make changes as needed, you may commit or record as needed now.
95 95 When you are finished, run hg histedit --continue to resume.
96 96 $ continueediting true "(leaving commit message unaltered)"
97 97 % finalize changeset editing (leaving commit message unaltered)
98 98
99 99
100 100 check state of working copy
101 101 $ hg id
102 102 794fe033d0a0 tip
103 103
104 104 $ graphlog "log after history editing"
105 105 % log after history editing
106 106 @ 5 794fe033d0a030f8df77c5de945fca35c9181c30 "f"
107 107 |
108 108 o 4 04d2fab980779f332dec458cc944f28de8b43435 "e"
109 109 |
110 110 o 3 055a42cdd88768532f9cf79daa407fc8d138de9b "d"
111 111 |
112 112 o 2 177f92b773850b59254aa5e923436f921b55483b "c"
113 113 |
114 114 o 1 d2ae7f538514cd87c17547b0de4cea71fe1af9fb "b"
115 115 |
116 116 o 0 cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b "a"
117 117
118 118
119 119 $ cd ..
120 120
121 121 $ initrepo r2 "test editing with no change, then abort"
122 122 % test editing with no change, then abort
123 123 -----------------------------------------
124 124 $ cd r2
125 125 $ graphlog "log before editing"
126 126 % log before editing
127 127 @ 5 652413bf663ef2a641cab26574e46d5f5a64a55a "f"
128 128 |
129 129 o 4 e860deea161a2f77de56603b340ebbb4536308ae "e"
130 130 |
131 131 o 3 055a42cdd88768532f9cf79daa407fc8d138de9b "d"
132 132 |
133 133 o 2 177f92b773850b59254aa5e923436f921b55483b "c"
134 134 |
135 135 o 1 d2ae7f538514cd87c17547b0de4cea71fe1af9fb "b"
136 136 |
137 137 o 0 cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b "a"
138 138
139 139 $ startediting 1,2 3 "(not changing anything)" # edit the 1st two of 3 changesets
140 140 % start editing the history (not changing anything)
141 141 | edit 055a42cdd887 3 d
142 142 | edit e860deea161a 4 e
143 143 | pick 652413bf663e 5 f
144 144 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
145 145 Make changes as needed, you may commit or record as needed now.
146 146 When you are finished, run hg histedit --continue to resume.
147 147 $ continueediting true "(leaving commit message unaltered)"
148 148 % finalize changeset editing (leaving commit message unaltered)
149 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
150 149 Make changes as needed, you may commit or record as needed now.
151 150 When you are finished, run hg histedit --continue to resume.
152 151 $ graphlog "log after first edit"
153 152 % log after first edit
154 153 @ 6 e5ae3ca2f1ffdbd89ec41ebc273a231f7c3022f2 "d"
155 154 |
156 155 | o 5 652413bf663ef2a641cab26574e46d5f5a64a55a "f"
157 156 | |
158 157 | o 4 e860deea161a2f77de56603b340ebbb4536308ae "e"
159 158 | |
160 159 | o 3 055a42cdd88768532f9cf79daa407fc8d138de9b "d"
161 160 |/
162 161 o 2 177f92b773850b59254aa5e923436f921b55483b "c"
163 162 |
164 163 o 1 d2ae7f538514cd87c17547b0de4cea71fe1af9fb "b"
165 164 |
166 165 o 0 cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b "a"
167 166
168 167
169 168 abort editing session, after first forcibly updating away
170 169 $ hg up 0
171 170 abort: histedit in progress
172 171 (use 'hg histedit --continue' or 'hg histedit --abort')
173 172 [255]
174 173 $ mv .hg/histedit-state .hg/histedit-state-ignore
175 174 $ hg up 0
176 175 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
177 176 $ mv .hg/histedit-state-ignore .hg/histedit-state
178 177 $ hg sum
179 178 parent: 0:cb9a9f314b8b
180 179 a
181 180 branch: default
182 181 commit: 1 added, 1 unknown (new branch head)
183 182 update: 6 new changesets (update)
184 183 phases: 7 draft
185 184 hist: 2 remaining (histedit --continue)
186 185
187 186 $ hg histedit --abort 2>&1 | fixbundle
188 187
189 188 modified files should survive the abort when we've moved away already
190 189 $ hg st
191 190 A e
192 191 ? edit.sh
193 192
194 193 $ graphlog "log after abort"
195 194 % log after abort
196 195 o 5 652413bf663ef2a641cab26574e46d5f5a64a55a "f"
197 196 |
198 197 o 4 e860deea161a2f77de56603b340ebbb4536308ae "e"
199 198 |
200 199 o 3 055a42cdd88768532f9cf79daa407fc8d138de9b "d"
201 200 |
202 201 o 2 177f92b773850b59254aa5e923436f921b55483b "c"
203 202 |
204 203 o 1 d2ae7f538514cd87c17547b0de4cea71fe1af9fb "b"
205 204 |
206 205 @ 0 cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b "a"
207 206
208 207 aborting and not changing files can skip mentioning updating (no) files
209 208 $ hg up
210 209 5 files updated, 0 files merged, 0 files removed, 0 files unresolved
211 210 $ hg commit --close-branch -m 'closebranch'
212 211 $ startediting 1 1 "(not changing anything)" # edit the 3rd of 3 changesets
213 212 % start editing the history (not changing anything)
214 213 | edit 292aec348d9e 6 closebranch
215 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
216 214 Make changes as needed, you may commit or record as needed now.
217 215 When you are finished, run hg histedit --continue to resume.
218 216 $ hg histedit --abort
219 217
220 218 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now