##// END OF EJS Templates
histedit: process bookmarks in sorted order
Mads Kiilerich -
r18370:c605e12d default
parent child Browse files
Show More
@@ -1,771 +1,771 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 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 111 If we clone the histedit-ed example repository above and add four more
112 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 cmdutil
148 148 from mercurial import discovery
149 149 from mercurial import error
150 150 from mercurial import copies
151 151 from mercurial import context
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 repair
156 156 from mercurial import scmutil
157 157 from mercurial import util
158 158 from mercurial import obsolete
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 Commit message is edited in all cases.
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 new = repo[n]
289 289 return new, [(oldctx.node(), (n,))]
290 290
291 291
292 292 def edit(ui, repo, ctx, ha, opts):
293 293 oldctx = repo[ha]
294 294 hg.update(repo, ctx.node())
295 295 applychanges(ui, repo, oldctx, opts)
296 296 raise util.Abort(_('Make changes as needed, you may commit or record as '
297 297 'needed now.\nWhen you are finished, run hg'
298 298 ' histedit --continue to resume.'))
299 299
300 300 def fold(ui, repo, ctx, ha, opts):
301 301 oldctx = repo[ha]
302 302 hg.update(repo, ctx.node())
303 303 stats = applychanges(ui, repo, oldctx, opts)
304 304 if stats and stats[3] > 0:
305 305 raise util.Abort(_('Fix up the change and run '
306 306 'hg histedit --continue'))
307 307 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(),
308 308 date=oldctx.date(), extra=oldctx.extra())
309 309 if n is None:
310 310 ui.warn(_('%s: empty changeset')
311 311 % node.hex(ha))
312 312 return ctx, []
313 313 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
314 314
315 315 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
316 316 parent = ctx.parents()[0].node()
317 317 hg.update(repo, parent)
318 318 ### prepare new commit data
319 319 commitopts = opts.copy()
320 320 # username
321 321 if ctx.user() == oldctx.user():
322 322 username = ctx.user()
323 323 else:
324 324 username = ui.username()
325 325 commitopts['user'] = username
326 326 # commit message
327 327 newmessage = '\n***\n'.join(
328 328 [ctx.description()] +
329 329 [repo[r].description() for r in internalchanges] +
330 330 [oldctx.description()]) + '\n'
331 331 commitopts['message'] = newmessage
332 332 # date
333 333 commitopts['date'] = max(ctx.date(), oldctx.date())
334 334 n = collapse(repo, ctx, repo[newnode], commitopts)
335 335 if n is None:
336 336 return ctx, []
337 337 hg.update(repo, n)
338 338 replacements = [(oldctx.node(), (newnode,)),
339 339 (ctx.node(), (n,)),
340 340 (newnode, (n,)),
341 341 ]
342 342 for ich in internalchanges:
343 343 replacements.append((ich, (n,)))
344 344 return repo[n], replacements
345 345
346 346 def drop(ui, repo, ctx, ha, opts):
347 347 return ctx, [(repo[ha].node(), ())]
348 348
349 349
350 350 def message(ui, repo, ctx, ha, opts):
351 351 oldctx = repo[ha]
352 352 hg.update(repo, ctx.node())
353 353 stats = applychanges(ui, repo, oldctx, opts)
354 354 if stats and stats[3] > 0:
355 355 raise util.Abort(_('Fix up the change and run '
356 356 'hg histedit --continue'))
357 357 message = oldctx.description() + '\n'
358 358 message = ui.edit(message, ui.username())
359 359 new = repo.commit(text=message, user=oldctx.user(), date=oldctx.date(),
360 360 extra=oldctx.extra())
361 361 newctx = repo[new]
362 362 if oldctx.node() != newctx.node():
363 363 return newctx, [(oldctx.node(), (new,))]
364 364 # We didn't make an edit, so just indicate no replaced nodes
365 365 return newctx, []
366 366
367 367 actiontable = {'p': pick,
368 368 'pick': pick,
369 369 'e': edit,
370 370 'edit': edit,
371 371 'f': fold,
372 372 'fold': fold,
373 373 'd': drop,
374 374 'drop': drop,
375 375 'm': message,
376 376 'mess': message,
377 377 }
378 378
379 379 @command('histedit',
380 380 [('', 'commands', '',
381 381 _('Read history edits from the specified file.')),
382 382 ('c', 'continue', False, _('continue an edit already in progress')),
383 383 ('k', 'keep', False,
384 384 _("don't strip old nodes after edit is complete")),
385 385 ('', 'abort', False, _('abort an edit in progress')),
386 386 ('o', 'outgoing', False, _('changesets not found in destination')),
387 387 ('f', 'force', False,
388 388 _('force outgoing even for unrelated repositories')),
389 389 ('r', 'rev', [], _('first revision to be edited'))],
390 390 _("[PARENT]"))
391 391 def histedit(ui, repo, *parent, **opts):
392 392 """interactively edit changeset history
393 393 """
394 394 # TODO only abort if we try and histedit mq patches, not just
395 395 # blanket if mq patches are applied somewhere
396 396 mq = getattr(repo, 'mq', None)
397 397 if mq and mq.applied:
398 398 raise util.Abort(_('source has mq patches applied'))
399 399
400 400 parent = list(parent) + opts.get('rev', [])
401 401 if opts.get('outgoing'):
402 402 if len(parent) > 1:
403 403 raise util.Abort(
404 404 _('only one repo argument allowed with --outgoing'))
405 405 elif parent:
406 406 parent = parent[0]
407 407
408 408 dest = ui.expandpath(parent or 'default-push', parent or 'default')
409 409 dest, revs = hg.parseurl(dest, None)[:2]
410 410 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
411 411
412 412 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
413 413 other = hg.peer(repo, opts, dest)
414 414
415 415 if revs:
416 416 revs = [repo.lookup(rev) for rev in revs]
417 417
418 418 parent = discovery.findcommonoutgoing(
419 419 repo, other, [], force=opts.get('force')).missing[0:1]
420 420 else:
421 421 if opts.get('force'):
422 422 raise util.Abort(_('--force only allowed with --outgoing'))
423 423
424 424 if opts.get('continue', False):
425 425 if len(parent) != 0:
426 426 raise util.Abort(_('no arguments allowed with --continue'))
427 427 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
428 428 currentparent, wantnull = repo.dirstate.parents()
429 429 parentctx = repo[parentctxnode]
430 430 parentctx, repl = bootstrapcontinue(ui, repo, parentctx, rules, opts)
431 431 replacements.extend(repl)
432 432 elif opts.get('abort', False):
433 433 if len(parent) != 0:
434 434 raise util.Abort(_('no arguments allowed with --abort'))
435 435 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
436 436 mapping, tmpnodes, leafs, _ntm = processreplacement(repo, replacements)
437 437 ui.debug('restore wc to old parent %s\n' % node.short(topmost))
438 438 hg.clean(repo, topmost)
439 439 cleanupnode(ui, repo, 'created', tmpnodes)
440 440 cleanupnode(ui, repo, 'temp', leafs)
441 441 os.unlink(os.path.join(repo.path, 'histedit-state'))
442 442 return
443 443 else:
444 444 cmdutil.bailifchanged(repo)
445 445 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
446 446 raise util.Abort(_('history edit already in progress, try '
447 447 '--continue or --abort'))
448 448
449 449 topmost, empty = repo.dirstate.parents()
450 450
451 451 if len(parent) != 1:
452 452 raise util.Abort(_('histedit requires exactly one parent revision'))
453 453 parent = scmutil.revsingle(repo, parent[0]).node()
454 454
455 455 keep = opts.get('keep', False)
456 456 revs = between(repo, parent, topmost, keep)
457 457 if not revs:
458 458 ui.warn(_('nothing to edit\n'))
459 459 return 1
460 460
461 461 ctxs = [repo[r] for r in revs]
462 462 rules = opts.get('commands', '')
463 463 if not rules:
464 464 rules = '\n'.join([makedesc(c) for c in ctxs])
465 465 rules += '\n\n'
466 466 rules += editcomment % (node.short(parent), node.short(topmost))
467 467 rules = ui.edit(rules, ui.username())
468 468 # Save edit rules in .hg/histedit-last-edit.txt in case
469 469 # the user needs to ask for help after something
470 470 # surprising happens.
471 471 f = open(repo.join('histedit-last-edit.txt'), 'w')
472 472 f.write(rules)
473 473 f.close()
474 474 else:
475 475 f = open(rules)
476 476 rules = f.read()
477 477 f.close()
478 478 rules = [l for l in (r.strip() for r in rules.splitlines())
479 479 if l and not l[0] == '#']
480 480 rules = verifyrules(rules, repo, ctxs)
481 481
482 482 parentctx = repo[parent].parents()[0]
483 483 keep = opts.get('keep', False)
484 484 replacements = []
485 485
486 486
487 487 while rules:
488 488 writestate(repo, parentctx.node(), rules, keep, topmost, replacements)
489 489 action, ha = rules.pop(0)
490 490 ui.debug('histedit: processing %s %s\n' % (action, ha))
491 491 actfunc = actiontable[action]
492 492 parentctx, replacement_ = actfunc(ui, repo, parentctx, ha, opts)
493 493 replacements.extend(replacement_)
494 494
495 495 hg.update(repo, parentctx.node())
496 496
497 497 mapping, tmpnodes, created, ntm = processreplacement(repo, replacements)
498 498 if mapping:
499 499 for prec, succs in mapping.iteritems():
500 500 if not succs:
501 501 ui.debug('histedit: %s is dropped\n' % node.short(prec))
502 502 else:
503 503 ui.debug('histedit: %s is replaced by %s\n' % (
504 504 node.short(prec), node.short(succs[0])))
505 505 if len(succs) > 1:
506 506 m = 'histedit: %s'
507 507 for n in succs[1:]:
508 508 ui.debug(m % node.short(n))
509 509
510 510 if not keep:
511 511 if mapping:
512 512 movebookmarks(ui, repo, mapping, topmost, ntm)
513 513 # TODO update mq state
514 514 if obsolete._enabled:
515 515 markers = []
516 516 # sort by revision number because it sound "right"
517 517 for prec in sorted(mapping, key=repo.changelog.rev):
518 518 succs = mapping[prec]
519 519 markers.append((repo[prec],
520 520 tuple(repo[s] for s in succs)))
521 521 if markers:
522 522 obsolete.createmarkers(repo, markers)
523 523 else:
524 524 cleanupnode(ui, repo, 'replaced', mapping)
525 525
526 526 cleanupnode(ui, repo, 'temp', tmpnodes)
527 527 os.unlink(os.path.join(repo.path, 'histedit-state'))
528 528 if os.path.exists(repo.sjoin('undo')):
529 529 os.unlink(repo.sjoin('undo'))
530 530
531 531
532 532 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
533 533 action, currentnode = rules.pop(0)
534 534 ctx = repo[currentnode]
535 535 # is there any new commit between the expected parent and "."
536 536 #
537 537 # note: does not take non linear new change in account (but previous
538 538 # implementation didn't used them anyway (issue3655)
539 539 newchildren = [c.node() for c in repo.set('(%d::.)', parentctx)]
540 540 if not newchildren:
541 541 # `parentctxnode` should match but no result. This means that
542 542 # currentnode is not a descendant from parentctxnode.
543 543 msg = _('working directory parent is not a descendant of %s')
544 544 hint = _('update to %s or descendant and run "hg histedit '
545 545 '--continue" again') % parentctx
546 546 raise util.Abort(msg % parentctx, hint=hint)
547 547 newchildren.pop(0) # remove parentctxnode
548 548 # Commit dirty working directory if necessary
549 549 new = None
550 550 m, a, r, d = repo.status()[:4]
551 551 if m or a or r or d:
552 552 # prepare the message for the commit to comes
553 553 if action in ('f', 'fold'):
554 554 message = 'fold-temp-revision %s' % currentnode
555 555 else:
556 556 message = ctx.description() + '\n'
557 557 if action in ('e', 'edit', 'm', 'mess'):
558 558 editor = cmdutil.commitforceeditor
559 559 else:
560 560 editor = False
561 561 new = repo.commit(text=message, user=ctx.user(),
562 562 date=ctx.date(), extra=ctx.extra(),
563 563 editor=editor)
564 564 if new is not None:
565 565 newchildren.append(new)
566 566
567 567 replacements = []
568 568 # track replacements
569 569 if ctx.node() not in newchildren:
570 570 # note: new children may be empty when the changeset is dropped.
571 571 # this happen e.g during conflicting pick where we revert content
572 572 # to parent.
573 573 replacements.append((ctx.node(), tuple(newchildren)))
574 574
575 575 if action in ('f', 'fold'):
576 576 # finalize fold operation if applicable
577 577 if new is None:
578 578 new = newchildren[-1]
579 579 else:
580 580 newchildren.pop() # remove new from internal changes
581 581 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new, opts,
582 582 newchildren)
583 583 replacements.extend(repl)
584 584 elif newchildren:
585 585 # otherwize update "parentctx" before proceding to further operation
586 586 parentctx = repo[newchildren[-1]]
587 587 return parentctx, replacements
588 588
589 589
590 590 def between(repo, old, new, keep):
591 591 """select and validate the set of revision to edit
592 592
593 593 When keep is false, the specified set can't have children."""
594 594 ctxs = list(repo.set('%n::%n', old, new))
595 595 if ctxs and not keep:
596 596 if (not obsolete._enabled and
597 597 repo.revs('(%ld::) - (%ld)', 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 for bk, old in repo._bookmarks.iteritems():
723 for bk, old in sorted(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 marks = repo._bookmarks
744 744 for mark, new in moves:
745 745 old = marks[mark]
746 746 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
747 747 % (mark, node.short(old), node.short(new)))
748 748 marks[mark] = new
749 749 marks.write()
750 750
751 751 def cleanupnode(ui, repo, name, nodes):
752 752 """strip a group of nodes from the repository
753 753
754 754 The set of node to strip may contains unknown nodes."""
755 755 ui.debug('should strip %s nodes %s\n' %
756 756 (name, ', '.join([node.short(n) for n in nodes])))
757 757 lock = None
758 758 try:
759 759 lock = repo.lock()
760 760 # Find all node that need to be stripped
761 761 # (we hg %lr instead of %ln to silently ignore unknown item
762 762 nm = repo.changelog.nodemap
763 763 nodes = [n for n in nodes if n in nm]
764 764 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
765 765 for c in roots:
766 766 # We should process node in reverse order to strip tip most first.
767 767 # but this trigger a bug in changegroup hook.
768 768 # This would reduce bundle overhead
769 769 repair.strip(ui, repo, c)
770 770 finally:
771 771 lockmod.release(lock)
@@ -1,179 +1,179 b''
1 1 $ . "$TESTDIR/histedit-helpers.sh"
2 2
3 3 $ cat >> $HGRCPATH <<EOF
4 4 > [extensions]
5 5 > graphlog=
6 6 > histedit=
7 7 > EOF
8 8
9 9 $ hg init r
10 10 $ cd r
11 11
12 12 $ for x in a b c d e f ; do
13 13 > echo $x > $x
14 14 > hg add $x
15 15 > hg ci -m $x
16 16 > done
17 17
18 18 $ hg book -r 1 will-move-backwards
19 19 $ hg book -r 2 two
20 20 $ hg book -r 2 also-two
21 21 $ hg book -r 3 three
22 22 $ hg book -r 4 four
23 23 $ hg book -r tip five
24 24 $ hg log --graph
25 25 @ changeset: 5:652413bf663e
26 26 | bookmark: five
27 27 | tag: tip
28 28 | user: test
29 29 | date: Thu Jan 01 00:00:00 1970 +0000
30 30 | summary: f
31 31 |
32 32 o changeset: 4:e860deea161a
33 33 | bookmark: four
34 34 | user: test
35 35 | date: Thu Jan 01 00:00:00 1970 +0000
36 36 | summary: e
37 37 |
38 38 o changeset: 3:055a42cdd887
39 39 | bookmark: three
40 40 | user: test
41 41 | date: Thu Jan 01 00:00:00 1970 +0000
42 42 | summary: d
43 43 |
44 44 o changeset: 2:177f92b77385
45 45 | bookmark: also-two
46 46 | bookmark: two
47 47 | user: test
48 48 | date: Thu Jan 01 00:00:00 1970 +0000
49 49 | summary: c
50 50 |
51 51 o changeset: 1:d2ae7f538514
52 52 | bookmark: will-move-backwards
53 53 | user: test
54 54 | date: Thu Jan 01 00:00:00 1970 +0000
55 55 | summary: b
56 56 |
57 57 o changeset: 0:cb9a9f314b8b
58 58 user: test
59 59 date: Thu Jan 01 00:00:00 1970 +0000
60 60 summary: a
61 61
62 62 $ HGEDITOR=cat hg histedit 1
63 63 pick d2ae7f538514 1 b
64 64 pick 177f92b77385 2 c
65 65 pick 055a42cdd887 3 d
66 66 pick e860deea161a 4 e
67 67 pick 652413bf663e 5 f
68 68
69 69 # Edit history between d2ae7f538514 and 652413bf663e
70 70 #
71 71 # Commands:
72 72 # p, pick = use commit
73 73 # e, edit = use commit, but stop for amending
74 74 # f, fold = use commit, but fold into previous commit (combines N and N-1)
75 75 # d, drop = remove commit from history
76 76 # m, mess = edit message without changing commit content
77 77 #
78 78 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
79 79 $ cat >> commands.txt <<EOF
80 80 > pick 177f92b77385 2 c
81 81 > drop d2ae7f538514 1 b
82 82 > pick 055a42cdd887 3 d
83 83 > fold e860deea161a 4 e
84 84 > pick 652413bf663e 5 f
85 85 > EOF
86 86 $ hg histedit 1 --commands commands.txt --verbose | grep histedit
87 histedit: moving bookmarks two from 177f92b77385 to d36c0562f908
88 histedit: moving bookmarks three from 055a42cdd887 to ae467701c500
87 histedit: moving bookmarks also-two from 177f92b77385 to d36c0562f908
88 histedit: moving bookmarks five from 652413bf663e to 0efacef7cb48
89 89 histedit: moving bookmarks four from e860deea161a to ae467701c500
90 histedit: moving bookmarks also-two from 177f92b77385 to d36c0562f908
90 histedit: moving bookmarks three from 055a42cdd887 to ae467701c500
91 histedit: moving bookmarks two from 177f92b77385 to d36c0562f908
91 92 histedit: moving bookmarks will-move-backwards from d2ae7f538514 to cb9a9f314b8b
92 histedit: moving bookmarks five from 652413bf663e to 0efacef7cb48
93 93 saved backup bundle to $TESTTMP/r/.hg/strip-backup/d2ae7f538514-backup.hg (glob)
94 94 saved backup bundle to $TESTTMP/r/.hg/strip-backup/34a9919932c1-backup.hg (glob)
95 95 $ hg log --graph
96 96 @ changeset: 3:0efacef7cb48
97 97 | bookmark: five
98 98 | tag: tip
99 99 | user: test
100 100 | date: Thu Jan 01 00:00:00 1970 +0000
101 101 | summary: f
102 102 |
103 103 o changeset: 2:ae467701c500
104 104 | bookmark: four
105 105 | bookmark: three
106 106 | user: test
107 107 | date: Thu Jan 01 00:00:00 1970 +0000
108 108 | summary: d
109 109 |
110 110 o changeset: 1:d36c0562f908
111 111 | bookmark: also-two
112 112 | bookmark: two
113 113 | user: test
114 114 | date: Thu Jan 01 00:00:00 1970 +0000
115 115 | summary: c
116 116 |
117 117 o changeset: 0:cb9a9f314b8b
118 118 bookmark: will-move-backwards
119 119 user: test
120 120 date: Thu Jan 01 00:00:00 1970 +0000
121 121 summary: a
122 122
123 123 $ HGEDITOR=cat hg histedit 1
124 124 pick d36c0562f908 1 c
125 125 pick ae467701c500 2 d
126 126 pick 0efacef7cb48 3 f
127 127
128 128 # Edit history between d36c0562f908 and 0efacef7cb48
129 129 #
130 130 # Commands:
131 131 # p, pick = use commit
132 132 # e, edit = use commit, but stop for amending
133 133 # f, fold = use commit, but fold into previous commit (combines N and N-1)
134 134 # d, drop = remove commit from history
135 135 # m, mess = edit message without changing commit content
136 136 #
137 137 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
138 138 $ cat > commands.txt << EOF
139 139 > pick d36c0562f908 1 c
140 140 > pick 0efacef7cb48 3 f
141 141 > pick ae467701c500 2 d
142 142 > EOF
143 143 $ hg histedit 1 --commands commands.txt --verbose | grep histedit
144 histedit: moving bookmarks three from ae467701c500 to 1be9c35b4cb2
144 histedit: moving bookmarks five from 0efacef7cb48 to 1be9c35b4cb2
145 145 histedit: moving bookmarks four from ae467701c500 to 1be9c35b4cb2
146 histedit: moving bookmarks five from 0efacef7cb48 to 1be9c35b4cb2
146 histedit: moving bookmarks three from ae467701c500 to 1be9c35b4cb2
147 147 saved backup bundle to $TESTTMP/r/.hg/strip-backup/ae467701c500-backup.hg (glob)
148 148
149 149 We expect 'five' to stay at tip, since the tipmost bookmark is most
150 150 likely the useful signal.
151 151
152 152 $ hg log --graph
153 153 @ changeset: 3:1be9c35b4cb2
154 154 | bookmark: five
155 155 | bookmark: four
156 156 | bookmark: three
157 157 | tag: tip
158 158 | user: test
159 159 | date: Thu Jan 01 00:00:00 1970 +0000
160 160 | summary: d
161 161 |
162 162 o changeset: 2:7c044e3e33a9
163 163 | user: test
164 164 | date: Thu Jan 01 00:00:00 1970 +0000
165 165 | summary: f
166 166 |
167 167 o changeset: 1:d36c0562f908
168 168 | bookmark: also-two
169 169 | bookmark: two
170 170 | user: test
171 171 | date: Thu Jan 01 00:00:00 1970 +0000
172 172 | summary: c
173 173 |
174 174 o changeset: 0:cb9a9f314b8b
175 175 bookmark: will-move-backwards
176 176 user: test
177 177 date: Thu Jan 01 00:00:00 1970 +0000
178 178 summary: a
179 179
General Comments 0
You need to be logged in to leave comments. Login now