##// END OF EJS Templates
histedit: select the lowest rev when looking for a root in a revset (bc)...
David Soria Parra -
r20806:d66862b8 default
parent child Browse files
Show More
@@ -1,927 +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 from mercurial import scmutil
162 161 from mercurial import util
163 162 from mercurial import obsolete
164 163 from mercurial import merge as mergemod
165 164 from mercurial.lock import release
166 165 from mercurial.i18n import _
167 166
168 167 cmdtable = {}
169 168 command = cmdutil.command(cmdtable)
170 169
171 170 testedwith = 'internal'
172 171
173 172 # i18n: command names and abbreviations must remain untranslated
174 173 editcomment = _("""# Edit history between %s and %s
175 174 #
176 175 # Commits are listed from least to most recent
177 176 #
178 177 # Commands:
179 178 # p, pick = use commit
180 179 # e, edit = use commit, but stop for amending
181 180 # f, fold = use commit, but combine it with the one above
182 181 # d, drop = remove commit from history
183 182 # m, mess = edit message without changing commit content
184 183 #
185 184 """)
186 185
187 186 def commitfuncfor(repo, src):
188 187 """Build a commit function for the replacement of <src>
189 188
190 189 This function ensure we apply the same treatment to all changesets.
191 190
192 191 - Add a 'histedit_source' entry in extra.
193 192
194 193 Note that fold have its own separated logic because its handling is a bit
195 194 different and not easily factored out of the fold method.
196 195 """
197 196 phasemin = src.phase()
198 197 def commitfunc(**kwargs):
199 198 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
200 199 try:
201 200 repo.ui.setconfig('phases', 'new-commit', phasemin,
202 201 'histedit')
203 202 extra = kwargs.get('extra', {}).copy()
204 203 extra['histedit_source'] = src.hex()
205 204 kwargs['extra'] = extra
206 205 return repo.commit(**kwargs)
207 206 finally:
208 207 repo.ui.restoreconfig(phasebackup)
209 208 return commitfunc
210 209
211 210
212 211
213 212 def applychanges(ui, repo, ctx, opts):
214 213 """Merge changeset from ctx (only) in the current working directory"""
215 214 wcpar = repo.dirstate.parents()[0]
216 215 if ctx.p1().node() == wcpar:
217 216 # edition ar "in place" we do not need to make any merge,
218 217 # just applies changes on parent for edition
219 218 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
220 219 stats = None
221 220 else:
222 221 try:
223 222 # ui.forcemerge is an internal variable, do not document
224 223 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
225 224 'histedit')
226 225 stats = mergemod.update(repo, ctx.node(), True, True, False,
227 226 ctx.p1().node())
228 227 finally:
229 228 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
230 229 repo.setparents(wcpar, node.nullid)
231 230 repo.dirstate.write()
232 231 # fix up dirstate for copies and renames
233 232 cmdutil.duplicatecopies(repo, ctx.rev(), ctx.p1().rev())
234 233 return stats
235 234
236 235 def collapse(repo, first, last, commitopts):
237 236 """collapse the set of revisions from first to last as new one.
238 237
239 238 Expected commit options are:
240 239 - message
241 240 - date
242 241 - username
243 242 Commit message is edited in all cases.
244 243
245 244 This function works in memory."""
246 245 ctxs = list(repo.set('%d::%d', first, last))
247 246 if not ctxs:
248 247 return None
249 248 base = first.parents()[0]
250 249
251 250 # commit a new version of the old changeset, including the update
252 251 # collect all files which might be affected
253 252 files = set()
254 253 for ctx in ctxs:
255 254 files.update(ctx.files())
256 255
257 256 # Recompute copies (avoid recording a -> b -> a)
258 257 copied = copies.pathcopies(base, last)
259 258
260 259 # prune files which were reverted by the updates
261 260 def samefile(f):
262 261 if f in last.manifest():
263 262 a = last.filectx(f)
264 263 if f in base.manifest():
265 264 b = base.filectx(f)
266 265 return (a.data() == b.data()
267 266 and a.flags() == b.flags())
268 267 else:
269 268 return False
270 269 else:
271 270 return f not in base.manifest()
272 271 files = [f for f in files if not samefile(f)]
273 272 # commit version of these files as defined by head
274 273 headmf = last.manifest()
275 274 def filectxfn(repo, ctx, path):
276 275 if path in headmf:
277 276 fctx = last[path]
278 277 flags = fctx.flags()
279 278 mctx = context.memfilectx(fctx.path(), fctx.data(),
280 279 islink='l' in flags,
281 280 isexec='x' in flags,
282 281 copied=copied.get(path))
283 282 return mctx
284 283 raise IOError()
285 284
286 285 if commitopts.get('message'):
287 286 message = commitopts['message']
288 287 else:
289 288 message = first.description()
290 289 user = commitopts.get('user')
291 290 date = commitopts.get('date')
292 291 extra = commitopts.get('extra')
293 292
294 293 parents = (first.p1().node(), first.p2().node())
295 294 new = context.memctx(repo,
296 295 parents=parents,
297 296 text=message,
298 297 files=files,
299 298 filectxfn=filectxfn,
300 299 user=user,
301 300 date=date,
302 301 extra=extra)
303 302 new._text = cmdutil.commitforceeditor(repo, new, [])
304 303 repo.savecommitmessage(new.description())
305 304 return repo.commitctx(new)
306 305
307 306 def pick(ui, repo, ctx, ha, opts):
308 307 oldctx = repo[ha]
309 308 if oldctx.parents()[0] == ctx:
310 309 ui.debug('node %s unchanged\n' % ha)
311 310 return oldctx, []
312 311 hg.update(repo, ctx.node())
313 312 stats = applychanges(ui, repo, oldctx, opts)
314 313 if stats and stats[3] > 0:
315 314 raise error.InterventionRequired(_('Fix up the change and run '
316 315 'hg histedit --continue'))
317 316 # drop the second merge parent
318 317 commit = commitfuncfor(repo, oldctx)
319 318 n = commit(text=oldctx.description(), user=oldctx.user(),
320 319 date=oldctx.date(), extra=oldctx.extra())
321 320 if n is None:
322 321 ui.warn(_('%s: empty changeset\n')
323 322 % node.hex(ha))
324 323 return ctx, []
325 324 new = repo[n]
326 325 return new, [(oldctx.node(), (n,))]
327 326
328 327
329 328 def edit(ui, repo, ctx, ha, opts):
330 329 oldctx = repo[ha]
331 330 hg.update(repo, ctx.node())
332 331 applychanges(ui, repo, oldctx, opts)
333 332 raise error.InterventionRequired(
334 333 _('Make changes as needed, you may commit or record as needed now.\n'
335 334 'When you are finished, run hg histedit --continue to resume.'))
336 335
337 336 def fold(ui, repo, ctx, ha, opts):
338 337 oldctx = repo[ha]
339 338 hg.update(repo, ctx.node())
340 339 stats = applychanges(ui, repo, oldctx, opts)
341 340 if stats and stats[3] > 0:
342 341 raise error.InterventionRequired(
343 342 _('Fix up the change and run hg histedit --continue'))
344 343 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(),
345 344 date=oldctx.date(), extra=oldctx.extra())
346 345 if n is None:
347 346 ui.warn(_('%s: empty changeset')
348 347 % node.hex(ha))
349 348 return ctx, []
350 349 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
351 350
352 351 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
353 352 parent = ctx.parents()[0].node()
354 353 hg.update(repo, parent)
355 354 ### prepare new commit data
356 355 commitopts = opts.copy()
357 356 # username
358 357 if ctx.user() == oldctx.user():
359 358 username = ctx.user()
360 359 else:
361 360 username = ui.username()
362 361 commitopts['user'] = username
363 362 # commit message
364 363 newmessage = '\n***\n'.join(
365 364 [ctx.description()] +
366 365 [repo[r].description() for r in internalchanges] +
367 366 [oldctx.description()]) + '\n'
368 367 commitopts['message'] = newmessage
369 368 # date
370 369 commitopts['date'] = max(ctx.date(), oldctx.date())
371 370 extra = ctx.extra().copy()
372 371 # histedit_source
373 372 # note: ctx is likely a temporary commit but that the best we can do here
374 373 # This is sufficient to solve issue3681 anyway
375 374 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
376 375 commitopts['extra'] = extra
377 376 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
378 377 try:
379 378 phasemin = max(ctx.phase(), oldctx.phase())
380 379 repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit')
381 380 n = collapse(repo, ctx, repo[newnode], commitopts)
382 381 finally:
383 382 repo.ui.restoreconfig(phasebackup)
384 383 if n is None:
385 384 return ctx, []
386 385 hg.update(repo, n)
387 386 replacements = [(oldctx.node(), (newnode,)),
388 387 (ctx.node(), (n,)),
389 388 (newnode, (n,)),
390 389 ]
391 390 for ich in internalchanges:
392 391 replacements.append((ich, (n,)))
393 392 return repo[n], replacements
394 393
395 394 def drop(ui, repo, ctx, ha, opts):
396 395 return ctx, [(repo[ha].node(), ())]
397 396
398 397
399 398 def message(ui, repo, ctx, ha, opts):
400 399 oldctx = repo[ha]
401 400 hg.update(repo, ctx.node())
402 401 stats = applychanges(ui, repo, oldctx, opts)
403 402 if stats and stats[3] > 0:
404 403 raise error.InterventionRequired(
405 404 _('Fix up the change and run hg histedit --continue'))
406 405 message = oldctx.description() + '\n'
407 406 message = ui.edit(message, ui.username())
408 407 commit = commitfuncfor(repo, oldctx)
409 408 new = commit(text=message, user=oldctx.user(), date=oldctx.date(),
410 409 extra=oldctx.extra())
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 root = revs[0]
572 root = scmutil.revsingle(repo, root).node()
570 rootrevs = list(repo.set('roots(%lr)', revs))
571 if len(rootrevs) != 1:
572 raise util.Abort(_('The specified revisions must have ' +
573 'exactly one common root'))
574 root = rootrevs[0].node()
573 575
574 576 keep = opts.get('keep', False)
575 577 revs = between(repo, root, topmost, keep)
576 578 if not revs:
577 579 raise util.Abort(_('%s is not an ancestor of working directory') %
578 580 node.short(root))
579 581
580 582 ctxs = [repo[r] for r in revs]
581 583 if not rules:
582 584 rules = '\n'.join([makedesc(c) for c in ctxs])
583 585 rules += '\n\n'
584 586 rules += editcomment % (node.short(root), node.short(topmost))
585 587 rules = ui.edit(rules, ui.username())
586 588 # Save edit rules in .hg/histedit-last-edit.txt in case
587 589 # the user needs to ask for help after something
588 590 # surprising happens.
589 591 f = open(repo.join('histedit-last-edit.txt'), 'w')
590 592 f.write(rules)
591 593 f.close()
592 594 else:
593 595 if rules == '-':
594 596 f = sys.stdin
595 597 else:
596 598 f = open(rules)
597 599 rules = f.read()
598 600 f.close()
599 601 rules = [l for l in (r.strip() for r in rules.splitlines())
600 602 if l and not l[0] == '#']
601 603 rules = verifyrules(rules, repo, ctxs)
602 604
603 605 parentctx = repo[root].parents()[0]
604 606 keep = opts.get('keep', False)
605 607 replacements = []
606 608
607 609
608 610 while rules:
609 611 writestate(repo, parentctx.node(), rules, keep, topmost, replacements)
610 612 action, ha = rules.pop(0)
611 613 ui.debug('histedit: processing %s %s\n' % (action, ha))
612 614 actfunc = actiontable[action]
613 615 parentctx, replacement_ = actfunc(ui, repo, parentctx, ha, opts)
614 616 replacements.extend(replacement_)
615 617
616 618 hg.update(repo, parentctx.node())
617 619
618 620 mapping, tmpnodes, created, ntm = processreplacement(repo, replacements)
619 621 if mapping:
620 622 for prec, succs in mapping.iteritems():
621 623 if not succs:
622 624 ui.debug('histedit: %s is dropped\n' % node.short(prec))
623 625 else:
624 626 ui.debug('histedit: %s is replaced by %s\n' % (
625 627 node.short(prec), node.short(succs[0])))
626 628 if len(succs) > 1:
627 629 m = 'histedit: %s'
628 630 for n in succs[1:]:
629 631 ui.debug(m % node.short(n))
630 632
631 633 if not keep:
632 634 if mapping:
633 635 movebookmarks(ui, repo, mapping, topmost, ntm)
634 636 # TODO update mq state
635 637 if obsolete._enabled:
636 638 markers = []
637 639 # sort by revision number because it sound "right"
638 640 for prec in sorted(mapping, key=repo.changelog.rev):
639 641 succs = mapping[prec]
640 642 markers.append((repo[prec],
641 643 tuple(repo[s] for s in succs)))
642 644 if markers:
643 645 obsolete.createmarkers(repo, markers)
644 646 else:
645 647 cleanupnode(ui, repo, 'replaced', mapping)
646 648
647 649 cleanupnode(ui, repo, 'temp', tmpnodes)
648 650 os.unlink(os.path.join(repo.path, 'histedit-state'))
649 651 if os.path.exists(repo.sjoin('undo')):
650 652 os.unlink(repo.sjoin('undo'))
651 653
652 654 def gatherchildren(repo, ctx):
653 655 # is there any new commit between the expected parent and "."
654 656 #
655 657 # note: does not take non linear new change in account (but previous
656 658 # implementation didn't used them anyway (issue3655)
657 659 newchildren = [c.node() for c in repo.set('(%d::.)', ctx)]
658 660 if ctx.node() != node.nullid:
659 661 if not newchildren:
660 662 # `ctx` should match but no result. This means that
661 663 # currentnode is not a descendant from ctx.
662 664 msg = _('%s is not an ancestor of working directory')
663 665 hint = _('use "histedit --abort" to clear broken state')
664 666 raise util.Abort(msg % ctx, hint=hint)
665 667 newchildren.pop(0) # remove ctx
666 668 return newchildren
667 669
668 670 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
669 671 action, currentnode = rules.pop(0)
670 672 ctx = repo[currentnode]
671 673
672 674 newchildren = gatherchildren(repo, parentctx)
673 675
674 676 # Commit dirty working directory if necessary
675 677 new = None
676 678 m, a, r, d = repo.status()[:4]
677 679 if m or a or r or d:
678 680 # prepare the message for the commit to comes
679 681 if action in ('f', 'fold'):
680 682 message = 'fold-temp-revision %s' % currentnode
681 683 else:
682 684 message = ctx.description() + '\n'
683 685 if action in ('e', 'edit', 'm', 'mess'):
684 686 editor = cmdutil.commitforceeditor
685 687 else:
686 688 editor = False
687 689 commit = commitfuncfor(repo, ctx)
688 690 new = commit(text=message, user=ctx.user(),
689 691 date=ctx.date(), extra=ctx.extra(),
690 692 editor=editor)
691 693 if new is not None:
692 694 newchildren.append(new)
693 695
694 696 replacements = []
695 697 # track replacements
696 698 if ctx.node() not in newchildren:
697 699 # note: new children may be empty when the changeset is dropped.
698 700 # this happen e.g during conflicting pick where we revert content
699 701 # to parent.
700 702 replacements.append((ctx.node(), tuple(newchildren)))
701 703
702 704 if action in ('f', 'fold'):
703 705 if newchildren:
704 706 # finalize fold operation if applicable
705 707 if new is None:
706 708 new = newchildren[-1]
707 709 else:
708 710 newchildren.pop() # remove new from internal changes
709 711 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new, opts,
710 712 newchildren)
711 713 replacements.extend(repl)
712 714 else:
713 715 # newchildren is empty if the fold did not result in any commit
714 716 # this happen when all folded change are discarded during the
715 717 # merge.
716 718 replacements.append((ctx.node(), (parentctx.node(),)))
717 719 elif newchildren:
718 720 # otherwise update "parentctx" before proceeding to further operation
719 721 parentctx = repo[newchildren[-1]]
720 722 return parentctx, replacements
721 723
722 724
723 725 def between(repo, old, new, keep):
724 726 """select and validate the set of revision to edit
725 727
726 728 When keep is false, the specified set can't have children."""
727 729 ctxs = list(repo.set('%n::%n', old, new))
728 730 if ctxs and not keep:
729 731 if (not obsolete._enabled and
730 732 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
731 733 raise util.Abort(_('cannot edit history that would orphan nodes'))
732 734 if repo.revs('(%ld) and merge()', ctxs):
733 735 raise util.Abort(_('cannot edit history that contains merges'))
734 736 root = ctxs[0] # list is already sorted by repo.set
735 737 if not root.phase():
736 738 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
737 739 return [c.node() for c in ctxs]
738 740
739 741
740 742 def writestate(repo, parentnode, rules, keep, topmost, replacements):
741 743 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
742 744 pickle.dump((parentnode, rules, keep, topmost, replacements), fp)
743 745 fp.close()
744 746
745 747 def readstate(repo):
746 748 """Returns a tuple of (parentnode, rules, keep, topmost, replacements).
747 749 """
748 750 fp = open(os.path.join(repo.path, 'histedit-state'))
749 751 return pickle.load(fp)
750 752
751 753
752 754 def makedesc(c):
753 755 """build a initial action line for a ctx `c`
754 756
755 757 line are in the form:
756 758
757 759 pick <hash> <rev> <summary>
758 760 """
759 761 summary = ''
760 762 if c.description():
761 763 summary = c.description().splitlines()[0]
762 764 line = 'pick %s %d %s' % (c, c.rev(), summary)
763 765 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
764 766
765 767 def verifyrules(rules, repo, ctxs):
766 768 """Verify that there exists exactly one edit rule per given changeset.
767 769
768 770 Will abort if there are to many or too few rules, a malformed rule,
769 771 or a rule on a changeset outside of the user-given range.
770 772 """
771 773 parsed = []
772 774 expected = set(str(c) for c in ctxs)
773 775 seen = set()
774 776 for r in rules:
775 777 if ' ' not in r:
776 778 raise util.Abort(_('malformed line "%s"') % r)
777 779 action, rest = r.split(' ', 1)
778 780 ha = rest.strip().split(' ', 1)[0]
779 781 try:
780 782 ha = str(repo[ha]) # ensure its a short hash
781 783 except error.RepoError:
782 784 raise util.Abort(_('unknown changeset %s listed') % ha)
783 785 if ha not in expected:
784 786 raise util.Abort(
785 787 _('may not use changesets other than the ones listed'))
786 788 if ha in seen:
787 789 raise util.Abort(_('duplicated command for changeset %s') % ha)
788 790 seen.add(ha)
789 791 if action not in actiontable:
790 792 raise util.Abort(_('unknown action "%s"') % action)
791 793 parsed.append([action, ha])
792 794 missing = sorted(expected - seen) # sort to stabilize output
793 795 if missing:
794 796 raise util.Abort(_('missing rules for changeset %s') % missing[0],
795 797 hint=_('do you want to use the drop action?'))
796 798 return parsed
797 799
798 800 def processreplacement(repo, replacements):
799 801 """process the list of replacements to return
800 802
801 803 1) the final mapping between original and created nodes
802 804 2) the list of temporary node created by histedit
803 805 3) the list of new commit created by histedit"""
804 806 allsuccs = set()
805 807 replaced = set()
806 808 fullmapping = {}
807 809 # initialise basic set
808 810 # fullmapping record all operation recorded in replacement
809 811 for rep in replacements:
810 812 allsuccs.update(rep[1])
811 813 replaced.add(rep[0])
812 814 fullmapping.setdefault(rep[0], set()).update(rep[1])
813 815 new = allsuccs - replaced
814 816 tmpnodes = allsuccs & replaced
815 817 # Reduce content fullmapping into direct relation between original nodes
816 818 # and final node created during history edition
817 819 # Dropped changeset are replaced by an empty list
818 820 toproceed = set(fullmapping)
819 821 final = {}
820 822 while toproceed:
821 823 for x in list(toproceed):
822 824 succs = fullmapping[x]
823 825 for s in list(succs):
824 826 if s in toproceed:
825 827 # non final node with unknown closure
826 828 # We can't process this now
827 829 break
828 830 elif s in final:
829 831 # non final node, replace with closure
830 832 succs.remove(s)
831 833 succs.update(final[s])
832 834 else:
833 835 final[x] = succs
834 836 toproceed.remove(x)
835 837 # remove tmpnodes from final mapping
836 838 for n in tmpnodes:
837 839 del final[n]
838 840 # we expect all changes involved in final to exist in the repo
839 841 # turn `final` into list (topologically sorted)
840 842 nm = repo.changelog.nodemap
841 843 for prec, succs in final.items():
842 844 final[prec] = sorted(succs, key=nm.get)
843 845
844 846 # computed topmost element (necessary for bookmark)
845 847 if new:
846 848 newtopmost = sorted(new, key=repo.changelog.rev)[-1]
847 849 elif not final:
848 850 # Nothing rewritten at all. we won't need `newtopmost`
849 851 # It is the same as `oldtopmost` and `processreplacement` know it
850 852 newtopmost = None
851 853 else:
852 854 # every body died. The newtopmost is the parent of the root.
853 855 newtopmost = repo[sorted(final, key=repo.changelog.rev)[0]].p1().node()
854 856
855 857 return final, tmpnodes, new, newtopmost
856 858
857 859 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
858 860 """Move bookmark from old to newly created node"""
859 861 if not mapping:
860 862 # if nothing got rewritten there is not purpose for this function
861 863 return
862 864 moves = []
863 865 for bk, old in sorted(repo._bookmarks.iteritems()):
864 866 if old == oldtopmost:
865 867 # special case ensure bookmark stay on tip.
866 868 #
867 869 # This is arguably a feature and we may only want that for the
868 870 # active bookmark. But the behavior is kept compatible with the old
869 871 # version for now.
870 872 moves.append((bk, newtopmost))
871 873 continue
872 874 base = old
873 875 new = mapping.get(base, None)
874 876 if new is None:
875 877 continue
876 878 while not new:
877 879 # base is killed, trying with parent
878 880 base = repo[base].p1().node()
879 881 new = mapping.get(base, (base,))
880 882 # nothing to move
881 883 moves.append((bk, new[-1]))
882 884 if moves:
883 885 marks = repo._bookmarks
884 886 for mark, new in moves:
885 887 old = marks[mark]
886 888 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
887 889 % (mark, node.short(old), node.short(new)))
888 890 marks[mark] = new
889 891 marks.write()
890 892
891 893 def cleanupnode(ui, repo, name, nodes):
892 894 """strip a group of nodes from the repository
893 895
894 896 The set of node to strip may contains unknown nodes."""
895 897 ui.debug('should strip %s nodes %s\n' %
896 898 (name, ', '.join([node.short(n) for n in nodes])))
897 899 lock = None
898 900 try:
899 901 lock = repo.lock()
900 902 # Find all node that need to be stripped
901 903 # (we hg %lr instead of %ln to silently ignore unknown item
902 904 nm = repo.changelog.nodemap
903 905 nodes = [n for n in nodes if n in nm]
904 906 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
905 907 for c in roots:
906 908 # We should process node in reverse order to strip tip most first.
907 909 # but this trigger a bug in changegroup hook.
908 910 # This would reduce bundle overhead
909 911 repair.strip(ui, repo, c)
910 912 finally:
911 913 release(lock)
912 914
913 915 def summaryhook(ui, repo):
914 916 if not os.path.exists(repo.join('histedit-state')):
915 917 return
916 918 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
917 919 if rules:
918 920 # i18n: column positioning for "hg summary"
919 921 ui.write(_('hist: %s (histedit --continue)\n') %
920 922 (ui.label(_('%d remaining'), 'histedit.remaining') %
921 923 len(rules)))
922 924
923 925 def extsetup(ui):
924 926 cmdutil.summaryhooks.add('histedit', summaryhook)
925 927 cmdutil.unfinishedstates.append(
926 928 ['histedit-state', False, True, _('histedit in progress'),
927 929 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
@@ -1,200 +1,229 b''
1 1 Test argument handling and various data parsing
2 2 ==================================================
3 3
4 4
5 5 Enable extensions used by this test.
6 6 $ cat >>$HGRCPATH <<EOF
7 7 > [extensions]
8 8 > histedit=
9 9 > EOF
10 10
11 11 Repo setup.
12 12 $ hg init foo
13 13 $ cd foo
14 14 $ echo alpha >> alpha
15 15 $ hg addr
16 16 adding alpha
17 17 $ hg ci -m one
18 18 $ echo alpha >> alpha
19 19 $ hg ci -m two
20 20 $ echo alpha >> alpha
21 21 $ hg ci -m three
22 22 $ echo alpha >> alpha
23 23 $ hg ci -m four
24 24 $ echo alpha >> alpha
25 25 $ hg ci -m five
26 26
27 27 $ hg log --style compact --graph
28 28 @ 4[tip] 08d98a8350f3 1970-01-01 00:00 +0000 test
29 29 | five
30 30 |
31 31 o 3 c8e68270e35a 1970-01-01 00:00 +0000 test
32 32 | four
33 33 |
34 34 o 2 eb57da33312f 1970-01-01 00:00 +0000 test
35 35 | three
36 36 |
37 37 o 1 579e40513370 1970-01-01 00:00 +0000 test
38 38 | two
39 39 |
40 40 o 0 6058cbb6cfd7 1970-01-01 00:00 +0000 test
41 41 one
42 42
43 43
44 44 Run a dummy edit to make sure we get tip^^ correctly via revsingle.
45 45 --------------------------------------------------------------------
46 46
47 47 $ HGEDITOR=cat hg histedit "tip^^"
48 48 pick eb57da33312f 2 three
49 49 pick c8e68270e35a 3 four
50 50 pick 08d98a8350f3 4 five
51 51
52 52 # Edit history between eb57da33312f and 08d98a8350f3
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 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
64 64
65 65 Run on a revision not ancestors of the current working directory.
66 66 --------------------------------------------------------------------
67 67
68 68 $ hg up 2
69 69 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
70 70 $ hg histedit -r 4
71 71 abort: 08d98a8350f3 is not an ancestor of working directory
72 72 [255]
73 73 $ hg up --quiet
74 74
75
76 Test that we pick the minimum of a revrange
77 ---------------------------------------
78
79 $ HGEDITOR=cat hg histedit '2::' --commands - << EOF
80 > pick eb57da33312f 2 three
81 > pick c8e68270e35a 3 four
82 > pick 08d98a8350f3 4 five
83 > EOF
84 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
85 $ hg up --quiet
86
87 $ HGEDITOR=cat hg histedit 'tip:2' --commands - << EOF
88 > pick eb57da33312f 2 three
89 > pick c8e68270e35a 3 four
90 > pick 08d98a8350f3 4 five
91 > EOF
92 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
93 $ hg up --quiet
94
75 95 Run on a revision not descendants of the initial parent
76 96 --------------------------------------------------------------------
77 97
78 98 Test the message shown for inconsistent histedit state, which may be
79 99 created (and forgotten) by Mercurial earlier than 2.7. This emulates
80 100 Mercurial earlier than 2.7 by renaming ".hg/histedit-state"
81 101 temporarily.
82 102
83 103 $ HGEDITOR=cat hg histedit -r 4 --commands - << EOF
84 104 > edit 08d98a8350f3 4 five
85 105 > EOF
86 106 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
87 107 reverting alpha
88 108 Make changes as needed, you may commit or record as needed now.
89 109 When you are finished, run hg histedit --continue to resume.
90 110 [1]
91 111
92 112 $ mv .hg/histedit-state .hg/histedit-state.back
93 113 $ hg update --quiet --clean 2
94 114 $ mv .hg/histedit-state.back .hg/histedit-state
95 115
96 116 $ hg histedit --continue
97 117 abort: c8e68270e35a is not an ancestor of working directory
98 118 (use "histedit --abort" to clear broken state)
99 119 [255]
100 120
101 121 $ hg histedit --abort
102 122 $ hg update --quiet --clean
103 123
104 124 Test that missing revisions are detected
105 125 ---------------------------------------
106 126
107 127 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
108 128 > pick eb57da33312f 2 three
109 129 > pick 08d98a8350f3 4 five
110 130 > EOF
111 131 abort: missing rules for changeset c8e68270e35a
112 132 (do you want to use the drop action?)
113 133 [255]
114 134
115 135 Test that extra revisions are detected
116 136 ---------------------------------------
117 137
118 138 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
119 139 > pick 6058cbb6cfd7 0 one
120 140 > pick c8e68270e35a 3 four
121 141 > pick 08d98a8350f3 4 five
122 142 > EOF
123 143 abort: may not use changesets other than the ones listed
124 144 [255]
125 145
126 146 Test malformed line
127 147 ---------------------------------------
128 148
129 149 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
130 150 > pickeb57da33312f2three
131 151 > pick c8e68270e35a 3 four
132 152 > pick 08d98a8350f3 4 five
133 153 > EOF
134 154 abort: malformed line "pickeb57da33312f2three"
135 155 [255]
136 156
137 157 Test unknown changeset
138 158 ---------------------------------------
139 159
140 160 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
141 161 > pick 0123456789ab 2 three
142 162 > pick c8e68270e35a 3 four
143 163 > pick 08d98a8350f3 4 five
144 164 > EOF
145 165 abort: unknown changeset 0123456789ab listed
146 166 [255]
147 167
148 168 Test unknown command
149 169 ---------------------------------------
150 170
151 171 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
152 172 > coin eb57da33312f 2 three
153 173 > pick c8e68270e35a 3 four
154 174 > pick 08d98a8350f3 4 five
155 175 > EOF
156 176 abort: unknown action "coin"
157 177 [255]
158 178
159 179 Test duplicated changeset
160 180 ---------------------------------------
161 181
162 182 So one is missing and one appear twice.
163 183
164 184 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
165 185 > pick eb57da33312f 2 three
166 186 > pick eb57da33312f 2 three
167 187 > pick 08d98a8350f3 4 five
168 188 > EOF
169 189 abort: duplicated command for changeset eb57da33312f
170 190 [255]
171 191
172 192 Test short version of command
173 193 ---------------------------------------
174 194
175 195 Note: we use varying amounts of white space between command name and changeset
176 196 short hash. This tests issue3893.
177 197
178 198 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
179 199 > pick eb57da33312f 2 three
180 200 > p c8e68270e35a 3 four
181 201 > f 08d98a8350f3 4 five
182 202 > EOF
183 203 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
184 204 reverting alpha
185 205 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
186 206 four
187 207 ***
188 208 five
189 209
190 210
191 211
192 212 HG: Enter commit message. Lines beginning with 'HG:' are removed.
193 213 HG: Leave message empty to abort commit.
194 214 HG: --
195 215 HG: user: test
196 216 HG: branch 'default'
197 217 HG: changed alpha
198 218 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
199 219 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
200 220 saved backup bundle to $TESTTMP/foo/.hg/strip-backup/*-backup.hg (glob)
221
222 $ hg update -q 2
223 $ echo x > x
224 $ hg add x
225 $ hg commit -m'x' x
226 created new head
227 $ hg histedit -r 'heads(all())'
228 abort: The specified revisions must have exactly one common root
229 [255]
General Comments 0
You need to be logged in to leave comments. Login now