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