##// END OF EJS Templates
histedit: get rid of state.rules...
Mateusz Kwapich -
r27208:994d8dce default
parent child Browse files
Show More
@@ -1,1402 +1,1400
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 commit 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 commit 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 customize this behavior 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 bundle2
164 164 from mercurial import cmdutil
165 165 from mercurial import discovery
166 166 from mercurial import error
167 167 from mercurial import copies
168 168 from mercurial import context
169 169 from mercurial import exchange
170 170 from mercurial import extensions
171 171 from mercurial import hg
172 172 from mercurial import node
173 173 from mercurial import repair
174 174 from mercurial import scmutil
175 175 from mercurial import util
176 176 from mercurial import obsolete
177 177 from mercurial import merge as mergemod
178 178 from mercurial.lock import release
179 179 from mercurial.i18n import _
180 180
181 181 cmdtable = {}
182 182 command = cmdutil.command(cmdtable)
183 183
184 184 class _constraints(object):
185 185 # aborts if there are multiple rules for one node
186 186 noduplicates = 'noduplicates'
187 187 # abort if the node does belong to edited stack
188 188 forceother = 'forceother'
189 189 # abort if the node doesn't belong to edited stack
190 190 noother = 'noother'
191 191
192 192 @classmethod
193 193 def known(cls):
194 194 return set([v for k, v in cls.__dict__.items() if k[0] != '_'])
195 195
196 196 # Note for extension authors: ONLY specify testedwith = 'internal' for
197 197 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
198 198 # be specifying the version(s) of Mercurial they are tested with, or
199 199 # leave the attribute unspecified.
200 200 testedwith = 'internal'
201 201
202 202 # i18n: command names and abbreviations must remain untranslated
203 203 editcomment = _("""# Edit history between %s and %s
204 204 #
205 205 # Commits are listed from least to most recent
206 206 #
207 207 # Commands:
208 208 # p, pick = use commit
209 209 # e, edit = use commit, but stop for amending
210 210 # f, fold = use commit, but combine it with the one above
211 211 # r, roll = like fold, but discard this commit's description
212 212 # d, drop = remove commit from history
213 213 # m, mess = edit commit message without changing commit content
214 214 #
215 215 """)
216 216
217 217 class histeditstate(object):
218 def __init__(self, repo, parentctxnode=None, rules=None, keep=None,
218 def __init__(self, repo, parentctxnode=None, actions=None, keep=None,
219 219 topmost=None, replacements=None, lock=None, wlock=None):
220 220 self.repo = repo
221 self.rules = rules
221 self.actions = actions
222 222 self.keep = keep
223 223 self.topmost = topmost
224 224 self.parentctxnode = parentctxnode
225 225 self.lock = lock
226 226 self.wlock = wlock
227 227 self.backupfile = None
228 228 if replacements is None:
229 229 self.replacements = []
230 230 else:
231 231 self.replacements = replacements
232 232
233 233 def read(self):
234 234 """Load histedit state from disk and set fields appropriately."""
235 235 try:
236 236 fp = self.repo.vfs('histedit-state', 'r')
237 237 except IOError as err:
238 238 if err.errno != errno.ENOENT:
239 239 raise
240 240 raise error.Abort(_('no histedit in progress'))
241 241
242 242 try:
243 243 data = pickle.load(fp)
244 244 parentctxnode, rules, keep, topmost, replacements = data
245 245 backupfile = None
246 246 except pickle.UnpicklingError:
247 247 data = self._load()
248 248 parentctxnode, rules, keep, topmost, replacements, backupfile = data
249 249
250 250 self.parentctxnode = parentctxnode
251 self.rules = rules
251 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
252 actions = parserules(rules, self)
253 self.actions = actions
252 254 self.keep = keep
253 255 self.topmost = topmost
254 256 self.replacements = replacements
255 257 self.backupfile = backupfile
256 258
257 259 def write(self):
258 260 fp = self.repo.vfs('histedit-state', 'w')
259 261 fp.write('v1\n')
260 262 fp.write('%s\n' % node.hex(self.parentctxnode))
261 263 fp.write('%s\n' % node.hex(self.topmost))
262 264 fp.write('%s\n' % self.keep)
263 fp.write('%d\n' % len(self.rules))
264 for rule in self.rules:
265 fp.write('%s\n' % rule[0]) # action
266 fp.write('%s\n' % rule[1]) # remainder
265 fp.write('%d\n' % len(self.actions))
266 for action in self.actions:
267 fp.write('%s\n' % action.tostate())
267 268 fp.write('%d\n' % len(self.replacements))
268 269 for replacement in self.replacements:
269 270 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
270 271 for r in replacement[1])))
271 272 backupfile = self.backupfile
272 273 if not backupfile:
273 274 backupfile = ''
274 275 fp.write('%s\n' % backupfile)
275 276 fp.close()
276 277
277 278 def _load(self):
278 279 fp = self.repo.vfs('histedit-state', 'r')
279 280 lines = [l[:-1] for l in fp.readlines()]
280 281
281 282 index = 0
282 283 lines[index] # version number
283 284 index += 1
284 285
285 286 parentctxnode = node.bin(lines[index])
286 287 index += 1
287 288
288 289 topmost = node.bin(lines[index])
289 290 index += 1
290 291
291 292 keep = lines[index] == 'True'
292 293 index += 1
293 294
294 295 # Rules
295 296 rules = []
296 297 rulelen = int(lines[index])
297 298 index += 1
298 299 for i in xrange(rulelen):
299 300 ruleaction = lines[index]
300 301 index += 1
301 302 rule = lines[index]
302 303 index += 1
303 304 rules.append((ruleaction, rule))
304 305
305 306 # Replacements
306 307 replacements = []
307 308 replacementlen = int(lines[index])
308 309 index += 1
309 310 for i in xrange(replacementlen):
310 311 replacement = lines[index]
311 312 original = node.bin(replacement[:40])
312 313 succ = [node.bin(replacement[i:i + 40]) for i in
313 314 range(40, len(replacement), 40)]
314 315 replacements.append((original, succ))
315 316 index += 1
316 317
317 318 backupfile = lines[index]
318 319 index += 1
319 320
320 321 fp.close()
321 322
322 323 return parentctxnode, rules, keep, topmost, replacements, backupfile
323 324
324 325 def clear(self):
325 326 if self.inprogress():
326 327 self.repo.vfs.unlink('histedit-state')
327 328
328 329 def inprogress(self):
329 330 return self.repo.vfs.exists('histedit-state')
330 331
331 @property
332 def actions(self):
333 actions = []
334 for (act, rest) in self.rules:
335 actions.append(actiontable[act].fromrule(self, rest))
336 return actions
337
338 332
339 333 class histeditaction(object):
340 334 def __init__(self, state, node):
341 335 self.state = state
342 336 self.repo = state.repo
343 337 self.node = node
344 338
345 339 @classmethod
346 340 def fromrule(cls, state, rule):
347 341 """Parses the given rule, returning an instance of the histeditaction.
348 342 """
349 343 rulehash = rule.strip().split(' ', 1)[0]
350 344 return cls(state, node.bin(rulehash))
351 345
352 346 def verify(self):
353 347 """ Verifies semantic correctness of the rule"""
354 348 repo = self.repo
355 349 ha = node.hex(self.node)
356 350 try:
357 351 self.node = repo[ha].node()
358 352 except error.RepoError:
359 353 raise error.Abort(_('unknown changeset %s listed')
360 354 % ha[:12])
361 355
362 356 def torule(self):
363 357 """build a histedit rule line for an action
364 358
365 359 by default lines are in the form:
366 360 <hash> <rev> <summary>
367 361 """
368 362 ctx = self.repo[self.node]
369 363 summary = ''
370 364 if ctx.description():
371 365 summary = ctx.description().splitlines()[0]
372 366 line = '%s %s %d %s' % (self.verb, ctx, ctx.rev(), summary)
373 367 # trim to 75 columns by default so it's not stupidly wide in my editor
374 368 # (the 5 more are left for verb)
375 369 maxlen = self.repo.ui.configint('histedit', 'linelen', default=80)
376 370 maxlen = max(maxlen, 22) # avoid truncating hash
377 371 return util.ellipsis(line, maxlen)
378 372
379 373 def tostate(self):
380 374 """Print an action in format used by histedit state files
381 375 (the first line is a verb, the remainder is the second)
382 376 """
383 377 return "%s\n%s" % (self.verb, node.hex(self.node))
384 378
385 379 def constraints(self):
386 380 """Return a set of constrains that this action should be verified for
387 381 """
388 382 return set([_constraints.noduplicates, _constraints.noother])
389 383
390 384 def nodetoverify(self):
391 385 """Returns a node associated with the action that will be used for
392 386 verification purposes.
393 387
394 388 If the action doesn't correspond to node it should return None
395 389 """
396 390 return self.node
397 391
398 392 def run(self):
399 393 """Runs the action. The default behavior is simply apply the action's
400 394 rulectx onto the current parentctx."""
401 395 self.applychange()
402 396 self.continuedirty()
403 397 return self.continueclean()
404 398
405 399 def applychange(self):
406 400 """Applies the changes from this action's rulectx onto the current
407 401 parentctx, but does not commit them."""
408 402 repo = self.repo
409 403 rulectx = repo[self.node]
410 404 hg.update(repo, self.state.parentctxnode)
411 405 stats = applychanges(repo.ui, repo, rulectx, {})
412 406 if stats and stats[3] > 0:
413 407 raise error.InterventionRequired(_('Fix up the change and run '
414 408 'hg histedit --continue'))
415 409
416 410 def continuedirty(self):
417 411 """Continues the action when changes have been applied to the working
418 412 copy. The default behavior is to commit the dirty changes."""
419 413 repo = self.repo
420 414 rulectx = repo[self.node]
421 415
422 416 editor = self.commiteditor()
423 417 commit = commitfuncfor(repo, rulectx)
424 418
425 419 commit(text=rulectx.description(), user=rulectx.user(),
426 420 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
427 421
428 422 def commiteditor(self):
429 423 """The editor to be used to edit the commit message."""
430 424 return False
431 425
432 426 def continueclean(self):
433 427 """Continues the action when the working copy is clean. The default
434 428 behavior is to accept the current commit as the new version of the
435 429 rulectx."""
436 430 ctx = self.repo['.']
437 431 if ctx.node() == self.state.parentctxnode:
438 432 self.repo.ui.warn(_('%s: empty changeset\n') %
439 433 node.short(self.node))
440 434 return ctx, [(self.node, tuple())]
441 435 if ctx.node() == self.node:
442 436 # Nothing changed
443 437 return ctx, []
444 438 return ctx, [(self.node, (ctx.node(),))]
445 439
446 440 def commitfuncfor(repo, src):
447 441 """Build a commit function for the replacement of <src>
448 442
449 443 This function ensure we apply the same treatment to all changesets.
450 444
451 445 - Add a 'histedit_source' entry in extra.
452 446
453 447 Note that fold has its own separated logic because its handling is a bit
454 448 different and not easily factored out of the fold method.
455 449 """
456 450 phasemin = src.phase()
457 451 def commitfunc(**kwargs):
458 452 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
459 453 try:
460 454 repo.ui.setconfig('phases', 'new-commit', phasemin,
461 455 'histedit')
462 456 extra = kwargs.get('extra', {}).copy()
463 457 extra['histedit_source'] = src.hex()
464 458 kwargs['extra'] = extra
465 459 return repo.commit(**kwargs)
466 460 finally:
467 461 repo.ui.restoreconfig(phasebackup)
468 462 return commitfunc
469 463
470 464 def applychanges(ui, repo, ctx, opts):
471 465 """Merge changeset from ctx (only) in the current working directory"""
472 466 wcpar = repo.dirstate.parents()[0]
473 467 if ctx.p1().node() == wcpar:
474 468 # edits are "in place" we do not need to make any merge,
475 469 # just applies changes on parent for edition
476 470 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
477 471 stats = None
478 472 else:
479 473 try:
480 474 # ui.forcemerge is an internal variable, do not document
481 475 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
482 476 'histedit')
483 477 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
484 478 finally:
485 479 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
486 480 return stats
487 481
488 482 def collapse(repo, first, last, commitopts, skipprompt=False):
489 483 """collapse the set of revisions from first to last as new one.
490 484
491 485 Expected commit options are:
492 486 - message
493 487 - date
494 488 - username
495 489 Commit message is edited in all cases.
496 490
497 491 This function works in memory."""
498 492 ctxs = list(repo.set('%d::%d', first, last))
499 493 if not ctxs:
500 494 return None
501 495 for c in ctxs:
502 496 if not c.mutable():
503 497 raise error.Abort(
504 498 _("cannot fold into public change %s") % node.short(c.node()))
505 499 base = first.parents()[0]
506 500
507 501 # commit a new version of the old changeset, including the update
508 502 # collect all files which might be affected
509 503 files = set()
510 504 for ctx in ctxs:
511 505 files.update(ctx.files())
512 506
513 507 # Recompute copies (avoid recording a -> b -> a)
514 508 copied = copies.pathcopies(base, last)
515 509
516 510 # prune files which were reverted by the updates
517 511 def samefile(f):
518 512 if f in last.manifest():
519 513 a = last.filectx(f)
520 514 if f in base.manifest():
521 515 b = base.filectx(f)
522 516 return (a.data() == b.data()
523 517 and a.flags() == b.flags())
524 518 else:
525 519 return False
526 520 else:
527 521 return f not in base.manifest()
528 522 files = [f for f in files if not samefile(f)]
529 523 # commit version of these files as defined by head
530 524 headmf = last.manifest()
531 525 def filectxfn(repo, ctx, path):
532 526 if path in headmf:
533 527 fctx = last[path]
534 528 flags = fctx.flags()
535 529 mctx = context.memfilectx(repo,
536 530 fctx.path(), fctx.data(),
537 531 islink='l' in flags,
538 532 isexec='x' in flags,
539 533 copied=copied.get(path))
540 534 return mctx
541 535 return None
542 536
543 537 if commitopts.get('message'):
544 538 message = commitopts['message']
545 539 else:
546 540 message = first.description()
547 541 user = commitopts.get('user')
548 542 date = commitopts.get('date')
549 543 extra = commitopts.get('extra')
550 544
551 545 parents = (first.p1().node(), first.p2().node())
552 546 editor = None
553 547 if not skipprompt:
554 548 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
555 549 new = context.memctx(repo,
556 550 parents=parents,
557 551 text=message,
558 552 files=files,
559 553 filectxfn=filectxfn,
560 554 user=user,
561 555 date=date,
562 556 extra=extra,
563 557 editor=editor)
564 558 return repo.commitctx(new)
565 559
566 560 def _isdirtywc(repo):
567 561 return repo[None].dirty(missing=True)
568 562
569 563 def abortdirty():
570 564 raise error.Abort(_('working copy has pending changes'),
571 565 hint=_('amend, commit, or revert them and run histedit '
572 566 '--continue, or abort with histedit --abort'))
573 567
574 568
575 569 actiontable = {}
576 570 actionlist = []
577 571
578 572 def addhisteditaction(verbs):
579 573 def wrap(cls):
580 574 cls.verb = verbs[0]
581 575 for verb in verbs:
582 576 actiontable[verb] = cls
583 577 actionlist.append(cls)
584 578 return cls
585 579 return wrap
586 580
587 581
588 582 @addhisteditaction(['pick', 'p'])
589 583 class pick(histeditaction):
590 584 def run(self):
591 585 rulectx = self.repo[self.node]
592 586 if rulectx.parents()[0].node() == self.state.parentctxnode:
593 587 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
594 588 return rulectx, []
595 589
596 590 return super(pick, self).run()
597 591
598 592 @addhisteditaction(['edit', 'e'])
599 593 class edit(histeditaction):
600 594 def run(self):
601 595 repo = self.repo
602 596 rulectx = repo[self.node]
603 597 hg.update(repo, self.state.parentctxnode)
604 598 applychanges(repo.ui, repo, rulectx, {})
605 599 raise error.InterventionRequired(
606 600 _('Make changes as needed, you may commit or record as needed '
607 601 'now.\nWhen you are finished, run hg histedit --continue to '
608 602 'resume.'))
609 603
610 604 def commiteditor(self):
611 605 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
612 606
613 607 @addhisteditaction(['fold', 'f'])
614 608 class fold(histeditaction):
615 609 def continuedirty(self):
616 610 repo = self.repo
617 611 rulectx = repo[self.node]
618 612
619 613 commit = commitfuncfor(repo, rulectx)
620 614 commit(text='fold-temp-revision %s' % node.short(self.node),
621 615 user=rulectx.user(), date=rulectx.date(),
622 616 extra=rulectx.extra())
623 617
624 618 def continueclean(self):
625 619 repo = self.repo
626 620 ctx = repo['.']
627 621 rulectx = repo[self.node]
628 622 parentctxnode = self.state.parentctxnode
629 623 if ctx.node() == parentctxnode:
630 624 repo.ui.warn(_('%s: empty changeset\n') %
631 625 node.short(self.node))
632 626 return ctx, [(self.node, (parentctxnode,))]
633 627
634 628 parentctx = repo[parentctxnode]
635 629 newcommits = set(c.node() for c in repo.set('(%d::. - %d)', parentctx,
636 630 parentctx))
637 631 if not newcommits:
638 632 repo.ui.warn(_('%s: cannot fold - working copy is not a '
639 633 'descendant of previous commit %s\n') %
640 634 (node.short(self.node), node.short(parentctxnode)))
641 635 return ctx, [(self.node, (ctx.node(),))]
642 636
643 637 middlecommits = newcommits.copy()
644 638 middlecommits.discard(ctx.node())
645 639
646 640 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
647 641 middlecommits)
648 642
649 643 def skipprompt(self):
650 644 """Returns true if the rule should skip the message editor.
651 645
652 646 For example, 'fold' wants to show an editor, but 'rollup'
653 647 doesn't want to.
654 648 """
655 649 return False
656 650
657 651 def mergedescs(self):
658 652 """Returns true if the rule should merge messages of multiple changes.
659 653
660 654 This exists mainly so that 'rollup' rules can be a subclass of
661 655 'fold'.
662 656 """
663 657 return True
664 658
665 659 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
666 660 parent = ctx.parents()[0].node()
667 661 hg.update(repo, parent)
668 662 ### prepare new commit data
669 663 commitopts = {}
670 664 commitopts['user'] = ctx.user()
671 665 # commit message
672 666 if not self.mergedescs():
673 667 newmessage = ctx.description()
674 668 else:
675 669 newmessage = '\n***\n'.join(
676 670 [ctx.description()] +
677 671 [repo[r].description() for r in internalchanges] +
678 672 [oldctx.description()]) + '\n'
679 673 commitopts['message'] = newmessage
680 674 # date
681 675 commitopts['date'] = max(ctx.date(), oldctx.date())
682 676 extra = ctx.extra().copy()
683 677 # histedit_source
684 678 # note: ctx is likely a temporary commit but that the best we can do
685 679 # here. This is sufficient to solve issue3681 anyway.
686 680 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
687 681 commitopts['extra'] = extra
688 682 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
689 683 try:
690 684 phasemin = max(ctx.phase(), oldctx.phase())
691 685 repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit')
692 686 n = collapse(repo, ctx, repo[newnode], commitopts,
693 687 skipprompt=self.skipprompt())
694 688 finally:
695 689 repo.ui.restoreconfig(phasebackup)
696 690 if n is None:
697 691 return ctx, []
698 692 hg.update(repo, n)
699 693 replacements = [(oldctx.node(), (newnode,)),
700 694 (ctx.node(), (n,)),
701 695 (newnode, (n,)),
702 696 ]
703 697 for ich in internalchanges:
704 698 replacements.append((ich, (n,)))
705 699 return repo[n], replacements
706 700
707 701 class base(histeditaction):
708 702 def constraints(self):
709 703 return set([_constraints.forceother])
710 704
711 705 def run(self):
712 706 if self.repo['.'].node() != self.node:
713 707 mergemod.update(self.repo, self.node, False, True, False)
714 708 # branchmerge, force, partial)
715 709 return self.continueclean()
716 710
717 711 def continuedirty(self):
718 712 abortdirty()
719 713
720 714 def continueclean(self):
721 715 basectx = self.repo['.']
722 716 return basectx, []
723 717
724 718 @addhisteditaction(['_multifold'])
725 719 class _multifold(fold):
726 720 """fold subclass used for when multiple folds happen in a row
727 721
728 722 We only want to fire the editor for the folded message once when
729 723 (say) four changes are folded down into a single change. This is
730 724 similar to rollup, but we should preserve both messages so that
731 725 when the last fold operation runs we can show the user all the
732 726 commit messages in their editor.
733 727 """
734 728 def skipprompt(self):
735 729 return True
736 730
737 731 @addhisteditaction(["roll", "r"])
738 732 class rollup(fold):
739 733 def mergedescs(self):
740 734 return False
741 735
742 736 def skipprompt(self):
743 737 return True
744 738
745 739 @addhisteditaction(["drop", "d"])
746 740 class drop(histeditaction):
747 741 def run(self):
748 742 parentctx = self.repo[self.state.parentctxnode]
749 743 return parentctx, [(self.node, tuple())]
750 744
751 745 @addhisteditaction(["mess", "m"])
752 746 class message(histeditaction):
753 747 def commiteditor(self):
754 748 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
755 749
756 750 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
757 751 """utility function to find the first outgoing changeset
758 752
759 753 Used by initialization code"""
760 754 if opts is None:
761 755 opts = {}
762 756 dest = ui.expandpath(remote or 'default-push', remote or 'default')
763 757 dest, revs = hg.parseurl(dest, None)[:2]
764 758 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
765 759
766 760 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
767 761 other = hg.peer(repo, opts, dest)
768 762
769 763 if revs:
770 764 revs = [repo.lookup(rev) for rev in revs]
771 765
772 766 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
773 767 if not outgoing.missing:
774 768 raise error.Abort(_('no outgoing ancestors'))
775 769 roots = list(repo.revs("roots(%ln)", outgoing.missing))
776 770 if 1 < len(roots):
777 771 msg = _('there are ambiguous outgoing revisions')
778 772 hint = _('see "hg help histedit" for more detail')
779 773 raise error.Abort(msg, hint=hint)
780 774 return repo.lookup(roots[0])
781 775
782 776
783 777 @command('histedit',
784 778 [('', 'commands', '',
785 779 _('read history edits from the specified file'), _('FILE')),
786 780 ('c', 'continue', False, _('continue an edit already in progress')),
787 781 ('', 'edit-plan', False, _('edit remaining actions list')),
788 782 ('k', 'keep', False,
789 783 _("don't strip old nodes after edit is complete")),
790 784 ('', 'abort', False, _('abort an edit in progress')),
791 785 ('o', 'outgoing', False, _('changesets not found in destination')),
792 786 ('f', 'force', False,
793 787 _('force outgoing even for unrelated repositories')),
794 788 ('r', 'rev', [], _('first revision to be edited'), _('REV'))],
795 789 _("ANCESTOR | --outgoing [URL]"))
796 790 def histedit(ui, repo, *freeargs, **opts):
797 791 """interactively edit changeset history
798 792
799 793 This command edits changesets between ANCESTOR and the parent of
800 794 the working directory.
801 795
802 796 With --outgoing, this edits changesets not found in the
803 797 destination repository. If URL of the destination is omitted, the
804 798 'default-push' (or 'default') path will be used.
805 799
806 800 For safety, this command is also aborted if there are ambiguous
807 801 outgoing revisions which may confuse users: for example, if there
808 802 are multiple branches containing outgoing revisions.
809 803
810 804 Use "min(outgoing() and ::.)" or similar revset specification
811 805 instead of --outgoing to specify edit target revision exactly in
812 806 such ambiguous situation. See :hg:`help revsets` for detail about
813 807 selecting revisions.
814 808
815 809 .. container:: verbose
816 810
817 811 Examples:
818 812
819 813 - A number of changes have been made.
820 814 Revision 3 is no longer needed.
821 815
822 816 Start history editing from revision 3::
823 817
824 818 hg histedit -r 3
825 819
826 820 An editor opens, containing the list of revisions,
827 821 with specific actions specified::
828 822
829 823 pick 5339bf82f0ca 3 Zworgle the foobar
830 824 pick 8ef592ce7cc4 4 Bedazzle the zerlog
831 825 pick 0a9639fcda9d 5 Morgify the cromulancy
832 826
833 827 Additional information about the possible actions
834 828 to take appears below the list of revisions.
835 829
836 830 To remove revision 3 from the history,
837 831 its action (at the beginning of the relevant line)
838 832 is changed to 'drop'::
839 833
840 834 drop 5339bf82f0ca 3 Zworgle the foobar
841 835 pick 8ef592ce7cc4 4 Bedazzle the zerlog
842 836 pick 0a9639fcda9d 5 Morgify the cromulancy
843 837
844 838 - A number of changes have been made.
845 839 Revision 2 and 4 need to be swapped.
846 840
847 841 Start history editing from revision 2::
848 842
849 843 hg histedit -r 2
850 844
851 845 An editor opens, containing the list of revisions,
852 846 with specific actions specified::
853 847
854 848 pick 252a1af424ad 2 Blorb a morgwazzle
855 849 pick 5339bf82f0ca 3 Zworgle the foobar
856 850 pick 8ef592ce7cc4 4 Bedazzle the zerlog
857 851
858 852 To swap revision 2 and 4, its lines are swapped
859 853 in the editor::
860 854
861 855 pick 8ef592ce7cc4 4 Bedazzle the zerlog
862 856 pick 5339bf82f0ca 3 Zworgle the foobar
863 857 pick 252a1af424ad 2 Blorb a morgwazzle
864 858
865 859 Returns 0 on success, 1 if user intervention is required (not only
866 860 for intentional "edit" command, but also for resolving unexpected
867 861 conflicts).
868 862 """
869 863 state = histeditstate(repo)
870 864 try:
871 865 state.wlock = repo.wlock()
872 866 state.lock = repo.lock()
873 867 _histedit(ui, repo, state, *freeargs, **opts)
874 868 except error.Abort:
875 869 if repo.vfs.exists('histedit-last-edit.txt'):
876 870 ui.warn(_('warning: histedit rules saved '
877 871 'to: .hg/histedit-last-edit.txt\n'))
878 872 raise
879 873 finally:
880 874 release(state.lock, state.wlock)
881 875
882 876 def _histedit(ui, repo, state, *freeargs, **opts):
883 877 # TODO only abort if we try to histedit mq patches, not just
884 878 # blanket if mq patches are applied somewhere
885 879 mq = getattr(repo, 'mq', None)
886 880 if mq and mq.applied:
887 881 raise error.Abort(_('source has mq patches applied'))
888 882
889 883 # basic argument incompatibility processing
890 884 outg = opts.get('outgoing')
891 885 cont = opts.get('continue')
892 886 editplan = opts.get('edit_plan')
893 887 abort = opts.get('abort')
894 888 force = opts.get('force')
895 889 rules = opts.get('commands', '')
896 890 revs = opts.get('rev', [])
897 891 goal = 'new' # This invocation goal, in new, continue, abort
898 892 if force and not outg:
899 893 raise error.Abort(_('--force only allowed with --outgoing'))
900 894 if cont:
901 895 if any((outg, abort, revs, freeargs, rules, editplan)):
902 896 raise error.Abort(_('no arguments allowed with --continue'))
903 897 goal = 'continue'
904 898 elif abort:
905 899 if any((outg, revs, freeargs, rules, editplan)):
906 900 raise error.Abort(_('no arguments allowed with --abort'))
907 901 goal = 'abort'
908 902 elif editplan:
909 903 if any((outg, revs, freeargs)):
910 904 raise error.Abort(_('only --commands argument allowed with '
911 905 '--edit-plan'))
912 906 goal = 'edit-plan'
913 907 else:
914 908 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
915 909 raise error.Abort(_('history edit already in progress, try '
916 910 '--continue or --abort'))
917 911 if outg:
918 912 if revs:
919 913 raise error.Abort(_('no revisions allowed with --outgoing'))
920 914 if len(freeargs) > 1:
921 915 raise error.Abort(
922 916 _('only one repo argument allowed with --outgoing'))
923 917 else:
924 918 revs.extend(freeargs)
925 919 if len(revs) == 0:
926 920 # experimental config: histedit.defaultrev
927 921 histeditdefault = ui.config('histedit', 'defaultrev')
928 922 if histeditdefault:
929 923 revs.append(histeditdefault)
930 924 if len(revs) != 1:
931 925 raise error.Abort(
932 926 _('histedit requires exactly one ancestor revision'))
933 927
934 928
935 929 replacements = []
936 930 state.keep = opts.get('keep', False)
937 931 supportsmarkers = obsolete.isenabled(repo, obsolete.createmarkersopt)
938 932
939 933 # rebuild state
940 934 if goal == 'continue':
941 935 state.read()
942 936 state = bootstrapcontinue(ui, state, opts)
943 937 elif goal == 'edit-plan':
944 938 state.read()
945 939 if not rules:
946 940 comment = editcomment % (node.short(state.parentctxnode),
947 941 node.short(state.topmost))
948 942 rules = ruleeditor(repo, ui, state.actions, comment)
949 943 else:
950 944 if rules == '-':
951 945 f = sys.stdin
952 946 else:
953 947 f = open(rules)
954 948 rules = f.read()
955 949 f.close()
956 rules = [l for l in (r.strip() for r in rules.splitlines())
957 if l and not l.startswith('#')]
958 rules = verifyrules(rules, state, [repo[c] for [_a, c] in state.rules])
959 state.rules = rules
950 actions = parserules(rules, state)
951 ctxs = [repo[act.nodetoverify()] \
952 for act in state.actions if act.nodetoverify()]
953 verifyactions(actions, state, ctxs)
954 state.actions = actions
960 955 state.write()
961 956 return
962 957 elif goal == 'abort':
963 958 try:
964 959 state.read()
965 960 tmpnodes, leafs = newnodestoabort(state)
966 961 ui.debug('restore wc to old parent %s\n'
967 962 % node.short(state.topmost))
968 963
969 964 # Recover our old commits if necessary
970 965 if not state.topmost in repo and state.backupfile:
971 966 backupfile = repo.join(state.backupfile)
972 967 f = hg.openpath(ui, backupfile)
973 968 gen = exchange.readbundle(ui, f, backupfile)
974 969 tr = repo.transaction('histedit.abort')
975 970 try:
976 971 if not isinstance(gen, bundle2.unbundle20):
977 972 gen.apply(repo, 'histedit', 'bundle:' + backupfile)
978 973 if isinstance(gen, bundle2.unbundle20):
979 974 bundle2.applybundle(repo, gen, tr,
980 975 source='histedit',
981 976 url='bundle:' + backupfile)
982 977 tr.close()
983 978 finally:
984 979 tr.release()
985 980
986 981 os.remove(backupfile)
987 982
988 983 # check whether we should update away
989 984 if repo.unfiltered().revs('parents() and (%n or %ln::)',
990 985 state.parentctxnode, leafs | tmpnodes):
991 986 hg.clean(repo, state.topmost)
992 987 cleanupnode(ui, repo, 'created', tmpnodes)
993 988 cleanupnode(ui, repo, 'temp', leafs)
994 989 except Exception:
995 990 if state.inprogress():
996 991 ui.warn(_('warning: encountered an exception during histedit '
997 992 '--abort; the repository may not have been completely '
998 993 'cleaned up\n'))
999 994 raise
1000 995 finally:
1001 996 state.clear()
1002 997 return
1003 998 else:
1004 999 cmdutil.checkunfinished(repo)
1005 1000 cmdutil.bailifchanged(repo)
1006 1001
1007 1002 if repo.vfs.exists('histedit-last-edit.txt'):
1008 1003 repo.vfs.unlink('histedit-last-edit.txt')
1009 1004 topmost, empty = repo.dirstate.parents()
1010 1005 if outg:
1011 1006 if freeargs:
1012 1007 remote = freeargs[0]
1013 1008 else:
1014 1009 remote = None
1015 1010 root = findoutgoing(ui, repo, remote, force, opts)
1016 1011 else:
1017 1012 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1018 1013 if len(rr) != 1:
1019 1014 raise error.Abort(_('The specified revisions must have '
1020 1015 'exactly one common root'))
1021 1016 root = rr[0].node()
1022 1017
1023 1018 revs = between(repo, root, topmost, state.keep)
1024 1019 if not revs:
1025 1020 raise error.Abort(_('%s is not an ancestor of working directory') %
1026 1021 node.short(root))
1027 1022
1028 1023 ctxs = [repo[r] for r in revs]
1029 1024 if not rules:
1030 1025 comment = editcomment % (node.short(root), node.short(topmost))
1031 1026 actions = [pick(state, r) for r in revs]
1032 1027 rules = ruleeditor(repo, ui, actions, comment)
1033 1028 else:
1034 1029 if rules == '-':
1035 1030 f = sys.stdin
1036 1031 else:
1037 1032 f = open(rules)
1038 1033 rules = f.read()
1039 1034 f.close()
1040 rules = [l for l in (r.strip() for r in rules.splitlines())
1041 if l and not l.startswith('#')]
1042 rules = verifyrules(rules, state, ctxs)
1035 actions = parserules(rules, state)
1036 verifyactions(actions, state, ctxs)
1043 1037
1044 1038 parentctxnode = repo[root].parents()[0].node()
1045 1039
1046 1040 state.parentctxnode = parentctxnode
1047 state.rules = rules
1041 state.actions = actions
1048 1042 state.topmost = topmost
1049 1043 state.replacements = replacements
1050 1044
1051 1045 # Create a backup so we can always abort completely.
1052 1046 backupfile = None
1053 1047 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1054 1048 backupfile = repair._bundle(repo, [parentctxnode], [topmost], root,
1055 1049 'histedit')
1056 1050 state.backupfile = backupfile
1057 1051
1058 1052 # preprocess rules so that we can hide inner folds from the user
1059 1053 # and only show one editor
1060 1054 actions = state.actions[:]
1061 1055 for idx, (action, nextact) in enumerate(
1062 1056 zip(actions, actions[1:] + [None])):
1063 1057 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1064 1058 state.actions[idx].__class__ = _multifold
1065 state.rules[idx] = '_multifold', action.nodetoverify() # TODO remove this
1066 1059
1067 1060 while state.actions:
1068 1061 state.write()
1069 1062 actobj = state.actions.pop(0)
1070 state.rules.pop(0) # TODO remove this
1071 1063 ui.debug('histedit: processing %s %s\n' % (actobj.verb,\
1072 1064 actobj.torule()))
1073 1065 parentctx, replacement_ = actobj.run()
1074 1066 state.parentctxnode = parentctx.node()
1075 1067 state.replacements.extend(replacement_)
1076 1068 state.write()
1077 1069
1078 1070 hg.update(repo, state.parentctxnode)
1079 1071
1080 1072 mapping, tmpnodes, created, ntm = processreplacement(state)
1081 1073 if mapping:
1082 1074 for prec, succs in mapping.iteritems():
1083 1075 if not succs:
1084 1076 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1085 1077 else:
1086 1078 ui.debug('histedit: %s is replaced by %s\n' % (
1087 1079 node.short(prec), node.short(succs[0])))
1088 1080 if len(succs) > 1:
1089 1081 m = 'histedit: %s'
1090 1082 for n in succs[1:]:
1091 1083 ui.debug(m % node.short(n))
1092 1084
1093 1085 if supportsmarkers:
1094 1086 # Only create markers if the temp nodes weren't already removed.
1095 1087 obsolete.createmarkers(repo, ((repo[t],()) for t in sorted(tmpnodes)
1096 1088 if t in repo))
1097 1089 else:
1098 1090 cleanupnode(ui, repo, 'temp', tmpnodes)
1099 1091
1100 1092 if not state.keep:
1101 1093 if mapping:
1102 1094 movebookmarks(ui, repo, mapping, state.topmost, ntm)
1103 1095 # TODO update mq state
1104 1096 if supportsmarkers:
1105 1097 markers = []
1106 1098 # sort by revision number because it sound "right"
1107 1099 for prec in sorted(mapping, key=repo.changelog.rev):
1108 1100 succs = mapping[prec]
1109 1101 markers.append((repo[prec],
1110 1102 tuple(repo[s] for s in succs)))
1111 1103 if markers:
1112 1104 obsolete.createmarkers(repo, markers)
1113 1105 else:
1114 1106 cleanupnode(ui, repo, 'replaced', mapping)
1115 1107
1116 1108 state.clear()
1117 1109 if os.path.exists(repo.sjoin('undo')):
1118 1110 os.unlink(repo.sjoin('undo'))
1119 1111
1120 1112 def bootstrapcontinue(ui, state, opts):
1121 1113 repo = state.repo
1122 1114 if state.actions:
1123 1115 actobj = state.actions.pop(0)
1124 state.rules.pop(0) # TODO remove this
1125 1116
1126 1117 if _isdirtywc(repo):
1127 1118 actobj.continuedirty()
1128 1119 if _isdirtywc(repo):
1129 1120 abortdirty()
1130 1121
1131 1122 parentctx, replacements = actobj.continueclean()
1132 1123
1133 1124 state.parentctxnode = parentctx.node()
1134 1125 state.replacements.extend(replacements)
1135 1126
1136 1127 return state
1137 1128
1138 1129 def between(repo, old, new, keep):
1139 1130 """select and validate the set of revision to edit
1140 1131
1141 1132 When keep is false, the specified set can't have children."""
1142 1133 ctxs = list(repo.set('%n::%n', old, new))
1143 1134 if ctxs and not keep:
1144 1135 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1145 1136 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
1146 1137 raise error.Abort(_('cannot edit history that would orphan nodes'))
1147 1138 if repo.revs('(%ld) and merge()', ctxs):
1148 1139 raise error.Abort(_('cannot edit history that contains merges'))
1149 1140 root = ctxs[0] # list is already sorted by repo.set
1150 1141 if not root.mutable():
1151 1142 raise error.Abort(_('cannot edit public changeset: %s') % root,
1152 1143 hint=_('see "hg help phases" for details'))
1153 1144 return [c.node() for c in ctxs]
1154 1145
1155 1146 def ruleeditor(repo, ui, actions, editcomment=""):
1156 1147 """open an editor to edit rules
1157 1148
1158 1149 rules are in the format [ [act, ctx], ...] like in state.rules
1159 1150 """
1160 1151 rules = '\n'.join([act.torule() for act in actions])
1161 1152 rules += '\n\n'
1162 1153 rules += editcomment
1163 1154 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'})
1164 1155
1165 1156 # Save edit rules in .hg/histedit-last-edit.txt in case
1166 1157 # the user needs to ask for help after something
1167 1158 # surprising happens.
1168 1159 f = open(repo.join('histedit-last-edit.txt'), 'w')
1169 1160 f.write(rules)
1170 1161 f.close()
1171 1162
1172 1163 return rules
1173 1164
1174 def verifyrules(rules, state, ctxs):
1175 """Verify that there exists exactly one edit rule per given changeset.
1176
1177 Will abort if there are to many or too few rules, a malformed rule,
1178 or a rule on a changeset outside of the user-given range.
1179 """
1180 parsed = []
1181 expected = set(c.hex() for c in ctxs)
1182 seen = set()
1165 def parserules(rules, state):
1166 """Read the histedit rules string and return list of action objects """
1167 rules = [l for l in (r.strip() for r in rules.splitlines())
1168 if l and not l.startswith('#')]
1169 actions = []
1183 1170 for r in rules:
1184 1171 if ' ' not in r:
1185 1172 raise error.Abort(_('malformed line "%s"') % r)
1186 1173 verb, rest = r.split(' ', 1)
1187 1174
1188 if verb not in actiontable or verb.startswith('_'):
1175 if verb not in actiontable:
1189 1176 raise error.Abort(_('unknown action "%s"') % verb)
1177
1190 1178 action = actiontable[verb].fromrule(state, rest)
1179 actions.append(action)
1180 return actions
1181
1182 def verifyactions(actions, state, ctxs):
1183 """Verify that there exists exactly one action per given changeset and
1184 other constraints.
1185
1186 Will abort if there are to many or too few rules, a malformed rule,
1187 or a rule on a changeset outside of the user-given range.
1188 """
1189 expected = set(c.hex() for c in ctxs)
1190 seen = set()
1191 for action in actions:
1191 1192 action.verify()
1192 1193 constraints = action.constraints()
1193 1194 for constraint in constraints:
1194 1195 if constraint not in _constraints.known():
1195 1196 raise error.Abort(_('unknown constraint "%s"') % constraint)
1196 1197
1197 1198 nodetoverify = action.nodetoverify()
1198 1199 if nodetoverify is not None:
1199 1200 ha = node.hex(nodetoverify)
1200 1201 if _constraints.noother in constraints and ha not in expected:
1201 1202 raise error.Abort(
1202 1203 _('may not use "%s" with changesets '
1203 'other than the ones listed') % verb)
1204 'other than the ones listed') % action.verb)
1204 1205 if _constraints.forceother in constraints and ha in expected:
1205 1206 raise error.Abort(
1206 1207 _('may not use "%s" with changesets '
1207 'within the edited list') % verb)
1208 'within the edited list') % action.verb)
1208 1209 if _constraints.noduplicates in constraints and ha in seen:
1209 1210 raise error.Abort(_('duplicated command for changeset %s') %
1210 1211 ha[:12])
1211 1212 seen.add(ha)
1212 rest = ha
1213 parsed.append([verb, rest])
1214 1213 missing = sorted(expected - seen) # sort to stabilize output
1215 1214 if missing:
1216 1215 raise error.Abort(_('missing rules for changeset %s') %
1217 1216 missing[0][:12],
1218 1217 hint=_('do you want to use the drop action?'))
1219 return parsed
1220 1218
1221 1219 def newnodestoabort(state):
1222 1220 """process the list of replacements to return
1223 1221
1224 1222 1) the list of final node
1225 1223 2) the list of temporary node
1226 1224
1227 1225 This meant to be used on abort as less data are required in this case.
1228 1226 """
1229 1227 replacements = state.replacements
1230 1228 allsuccs = set()
1231 1229 replaced = set()
1232 1230 for rep in replacements:
1233 1231 allsuccs.update(rep[1])
1234 1232 replaced.add(rep[0])
1235 1233 newnodes = allsuccs - replaced
1236 1234 tmpnodes = allsuccs & replaced
1237 1235 return newnodes, tmpnodes
1238 1236
1239 1237
1240 1238 def processreplacement(state):
1241 1239 """process the list of replacements to return
1242 1240
1243 1241 1) the final mapping between original and created nodes
1244 1242 2) the list of temporary node created by histedit
1245 1243 3) the list of new commit created by histedit"""
1246 1244 replacements = state.replacements
1247 1245 allsuccs = set()
1248 1246 replaced = set()
1249 1247 fullmapping = {}
1250 1248 # initialize basic set
1251 1249 # fullmapping records all operations recorded in replacement
1252 1250 for rep in replacements:
1253 1251 allsuccs.update(rep[1])
1254 1252 replaced.add(rep[0])
1255 1253 fullmapping.setdefault(rep[0], set()).update(rep[1])
1256 1254 new = allsuccs - replaced
1257 1255 tmpnodes = allsuccs & replaced
1258 1256 # Reduce content fullmapping into direct relation between original nodes
1259 1257 # and final node created during history edition
1260 1258 # Dropped changeset are replaced by an empty list
1261 1259 toproceed = set(fullmapping)
1262 1260 final = {}
1263 1261 while toproceed:
1264 1262 for x in list(toproceed):
1265 1263 succs = fullmapping[x]
1266 1264 for s in list(succs):
1267 1265 if s in toproceed:
1268 1266 # non final node with unknown closure
1269 1267 # We can't process this now
1270 1268 break
1271 1269 elif s in final:
1272 1270 # non final node, replace with closure
1273 1271 succs.remove(s)
1274 1272 succs.update(final[s])
1275 1273 else:
1276 1274 final[x] = succs
1277 1275 toproceed.remove(x)
1278 1276 # remove tmpnodes from final mapping
1279 1277 for n in tmpnodes:
1280 1278 del final[n]
1281 1279 # we expect all changes involved in final to exist in the repo
1282 1280 # turn `final` into list (topologically sorted)
1283 1281 nm = state.repo.changelog.nodemap
1284 1282 for prec, succs in final.items():
1285 1283 final[prec] = sorted(succs, key=nm.get)
1286 1284
1287 1285 # computed topmost element (necessary for bookmark)
1288 1286 if new:
1289 1287 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1290 1288 elif not final:
1291 1289 # Nothing rewritten at all. we won't need `newtopmost`
1292 1290 # It is the same as `oldtopmost` and `processreplacement` know it
1293 1291 newtopmost = None
1294 1292 else:
1295 1293 # every body died. The newtopmost is the parent of the root.
1296 1294 r = state.repo.changelog.rev
1297 1295 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1298 1296
1299 1297 return final, tmpnodes, new, newtopmost
1300 1298
1301 1299 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
1302 1300 """Move bookmark from old to newly created node"""
1303 1301 if not mapping:
1304 1302 # if nothing got rewritten there is not purpose for this function
1305 1303 return
1306 1304 moves = []
1307 1305 for bk, old in sorted(repo._bookmarks.iteritems()):
1308 1306 if old == oldtopmost:
1309 1307 # special case ensure bookmark stay on tip.
1310 1308 #
1311 1309 # This is arguably a feature and we may only want that for the
1312 1310 # active bookmark. But the behavior is kept compatible with the old
1313 1311 # version for now.
1314 1312 moves.append((bk, newtopmost))
1315 1313 continue
1316 1314 base = old
1317 1315 new = mapping.get(base, None)
1318 1316 if new is None:
1319 1317 continue
1320 1318 while not new:
1321 1319 # base is killed, trying with parent
1322 1320 base = repo[base].p1().node()
1323 1321 new = mapping.get(base, (base,))
1324 1322 # nothing to move
1325 1323 moves.append((bk, new[-1]))
1326 1324 if moves:
1327 1325 lock = tr = None
1328 1326 try:
1329 1327 lock = repo.lock()
1330 1328 tr = repo.transaction('histedit')
1331 1329 marks = repo._bookmarks
1332 1330 for mark, new in moves:
1333 1331 old = marks[mark]
1334 1332 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
1335 1333 % (mark, node.short(old), node.short(new)))
1336 1334 marks[mark] = new
1337 1335 marks.recordchange(tr)
1338 1336 tr.close()
1339 1337 finally:
1340 1338 release(tr, lock)
1341 1339
1342 1340 def cleanupnode(ui, repo, name, nodes):
1343 1341 """strip a group of nodes from the repository
1344 1342
1345 1343 The set of node to strip may contains unknown nodes."""
1346 1344 ui.debug('should strip %s nodes %s\n' %
1347 1345 (name, ', '.join([node.short(n) for n in nodes])))
1348 1346 lock = None
1349 1347 try:
1350 1348 lock = repo.lock()
1351 1349 # do not let filtering get in the way of the cleanse
1352 1350 # we should probably get rid of obsolescence marker created during the
1353 1351 # histedit, but we currently do not have such information.
1354 1352 repo = repo.unfiltered()
1355 1353 # Find all nodes that need to be stripped
1356 1354 # (we use %lr instead of %ln to silently ignore unknown items)
1357 1355 nm = repo.changelog.nodemap
1358 1356 nodes = sorted(n for n in nodes if n in nm)
1359 1357 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1360 1358 for c in roots:
1361 1359 # We should process node in reverse order to strip tip most first.
1362 1360 # but this trigger a bug in changegroup hook.
1363 1361 # This would reduce bundle overhead
1364 1362 repair.strip(ui, repo, c)
1365 1363 finally:
1366 1364 release(lock)
1367 1365
1368 1366 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1369 1367 if isinstance(nodelist, str):
1370 1368 nodelist = [nodelist]
1371 1369 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1372 1370 state = histeditstate(repo)
1373 1371 state.read()
1374 1372 histedit_nodes = set([action.nodetoverify() for action
1375 1373 in state.actions if action.nodetoverify()])
1376 1374 strip_nodes = set([repo[n].node() for n in nodelist])
1377 1375 common_nodes = histedit_nodes & strip_nodes
1378 1376 if common_nodes:
1379 1377 raise error.Abort(_("histedit in progress, can't strip %s")
1380 1378 % ', '.join(node.short(x) for x in common_nodes))
1381 1379 return orig(ui, repo, nodelist, *args, **kwargs)
1382 1380
1383 1381 extensions.wrapfunction(repair, 'strip', stripwrapper)
1384 1382
1385 1383 def summaryhook(ui, repo):
1386 1384 if not os.path.exists(repo.join('histedit-state')):
1387 1385 return
1388 1386 state = histeditstate(repo)
1389 1387 state.read()
1390 1388 if state.actions:
1391 1389 # i18n: column positioning for "hg summary"
1392 1390 ui.write(_('hist: %s (histedit --continue)\n') %
1393 1391 (ui.label(_('%d remaining'), 'histedit.remaining') %
1394 1392 len(state.actions)))
1395 1393
1396 1394 def extsetup(ui):
1397 1395 cmdutil.summaryhooks.add('histedit', summaryhook)
1398 1396 cmdutil.unfinishedstates.append(
1399 1397 ['histedit-state', False, True, _('histedit in progress'),
1400 1398 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
1401 1399 if ui.configbool("experimental", "histeditng"):
1402 1400 globals()['base'] = addhisteditaction(['base', 'b'])(base)
General Comments 0
You need to be logged in to leave comments. Login now