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