##// END OF EJS Templates
histedit: clean abort when there is nothing to edit
Pierre-Yves David -
r17766:d9da3275 default
parent child Browse files
Show More
@@ -1,765 +1,768
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 if not revs:
459 ui.warn(_('nothing to edit\n'))
460 return 1
458 461
459 462 ctxs = [repo[r] for r in revs]
460 463 rules = opts.get('commands', '')
461 464 if not rules:
462 465 rules = '\n'.join([makedesc(c) for c in ctxs])
463 466 rules += '\n\n'
464 467 rules += editcomment % (node.short(parent), node.short(topmost))
465 468 rules = ui.edit(rules, ui.username())
466 469 # Save edit rules in .hg/histedit-last-edit.txt in case
467 470 # the user needs to ask for help after something
468 471 # surprising happens.
469 472 f = open(repo.join('histedit-last-edit.txt'), 'w')
470 473 f.write(rules)
471 474 f.close()
472 475 else:
473 476 f = open(rules)
474 477 rules = f.read()
475 478 f.close()
476 479 rules = [l for l in (r.strip() for r in rules.splitlines())
477 480 if l and not l[0] == '#']
478 481 rules = verifyrules(rules, repo, ctxs)
479 482
480 483 parentctx = repo[parent].parents()[0]
481 484 keep = opts.get('keep', False)
482 485 replacements = []
483 486
484 487
485 488 while rules:
486 489 writestate(repo, parentctx.node(), rules, keep, topmost, replacements)
487 490 action, ha = rules.pop(0)
488 491 ui.debug('histedit: processing %s %s\n' % (action, ha))
489 492 actfunc = actiontable[action]
490 493 parentctx, replacement_ = actfunc(ui, repo, parentctx, ha, opts)
491 494 replacements.extend(replacement_)
492 495
493 496 hg.update(repo, parentctx.node())
494 497
495 498 mapping, tmpnodes, created, ntm = processreplacement(repo, replacements)
496 499 if mapping:
497 500 for prec, succs in mapping.iteritems():
498 501 if not succs:
499 502 ui.debug('histedit: %s is dropped\n' % node.short(prec))
500 503 else:
501 504 ui.debug('histedit: %s is replaced by %s\n' % (
502 505 node.short(prec), node.short(succs[0])))
503 506 if len(succs) > 1:
504 507 m = 'histedit: %s'
505 508 for n in succs[1:]:
506 509 ui.debug(m % node.short(n))
507 510
508 511 if not keep:
509 512 if mapping:
510 513 movebookmarks(ui, repo, mapping, topmost, ntm)
511 514 # TODO update mq state
512 515 if obsolete._enabled:
513 516 markers = []
514 517 for prec, succs in mapping.iteritems():
515 518 markers.append((repo[prec],
516 519 tuple(repo[s] for s in succs)))
517 520 if markers:
518 521 obsolete.createmarkers(repo, markers)
519 522 else:
520 523 cleanupnode(ui, repo, 'replaced', mapping)
521 524
522 525 cleanupnode(ui, repo, 'temp', tmpnodes)
523 526 os.unlink(os.path.join(repo.path, 'histedit-state'))
524 527 if os.path.exists(repo.sjoin('undo')):
525 528 os.unlink(repo.sjoin('undo'))
526 529
527 530
528 531 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
529 532 action, currentnode = rules.pop(0)
530 533 ctx = repo[currentnode]
531 534 # is there any new commit between the expected parent and "."
532 535 #
533 536 # note: does not take non linear new change in account (but previous
534 537 # implementation didn't used them anyway (issue3655)
535 538 newchildren = [c.node() for c in repo.set('(%d::.)', parentctx)]
536 539 if not newchildren:
537 540 # `parentctxnode` should match but no result. This means that
538 541 # currentnode is not a descendant from parentctxnode.
539 542 msg = _('working directory parent is not a descendant of %s')
540 543 hint = _('update to %s or descendant and run "hg histedit '
541 544 '--continue" again') % parentctx
542 545 raise util.Abort(msg % parentctx, hint=hint)
543 546 newchildren.pop(0) # remove parentctxnode
544 547 # Commit dirty working directory if necessary
545 548 new = None
546 549 m, a, r, d = repo.status()[:4]
547 550 if m or a or r or d:
548 551 # prepare the message for the commit to comes
549 552 if action in ('f', 'fold'):
550 553 message = 'fold-temp-revision %s' % currentnode
551 554 else:
552 555 message = ctx.description() + '\n'
553 556 if action in ('e', 'edit', 'm', 'mess'):
554 557 editor = cmdutil.commitforceeditor
555 558 else:
556 559 editor = False
557 560 new = repo.commit(text=message, user=ctx.user(),
558 561 date=ctx.date(), extra=ctx.extra(),
559 562 editor=editor)
560 563 if new is not None:
561 564 newchildren.append(new)
562 565
563 566 replacements = []
564 567 # track replacements
565 568 if ctx.node() not in newchildren:
566 569 # note: new children may be empty when the changeset is dropped.
567 570 # this happen e.g during conflicting pick where we revert content
568 571 # to parent.
569 572 replacements.append((ctx.node(), tuple(newchildren)))
570 573
571 574 if action in ('f', 'fold'):
572 575 # finalize fold operation if applicable
573 576 if new is None:
574 577 new = newchildren[-1]
575 578 else:
576 579 newchildren.pop() # remove new from internal changes
577 580 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new, opts,
578 581 newchildren)
579 582 replacements.extend(repl)
580 583 elif newchildren:
581 584 # otherwize update "parentctx" before proceding to further operation
582 585 parentctx = repo[newchildren[-1]]
583 586 return parentctx, replacements
584 587
585 588
586 589 def between(repo, old, new, keep):
587 590 """select and validate the set of revision to edit
588 591
589 592 When keep is false, the specified set can't have children."""
590 593 ctxs = list(repo.set('%n::%n', old, new))
591 if not keep:
594 if ctxs and not keep:
592 595 if repo.revs('(%ld::) - (%ld + hidden())', ctxs, ctxs):
593 596 raise util.Abort(_('cannot edit history that would orphan nodes'))
594 597 root = min(ctxs)
595 598 if not root.phase():
596 599 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
597 600 return [c.node() for c in ctxs]
598 601
599 602
600 603 def writestate(repo, parentnode, rules, keep, topmost, replacements):
601 604 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
602 605 pickle.dump((parentnode, rules, keep, topmost, replacements), fp)
603 606 fp.close()
604 607
605 608 def readstate(repo):
606 609 """Returns a tuple of (parentnode, rules, keep, topmost, replacements).
607 610 """
608 611 fp = open(os.path.join(repo.path, 'histedit-state'))
609 612 return pickle.load(fp)
610 613
611 614
612 615 def makedesc(c):
613 616 """build a initial action line for a ctx `c`
614 617
615 618 line are in the form:
616 619
617 620 pick <hash> <rev> <summary>
618 621 """
619 622 summary = ''
620 623 if c.description():
621 624 summary = c.description().splitlines()[0]
622 625 line = 'pick %s %d %s' % (c, c.rev(), summary)
623 626 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
624 627
625 628 def verifyrules(rules, repo, ctxs):
626 629 """Verify that there exists exactly one edit rule per given changeset.
627 630
628 631 Will abort if there are to many or too few rules, a malformed rule,
629 632 or a rule on a changeset outside of the user-given range.
630 633 """
631 634 parsed = []
632 635 if len(rules) != len(ctxs):
633 636 raise util.Abort(_('must specify a rule for each changeset once'))
634 637 for r in rules:
635 638 if ' ' not in r:
636 639 raise util.Abort(_('malformed line "%s"') % r)
637 640 action, rest = r.split(' ', 1)
638 641 if ' ' in rest.strip():
639 642 ha, rest = rest.split(' ', 1)
640 643 else:
641 644 ha = r.strip()
642 645 try:
643 646 if repo[ha] not in ctxs:
644 647 raise util.Abort(
645 648 _('may not use changesets other than the ones listed'))
646 649 except error.RepoError:
647 650 raise util.Abort(_('unknown changeset %s listed') % ha)
648 651 if action not in actiontable:
649 652 raise util.Abort(_('unknown action "%s"') % action)
650 653 parsed.append([action, ha])
651 654 return parsed
652 655
653 656 def processreplacement(repo, replacements):
654 657 """process the list of replacements to return
655 658
656 659 1) the final mapping between original and created nodes
657 660 2) the list of temporary node created by histedit
658 661 3) the list of new commit created by histedit"""
659 662 allsuccs = set()
660 663 replaced = set()
661 664 fullmapping = {}
662 665 # initialise basic set
663 666 # fullmapping record all operation recorded in replacement
664 667 for rep in replacements:
665 668 allsuccs.update(rep[1])
666 669 replaced.add(rep[0])
667 670 fullmapping.setdefault(rep[0], set()).update(rep[1])
668 671 new = allsuccs - replaced
669 672 tmpnodes = allsuccs & replaced
670 673 # Reduce content fullmapping into direct relation between original nodes
671 674 # and final node created during history edition
672 675 # Dropped changeset are replaced by an empty list
673 676 toproceed = set(fullmapping)
674 677 final = {}
675 678 while toproceed:
676 679 for x in list(toproceed):
677 680 succs = fullmapping[x]
678 681 for s in list(succs):
679 682 if s in toproceed:
680 683 # non final node with unknown closure
681 684 # We can't process this now
682 685 break
683 686 elif s in final:
684 687 # non final node, replace with closure
685 688 succs.remove(s)
686 689 succs.update(final[s])
687 690 else:
688 691 final[x] = succs
689 692 toproceed.remove(x)
690 693 # remove tmpnodes from final mapping
691 694 for n in tmpnodes:
692 695 del final[n]
693 696 # we expect all changes involved in final to exist in the repo
694 697 # turn `final` into list (topologically sorted)
695 698 nm = repo.changelog.nodemap
696 699 for prec, succs in final.items():
697 700 final[prec] = sorted(succs, key=nm.get)
698 701
699 702 # computed topmost element (necessary for bookmark)
700 703 if new:
701 704 newtopmost = max(new, key=repo.changelog.rev)
702 705 elif not final:
703 706 # Nothing rewritten at all. we won't need `newtopmost`
704 707 # It is the same as `oldtopmost` and `processreplacement` know it
705 708 newtopmost = None
706 709 else:
707 710 # every body died. The newtopmost is the parent of the root.
708 711 newtopmost = repo[min(final, key=repo.changelog.rev)].p1().node()
709 712
710 713 return final, tmpnodes, new, newtopmost
711 714
712 715 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
713 716 """Move bookmark from old to newly created node"""
714 717 if not mapping:
715 718 # if nothing got rewritten there is not purpose for this function
716 719 return
717 720 moves = []
718 721 for bk, old in repo._bookmarks.iteritems():
719 722 if old == oldtopmost:
720 723 # special case ensure bookmark stay on tip.
721 724 #
722 725 # This is arguably a feature and we may only want that for the
723 726 # active bookmark. But the behavior is kept compatible with the old
724 727 # version for now.
725 728 moves.append((bk, newtopmost))
726 729 continue
727 730 base = old
728 731 new = mapping.get(base, None)
729 732 if new is None:
730 733 continue
731 734 while not new:
732 735 # base is killed, trying with parent
733 736 base = repo[base].p1().node()
734 737 new = mapping.get(base, (base,))
735 738 # nothing to move
736 739 moves.append((bk, new[-1]))
737 740 if moves:
738 741 for mark, new in moves:
739 742 old = repo._bookmarks[mark]
740 743 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
741 744 % (mark, node.short(old), node.short(new)))
742 745 repo._bookmarks[mark] = new
743 746 bookmarks.write(repo)
744 747
745 748 def cleanupnode(ui, repo, name, nodes):
746 749 """strip a group of nodes from the repository
747 750
748 751 The set of node to strip may contains unknown nodes."""
749 752 ui.debug('should strip %s nodes %s\n' %
750 753 (name, ', '.join([node.short(n) for n in nodes])))
751 754 lock = None
752 755 try:
753 756 lock = repo.lock()
754 757 # Find all node that need to be stripped
755 758 # (we hg %lr instead of %ln to silently ignore unknown item
756 759 nm = repo.changelog.nodemap
757 760 nodes = [n for n in nodes if n in nm]
758 761 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
759 762 for c in roots:
760 763 # We should process node in reverse order to strip tip most first.
761 764 # but this trigger a bug in changegroup hook.
762 765 # This would reduce bundle overhead
763 766 repair.strip(ui, repo, c)
764 767 finally:
765 768 lockmod.release(lock)
@@ -1,62 +1,69
1 1 This test requires parentrevspec support in revsets, so check for that
2 2 and skip the test if we're on an unusual hg that supports .t tests but
3 3 not parentrevspec.
4 4 $ python -c 'from mercurial import revset ; revset.methods["parentpost"]' || exit 80
5 5
6 6 Enable extensions used by this test.
7 7 $ cat >>$HGRCPATH <<EOF
8 8 > [extensions]
9 9 > graphlog=
10 10 > histedit=
11 11 > EOF
12 12
13 13 Repo setup.
14 14 $ hg init foo
15 15 $ cd foo
16 16 $ echo alpha >> alpha
17 17 $ hg addr
18 18 adding alpha
19 19 $ hg ci -m one
20 20 $ echo alpha >> alpha
21 21 $ hg ci -m two
22 22 $ echo alpha >> alpha
23 23 $ hg ci -m three
24 24 $ echo alpha >> alpha
25 25 $ hg ci -m four
26 26 $ echo alpha >> alpha
27 27 $ hg ci -m five
28 28
29 29 $ hg log --style compact --graph
30 30 @ 4[tip] 08d98a8350f3 1970-01-01 00:00 +0000 test
31 31 | five
32 32 |
33 33 o 3 c8e68270e35a 1970-01-01 00:00 +0000 test
34 34 | four
35 35 |
36 36 o 2 eb57da33312f 1970-01-01 00:00 +0000 test
37 37 | three
38 38 |
39 39 o 1 579e40513370 1970-01-01 00:00 +0000 test
40 40 | two
41 41 |
42 42 o 0 6058cbb6cfd7 1970-01-01 00:00 +0000 test
43 43 one
44 44
45 45
46 46 Run a dummy edit to make sure we get tip^^ correctly via revsingle.
47 47 $ HGEDITOR=cat hg histedit "tip^^"
48 48 pick eb57da33312f 2 three
49 49 pick c8e68270e35a 3 four
50 50 pick 08d98a8350f3 4 five
51 51
52 52 # Edit history between eb57da33312f and 08d98a8350f3
53 53 #
54 54 # Commands:
55 55 # p, pick = use commit
56 56 # e, edit = use commit, but stop for amending
57 57 # f, fold = use commit, but fold into previous commit (combines N and N-1)
58 58 # d, drop = remove commit from history
59 59 # m, mess = edit message without changing commit content
60 60 #
61 61 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
62 62
63 Run on a revision not ancestors of the current working directory.
64
65 $ hg up 2
66 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
67 $ hg histedit -r 4
68 nothing to edit
69 [1]
General Comments 0
You need to be logged in to leave comments. Login now