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