##// END OF EJS Templates
histedit: correct the number of added revisions in online help...
FUJIWARA Katsunori -
r18323:7648b87e stable
parent child Browse files
Show More
@@ -1,770 +1,770
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 c561b4e977df 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 c561b4e977df 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 If we clone the example repository above and add three more changes, such that
112 we have the following history::
111 If we clone the histedit-ed example repository above and add four more
112 changes, such that 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 # sort by revision number because it sound "right"
518 518 for prec in sorted(mapping, key=repo.changelog.rev):
519 519 succs = mapping[prec]
520 520 markers.append((repo[prec],
521 521 tuple(repo[s] for s in succs)))
522 522 if markers:
523 523 obsolete.createmarkers(repo, markers)
524 524 else:
525 525 cleanupnode(ui, repo, 'replaced', mapping)
526 526
527 527 cleanupnode(ui, repo, 'temp', tmpnodes)
528 528 os.unlink(os.path.join(repo.path, 'histedit-state'))
529 529 if os.path.exists(repo.sjoin('undo')):
530 530 os.unlink(repo.sjoin('undo'))
531 531
532 532
533 533 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
534 534 action, currentnode = rules.pop(0)
535 535 ctx = repo[currentnode]
536 536 # is there any new commit between the expected parent and "."
537 537 #
538 538 # note: does not take non linear new change in account (but previous
539 539 # implementation didn't used them anyway (issue3655)
540 540 newchildren = [c.node() for c in repo.set('(%d::.)', parentctx)]
541 541 if not newchildren:
542 542 # `parentctxnode` should match but no result. This means that
543 543 # currentnode is not a descendant from parentctxnode.
544 544 msg = _('working directory parent is not a descendant of %s')
545 545 hint = _('update to %s or descendant and run "hg histedit '
546 546 '--continue" again') % parentctx
547 547 raise util.Abort(msg % parentctx, hint=hint)
548 548 newchildren.pop(0) # remove parentctxnode
549 549 # Commit dirty working directory if necessary
550 550 new = None
551 551 m, a, r, d = repo.status()[:4]
552 552 if m or a or r or d:
553 553 # prepare the message for the commit to comes
554 554 if action in ('f', 'fold'):
555 555 message = 'fold-temp-revision %s' % currentnode
556 556 else:
557 557 message = ctx.description() + '\n'
558 558 if action in ('e', 'edit', 'm', 'mess'):
559 559 editor = cmdutil.commitforceeditor
560 560 else:
561 561 editor = False
562 562 new = repo.commit(text=message, user=ctx.user(),
563 563 date=ctx.date(), extra=ctx.extra(),
564 564 editor=editor)
565 565 if new is not None:
566 566 newchildren.append(new)
567 567
568 568 replacements = []
569 569 # track replacements
570 570 if ctx.node() not in newchildren:
571 571 # note: new children may be empty when the changeset is dropped.
572 572 # this happen e.g during conflicting pick where we revert content
573 573 # to parent.
574 574 replacements.append((ctx.node(), tuple(newchildren)))
575 575
576 576 if action in ('f', 'fold'):
577 577 # finalize fold operation if applicable
578 578 if new is None:
579 579 new = newchildren[-1]
580 580 else:
581 581 newchildren.pop() # remove new from internal changes
582 582 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new, opts,
583 583 newchildren)
584 584 replacements.extend(repl)
585 585 elif newchildren:
586 586 # otherwize update "parentctx" before proceding to further operation
587 587 parentctx = repo[newchildren[-1]]
588 588 return parentctx, replacements
589 589
590 590
591 591 def between(repo, old, new, keep):
592 592 """select and validate the set of revision to edit
593 593
594 594 When keep is false, the specified set can't have children."""
595 595 ctxs = list(repo.set('%n::%n', old, new))
596 596 if ctxs and not keep:
597 597 if repo.revs('(%ld::) - (%ld + hidden())', ctxs, ctxs):
598 598 raise util.Abort(_('cannot edit history that would orphan nodes'))
599 599 root = ctxs[0] # list is already sorted by repo.set
600 600 if not root.phase():
601 601 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
602 602 return [c.node() for c in ctxs]
603 603
604 604
605 605 def writestate(repo, parentnode, rules, keep, topmost, replacements):
606 606 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
607 607 pickle.dump((parentnode, rules, keep, topmost, replacements), fp)
608 608 fp.close()
609 609
610 610 def readstate(repo):
611 611 """Returns a tuple of (parentnode, rules, keep, topmost, replacements).
612 612 """
613 613 fp = open(os.path.join(repo.path, 'histedit-state'))
614 614 return pickle.load(fp)
615 615
616 616
617 617 def makedesc(c):
618 618 """build a initial action line for a ctx `c`
619 619
620 620 line are in the form:
621 621
622 622 pick <hash> <rev> <summary>
623 623 """
624 624 summary = ''
625 625 if c.description():
626 626 summary = c.description().splitlines()[0]
627 627 line = 'pick %s %d %s' % (c, c.rev(), summary)
628 628 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
629 629
630 630 def verifyrules(rules, repo, ctxs):
631 631 """Verify that there exists exactly one edit rule per given changeset.
632 632
633 633 Will abort if there are to many or too few rules, a malformed rule,
634 634 or a rule on a changeset outside of the user-given range.
635 635 """
636 636 parsed = []
637 637 if len(rules) != len(ctxs):
638 638 raise util.Abort(_('must specify a rule for each changeset once'))
639 639 for r in rules:
640 640 if ' ' not in r:
641 641 raise util.Abort(_('malformed line "%s"') % r)
642 642 action, rest = r.split(' ', 1)
643 643 if ' ' in rest.strip():
644 644 ha, rest = rest.split(' ', 1)
645 645 else:
646 646 ha = r.strip()
647 647 try:
648 648 if repo[ha] not in ctxs:
649 649 raise util.Abort(
650 650 _('may not use changesets other than the ones listed'))
651 651 except error.RepoError:
652 652 raise util.Abort(_('unknown changeset %s listed') % ha)
653 653 if action not in actiontable:
654 654 raise util.Abort(_('unknown action "%s"') % action)
655 655 parsed.append([action, ha])
656 656 return parsed
657 657
658 658 def processreplacement(repo, replacements):
659 659 """process the list of replacements to return
660 660
661 661 1) the final mapping between original and created nodes
662 662 2) the list of temporary node created by histedit
663 663 3) the list of new commit created by histedit"""
664 664 allsuccs = set()
665 665 replaced = set()
666 666 fullmapping = {}
667 667 # initialise basic set
668 668 # fullmapping record all operation recorded in replacement
669 669 for rep in replacements:
670 670 allsuccs.update(rep[1])
671 671 replaced.add(rep[0])
672 672 fullmapping.setdefault(rep[0], set()).update(rep[1])
673 673 new = allsuccs - replaced
674 674 tmpnodes = allsuccs & replaced
675 675 # Reduce content fullmapping into direct relation between original nodes
676 676 # and final node created during history edition
677 677 # Dropped changeset are replaced by an empty list
678 678 toproceed = set(fullmapping)
679 679 final = {}
680 680 while toproceed:
681 681 for x in list(toproceed):
682 682 succs = fullmapping[x]
683 683 for s in list(succs):
684 684 if s in toproceed:
685 685 # non final node with unknown closure
686 686 # We can't process this now
687 687 break
688 688 elif s in final:
689 689 # non final node, replace with closure
690 690 succs.remove(s)
691 691 succs.update(final[s])
692 692 else:
693 693 final[x] = succs
694 694 toproceed.remove(x)
695 695 # remove tmpnodes from final mapping
696 696 for n in tmpnodes:
697 697 del final[n]
698 698 # we expect all changes involved in final to exist in the repo
699 699 # turn `final` into list (topologically sorted)
700 700 nm = repo.changelog.nodemap
701 701 for prec, succs in final.items():
702 702 final[prec] = sorted(succs, key=nm.get)
703 703
704 704 # computed topmost element (necessary for bookmark)
705 705 if new:
706 706 newtopmost = sorted(new, key=repo.changelog.rev)[-1]
707 707 elif not final:
708 708 # Nothing rewritten at all. we won't need `newtopmost`
709 709 # It is the same as `oldtopmost` and `processreplacement` know it
710 710 newtopmost = None
711 711 else:
712 712 # every body died. The newtopmost is the parent of the root.
713 713 newtopmost = repo[sorted(final, key=repo.changelog.rev)[0]].p1().node()
714 714
715 715 return final, tmpnodes, new, newtopmost
716 716
717 717 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
718 718 """Move bookmark from old to newly created node"""
719 719 if not mapping:
720 720 # if nothing got rewritten there is not purpose for this function
721 721 return
722 722 moves = []
723 723 for bk, old in repo._bookmarks.iteritems():
724 724 if old == oldtopmost:
725 725 # special case ensure bookmark stay on tip.
726 726 #
727 727 # This is arguably a feature and we may only want that for the
728 728 # active bookmark. But the behavior is kept compatible with the old
729 729 # version for now.
730 730 moves.append((bk, newtopmost))
731 731 continue
732 732 base = old
733 733 new = mapping.get(base, None)
734 734 if new is None:
735 735 continue
736 736 while not new:
737 737 # base is killed, trying with parent
738 738 base = repo[base].p1().node()
739 739 new = mapping.get(base, (base,))
740 740 # nothing to move
741 741 moves.append((bk, new[-1]))
742 742 if moves:
743 743 for mark, new in moves:
744 744 old = repo._bookmarks[mark]
745 745 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
746 746 % (mark, node.short(old), node.short(new)))
747 747 repo._bookmarks[mark] = new
748 748 bookmarks.write(repo)
749 749
750 750 def cleanupnode(ui, repo, name, nodes):
751 751 """strip a group of nodes from the repository
752 752
753 753 The set of node to strip may contains unknown nodes."""
754 754 ui.debug('should strip %s nodes %s\n' %
755 755 (name, ', '.join([node.short(n) for n in nodes])))
756 756 lock = None
757 757 try:
758 758 lock = repo.lock()
759 759 # Find all node that need to be stripped
760 760 # (we hg %lr instead of %ln to silently ignore unknown item
761 761 nm = repo.changelog.nodemap
762 762 nodes = [n for n in nodes if n in nm]
763 763 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
764 764 for c in roots:
765 765 # We should process node in reverse order to strip tip most first.
766 766 # but this trigger a bug in changegroup hook.
767 767 # This would reduce bundle overhead
768 768 repair.strip(ui, repo, c)
769 769 finally:
770 770 lockmod.release(lock)
General Comments 0
You need to be logged in to leave comments. Login now