##// END OF EJS Templates
histedit: abort if there are multiple roots in "--outgoing" revisions...
FUJIWARA Katsunori -
r19841:fab75342 stable
parent child Browse files
Show More
@@ -1,892 +1,897 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 return outgoing.missing[0]
429 roots = list(repo.revs("roots(%ln)", outgoing.missing))
430 if 1 < len(roots):
431 msg = _('there are ambiguous outgoing revisions')
432 hint = _('see "hg help histedit" for more detail')
433 raise util.Abort(msg, hint=hint)
434 return repo.lookup(roots[0])
430 435
431 436 actiontable = {'p': pick,
432 437 'pick': pick,
433 438 'e': edit,
434 439 'edit': edit,
435 440 'f': fold,
436 441 'fold': fold,
437 442 'd': drop,
438 443 'drop': drop,
439 444 'm': message,
440 445 'mess': message,
441 446 }
442 447
443 448 @command('histedit',
444 449 [('', 'commands', '',
445 450 _('Read history edits from the specified file.')),
446 451 ('c', 'continue', False, _('continue an edit already in progress')),
447 452 ('k', 'keep', False,
448 453 _("don't strip old nodes after edit is complete")),
449 454 ('', 'abort', False, _('abort an edit in progress')),
450 455 ('o', 'outgoing', False, _('changesets not found in destination')),
451 456 ('f', 'force', False,
452 457 _('force outgoing even for unrelated repositories')),
453 458 ('r', 'rev', [], _('first revision to be edited'))],
454 459 _("ANCESTOR | --outgoing [URL]"))
455 460 def histedit(ui, repo, *freeargs, **opts):
456 461 """interactively edit changeset history
457 462
458 463 This command edits changesets between ANCESTOR and the parent of
459 464 the working directory.
460 465
461 466 With --outgoing, this edits changesets not found in the
462 467 destination repository. If URL of the destination is omitted, the
463 468 'default-push' (or 'default') path will be used.
464 469 """
465 470 # TODO only abort if we try and histedit mq patches, not just
466 471 # blanket if mq patches are applied somewhere
467 472 mq = getattr(repo, 'mq', None)
468 473 if mq and mq.applied:
469 474 raise util.Abort(_('source has mq patches applied'))
470 475
471 476 # basic argument incompatibility processing
472 477 outg = opts.get('outgoing')
473 478 cont = opts.get('continue')
474 479 abort = opts.get('abort')
475 480 force = opts.get('force')
476 481 rules = opts.get('commands', '')
477 482 revs = opts.get('rev', [])
478 483 goal = 'new' # This invocation goal, in new, continue, abort
479 484 if force and not outg:
480 485 raise util.Abort(_('--force only allowed with --outgoing'))
481 486 if cont:
482 487 if util.any((outg, abort, revs, freeargs, rules)):
483 488 raise util.Abort(_('no arguments allowed with --continue'))
484 489 goal = 'continue'
485 490 elif abort:
486 491 if util.any((outg, revs, freeargs, rules)):
487 492 raise util.Abort(_('no arguments allowed with --abort'))
488 493 goal = 'abort'
489 494 else:
490 495 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
491 496 raise util.Abort(_('history edit already in progress, try '
492 497 '--continue or --abort'))
493 498 if outg:
494 499 if revs:
495 500 raise util.Abort(_('no revisions allowed with --outgoing'))
496 501 if len(freeargs) > 1:
497 502 raise util.Abort(
498 503 _('only one repo argument allowed with --outgoing'))
499 504 else:
500 505 revs.extend(freeargs)
501 506 if len(revs) != 1:
502 507 raise util.Abort(
503 508 _('histedit requires exactly one ancestor revision'))
504 509
505 510
506 511 if goal == 'continue':
507 512 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
508 513 currentparent, wantnull = repo.dirstate.parents()
509 514 parentctx = repo[parentctxnode]
510 515 parentctx, repl = bootstrapcontinue(ui, repo, parentctx, rules, opts)
511 516 replacements.extend(repl)
512 517 elif goal == 'abort':
513 518 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
514 519 mapping, tmpnodes, leafs, _ntm = processreplacement(repo, replacements)
515 520 ui.debug('restore wc to old parent %s\n' % node.short(topmost))
516 521 # check whether we should update away
517 522 parentnodes = [c.node() for c in repo[None].parents()]
518 523 for n in leafs | set([parentctxnode]):
519 524 if n in parentnodes:
520 525 hg.clean(repo, topmost)
521 526 break
522 527 else:
523 528 pass
524 529 cleanupnode(ui, repo, 'created', tmpnodes)
525 530 cleanupnode(ui, repo, 'temp', leafs)
526 531 os.unlink(os.path.join(repo.path, 'histedit-state'))
527 532 return
528 533 else:
529 534 cmdutil.checkunfinished(repo)
530 535 cmdutil.bailifchanged(repo)
531 536
532 537 topmost, empty = repo.dirstate.parents()
533 538 if outg:
534 539 if freeargs:
535 540 remote = freeargs[0]
536 541 else:
537 542 remote = None
538 543 root = findoutgoing(ui, repo, remote, force, opts)
539 544 else:
540 545 root = revs[0]
541 546 root = scmutil.revsingle(repo, root).node()
542 547
543 548 keep = opts.get('keep', False)
544 549 revs = between(repo, root, topmost, keep)
545 550 if not revs:
546 551 raise util.Abort(_('%s is not an ancestor of working directory') %
547 552 node.short(root))
548 553
549 554 ctxs = [repo[r] for r in revs]
550 555 if not rules:
551 556 rules = '\n'.join([makedesc(c) for c in ctxs])
552 557 rules += '\n\n'
553 558 rules += editcomment % (node.short(root), node.short(topmost))
554 559 rules = ui.edit(rules, ui.username())
555 560 # Save edit rules in .hg/histedit-last-edit.txt in case
556 561 # the user needs to ask for help after something
557 562 # surprising happens.
558 563 f = open(repo.join('histedit-last-edit.txt'), 'w')
559 564 f.write(rules)
560 565 f.close()
561 566 else:
562 567 if rules == '-':
563 568 f = sys.stdin
564 569 else:
565 570 f = open(rules)
566 571 rules = f.read()
567 572 f.close()
568 573 rules = [l for l in (r.strip() for r in rules.splitlines())
569 574 if l and not l[0] == '#']
570 575 rules = verifyrules(rules, repo, ctxs)
571 576
572 577 parentctx = repo[root].parents()[0]
573 578 keep = opts.get('keep', False)
574 579 replacements = []
575 580
576 581
577 582 while rules:
578 583 writestate(repo, parentctx.node(), rules, keep, topmost, replacements)
579 584 action, ha = rules.pop(0)
580 585 ui.debug('histedit: processing %s %s\n' % (action, ha))
581 586 actfunc = actiontable[action]
582 587 parentctx, replacement_ = actfunc(ui, repo, parentctx, ha, opts)
583 588 replacements.extend(replacement_)
584 589
585 590 hg.update(repo, parentctx.node())
586 591
587 592 mapping, tmpnodes, created, ntm = processreplacement(repo, replacements)
588 593 if mapping:
589 594 for prec, succs in mapping.iteritems():
590 595 if not succs:
591 596 ui.debug('histedit: %s is dropped\n' % node.short(prec))
592 597 else:
593 598 ui.debug('histedit: %s is replaced by %s\n' % (
594 599 node.short(prec), node.short(succs[0])))
595 600 if len(succs) > 1:
596 601 m = 'histedit: %s'
597 602 for n in succs[1:]:
598 603 ui.debug(m % node.short(n))
599 604
600 605 if not keep:
601 606 if mapping:
602 607 movebookmarks(ui, repo, mapping, topmost, ntm)
603 608 # TODO update mq state
604 609 if obsolete._enabled:
605 610 markers = []
606 611 # sort by revision number because it sound "right"
607 612 for prec in sorted(mapping, key=repo.changelog.rev):
608 613 succs = mapping[prec]
609 614 markers.append((repo[prec],
610 615 tuple(repo[s] for s in succs)))
611 616 if markers:
612 617 obsolete.createmarkers(repo, markers)
613 618 else:
614 619 cleanupnode(ui, repo, 'replaced', mapping)
615 620
616 621 cleanupnode(ui, repo, 'temp', tmpnodes)
617 622 os.unlink(os.path.join(repo.path, 'histedit-state'))
618 623 if os.path.exists(repo.sjoin('undo')):
619 624 os.unlink(repo.sjoin('undo'))
620 625
621 626
622 627 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
623 628 action, currentnode = rules.pop(0)
624 629 ctx = repo[currentnode]
625 630 # is there any new commit between the expected parent and "."
626 631 #
627 632 # note: does not take non linear new change in account (but previous
628 633 # implementation didn't used them anyway (issue3655)
629 634 newchildren = [c.node() for c in repo.set('(%d::.)', parentctx)]
630 635 if parentctx.node() != node.nullid:
631 636 if not newchildren:
632 637 # `parentctxnode` should match but no result. This means that
633 638 # currentnode is not a descendant from parentctxnode.
634 639 msg = _('%s is not an ancestor of working directory')
635 640 hint = _('update to %s or descendant and run "hg histedit '
636 641 '--continue" again') % parentctx
637 642 raise util.Abort(msg % parentctx, hint=hint)
638 643 newchildren.pop(0) # remove parentctxnode
639 644 # Commit dirty working directory if necessary
640 645 new = None
641 646 m, a, r, d = repo.status()[:4]
642 647 if m or a or r or d:
643 648 # prepare the message for the commit to comes
644 649 if action in ('f', 'fold'):
645 650 message = 'fold-temp-revision %s' % currentnode
646 651 else:
647 652 message = ctx.description() + '\n'
648 653 if action in ('e', 'edit', 'm', 'mess'):
649 654 editor = cmdutil.commitforceeditor
650 655 else:
651 656 editor = False
652 657 commit = commitfuncfor(repo, ctx)
653 658 new = commit(text=message, user=ctx.user(),
654 659 date=ctx.date(), extra=ctx.extra(),
655 660 editor=editor)
656 661 if new is not None:
657 662 newchildren.append(new)
658 663
659 664 replacements = []
660 665 # track replacements
661 666 if ctx.node() not in newchildren:
662 667 # note: new children may be empty when the changeset is dropped.
663 668 # this happen e.g during conflicting pick where we revert content
664 669 # to parent.
665 670 replacements.append((ctx.node(), tuple(newchildren)))
666 671
667 672 if action in ('f', 'fold'):
668 673 if newchildren:
669 674 # finalize fold operation if applicable
670 675 if new is None:
671 676 new = newchildren[-1]
672 677 else:
673 678 newchildren.pop() # remove new from internal changes
674 679 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new, opts,
675 680 newchildren)
676 681 replacements.extend(repl)
677 682 else:
678 683 # newchildren is empty if the fold did not result in any commit
679 684 # this happen when all folded change are discarded during the
680 685 # merge.
681 686 replacements.append((ctx.node(), (parentctx.node(),)))
682 687 elif newchildren:
683 688 # otherwise update "parentctx" before proceeding to further operation
684 689 parentctx = repo[newchildren[-1]]
685 690 return parentctx, replacements
686 691
687 692
688 693 def between(repo, old, new, keep):
689 694 """select and validate the set of revision to edit
690 695
691 696 When keep is false, the specified set can't have children."""
692 697 ctxs = list(repo.set('%n::%n', old, new))
693 698 if ctxs and not keep:
694 699 if (not obsolete._enabled and
695 700 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
696 701 raise util.Abort(_('cannot edit history that would orphan nodes'))
697 702 if repo.revs('(%ld) and merge()', ctxs):
698 703 raise util.Abort(_('cannot edit history that contains merges'))
699 704 root = ctxs[0] # list is already sorted by repo.set
700 705 if not root.phase():
701 706 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
702 707 return [c.node() for c in ctxs]
703 708
704 709
705 710 def writestate(repo, parentnode, rules, keep, topmost, replacements):
706 711 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
707 712 pickle.dump((parentnode, rules, keep, topmost, replacements), fp)
708 713 fp.close()
709 714
710 715 def readstate(repo):
711 716 """Returns a tuple of (parentnode, rules, keep, topmost, replacements).
712 717 """
713 718 fp = open(os.path.join(repo.path, 'histedit-state'))
714 719 return pickle.load(fp)
715 720
716 721
717 722 def makedesc(c):
718 723 """build a initial action line for a ctx `c`
719 724
720 725 line are in the form:
721 726
722 727 pick <hash> <rev> <summary>
723 728 """
724 729 summary = ''
725 730 if c.description():
726 731 summary = c.description().splitlines()[0]
727 732 line = 'pick %s %d %s' % (c, c.rev(), summary)
728 733 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
729 734
730 735 def verifyrules(rules, repo, ctxs):
731 736 """Verify that there exists exactly one edit rule per given changeset.
732 737
733 738 Will abort if there are to many or too few rules, a malformed rule,
734 739 or a rule on a changeset outside of the user-given range.
735 740 """
736 741 parsed = []
737 742 expected = set(str(c) for c in ctxs)
738 743 seen = set()
739 744 for r in rules:
740 745 if ' ' not in r:
741 746 raise util.Abort(_('malformed line "%s"') % r)
742 747 action, rest = r.split(' ', 1)
743 748 ha = rest.strip().split(' ', 1)[0]
744 749 try:
745 750 ha = str(repo[ha]) # ensure its a short hash
746 751 except error.RepoError:
747 752 raise util.Abort(_('unknown changeset %s listed') % ha)
748 753 if ha not in expected:
749 754 raise util.Abort(
750 755 _('may not use changesets other than the ones listed'))
751 756 if ha in seen:
752 757 raise util.Abort(_('duplicated command for changeset %s') % ha)
753 758 seen.add(ha)
754 759 if action not in actiontable:
755 760 raise util.Abort(_('unknown action "%s"') % action)
756 761 parsed.append([action, ha])
757 762 missing = sorted(expected - seen) # sort to stabilize output
758 763 if missing:
759 764 raise util.Abort(_('missing rules for changeset %s') % missing[0],
760 765 hint=_('do you want to use the drop action?'))
761 766 return parsed
762 767
763 768 def processreplacement(repo, replacements):
764 769 """process the list of replacements to return
765 770
766 771 1) the final mapping between original and created nodes
767 772 2) the list of temporary node created by histedit
768 773 3) the list of new commit created by histedit"""
769 774 allsuccs = set()
770 775 replaced = set()
771 776 fullmapping = {}
772 777 # initialise basic set
773 778 # fullmapping record all operation recorded in replacement
774 779 for rep in replacements:
775 780 allsuccs.update(rep[1])
776 781 replaced.add(rep[0])
777 782 fullmapping.setdefault(rep[0], set()).update(rep[1])
778 783 new = allsuccs - replaced
779 784 tmpnodes = allsuccs & replaced
780 785 # Reduce content fullmapping into direct relation between original nodes
781 786 # and final node created during history edition
782 787 # Dropped changeset are replaced by an empty list
783 788 toproceed = set(fullmapping)
784 789 final = {}
785 790 while toproceed:
786 791 for x in list(toproceed):
787 792 succs = fullmapping[x]
788 793 for s in list(succs):
789 794 if s in toproceed:
790 795 # non final node with unknown closure
791 796 # We can't process this now
792 797 break
793 798 elif s in final:
794 799 # non final node, replace with closure
795 800 succs.remove(s)
796 801 succs.update(final[s])
797 802 else:
798 803 final[x] = succs
799 804 toproceed.remove(x)
800 805 # remove tmpnodes from final mapping
801 806 for n in tmpnodes:
802 807 del final[n]
803 808 # we expect all changes involved in final to exist in the repo
804 809 # turn `final` into list (topologically sorted)
805 810 nm = repo.changelog.nodemap
806 811 for prec, succs in final.items():
807 812 final[prec] = sorted(succs, key=nm.get)
808 813
809 814 # computed topmost element (necessary for bookmark)
810 815 if new:
811 816 newtopmost = sorted(new, key=repo.changelog.rev)[-1]
812 817 elif not final:
813 818 # Nothing rewritten at all. we won't need `newtopmost`
814 819 # It is the same as `oldtopmost` and `processreplacement` know it
815 820 newtopmost = None
816 821 else:
817 822 # every body died. The newtopmost is the parent of the root.
818 823 newtopmost = repo[sorted(final, key=repo.changelog.rev)[0]].p1().node()
819 824
820 825 return final, tmpnodes, new, newtopmost
821 826
822 827 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
823 828 """Move bookmark from old to newly created node"""
824 829 if not mapping:
825 830 # if nothing got rewritten there is not purpose for this function
826 831 return
827 832 moves = []
828 833 for bk, old in sorted(repo._bookmarks.iteritems()):
829 834 if old == oldtopmost:
830 835 # special case ensure bookmark stay on tip.
831 836 #
832 837 # This is arguably a feature and we may only want that for the
833 838 # active bookmark. But the behavior is kept compatible with the old
834 839 # version for now.
835 840 moves.append((bk, newtopmost))
836 841 continue
837 842 base = old
838 843 new = mapping.get(base, None)
839 844 if new is None:
840 845 continue
841 846 while not new:
842 847 # base is killed, trying with parent
843 848 base = repo[base].p1().node()
844 849 new = mapping.get(base, (base,))
845 850 # nothing to move
846 851 moves.append((bk, new[-1]))
847 852 if moves:
848 853 marks = repo._bookmarks
849 854 for mark, new in moves:
850 855 old = marks[mark]
851 856 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
852 857 % (mark, node.short(old), node.short(new)))
853 858 marks[mark] = new
854 859 marks.write()
855 860
856 861 def cleanupnode(ui, repo, name, nodes):
857 862 """strip a group of nodes from the repository
858 863
859 864 The set of node to strip may contains unknown nodes."""
860 865 ui.debug('should strip %s nodes %s\n' %
861 866 (name, ', '.join([node.short(n) for n in nodes])))
862 867 lock = None
863 868 try:
864 869 lock = repo.lock()
865 870 # Find all node that need to be stripped
866 871 # (we hg %lr instead of %ln to silently ignore unknown item
867 872 nm = repo.changelog.nodemap
868 873 nodes = [n for n in nodes if n in nm]
869 874 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
870 875 for c in roots:
871 876 # We should process node in reverse order to strip tip most first.
872 877 # but this trigger a bug in changegroup hook.
873 878 # This would reduce bundle overhead
874 879 repair.strip(ui, repo, c)
875 880 finally:
876 881 lockmod.release(lock)
877 882
878 883 def summaryhook(ui, repo):
879 884 if not os.path.exists(repo.join('histedit-state')):
880 885 return
881 886 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
882 887 if rules:
883 888 # i18n: column positioning for "hg summary"
884 889 ui.write(_('hist: %s (histedit --continue)\n') %
885 890 (ui.label(_('%d remaining'), 'histedit.remaining') %
886 891 len(rules)))
887 892
888 893 def extsetup(ui):
889 894 cmdutil.summaryhooks.add('histedit', summaryhook)
890 895 cmdutil.unfinishedstates.append(
891 896 ['histedit-state', False, True, _('histedit in progress'),
892 897 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
@@ -1,105 +1,139 b''
1 1 $ cat >> $HGRCPATH <<EOF
2 2 > [extensions]
3 3 > graphlog=
4 4 > histedit=
5 5 > EOF
6 6
7 7 $ initrepos ()
8 8 > {
9 9 > hg init r
10 10 > cd r
11 11 > for x in a b c ; do
12 12 > echo $x > $x
13 13 > hg add $x
14 14 > hg ci -m $x
15 15 > done
16 16 > cd ..
17 17 > hg clone r r2 | grep -v updating
18 18 > cd r2
19 19 > for x in d e f ; do
20 20 > echo $x > $x
21 21 > hg add $x
22 22 > hg ci -m $x
23 23 > done
24 24 > cd ..
25 25 > hg init r3
26 26 > cd r3
27 27 > for x in g h i ; do
28 28 > echo $x > $x
29 29 > hg add $x
30 30 > hg ci -m $x
31 31 > done
32 32 > cd ..
33 33 > }
34 34
35 35 $ initrepos
36 36 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
37 37
38 38 show the edit commands offered by outgoing
39 39 $ cd r2
40 40 $ HGEDITOR=cat hg histedit --outgoing ../r | grep -v comparing | grep -v searching
41 41 pick 055a42cdd887 3 d
42 42 pick e860deea161a 4 e
43 43 pick 652413bf663e 5 f
44 44
45 45 # Edit history between 055a42cdd887 and 652413bf663e
46 46 #
47 47 # Commands:
48 48 # p, pick = use commit
49 49 # e, edit = use commit, but stop for amending
50 50 # f, fold = use commit, but fold into previous commit (combines N and N-1)
51 51 # d, drop = remove commit from history
52 52 # m, mess = edit message without changing commit content
53 53 #
54 54 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
55 55 $ cd ..
56 56
57 57 show the error from unrelated repos
58 58 $ cd r3
59 59 $ HGEDITOR=cat hg histedit --outgoing ../r | grep -v comparing | grep -v searching
60 60 abort: repository is unrelated
61 61 [1]
62 62 $ cd ..
63 63
64 64 show the error from unrelated repos
65 65 $ cd r3
66 66 $ HGEDITOR=cat hg histedit --force --outgoing ../r
67 67 comparing with ../r
68 68 searching for changes
69 69 warning: repository is unrelated
70 70 pick 2a4042b45417 0 g
71 71 pick 68c46b4927ce 1 h
72 72 pick 51281e65ba79 2 i
73 73
74 74 # Edit history between 2a4042b45417 and 51281e65ba79
75 75 #
76 76 # Commands:
77 77 # p, pick = use commit
78 78 # e, edit = use commit, but stop for amending
79 79 # f, fold = use commit, but fold into previous commit (combines N and N-1)
80 80 # d, drop = remove commit from history
81 81 # m, mess = edit message without changing commit content
82 82 #
83 83 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
84 84 $ cd ..
85 85
86 86 test sensitivity to branch in URL:
87 87
88 88 $ cd r2
89 89 $ hg -q update 2
90 90 $ hg -q branch foo
91 91 $ hg commit -m 'create foo branch'
92 92 $ HGEDITOR=cat hg histedit --outgoing '../r#foo' | grep -v comparing | grep -v searching
93 93 pick f26599ee3441 6 create foo branch
94 94
95 95 # Edit history between f26599ee3441 and f26599ee3441
96 96 #
97 97 # Commands:
98 98 # p, pick = use commit
99 99 # e, edit = use commit, but stop for amending
100 100 # f, fold = use commit, but fold into previous commit (combines N and N-1)
101 101 # d, drop = remove commit from history
102 102 # m, mess = edit message without changing commit content
103 103 #
104 104 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
105
106 test to check number of roots in outgoing revisions
107
108 $ hg -q outgoing -G --template '{node|short}({branch})' '../r'
109 @ f26599ee3441(foo)
110
111 o 652413bf663e(default)
112 |
113 o e860deea161a(default)
114 |
115 o 055a42cdd887(default)
116
117 $ HGEDITOR=cat hg -q histedit --outgoing '../r'
118 abort: there are ambiguous outgoing revisions
119 (see "hg help histedit" for more detail)
120 [255]
121
122 $ hg -q update -C 2
123 $ echo aa >> a
124 $ hg -q commit -m 'another head on default'
125 $ hg -q outgoing -G --template '{node|short}({branch})' '../r#default'
126 @ 3879dc049647(default)
127
128 o 652413bf663e(default)
129 |
130 o e860deea161a(default)
131 |
132 o 055a42cdd887(default)
133
134 $ HGEDITOR=cat hg -q histedit --outgoing '../r#default'
135 abort: there are ambiguous outgoing revisions
136 (see "hg help histedit" for more detail)
137 [255]
138
105 139 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now