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