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