##// END OF EJS Templates
histedit: end folding message with an LF...
Patrick Mezard -
r17241:c2f13180 stable
parent child Browse files
Show More
@@ -1,695 +1,695 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 [oldctx.description()])
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 433 # discover any nodes the user has added in the interim
434 434 newchildren = [c for c in parentctx.children()
435 435 if c.node() not in existing]
436 436 action, currentnode = rules.pop(0)
437 437 while newchildren:
438 438 if action in ('f', 'fold'):
439 439 tmpnodes.extend([n.node() for n in newchildren])
440 440 else:
441 441 created.extend([n.node() for n in newchildren])
442 442 filtered = []
443 443 for r in newchildren:
444 444 filtered += [c for c in r.children() if c.node not in existing]
445 445 newchildren = filtered
446 446 m, a, r, d = repo.status()[:4]
447 447 oldctx = repo[currentnode]
448 448 message = oldctx.description()
449 449 if action in ('e', 'edit', 'm', 'mess'):
450 450 message = ui.edit(message, ui.username())
451 451 elif action in ('f', 'fold'):
452 452 message = 'fold-temp-revision %s' % currentnode
453 453 new = None
454 454 if m or a or r or d:
455 455 new = repo.commit(text=message, user=oldctx.user(),
456 456 date=oldctx.date(), extra=oldctx.extra())
457 457
458 458 # If we're resuming a fold and we have new changes, mark the
459 459 # replacements and finish the fold. If not, it's more like a
460 460 # drop of the changesets that disappeared, and we can skip
461 461 # this step.
462 462 if action in ('f', 'fold') and (new or newchildren):
463 463 if new:
464 464 tmpnodes.append(new)
465 465 else:
466 466 new = newchildren[-1]
467 467 (parentctx, created_, replaced_, tmpnodes_) = finishfold(
468 468 ui, repo, parentctx, oldctx, new, opts, newchildren)
469 469 replaced.extend(replaced_)
470 470 created.extend(created_)
471 471 tmpnodes.extend(tmpnodes_)
472 472 elif action not in ('d', 'drop'):
473 473 if new != oldctx.node():
474 474 replaced.append(oldctx.node())
475 475 if new:
476 476 if new != oldctx.node():
477 477 created.append(new)
478 478 parentctx = repo[new]
479 479
480 480 elif opts.get('abort', False):
481 481 if len(parent) != 0:
482 482 raise util.Abort(_('no arguments allowed with --abort'))
483 483 (parentctxnode, created, replaced, tmpnodes,
484 484 existing, rules, keep, tip, replacemap) = readstate(repo)
485 485 ui.debug('restore wc to old tip %s\n' % node.hex(tip))
486 486 hg.clean(repo, tip)
487 487 ui.debug('should strip created nodes %s\n' %
488 488 ', '.join([node.hex(n)[:12] for n in created]))
489 489 ui.debug('should strip temp nodes %s\n' %
490 490 ', '.join([node.hex(n)[:12] for n in tmpnodes]))
491 491 for nodes in (created, tmpnodes):
492 492 for n in reversed(nodes):
493 493 try:
494 494 repair.strip(ui, repo, n)
495 495 except error.LookupError:
496 496 pass
497 497 os.unlink(os.path.join(repo.path, 'histedit-state'))
498 498 return
499 499 else:
500 500 cmdutil.bailifchanged(repo)
501 501 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
502 502 raise util.Abort(_('history edit already in progress, try '
503 503 '--continue or --abort'))
504 504
505 505 tip, empty = repo.dirstate.parents()
506 506
507 507
508 508 if len(parent) != 1:
509 509 raise util.Abort(_('histedit requires exactly one parent revision'))
510 510 parent = scmutil.revsingle(repo, parent[0]).node()
511 511
512 512 keep = opts.get('keep', False)
513 513 revs = between(repo, parent, tip, keep)
514 514
515 515 ctxs = [repo[r] for r in revs]
516 516 existing = [r.node() for r in ctxs]
517 517 rules = opts.get('commands', '')
518 518 if not rules:
519 519 rules = '\n'.join([makedesc(c) for c in ctxs])
520 520 rules += editcomment % (node.hex(parent)[:12], node.hex(tip)[:12])
521 521 rules = ui.edit(rules, ui.username())
522 522 # Save edit rules in .hg/histedit-last-edit.txt in case
523 523 # the user needs to ask for help after something
524 524 # surprising happens.
525 525 f = open(repo.join('histedit-last-edit.txt'), 'w')
526 526 f.write(rules)
527 527 f.close()
528 528 else:
529 529 f = open(rules)
530 530 rules = f.read()
531 531 f.close()
532 532 rules = [l for l in (r.strip() for r in rules.splitlines())
533 533 if l and not l[0] == '#']
534 534 rules = verifyrules(rules, repo, ctxs)
535 535
536 536 parentctx = repo[parent].parents()[0]
537 537 keep = opts.get('keep', False)
538 538 replaced = []
539 539 replacemap = {}
540 540 tmpnodes = []
541 541 created = []
542 542
543 543
544 544 while rules:
545 545 writestate(repo, parentctx.node(), created, replaced,
546 546 tmpnodes, existing, rules, keep, tip, replacemap)
547 547 action, ha = rules.pop(0)
548 548 (parentctx, created_, replaced_, tmpnodes_) = actiontable[action](
549 549 ui, repo, parentctx, ha, opts)
550 550
551 551 if replaced_:
552 552 clen, rlen = len(created_), len(replaced_)
553 553 if clen == rlen == 1:
554 554 ui.debug('histedit: exact replacement of %s with %s\n' % (
555 555 node.short(replaced_[0]), node.short(created_[0])))
556 556
557 557 replacemap[replaced_[0]] = created_[0]
558 558 elif clen > rlen:
559 559 assert rlen == 1, ('unexpected replacement of '
560 560 '%d changes with %d changes' % (rlen, clen))
561 561 # made more changesets than we're replacing
562 562 # TODO synthesize patch names for created patches
563 563 replacemap[replaced_[0]] = created_[-1]
564 564 ui.debug('histedit: created many, assuming %s replaced by %s' %
565 565 (node.short(replaced_[0]), node.short(created_[-1])))
566 566 elif rlen > clen:
567 567 if not created_:
568 568 # This must be a drop. Try and put our metadata on
569 569 # the parent change.
570 570 assert rlen == 1
571 571 r = replaced_[0]
572 572 ui.debug('histedit: %s seems replaced with nothing, '
573 573 'finding a parent\n' % (node.short(r)))
574 574 pctx = repo[r].parents()[0]
575 575 if pctx.node() in replacemap:
576 576 ui.debug('histedit: parent is already replaced\n')
577 577 replacemap[r] = replacemap[pctx.node()]
578 578 else:
579 579 replacemap[r] = pctx.node()
580 580 ui.debug('histedit: %s best replaced by %s\n' % (
581 581 node.short(r), node.short(replacemap[r])))
582 582 else:
583 583 assert len(created_) == 1
584 584 for r in replaced_:
585 585 ui.debug('histedit: %s replaced by %s\n' % (
586 586 node.short(r), node.short(created_[0])))
587 587 replacemap[r] = created_[0]
588 588 else:
589 589 assert False, (
590 590 'Unhandled case in replacement mapping! '
591 591 'replacing %d changes with %d changes' % (rlen, clen))
592 592 created.extend(created_)
593 593 replaced.extend(replaced_)
594 594 tmpnodes.extend(tmpnodes_)
595 595
596 596 hg.update(repo, parentctx.node())
597 597
598 598 if not keep:
599 599 if replacemap:
600 600 ui.note(_('histedit: Should update metadata for the following '
601 601 'changes:\n'))
602 602
603 603 def copybms(old, new):
604 604 if old in tmpnodes or old in created:
605 605 # can't have any metadata we'd want to update
606 606 return
607 607 while new in replacemap:
608 608 new = replacemap[new]
609 609 ui.note(_('histedit: %s to %s\n') % (node.short(old),
610 610 node.short(new)))
611 611 octx = repo[old]
612 612 marks = octx.bookmarks()
613 613 if marks:
614 614 ui.note(_('histedit: moving bookmarks %s\n') %
615 615 ', '.join(marks))
616 616 for mark in marks:
617 617 repo._bookmarks[mark] = new
618 618 bookmarks.write(repo)
619 619
620 620 # We assume that bookmarks on the tip should remain
621 621 # tipmost, but bookmarks on non-tip changesets should go
622 622 # to their most reasonable successor. As a result, find
623 623 # the old tip and new tip and copy those bookmarks first,
624 624 # then do the rest of the bookmark copies.
625 625 oldtip = sorted(replacemap.keys(), key=repo.changelog.rev)[-1]
626 626 newtip = sorted(replacemap.values(), key=repo.changelog.rev)[-1]
627 627 copybms(oldtip, newtip)
628 628
629 629 for old, new in sorted(replacemap.iteritems()):
630 630 copybms(old, new)
631 631 # TODO update mq state
632 632
633 633 ui.debug('should strip replaced nodes %s\n' %
634 634 ', '.join([node.hex(n)[:12] for n in replaced]))
635 635 for n in sorted(replaced, key=lambda x: repo[x].rev()):
636 636 try:
637 637 repair.strip(ui, repo, n)
638 638 except error.LookupError:
639 639 pass
640 640
641 641 ui.debug('should strip temp nodes %s\n' %
642 642 ', '.join([node.hex(n)[:12] for n in tmpnodes]))
643 643 for n in reversed(tmpnodes):
644 644 try:
645 645 repair.strip(ui, repo, n)
646 646 except error.LookupError:
647 647 pass
648 648 os.unlink(os.path.join(repo.path, 'histedit-state'))
649 649 if os.path.exists(repo.sjoin('undo')):
650 650 os.unlink(repo.sjoin('undo'))
651 651
652 652
653 653 def writestate(repo, parentctxnode, created, replaced,
654 654 tmpnodes, existing, rules, keep, oldtip, replacemap):
655 655 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
656 656 pickle.dump((parentctxnode, created, replaced,
657 657 tmpnodes, existing, rules, keep, oldtip, replacemap),
658 658 fp)
659 659 fp.close()
660 660
661 661 def readstate(repo):
662 662 """Returns a tuple of (parentnode, created, replaced, tmp, existing, rules,
663 663 keep, oldtip, replacemap ).
664 664 """
665 665 fp = open(os.path.join(repo.path, 'histedit-state'))
666 666 return pickle.load(fp)
667 667
668 668
669 669 def verifyrules(rules, repo, ctxs):
670 670 """Verify that there exists exactly one edit rule per given changeset.
671 671
672 672 Will abort if there are to many or too few rules, a malformed rule,
673 673 or a rule on a changeset outside of the user-given range.
674 674 """
675 675 parsed = []
676 676 if len(rules) != len(ctxs):
677 677 raise util.Abort(_('must specify a rule for each changeset once'))
678 678 for r in rules:
679 679 if ' ' not in r:
680 680 raise util.Abort(_('malformed line "%s"') % r)
681 681 action, rest = r.split(' ', 1)
682 682 if ' ' in rest.strip():
683 683 ha, rest = rest.split(' ', 1)
684 684 else:
685 685 ha = r.strip()
686 686 try:
687 687 if repo[ha] not in ctxs:
688 688 raise util.Abort(
689 689 _('may not use changesets other than the ones listed'))
690 690 except error.RepoError:
691 691 raise util.Abort(_('unknown changeset %s listed') % ha)
692 692 if action not in actiontable:
693 693 raise util.Abort(_('unknown action "%s"') % action)
694 694 parsed.append([action, ha])
695 695 return parsed
@@ -1,146 +1,147 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 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
95 96 file e already exists
96 97 1 out of 1 hunks FAILED -- saving rejects to file e.rej
97 98 abort: Fix up the change and run hg histedit --continue
98 99
99 100 just continue this time
100 101 $ hg histedit --continue 2>&1 | fixbundle
101 102 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
102 103 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
103 104
104 105 log after edit
105 106 $ hg log --graph
106 107 @ changeset: 4:f768fd60ca34
107 108 | tag: tip
108 109 | user: test
109 110 | date: Thu Jan 01 00:00:00 1970 +0000
110 111 | summary: f
111 112 |
112 113 o changeset: 3:671efe372e33
113 114 | user: test
114 115 | date: Thu Jan 01 00:00:00 1970 +0000
115 116 | summary: d
116 117 |
117 118 o changeset: 2:177f92b77385
118 119 | user: test
119 120 | date: Thu Jan 01 00:00:00 1970 +0000
120 121 | summary: c
121 122 |
122 123 o changeset: 1:d2ae7f538514
123 124 | user: test
124 125 | date: Thu Jan 01 00:00:00 1970 +0000
125 126 | summary: b
126 127 |
127 128 o changeset: 0:cb9a9f314b8b
128 129 user: test
129 130 date: Thu Jan 01 00:00:00 1970 +0000
130 131 summary: a
131 132
132 133
133 134 contents of e
134 135 $ hg cat e
135 136 a
136 137
137 138 manifest
138 139 $ hg manifest
139 140 a
140 141 b
141 142 c
142 143 d
143 144 e
144 145 f
145 146
146 147 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now