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