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