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