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