##// END OF EJS Templates
histedit: refuse to edit history that contains merges (issue3962)
Augie Fackler -
r19473:10a0ae66 stable
parent child Browse files
Show More
@@ -1,872 +1,874 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.i18n import _
163 163
164 164 cmdtable = {}
165 165 command = cmdutil.command(cmdtable)
166 166
167 167 testedwith = 'internal'
168 168
169 169 # i18n: command names and abbreviations must remain untranslated
170 170 editcomment = _("""# Edit history between %s and %s
171 171 #
172 172 # Commands:
173 173 # p, pick = use commit
174 174 # e, edit = use commit, but stop for amending
175 175 # f, fold = use commit, but fold into previous commit (combines N and N-1)
176 176 # d, drop = remove commit from history
177 177 # m, mess = edit message without changing commit content
178 178 #
179 179 """)
180 180
181 181 def commitfuncfor(repo, src):
182 182 """Build a commit function for the replacement of <src>
183 183
184 184 This function ensure we apply the same treatment to all changesets.
185 185
186 186 - Add a 'histedit_source' entry in extra.
187 187
188 188 Note that fold have its own separated logic because its handling is a bit
189 189 different and not easily factored out of the fold method.
190 190 """
191 191 phasemin = src.phase()
192 192 def commitfunc(**kwargs):
193 193 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
194 194 try:
195 195 repo.ui.setconfig('phases', 'new-commit', phasemin)
196 196 extra = kwargs.get('extra', {}).copy()
197 197 extra['histedit_source'] = src.hex()
198 198 kwargs['extra'] = extra
199 199 return repo.commit(**kwargs)
200 200 finally:
201 201 repo.ui.restoreconfig(phasebackup)
202 202 return commitfunc
203 203
204 204
205 205
206 206 def applychanges(ui, repo, ctx, opts):
207 207 """Merge changeset from ctx (only) in the current working directory"""
208 208 wcpar = repo.dirstate.parents()[0]
209 209 if ctx.p1().node() == wcpar:
210 210 # edition ar "in place" we do not need to make any merge,
211 211 # just applies changes on parent for edition
212 212 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
213 213 stats = None
214 214 else:
215 215 try:
216 216 # ui.forcemerge is an internal variable, do not document
217 217 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''))
218 218 stats = mergemod.update(repo, ctx.node(), True, True, False,
219 219 ctx.p1().node())
220 220 finally:
221 221 repo.ui.setconfig('ui', 'forcemerge', '')
222 222 repo.setparents(wcpar, node.nullid)
223 223 repo.dirstate.write()
224 224 # fix up dirstate for copies and renames
225 225 cmdutil.duplicatecopies(repo, ctx.rev(), ctx.p1().rev())
226 226 return stats
227 227
228 228 def collapse(repo, first, last, commitopts):
229 229 """collapse the set of revisions from first to last as new one.
230 230
231 231 Expected commit options are:
232 232 - message
233 233 - date
234 234 - username
235 235 Commit message is edited in all cases.
236 236
237 237 This function works in memory."""
238 238 ctxs = list(repo.set('%d::%d', first, last))
239 239 if not ctxs:
240 240 return None
241 241 base = first.parents()[0]
242 242
243 243 # commit a new version of the old changeset, including the update
244 244 # collect all files which might be affected
245 245 files = set()
246 246 for ctx in ctxs:
247 247 files.update(ctx.files())
248 248
249 249 # Recompute copies (avoid recording a -> b -> a)
250 250 copied = copies.pathcopies(base, last)
251 251
252 252 # prune files which were reverted by the updates
253 253 def samefile(f):
254 254 if f in last.manifest():
255 255 a = last.filectx(f)
256 256 if f in base.manifest():
257 257 b = base.filectx(f)
258 258 return (a.data() == b.data()
259 259 and a.flags() == b.flags())
260 260 else:
261 261 return False
262 262 else:
263 263 return f not in base.manifest()
264 264 files = [f for f in files if not samefile(f)]
265 265 # commit version of these files as defined by head
266 266 headmf = last.manifest()
267 267 def filectxfn(repo, ctx, path):
268 268 if path in headmf:
269 269 fctx = last[path]
270 270 flags = fctx.flags()
271 271 mctx = context.memfilectx(fctx.path(), fctx.data(),
272 272 islink='l' in flags,
273 273 isexec='x' in flags,
274 274 copied=copied.get(path))
275 275 return mctx
276 276 raise IOError()
277 277
278 278 if commitopts.get('message'):
279 279 message = commitopts['message']
280 280 else:
281 281 message = first.description()
282 282 user = commitopts.get('user')
283 283 date = commitopts.get('date')
284 284 extra = commitopts.get('extra')
285 285
286 286 parents = (first.p1().node(), first.p2().node())
287 287 new = context.memctx(repo,
288 288 parents=parents,
289 289 text=message,
290 290 files=files,
291 291 filectxfn=filectxfn,
292 292 user=user,
293 293 date=date,
294 294 extra=extra)
295 295 new._text = cmdutil.commitforceeditor(repo, new, [])
296 296 return repo.commitctx(new)
297 297
298 298 def pick(ui, repo, ctx, ha, opts):
299 299 oldctx = repo[ha]
300 300 if oldctx.parents()[0] == ctx:
301 301 ui.debug('node %s unchanged\n' % ha)
302 302 return oldctx, []
303 303 hg.update(repo, ctx.node())
304 304 stats = applychanges(ui, repo, oldctx, opts)
305 305 if stats and stats[3] > 0:
306 306 raise error.InterventionRequired(_('Fix up the change and run '
307 307 'hg histedit --continue'))
308 308 # drop the second merge parent
309 309 commit = commitfuncfor(repo, oldctx)
310 310 n = commit(text=oldctx.description(), user=oldctx.user(),
311 311 date=oldctx.date(), extra=oldctx.extra())
312 312 if n is None:
313 313 ui.warn(_('%s: empty changeset\n')
314 314 % node.hex(ha))
315 315 return ctx, []
316 316 new = repo[n]
317 317 return new, [(oldctx.node(), (n,))]
318 318
319 319
320 320 def edit(ui, repo, ctx, ha, opts):
321 321 oldctx = repo[ha]
322 322 hg.update(repo, ctx.node())
323 323 applychanges(ui, repo, oldctx, opts)
324 324 raise error.InterventionRequired(
325 325 _('Make changes as needed, you may commit or record as needed now.\n'
326 326 'When you are finished, run hg histedit --continue to resume.'))
327 327
328 328 def fold(ui, repo, ctx, ha, opts):
329 329 oldctx = repo[ha]
330 330 hg.update(repo, ctx.node())
331 331 stats = applychanges(ui, repo, oldctx, opts)
332 332 if stats and stats[3] > 0:
333 333 raise error.InterventionRequired(
334 334 _('Fix up the change and run hg histedit --continue'))
335 335 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(),
336 336 date=oldctx.date(), extra=oldctx.extra())
337 337 if n is None:
338 338 ui.warn(_('%s: empty changeset')
339 339 % node.hex(ha))
340 340 return ctx, []
341 341 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
342 342
343 343 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
344 344 parent = ctx.parents()[0].node()
345 345 hg.update(repo, parent)
346 346 ### prepare new commit data
347 347 commitopts = opts.copy()
348 348 # username
349 349 if ctx.user() == oldctx.user():
350 350 username = ctx.user()
351 351 else:
352 352 username = ui.username()
353 353 commitopts['user'] = username
354 354 # commit message
355 355 newmessage = '\n***\n'.join(
356 356 [ctx.description()] +
357 357 [repo[r].description() for r in internalchanges] +
358 358 [oldctx.description()]) + '\n'
359 359 commitopts['message'] = newmessage
360 360 # date
361 361 commitopts['date'] = max(ctx.date(), oldctx.date())
362 362 extra = ctx.extra().copy()
363 363 # histedit_source
364 364 # note: ctx is likely a temporary commit but that the best we can do here
365 365 # This is sufficient to solve issue3681 anyway
366 366 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
367 367 commitopts['extra'] = extra
368 368 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
369 369 try:
370 370 phasemin = max(ctx.phase(), oldctx.phase())
371 371 repo.ui.setconfig('phases', 'new-commit', phasemin)
372 372 n = collapse(repo, ctx, repo[newnode], commitopts)
373 373 finally:
374 374 repo.ui.restoreconfig(phasebackup)
375 375 if n is None:
376 376 return ctx, []
377 377 hg.update(repo, n)
378 378 replacements = [(oldctx.node(), (newnode,)),
379 379 (ctx.node(), (n,)),
380 380 (newnode, (n,)),
381 381 ]
382 382 for ich in internalchanges:
383 383 replacements.append((ich, (n,)))
384 384 return repo[n], replacements
385 385
386 386 def drop(ui, repo, ctx, ha, opts):
387 387 return ctx, [(repo[ha].node(), ())]
388 388
389 389
390 390 def message(ui, repo, ctx, ha, opts):
391 391 oldctx = repo[ha]
392 392 hg.update(repo, ctx.node())
393 393 stats = applychanges(ui, repo, oldctx, opts)
394 394 if stats and stats[3] > 0:
395 395 raise error.InterventionRequired(
396 396 _('Fix up the change and run hg histedit --continue'))
397 397 message = oldctx.description() + '\n'
398 398 message = ui.edit(message, ui.username())
399 399 commit = commitfuncfor(repo, oldctx)
400 400 new = commit(text=message, user=oldctx.user(), date=oldctx.date(),
401 401 extra=oldctx.extra())
402 402 newctx = repo[new]
403 403 if oldctx.node() != newctx.node():
404 404 return newctx, [(oldctx.node(), (new,))]
405 405 # We didn't make an edit, so just indicate no replaced nodes
406 406 return newctx, []
407 407
408 408 def findoutgoing(ui, repo, remote=None, force=False, opts={}):
409 409 """utility function to find the first outgoing changeset
410 410
411 411 Used by initialisation code"""
412 412 dest = ui.expandpath(remote or 'default-push', remote or 'default')
413 413 dest, revs = hg.parseurl(dest, None)[:2]
414 414 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
415 415
416 416 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
417 417 other = hg.peer(repo, opts, dest)
418 418
419 419 if revs:
420 420 revs = [repo.lookup(rev) for rev in revs]
421 421
422 422 # hexlify nodes from outgoing, because we're going to parse
423 423 # parent[0] using revsingle below, and if the binary hash
424 424 # contains special revset characters like ":" the revset
425 425 # parser can choke.
426 426 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
427 427 if not outgoing.missing:
428 428 raise util.Abort(_('no outgoing ancestors'))
429 429 return outgoing.missing[0]
430 430
431 431 actiontable = {'p': pick,
432 432 'pick': pick,
433 433 'e': edit,
434 434 'edit': edit,
435 435 'f': fold,
436 436 'fold': fold,
437 437 'd': drop,
438 438 'drop': drop,
439 439 'm': message,
440 440 'mess': message,
441 441 }
442 442
443 443 @command('histedit',
444 444 [('', 'commands', '',
445 445 _('Read history edits from the specified file.')),
446 446 ('c', 'continue', False, _('continue an edit already in progress')),
447 447 ('k', 'keep', False,
448 448 _("don't strip old nodes after edit is complete")),
449 449 ('', 'abort', False, _('abort an edit in progress')),
450 450 ('o', 'outgoing', False, _('changesets not found in destination')),
451 451 ('f', 'force', False,
452 452 _('force outgoing even for unrelated repositories')),
453 453 ('r', 'rev', [], _('first revision to be edited'))],
454 454 _("[PARENT]"))
455 455 def histedit(ui, repo, *freeargs, **opts):
456 456 """interactively edit changeset history
457 457 """
458 458 # TODO only abort if we try and histedit mq patches, not just
459 459 # blanket if mq patches are applied somewhere
460 460 mq = getattr(repo, 'mq', None)
461 461 if mq and mq.applied:
462 462 raise util.Abort(_('source has mq patches applied'))
463 463
464 464 # basic argument incompatibility processing
465 465 outg = opts.get('outgoing')
466 466 cont = opts.get('continue')
467 467 abort = opts.get('abort')
468 468 force = opts.get('force')
469 469 rules = opts.get('commands', '')
470 470 revs = opts.get('rev', [])
471 471 goal = 'new' # This invocation goal, in new, continue, abort
472 472 if force and not outg:
473 473 raise util.Abort(_('--force only allowed with --outgoing'))
474 474 if cont:
475 475 if util.any((outg, abort, revs, freeargs, rules)):
476 476 raise util.Abort(_('no arguments allowed with --continue'))
477 477 goal = 'continue'
478 478 elif abort:
479 479 if util.any((outg, revs, freeargs, rules)):
480 480 raise util.Abort(_('no arguments allowed with --abort'))
481 481 goal = 'abort'
482 482 else:
483 483 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
484 484 raise util.Abort(_('history edit already in progress, try '
485 485 '--continue or --abort'))
486 486 if outg:
487 487 if revs:
488 488 raise util.Abort(_('no revisions allowed with --outgoing'))
489 489 if len(freeargs) > 1:
490 490 raise util.Abort(
491 491 _('only one repo argument allowed with --outgoing'))
492 492 else:
493 493 revs.extend(freeargs)
494 494 if len(revs) != 1:
495 495 raise util.Abort(
496 496 _('histedit requires exactly one parent revision'))
497 497
498 498
499 499 if goal == 'continue':
500 500 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
501 501 currentparent, wantnull = repo.dirstate.parents()
502 502 parentctx = repo[parentctxnode]
503 503 parentctx, repl = bootstrapcontinue(ui, repo, parentctx, rules, opts)
504 504 replacements.extend(repl)
505 505 elif goal == 'abort':
506 506 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
507 507 mapping, tmpnodes, leafs, _ntm = processreplacement(repo, replacements)
508 508 ui.debug('restore wc to old parent %s\n' % node.short(topmost))
509 509 hg.clean(repo, topmost)
510 510 cleanupnode(ui, repo, 'created', tmpnodes)
511 511 cleanupnode(ui, repo, 'temp', leafs)
512 512 os.unlink(os.path.join(repo.path, 'histedit-state'))
513 513 return
514 514 else:
515 515 cmdutil.bailifchanged(repo)
516 516
517 517 topmost, empty = repo.dirstate.parents()
518 518 if outg:
519 519 if freeargs:
520 520 remote = freeargs[0]
521 521 else:
522 522 remote = None
523 523 root = findoutgoing(ui, repo, remote, force, opts)
524 524 else:
525 525 root = revs[0]
526 526 root = scmutil.revsingle(repo, root).node()
527 527
528 528 keep = opts.get('keep', False)
529 529 revs = between(repo, root, topmost, keep)
530 530 if not revs:
531 531 raise util.Abort(_('%s is not an ancestor of working directory') %
532 532 node.short(root))
533 533
534 534 ctxs = [repo[r] for r in revs]
535 535 if not rules:
536 536 rules = '\n'.join([makedesc(c) for c in ctxs])
537 537 rules += '\n\n'
538 538 rules += editcomment % (node.short(root), node.short(topmost))
539 539 rules = ui.edit(rules, ui.username())
540 540 # Save edit rules in .hg/histedit-last-edit.txt in case
541 541 # the user needs to ask for help after something
542 542 # surprising happens.
543 543 f = open(repo.join('histedit-last-edit.txt'), 'w')
544 544 f.write(rules)
545 545 f.close()
546 546 else:
547 547 if rules == '-':
548 548 f = sys.stdin
549 549 else:
550 550 f = open(rules)
551 551 rules = f.read()
552 552 f.close()
553 553 rules = [l for l in (r.strip() for r in rules.splitlines())
554 554 if l and not l[0] == '#']
555 555 rules = verifyrules(rules, repo, ctxs)
556 556
557 557 parentctx = repo[root].parents()[0]
558 558 keep = opts.get('keep', False)
559 559 replacements = []
560 560
561 561
562 562 while rules:
563 563 writestate(repo, parentctx.node(), rules, keep, topmost, replacements)
564 564 action, ha = rules.pop(0)
565 565 ui.debug('histedit: processing %s %s\n' % (action, ha))
566 566 actfunc = actiontable[action]
567 567 parentctx, replacement_ = actfunc(ui, repo, parentctx, ha, opts)
568 568 replacements.extend(replacement_)
569 569
570 570 hg.update(repo, parentctx.node())
571 571
572 572 mapping, tmpnodes, created, ntm = processreplacement(repo, replacements)
573 573 if mapping:
574 574 for prec, succs in mapping.iteritems():
575 575 if not succs:
576 576 ui.debug('histedit: %s is dropped\n' % node.short(prec))
577 577 else:
578 578 ui.debug('histedit: %s is replaced by %s\n' % (
579 579 node.short(prec), node.short(succs[0])))
580 580 if len(succs) > 1:
581 581 m = 'histedit: %s'
582 582 for n in succs[1:]:
583 583 ui.debug(m % node.short(n))
584 584
585 585 if not keep:
586 586 if mapping:
587 587 movebookmarks(ui, repo, mapping, topmost, ntm)
588 588 # TODO update mq state
589 589 if obsolete._enabled:
590 590 markers = []
591 591 # sort by revision number because it sound "right"
592 592 for prec in sorted(mapping, key=repo.changelog.rev):
593 593 succs = mapping[prec]
594 594 markers.append((repo[prec],
595 595 tuple(repo[s] for s in succs)))
596 596 if markers:
597 597 obsolete.createmarkers(repo, markers)
598 598 else:
599 599 cleanupnode(ui, repo, 'replaced', mapping)
600 600
601 601 cleanupnode(ui, repo, 'temp', tmpnodes)
602 602 os.unlink(os.path.join(repo.path, 'histedit-state'))
603 603 if os.path.exists(repo.sjoin('undo')):
604 604 os.unlink(repo.sjoin('undo'))
605 605
606 606
607 607 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
608 608 action, currentnode = rules.pop(0)
609 609 ctx = repo[currentnode]
610 610 # is there any new commit between the expected parent and "."
611 611 #
612 612 # note: does not take non linear new change in account (but previous
613 613 # implementation didn't used them anyway (issue3655)
614 614 newchildren = [c.node() for c in repo.set('(%d::.)', parentctx)]
615 615 if parentctx.node() != node.nullid:
616 616 if not newchildren:
617 617 # `parentctxnode` should match but no result. This means that
618 618 # currentnode is not a descendant from parentctxnode.
619 619 msg = _('%s is not an ancestor of working directory')
620 620 hint = _('update to %s or descendant and run "hg histedit '
621 621 '--continue" again') % parentctx
622 622 raise util.Abort(msg % parentctx, hint=hint)
623 623 newchildren.pop(0) # remove parentctxnode
624 624 # Commit dirty working directory if necessary
625 625 new = None
626 626 m, a, r, d = repo.status()[:4]
627 627 if m or a or r or d:
628 628 # prepare the message for the commit to comes
629 629 if action in ('f', 'fold'):
630 630 message = 'fold-temp-revision %s' % currentnode
631 631 else:
632 632 message = ctx.description() + '\n'
633 633 if action in ('e', 'edit', 'm', 'mess'):
634 634 editor = cmdutil.commitforceeditor
635 635 else:
636 636 editor = False
637 637 commit = commitfuncfor(repo, ctx)
638 638 new = commit(text=message, user=ctx.user(),
639 639 date=ctx.date(), extra=ctx.extra(),
640 640 editor=editor)
641 641 if new is not None:
642 642 newchildren.append(new)
643 643
644 644 replacements = []
645 645 # track replacements
646 646 if ctx.node() not in newchildren:
647 647 # note: new children may be empty when the changeset is dropped.
648 648 # this happen e.g during conflicting pick where we revert content
649 649 # to parent.
650 650 replacements.append((ctx.node(), tuple(newchildren)))
651 651
652 652 if action in ('f', 'fold'):
653 653 if newchildren:
654 654 # finalize fold operation if applicable
655 655 if new is None:
656 656 new = newchildren[-1]
657 657 else:
658 658 newchildren.pop() # remove new from internal changes
659 659 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new, opts,
660 660 newchildren)
661 661 replacements.extend(repl)
662 662 else:
663 663 # newchildren is empty if the fold did not result in any commit
664 664 # this happen when all folded change are discarded during the
665 665 # merge.
666 666 replacements.append((ctx.node(), (parentctx.node(),)))
667 667 elif newchildren:
668 668 # otherwise update "parentctx" before proceeding to further operation
669 669 parentctx = repo[newchildren[-1]]
670 670 return parentctx, replacements
671 671
672 672
673 673 def between(repo, old, new, keep):
674 674 """select and validate the set of revision to edit
675 675
676 676 When keep is false, the specified set can't have children."""
677 677 ctxs = list(repo.set('%n::%n', old, new))
678 678 if ctxs and not keep:
679 679 if (not obsolete._enabled and
680 680 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
681 681 raise util.Abort(_('cannot edit history that would orphan nodes'))
682 if repo.revs('(%ld) and merge()', ctxs):
683 raise util.Abort(_('cannot edit history that contains merges'))
682 684 root = ctxs[0] # list is already sorted by repo.set
683 685 if not root.phase():
684 686 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
685 687 return [c.node() for c in ctxs]
686 688
687 689
688 690 def writestate(repo, parentnode, rules, keep, topmost, replacements):
689 691 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
690 692 pickle.dump((parentnode, rules, keep, topmost, replacements), fp)
691 693 fp.close()
692 694
693 695 def readstate(repo):
694 696 """Returns a tuple of (parentnode, rules, keep, topmost, replacements).
695 697 """
696 698 fp = open(os.path.join(repo.path, 'histedit-state'))
697 699 return pickle.load(fp)
698 700
699 701
700 702 def makedesc(c):
701 703 """build a initial action line for a ctx `c`
702 704
703 705 line are in the form:
704 706
705 707 pick <hash> <rev> <summary>
706 708 """
707 709 summary = ''
708 710 if c.description():
709 711 summary = c.description().splitlines()[0]
710 712 line = 'pick %s %d %s' % (c, c.rev(), summary)
711 713 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
712 714
713 715 def verifyrules(rules, repo, ctxs):
714 716 """Verify that there exists exactly one edit rule per given changeset.
715 717
716 718 Will abort if there are to many or too few rules, a malformed rule,
717 719 or a rule on a changeset outside of the user-given range.
718 720 """
719 721 parsed = []
720 722 expected = set(str(c) for c in ctxs)
721 723 seen = set()
722 724 for r in rules:
723 725 if ' ' not in r:
724 726 raise util.Abort(_('malformed line "%s"') % r)
725 727 action, rest = r.split(' ', 1)
726 728 ha = rest.strip().split(' ', 1)[0]
727 729 try:
728 730 ha = str(repo[ha]) # ensure its a short hash
729 731 except error.RepoError:
730 732 raise util.Abort(_('unknown changeset %s listed') % ha)
731 733 if ha not in expected:
732 734 raise util.Abort(
733 735 _('may not use changesets other than the ones listed'))
734 736 if ha in seen:
735 737 raise util.Abort(_('duplicated command for changeset %s') % ha)
736 738 seen.add(ha)
737 739 if action not in actiontable:
738 740 raise util.Abort(_('unknown action "%s"') % action)
739 741 parsed.append([action, ha])
740 742 missing = sorted(expected - seen) # sort to stabilize output
741 743 if missing:
742 744 raise util.Abort(_('missing rules for changeset %s') % missing[0],
743 745 hint=_('do you want to use the drop action?'))
744 746 return parsed
745 747
746 748 def processreplacement(repo, replacements):
747 749 """process the list of replacements to return
748 750
749 751 1) the final mapping between original and created nodes
750 752 2) the list of temporary node created by histedit
751 753 3) the list of new commit created by histedit"""
752 754 allsuccs = set()
753 755 replaced = set()
754 756 fullmapping = {}
755 757 # initialise basic set
756 758 # fullmapping record all operation recorded in replacement
757 759 for rep in replacements:
758 760 allsuccs.update(rep[1])
759 761 replaced.add(rep[0])
760 762 fullmapping.setdefault(rep[0], set()).update(rep[1])
761 763 new = allsuccs - replaced
762 764 tmpnodes = allsuccs & replaced
763 765 # Reduce content fullmapping into direct relation between original nodes
764 766 # and final node created during history edition
765 767 # Dropped changeset are replaced by an empty list
766 768 toproceed = set(fullmapping)
767 769 final = {}
768 770 while toproceed:
769 771 for x in list(toproceed):
770 772 succs = fullmapping[x]
771 773 for s in list(succs):
772 774 if s in toproceed:
773 775 # non final node with unknown closure
774 776 # We can't process this now
775 777 break
776 778 elif s in final:
777 779 # non final node, replace with closure
778 780 succs.remove(s)
779 781 succs.update(final[s])
780 782 else:
781 783 final[x] = succs
782 784 toproceed.remove(x)
783 785 # remove tmpnodes from final mapping
784 786 for n in tmpnodes:
785 787 del final[n]
786 788 # we expect all changes involved in final to exist in the repo
787 789 # turn `final` into list (topologically sorted)
788 790 nm = repo.changelog.nodemap
789 791 for prec, succs in final.items():
790 792 final[prec] = sorted(succs, key=nm.get)
791 793
792 794 # computed topmost element (necessary for bookmark)
793 795 if new:
794 796 newtopmost = sorted(new, key=repo.changelog.rev)[-1]
795 797 elif not final:
796 798 # Nothing rewritten at all. we won't need `newtopmost`
797 799 # It is the same as `oldtopmost` and `processreplacement` know it
798 800 newtopmost = None
799 801 else:
800 802 # every body died. The newtopmost is the parent of the root.
801 803 newtopmost = repo[sorted(final, key=repo.changelog.rev)[0]].p1().node()
802 804
803 805 return final, tmpnodes, new, newtopmost
804 806
805 807 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
806 808 """Move bookmark from old to newly created node"""
807 809 if not mapping:
808 810 # if nothing got rewritten there is not purpose for this function
809 811 return
810 812 moves = []
811 813 for bk, old in sorted(repo._bookmarks.iteritems()):
812 814 if old == oldtopmost:
813 815 # special case ensure bookmark stay on tip.
814 816 #
815 817 # This is arguably a feature and we may only want that for the
816 818 # active bookmark. But the behavior is kept compatible with the old
817 819 # version for now.
818 820 moves.append((bk, newtopmost))
819 821 continue
820 822 base = old
821 823 new = mapping.get(base, None)
822 824 if new is None:
823 825 continue
824 826 while not new:
825 827 # base is killed, trying with parent
826 828 base = repo[base].p1().node()
827 829 new = mapping.get(base, (base,))
828 830 # nothing to move
829 831 moves.append((bk, new[-1]))
830 832 if moves:
831 833 marks = repo._bookmarks
832 834 for mark, new in moves:
833 835 old = marks[mark]
834 836 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
835 837 % (mark, node.short(old), node.short(new)))
836 838 marks[mark] = new
837 839 marks.write()
838 840
839 841 def cleanupnode(ui, repo, name, nodes):
840 842 """strip a group of nodes from the repository
841 843
842 844 The set of node to strip may contains unknown nodes."""
843 845 ui.debug('should strip %s nodes %s\n' %
844 846 (name, ', '.join([node.short(n) for n in nodes])))
845 847 lock = None
846 848 try:
847 849 lock = repo.lock()
848 850 # Find all node that need to be stripped
849 851 # (we hg %lr instead of %ln to silently ignore unknown item
850 852 nm = repo.changelog.nodemap
851 853 nodes = [n for n in nodes if n in nm]
852 854 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
853 855 for c in roots:
854 856 # We should process node in reverse order to strip tip most first.
855 857 # but this trigger a bug in changegroup hook.
856 858 # This would reduce bundle overhead
857 859 repair.strip(ui, repo, c)
858 860 finally:
859 861 lockmod.release(lock)
860 862
861 863 def summaryhook(ui, repo):
862 864 if not os.path.exists(repo.join('histedit-state')):
863 865 return
864 866 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
865 867 if rules:
866 868 # i18n: column positioning for "hg summary"
867 869 ui.write(_('hist: %s (histedit --continue)\n') %
868 870 (ui.label(_('%d remaining'), 'histedit.remaining') %
869 871 len(rules)))
870 872
871 873 def extsetup(ui):
872 874 cmdutil.summaryhooks.add('histedit', summaryhook)
@@ -1,444 +1,459 b''
1 1 $ . "$TESTDIR/histedit-helpers.sh"
2 2
3 3 Enable obsolete
4 4
5 5 $ cat > ${TESTTMP}/obs.py << EOF
6 6 > import mercurial.obsolete
7 7 > mercurial.obsolete._enabled = True
8 8 > EOF
9 9
10 10 $ cat >> $HGRCPATH << EOF
11 11 > [ui]
12 12 > logtemplate= {rev}:{node|short} {desc|firstline}
13 13 > [phases]
14 14 > publish=False
15 15 > [extensions]'
16 16 > histedit=
17 17 > rebase=
18 18 >
19 19 > obs=${TESTTMP}/obs.py
20 20 > EOF
21 21
22 22 $ hg init base
23 23 $ cd base
24 24
25 25 $ for x in a b c d e f ; do
26 26 > echo $x > $x
27 27 > hg add $x
28 28 > hg ci -m $x
29 29 > done
30 30
31 31 $ hg log --graph
32 32 @ 5:652413bf663e f
33 33 |
34 34 o 4:e860deea161a e
35 35 |
36 36 o 3:055a42cdd887 d
37 37 |
38 38 o 2:177f92b77385 c
39 39 |
40 40 o 1:d2ae7f538514 b
41 41 |
42 42 o 0:cb9a9f314b8b a
43 43
44 44
45 45 $ HGEDITOR=cat hg histedit 1
46 46 pick d2ae7f538514 1 b
47 47 pick 177f92b77385 2 c
48 48 pick 055a42cdd887 3 d
49 49 pick e860deea161a 4 e
50 50 pick 652413bf663e 5 f
51 51
52 52 # Edit history between d2ae7f538514 and 652413bf663e
53 53 #
54 54 # Commands:
55 55 # p, pick = use commit
56 56 # e, edit = use commit, but stop for amending
57 57 # f, fold = use commit, but fold into previous commit (combines N and N-1)
58 58 # d, drop = remove commit from history
59 59 # m, mess = edit message without changing commit content
60 60 #
61 61 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
62 62 $ hg histedit 1 --commands - --verbose <<EOF | grep histedit
63 63 > pick 177f92b77385 2 c
64 64 > drop d2ae7f538514 1 b
65 65 > pick 055a42cdd887 3 d
66 66 > fold e860deea161a 4 e
67 67 > pick 652413bf663e 5 f
68 68 > EOF
69 69 saved backup bundle to $TESTTMP/base/.hg/strip-backup/96e494a2d553-backup.hg (glob)
70 70 $ hg log --graph --hidden
71 71 @ 8:cacdfd884a93 f
72 72 |
73 73 o 7:59d9f330561f d
74 74 |
75 75 o 6:b346ab9a313d c
76 76 |
77 77 | x 5:652413bf663e f
78 78 | |
79 79 | x 4:e860deea161a e
80 80 | |
81 81 | x 3:055a42cdd887 d
82 82 | |
83 83 | x 2:177f92b77385 c
84 84 | |
85 85 | x 1:d2ae7f538514 b
86 86 |/
87 87 o 0:cb9a9f314b8b a
88 88
89 89 $ hg debugobsolete
90 90 d2ae7f538514cd87c17547b0de4cea71fe1af9fb 0 {'date': '* *', 'user': 'test'} (glob)
91 91 177f92b773850b59254aa5e923436f921b55483b b346ab9a313db8537ecf96fca3ca3ca984ef3bd7 0 {'date': '* *', 'user': 'test'} (glob)
92 92 055a42cdd88768532f9cf79daa407fc8d138de9b 59d9f330561fd6c88b1a6b32f0e45034d88db784 0 {'date': '* *', 'user': 'test'} (glob)
93 93 e860deea161a2f77de56603b340ebbb4536308ae 59d9f330561fd6c88b1a6b32f0e45034d88db784 0 {'date': '* *', 'user': 'test'} (glob)
94 94 652413bf663ef2a641cab26574e46d5f5a64a55a cacdfd884a9321ec4e1de275ef3949fa953a1f83 0 {'date': '* *', 'user': 'test'} (glob)
95 95
96 96
97 97 Ensure hidden revision does not prevent histedit
98 98 -------------------------------------------------
99 99
100 100 create an hidden revision
101 101
102 102 $ hg histedit 6 --commands - << EOF
103 103 > pick b346ab9a313d 6 c
104 104 > drop 59d9f330561f 7 d
105 105 > pick cacdfd884a93 8 f
106 106 > EOF
107 107 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
108 108 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
109 109 $ hg log --graph
110 110 @ 9:c13eb81022ca f
111 111 |
112 112 o 6:b346ab9a313d c
113 113 |
114 114 o 0:cb9a9f314b8b a
115 115
116 116 check hidden revision are ignored (6 have hidden children 7 and 8)
117 117
118 118 $ hg histedit 6 --commands - << EOF
119 119 > pick b346ab9a313d 6 c
120 120 > pick c13eb81022ca 8 f
121 121 > EOF
122 122 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
123 123
124 124
125 125
126 126 Test that rewriting leaving instability behind is allowed
127 127 ---------------------------------------------------------------------
128 128
129 129 $ hg up '.^'
130 130 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
131 131 $ hg log -r 'children(.)'
132 132 9:c13eb81022ca f (no-eol)
133 133 $ hg histedit -r '.' --commands - <<EOF
134 134 > edit b346ab9a313d 6 c
135 135 > EOF
136 136 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
137 137 adding c
138 138 Make changes as needed, you may commit or record as needed now.
139 139 When you are finished, run hg histedit --continue to resume.
140 140 [1]
141 141 $ echo c >> c
142 142 $ hg histedit --continue
143 143 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
144 144
145 145 $ hg log -r 'unstable()'
146 146 9:c13eb81022ca f (no-eol)
147 147
148 148 stabilise
149 149
150 150 $ hg rebase -r 'unstable()' -d .
151 151
152 152 Test dropping of changeset on the top of the stack
153 153 -------------------------------------------------------
154 154
155 155 Nothing is rewritten below, the working directory parent must be change for the
156 156 dropped changeset to be hidden.
157 157
158 158 $ cd ..
159 159 $ hg clone base droplast
160 160 updating to branch default
161 161 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
162 162 $ cd droplast
163 163 $ hg histedit -r '40db8afa467b' --commands - << EOF
164 164 > pick 40db8afa467b 10 c
165 165 > drop b449568bf7fc 11 f
166 166 > EOF
167 167 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
168 168 $ hg log -G
169 169 @ 10:40db8afa467b c
170 170 |
171 171 o 0:cb9a9f314b8b a
172 172
173 173
174 174 With rewritten ancestors
175 175
176 176 $ echo e > e
177 177 $ hg add e
178 178 $ hg commit -m g
179 179 $ echo f > f
180 180 $ hg add f
181 181 $ hg commit -m h
182 182 $ hg histedit -r '40db8afa467b' --commands - << EOF
183 183 > pick 47a8561c0449 12 g
184 184 > pick 40db8afa467b 10 c
185 185 > drop 1b3b05f35ff0 13 h
186 186 > EOF
187 187 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
188 188 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
189 189 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
190 190 $ hg log -G
191 191 @ 15:ee6544123ab8 c
192 192 |
193 193 o 14:269e713e9eae g
194 194 |
195 195 o 0:cb9a9f314b8b a
196 196
197 197 $ cd ../base
198 198
199 199
200 200
201 201 Test phases support
202 202 ===========================================
203 203
204 204 Check that histedit respect immutability
205 205 -------------------------------------------
206 206
207 207 $ cat >> $HGRCPATH << EOF
208 208 > [ui]
209 209 > logtemplate= {rev}:{node|short} ({phase}) {desc|firstline}\n
210 210 > EOF
211 211
212 212 $ hg ph -pv '.^'
213 213 phase changed for 2 changesets
214 214 $ hg log -G
215 215 @ 11:b449568bf7fc (draft) f
216 216 |
217 217 o 10:40db8afa467b (public) c
218 218 |
219 219 o 0:cb9a9f314b8b (public) a
220 220
221 221 $ hg histedit -r '.~2'
222 222 abort: cannot edit immutable changeset: cb9a9f314b8b
223 223 [255]
224 224
225 225
226 226 Prepare further testing
227 227 -------------------------------------------
228 228
229 229 $ for x in g h i j k ; do
230 230 > echo $x > $x
231 231 > hg add $x
232 232 > hg ci -m $x
233 233 > done
234 234 $ hg phase --force --secret .~2
235 235 $ hg log -G
236 236 @ 16:ee118ab9fa44 (secret) k
237 237 |
238 238 o 15:3a6c53ee7f3d (secret) j
239 239 |
240 240 o 14:b605fb7503f2 (secret) i
241 241 |
242 242 o 13:7395e1ff83bd (draft) h
243 243 |
244 244 o 12:6b70183d2492 (draft) g
245 245 |
246 246 o 11:b449568bf7fc (draft) f
247 247 |
248 248 o 10:40db8afa467b (public) c
249 249 |
250 250 o 0:cb9a9f314b8b (public) a
251 251
252 252 $ cd ..
253 253
254 254 simple phase conservation
255 255 -------------------------------------------
256 256
257 257 Resulting changeset should conserve the phase of the original one whatever the
258 258 phases.new-commit option is.
259 259
260 260 New-commit as draft (default)
261 261
262 262 $ cp -r base simple-draft
263 263 $ cd simple-draft
264 264 $ hg histedit -r 'b449568bf7fc' --commands - << EOF
265 265 > edit b449568bf7fc 11 f
266 266 > pick 6b70183d2492 12 g
267 267 > pick 7395e1ff83bd 13 h
268 268 > pick b605fb7503f2 14 i
269 269 > pick 3a6c53ee7f3d 15 j
270 270 > pick ee118ab9fa44 16 k
271 271 > EOF
272 272 0 files updated, 0 files merged, 6 files removed, 0 files unresolved
273 273 adding f
274 274 Make changes as needed, you may commit or record as needed now.
275 275 When you are finished, run hg histedit --continue to resume.
276 276 [1]
277 277 $ echo f >> f
278 278 $ hg histedit --continue
279 279 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
280 280 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
281 281 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
282 282 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
283 283 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
284 284 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
285 285 $ hg log -G
286 286 @ 22:12e89af74238 (secret) k
287 287 |
288 288 o 21:636a8687b22e (secret) j
289 289 |
290 290 o 20:ccaf0a38653f (secret) i
291 291 |
292 292 o 19:11a89d1c2613 (draft) h
293 293 |
294 294 o 18:c1dec7ca82ea (draft) g
295 295 |
296 296 o 17:087281e68428 (draft) f
297 297 |
298 298 o 10:40db8afa467b (public) c
299 299 |
300 300 o 0:cb9a9f314b8b (public) a
301 301
302 302 $ cd ..
303 303
304 304
305 305 New-commit as draft (default)
306 306
307 307 $ cp -r base simple-secret
308 308 $ cd simple-secret
309 309 $ cat >> .hg/hgrc << EOF
310 310 > [phases]
311 311 > new-commit=secret
312 312 > EOF
313 313 $ hg histedit -r 'b449568bf7fc' --commands - << EOF
314 314 > edit b449568bf7fc 11 f
315 315 > pick 6b70183d2492 12 g
316 316 > pick 7395e1ff83bd 13 h
317 317 > pick b605fb7503f2 14 i
318 318 > pick 3a6c53ee7f3d 15 j
319 319 > pick ee118ab9fa44 16 k
320 320 > EOF
321 321 0 files updated, 0 files merged, 6 files removed, 0 files unresolved
322 322 adding f
323 323 Make changes as needed, you may commit or record as needed now.
324 324 When you are finished, run hg histedit --continue to resume.
325 325 [1]
326 326 $ echo f >> f
327 327 $ hg histedit --continue
328 328 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
329 329 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
330 330 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
331 331 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
332 332 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
333 333 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
334 334 $ hg log -G
335 335 @ 22:12e89af74238 (secret) k
336 336 |
337 337 o 21:636a8687b22e (secret) j
338 338 |
339 339 o 20:ccaf0a38653f (secret) i
340 340 |
341 341 o 19:11a89d1c2613 (draft) h
342 342 |
343 343 o 18:c1dec7ca82ea (draft) g
344 344 |
345 345 o 17:087281e68428 (draft) f
346 346 |
347 347 o 10:40db8afa467b (public) c
348 348 |
349 349 o 0:cb9a9f314b8b (public) a
350 350
351 351 $ cd ..
352 352
353 353
354 354 Changeset reordering
355 355 -------------------------------------------
356 356
357 357 If a secret changeset is put before a draft one, all descendant should be secret.
358 358 It seems more important to present the secret phase.
359 359
360 360 $ cp -r base reorder
361 361 $ cd reorder
362 362 $ hg histedit -r 'b449568bf7fc' --commands - << EOF
363 363 > pick b449568bf7fc 11 f
364 364 > pick 3a6c53ee7f3d 15 j
365 365 > pick 6b70183d2492 12 g
366 366 > pick b605fb7503f2 14 i
367 367 > pick 7395e1ff83bd 13 h
368 368 > pick ee118ab9fa44 16 k
369 369 > EOF
370 370 0 files updated, 0 files merged, 5 files removed, 0 files unresolved
371 371 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
372 372 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
373 373 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
374 374 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
375 375 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
376 376 $ hg log -G
377 377 @ 21:558246857888 (secret) k
378 378 |
379 379 o 20:28bd44768535 (secret) h
380 380 |
381 381 o 19:d5395202aeb9 (secret) i
382 382 |
383 383 o 18:21edda8e341b (secret) g
384 384 |
385 385 o 17:5ab64f3a4832 (secret) j
386 386 |
387 387 o 11:b449568bf7fc (draft) f
388 388 |
389 389 o 10:40db8afa467b (public) c
390 390 |
391 391 o 0:cb9a9f314b8b (public) a
392 392
393 393 $ cd ..
394 394
395 395 Changeset folding
396 396 -------------------------------------------
397 397
398 398 Folding a secret changeset with a draft one turn the result secret (again,
399 399 better safe than sorry). Folding between same phase changeset still works
400 400
401 401 Note that there is a few reordering in this series for more extensive test
402 402
403 403 $ cp -r base folding
404 404 $ cd folding
405 405 $ cat >> .hg/hgrc << EOF
406 406 > [phases]
407 407 > new-commit=secret
408 408 > EOF
409 409 $ hg histedit -r 'b449568bf7fc' --commands - << EOF
410 410 > pick 7395e1ff83bd 13 h
411 411 > fold b449568bf7fc 11 f
412 412 > pick 6b70183d2492 12 g
413 413 > fold 3a6c53ee7f3d 15 j
414 414 > pick b605fb7503f2 14 i
415 415 > fold ee118ab9fa44 16 k
416 416 > EOF
417 417 0 files updated, 0 files merged, 6 files removed, 0 files unresolved
418 418 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
419 419 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
420 420 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
421 421 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
422 422 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
423 423 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
424 424 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
425 425 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
426 426 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
427 427 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
428 428 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
429 429 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
430 430 saved backup bundle to $TESTTMP/folding/.hg/strip-backup/58019c66f35f-backup.hg (glob)
431 431 saved backup bundle to $TESTTMP/folding/.hg/strip-backup/83d1858e070b-backup.hg (glob)
432 432 saved backup bundle to $TESTTMP/folding/.hg/strip-backup/859969f5ed7e-backup.hg (glob)
433 433 $ hg log -G
434 434 @ 19:f9daec13fb98 (secret) i
435 435 |
436 436 o 18:49807617f46a (secret) g
437 437 |
438 438 o 17:050280826e04 (draft) h
439 439 |
440 440 o 10:40db8afa467b (public) c
441 441 |
442 442 o 0:cb9a9f314b8b (public) a
443 443
444 $ hg co 18
445 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
446 $ echo wat >> wat
447 $ hg add wat
448 $ hg ci -m 'add wat'
449 created new head
450 $ hg merge 19
451 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
452 (branch merge, don't forget to commit)
453 $ hg ci -m 'merge'
454 $ echo not wat > wat
455 $ hg ci -m 'modify wat'
456 $ hg histedit 17
457 abort: cannot edit history that contains merges
458 [255]
444 459 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now