##// END OF EJS Templates
histedit: pass 'editform' argument to 'cmdutil.getcommiteditor'...
FUJIWARA Katsunori -
r22002:a44b7b6f default
parent child Browse files
Show More
@@ -1,929 +1,933
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 # Commits are listed from least to most recent
34 34 #
35 35 # Commands:
36 36 # p, pick = use commit
37 37 # e, edit = use commit, but stop for amending
38 38 # f, fold = use commit, but combine it with the one above
39 39 # d, drop = remove commit from history
40 40 # m, mess = edit message without changing commit content
41 41 #
42 42
43 43 In this file, lines beginning with ``#`` are ignored. You must specify a rule
44 44 for each revision in your history. For example, if you had meant to add gamma
45 45 before beta, and then wanted to add delta in the same revision as beta, you
46 46 would reorganize the file to look like this::
47 47
48 48 pick 030b686bedc4 Add gamma
49 49 pick c561b4e977df Add beta
50 50 fold 7c2fd3b9020c Add delta
51 51
52 52 # Edit history between c561b4e977df and 7c2fd3b9020c
53 53 #
54 54 # Commits are listed from least to most recent
55 55 #
56 56 # Commands:
57 57 # p, pick = use commit
58 58 # e, edit = use commit, but stop for amending
59 59 # f, fold = use commit, but combine it with the one above
60 60 # d, drop = remove commit from history
61 61 # m, mess = edit message without changing commit content
62 62 #
63 63
64 64 At which point you close the editor and ``histedit`` starts working. When you
65 65 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
66 66 those revisions together, offering you a chance to clean up the commit message::
67 67
68 68 Add beta
69 69 ***
70 70 Add delta
71 71
72 72 Edit the commit message to your liking, then close the editor. For
73 73 this example, let's assume that the commit message was changed to
74 74 ``Add beta and delta.`` After histedit has run and had a chance to
75 75 remove any old or temporary revisions it needed, the history looks
76 76 like this::
77 77
78 78 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
79 79 | Add beta and delta.
80 80 |
81 81 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
82 82 | Add gamma
83 83 |
84 84 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
85 85 Add alpha
86 86
87 87 Note that ``histedit`` does *not* remove any revisions (even its own temporary
88 88 ones) until after it has completed all the editing operations, so it will
89 89 probably perform several strip operations when it's done. For the above example,
90 90 it had to run strip twice. Strip can be slow depending on a variety of factors,
91 91 so you might need to be a little patient. You can choose to keep the original
92 92 revisions by passing the ``--keep`` flag.
93 93
94 94 The ``edit`` operation will drop you back to a command prompt,
95 95 allowing you to edit files freely, or even use ``hg record`` to commit
96 96 some changes as a separate commit. When you're done, any remaining
97 97 uncommitted changes will be committed as well. When done, run ``hg
98 98 histedit --continue`` to finish this step. You'll be prompted for a
99 99 new commit message, but the default commit message will be the
100 100 original message for the ``edit`` ed revision.
101 101
102 102 The ``message`` operation will give you a chance to revise a commit
103 103 message without changing the contents. It's a shortcut for doing
104 104 ``edit`` immediately followed by `hg histedit --continue``.
105 105
106 106 If ``histedit`` encounters a conflict when moving a revision (while
107 107 handling ``pick`` or ``fold``), it'll stop in a similar manner to
108 108 ``edit`` with the difference that it won't prompt you for a commit
109 109 message when done. If you decide at this point that you don't like how
110 110 much work it will be to rearrange history, or that you made a mistake,
111 111 you can use ``hg histedit --abort`` to abandon the new changes you
112 112 have made and return to the state before you attempted to edit your
113 113 history.
114 114
115 115 If we clone the histedit-ed example repository above and add four more
116 116 changes, such that we have the following history::
117 117
118 118 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
119 119 | Add theta
120 120 |
121 121 o 5 140988835471 2009-04-27 18:04 -0500 stefan
122 122 | Add eta
123 123 |
124 124 o 4 122930637314 2009-04-27 18:04 -0500 stefan
125 125 | Add zeta
126 126 |
127 127 o 3 836302820282 2009-04-27 18:04 -0500 stefan
128 128 | Add epsilon
129 129 |
130 130 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
131 131 | Add beta and delta.
132 132 |
133 133 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
134 134 | Add gamma
135 135 |
136 136 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
137 137 Add alpha
138 138
139 139 If you run ``hg histedit --outgoing`` on the clone then it is the same
140 140 as running ``hg histedit 836302820282``. If you need plan to push to a
141 141 repository that Mercurial does not detect to be related to the source
142 142 repo, you can add a ``--force`` option.
143 143 """
144 144
145 145 try:
146 146 import cPickle as pickle
147 147 pickle.dump # import now
148 148 except ImportError:
149 149 import pickle
150 150 import os
151 151 import sys
152 152
153 153 from mercurial import cmdutil
154 154 from mercurial import discovery
155 155 from mercurial import error
156 156 from mercurial import copies
157 157 from mercurial import context
158 158 from mercurial import hg
159 159 from mercurial import node
160 160 from mercurial import repair
161 161 from mercurial import scmutil
162 162 from mercurial import util
163 163 from mercurial import obsolete
164 164 from mercurial import merge as mergemod
165 165 from mercurial.lock import release
166 166 from mercurial.i18n import _
167 167
168 168 cmdtable = {}
169 169 command = cmdutil.command(cmdtable)
170 170
171 171 testedwith = 'internal'
172 172
173 173 # i18n: command names and abbreviations must remain untranslated
174 174 editcomment = _("""# Edit history between %s and %s
175 175 #
176 176 # Commits are listed from least to most recent
177 177 #
178 178 # Commands:
179 179 # p, pick = use commit
180 180 # e, edit = use commit, but stop for amending
181 181 # f, fold = use commit, but combine it with the one above
182 182 # d, drop = remove commit from history
183 183 # m, mess = edit message without changing commit content
184 184 #
185 185 """)
186 186
187 187 def commitfuncfor(repo, src):
188 188 """Build a commit function for the replacement of <src>
189 189
190 190 This function ensure we apply the same treatment to all changesets.
191 191
192 192 - Add a 'histedit_source' entry in extra.
193 193
194 194 Note that fold have its own separated logic because its handling is a bit
195 195 different and not easily factored out of the fold method.
196 196 """
197 197 phasemin = src.phase()
198 198 def commitfunc(**kwargs):
199 199 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
200 200 try:
201 201 repo.ui.setconfig('phases', 'new-commit', phasemin,
202 202 'histedit')
203 203 extra = kwargs.get('extra', {}).copy()
204 204 extra['histedit_source'] = src.hex()
205 205 kwargs['extra'] = extra
206 206 return repo.commit(**kwargs)
207 207 finally:
208 208 repo.ui.restoreconfig(phasebackup)
209 209 return commitfunc
210 210
211 211
212 212
213 213 def applychanges(ui, repo, ctx, opts):
214 214 """Merge changeset from ctx (only) in the current working directory"""
215 215 wcpar = repo.dirstate.parents()[0]
216 216 if ctx.p1().node() == wcpar:
217 217 # edition ar "in place" we do not need to make any merge,
218 218 # just applies changes on parent for edition
219 219 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
220 220 stats = None
221 221 else:
222 222 try:
223 223 # ui.forcemerge is an internal variable, do not document
224 224 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
225 225 'histedit')
226 226 stats = mergemod.update(repo, ctx.node(), True, True, False,
227 227 ctx.p1().node())
228 228 finally:
229 229 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
230 230 repo.setparents(wcpar, node.nullid)
231 231 repo.dirstate.write()
232 232 # fix up dirstate for copies and renames
233 233 cmdutil.duplicatecopies(repo, ctx.rev(), ctx.p1().rev())
234 234 return stats
235 235
236 236 def collapse(repo, first, last, commitopts):
237 237 """collapse the set of revisions from first to last as new one.
238 238
239 239 Expected commit options are:
240 240 - message
241 241 - date
242 242 - username
243 243 Commit message is edited in all cases.
244 244
245 245 This function works in memory."""
246 246 ctxs = list(repo.set('%d::%d', first, last))
247 247 if not ctxs:
248 248 return None
249 249 base = first.parents()[0]
250 250
251 251 # commit a new version of the old changeset, including the update
252 252 # collect all files which might be affected
253 253 files = set()
254 254 for ctx in ctxs:
255 255 files.update(ctx.files())
256 256
257 257 # Recompute copies (avoid recording a -> b -> a)
258 258 copied = copies.pathcopies(base, last)
259 259
260 260 # prune files which were reverted by the updates
261 261 def samefile(f):
262 262 if f in last.manifest():
263 263 a = last.filectx(f)
264 264 if f in base.manifest():
265 265 b = base.filectx(f)
266 266 return (a.data() == b.data()
267 267 and a.flags() == b.flags())
268 268 else:
269 269 return False
270 270 else:
271 271 return f not in base.manifest()
272 272 files = [f for f in files if not samefile(f)]
273 273 # commit version of these files as defined by head
274 274 headmf = last.manifest()
275 275 def filectxfn(repo, ctx, path):
276 276 if path in headmf:
277 277 fctx = last[path]
278 278 flags = fctx.flags()
279 279 mctx = context.memfilectx(repo,
280 280 fctx.path(), fctx.data(),
281 281 islink='l' in flags,
282 282 isexec='x' in flags,
283 283 copied=copied.get(path))
284 284 return mctx
285 285 raise IOError()
286 286
287 287 if commitopts.get('message'):
288 288 message = commitopts['message']
289 289 else:
290 290 message = first.description()
291 291 user = commitopts.get('user')
292 292 date = commitopts.get('date')
293 293 extra = commitopts.get('extra')
294 294
295 295 parents = (first.p1().node(), first.p2().node())
296 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
296 297 new = context.memctx(repo,
297 298 parents=parents,
298 299 text=message,
299 300 files=files,
300 301 filectxfn=filectxfn,
301 302 user=user,
302 303 date=date,
303 304 extra=extra,
304 editor=cmdutil.getcommiteditor(edit=True))
305 editor=editor)
305 306 return repo.commitctx(new)
306 307
307 308 def pick(ui, repo, ctx, ha, opts):
308 309 oldctx = repo[ha]
309 310 if oldctx.parents()[0] == ctx:
310 311 ui.debug('node %s unchanged\n' % ha)
311 312 return oldctx, []
312 313 hg.update(repo, ctx.node())
313 314 stats = applychanges(ui, repo, oldctx, opts)
314 315 if stats and stats[3] > 0:
315 316 raise error.InterventionRequired(_('Fix up the change and run '
316 317 'hg histedit --continue'))
317 318 # drop the second merge parent
318 319 commit = commitfuncfor(repo, oldctx)
319 320 n = commit(text=oldctx.description(), user=oldctx.user(),
320 321 date=oldctx.date(), extra=oldctx.extra())
321 322 if n is None:
322 323 ui.warn(_('%s: empty changeset\n')
323 324 % node.hex(ha))
324 325 return ctx, []
325 326 new = repo[n]
326 327 return new, [(oldctx.node(), (n,))]
327 328
328 329
329 330 def edit(ui, repo, ctx, ha, opts):
330 331 oldctx = repo[ha]
331 332 hg.update(repo, ctx.node())
332 333 applychanges(ui, repo, oldctx, opts)
333 334 raise error.InterventionRequired(
334 335 _('Make changes as needed, you may commit or record as needed now.\n'
335 336 'When you are finished, run hg histedit --continue to resume.'))
336 337
337 338 def fold(ui, repo, ctx, ha, opts):
338 339 oldctx = repo[ha]
339 340 hg.update(repo, ctx.node())
340 341 stats = applychanges(ui, repo, oldctx, opts)
341 342 if stats and stats[3] > 0:
342 343 raise error.InterventionRequired(
343 344 _('Fix up the change and run hg histedit --continue'))
344 345 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(),
345 346 date=oldctx.date(), extra=oldctx.extra())
346 347 if n is None:
347 348 ui.warn(_('%s: empty changeset')
348 349 % node.hex(ha))
349 350 return ctx, []
350 351 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
351 352
352 353 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
353 354 parent = ctx.parents()[0].node()
354 355 hg.update(repo, parent)
355 356 ### prepare new commit data
356 357 commitopts = opts.copy()
357 358 # username
358 359 if ctx.user() == oldctx.user():
359 360 username = ctx.user()
360 361 else:
361 362 username = ui.username()
362 363 commitopts['user'] = username
363 364 # commit message
364 365 newmessage = '\n***\n'.join(
365 366 [ctx.description()] +
366 367 [repo[r].description() for r in internalchanges] +
367 368 [oldctx.description()]) + '\n'
368 369 commitopts['message'] = newmessage
369 370 # date
370 371 commitopts['date'] = max(ctx.date(), oldctx.date())
371 372 extra = ctx.extra().copy()
372 373 # histedit_source
373 374 # note: ctx is likely a temporary commit but that the best we can do here
374 375 # This is sufficient to solve issue3681 anyway
375 376 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
376 377 commitopts['extra'] = extra
377 378 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
378 379 try:
379 380 phasemin = max(ctx.phase(), oldctx.phase())
380 381 repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit')
381 382 n = collapse(repo, ctx, repo[newnode], commitopts)
382 383 finally:
383 384 repo.ui.restoreconfig(phasebackup)
384 385 if n is None:
385 386 return ctx, []
386 387 hg.update(repo, n)
387 388 replacements = [(oldctx.node(), (newnode,)),
388 389 (ctx.node(), (n,)),
389 390 (newnode, (n,)),
390 391 ]
391 392 for ich in internalchanges:
392 393 replacements.append((ich, (n,)))
393 394 return repo[n], replacements
394 395
395 396 def drop(ui, repo, ctx, ha, opts):
396 397 return ctx, [(repo[ha].node(), ())]
397 398
398 399
399 400 def message(ui, repo, ctx, ha, opts):
400 401 oldctx = repo[ha]
401 402 hg.update(repo, ctx.node())
402 403 stats = applychanges(ui, repo, oldctx, opts)
403 404 if stats and stats[3] > 0:
404 405 raise error.InterventionRequired(
405 406 _('Fix up the change and run hg histedit --continue'))
406 407 message = oldctx.description()
407 408 commit = commitfuncfor(repo, oldctx)
409 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
408 410 new = commit(text=message, user=oldctx.user(), date=oldctx.date(),
409 411 extra=oldctx.extra(),
410 editor=cmdutil.getcommiteditor(edit=True))
412 editor=editor)
411 413 newctx = repo[new]
412 414 if oldctx.node() != newctx.node():
413 415 return newctx, [(oldctx.node(), (new,))]
414 416 # We didn't make an edit, so just indicate no replaced nodes
415 417 return newctx, []
416 418
417 419 def findoutgoing(ui, repo, remote=None, force=False, opts={}):
418 420 """utility function to find the first outgoing changeset
419 421
420 422 Used by initialisation code"""
421 423 dest = ui.expandpath(remote or 'default-push', remote or 'default')
422 424 dest, revs = hg.parseurl(dest, None)[:2]
423 425 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
424 426
425 427 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
426 428 other = hg.peer(repo, opts, dest)
427 429
428 430 if revs:
429 431 revs = [repo.lookup(rev) for rev in revs]
430 432
431 433 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
432 434 if not outgoing.missing:
433 435 raise util.Abort(_('no outgoing ancestors'))
434 436 roots = list(repo.revs("roots(%ln)", outgoing.missing))
435 437 if 1 < len(roots):
436 438 msg = _('there are ambiguous outgoing revisions')
437 439 hint = _('see "hg help histedit" for more detail')
438 440 raise util.Abort(msg, hint=hint)
439 441 return repo.lookup(roots[0])
440 442
441 443 actiontable = {'p': pick,
442 444 'pick': pick,
443 445 'e': edit,
444 446 'edit': edit,
445 447 'f': fold,
446 448 'fold': fold,
447 449 'd': drop,
448 450 'drop': drop,
449 451 'm': message,
450 452 'mess': message,
451 453 }
452 454
453 455 @command('histedit',
454 456 [('', 'commands', '',
455 457 _('Read history edits from the specified file.')),
456 458 ('c', 'continue', False, _('continue an edit already in progress')),
457 459 ('k', 'keep', False,
458 460 _("don't strip old nodes after edit is complete")),
459 461 ('', 'abort', False, _('abort an edit in progress')),
460 462 ('o', 'outgoing', False, _('changesets not found in destination')),
461 463 ('f', 'force', False,
462 464 _('force outgoing even for unrelated repositories')),
463 465 ('r', 'rev', [], _('first revision to be edited'))],
464 466 _("ANCESTOR | --outgoing [URL]"))
465 467 def histedit(ui, repo, *freeargs, **opts):
466 468 """interactively edit changeset history
467 469
468 470 This command edits changesets between ANCESTOR and the parent of
469 471 the working directory.
470 472
471 473 With --outgoing, this edits changesets not found in the
472 474 destination repository. If URL of the destination is omitted, the
473 475 'default-push' (or 'default') path will be used.
474 476
475 477 For safety, this command is aborted, also if there are ambiguous
476 478 outgoing revisions which may confuse users: for example, there are
477 479 multiple branches containing outgoing revisions.
478 480
479 481 Use "min(outgoing() and ::.)" or similar revset specification
480 482 instead of --outgoing to specify edit target revision exactly in
481 483 such ambiguous situation. See :hg:`help revsets` for detail about
482 484 selecting revisions.
483 485
484 486 Returns 0 on success, 1 if user intervention is required (not only
485 487 for intentional "edit" command, but also for resolving unexpected
486 488 conflicts).
487 489 """
488 490 lock = wlock = None
489 491 try:
490 492 wlock = repo.wlock()
491 493 lock = repo.lock()
492 494 _histedit(ui, repo, *freeargs, **opts)
493 495 finally:
494 496 release(lock, wlock)
495 497
496 498 def _histedit(ui, repo, *freeargs, **opts):
497 499 # TODO only abort if we try and histedit mq patches, not just
498 500 # blanket if mq patches are applied somewhere
499 501 mq = getattr(repo, 'mq', None)
500 502 if mq and mq.applied:
501 503 raise util.Abort(_('source has mq patches applied'))
502 504
503 505 # basic argument incompatibility processing
504 506 outg = opts.get('outgoing')
505 507 cont = opts.get('continue')
506 508 abort = opts.get('abort')
507 509 force = opts.get('force')
508 510 rules = opts.get('commands', '')
509 511 revs = opts.get('rev', [])
510 512 goal = 'new' # This invocation goal, in new, continue, abort
511 513 if force and not outg:
512 514 raise util.Abort(_('--force only allowed with --outgoing'))
513 515 if cont:
514 516 if util.any((outg, abort, revs, freeargs, rules)):
515 517 raise util.Abort(_('no arguments allowed with --continue'))
516 518 goal = 'continue'
517 519 elif abort:
518 520 if util.any((outg, revs, freeargs, rules)):
519 521 raise util.Abort(_('no arguments allowed with --abort'))
520 522 goal = 'abort'
521 523 else:
522 524 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
523 525 raise util.Abort(_('history edit already in progress, try '
524 526 '--continue or --abort'))
525 527 if outg:
526 528 if revs:
527 529 raise util.Abort(_('no revisions allowed with --outgoing'))
528 530 if len(freeargs) > 1:
529 531 raise util.Abort(
530 532 _('only one repo argument allowed with --outgoing'))
531 533 else:
532 534 revs.extend(freeargs)
533 535 if len(revs) != 1:
534 536 raise util.Abort(
535 537 _('histedit requires exactly one ancestor revision'))
536 538
537 539
538 540 if goal == 'continue':
539 541 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
540 542 parentctx = repo[parentctxnode]
541 543 parentctx, repl = bootstrapcontinue(ui, repo, parentctx, rules, opts)
542 544 replacements.extend(repl)
543 545 elif goal == 'abort':
544 546 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
545 547 mapping, tmpnodes, leafs, _ntm = processreplacement(repo, replacements)
546 548 ui.debug('restore wc to old parent %s\n' % node.short(topmost))
547 549 # check whether we should update away
548 550 parentnodes = [c.node() for c in repo[None].parents()]
549 551 for n in leafs | set([parentctxnode]):
550 552 if n in parentnodes:
551 553 hg.clean(repo, topmost)
552 554 break
553 555 else:
554 556 pass
555 557 cleanupnode(ui, repo, 'created', tmpnodes)
556 558 cleanupnode(ui, repo, 'temp', leafs)
557 559 os.unlink(os.path.join(repo.path, 'histedit-state'))
558 560 return
559 561 else:
560 562 cmdutil.checkunfinished(repo)
561 563 cmdutil.bailifchanged(repo)
562 564
563 565 topmost, empty = repo.dirstate.parents()
564 566 if outg:
565 567 if freeargs:
566 568 remote = freeargs[0]
567 569 else:
568 570 remote = None
569 571 root = findoutgoing(ui, repo, remote, force, opts)
570 572 else:
571 573 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
572 574 if len(rr) != 1:
573 575 raise util.Abort(_('The specified revisions must have '
574 576 'exactly one common root'))
575 577 root = rr[0].node()
576 578
577 579 keep = opts.get('keep', False)
578 580 revs = between(repo, root, topmost, keep)
579 581 if not revs:
580 582 raise util.Abort(_('%s is not an ancestor of working directory') %
581 583 node.short(root))
582 584
583 585 ctxs = [repo[r] for r in revs]
584 586 if not rules:
585 587 rules = '\n'.join([makedesc(c) for c in ctxs])
586 588 rules += '\n\n'
587 589 rules += editcomment % (node.short(root), node.short(topmost))
588 590 rules = ui.edit(rules, ui.username())
589 591 # Save edit rules in .hg/histedit-last-edit.txt in case
590 592 # the user needs to ask for help after something
591 593 # surprising happens.
592 594 f = open(repo.join('histedit-last-edit.txt'), 'w')
593 595 f.write(rules)
594 596 f.close()
595 597 else:
596 598 if rules == '-':
597 599 f = sys.stdin
598 600 else:
599 601 f = open(rules)
600 602 rules = f.read()
601 603 f.close()
602 604 rules = [l for l in (r.strip() for r in rules.splitlines())
603 605 if l and not l[0] == '#']
604 606 rules = verifyrules(rules, repo, ctxs)
605 607
606 608 parentctx = repo[root].parents()[0]
607 609 keep = opts.get('keep', False)
608 610 replacements = []
609 611
610 612
611 613 while rules:
612 614 writestate(repo, parentctx.node(), rules, keep, topmost, replacements)
613 615 action, ha = rules.pop(0)
614 616 ui.debug('histedit: processing %s %s\n' % (action, ha))
615 617 actfunc = actiontable[action]
616 618 parentctx, replacement_ = actfunc(ui, repo, parentctx, ha, opts)
617 619 replacements.extend(replacement_)
618 620
619 621 hg.update(repo, parentctx.node())
620 622
621 623 mapping, tmpnodes, created, ntm = processreplacement(repo, replacements)
622 624 if mapping:
623 625 for prec, succs in mapping.iteritems():
624 626 if not succs:
625 627 ui.debug('histedit: %s is dropped\n' % node.short(prec))
626 628 else:
627 629 ui.debug('histedit: %s is replaced by %s\n' % (
628 630 node.short(prec), node.short(succs[0])))
629 631 if len(succs) > 1:
630 632 m = 'histedit: %s'
631 633 for n in succs[1:]:
632 634 ui.debug(m % node.short(n))
633 635
634 636 if not keep:
635 637 if mapping:
636 638 movebookmarks(ui, repo, mapping, topmost, ntm)
637 639 # TODO update mq state
638 640 if obsolete._enabled:
639 641 markers = []
640 642 # sort by revision number because it sound "right"
641 643 for prec in sorted(mapping, key=repo.changelog.rev):
642 644 succs = mapping[prec]
643 645 markers.append((repo[prec],
644 646 tuple(repo[s] for s in succs)))
645 647 if markers:
646 648 obsolete.createmarkers(repo, markers)
647 649 else:
648 650 cleanupnode(ui, repo, 'replaced', mapping)
649 651
650 652 cleanupnode(ui, repo, 'temp', tmpnodes)
651 653 os.unlink(os.path.join(repo.path, 'histedit-state'))
652 654 if os.path.exists(repo.sjoin('undo')):
653 655 os.unlink(repo.sjoin('undo'))
654 656
655 657 def gatherchildren(repo, ctx):
656 658 # is there any new commit between the expected parent and "."
657 659 #
658 660 # note: does not take non linear new change in account (but previous
659 661 # implementation didn't used them anyway (issue3655)
660 662 newchildren = [c.node() for c in repo.set('(%d::.)', ctx)]
661 663 if ctx.node() != node.nullid:
662 664 if not newchildren:
663 665 # `ctx` should match but no result. This means that
664 666 # currentnode is not a descendant from ctx.
665 667 msg = _('%s is not an ancestor of working directory')
666 668 hint = _('use "histedit --abort" to clear broken state')
667 669 raise util.Abort(msg % ctx, hint=hint)
668 670 newchildren.pop(0) # remove ctx
669 671 return newchildren
670 672
671 673 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
672 674 action, currentnode = rules.pop(0)
673 675 ctx = repo[currentnode]
674 676
675 677 newchildren = gatherchildren(repo, parentctx)
676 678
677 679 # Commit dirty working directory if necessary
678 680 new = None
679 681 m, a, r, d = repo.status()[:4]
680 682 if m or a or r or d:
681 683 # prepare the message for the commit to comes
682 684 if action in ('f', 'fold'):
683 685 message = 'fold-temp-revision %s' % currentnode
684 686 else:
685 687 message = ctx.description()
686 688 editopt = action in ('e', 'edit', 'm', 'mess')
687 editor = cmdutil.getcommiteditor(edit=editopt)
689 canonaction = {'e': 'edit', 'm': 'mess', 'p': 'pick'}
690 editform = 'histedit.%s' % canonaction.get(action, action)
691 editor = cmdutil.getcommiteditor(edit=editopt, editform=editform)
688 692 commit = commitfuncfor(repo, ctx)
689 693 new = commit(text=message, user=ctx.user(),
690 694 date=ctx.date(), extra=ctx.extra(),
691 695 editor=editor)
692 696 if new is not None:
693 697 newchildren.append(new)
694 698
695 699 replacements = []
696 700 # track replacements
697 701 if ctx.node() not in newchildren:
698 702 # note: new children may be empty when the changeset is dropped.
699 703 # this happen e.g during conflicting pick where we revert content
700 704 # to parent.
701 705 replacements.append((ctx.node(), tuple(newchildren)))
702 706
703 707 if action in ('f', 'fold'):
704 708 if newchildren:
705 709 # finalize fold operation if applicable
706 710 if new is None:
707 711 new = newchildren[-1]
708 712 else:
709 713 newchildren.pop() # remove new from internal changes
710 714 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new, opts,
711 715 newchildren)
712 716 replacements.extend(repl)
713 717 else:
714 718 # newchildren is empty if the fold did not result in any commit
715 719 # this happen when all folded change are discarded during the
716 720 # merge.
717 721 replacements.append((ctx.node(), (parentctx.node(),)))
718 722 elif newchildren:
719 723 # otherwise update "parentctx" before proceeding to further operation
720 724 parentctx = repo[newchildren[-1]]
721 725 return parentctx, replacements
722 726
723 727
724 728 def between(repo, old, new, keep):
725 729 """select and validate the set of revision to edit
726 730
727 731 When keep is false, the specified set can't have children."""
728 732 ctxs = list(repo.set('%n::%n', old, new))
729 733 if ctxs and not keep:
730 734 if (not obsolete._enabled and
731 735 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
732 736 raise util.Abort(_('cannot edit history that would orphan nodes'))
733 737 if repo.revs('(%ld) and merge()', ctxs):
734 738 raise util.Abort(_('cannot edit history that contains merges'))
735 739 root = ctxs[0] # list is already sorted by repo.set
736 740 if not root.phase():
737 741 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
738 742 return [c.node() for c in ctxs]
739 743
740 744
741 745 def writestate(repo, parentnode, rules, keep, topmost, replacements):
742 746 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
743 747 pickle.dump((parentnode, rules, keep, topmost, replacements), fp)
744 748 fp.close()
745 749
746 750 def readstate(repo):
747 751 """Returns a tuple of (parentnode, rules, keep, topmost, replacements).
748 752 """
749 753 fp = open(os.path.join(repo.path, 'histedit-state'))
750 754 return pickle.load(fp)
751 755
752 756
753 757 def makedesc(c):
754 758 """build a initial action line for a ctx `c`
755 759
756 760 line are in the form:
757 761
758 762 pick <hash> <rev> <summary>
759 763 """
760 764 summary = ''
761 765 if c.description():
762 766 summary = c.description().splitlines()[0]
763 767 line = 'pick %s %d %s' % (c, c.rev(), summary)
764 768 # trim to 80 columns so it's not stupidly wide in my editor
765 769 return util.ellipsis(line, 80)
766 770
767 771 def verifyrules(rules, repo, ctxs):
768 772 """Verify that there exists exactly one edit rule per given changeset.
769 773
770 774 Will abort if there are to many or too few rules, a malformed rule,
771 775 or a rule on a changeset outside of the user-given range.
772 776 """
773 777 parsed = []
774 778 expected = set(str(c) for c in ctxs)
775 779 seen = set()
776 780 for r in rules:
777 781 if ' ' not in r:
778 782 raise util.Abort(_('malformed line "%s"') % r)
779 783 action, rest = r.split(' ', 1)
780 784 ha = rest.strip().split(' ', 1)[0]
781 785 try:
782 786 ha = str(repo[ha]) # ensure its a short hash
783 787 except error.RepoError:
784 788 raise util.Abort(_('unknown changeset %s listed') % ha)
785 789 if ha not in expected:
786 790 raise util.Abort(
787 791 _('may not use changesets other than the ones listed'))
788 792 if ha in seen:
789 793 raise util.Abort(_('duplicated command for changeset %s') % ha)
790 794 seen.add(ha)
791 795 if action not in actiontable:
792 796 raise util.Abort(_('unknown action "%s"') % action)
793 797 parsed.append([action, ha])
794 798 missing = sorted(expected - seen) # sort to stabilize output
795 799 if missing:
796 800 raise util.Abort(_('missing rules for changeset %s') % missing[0],
797 801 hint=_('do you want to use the drop action?'))
798 802 return parsed
799 803
800 804 def processreplacement(repo, replacements):
801 805 """process the list of replacements to return
802 806
803 807 1) the final mapping between original and created nodes
804 808 2) the list of temporary node created by histedit
805 809 3) the list of new commit created by histedit"""
806 810 allsuccs = set()
807 811 replaced = set()
808 812 fullmapping = {}
809 813 # initialise basic set
810 814 # fullmapping record all operation recorded in replacement
811 815 for rep in replacements:
812 816 allsuccs.update(rep[1])
813 817 replaced.add(rep[0])
814 818 fullmapping.setdefault(rep[0], set()).update(rep[1])
815 819 new = allsuccs - replaced
816 820 tmpnodes = allsuccs & replaced
817 821 # Reduce content fullmapping into direct relation between original nodes
818 822 # and final node created during history edition
819 823 # Dropped changeset are replaced by an empty list
820 824 toproceed = set(fullmapping)
821 825 final = {}
822 826 while toproceed:
823 827 for x in list(toproceed):
824 828 succs = fullmapping[x]
825 829 for s in list(succs):
826 830 if s in toproceed:
827 831 # non final node with unknown closure
828 832 # We can't process this now
829 833 break
830 834 elif s in final:
831 835 # non final node, replace with closure
832 836 succs.remove(s)
833 837 succs.update(final[s])
834 838 else:
835 839 final[x] = succs
836 840 toproceed.remove(x)
837 841 # remove tmpnodes from final mapping
838 842 for n in tmpnodes:
839 843 del final[n]
840 844 # we expect all changes involved in final to exist in the repo
841 845 # turn `final` into list (topologically sorted)
842 846 nm = repo.changelog.nodemap
843 847 for prec, succs in final.items():
844 848 final[prec] = sorted(succs, key=nm.get)
845 849
846 850 # computed topmost element (necessary for bookmark)
847 851 if new:
848 852 newtopmost = sorted(new, key=repo.changelog.rev)[-1]
849 853 elif not final:
850 854 # Nothing rewritten at all. we won't need `newtopmost`
851 855 # It is the same as `oldtopmost` and `processreplacement` know it
852 856 newtopmost = None
853 857 else:
854 858 # every body died. The newtopmost is the parent of the root.
855 859 newtopmost = repo[sorted(final, key=repo.changelog.rev)[0]].p1().node()
856 860
857 861 return final, tmpnodes, new, newtopmost
858 862
859 863 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
860 864 """Move bookmark from old to newly created node"""
861 865 if not mapping:
862 866 # if nothing got rewritten there is not purpose for this function
863 867 return
864 868 moves = []
865 869 for bk, old in sorted(repo._bookmarks.iteritems()):
866 870 if old == oldtopmost:
867 871 # special case ensure bookmark stay on tip.
868 872 #
869 873 # This is arguably a feature and we may only want that for the
870 874 # active bookmark. But the behavior is kept compatible with the old
871 875 # version for now.
872 876 moves.append((bk, newtopmost))
873 877 continue
874 878 base = old
875 879 new = mapping.get(base, None)
876 880 if new is None:
877 881 continue
878 882 while not new:
879 883 # base is killed, trying with parent
880 884 base = repo[base].p1().node()
881 885 new = mapping.get(base, (base,))
882 886 # nothing to move
883 887 moves.append((bk, new[-1]))
884 888 if moves:
885 889 marks = repo._bookmarks
886 890 for mark, new in moves:
887 891 old = marks[mark]
888 892 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
889 893 % (mark, node.short(old), node.short(new)))
890 894 marks[mark] = new
891 895 marks.write()
892 896
893 897 def cleanupnode(ui, repo, name, nodes):
894 898 """strip a group of nodes from the repository
895 899
896 900 The set of node to strip may contains unknown nodes."""
897 901 ui.debug('should strip %s nodes %s\n' %
898 902 (name, ', '.join([node.short(n) for n in nodes])))
899 903 lock = None
900 904 try:
901 905 lock = repo.lock()
902 906 # Find all node that need to be stripped
903 907 # (we hg %lr instead of %ln to silently ignore unknown item
904 908 nm = repo.changelog.nodemap
905 909 nodes = [n for n in nodes if n in nm]
906 910 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
907 911 for c in roots:
908 912 # We should process node in reverse order to strip tip most first.
909 913 # but this trigger a bug in changegroup hook.
910 914 # This would reduce bundle overhead
911 915 repair.strip(ui, repo, c)
912 916 finally:
913 917 release(lock)
914 918
915 919 def summaryhook(ui, repo):
916 920 if not os.path.exists(repo.join('histedit-state')):
917 921 return
918 922 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
919 923 if rules:
920 924 # i18n: column positioning for "hg summary"
921 925 ui.write(_('hist: %s (histedit --continue)\n') %
922 926 (ui.label(_('%d remaining'), 'histedit.remaining') %
923 927 len(rules)))
924 928
925 929 def extsetup(ui):
926 930 cmdutil.summaryhooks.add('histedit', summaryhook)
927 931 cmdutil.unfinishedstates.append(
928 932 ['histedit-state', False, True, _('histedit in progress'),
929 933 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
General Comments 0
You need to be logged in to leave comments. Login now