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