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