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