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