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