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