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