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