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