##// END OF EJS Templates
histedit: save manually edited commit message into ".hg/last-message.txt"...
FUJIWARA Katsunori -
r20770:5d22cadd stable
parent child Browse files
Show More
@@ -1,914 +1,915 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 # Commands:
34 34 # p, pick = use commit
35 35 # e, edit = use commit, but stop for amending
36 36 # f, fold = use commit, but fold into previous commit (combines N and N-1)
37 37 # d, drop = remove commit from history
38 38 # m, mess = edit message without changing commit content
39 39 #
40 40
41 41 In this file, lines beginning with ``#`` are ignored. You must specify a rule
42 42 for each revision in your history. For example, if you had meant to add gamma
43 43 before beta, and then wanted to add delta in the same revision as beta, you
44 44 would reorganize the file to look like this::
45 45
46 46 pick 030b686bedc4 Add gamma
47 47 pick c561b4e977df Add beta
48 48 fold 7c2fd3b9020c Add delta
49 49
50 50 # Edit history between c561b4e977df and 7c2fd3b9020c
51 51 #
52 52 # Commands:
53 53 # p, pick = use commit
54 54 # e, edit = use commit, but stop for amending
55 55 # f, fold = use commit, but fold into previous commit (combines N and N-1)
56 56 # d, drop = remove commit from history
57 57 # m, mess = edit message without changing commit content
58 58 #
59 59
60 60 At which point you close the editor and ``histedit`` starts working. When you
61 61 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
62 62 those revisions together, offering you a chance to clean up the commit message::
63 63
64 64 Add beta
65 65 ***
66 66 Add delta
67 67
68 68 Edit the commit message to your liking, then close the editor. For
69 69 this example, let's assume that the commit message was changed to
70 70 ``Add beta and delta.`` After histedit has run and had a chance to
71 71 remove any old or temporary revisions it needed, the history looks
72 72 like this::
73 73
74 74 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
75 75 | Add beta and delta.
76 76 |
77 77 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
78 78 | Add gamma
79 79 |
80 80 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
81 81 Add alpha
82 82
83 83 Note that ``histedit`` does *not* remove any revisions (even its own temporary
84 84 ones) until after it has completed all the editing operations, so it will
85 85 probably perform several strip operations when it's done. For the above example,
86 86 it had to run strip twice. Strip can be slow depending on a variety of factors,
87 87 so you might need to be a little patient. You can choose to keep the original
88 88 revisions by passing the ``--keep`` flag.
89 89
90 90 The ``edit`` operation will drop you back to a command prompt,
91 91 allowing you to edit files freely, or even use ``hg record`` to commit
92 92 some changes as a separate commit. When you're done, any remaining
93 93 uncommitted changes will be committed as well. When done, run ``hg
94 94 histedit --continue`` to finish this step. You'll be prompted for a
95 95 new commit message, but the default commit message will be the
96 96 original message for the ``edit`` ed revision.
97 97
98 98 The ``message`` operation will give you a chance to revise a commit
99 99 message without changing the contents. It's a shortcut for doing
100 100 ``edit`` immediately followed by `hg histedit --continue``.
101 101
102 102 If ``histedit`` encounters a conflict when moving a revision (while
103 103 handling ``pick`` or ``fold``), it'll stop in a similar manner to
104 104 ``edit`` with the difference that it won't prompt you for a commit
105 105 message when done. If you decide at this point that you don't like how
106 106 much work it will be to rearrange history, or that you made a mistake,
107 107 you can use ``hg histedit --abort`` to abandon the new changes you
108 108 have made and return to the state before you attempted to edit your
109 109 history.
110 110
111 111 If we clone the histedit-ed example repository above and add four more
112 112 changes, such that we have the following history::
113 113
114 114 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
115 115 | Add theta
116 116 |
117 117 o 5 140988835471 2009-04-27 18:04 -0500 stefan
118 118 | Add eta
119 119 |
120 120 o 4 122930637314 2009-04-27 18:04 -0500 stefan
121 121 | Add zeta
122 122 |
123 123 o 3 836302820282 2009-04-27 18:04 -0500 stefan
124 124 | Add epsilon
125 125 |
126 126 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
127 127 | Add beta and delta.
128 128 |
129 129 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
130 130 | Add gamma
131 131 |
132 132 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
133 133 Add alpha
134 134
135 135 If you run ``hg histedit --outgoing`` on the clone then it is the same
136 136 as running ``hg histedit 836302820282``. If you need plan to push to a
137 137 repository that Mercurial does not detect to be related to the source
138 138 repo, you can add a ``--force`` option.
139 139 """
140 140
141 141 try:
142 142 import cPickle as pickle
143 143 pickle.dump # import now
144 144 except ImportError:
145 145 import pickle
146 146 import os
147 147 import sys
148 148
149 149 from mercurial import cmdutil
150 150 from mercurial import discovery
151 151 from mercurial import error
152 152 from mercurial import copies
153 153 from mercurial import context
154 154 from mercurial import hg
155 155 from mercurial import lock as lockmod
156 156 from mercurial import node
157 157 from mercurial import repair
158 158 from mercurial import scmutil
159 159 from mercurial import util
160 160 from mercurial import obsolete
161 161 from mercurial import merge as mergemod
162 162 from mercurial.lock import release
163 163 from mercurial.i18n import _
164 164
165 165 cmdtable = {}
166 166 command = cmdutil.command(cmdtable)
167 167
168 168 testedwith = 'internal'
169 169
170 170 # i18n: command names and abbreviations must remain untranslated
171 171 editcomment = _("""# Edit history between %s and %s
172 172 #
173 173 # Commands:
174 174 # p, pick = use commit
175 175 # e, edit = use commit, but stop for amending
176 176 # f, fold = use commit, but fold into previous commit (combines N and N-1)
177 177 # d, drop = remove commit from history
178 178 # m, mess = edit message without changing commit content
179 179 #
180 180 """)
181 181
182 182 def commitfuncfor(repo, src):
183 183 """Build a commit function for the replacement of <src>
184 184
185 185 This function ensure we apply the same treatment to all changesets.
186 186
187 187 - Add a 'histedit_source' entry in extra.
188 188
189 189 Note that fold have its own separated logic because its handling is a bit
190 190 different and not easily factored out of the fold method.
191 191 """
192 192 phasemin = src.phase()
193 193 def commitfunc(**kwargs):
194 194 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
195 195 try:
196 196 repo.ui.setconfig('phases', 'new-commit', phasemin)
197 197 extra = kwargs.get('extra', {}).copy()
198 198 extra['histedit_source'] = src.hex()
199 199 kwargs['extra'] = extra
200 200 return repo.commit(**kwargs)
201 201 finally:
202 202 repo.ui.restoreconfig(phasebackup)
203 203 return commitfunc
204 204
205 205
206 206
207 207 def applychanges(ui, repo, ctx, opts):
208 208 """Merge changeset from ctx (only) in the current working directory"""
209 209 wcpar = repo.dirstate.parents()[0]
210 210 if ctx.p1().node() == wcpar:
211 211 # edition ar "in place" we do not need to make any merge,
212 212 # just applies changes on parent for edition
213 213 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
214 214 stats = None
215 215 else:
216 216 try:
217 217 # ui.forcemerge is an internal variable, do not document
218 218 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''))
219 219 stats = mergemod.update(repo, ctx.node(), True, True, False,
220 220 ctx.p1().node())
221 221 finally:
222 222 repo.ui.setconfig('ui', 'forcemerge', '')
223 223 repo.setparents(wcpar, node.nullid)
224 224 repo.dirstate.write()
225 225 # fix up dirstate for copies and renames
226 226 cmdutil.duplicatecopies(repo, ctx.rev(), ctx.p1().rev())
227 227 return stats
228 228
229 229 def collapse(repo, first, last, commitopts):
230 230 """collapse the set of revisions from first to last as new one.
231 231
232 232 Expected commit options are:
233 233 - message
234 234 - date
235 235 - username
236 236 Commit message is edited in all cases.
237 237
238 238 This function works in memory."""
239 239 ctxs = list(repo.set('%d::%d', first, last))
240 240 if not ctxs:
241 241 return None
242 242 base = first.parents()[0]
243 243
244 244 # commit a new version of the old changeset, including the update
245 245 # collect all files which might be affected
246 246 files = set()
247 247 for ctx in ctxs:
248 248 files.update(ctx.files())
249 249
250 250 # Recompute copies (avoid recording a -> b -> a)
251 251 copied = copies.pathcopies(base, last)
252 252
253 253 # prune files which were reverted by the updates
254 254 def samefile(f):
255 255 if f in last.manifest():
256 256 a = last.filectx(f)
257 257 if f in base.manifest():
258 258 b = base.filectx(f)
259 259 return (a.data() == b.data()
260 260 and a.flags() == b.flags())
261 261 else:
262 262 return False
263 263 else:
264 264 return f not in base.manifest()
265 265 files = [f for f in files if not samefile(f)]
266 266 # commit version of these files as defined by head
267 267 headmf = last.manifest()
268 268 def filectxfn(repo, ctx, path):
269 269 if path in headmf:
270 270 fctx = last[path]
271 271 flags = fctx.flags()
272 272 mctx = context.memfilectx(fctx.path(), fctx.data(),
273 273 islink='l' in flags,
274 274 isexec='x' in flags,
275 275 copied=copied.get(path))
276 276 return mctx
277 277 raise IOError()
278 278
279 279 if commitopts.get('message'):
280 280 message = commitopts['message']
281 281 else:
282 282 message = first.description()
283 283 user = commitopts.get('user')
284 284 date = commitopts.get('date')
285 285 extra = commitopts.get('extra')
286 286
287 287 parents = (first.p1().node(), first.p2().node())
288 288 new = context.memctx(repo,
289 289 parents=parents,
290 290 text=message,
291 291 files=files,
292 292 filectxfn=filectxfn,
293 293 user=user,
294 294 date=date,
295 295 extra=extra)
296 296 new._text = cmdutil.commitforceeditor(repo, new, [])
297 repo.savecommitmessage(new.description())
297 298 return repo.commitctx(new)
298 299
299 300 def pick(ui, repo, ctx, ha, opts):
300 301 oldctx = repo[ha]
301 302 if oldctx.parents()[0] == ctx:
302 303 ui.debug('node %s unchanged\n' % ha)
303 304 return oldctx, []
304 305 hg.update(repo, ctx.node())
305 306 stats = applychanges(ui, repo, oldctx, opts)
306 307 if stats and stats[3] > 0:
307 308 raise error.InterventionRequired(_('Fix up the change and run '
308 309 'hg histedit --continue'))
309 310 # drop the second merge parent
310 311 commit = commitfuncfor(repo, oldctx)
311 312 n = commit(text=oldctx.description(), user=oldctx.user(),
312 313 date=oldctx.date(), extra=oldctx.extra())
313 314 if n is None:
314 315 ui.warn(_('%s: empty changeset\n')
315 316 % node.hex(ha))
316 317 return ctx, []
317 318 new = repo[n]
318 319 return new, [(oldctx.node(), (n,))]
319 320
320 321
321 322 def edit(ui, repo, ctx, ha, opts):
322 323 oldctx = repo[ha]
323 324 hg.update(repo, ctx.node())
324 325 applychanges(ui, repo, oldctx, opts)
325 326 raise error.InterventionRequired(
326 327 _('Make changes as needed, you may commit or record as needed now.\n'
327 328 'When you are finished, run hg histedit --continue to resume.'))
328 329
329 330 def fold(ui, repo, ctx, ha, opts):
330 331 oldctx = repo[ha]
331 332 hg.update(repo, ctx.node())
332 333 stats = applychanges(ui, repo, oldctx, opts)
333 334 if stats and stats[3] > 0:
334 335 raise error.InterventionRequired(
335 336 _('Fix up the change and run hg histedit --continue'))
336 337 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(),
337 338 date=oldctx.date(), extra=oldctx.extra())
338 339 if n is None:
339 340 ui.warn(_('%s: empty changeset')
340 341 % node.hex(ha))
341 342 return ctx, []
342 343 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
343 344
344 345 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
345 346 parent = ctx.parents()[0].node()
346 347 hg.update(repo, parent)
347 348 ### prepare new commit data
348 349 commitopts = opts.copy()
349 350 # username
350 351 if ctx.user() == oldctx.user():
351 352 username = ctx.user()
352 353 else:
353 354 username = ui.username()
354 355 commitopts['user'] = username
355 356 # commit message
356 357 newmessage = '\n***\n'.join(
357 358 [ctx.description()] +
358 359 [repo[r].description() for r in internalchanges] +
359 360 [oldctx.description()]) + '\n'
360 361 commitopts['message'] = newmessage
361 362 # date
362 363 commitopts['date'] = max(ctx.date(), oldctx.date())
363 364 extra = ctx.extra().copy()
364 365 # histedit_source
365 366 # note: ctx is likely a temporary commit but that the best we can do here
366 367 # This is sufficient to solve issue3681 anyway
367 368 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
368 369 commitopts['extra'] = extra
369 370 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
370 371 try:
371 372 phasemin = max(ctx.phase(), oldctx.phase())
372 373 repo.ui.setconfig('phases', 'new-commit', phasemin)
373 374 n = collapse(repo, ctx, repo[newnode], commitopts)
374 375 finally:
375 376 repo.ui.restoreconfig(phasebackup)
376 377 if n is None:
377 378 return ctx, []
378 379 hg.update(repo, n)
379 380 replacements = [(oldctx.node(), (newnode,)),
380 381 (ctx.node(), (n,)),
381 382 (newnode, (n,)),
382 383 ]
383 384 for ich in internalchanges:
384 385 replacements.append((ich, (n,)))
385 386 return repo[n], replacements
386 387
387 388 def drop(ui, repo, ctx, ha, opts):
388 389 return ctx, [(repo[ha].node(), ())]
389 390
390 391
391 392 def message(ui, repo, ctx, ha, opts):
392 393 oldctx = repo[ha]
393 394 hg.update(repo, ctx.node())
394 395 stats = applychanges(ui, repo, oldctx, opts)
395 396 if stats and stats[3] > 0:
396 397 raise error.InterventionRequired(
397 398 _('Fix up the change and run hg histedit --continue'))
398 399 message = oldctx.description() + '\n'
399 400 message = ui.edit(message, ui.username())
400 401 commit = commitfuncfor(repo, oldctx)
401 402 new = commit(text=message, user=oldctx.user(), date=oldctx.date(),
402 403 extra=oldctx.extra())
403 404 newctx = repo[new]
404 405 if oldctx.node() != newctx.node():
405 406 return newctx, [(oldctx.node(), (new,))]
406 407 # We didn't make an edit, so just indicate no replaced nodes
407 408 return newctx, []
408 409
409 410 def findoutgoing(ui, repo, remote=None, force=False, opts={}):
410 411 """utility function to find the first outgoing changeset
411 412
412 413 Used by initialisation code"""
413 414 dest = ui.expandpath(remote or 'default-push', remote or 'default')
414 415 dest, revs = hg.parseurl(dest, None)[:2]
415 416 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
416 417
417 418 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
418 419 other = hg.peer(repo, opts, dest)
419 420
420 421 if revs:
421 422 revs = [repo.lookup(rev) for rev in revs]
422 423
423 424 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
424 425 if not outgoing.missing:
425 426 raise util.Abort(_('no outgoing ancestors'))
426 427 roots = list(repo.revs("roots(%ln)", outgoing.missing))
427 428 if 1 < len(roots):
428 429 msg = _('there are ambiguous outgoing revisions')
429 430 hint = _('see "hg help histedit" for more detail')
430 431 raise util.Abort(msg, hint=hint)
431 432 return repo.lookup(roots[0])
432 433
433 434 actiontable = {'p': pick,
434 435 'pick': pick,
435 436 'e': edit,
436 437 'edit': edit,
437 438 'f': fold,
438 439 'fold': fold,
439 440 'd': drop,
440 441 'drop': drop,
441 442 'm': message,
442 443 'mess': message,
443 444 }
444 445
445 446 @command('histedit',
446 447 [('', 'commands', '',
447 448 _('Read history edits from the specified file.')),
448 449 ('c', 'continue', False, _('continue an edit already in progress')),
449 450 ('k', 'keep', False,
450 451 _("don't strip old nodes after edit is complete")),
451 452 ('', 'abort', False, _('abort an edit in progress')),
452 453 ('o', 'outgoing', False, _('changesets not found in destination')),
453 454 ('f', 'force', False,
454 455 _('force outgoing even for unrelated repositories')),
455 456 ('r', 'rev', [], _('first revision to be edited'))],
456 457 _("ANCESTOR | --outgoing [URL]"))
457 458 def histedit(ui, repo, *freeargs, **opts):
458 459 """interactively edit changeset history
459 460
460 461 This command edits changesets between ANCESTOR and the parent of
461 462 the working directory.
462 463
463 464 With --outgoing, this edits changesets not found in the
464 465 destination repository. If URL of the destination is omitted, the
465 466 'default-push' (or 'default') path will be used.
466 467
467 468 For safety, this command is aborted, also if there are ambiguous
468 469 outgoing revisions which may confuse users: for example, there are
469 470 multiple branches containing outgoing revisions.
470 471
471 472 Use "min(outgoing() and ::.)" or similar revset specification
472 473 instead of --outgoing to specify edit target revision exactly in
473 474 such ambiguous situation. See :hg:`help revsets` for detail about
474 475 selecting revisions.
475 476
476 477 Returns 0 on success, 1 if user intervention is required (not only
477 478 for intentional "edit" command, but also for resolving unexpected
478 479 conflicts).
479 480 """
480 481 lock = wlock = None
481 482 try:
482 483 wlock = repo.wlock()
483 484 lock = repo.lock()
484 485 _histedit(ui, repo, *freeargs, **opts)
485 486 finally:
486 487 release(lock, wlock)
487 488
488 489 def _histedit(ui, repo, *freeargs, **opts):
489 490 # TODO only abort if we try and histedit mq patches, not just
490 491 # blanket if mq patches are applied somewhere
491 492 mq = getattr(repo, 'mq', None)
492 493 if mq and mq.applied:
493 494 raise util.Abort(_('source has mq patches applied'))
494 495
495 496 # basic argument incompatibility processing
496 497 outg = opts.get('outgoing')
497 498 cont = opts.get('continue')
498 499 abort = opts.get('abort')
499 500 force = opts.get('force')
500 501 rules = opts.get('commands', '')
501 502 revs = opts.get('rev', [])
502 503 goal = 'new' # This invocation goal, in new, continue, abort
503 504 if force and not outg:
504 505 raise util.Abort(_('--force only allowed with --outgoing'))
505 506 if cont:
506 507 if util.any((outg, abort, revs, freeargs, rules)):
507 508 raise util.Abort(_('no arguments allowed with --continue'))
508 509 goal = 'continue'
509 510 elif abort:
510 511 if util.any((outg, revs, freeargs, rules)):
511 512 raise util.Abort(_('no arguments allowed with --abort'))
512 513 goal = 'abort'
513 514 else:
514 515 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
515 516 raise util.Abort(_('history edit already in progress, try '
516 517 '--continue or --abort'))
517 518 if outg:
518 519 if revs:
519 520 raise util.Abort(_('no revisions allowed with --outgoing'))
520 521 if len(freeargs) > 1:
521 522 raise util.Abort(
522 523 _('only one repo argument allowed with --outgoing'))
523 524 else:
524 525 revs.extend(freeargs)
525 526 if len(revs) != 1:
526 527 raise util.Abort(
527 528 _('histedit requires exactly one ancestor revision'))
528 529
529 530
530 531 if goal == 'continue':
531 532 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
532 533 parentctx = repo[parentctxnode]
533 534 parentctx, repl = bootstrapcontinue(ui, repo, parentctx, rules, opts)
534 535 replacements.extend(repl)
535 536 elif goal == 'abort':
536 537 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
537 538 mapping, tmpnodes, leafs, _ntm = processreplacement(repo, replacements)
538 539 ui.debug('restore wc to old parent %s\n' % node.short(topmost))
539 540 # check whether we should update away
540 541 parentnodes = [c.node() for c in repo[None].parents()]
541 542 for n in leafs | set([parentctxnode]):
542 543 if n in parentnodes:
543 544 hg.clean(repo, topmost)
544 545 break
545 546 else:
546 547 pass
547 548 cleanupnode(ui, repo, 'created', tmpnodes)
548 549 cleanupnode(ui, repo, 'temp', leafs)
549 550 os.unlink(os.path.join(repo.path, 'histedit-state'))
550 551 return
551 552 else:
552 553 cmdutil.checkunfinished(repo)
553 554 cmdutil.bailifchanged(repo)
554 555
555 556 topmost, empty = repo.dirstate.parents()
556 557 if outg:
557 558 if freeargs:
558 559 remote = freeargs[0]
559 560 else:
560 561 remote = None
561 562 root = findoutgoing(ui, repo, remote, force, opts)
562 563 else:
563 564 root = revs[0]
564 565 root = scmutil.revsingle(repo, root).node()
565 566
566 567 keep = opts.get('keep', False)
567 568 revs = between(repo, root, topmost, keep)
568 569 if not revs:
569 570 raise util.Abort(_('%s is not an ancestor of working directory') %
570 571 node.short(root))
571 572
572 573 ctxs = [repo[r] for r in revs]
573 574 if not rules:
574 575 rules = '\n'.join([makedesc(c) for c in ctxs])
575 576 rules += '\n\n'
576 577 rules += editcomment % (node.short(root), node.short(topmost))
577 578 rules = ui.edit(rules, ui.username())
578 579 # Save edit rules in .hg/histedit-last-edit.txt in case
579 580 # the user needs to ask for help after something
580 581 # surprising happens.
581 582 f = open(repo.join('histedit-last-edit.txt'), 'w')
582 583 f.write(rules)
583 584 f.close()
584 585 else:
585 586 if rules == '-':
586 587 f = sys.stdin
587 588 else:
588 589 f = open(rules)
589 590 rules = f.read()
590 591 f.close()
591 592 rules = [l for l in (r.strip() for r in rules.splitlines())
592 593 if l and not l[0] == '#']
593 594 rules = verifyrules(rules, repo, ctxs)
594 595
595 596 parentctx = repo[root].parents()[0]
596 597 keep = opts.get('keep', False)
597 598 replacements = []
598 599
599 600
600 601 while rules:
601 602 writestate(repo, parentctx.node(), rules, keep, topmost, replacements)
602 603 action, ha = rules.pop(0)
603 604 ui.debug('histedit: processing %s %s\n' % (action, ha))
604 605 actfunc = actiontable[action]
605 606 parentctx, replacement_ = actfunc(ui, repo, parentctx, ha, opts)
606 607 replacements.extend(replacement_)
607 608
608 609 hg.update(repo, parentctx.node())
609 610
610 611 mapping, tmpnodes, created, ntm = processreplacement(repo, replacements)
611 612 if mapping:
612 613 for prec, succs in mapping.iteritems():
613 614 if not succs:
614 615 ui.debug('histedit: %s is dropped\n' % node.short(prec))
615 616 else:
616 617 ui.debug('histedit: %s is replaced by %s\n' % (
617 618 node.short(prec), node.short(succs[0])))
618 619 if len(succs) > 1:
619 620 m = 'histedit: %s'
620 621 for n in succs[1:]:
621 622 ui.debug(m % node.short(n))
622 623
623 624 if not keep:
624 625 if mapping:
625 626 movebookmarks(ui, repo, mapping, topmost, ntm)
626 627 # TODO update mq state
627 628 if obsolete._enabled:
628 629 markers = []
629 630 # sort by revision number because it sound "right"
630 631 for prec in sorted(mapping, key=repo.changelog.rev):
631 632 succs = mapping[prec]
632 633 markers.append((repo[prec],
633 634 tuple(repo[s] for s in succs)))
634 635 if markers:
635 636 obsolete.createmarkers(repo, markers)
636 637 else:
637 638 cleanupnode(ui, repo, 'replaced', mapping)
638 639
639 640 cleanupnode(ui, repo, 'temp', tmpnodes)
640 641 os.unlink(os.path.join(repo.path, 'histedit-state'))
641 642 if os.path.exists(repo.sjoin('undo')):
642 643 os.unlink(repo.sjoin('undo'))
643 644
644 645
645 646 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
646 647 action, currentnode = rules.pop(0)
647 648 ctx = repo[currentnode]
648 649 # is there any new commit between the expected parent and "."
649 650 #
650 651 # note: does not take non linear new change in account (but previous
651 652 # implementation didn't used them anyway (issue3655)
652 653 newchildren = [c.node() for c in repo.set('(%d::.)', parentctx)]
653 654 if parentctx.node() != node.nullid:
654 655 if not newchildren:
655 656 # `parentctxnode` should match but no result. This means that
656 657 # currentnode is not a descendant from parentctxnode.
657 658 msg = _('%s is not an ancestor of working directory')
658 659 hint = _('use "histedit --abort" to clear broken state')
659 660 raise util.Abort(msg % parentctx, hint=hint)
660 661 newchildren.pop(0) # remove parentctxnode
661 662 # Commit dirty working directory if necessary
662 663 new = None
663 664 m, a, r, d = repo.status()[:4]
664 665 if m or a or r or d:
665 666 # prepare the message for the commit to comes
666 667 if action in ('f', 'fold'):
667 668 message = 'fold-temp-revision %s' % currentnode
668 669 else:
669 670 message = ctx.description() + '\n'
670 671 if action in ('e', 'edit', 'm', 'mess'):
671 672 editor = cmdutil.commitforceeditor
672 673 else:
673 674 editor = False
674 675 commit = commitfuncfor(repo, ctx)
675 676 new = commit(text=message, user=ctx.user(),
676 677 date=ctx.date(), extra=ctx.extra(),
677 678 editor=editor)
678 679 if new is not None:
679 680 newchildren.append(new)
680 681
681 682 replacements = []
682 683 # track replacements
683 684 if ctx.node() not in newchildren:
684 685 # note: new children may be empty when the changeset is dropped.
685 686 # this happen e.g during conflicting pick where we revert content
686 687 # to parent.
687 688 replacements.append((ctx.node(), tuple(newchildren)))
688 689
689 690 if action in ('f', 'fold'):
690 691 if newchildren:
691 692 # finalize fold operation if applicable
692 693 if new is None:
693 694 new = newchildren[-1]
694 695 else:
695 696 newchildren.pop() # remove new from internal changes
696 697 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new, opts,
697 698 newchildren)
698 699 replacements.extend(repl)
699 700 else:
700 701 # newchildren is empty if the fold did not result in any commit
701 702 # this happen when all folded change are discarded during the
702 703 # merge.
703 704 replacements.append((ctx.node(), (parentctx.node(),)))
704 705 elif newchildren:
705 706 # otherwise update "parentctx" before proceeding to further operation
706 707 parentctx = repo[newchildren[-1]]
707 708 return parentctx, replacements
708 709
709 710
710 711 def between(repo, old, new, keep):
711 712 """select and validate the set of revision to edit
712 713
713 714 When keep is false, the specified set can't have children."""
714 715 ctxs = list(repo.set('%n::%n', old, new))
715 716 if ctxs and not keep:
716 717 if (not obsolete._enabled and
717 718 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
718 719 raise util.Abort(_('cannot edit history that would orphan nodes'))
719 720 if repo.revs('(%ld) and merge()', ctxs):
720 721 raise util.Abort(_('cannot edit history that contains merges'))
721 722 root = ctxs[0] # list is already sorted by repo.set
722 723 if not root.phase():
723 724 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
724 725 return [c.node() for c in ctxs]
725 726
726 727
727 728 def writestate(repo, parentnode, rules, keep, topmost, replacements):
728 729 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
729 730 pickle.dump((parentnode, rules, keep, topmost, replacements), fp)
730 731 fp.close()
731 732
732 733 def readstate(repo):
733 734 """Returns a tuple of (parentnode, rules, keep, topmost, replacements).
734 735 """
735 736 fp = open(os.path.join(repo.path, 'histedit-state'))
736 737 return pickle.load(fp)
737 738
738 739
739 740 def makedesc(c):
740 741 """build a initial action line for a ctx `c`
741 742
742 743 line are in the form:
743 744
744 745 pick <hash> <rev> <summary>
745 746 """
746 747 summary = ''
747 748 if c.description():
748 749 summary = c.description().splitlines()[0]
749 750 line = 'pick %s %d %s' % (c, c.rev(), summary)
750 751 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
751 752
752 753 def verifyrules(rules, repo, ctxs):
753 754 """Verify that there exists exactly one edit rule per given changeset.
754 755
755 756 Will abort if there are to many or too few rules, a malformed rule,
756 757 or a rule on a changeset outside of the user-given range.
757 758 """
758 759 parsed = []
759 760 expected = set(str(c) for c in ctxs)
760 761 seen = set()
761 762 for r in rules:
762 763 if ' ' not in r:
763 764 raise util.Abort(_('malformed line "%s"') % r)
764 765 action, rest = r.split(' ', 1)
765 766 ha = rest.strip().split(' ', 1)[0]
766 767 try:
767 768 ha = str(repo[ha]) # ensure its a short hash
768 769 except error.RepoError:
769 770 raise util.Abort(_('unknown changeset %s listed') % ha)
770 771 if ha not in expected:
771 772 raise util.Abort(
772 773 _('may not use changesets other than the ones listed'))
773 774 if ha in seen:
774 775 raise util.Abort(_('duplicated command for changeset %s') % ha)
775 776 seen.add(ha)
776 777 if action not in actiontable:
777 778 raise util.Abort(_('unknown action "%s"') % action)
778 779 parsed.append([action, ha])
779 780 missing = sorted(expected - seen) # sort to stabilize output
780 781 if missing:
781 782 raise util.Abort(_('missing rules for changeset %s') % missing[0],
782 783 hint=_('do you want to use the drop action?'))
783 784 return parsed
784 785
785 786 def processreplacement(repo, replacements):
786 787 """process the list of replacements to return
787 788
788 789 1) the final mapping between original and created nodes
789 790 2) the list of temporary node created by histedit
790 791 3) the list of new commit created by histedit"""
791 792 allsuccs = set()
792 793 replaced = set()
793 794 fullmapping = {}
794 795 # initialise basic set
795 796 # fullmapping record all operation recorded in replacement
796 797 for rep in replacements:
797 798 allsuccs.update(rep[1])
798 799 replaced.add(rep[0])
799 800 fullmapping.setdefault(rep[0], set()).update(rep[1])
800 801 new = allsuccs - replaced
801 802 tmpnodes = allsuccs & replaced
802 803 # Reduce content fullmapping into direct relation between original nodes
803 804 # and final node created during history edition
804 805 # Dropped changeset are replaced by an empty list
805 806 toproceed = set(fullmapping)
806 807 final = {}
807 808 while toproceed:
808 809 for x in list(toproceed):
809 810 succs = fullmapping[x]
810 811 for s in list(succs):
811 812 if s in toproceed:
812 813 # non final node with unknown closure
813 814 # We can't process this now
814 815 break
815 816 elif s in final:
816 817 # non final node, replace with closure
817 818 succs.remove(s)
818 819 succs.update(final[s])
819 820 else:
820 821 final[x] = succs
821 822 toproceed.remove(x)
822 823 # remove tmpnodes from final mapping
823 824 for n in tmpnodes:
824 825 del final[n]
825 826 # we expect all changes involved in final to exist in the repo
826 827 # turn `final` into list (topologically sorted)
827 828 nm = repo.changelog.nodemap
828 829 for prec, succs in final.items():
829 830 final[prec] = sorted(succs, key=nm.get)
830 831
831 832 # computed topmost element (necessary for bookmark)
832 833 if new:
833 834 newtopmost = sorted(new, key=repo.changelog.rev)[-1]
834 835 elif not final:
835 836 # Nothing rewritten at all. we won't need `newtopmost`
836 837 # It is the same as `oldtopmost` and `processreplacement` know it
837 838 newtopmost = None
838 839 else:
839 840 # every body died. The newtopmost is the parent of the root.
840 841 newtopmost = repo[sorted(final, key=repo.changelog.rev)[0]].p1().node()
841 842
842 843 return final, tmpnodes, new, newtopmost
843 844
844 845 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
845 846 """Move bookmark from old to newly created node"""
846 847 if not mapping:
847 848 # if nothing got rewritten there is not purpose for this function
848 849 return
849 850 moves = []
850 851 for bk, old in sorted(repo._bookmarks.iteritems()):
851 852 if old == oldtopmost:
852 853 # special case ensure bookmark stay on tip.
853 854 #
854 855 # This is arguably a feature and we may only want that for the
855 856 # active bookmark. But the behavior is kept compatible with the old
856 857 # version for now.
857 858 moves.append((bk, newtopmost))
858 859 continue
859 860 base = old
860 861 new = mapping.get(base, None)
861 862 if new is None:
862 863 continue
863 864 while not new:
864 865 # base is killed, trying with parent
865 866 base = repo[base].p1().node()
866 867 new = mapping.get(base, (base,))
867 868 # nothing to move
868 869 moves.append((bk, new[-1]))
869 870 if moves:
870 871 marks = repo._bookmarks
871 872 for mark, new in moves:
872 873 old = marks[mark]
873 874 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
874 875 % (mark, node.short(old), node.short(new)))
875 876 marks[mark] = new
876 877 marks.write()
877 878
878 879 def cleanupnode(ui, repo, name, nodes):
879 880 """strip a group of nodes from the repository
880 881
881 882 The set of node to strip may contains unknown nodes."""
882 883 ui.debug('should strip %s nodes %s\n' %
883 884 (name, ', '.join([node.short(n) for n in nodes])))
884 885 lock = None
885 886 try:
886 887 lock = repo.lock()
887 888 # Find all node that need to be stripped
888 889 # (we hg %lr instead of %ln to silently ignore unknown item
889 890 nm = repo.changelog.nodemap
890 891 nodes = [n for n in nodes if n in nm]
891 892 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
892 893 for c in roots:
893 894 # We should process node in reverse order to strip tip most first.
894 895 # but this trigger a bug in changegroup hook.
895 896 # This would reduce bundle overhead
896 897 repair.strip(ui, repo, c)
897 898 finally:
898 899 lockmod.release(lock)
899 900
900 901 def summaryhook(ui, repo):
901 902 if not os.path.exists(repo.join('histedit-state')):
902 903 return
903 904 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
904 905 if rules:
905 906 # i18n: column positioning for "hg summary"
906 907 ui.write(_('hist: %s (histedit --continue)\n') %
907 908 (ui.label(_('%d remaining'), 'histedit.remaining') %
908 909 len(rules)))
909 910
910 911 def extsetup(ui):
911 912 cmdutil.summaryhooks.add('histedit', summaryhook)
912 913 cmdutil.unfinishedstates.append(
913 914 ['histedit-state', False, True, _('histedit in progress'),
914 915 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
@@ -1,301 +1,364 b''
1 1 Test histedit extention: 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 check saving last-message.txt
109
110 $ cat > $TESTDIR/abortfolding.py <<EOF
111 > from mercurial import util
112 > def abortfolding(ui, repo, hooktype, **kwargs):
113 > ctx = repo[kwargs.get('node')]
114 > if set(ctx.files()) == set(['c', 'd', 'f']):
115 > return True # abort folding commit only
116 > ui.warn('allow non-folding commit\\n')
117 > EOF
118 $ cat > .hg/hgrc <<EOF
119 > [hooks]
120 > pretxncommit.abortfolding = python:$TESTDIR/abortfolding.py:abortfolding
121 > EOF
122
123 $ cat > $TESTDIR/editor.sh << EOF
124 > echo "==== before editing"
125 > cat \$1
126 > echo "===="
127 > echo "check saving last-message.txt" >> \$1
128 > EOF
129
130 $ rm -f .hg/last-message.txt
131 $ HGEDITOR="sh $TESTDIR/editor.sh" hg histedit 6de59d13424a --commands - 2>&1 <<EOF | fixbundle
132 > pick 6de59d13424a f
133 > fold 9c277da72c9b d
134 > EOF
135 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
136 allow non-folding commit
137 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
138 ==== before editing
139 f
140 ***
141 c
142 ***
143 d
144
145
146
147 HG: Enter commit message. Lines beginning with 'HG:' are removed.
148 HG: Leave message empty to abort commit.
149 HG: --
150 HG: user: test
151 HG: branch 'default'
152 HG: changed c
153 HG: changed d
154 HG: changed f
155 ====
156 transaction abort!
157 rollback completed
158 abort: pretxncommit.abortfolding hook failed
159
160 $ cat .hg/last-message.txt
161 f
162 ***
163 c
164 ***
165 d
166
167
168
169 check saving last-message.txt
170
108 171 $ cd ..
109 172
110 173 folding and creating no new change doesn't break:
111 174 -------------------------------------------------
112 175
113 176 folded content is dropped during a merge. The folded commit should properly disapear.
114 177
115 178 $ mkdir fold-to-empty-test
116 179 $ cd fold-to-empty-test
117 180 $ hg init
118 181 $ printf "1\n2\n3\n" > file
119 182 $ hg add file
120 183 $ hg commit -m '1+2+3'
121 184 $ echo 4 >> file
122 185 $ hg commit -m '+4'
123 186 $ echo 5 >> file
124 187 $ hg commit -m '+5'
125 188 $ echo 6 >> file
126 189 $ hg commit -m '+6'
127 190 $ hg logt --graph
128 191 @ 3:251d831eeec5 +6
129 192 |
130 193 o 2:888f9082bf99 +5
131 194 |
132 195 o 1:617f94f13c0f +4
133 196 |
134 197 o 0:0189ba417d34 1+2+3
135 198
136 199
137 200 $ hg histedit 1 --commands - << EOF
138 201 > pick 617f94f13c0f 1 +4
139 202 > drop 888f9082bf99 2 +5
140 203 > fold 251d831eeec5 3 +6
141 204 > EOF
142 205 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
143 206 merging file
144 207 warning: conflicts during merge.
145 208 merging file incomplete! (edit conflicts, then use 'hg resolve --mark')
146 209 Fix up the change and run hg histedit --continue
147 210 [1]
148 211 There were conflicts, we keep P1 content. This
149 212 should effectively drop the changes from +6.
150 213 $ hg status
151 214 M file
152 215 ? file.orig
153 216 $ hg resolve -l
154 217 U file
155 218 $ hg revert -r 'p1()' file
156 219 $ hg resolve --mark file
157 220 $ hg histedit --continue
158 221 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
159 222 saved backup bundle to $TESTTMP/*-backup.hg (glob)
160 223 $ hg logt --graph
161 224 @ 1:617f94f13c0f +4
162 225 |
163 226 o 0:0189ba417d34 1+2+3
164 227
165 228
166 229 $ cd ..
167 230
168 231
169 232 Test fold through dropped
170 233 -------------------------
171 234
172 235
173 236 Test corner case where folded revision is separated from its parent by a
174 237 dropped revision.
175 238
176 239
177 240 $ hg init fold-with-dropped
178 241 $ cd fold-with-dropped
179 242 $ printf "1\n2\n3\n" > file
180 243 $ hg commit -Am '1+2+3'
181 244 adding file
182 245 $ echo 4 >> file
183 246 $ hg commit -m '+4'
184 247 $ echo 5 >> file
185 248 $ hg commit -m '+5'
186 249 $ echo 6 >> file
187 250 $ hg commit -m '+6'
188 251 $ hg logt -G
189 252 @ 3:251d831eeec5 +6
190 253 |
191 254 o 2:888f9082bf99 +5
192 255 |
193 256 o 1:617f94f13c0f +4
194 257 |
195 258 o 0:0189ba417d34 1+2+3
196 259
197 260 $ hg histedit 1 --commands - << EOF
198 261 > pick 617f94f13c0f 1 +4
199 262 > drop 888f9082bf99 2 +5
200 263 > fold 251d831eeec5 3 +6
201 264 > EOF
202 265 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
203 266 merging file
204 267 warning: conflicts during merge.
205 268 merging file incomplete! (edit conflicts, then use 'hg resolve --mark')
206 269 Fix up the change and run hg histedit --continue
207 270 [1]
208 271 $ cat > file << EOF
209 272 > 1
210 273 > 2
211 274 > 3
212 275 > 4
213 276 > 5
214 277 > EOF
215 278 $ hg resolve --mark file
216 279 $ hg commit -m '+5.2'
217 280 created new head
218 281 $ echo 6 >> file
219 282 $ HGEDITOR=cat hg histedit --continue
220 283 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
221 284 +4
222 285 ***
223 286 +5.2
224 287 ***
225 288 +6
226 289
227 290
228 291
229 292 HG: Enter commit message. Lines beginning with 'HG:' are removed.
230 293 HG: Leave message empty to abort commit.
231 294 HG: --
232 295 HG: user: test
233 296 HG: branch 'default'
234 297 HG: changed file
235 298 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
236 299 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
237 300 saved backup bundle to $TESTTMP/fold-with-dropped/.hg/strip-backup/617f94f13c0f-backup.hg (glob)
238 301 $ hg logt -G
239 302 @ 1:10c647b2cdd5 +4
240 303 |
241 304 o 0:0189ba417d34 1+2+3
242 305
243 306 $ hg export tip
244 307 # HG changeset patch
245 308 # User test
246 309 # Date 0 0
247 310 # Thu Jan 01 00:00:00 1970 +0000
248 311 # Node ID 10c647b2cdd54db0603ecb99b2ff5ce66d5a5323
249 312 # Parent 0189ba417d34df9dda55f88b637dcae9917b5964
250 313 +4
251 314 ***
252 315 +5.2
253 316 ***
254 317 +6
255 318
256 319 diff -r 0189ba417d34 -r 10c647b2cdd5 file
257 320 --- a/file Thu Jan 01 00:00:00 1970 +0000
258 321 +++ b/file Thu Jan 01 00:00:00 1970 +0000
259 322 @@ -1,3 +1,6 @@
260 323 1
261 324 2
262 325 3
263 326 +4
264 327 +5
265 328 +6
266 329 $ cd ..
267 330
268 331
269 332 Folding with initial rename (issue3729)
270 333 ---------------------------------------
271 334
272 335 $ hg init fold-rename
273 336 $ cd fold-rename
274 337 $ echo a > a.txt
275 338 $ hg add a.txt
276 339 $ hg commit -m a
277 340 $ hg rename a.txt b.txt
278 341 $ hg commit -m rename
279 342 $ echo b >> b.txt
280 343 $ hg commit -m b
281 344
282 345 $ hg logt --follow b.txt
283 346 2:e0371e0426bc b
284 347 1:1c4f440a8085 rename
285 348 0:6c795aa153cb a
286 349
287 350 $ hg histedit 1c4f440a8085 --commands - 2>&1 << EOF | fixbundle
288 351 > pick 1c4f440a8085 rename
289 352 > fold e0371e0426bc b
290 353 > EOF
291 354 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
292 355 reverting b.txt
293 356 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
294 357 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
295 358 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
296 359
297 360 $ hg logt --follow b.txt
298 361 1:cf858d235c76 rename
299 362 0:6c795aa153cb a
300 363
301 364 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now