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