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