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