##// END OF EJS Templates
histedit: update bookmark movement notice...
Pierre-Yves David -
r17750:bb6149f1 default
parent child Browse files
Show More
@@ -1,742 +1,740 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 633536316234 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 633536316234 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 example repository above and add three more changes, such that
112 112 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 except ImportError:
144 144 import pickle
145 145 import os
146 146
147 147 from mercurial import bookmarks
148 148 from mercurial import cmdutil
149 149 from mercurial import discovery
150 150 from mercurial import error
151 151 from mercurial import copies
152 152 from mercurial import context
153 153 from mercurial import hg
154 154 from mercurial import lock as lockmod
155 155 from mercurial import node
156 156 from mercurial import repair
157 157 from mercurial import scmutil
158 158 from mercurial import util
159 159 from mercurial import merge as mergemod
160 160 from mercurial.i18n import _
161 161
162 162 cmdtable = {}
163 163 command = cmdutil.command(cmdtable)
164 164
165 165 testedwith = 'internal'
166 166
167 167 # i18n: command names and abbreviations must remain untranslated
168 168 editcomment = _("""# Edit history between %s and %s
169 169 #
170 170 # Commands:
171 171 # p, pick = use commit
172 172 # e, edit = use commit, but stop for amending
173 173 # f, fold = use commit, but fold into previous commit (combines N and N-1)
174 174 # d, drop = remove commit from history
175 175 # m, mess = edit message without changing commit content
176 176 #
177 177 """)
178 178
179 179 def applychanges(ui, repo, ctx, opts):
180 180 """Merge changeset from ctx (only) in the current working directory"""
181 181 wcpar = repo.dirstate.parents()[0]
182 182 if ctx.p1().node() == wcpar:
183 183 # edition ar "in place" we do not need to make any merge,
184 184 # just applies changes on parent for edition
185 185 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
186 186 stats = None
187 187 else:
188 188 try:
189 189 # ui.forcemerge is an internal variable, do not document
190 190 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''))
191 191 stats = mergemod.update(repo, ctx.node(), True, True, False,
192 192 ctx.p1().node())
193 193 finally:
194 194 repo.ui.setconfig('ui', 'forcemerge', '')
195 195 repo.setparents(wcpar, node.nullid)
196 196 repo.dirstate.write()
197 197 # fix up dirstate for copies and renames
198 198 cmdutil.duplicatecopies(repo, ctx.rev(), ctx.p1().rev())
199 199 return stats
200 200
201 201 def collapse(repo, first, last, commitopts):
202 202 """collapse the set of revisions from first to last as new one.
203 203
204 204 Expected commit options are:
205 205 - message
206 206 - date
207 207 - username
208 208 Edition of commit message is trigered in all case.
209 209
210 210 This function works in memory."""
211 211 ctxs = list(repo.set('%d::%d', first, last))
212 212 if not ctxs:
213 213 return None
214 214 base = first.parents()[0]
215 215
216 216 # commit a new version of the old changeset, including the update
217 217 # collect all files which might be affected
218 218 files = set()
219 219 for ctx in ctxs:
220 220 files.update(ctx.files())
221 221
222 222 # Recompute copies (avoid recording a -> b -> a)
223 223 copied = copies.pathcopies(first, last)
224 224
225 225 # prune files which were reverted by the updates
226 226 def samefile(f):
227 227 if f in last.manifest():
228 228 a = last.filectx(f)
229 229 if f in base.manifest():
230 230 b = base.filectx(f)
231 231 return (a.data() == b.data()
232 232 and a.flags() == b.flags())
233 233 else:
234 234 return False
235 235 else:
236 236 return f not in base.manifest()
237 237 files = [f for f in files if not samefile(f)]
238 238 # commit version of these files as defined by head
239 239 headmf = last.manifest()
240 240 def filectxfn(repo, ctx, path):
241 241 if path in headmf:
242 242 fctx = last[path]
243 243 flags = fctx.flags()
244 244 mctx = context.memfilectx(fctx.path(), fctx.data(),
245 245 islink='l' in flags,
246 246 isexec='x' in flags,
247 247 copied=copied.get(path))
248 248 return mctx
249 249 raise IOError()
250 250
251 251 if commitopts.get('message'):
252 252 message = commitopts['message']
253 253 else:
254 254 message = first.description()
255 255 user = commitopts.get('user')
256 256 date = commitopts.get('date')
257 257 extra = first.extra()
258 258
259 259 parents = (first.p1().node(), first.p2().node())
260 260 new = context.memctx(repo,
261 261 parents=parents,
262 262 text=message,
263 263 files=files,
264 264 filectxfn=filectxfn,
265 265 user=user,
266 266 date=date,
267 267 extra=extra)
268 268 new._text = cmdutil.commitforceeditor(repo, new, [])
269 269 return repo.commitctx(new)
270 270
271 271 def pick(ui, repo, ctx, ha, opts):
272 272 oldctx = repo[ha]
273 273 if oldctx.parents()[0] == ctx:
274 274 ui.debug('node %s unchanged\n' % ha)
275 275 return oldctx, [], [], []
276 276 hg.update(repo, ctx.node())
277 277 stats = applychanges(ui, repo, oldctx, opts)
278 278 if stats and stats[3] > 0:
279 279 raise util.Abort(_('Fix up the change and run '
280 280 'hg histedit --continue'))
281 281 # drop the second merge parent
282 282 n = repo.commit(text=oldctx.description(), user=oldctx.user(),
283 283 date=oldctx.date(), extra=oldctx.extra())
284 284 if n is None:
285 285 ui.warn(_('%s: empty changeset\n')
286 286 % node.hex(ha))
287 287 return ctx, [], [], []
288 288 return repo[n], [n], [oldctx.node()], []
289 289
290 290
291 291 def edit(ui, repo, ctx, ha, opts):
292 292 oldctx = repo[ha]
293 293 hg.update(repo, ctx.node())
294 294 applychanges(ui, repo, oldctx, opts)
295 295 raise util.Abort(_('Make changes as needed, you may commit or record as '
296 296 'needed now.\nWhen you are finished, run hg'
297 297 ' histedit --continue to resume.'))
298 298
299 299 def fold(ui, repo, ctx, ha, opts):
300 300 oldctx = repo[ha]
301 301 hg.update(repo, ctx.node())
302 302 stats = applychanges(ui, repo, oldctx, opts)
303 303 if stats and stats[3] > 0:
304 304 raise util.Abort(_('Fix up the change and run '
305 305 'hg histedit --continue'))
306 306 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(),
307 307 date=oldctx.date(), extra=oldctx.extra())
308 308 if n is None:
309 309 ui.warn(_('%s: empty changeset')
310 310 % node.hex(ha))
311 311 return ctx, [], [], []
312 312 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
313 313
314 314 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
315 315 parent = ctx.parents()[0].node()
316 316 hg.update(repo, parent)
317 317 ### prepare new commit data
318 318 commitopts = opts.copy()
319 319 # username
320 320 if ctx.user() == oldctx.user():
321 321 username = ctx.user()
322 322 else:
323 323 username = ui.username()
324 324 commitopts['user'] = username
325 325 # commit message
326 326 newmessage = '\n***\n'.join(
327 327 [ctx.description()] +
328 328 [repo[r].description() for r in internalchanges] +
329 329 [oldctx.description()]) + '\n'
330 330 commitopts['message'] = newmessage
331 331 # date
332 332 commitopts['date'] = max(ctx.date(), oldctx.date())
333 333 n = collapse(repo, ctx, repo[newnode], commitopts)
334 334 if n is None:
335 335 return ctx, [], [], []
336 336 hg.update(repo, n)
337 337 return repo[n], [n], [oldctx.node(), ctx.node()], [newnode]
338 338
339 339 def drop(ui, repo, ctx, ha, opts):
340 340 return ctx, [], [repo[ha].node()], []
341 341
342 342
343 343 def message(ui, repo, ctx, ha, opts):
344 344 oldctx = repo[ha]
345 345 hg.update(repo, ctx.node())
346 346 stats = applychanges(ui, repo, oldctx, opts)
347 347 if stats and stats[3] > 0:
348 348 raise util.Abort(_('Fix up the change and run '
349 349 'hg histedit --continue'))
350 350 message = oldctx.description() + '\n'
351 351 message = ui.edit(message, ui.username())
352 352 new = repo.commit(text=message, user=oldctx.user(), date=oldctx.date(),
353 353 extra=oldctx.extra())
354 354 newctx = repo[new]
355 355 if oldctx.node() != newctx.node():
356 356 return newctx, [new], [oldctx.node()], []
357 357 # We didn't make an edit, so just indicate no replaced nodes
358 358 return newctx, [new], [], []
359 359
360 360 actiontable = {'p': pick,
361 361 'pick': pick,
362 362 'e': edit,
363 363 'edit': edit,
364 364 'f': fold,
365 365 'fold': fold,
366 366 'd': drop,
367 367 'drop': drop,
368 368 'm': message,
369 369 'mess': message,
370 370 }
371 371
372 372 @command('histedit',
373 373 [('', 'commands', '',
374 374 _('Read history edits from the specified file.')),
375 375 ('c', 'continue', False, _('continue an edit already in progress')),
376 376 ('k', 'keep', False,
377 377 _("don't strip old nodes after edit is complete")),
378 378 ('', 'abort', False, _('abort an edit in progress')),
379 379 ('o', 'outgoing', False, _('changesets not found in destination')),
380 380 ('f', 'force', False,
381 381 _('force outgoing even for unrelated repositories')),
382 382 ('r', 'rev', [], _('first revision to be edited'))],
383 383 _("[PARENT]"))
384 384 def histedit(ui, repo, *parent, **opts):
385 385 """interactively edit changeset history
386 386 """
387 387 # TODO only abort if we try and histedit mq patches, not just
388 388 # blanket if mq patches are applied somewhere
389 389 mq = getattr(repo, 'mq', None)
390 390 if mq and mq.applied:
391 391 raise util.Abort(_('source has mq patches applied'))
392 392
393 393 parent = list(parent) + opts.get('rev', [])
394 394 if opts.get('outgoing'):
395 395 if len(parent) > 1:
396 396 raise util.Abort(
397 397 _('only one repo argument allowed with --outgoing'))
398 398 elif parent:
399 399 parent = parent[0]
400 400
401 401 dest = ui.expandpath(parent or 'default-push', parent or 'default')
402 402 dest, revs = hg.parseurl(dest, None)[:2]
403 403 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
404 404
405 405 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
406 406 other = hg.peer(repo, opts, dest)
407 407
408 408 if revs:
409 409 revs = [repo.lookup(rev) for rev in revs]
410 410
411 411 parent = discovery.findcommonoutgoing(
412 412 repo, other, [], force=opts.get('force')).missing[0:1]
413 413 else:
414 414 if opts.get('force'):
415 415 raise util.Abort(_('--force only allowed with --outgoing'))
416 416
417 417 if opts.get('continue', False):
418 418 if len(parent) != 0:
419 419 raise util.Abort(_('no arguments allowed with --continue'))
420 420 (parentctxnode, created, replaced, tmpnodes,
421 421 existing, rules, keep, topmost, replacemap) = readstate(repo)
422 422 parentctx = repo[parentctxnode]
423 423 existing = set(existing)
424 424 parentctx = bootstrapcontinue(ui, repo, parentctx, existing,
425 425 replacemap, rules, tmpnodes, created,
426 426 replaced, opts)
427 427 elif opts.get('abort', False):
428 428 if len(parent) != 0:
429 429 raise util.Abort(_('no arguments allowed with --abort'))
430 430 (parentctxnode, created, replaced, tmpnodes,
431 431 existing, rules, keep, topmost, replacemap) = readstate(repo)
432 432 ui.debug('restore wc to old parent %s\n' % node.short(topmost))
433 433 hg.clean(repo, topmost)
434 434 cleanupnode(ui, repo, 'created', created)
435 435 cleanupnode(ui, repo, 'temp', tmpnodes)
436 436 os.unlink(os.path.join(repo.path, 'histedit-state'))
437 437 return
438 438 else:
439 439 cmdutil.bailifchanged(repo)
440 440 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
441 441 raise util.Abort(_('history edit already in progress, try '
442 442 '--continue or --abort'))
443 443
444 444 topmost, empty = repo.dirstate.parents()
445 445
446 446
447 447 if len(parent) != 1:
448 448 raise util.Abort(_('histedit requires exactly one parent revision'))
449 449 parent = scmutil.revsingle(repo, parent[0]).node()
450 450
451 451 keep = opts.get('keep', False)
452 452 revs = between(repo, parent, topmost, keep)
453 453
454 454 ctxs = [repo[r] for r in revs]
455 455 existing = [r.node() for r in ctxs]
456 456 rules = opts.get('commands', '')
457 457 if not rules:
458 458 rules = '\n'.join([makedesc(c) for c in ctxs])
459 459 rules += '\n\n'
460 460 rules += editcomment % (node.short(parent), node.short(topmost))
461 461 rules = ui.edit(rules, ui.username())
462 462 # Save edit rules in .hg/histedit-last-edit.txt in case
463 463 # the user needs to ask for help after something
464 464 # surprising happens.
465 465 f = open(repo.join('histedit-last-edit.txt'), 'w')
466 466 f.write(rules)
467 467 f.close()
468 468 else:
469 469 f = open(rules)
470 470 rules = f.read()
471 471 f.close()
472 472 rules = [l for l in (r.strip() for r in rules.splitlines())
473 473 if l and not l[0] == '#']
474 474 rules = verifyrules(rules, repo, ctxs)
475 475
476 476 parentctx = repo[parent].parents()[0]
477 477 keep = opts.get('keep', False)
478 478 replaced = []
479 479 replacemap = {}
480 480 tmpnodes = []
481 481 created = []
482 482
483 483
484 484 while rules:
485 485 writestate(repo, parentctx.node(), created, replaced,
486 486 tmpnodes, existing, rules, keep, topmost, replacemap)
487 487 action, ha = rules.pop(0)
488 488 ui.debug('histedit: processing %s %s\n' % (action, ha))
489 489 (parentctx, created_, replaced_, tmpnodes_) = actiontable[action](
490 490 ui, repo, parentctx, ha, opts)
491 491
492 492 if replaced_:
493 493 clen, rlen = len(created_), len(replaced_)
494 494 if clen == rlen == 1:
495 495 ui.debug('histedit: exact replacement of %s with %s\n' % (
496 496 node.short(replaced_[0]), node.short(created_[0])))
497 497
498 498 replacemap[replaced_[0]] = created_[0]
499 499 elif clen > rlen:
500 500 assert rlen == 1, ('unexpected replacement of '
501 501 '%d changes with %d changes' % (rlen, clen))
502 502 # made more changesets than we're replacing
503 503 # TODO synthesize patch names for created patches
504 504 replacemap[replaced_[0]] = created_[-1]
505 505 ui.debug('histedit: created many, assuming %s replaced by %s' %
506 506 (node.short(replaced_[0]), node.short(created_[-1])))
507 507 elif rlen > clen:
508 508 if not created_:
509 509 # This must be a drop. Try and put our metadata on
510 510 # the parent change.
511 511 assert rlen == 1
512 512 r = replaced_[0]
513 513 ui.debug('histedit: %s seems replaced with nothing, '
514 514 'finding a parent\n' % (node.short(r)))
515 515 pctx = repo[r].parents()[0]
516 516 if pctx.node() in replacemap:
517 517 ui.debug('histedit: parent is already replaced\n')
518 518 replacemap[r] = replacemap[pctx.node()]
519 519 else:
520 520 replacemap[r] = pctx.node()
521 521 ui.debug('histedit: %s best replaced by %s\n' % (
522 522 node.short(r), node.short(replacemap[r])))
523 523 else:
524 524 assert len(created_) == 1
525 525 for r in replaced_:
526 526 ui.debug('histedit: %s replaced by %s\n' % (
527 527 node.short(r), node.short(created_[0])))
528 528 replacemap[r] = created_[0]
529 529 else:
530 530 assert False, (
531 531 'Unhandled case in replacement mapping! '
532 532 'replacing %d changes with %d changes' % (rlen, clen))
533 533 created.extend(created_)
534 534 replaced.extend(replaced_)
535 535 tmpnodes.extend(tmpnodes_)
536 536
537 537 hg.update(repo, parentctx.node())
538 538
539 539 if not keep:
540 540 if replacemap:
541 541 movebookmarks(ui, repo, replacemap, tmpnodes, created)
542 542 # TODO update mq state
543 543 cleanupnode(ui, repo, 'replaced', replaced)
544 544
545 545 cleanupnode(ui, repo, 'temp', tmpnodes)
546 546 os.unlink(os.path.join(repo.path, 'histedit-state'))
547 547 if os.path.exists(repo.sjoin('undo')):
548 548 os.unlink(repo.sjoin('undo'))
549 549
550 550
551 551 def bootstrapcontinue(ui, repo, parentctx, existing, replacemap, rules,
552 552 tmpnodes, created, replaced, opts):
553 553 action, currentnode = rules.pop(0)
554 554 # is there any new commit between the expected parent and "."
555 555 #
556 556 # note: does not take non linear new change in account (but previous
557 557 # implementation didn't used them anyway (issue3655)
558 558 newchildren = [c.node() for c in repo.set('(%d::.)', parentctx)]
559 559 if not newchildren:
560 560 # `parentctxnode` should match but no result. This means that
561 561 # currentnode is not a descendant from parentctxnode.
562 562 msg = _('working directory parent is not a descendant of %s')
563 563 hint = _('update to %s or descendant and run "hg histedit '
564 564 '--continue" again') % parentctx
565 565 raise util.Abort(msg % parentctx, hint=hint)
566 566 newchildren.pop(0) # remove parentctxnode
567 567 if action in ('f', 'fold'):
568 568 tmpnodes.extend(newchildren)
569 569 else:
570 570 created.extend(newchildren)
571 571
572 572 m, a, r, d = repo.status()[:4]
573 573 oldctx = repo[currentnode]
574 574 message = oldctx.description() + '\n'
575 575 if action in ('e', 'edit', 'm', 'mess'):
576 576 message = ui.edit(message, ui.username())
577 577 elif action in ('f', 'fold'):
578 578 message = 'fold-temp-revision %s' % currentnode
579 579 new = None
580 580 if m or a or r or d:
581 581 new = repo.commit(text=message, user=oldctx.user(),
582 582 date=oldctx.date(), extra=oldctx.extra())
583 583
584 584 # If we're resuming a fold and we have new changes, mark the
585 585 # replacements and finish the fold. If not, it's more like a
586 586 # drop of the changesets that disappeared, and we can skip
587 587 # this step.
588 588 if action in ('f', 'fold') and (new or newchildren):
589 589 if new:
590 590 tmpnodes.append(new)
591 591 else:
592 592 new = newchildren[-1]
593 593 (parentctx, created_, replaced_, tmpnodes_) = finishfold(
594 594 ui, repo, parentctx, oldctx, new, opts, newchildren)
595 595 replaced.extend(replaced_)
596 596 created.extend(created_)
597 597 tmpnodes.extend(tmpnodes_)
598 598 elif action not in ('d', 'drop'):
599 599 if new != oldctx.node():
600 600 replaced.append(oldctx.node())
601 601 if new:
602 602 if new != oldctx.node():
603 603 created.append(new)
604 604 parentctx = repo[new]
605 605 return parentctx
606 606
607 607
608 608 def between(repo, old, new, keep):
609 609 """select and validate the set of revision to edit
610 610
611 611 When keep is false, the specified set can't have children."""
612 612 revs = [old]
613 613 current = old
614 614 while current != new:
615 615 ctx = repo[current]
616 616 if not keep and len(ctx.children()) > 1:
617 617 raise util.Abort(_('cannot edit history that would orphan nodes'))
618 618 if len(ctx.parents()) != 1 and ctx.parents()[1] != node.nullid:
619 619 raise util.Abort(_("can't edit history with merges"))
620 620 if not ctx.children():
621 621 current = new
622 622 else:
623 623 current = ctx.children()[0].node()
624 624 revs.append(current)
625 625 if len(repo[current].children()) and not keep:
626 626 raise util.Abort(_('cannot edit history that would orphan nodes'))
627 627 return revs
628 628
629 629
630 630 def writestate(repo, parentctxnode, created, replaced,
631 631 tmpnodes, existing, rules, keep, topmost, replacemap):
632 632 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
633 633 pickle.dump((parentctxnode, created, replaced,
634 634 tmpnodes, existing, rules, keep, topmost, replacemap),
635 635 fp)
636 636 fp.close()
637 637
638 638 def readstate(repo):
639 639 """Returns a tuple of (parentnode, created, replaced, tmp, existing, rules,
640 640 keep, topmost, replacemap ).
641 641 """
642 642 fp = open(os.path.join(repo.path, 'histedit-state'))
643 643 return pickle.load(fp)
644 644
645 645
646 646 def makedesc(c):
647 647 """build a initial action line for a ctx `c`
648 648
649 649 line are in the form:
650 650
651 651 pick <hash> <rev> <summary>
652 652 """
653 653 summary = ''
654 654 if c.description():
655 655 summary = c.description().splitlines()[0]
656 656 line = 'pick %s %d %s' % (c, c.rev(), summary)
657 657 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
658 658
659 659 def verifyrules(rules, repo, ctxs):
660 660 """Verify that there exists exactly one edit rule per given changeset.
661 661
662 662 Will abort if there are to many or too few rules, a malformed rule,
663 663 or a rule on a changeset outside of the user-given range.
664 664 """
665 665 parsed = []
666 666 if len(rules) != len(ctxs):
667 667 raise util.Abort(_('must specify a rule for each changeset once'))
668 668 for r in rules:
669 669 if ' ' not in r:
670 670 raise util.Abort(_('malformed line "%s"') % r)
671 671 action, rest = r.split(' ', 1)
672 672 if ' ' in rest.strip():
673 673 ha, rest = rest.split(' ', 1)
674 674 else:
675 675 ha = r.strip()
676 676 try:
677 677 if repo[ha] not in ctxs:
678 678 raise util.Abort(
679 679 _('may not use changesets other than the ones listed'))
680 680 except error.RepoError:
681 681 raise util.Abort(_('unknown changeset %s listed') % ha)
682 682 if action not in actiontable:
683 683 raise util.Abort(_('unknown action "%s"') % action)
684 684 parsed.append([action, ha])
685 685 return parsed
686 686
687 687 def movebookmarks(ui, repo, replacemap, tmpnodes, created):
688 688 """Move bookmark from old to newly created node"""
689 689 ui.note(_('histedit: Should update metadata for the following '
690 690 'changes:\n'))
691 691
692 692 def copybms(old, new):
693 693 if old in tmpnodes or old in created:
694 694 # can't have any metadata we'd want to update
695 695 return
696 696 while new in replacemap:
697 697 new = replacemap[new]
698 ui.note(_('histedit: %s to %s\n') % (node.short(old),
699 node.short(new)))
700 698 octx = repo[old]
701 699 marks = octx.bookmarks()
702 700 if marks:
703 ui.note(_('histedit: moving bookmarks %s\n') %
704 ', '.join(marks))
705 701 for mark in marks:
702 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
703 % (mark, octx, node.short(new)))
706 704 repo._bookmarks[mark] = new
707 705 bookmarks.write(repo)
708 706
709 707 # We assume that bookmarks on the tip should remain
710 708 # tipmost, but bookmarks on non-tip changesets should go
711 709 # to their most reasonable successor. As a result, find
712 710 # the old tip and new tip and copy those bookmarks first,
713 711 # then do the rest of the bookmark copies.
714 712 oldtip = sorted(replacemap.keys(), key=repo.changelog.rev)[-1]
715 713 newtip = sorted(replacemap.values(), key=repo.changelog.rev)[-1]
716 714 copybms(oldtip, newtip)
717 715
718 716 for old, new in sorted(replacemap.iteritems()):
719 717 copybms(old, new)
720 718
721 719 def cleanupnode(ui, repo, name, nodes):
722 720 """strip a group of nodes from the repository
723 721
724 722 The set of node to strip may contains unknown nodes."""
725 723 ui.debug('should strip %s nodes %s\n' %
726 724 (name, ', '.join([node.short(n) for n in nodes])))
727 725 lock = None
728 726 try:
729 727 lock = repo.lock()
730 728 # Find all node that need to be stripped
731 729 # (we hg %lr instead of %ln to silently ignore unknown item
732 730 nm = repo.changelog.nodemap
733 731 nodes = [n for n in nodes if n in nm]
734 732 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
735 733 for c in roots:
736 734 # We should process node in reverse order to strip tip most first.
737 735 # but this trigger a bug in changegroup hook.
738 736 # This would reduce bundle overhead
739 737 repair.strip(ui, repo, c)
740 738 finally:
741 739 lockmod.release(lock)
742 740
@@ -1,187 +1,181 b''
1 1 $ . "$TESTDIR/histedit-helpers.sh"
2 2
3 3 $ cat >> $HGRCPATH <<EOF
4 4 > [extensions]
5 5 > graphlog=
6 6 > histedit=
7 7 > EOF
8 8
9 9 $ hg init r
10 10 $ cd r
11 11
12 12 $ for x in a b c d e f ; do
13 13 > echo $x > $x
14 14 > hg add $x
15 15 > hg ci -m $x
16 16 > done
17 17
18 18 $ hg book -r 1 will-move-backwards
19 19 $ hg book -r 2 two
20 20 $ hg book -r 2 also-two
21 21 $ hg book -r 3 three
22 22 $ hg book -r 4 four
23 23 $ hg book -r tip five
24 24 $ hg log --graph
25 25 @ changeset: 5:652413bf663e
26 26 | bookmark: five
27 27 | tag: tip
28 28 | user: test
29 29 | date: Thu Jan 01 00:00:00 1970 +0000
30 30 | summary: f
31 31 |
32 32 o changeset: 4:e860deea161a
33 33 | bookmark: four
34 34 | user: test
35 35 | date: Thu Jan 01 00:00:00 1970 +0000
36 36 | summary: e
37 37 |
38 38 o changeset: 3:055a42cdd887
39 39 | bookmark: three
40 40 | user: test
41 41 | date: Thu Jan 01 00:00:00 1970 +0000
42 42 | summary: d
43 43 |
44 44 o changeset: 2:177f92b77385
45 45 | bookmark: also-two
46 46 | bookmark: two
47 47 | user: test
48 48 | date: Thu Jan 01 00:00:00 1970 +0000
49 49 | summary: c
50 50 |
51 51 o changeset: 1:d2ae7f538514
52 52 | bookmark: will-move-backwards
53 53 | user: test
54 54 | date: Thu Jan 01 00:00:00 1970 +0000
55 55 | summary: b
56 56 |
57 57 o changeset: 0:cb9a9f314b8b
58 58 user: test
59 59 date: Thu Jan 01 00:00:00 1970 +0000
60 60 summary: a
61 61
62 62 $ HGEDITOR=cat hg histedit 1
63 63 pick d2ae7f538514 1 b
64 64 pick 177f92b77385 2 c
65 65 pick 055a42cdd887 3 d
66 66 pick e860deea161a 4 e
67 67 pick 652413bf663e 5 f
68 68
69 69 # Edit history between d2ae7f538514 and 652413bf663e
70 70 #
71 71 # Commands:
72 72 # p, pick = use commit
73 73 # e, edit = use commit, but stop for amending
74 74 # f, fold = use commit, but fold into previous commit (combines N and N-1)
75 75 # d, drop = remove commit from history
76 76 # m, mess = edit message without changing commit content
77 77 #
78 78 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
79 79 $ cat >> commands.txt <<EOF
80 80 > pick 177f92b77385 2 c
81 81 > drop d2ae7f538514 1 b
82 82 > pick 055a42cdd887 3 d
83 83 > fold e860deea161a 4 e
84 84 > pick 652413bf663e 5 f
85 85 > EOF
86 86 $ hg histedit 1 --commands commands.txt --verbose | grep histedit
87 87 histedit: Should update metadata for the following changes:
88 histedit: 055a42cdd887 to ae467701c500
89 histedit: moving bookmarks three
90 histedit: 177f92b77385 to d36c0562f908
91 histedit: moving bookmarks also-two, two
92 histedit: 652413bf663e to 0efacef7cb48
93 histedit: moving bookmarks five
94 histedit: d2ae7f538514 to cb9a9f314b8b
95 histedit: moving bookmarks will-move-backwards
96 histedit: e860deea161a to ae467701c500
97 histedit: moving bookmarks four
88 histedit: moving bookmarks three from 055a42cdd887 to ae467701c500
89 histedit: moving bookmarks also-two from 177f92b77385 to d36c0562f908
90 histedit: moving bookmarks two from 177f92b77385 to d36c0562f908
91 histedit: moving bookmarks five from 652413bf663e to 0efacef7cb48
92 histedit: moving bookmarks will-move-backwards from d2ae7f538514 to cb9a9f314b8b
93 histedit: moving bookmarks four from e860deea161a to ae467701c500
98 94 saved backup bundle to $TESTTMP/r/.hg/strip-backup/d2ae7f538514-backup.hg (glob)
99 95 saved backup bundle to $TESTTMP/r/.hg/strip-backup/34a9919932c1-backup.hg (glob)
100 96 $ hg log --graph
101 97 @ changeset: 3:0efacef7cb48
102 98 | bookmark: five
103 99 | tag: tip
104 100 | user: test
105 101 | date: Thu Jan 01 00:00:00 1970 +0000
106 102 | summary: f
107 103 |
108 104 o changeset: 2:ae467701c500
109 105 | bookmark: four
110 106 | bookmark: three
111 107 | user: test
112 108 | date: Thu Jan 01 00:00:00 1970 +0000
113 109 | summary: d
114 110 |
115 111 o changeset: 1:d36c0562f908
116 112 | bookmark: also-two
117 113 | bookmark: two
118 114 | user: test
119 115 | date: Thu Jan 01 00:00:00 1970 +0000
120 116 | summary: c
121 117 |
122 118 o changeset: 0:cb9a9f314b8b
123 119 bookmark: will-move-backwards
124 120 user: test
125 121 date: Thu Jan 01 00:00:00 1970 +0000
126 122 summary: a
127 123
128 124 $ HGEDITOR=cat hg histedit 1
129 125 pick d36c0562f908 1 c
130 126 pick ae467701c500 2 d
131 127 pick 0efacef7cb48 3 f
132 128
133 129 # Edit history between d36c0562f908 and 0efacef7cb48
134 130 #
135 131 # Commands:
136 132 # p, pick = use commit
137 133 # e, edit = use commit, but stop for amending
138 134 # f, fold = use commit, but fold into previous commit (combines N and N-1)
139 135 # d, drop = remove commit from history
140 136 # m, mess = edit message without changing commit content
141 137 #
142 138 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
143 139 $ cat > commands.txt << EOF
144 140 > pick d36c0562f908 1 c
145 141 > pick 0efacef7cb48 3 f
146 142 > pick ae467701c500 2 d
147 143 > EOF
148 144 $ hg histedit 1 --commands commands.txt --verbose | grep histedit
149 145 histedit: Should update metadata for the following changes:
150 histedit: 0efacef7cb48 to 1be9c35b4cb2
151 histedit: moving bookmarks five
152 histedit: 0efacef7cb48 to 7c044e3e33a9
153 histedit: ae467701c500 to 1be9c35b4cb2
154 histedit: moving bookmarks four, three
146 histedit: moving bookmarks five from 0efacef7cb48 to 1be9c35b4cb2
147 histedit: moving bookmarks four from ae467701c500 to 1be9c35b4cb2
148 histedit: moving bookmarks three from ae467701c500 to 1be9c35b4cb2
155 149 saved backup bundle to $TESTTMP/r/.hg/strip-backup/ae467701c500-backup.hg (glob)
156 150
157 151 We expect 'five' to stay at tip, since the tipmost bookmark is most
158 152 likely the useful signal.
159 153
160 154 $ hg log --graph
161 155 @ changeset: 3:1be9c35b4cb2
162 156 | bookmark: five
163 157 | bookmark: four
164 158 | bookmark: three
165 159 | tag: tip
166 160 | user: test
167 161 | date: Thu Jan 01 00:00:00 1970 +0000
168 162 | summary: d
169 163 |
170 164 o changeset: 2:7c044e3e33a9
171 165 | user: test
172 166 | date: Thu Jan 01 00:00:00 1970 +0000
173 167 | summary: f
174 168 |
175 169 o changeset: 1:d36c0562f908
176 170 | bookmark: also-two
177 171 | bookmark: two
178 172 | user: test
179 173 | date: Thu Jan 01 00:00:00 1970 +0000
180 174 | summary: c
181 175 |
182 176 o changeset: 0:cb9a9f314b8b
183 177 bookmark: will-move-backwards
184 178 user: test
185 179 date: Thu Jan 01 00:00:00 1970 +0000
186 180 summary: a
187 181
General Comments 0
You need to be logged in to leave comments. Login now