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