##// END OF EJS Templates
histedit: use cmdutil.command decorator
Adrian Buehlmann -
r17147:80e86151 default
parent child Browse files
Show More
@@ -1,698 +1,695
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 cmdtable = {}
163 command = cmdutil.command(cmdtable)
164
162 165 testedwith = 'internal'
163 166
164 167 editcomment = """
165 168
166 169 # Edit history between %s and %s
167 170 #
168 171 # Commands:
169 172 # p, pick = use commit
170 173 # e, edit = use commit, but stop for amending
171 174 # f, fold = use commit, but fold into previous commit (combines N and N-1)
172 175 # d, drop = remove commit from history
173 176 # m, mess = edit message without changing commit content
174 177 #
175 178 """
176 179
177 180 def between(repo, old, new, keep):
178 181 revs = [old]
179 182 current = old
180 183 while current != new:
181 184 ctx = repo[current]
182 185 if not keep and len(ctx.children()) > 1:
183 186 raise util.Abort(_('cannot edit history that would orphan nodes'))
184 187 if len(ctx.parents()) != 1 and ctx.parents()[1] != node.nullid:
185 188 raise util.Abort(_("can't edit history with merges"))
186 189 if not ctx.children():
187 190 current = new
188 191 else:
189 192 current = ctx.children()[0].node()
190 193 revs.append(current)
191 194 if len(repo[current].children()) and not keep:
192 195 raise util.Abort(_('cannot edit history that would orphan nodes'))
193 196 return revs
194 197
195 198
196 199 def pick(ui, repo, ctx, ha, opts):
197 200 oldctx = repo[ha]
198 201 if oldctx.parents()[0] == ctx:
199 202 ui.debug('node %s unchanged\n' % ha)
200 203 return oldctx, [], [], []
201 204 hg.update(repo, ctx.node())
202 205 fd, patchfile = tempfile.mkstemp(prefix='hg-histedit-')
203 206 fp = os.fdopen(fd, 'w')
204 207 diffopts = patch.diffopts(ui, opts)
205 208 diffopts.git = True
206 209 diffopts.ignorews = False
207 210 diffopts.ignorewsamount = False
208 211 diffopts.ignoreblanklines = False
209 212 gen = patch.diff(repo, oldctx.parents()[0].node(), ha, opts=diffopts)
210 213 for chunk in gen:
211 214 fp.write(chunk)
212 215 fp.close()
213 216 try:
214 217 files = set()
215 218 try:
216 219 patch.patch(ui, repo, patchfile, files=files, eolmode=None)
217 220 if not files:
218 221 ui.warn(_('%s: empty changeset')
219 222 % node.hex(ha))
220 223 return ctx, [], [], []
221 224 finally:
222 225 os.unlink(patchfile)
223 226 except Exception:
224 227 raise util.Abort(_('Fix up the change and run '
225 228 'hg histedit --continue'))
226 229 n = repo.commit(text=oldctx.description(), user=oldctx.user(),
227 230 date=oldctx.date(), extra=oldctx.extra())
228 231 return repo[n], [n], [oldctx.node()], []
229 232
230 233
231 234 def edit(ui, repo, ctx, ha, opts):
232 235 oldctx = repo[ha]
233 236 hg.update(repo, ctx.node())
234 237 fd, patchfile = tempfile.mkstemp(prefix='hg-histedit-')
235 238 fp = os.fdopen(fd, 'w')
236 239 diffopts = patch.diffopts(ui, opts)
237 240 diffopts.git = True
238 241 diffopts.ignorews = False
239 242 diffopts.ignorewsamount = False
240 243 diffopts.ignoreblanklines = False
241 244 gen = patch.diff(repo, oldctx.parents()[0].node(), ha, opts=diffopts)
242 245 for chunk in gen:
243 246 fp.write(chunk)
244 247 fp.close()
245 248 try:
246 249 files = set()
247 250 try:
248 251 patch.patch(ui, repo, patchfile, files=files, eolmode=None)
249 252 finally:
250 253 os.unlink(patchfile)
251 254 except Exception:
252 255 pass
253 256 raise util.Abort(_('Make changes as needed, you may commit or record as '
254 257 'needed now.\nWhen you are finished, run hg'
255 258 ' histedit --continue to resume.'))
256 259
257 260 def fold(ui, repo, ctx, ha, opts):
258 261 oldctx = repo[ha]
259 262 hg.update(repo, ctx.node())
260 263 fd, patchfile = tempfile.mkstemp(prefix='hg-histedit-')
261 264 fp = os.fdopen(fd, 'w')
262 265 diffopts = patch.diffopts(ui, opts)
263 266 diffopts.git = True
264 267 diffopts.ignorews = False
265 268 diffopts.ignorewsamount = False
266 269 diffopts.ignoreblanklines = False
267 270 gen = patch.diff(repo, oldctx.parents()[0].node(), ha, opts=diffopts)
268 271 for chunk in gen:
269 272 fp.write(chunk)
270 273 fp.close()
271 274 try:
272 275 files = set()
273 276 try:
274 277 patch.patch(ui, repo, patchfile, files=files, eolmode=None)
275 278 if not files:
276 279 ui.warn(_('%s: empty changeset')
277 280 % node.hex(ha))
278 281 return ctx, [], [], []
279 282 finally:
280 283 os.unlink(patchfile)
281 284 except Exception:
282 285 raise util.Abort(_('Fix up the change and run '
283 286 'hg histedit --continue'))
284 287 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(),
285 288 date=oldctx.date(), extra=oldctx.extra())
286 289 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
287 290
288 291 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
289 292 parent = ctx.parents()[0].node()
290 293 hg.update(repo, parent)
291 294 fd, patchfile = tempfile.mkstemp(prefix='hg-histedit-')
292 295 fp = os.fdopen(fd, 'w')
293 296 diffopts = patch.diffopts(ui, opts)
294 297 diffopts.git = True
295 298 diffopts.ignorews = False
296 299 diffopts.ignorewsamount = False
297 300 diffopts.ignoreblanklines = False
298 301 gen = patch.diff(repo, parent, newnode, opts=diffopts)
299 302 for chunk in gen:
300 303 fp.write(chunk)
301 304 fp.close()
302 305 files = set()
303 306 try:
304 307 patch.patch(ui, repo, patchfile, files=files, eolmode=None)
305 308 finally:
306 309 os.unlink(patchfile)
307 310 newmessage = '\n***\n'.join(
308 311 [ctx.description()] +
309 312 [repo[r].description() for r in internalchanges] +
310 313 [oldctx.description()])
311 314 # If the changesets are from the same author, keep it.
312 315 if ctx.user() == oldctx.user():
313 316 username = ctx.user()
314 317 else:
315 318 username = ui.username()
316 319 newmessage = ui.edit(newmessage, username)
317 320 n = repo.commit(text=newmessage, user=username,
318 321 date=max(ctx.date(), oldctx.date()), extra=oldctx.extra())
319 322 return repo[n], [n], [oldctx.node(), ctx.node()], [newnode]
320 323
321 324 def drop(ui, repo, ctx, ha, opts):
322 325 return ctx, [], [repo[ha].node()], []
323 326
324 327
325 328 def message(ui, repo, ctx, ha, opts):
326 329 oldctx = repo[ha]
327 330 hg.update(repo, ctx.node())
328 331 fd, patchfile = tempfile.mkstemp(prefix='hg-histedit-')
329 332 fp = os.fdopen(fd, 'w')
330 333 diffopts = patch.diffopts(ui, opts)
331 334 diffopts.git = True
332 335 diffopts.ignorews = False
333 336 diffopts.ignorewsamount = False
334 337 diffopts.ignoreblanklines = False
335 338 gen = patch.diff(repo, oldctx.parents()[0].node(), ha, opts=diffopts)
336 339 for chunk in gen:
337 340 fp.write(chunk)
338 341 fp.close()
339 342 try:
340 343 files = set()
341 344 try:
342 345 patch.patch(ui, repo, patchfile, files=files, eolmode=None)
343 346 finally:
344 347 os.unlink(patchfile)
345 348 except Exception:
346 349 raise util.Abort(_('Fix up the change and run '
347 350 'hg histedit --continue'))
348 351 message = oldctx.description()
349 352 message = ui.edit(message, ui.username())
350 353 new = repo.commit(text=message, user=oldctx.user(), date=oldctx.date(),
351 354 extra=oldctx.extra())
352 355 newctx = repo[new]
353 356 if oldctx.node() != newctx.node():
354 357 return newctx, [new], [oldctx.node()], []
355 358 # We didn't make an edit, so just indicate no replaced nodes
356 359 return newctx, [new], [], []
357 360
358 361
359 362 def makedesc(c):
360 363 summary = ''
361 364 if c.description():
362 365 summary = c.description().splitlines()[0]
363 366 line = 'pick %s %d %s' % (c.hex()[:12], c.rev(), summary)
364 367 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
365 368
366 369 actiontable = {'p': pick,
367 370 'pick': pick,
368 371 'e': edit,
369 372 'edit': edit,
370 373 'f': fold,
371 374 'fold': fold,
372 375 'd': drop,
373 376 'drop': drop,
374 377 'm': message,
375 378 'mess': message,
376 379 }
380
381 @command('histedit',
382 [('', 'commands', '',
383 _('Read history edits from the specified file.')),
384 ('c', 'continue', False, _('continue an edit already in progress')),
385 ('k', 'keep', False,
386 _("don't strip old nodes after edit is complete")),
387 ('', 'abort', False, _('abort an edit in progress')),
388 ('o', 'outgoing', False, _('changesets not found in destination')),
389 ('f', 'force', False,
390 _('force outgoing even for unrelated repositories')),
391 ('r', 'rev', [], _('first revision to be edited'))],
392 _("[PARENT]"))
377 393 def histedit(ui, repo, *parent, **opts):
378 394 """interactively edit changeset history
379 395 """
380 396 # TODO only abort if we try and histedit mq patches, not just
381 397 # blanket if mq patches are applied somewhere
382 398 mq = getattr(repo, 'mq', None)
383 399 if mq and mq.applied:
384 400 raise util.Abort(_('source has mq patches applied'))
385 401
386 402 parent = list(parent) + opts.get('rev', [])
387 403 if opts.get('outgoing'):
388 404 if len(parent) > 1:
389 405 raise util.Abort(
390 406 _('only one repo argument allowed with --outgoing'))
391 407 elif parent:
392 408 parent = parent[0]
393 409
394 410 dest = ui.expandpath(parent or 'default-push', parent or 'default')
395 411 dest, revs = hg.parseurl(dest, None)[:2]
396 412 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
397 413
398 414 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
399 415 other = hg.repository(hg.remoteui(repo, opts), dest)
400 416
401 417 if revs:
402 418 revs = [repo.lookup(rev) for rev in revs]
403 419
404 420 parent = discovery.findcommonoutgoing(
405 421 repo, other, [], force=opts.get('force')).missing[0:1]
406 422 else:
407 423 if opts.get('force'):
408 424 raise util.Abort(_('--force only allowed with --outgoing'))
409 425
410 426 if opts.get('continue', False):
411 427 if len(parent) != 0:
412 428 raise util.Abort(_('no arguments allowed with --continue'))
413 429 (parentctxnode, created, replaced,
414 430 tmpnodes, existing, rules, keep, tip, replacemap) = readstate(repo)
415 431 currentparent, wantnull = repo.dirstate.parents()
416 432 parentctx = repo[parentctxnode]
417 433 # discover any nodes the user has added in the interim
418 434 newchildren = [c for c in parentctx.children()
419 435 if c.node() not in existing]
420 436 action, currentnode = rules.pop(0)
421 437 while newchildren:
422 438 if action in ('f', 'fold'):
423 439 tmpnodes.extend([n.node() for n in newchildren])
424 440 else:
425 441 created.extend([n.node() for n in newchildren])
426 442 filtered = []
427 443 for r in newchildren:
428 444 filtered += [c for c in r.children() if c.node not in existing]
429 445 newchildren = filtered
430 446 m, a, r, d = repo.status()[:4]
431 447 oldctx = repo[currentnode]
432 448 message = oldctx.description()
433 449 if action in ('e', 'edit', 'm', 'mess'):
434 450 message = ui.edit(message, ui.username())
435 451 elif action in ('f', 'fold'):
436 452 message = 'fold-temp-revision %s' % currentnode
437 453 new = None
438 454 if m or a or r or d:
439 455 new = repo.commit(text=message, user=oldctx.user(),
440 456 date=oldctx.date(), extra=oldctx.extra())
441 457
442 458 # If we're resuming a fold and we have new changes, mark the
443 459 # replacements and finish the fold. If not, it's more like a
444 460 # drop of the changesets that disappeared, and we can skip
445 461 # this step.
446 462 if action in ('f', 'fold') and (new or newchildren):
447 463 if new:
448 464 tmpnodes.append(new)
449 465 else:
450 466 new = newchildren[-1]
451 467 (parentctx, created_, replaced_, tmpnodes_) = finishfold(
452 468 ui, repo, parentctx, oldctx, new, opts, newchildren)
453 469 replaced.extend(replaced_)
454 470 created.extend(created_)
455 471 tmpnodes.extend(tmpnodes_)
456 472 elif action not in ('d', 'drop'):
457 473 if new != oldctx.node():
458 474 replaced.append(oldctx.node())
459 475 if new:
460 476 if new != oldctx.node():
461 477 created.append(new)
462 478 parentctx = repo[new]
463 479
464 480 elif opts.get('abort', False):
465 481 if len(parent) != 0:
466 482 raise util.Abort(_('no arguments allowed with --abort'))
467 483 (parentctxnode, created, replaced, tmpnodes,
468 484 existing, rules, keep, tip, replacemap) = readstate(repo)
469 485 ui.debug('restore wc to old tip %s\n' % node.hex(tip))
470 486 hg.clean(repo, tip)
471 487 ui.debug('should strip created nodes %s\n' %
472 488 ', '.join([node.hex(n)[:12] for n in created]))
473 489 ui.debug('should strip temp nodes %s\n' %
474 490 ', '.join([node.hex(n)[:12] for n in tmpnodes]))
475 491 for nodes in (created, tmpnodes):
476 492 for n in reversed(nodes):
477 493 try:
478 494 repair.strip(ui, repo, n)
479 495 except error.LookupError:
480 496 pass
481 497 os.unlink(os.path.join(repo.path, 'histedit-state'))
482 498 return
483 499 else:
484 500 cmdutil.bailifchanged(repo)
485 501 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
486 502 raise util.Abort(_('history edit already in progress, try '
487 503 '--continue or --abort'))
488 504
489 505 tip, empty = repo.dirstate.parents()
490 506
491 507
492 508 if len(parent) != 1:
493 509 raise util.Abort(_('histedit requires exactly one parent revision'))
494 510 parent = scmutil.revsingle(repo, parent[0]).node()
495 511
496 512 keep = opts.get('keep', False)
497 513 revs = between(repo, parent, tip, keep)
498 514
499 515 ctxs = [repo[r] for r in revs]
500 516 existing = [r.node() for r in ctxs]
501 517 rules = opts.get('commands', '')
502 518 if not rules:
503 519 rules = '\n'.join([makedesc(c) for c in ctxs])
504 520 rules += editcomment % (node.hex(parent)[:12], node.hex(tip)[:12])
505 521 rules = ui.edit(rules, ui.username())
506 522 # Save edit rules in .hg/histedit-last-edit.txt in case
507 523 # the user needs to ask for help after something
508 524 # surprising happens.
509 525 f = open(repo.join('histedit-last-edit.txt'), 'w')
510 526 f.write(rules)
511 527 f.close()
512 528 else:
513 529 f = open(rules)
514 530 rules = f.read()
515 531 f.close()
516 532 rules = [l for l in (r.strip() for r in rules.splitlines())
517 533 if l and not l[0] == '#']
518 534 rules = verifyrules(rules, repo, ctxs)
519 535
520 536 parentctx = repo[parent].parents()[0]
521 537 keep = opts.get('keep', False)
522 538 replaced = []
523 539 replacemap = {}
524 540 tmpnodes = []
525 541 created = []
526 542
527 543
528 544 while rules:
529 545 writestate(repo, parentctx.node(), created, replaced,
530 546 tmpnodes, existing, rules, keep, tip, replacemap)
531 547 action, ha = rules.pop(0)
532 548 (parentctx, created_, replaced_, tmpnodes_) = actiontable[action](
533 549 ui, repo, parentctx, ha, opts)
534 550
535 551 if replaced_:
536 552 clen, rlen = len(created_), len(replaced_)
537 553 if clen == rlen == 1:
538 554 ui.debug('histedit: exact replacement of %s with %s\n' % (
539 555 node.short(replaced_[0]), node.short(created_[0])))
540 556
541 557 replacemap[replaced_[0]] = created_[0]
542 558 elif clen > rlen:
543 559 assert rlen == 1, ('unexpected replacement of '
544 560 '%d changes with %d changes' % (rlen, clen))
545 561 # made more changesets than we're replacing
546 562 # TODO synthesize patch names for created patches
547 563 replacemap[replaced_[0]] = created_[-1]
548 564 ui.debug('histedit: created many, assuming %s replaced by %s' %
549 565 (node.short(replaced_[0]), node.short(created_[-1])))
550 566 elif rlen > clen:
551 567 if not created_:
552 568 # This must be a drop. Try and put our metadata on
553 569 # the parent change.
554 570 assert rlen == 1
555 571 r = replaced_[0]
556 572 ui.debug('histedit: %s seems replaced with nothing, '
557 573 'finding a parent\n' % (node.short(r)))
558 574 pctx = repo[r].parents()[0]
559 575 if pctx.node() in replacemap:
560 576 ui.debug('histedit: parent is already replaced\n')
561 577 replacemap[r] = replacemap[pctx.node()]
562 578 else:
563 579 replacemap[r] = pctx.node()
564 580 ui.debug('histedit: %s best replaced by %s\n' % (
565 581 node.short(r), node.short(replacemap[r])))
566 582 else:
567 583 assert len(created_) == 1
568 584 for r in replaced_:
569 585 ui.debug('histedit: %s replaced by %s\n' % (
570 586 node.short(r), node.short(created_[0])))
571 587 replacemap[r] = created_[0]
572 588 else:
573 589 assert False, (
574 590 'Unhandled case in replacement mapping! '
575 591 'replacing %d changes with %d changes' % (rlen, clen))
576 592 created.extend(created_)
577 593 replaced.extend(replaced_)
578 594 tmpnodes.extend(tmpnodes_)
579 595
580 596 hg.update(repo, parentctx.node())
581 597
582 598 if not keep:
583 599 if replacemap:
584 600 ui.note(_('histedit: Should update metadata for the following '
585 601 'changes:\n'))
586 602
587 603 def copybms(old, new):
588 604 if old in tmpnodes or old in created:
589 605 # can't have any metadata we'd want to update
590 606 return
591 607 while new in replacemap:
592 608 new = replacemap[new]
593 609 ui.note(_('histedit: %s to %s\n') % (node.short(old),
594 610 node.short(new)))
595 611 octx = repo[old]
596 612 marks = octx.bookmarks()
597 613 if marks:
598 614 ui.note(_('histedit: moving bookmarks %s\n') %
599 615 ', '.join(marks))
600 616 for mark in marks:
601 617 repo._bookmarks[mark] = new
602 618 bookmarks.write(repo)
603 619
604 620 # We assume that bookmarks on the tip should remain
605 621 # tipmost, but bookmarks on non-tip changesets should go
606 622 # to their most reasonable successor. As a result, find
607 623 # the old tip and new tip and copy those bookmarks first,
608 624 # then do the rest of the bookmark copies.
609 625 oldtip = sorted(replacemap.keys(), key=repo.changelog.rev)[-1]
610 626 newtip = sorted(replacemap.values(), key=repo.changelog.rev)[-1]
611 627 copybms(oldtip, newtip)
612 628
613 629 for old, new in sorted(replacemap.iteritems()):
614 630 copybms(old, new)
615 631 # TODO update mq state
616 632
617 633 ui.debug('should strip replaced nodes %s\n' %
618 634 ', '.join([node.hex(n)[:12] for n in replaced]))
619 635 for n in sorted(replaced, key=lambda x: repo[x].rev()):
620 636 try:
621 637 repair.strip(ui, repo, n)
622 638 except error.LookupError:
623 639 pass
624 640
625 641 ui.debug('should strip temp nodes %s\n' %
626 642 ', '.join([node.hex(n)[:12] for n in tmpnodes]))
627 643 for n in reversed(tmpnodes):
628 644 try:
629 645 repair.strip(ui, repo, n)
630 646 except error.LookupError:
631 647 pass
632 648 os.unlink(os.path.join(repo.path, 'histedit-state'))
633 649 if os.path.exists(repo.sjoin('undo')):
634 650 os.unlink(repo.sjoin('undo'))
635 651
636 652
637 653 def writestate(repo, parentctxnode, created, replaced,
638 654 tmpnodes, existing, rules, keep, oldtip, replacemap):
639 655 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
640 656 pickle.dump((parentctxnode, created, replaced,
641 657 tmpnodes, existing, rules, keep, oldtip, replacemap),
642 658 fp)
643 659 fp.close()
644 660
645 661 def readstate(repo):
646 662 """Returns a tuple of (parentnode, created, replaced, tmp, existing, rules,
647 663 keep, oldtip, replacemap ).
648 664 """
649 665 fp = open(os.path.join(repo.path, 'histedit-state'))
650 666 return pickle.load(fp)
651 667
652 668
653 669 def verifyrules(rules, repo, ctxs):
654 670 """Verify that there exists exactly one edit rule per given changeset.
655 671
656 672 Will abort if there are to many or too few rules, a malformed rule,
657 673 or a rule on a changeset outside of the user-given range.
658 674 """
659 675 parsed = []
660 676 if len(rules) != len(ctxs):
661 677 raise util.Abort(_('must specify a rule for each changeset once'))
662 678 for r in rules:
663 679 if ' ' not in r:
664 680 raise util.Abort(_('malformed line "%s"') % r)
665 681 action, rest = r.split(' ', 1)
666 682 if ' ' in rest.strip():
667 683 ha, rest = rest.split(' ', 1)
668 684 else:
669 685 ha = r.strip()
670 686 try:
671 687 if repo[ha] not in ctxs:
672 688 raise util.Abort(
673 689 _('may not use changesets other than the ones listed'))
674 690 except error.RepoError:
675 691 raise util.Abort(_('unknown changeset %s listed') % ha)
676 692 if action not in actiontable:
677 693 raise util.Abort(_('unknown action "%s"') % action)
678 694 parsed.append([action, ha])
679 695 return parsed
680
681
682 cmdtable = {
683 "histedit":
684 (histedit,
685 [('', 'commands', '', _(
686 'Read history edits from the specified file.')),
687 ('c', 'continue', False, _('continue an edit already in progress')),
688 ('k', 'keep', False, _(
689 "don't strip old nodes after edit is complete")),
690 ('', 'abort', False, _('abort an edit in progress')),
691 ('o', 'outgoing', False, _('changesets not found in destination')),
692 ('f', 'force', False, _(
693 'force outgoing even for unrelated repositories')),
694 ('r', 'rev', [], _('first revision to be edited')),
695 ],
696 _("[PARENT]"),
697 ),
698 }
General Comments 0
You need to be logged in to leave comments. Login now