##// END OF EJS Templates
configitems: register the 'histedit.defaultrev' config
Boris Feld -
r34472:6567002a default
parent child Browse files
Show More
@@ -1,1615 +1,1622 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 and date
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 and date
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. The date used
75 75 for the commit will be the later of the two commits' dates. For this example,
76 76 let's assume that the commit message was changed to ``Add beta and delta.``
77 77 After histedit has run and had a chance to remove any old or temporary
78 78 revisions it needed, the history looks 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. If there are uncommitted
101 101 changes, you'll be prompted for a new commit message, but the default
102 102 commit message will be the original message for the ``edit`` ed
103 103 revision, and the date of the original commit will be preserved.
104 104
105 105 The ``message`` operation will give you a chance to revise a commit
106 106 message without changing the contents. It's a shortcut for doing
107 107 ``edit`` immediately followed by `hg histedit --continue``.
108 108
109 109 If ``histedit`` encounters a conflict when moving a revision (while
110 110 handling ``pick`` or ``fold``), it'll stop in a similar manner to
111 111 ``edit`` with the difference that it won't prompt you for a commit
112 112 message when done. If you decide at this point that you don't like how
113 113 much work it will be to rearrange history, or that you made a mistake,
114 114 you can use ``hg histedit --abort`` to abandon the new changes you
115 115 have made and return to the state before you attempted to edit your
116 116 history.
117 117
118 118 If we clone the histedit-ed example repository above and add four more
119 119 changes, such that we have the following history::
120 120
121 121 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
122 122 | Add theta
123 123 |
124 124 o 5 140988835471 2009-04-27 18:04 -0500 stefan
125 125 | Add eta
126 126 |
127 127 o 4 122930637314 2009-04-27 18:04 -0500 stefan
128 128 | Add zeta
129 129 |
130 130 o 3 836302820282 2009-04-27 18:04 -0500 stefan
131 131 | Add epsilon
132 132 |
133 133 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
134 134 | Add beta and delta.
135 135 |
136 136 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
137 137 | Add gamma
138 138 |
139 139 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
140 140 Add alpha
141 141
142 142 If you run ``hg histedit --outgoing`` on the clone then it is the same
143 143 as running ``hg histedit 836302820282``. If you need plan to push to a
144 144 repository that Mercurial does not detect to be related to the source
145 145 repo, you can add a ``--force`` option.
146 146
147 147 Config
148 148 ------
149 149
150 150 Histedit rule lines are truncated to 80 characters by default. You
151 151 can customize this behavior by setting a different length in your
152 152 configuration file::
153 153
154 154 [histedit]
155 155 linelen = 120 # truncate rule lines at 120 characters
156 156
157 157 ``hg histedit`` attempts to automatically choose an appropriate base
158 158 revision to use. To change which base revision is used, define a
159 159 revset in your configuration file::
160 160
161 161 [histedit]
162 162 defaultrev = only(.) & draft()
163 163
164 164 By default each edited revision needs to be present in histedit commands.
165 165 To remove revision you need to use ``drop`` operation. You can configure
166 166 the drop to be implicit for missing commits by adding::
167 167
168 168 [histedit]
169 169 dropmissing = True
170 170
171 171 By default, histedit will close the transaction after each action. For
172 172 performance purposes, you can configure histedit to use a single transaction
173 173 across the entire histedit. WARNING: This setting introduces a significant risk
174 174 of losing the work you've done in a histedit if the histedit aborts
175 175 unexpectedly::
176 176
177 177 [histedit]
178 178 singletransaction = True
179 179
180 180 """
181 181
182 182 from __future__ import absolute_import
183 183
184 184 import errno
185 185 import os
186 186
187 187 from mercurial.i18n import _
188 188 from mercurial import (
189 189 bundle2,
190 190 cmdutil,
191 configitems,
191 192 context,
192 193 copies,
193 194 destutil,
194 195 discovery,
195 196 error,
196 197 exchange,
197 198 extensions,
198 199 hg,
199 200 lock,
200 201 merge as mergemod,
201 202 mergeutil,
202 203 node,
203 204 obsolete,
204 205 registrar,
205 206 repair,
206 207 scmutil,
207 208 util,
208 209 )
209 210
210 211 pickle = util.pickle
211 212 release = lock.release
212 213 cmdtable = {}
213 214 command = registrar.command(cmdtable)
214 215
216 configtable = {}
217 configitem = registrar.configitem(configtable)
218 configitem('histedit', 'defaultrev',
219 default=configitems.dynamicdefault,
220 )
221
215 222 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
216 223 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
217 224 # be specifying the version(s) of Mercurial they are tested with, or
218 225 # leave the attribute unspecified.
219 226 testedwith = 'ships-with-hg-core'
220 227
221 228 actiontable = {}
222 229 primaryactions = set()
223 230 secondaryactions = set()
224 231 tertiaryactions = set()
225 232 internalactions = set()
226 233
227 234 def geteditcomment(ui, first, last):
228 235 """ construct the editor comment
229 236 The comment includes::
230 237 - an intro
231 238 - sorted primary commands
232 239 - sorted short commands
233 240 - sorted long commands
234 241 - additional hints
235 242
236 243 Commands are only included once.
237 244 """
238 245 intro = _("""Edit history between %s and %s
239 246
240 247 Commits are listed from least to most recent
241 248
242 249 You can reorder changesets by reordering the lines
243 250
244 251 Commands:
245 252 """)
246 253 actions = []
247 254 def addverb(v):
248 255 a = actiontable[v]
249 256 lines = a.message.split("\n")
250 257 if len(a.verbs):
251 258 v = ', '.join(sorted(a.verbs, key=lambda v: len(v)))
252 259 actions.append(" %s = %s" % (v, lines[0]))
253 260 actions.extend([' %s' for l in lines[1:]])
254 261
255 262 for v in (
256 263 sorted(primaryactions) +
257 264 sorted(secondaryactions) +
258 265 sorted(tertiaryactions)
259 266 ):
260 267 addverb(v)
261 268 actions.append('')
262 269
263 270 hints = []
264 271 if ui.configbool('histedit', 'dropmissing'):
265 272 hints.append("Deleting a changeset from the list "
266 273 "will DISCARD it from the edited history!")
267 274
268 275 lines = (intro % (first, last)).split('\n') + actions + hints
269 276
270 277 return ''.join(['# %s\n' % l if l else '#\n' for l in lines])
271 278
272 279 class histeditstate(object):
273 280 def __init__(self, repo, parentctxnode=None, actions=None, keep=None,
274 281 topmost=None, replacements=None, lock=None, wlock=None):
275 282 self.repo = repo
276 283 self.actions = actions
277 284 self.keep = keep
278 285 self.topmost = topmost
279 286 self.parentctxnode = parentctxnode
280 287 self.lock = lock
281 288 self.wlock = wlock
282 289 self.backupfile = None
283 290 if replacements is None:
284 291 self.replacements = []
285 292 else:
286 293 self.replacements = replacements
287 294
288 295 def read(self):
289 296 """Load histedit state from disk and set fields appropriately."""
290 297 try:
291 298 state = self.repo.vfs.read('histedit-state')
292 299 except IOError as err:
293 300 if err.errno != errno.ENOENT:
294 301 raise
295 302 cmdutil.wrongtooltocontinue(self.repo, _('histedit'))
296 303
297 304 if state.startswith('v1\n'):
298 305 data = self._load()
299 306 parentctxnode, rules, keep, topmost, replacements, backupfile = data
300 307 else:
301 308 data = pickle.loads(state)
302 309 parentctxnode, rules, keep, topmost, replacements = data
303 310 backupfile = None
304 311
305 312 self.parentctxnode = parentctxnode
306 313 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
307 314 actions = parserules(rules, self)
308 315 self.actions = actions
309 316 self.keep = keep
310 317 self.topmost = topmost
311 318 self.replacements = replacements
312 319 self.backupfile = backupfile
313 320
314 321 def write(self, tr=None):
315 322 if tr:
316 323 tr.addfilegenerator('histedit-state', ('histedit-state',),
317 324 self._write, location='plain')
318 325 else:
319 326 with self.repo.vfs("histedit-state", "w") as f:
320 327 self._write(f)
321 328
322 329 def _write(self, fp):
323 330 fp.write('v1\n')
324 331 fp.write('%s\n' % node.hex(self.parentctxnode))
325 332 fp.write('%s\n' % node.hex(self.topmost))
326 333 fp.write('%s\n' % self.keep)
327 334 fp.write('%d\n' % len(self.actions))
328 335 for action in self.actions:
329 336 fp.write('%s\n' % action.tostate())
330 337 fp.write('%d\n' % len(self.replacements))
331 338 for replacement in self.replacements:
332 339 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
333 340 for r in replacement[1])))
334 341 backupfile = self.backupfile
335 342 if not backupfile:
336 343 backupfile = ''
337 344 fp.write('%s\n' % backupfile)
338 345
339 346 def _load(self):
340 347 fp = self.repo.vfs('histedit-state', 'r')
341 348 lines = [l[:-1] for l in fp.readlines()]
342 349
343 350 index = 0
344 351 lines[index] # version number
345 352 index += 1
346 353
347 354 parentctxnode = node.bin(lines[index])
348 355 index += 1
349 356
350 357 topmost = node.bin(lines[index])
351 358 index += 1
352 359
353 360 keep = lines[index] == 'True'
354 361 index += 1
355 362
356 363 # Rules
357 364 rules = []
358 365 rulelen = int(lines[index])
359 366 index += 1
360 367 for i in xrange(rulelen):
361 368 ruleaction = lines[index]
362 369 index += 1
363 370 rule = lines[index]
364 371 index += 1
365 372 rules.append((ruleaction, rule))
366 373
367 374 # Replacements
368 375 replacements = []
369 376 replacementlen = int(lines[index])
370 377 index += 1
371 378 for i in xrange(replacementlen):
372 379 replacement = lines[index]
373 380 original = node.bin(replacement[:40])
374 381 succ = [node.bin(replacement[i:i + 40]) for i in
375 382 range(40, len(replacement), 40)]
376 383 replacements.append((original, succ))
377 384 index += 1
378 385
379 386 backupfile = lines[index]
380 387 index += 1
381 388
382 389 fp.close()
383 390
384 391 return parentctxnode, rules, keep, topmost, replacements, backupfile
385 392
386 393 def clear(self):
387 394 if self.inprogress():
388 395 self.repo.vfs.unlink('histedit-state')
389 396
390 397 def inprogress(self):
391 398 return self.repo.vfs.exists('histedit-state')
392 399
393 400
394 401 class histeditaction(object):
395 402 def __init__(self, state, node):
396 403 self.state = state
397 404 self.repo = state.repo
398 405 self.node = node
399 406
400 407 @classmethod
401 408 def fromrule(cls, state, rule):
402 409 """Parses the given rule, returning an instance of the histeditaction.
403 410 """
404 411 rulehash = rule.strip().split(' ', 1)[0]
405 412 try:
406 413 rev = node.bin(rulehash)
407 414 except TypeError:
408 415 raise error.ParseError("invalid changeset %s" % rulehash)
409 416 return cls(state, rev)
410 417
411 418 def verify(self, prev, expected, seen):
412 419 """ Verifies semantic correctness of the rule"""
413 420 repo = self.repo
414 421 ha = node.hex(self.node)
415 422 try:
416 423 self.node = repo[ha].node()
417 424 except error.RepoError:
418 425 raise error.ParseError(_('unknown changeset %s listed')
419 426 % ha[:12])
420 427 if self.node is not None:
421 428 self._verifynodeconstraints(prev, expected, seen)
422 429
423 430 def _verifynodeconstraints(self, prev, expected, seen):
424 431 # by default command need a node in the edited list
425 432 if self.node not in expected:
426 433 raise error.ParseError(_('%s "%s" changeset was not a candidate')
427 434 % (self.verb, node.short(self.node)),
428 435 hint=_('only use listed changesets'))
429 436 # and only one command per node
430 437 if self.node in seen:
431 438 raise error.ParseError(_('duplicated command for changeset %s') %
432 439 node.short(self.node))
433 440
434 441 def torule(self):
435 442 """build a histedit rule line for an action
436 443
437 444 by default lines are in the form:
438 445 <hash> <rev> <summary>
439 446 """
440 447 ctx = self.repo[self.node]
441 448 summary = _getsummary(ctx)
442 449 line = '%s %s %d %s' % (self.verb, ctx, ctx.rev(), summary)
443 450 # trim to 75 columns by default so it's not stupidly wide in my editor
444 451 # (the 5 more are left for verb)
445 452 maxlen = self.repo.ui.configint('histedit', 'linelen', default=80)
446 453 maxlen = max(maxlen, 22) # avoid truncating hash
447 454 return util.ellipsis(line, maxlen)
448 455
449 456 def tostate(self):
450 457 """Print an action in format used by histedit state files
451 458 (the first line is a verb, the remainder is the second)
452 459 """
453 460 return "%s\n%s" % (self.verb, node.hex(self.node))
454 461
455 462 def run(self):
456 463 """Runs the action. The default behavior is simply apply the action's
457 464 rulectx onto the current parentctx."""
458 465 self.applychange()
459 466 self.continuedirty()
460 467 return self.continueclean()
461 468
462 469 def applychange(self):
463 470 """Applies the changes from this action's rulectx onto the current
464 471 parentctx, but does not commit them."""
465 472 repo = self.repo
466 473 rulectx = repo[self.node]
467 474 repo.ui.pushbuffer(error=True, labeled=True)
468 475 hg.update(repo, self.state.parentctxnode, quietempty=True)
469 476 stats = applychanges(repo.ui, repo, rulectx, {})
470 477 if stats and stats[3] > 0:
471 478 buf = repo.ui.popbuffer()
472 479 repo.ui.write(*buf)
473 480 raise error.InterventionRequired(
474 481 _('Fix up the change (%s %s)') %
475 482 (self.verb, node.short(self.node)),
476 483 hint=_('hg histedit --continue to resume'))
477 484 else:
478 485 repo.ui.popbuffer()
479 486
480 487 def continuedirty(self):
481 488 """Continues the action when changes have been applied to the working
482 489 copy. The default behavior is to commit the dirty changes."""
483 490 repo = self.repo
484 491 rulectx = repo[self.node]
485 492
486 493 editor = self.commiteditor()
487 494 commit = commitfuncfor(repo, rulectx)
488 495
489 496 commit(text=rulectx.description(), user=rulectx.user(),
490 497 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
491 498
492 499 def commiteditor(self):
493 500 """The editor to be used to edit the commit message."""
494 501 return False
495 502
496 503 def continueclean(self):
497 504 """Continues the action when the working copy is clean. The default
498 505 behavior is to accept the current commit as the new version of the
499 506 rulectx."""
500 507 ctx = self.repo['.']
501 508 if ctx.node() == self.state.parentctxnode:
502 509 self.repo.ui.warn(_('%s: skipping changeset (no changes)\n') %
503 510 node.short(self.node))
504 511 return ctx, [(self.node, tuple())]
505 512 if ctx.node() == self.node:
506 513 # Nothing changed
507 514 return ctx, []
508 515 return ctx, [(self.node, (ctx.node(),))]
509 516
510 517 def commitfuncfor(repo, src):
511 518 """Build a commit function for the replacement of <src>
512 519
513 520 This function ensure we apply the same treatment to all changesets.
514 521
515 522 - Add a 'histedit_source' entry in extra.
516 523
517 524 Note that fold has its own separated logic because its handling is a bit
518 525 different and not easily factored out of the fold method.
519 526 """
520 527 phasemin = src.phase()
521 528 def commitfunc(**kwargs):
522 529 overrides = {('phases', 'new-commit'): phasemin}
523 530 with repo.ui.configoverride(overrides, 'histedit'):
524 531 extra = kwargs.get('extra', {}).copy()
525 532 extra['histedit_source'] = src.hex()
526 533 kwargs['extra'] = extra
527 534 return repo.commit(**kwargs)
528 535 return commitfunc
529 536
530 537 def applychanges(ui, repo, ctx, opts):
531 538 """Merge changeset from ctx (only) in the current working directory"""
532 539 wcpar = repo.dirstate.parents()[0]
533 540 if ctx.p1().node() == wcpar:
534 541 # edits are "in place" we do not need to make any merge,
535 542 # just applies changes on parent for editing
536 543 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
537 544 stats = None
538 545 else:
539 546 try:
540 547 # ui.forcemerge is an internal variable, do not document
541 548 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
542 549 'histedit')
543 550 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
544 551 finally:
545 552 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
546 553 return stats
547 554
548 555 def collapse(repo, first, last, commitopts, skipprompt=False):
549 556 """collapse the set of revisions from first to last as new one.
550 557
551 558 Expected commit options are:
552 559 - message
553 560 - date
554 561 - username
555 562 Commit message is edited in all cases.
556 563
557 564 This function works in memory."""
558 565 ctxs = list(repo.set('%d::%d', first, last))
559 566 if not ctxs:
560 567 return None
561 568 for c in ctxs:
562 569 if not c.mutable():
563 570 raise error.ParseError(
564 571 _("cannot fold into public change %s") % node.short(c.node()))
565 572 base = first.parents()[0]
566 573
567 574 # commit a new version of the old changeset, including the update
568 575 # collect all files which might be affected
569 576 files = set()
570 577 for ctx in ctxs:
571 578 files.update(ctx.files())
572 579
573 580 # Recompute copies (avoid recording a -> b -> a)
574 581 copied = copies.pathcopies(base, last)
575 582
576 583 # prune files which were reverted by the updates
577 584 files = [f for f in files if not cmdutil.samefile(f, last, base)]
578 585 # commit version of these files as defined by head
579 586 headmf = last.manifest()
580 587 def filectxfn(repo, ctx, path):
581 588 if path in headmf:
582 589 fctx = last[path]
583 590 flags = fctx.flags()
584 591 mctx = context.memfilectx(repo,
585 592 fctx.path(), fctx.data(),
586 593 islink='l' in flags,
587 594 isexec='x' in flags,
588 595 copied=copied.get(path))
589 596 return mctx
590 597 return None
591 598
592 599 if commitopts.get('message'):
593 600 message = commitopts['message']
594 601 else:
595 602 message = first.description()
596 603 user = commitopts.get('user')
597 604 date = commitopts.get('date')
598 605 extra = commitopts.get('extra')
599 606
600 607 parents = (first.p1().node(), first.p2().node())
601 608 editor = None
602 609 if not skipprompt:
603 610 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
604 611 new = context.memctx(repo,
605 612 parents=parents,
606 613 text=message,
607 614 files=files,
608 615 filectxfn=filectxfn,
609 616 user=user,
610 617 date=date,
611 618 extra=extra,
612 619 editor=editor)
613 620 return repo.commitctx(new)
614 621
615 622 def _isdirtywc(repo):
616 623 return repo[None].dirty(missing=True)
617 624
618 625 def abortdirty():
619 626 raise error.Abort(_('working copy has pending changes'),
620 627 hint=_('amend, commit, or revert them and run histedit '
621 628 '--continue, or abort with histedit --abort'))
622 629
623 630 def action(verbs, message, priority=False, internal=False):
624 631 def wrap(cls):
625 632 assert not priority or not internal
626 633 verb = verbs[0]
627 634 if priority:
628 635 primaryactions.add(verb)
629 636 elif internal:
630 637 internalactions.add(verb)
631 638 elif len(verbs) > 1:
632 639 secondaryactions.add(verb)
633 640 else:
634 641 tertiaryactions.add(verb)
635 642
636 643 cls.verb = verb
637 644 cls.verbs = verbs
638 645 cls.message = message
639 646 for verb in verbs:
640 647 actiontable[verb] = cls
641 648 return cls
642 649 return wrap
643 650
644 651 @action(['pick', 'p'],
645 652 _('use commit'),
646 653 priority=True)
647 654 class pick(histeditaction):
648 655 def run(self):
649 656 rulectx = self.repo[self.node]
650 657 if rulectx.parents()[0].node() == self.state.parentctxnode:
651 658 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
652 659 return rulectx, []
653 660
654 661 return super(pick, self).run()
655 662
656 663 @action(['edit', 'e'],
657 664 _('use commit, but stop for amending'),
658 665 priority=True)
659 666 class edit(histeditaction):
660 667 def run(self):
661 668 repo = self.repo
662 669 rulectx = repo[self.node]
663 670 hg.update(repo, self.state.parentctxnode, quietempty=True)
664 671 applychanges(repo.ui, repo, rulectx, {})
665 672 raise error.InterventionRequired(
666 673 _('Editing (%s), you may commit or record as needed now.')
667 674 % node.short(self.node),
668 675 hint=_('hg histedit --continue to resume'))
669 676
670 677 def commiteditor(self):
671 678 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
672 679
673 680 @action(['fold', 'f'],
674 681 _('use commit, but combine it with the one above'))
675 682 class fold(histeditaction):
676 683 def verify(self, prev, expected, seen):
677 684 """ Verifies semantic correctness of the fold rule"""
678 685 super(fold, self).verify(prev, expected, seen)
679 686 repo = self.repo
680 687 if not prev:
681 688 c = repo[self.node].parents()[0]
682 689 elif not prev.verb in ('pick', 'base'):
683 690 return
684 691 else:
685 692 c = repo[prev.node]
686 693 if not c.mutable():
687 694 raise error.ParseError(
688 695 _("cannot fold into public change %s") % node.short(c.node()))
689 696
690 697
691 698 def continuedirty(self):
692 699 repo = self.repo
693 700 rulectx = repo[self.node]
694 701
695 702 commit = commitfuncfor(repo, rulectx)
696 703 commit(text='fold-temp-revision %s' % node.short(self.node),
697 704 user=rulectx.user(), date=rulectx.date(),
698 705 extra=rulectx.extra())
699 706
700 707 def continueclean(self):
701 708 repo = self.repo
702 709 ctx = repo['.']
703 710 rulectx = repo[self.node]
704 711 parentctxnode = self.state.parentctxnode
705 712 if ctx.node() == parentctxnode:
706 713 repo.ui.warn(_('%s: empty changeset\n') %
707 714 node.short(self.node))
708 715 return ctx, [(self.node, (parentctxnode,))]
709 716
710 717 parentctx = repo[parentctxnode]
711 718 newcommits = set(c.node() for c in repo.set('(%d::. - %d)', parentctx,
712 719 parentctx))
713 720 if not newcommits:
714 721 repo.ui.warn(_('%s: cannot fold - working copy is not a '
715 722 'descendant of previous commit %s\n') %
716 723 (node.short(self.node), node.short(parentctxnode)))
717 724 return ctx, [(self.node, (ctx.node(),))]
718 725
719 726 middlecommits = newcommits.copy()
720 727 middlecommits.discard(ctx.node())
721 728
722 729 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
723 730 middlecommits)
724 731
725 732 def skipprompt(self):
726 733 """Returns true if the rule should skip the message editor.
727 734
728 735 For example, 'fold' wants to show an editor, but 'rollup'
729 736 doesn't want to.
730 737 """
731 738 return False
732 739
733 740 def mergedescs(self):
734 741 """Returns true if the rule should merge messages of multiple changes.
735 742
736 743 This exists mainly so that 'rollup' rules can be a subclass of
737 744 'fold'.
738 745 """
739 746 return True
740 747
741 748 def firstdate(self):
742 749 """Returns true if the rule should preserve the date of the first
743 750 change.
744 751
745 752 This exists mainly so that 'rollup' rules can be a subclass of
746 753 'fold'.
747 754 """
748 755 return False
749 756
750 757 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
751 758 parent = ctx.parents()[0].node()
752 759 repo.ui.pushbuffer()
753 760 hg.update(repo, parent)
754 761 repo.ui.popbuffer()
755 762 ### prepare new commit data
756 763 commitopts = {}
757 764 commitopts['user'] = ctx.user()
758 765 # commit message
759 766 if not self.mergedescs():
760 767 newmessage = ctx.description()
761 768 else:
762 769 newmessage = '\n***\n'.join(
763 770 [ctx.description()] +
764 771 [repo[r].description() for r in internalchanges] +
765 772 [oldctx.description()]) + '\n'
766 773 commitopts['message'] = newmessage
767 774 # date
768 775 if self.firstdate():
769 776 commitopts['date'] = ctx.date()
770 777 else:
771 778 commitopts['date'] = max(ctx.date(), oldctx.date())
772 779 extra = ctx.extra().copy()
773 780 # histedit_source
774 781 # note: ctx is likely a temporary commit but that the best we can do
775 782 # here. This is sufficient to solve issue3681 anyway.
776 783 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
777 784 commitopts['extra'] = extra
778 785 phasemin = max(ctx.phase(), oldctx.phase())
779 786 overrides = {('phases', 'new-commit'): phasemin}
780 787 with repo.ui.configoverride(overrides, 'histedit'):
781 788 n = collapse(repo, ctx, repo[newnode], commitopts,
782 789 skipprompt=self.skipprompt())
783 790 if n is None:
784 791 return ctx, []
785 792 repo.ui.pushbuffer()
786 793 hg.update(repo, n)
787 794 repo.ui.popbuffer()
788 795 replacements = [(oldctx.node(), (newnode,)),
789 796 (ctx.node(), (n,)),
790 797 (newnode, (n,)),
791 798 ]
792 799 for ich in internalchanges:
793 800 replacements.append((ich, (n,)))
794 801 return repo[n], replacements
795 802
796 803 class base(histeditaction):
797 804
798 805 def run(self):
799 806 if self.repo['.'].node() != self.node:
800 807 mergemod.update(self.repo, self.node, False, True)
801 808 # branchmerge, force)
802 809 return self.continueclean()
803 810
804 811 def continuedirty(self):
805 812 abortdirty()
806 813
807 814 def continueclean(self):
808 815 basectx = self.repo['.']
809 816 return basectx, []
810 817
811 818 def _verifynodeconstraints(self, prev, expected, seen):
812 819 # base can only be use with a node not in the edited set
813 820 if self.node in expected:
814 821 msg = _('%s "%s" changeset was an edited list candidate')
815 822 raise error.ParseError(
816 823 msg % (self.verb, node.short(self.node)),
817 824 hint=_('base must only use unlisted changesets'))
818 825
819 826 @action(['_multifold'],
820 827 _(
821 828 """fold subclass used for when multiple folds happen in a row
822 829
823 830 We only want to fire the editor for the folded message once when
824 831 (say) four changes are folded down into a single change. This is
825 832 similar to rollup, but we should preserve both messages so that
826 833 when the last fold operation runs we can show the user all the
827 834 commit messages in their editor.
828 835 """),
829 836 internal=True)
830 837 class _multifold(fold):
831 838 def skipprompt(self):
832 839 return True
833 840
834 841 @action(["roll", "r"],
835 842 _("like fold, but discard this commit's description and date"))
836 843 class rollup(fold):
837 844 def mergedescs(self):
838 845 return False
839 846
840 847 def skipprompt(self):
841 848 return True
842 849
843 850 def firstdate(self):
844 851 return True
845 852
846 853 @action(["drop", "d"],
847 854 _('remove commit from history'))
848 855 class drop(histeditaction):
849 856 def run(self):
850 857 parentctx = self.repo[self.state.parentctxnode]
851 858 return parentctx, [(self.node, tuple())]
852 859
853 860 @action(["mess", "m"],
854 861 _('edit commit message without changing commit content'),
855 862 priority=True)
856 863 class message(histeditaction):
857 864 def commiteditor(self):
858 865 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
859 866
860 867 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
861 868 """utility function to find the first outgoing changeset
862 869
863 870 Used by initialization code"""
864 871 if opts is None:
865 872 opts = {}
866 873 dest = ui.expandpath(remote or 'default-push', remote or 'default')
867 874 dest, revs = hg.parseurl(dest, None)[:2]
868 875 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
869 876
870 877 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
871 878 other = hg.peer(repo, opts, dest)
872 879
873 880 if revs:
874 881 revs = [repo.lookup(rev) for rev in revs]
875 882
876 883 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
877 884 if not outgoing.missing:
878 885 raise error.Abort(_('no outgoing ancestors'))
879 886 roots = list(repo.revs("roots(%ln)", outgoing.missing))
880 887 if 1 < len(roots):
881 888 msg = _('there are ambiguous outgoing revisions')
882 889 hint = _("see 'hg help histedit' for more detail")
883 890 raise error.Abort(msg, hint=hint)
884 891 return repo.lookup(roots[0])
885 892
886 893
887 894 @command('histedit',
888 895 [('', 'commands', '',
889 896 _('read history edits from the specified file'), _('FILE')),
890 897 ('c', 'continue', False, _('continue an edit already in progress')),
891 898 ('', 'edit-plan', False, _('edit remaining actions list')),
892 899 ('k', 'keep', False,
893 900 _("don't strip old nodes after edit is complete")),
894 901 ('', 'abort', False, _('abort an edit in progress')),
895 902 ('o', 'outgoing', False, _('changesets not found in destination')),
896 903 ('f', 'force', False,
897 904 _('force outgoing even for unrelated repositories')),
898 905 ('r', 'rev', [], _('first revision to be edited'), _('REV'))],
899 906 _("[OPTIONS] ([ANCESTOR] | --outgoing [URL])"))
900 907 def histedit(ui, repo, *freeargs, **opts):
901 908 """interactively edit changeset history
902 909
903 910 This command lets you edit a linear series of changesets (up to
904 911 and including the working directory, which should be clean).
905 912 You can:
906 913
907 914 - `pick` to [re]order a changeset
908 915
909 916 - `drop` to omit changeset
910 917
911 918 - `mess` to reword the changeset commit message
912 919
913 920 - `fold` to combine it with the preceding changeset (using the later date)
914 921
915 922 - `roll` like fold, but discarding this commit's description and date
916 923
917 924 - `edit` to edit this changeset (preserving date)
918 925
919 926 There are a number of ways to select the root changeset:
920 927
921 928 - Specify ANCESTOR directly
922 929
923 930 - Use --outgoing -- it will be the first linear changeset not
924 931 included in destination. (See :hg:`help config.paths.default-push`)
925 932
926 933 - Otherwise, the value from the "histedit.defaultrev" config option
927 934 is used as a revset to select the base revision when ANCESTOR is not
928 935 specified. The first revision returned by the revset is used. By
929 936 default, this selects the editable history that is unique to the
930 937 ancestry of the working directory.
931 938
932 939 .. container:: verbose
933 940
934 941 If you use --outgoing, this command will abort if there are ambiguous
935 942 outgoing revisions. For example, if there are multiple branches
936 943 containing outgoing revisions.
937 944
938 945 Use "min(outgoing() and ::.)" or similar revset specification
939 946 instead of --outgoing to specify edit target revision exactly in
940 947 such ambiguous situation. See :hg:`help revsets` for detail about
941 948 selecting revisions.
942 949
943 950 .. container:: verbose
944 951
945 952 Examples:
946 953
947 954 - A number of changes have been made.
948 955 Revision 3 is no longer needed.
949 956
950 957 Start history editing from revision 3::
951 958
952 959 hg histedit -r 3
953 960
954 961 An editor opens, containing the list of revisions,
955 962 with specific actions specified::
956 963
957 964 pick 5339bf82f0ca 3 Zworgle the foobar
958 965 pick 8ef592ce7cc4 4 Bedazzle the zerlog
959 966 pick 0a9639fcda9d 5 Morgify the cromulancy
960 967
961 968 Additional information about the possible actions
962 969 to take appears below the list of revisions.
963 970
964 971 To remove revision 3 from the history,
965 972 its action (at the beginning of the relevant line)
966 973 is changed to 'drop'::
967 974
968 975 drop 5339bf82f0ca 3 Zworgle the foobar
969 976 pick 8ef592ce7cc4 4 Bedazzle the zerlog
970 977 pick 0a9639fcda9d 5 Morgify the cromulancy
971 978
972 979 - A number of changes have been made.
973 980 Revision 2 and 4 need to be swapped.
974 981
975 982 Start history editing from revision 2::
976 983
977 984 hg histedit -r 2
978 985
979 986 An editor opens, containing the list of revisions,
980 987 with specific actions specified::
981 988
982 989 pick 252a1af424ad 2 Blorb a morgwazzle
983 990 pick 5339bf82f0ca 3 Zworgle the foobar
984 991 pick 8ef592ce7cc4 4 Bedazzle the zerlog
985 992
986 993 To swap revision 2 and 4, its lines are swapped
987 994 in the editor::
988 995
989 996 pick 8ef592ce7cc4 4 Bedazzle the zerlog
990 997 pick 5339bf82f0ca 3 Zworgle the foobar
991 998 pick 252a1af424ad 2 Blorb a morgwazzle
992 999
993 1000 Returns 0 on success, 1 if user intervention is required (not only
994 1001 for intentional "edit" command, but also for resolving unexpected
995 1002 conflicts).
996 1003 """
997 1004 state = histeditstate(repo)
998 1005 try:
999 1006 state.wlock = repo.wlock()
1000 1007 state.lock = repo.lock()
1001 1008 _histedit(ui, repo, state, *freeargs, **opts)
1002 1009 finally:
1003 1010 release(state.lock, state.wlock)
1004 1011
1005 1012 goalcontinue = 'continue'
1006 1013 goalabort = 'abort'
1007 1014 goaleditplan = 'edit-plan'
1008 1015 goalnew = 'new'
1009 1016
1010 1017 def _getgoal(opts):
1011 1018 if opts.get('continue'):
1012 1019 return goalcontinue
1013 1020 if opts.get('abort'):
1014 1021 return goalabort
1015 1022 if opts.get('edit_plan'):
1016 1023 return goaleditplan
1017 1024 return goalnew
1018 1025
1019 1026 def _readfile(ui, path):
1020 1027 if path == '-':
1021 1028 with ui.timeblockedsection('histedit'):
1022 1029 return ui.fin.read()
1023 1030 else:
1024 1031 with open(path, 'rb') as f:
1025 1032 return f.read()
1026 1033
1027 1034 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1028 1035 # TODO only abort if we try to histedit mq patches, not just
1029 1036 # blanket if mq patches are applied somewhere
1030 1037 mq = getattr(repo, 'mq', None)
1031 1038 if mq and mq.applied:
1032 1039 raise error.Abort(_('source has mq patches applied'))
1033 1040
1034 1041 # basic argument incompatibility processing
1035 1042 outg = opts.get('outgoing')
1036 1043 editplan = opts.get('edit_plan')
1037 1044 abort = opts.get('abort')
1038 1045 force = opts.get('force')
1039 1046 if force and not outg:
1040 1047 raise error.Abort(_('--force only allowed with --outgoing'))
1041 1048 if goal == 'continue':
1042 1049 if any((outg, abort, revs, freeargs, rules, editplan)):
1043 1050 raise error.Abort(_('no arguments allowed with --continue'))
1044 1051 elif goal == 'abort':
1045 1052 if any((outg, revs, freeargs, rules, editplan)):
1046 1053 raise error.Abort(_('no arguments allowed with --abort'))
1047 1054 elif goal == 'edit-plan':
1048 1055 if any((outg, revs, freeargs)):
1049 1056 raise error.Abort(_('only --commands argument allowed with '
1050 1057 '--edit-plan'))
1051 1058 else:
1052 1059 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1053 1060 raise error.Abort(_('history edit already in progress, try '
1054 1061 '--continue or --abort'))
1055 1062 if outg:
1056 1063 if revs:
1057 1064 raise error.Abort(_('no revisions allowed with --outgoing'))
1058 1065 if len(freeargs) > 1:
1059 1066 raise error.Abort(
1060 1067 _('only one repo argument allowed with --outgoing'))
1061 1068 else:
1062 1069 revs.extend(freeargs)
1063 1070 if len(revs) == 0:
1064 1071 defaultrev = destutil.desthistedit(ui, repo)
1065 1072 if defaultrev is not None:
1066 1073 revs.append(defaultrev)
1067 1074
1068 1075 if len(revs) != 1:
1069 1076 raise error.Abort(
1070 1077 _('histedit requires exactly one ancestor revision'))
1071 1078
1072 1079 def _histedit(ui, repo, state, *freeargs, **opts):
1073 1080 goal = _getgoal(opts)
1074 1081 revs = opts.get('rev', [])
1075 1082 rules = opts.get('commands', '')
1076 1083 state.keep = opts.get('keep', False)
1077 1084
1078 1085 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1079 1086
1080 1087 # rebuild state
1081 1088 if goal == goalcontinue:
1082 1089 state.read()
1083 1090 state = bootstrapcontinue(ui, state, opts)
1084 1091 elif goal == goaleditplan:
1085 1092 _edithisteditplan(ui, repo, state, rules)
1086 1093 return
1087 1094 elif goal == goalabort:
1088 1095 _aborthistedit(ui, repo, state)
1089 1096 return
1090 1097 else:
1091 1098 # goal == goalnew
1092 1099 _newhistedit(ui, repo, state, revs, freeargs, opts)
1093 1100
1094 1101 _continuehistedit(ui, repo, state)
1095 1102 _finishhistedit(ui, repo, state)
1096 1103
1097 1104 def _continuehistedit(ui, repo, state):
1098 1105 """This function runs after either:
1099 1106 - bootstrapcontinue (if the goal is 'continue')
1100 1107 - _newhistedit (if the goal is 'new')
1101 1108 """
1102 1109 # preprocess rules so that we can hide inner folds from the user
1103 1110 # and only show one editor
1104 1111 actions = state.actions[:]
1105 1112 for idx, (action, nextact) in enumerate(
1106 1113 zip(actions, actions[1:] + [None])):
1107 1114 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1108 1115 state.actions[idx].__class__ = _multifold
1109 1116
1110 1117 # Force an initial state file write, so the user can run --abort/continue
1111 1118 # even if there's an exception before the first transaction serialize.
1112 1119 state.write()
1113 1120
1114 1121 total = len(state.actions)
1115 1122 pos = 0
1116 1123 tr = None
1117 1124 # Don't use singletransaction by default since it rolls the entire
1118 1125 # transaction back if an unexpected exception happens (like a
1119 1126 # pretxncommit hook throws, or the user aborts the commit msg editor).
1120 1127 if ui.configbool("histedit", "singletransaction", False):
1121 1128 # Don't use a 'with' for the transaction, since actions may close
1122 1129 # and reopen a transaction. For example, if the action executes an
1123 1130 # external process it may choose to commit the transaction first.
1124 1131 tr = repo.transaction('histedit')
1125 1132 with util.acceptintervention(tr):
1126 1133 while state.actions:
1127 1134 state.write(tr=tr)
1128 1135 actobj = state.actions[0]
1129 1136 pos += 1
1130 1137 ui.progress(_("editing"), pos, actobj.torule(),
1131 1138 _('changes'), total)
1132 1139 ui.debug('histedit: processing %s %s\n' % (actobj.verb,\
1133 1140 actobj.torule()))
1134 1141 parentctx, replacement_ = actobj.run()
1135 1142 state.parentctxnode = parentctx.node()
1136 1143 state.replacements.extend(replacement_)
1137 1144 state.actions.pop(0)
1138 1145
1139 1146 state.write()
1140 1147 ui.progress(_("editing"), None)
1141 1148
1142 1149 def _finishhistedit(ui, repo, state):
1143 1150 """This action runs when histedit is finishing its session"""
1144 1151 repo.ui.pushbuffer()
1145 1152 hg.update(repo, state.parentctxnode, quietempty=True)
1146 1153 repo.ui.popbuffer()
1147 1154
1148 1155 mapping, tmpnodes, created, ntm = processreplacement(state)
1149 1156 if mapping:
1150 1157 for prec, succs in mapping.iteritems():
1151 1158 if not succs:
1152 1159 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1153 1160 else:
1154 1161 ui.debug('histedit: %s is replaced by %s\n' % (
1155 1162 node.short(prec), node.short(succs[0])))
1156 1163 if len(succs) > 1:
1157 1164 m = 'histedit: %s'
1158 1165 for n in succs[1:]:
1159 1166 ui.debug(m % node.short(n))
1160 1167
1161 1168 if not state.keep:
1162 1169 if mapping:
1163 1170 movetopmostbookmarks(repo, state.topmost, ntm)
1164 1171 # TODO update mq state
1165 1172 else:
1166 1173 mapping = {}
1167 1174
1168 1175 for n in tmpnodes:
1169 1176 mapping[n] = ()
1170 1177
1171 1178 # remove entries about unknown nodes
1172 1179 nodemap = repo.unfiltered().changelog.nodemap
1173 1180 mapping = {k: v for k, v in mapping.items()
1174 1181 if k in nodemap and all(n in nodemap for n in v)}
1175 1182 scmutil.cleanupnodes(repo, mapping, 'histedit')
1176 1183
1177 1184 state.clear()
1178 1185 if os.path.exists(repo.sjoin('undo')):
1179 1186 os.unlink(repo.sjoin('undo'))
1180 1187 if repo.vfs.exists('histedit-last-edit.txt'):
1181 1188 repo.vfs.unlink('histedit-last-edit.txt')
1182 1189
1183 1190 def _aborthistedit(ui, repo, state):
1184 1191 try:
1185 1192 state.read()
1186 1193 __, leafs, tmpnodes, __ = processreplacement(state)
1187 1194 ui.debug('restore wc to old parent %s\n'
1188 1195 % node.short(state.topmost))
1189 1196
1190 1197 # Recover our old commits if necessary
1191 1198 if not state.topmost in repo and state.backupfile:
1192 1199 backupfile = repo.vfs.join(state.backupfile)
1193 1200 f = hg.openpath(ui, backupfile)
1194 1201 gen = exchange.readbundle(ui, f, backupfile)
1195 1202 with repo.transaction('histedit.abort') as tr:
1196 1203 bundle2.applybundle(repo, gen, tr, source='histedit',
1197 1204 url='bundle:' + backupfile)
1198 1205
1199 1206 os.remove(backupfile)
1200 1207
1201 1208 # check whether we should update away
1202 1209 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1203 1210 state.parentctxnode, leafs | tmpnodes):
1204 1211 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1205 1212 cleanupnode(ui, repo, tmpnodes)
1206 1213 cleanupnode(ui, repo, leafs)
1207 1214 except Exception:
1208 1215 if state.inprogress():
1209 1216 ui.warn(_('warning: encountered an exception during histedit '
1210 1217 '--abort; the repository may not have been completely '
1211 1218 'cleaned up\n'))
1212 1219 raise
1213 1220 finally:
1214 1221 state.clear()
1215 1222
1216 1223 def _edithisteditplan(ui, repo, state, rules):
1217 1224 state.read()
1218 1225 if not rules:
1219 1226 comment = geteditcomment(ui,
1220 1227 node.short(state.parentctxnode),
1221 1228 node.short(state.topmost))
1222 1229 rules = ruleeditor(repo, ui, state.actions, comment)
1223 1230 else:
1224 1231 rules = _readfile(ui, rules)
1225 1232 actions = parserules(rules, state)
1226 1233 ctxs = [repo[act.node] \
1227 1234 for act in state.actions if act.node]
1228 1235 warnverifyactions(ui, repo, actions, state, ctxs)
1229 1236 state.actions = actions
1230 1237 state.write()
1231 1238
1232 1239 def _newhistedit(ui, repo, state, revs, freeargs, opts):
1233 1240 outg = opts.get('outgoing')
1234 1241 rules = opts.get('commands', '')
1235 1242 force = opts.get('force')
1236 1243
1237 1244 cmdutil.checkunfinished(repo)
1238 1245 cmdutil.bailifchanged(repo)
1239 1246
1240 1247 topmost, empty = repo.dirstate.parents()
1241 1248 if outg:
1242 1249 if freeargs:
1243 1250 remote = freeargs[0]
1244 1251 else:
1245 1252 remote = None
1246 1253 root = findoutgoing(ui, repo, remote, force, opts)
1247 1254 else:
1248 1255 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1249 1256 if len(rr) != 1:
1250 1257 raise error.Abort(_('The specified revisions must have '
1251 1258 'exactly one common root'))
1252 1259 root = rr[0].node()
1253 1260
1254 1261 revs = between(repo, root, topmost, state.keep)
1255 1262 if not revs:
1256 1263 raise error.Abort(_('%s is not an ancestor of working directory') %
1257 1264 node.short(root))
1258 1265
1259 1266 ctxs = [repo[r] for r in revs]
1260 1267 if not rules:
1261 1268 comment = geteditcomment(ui, node.short(root), node.short(topmost))
1262 1269 actions = [pick(state, r) for r in revs]
1263 1270 rules = ruleeditor(repo, ui, actions, comment)
1264 1271 else:
1265 1272 rules = _readfile(ui, rules)
1266 1273 actions = parserules(rules, state)
1267 1274 warnverifyactions(ui, repo, actions, state, ctxs)
1268 1275
1269 1276 parentctxnode = repo[root].parents()[0].node()
1270 1277
1271 1278 state.parentctxnode = parentctxnode
1272 1279 state.actions = actions
1273 1280 state.topmost = topmost
1274 1281 state.replacements = []
1275 1282
1276 1283 # Create a backup so we can always abort completely.
1277 1284 backupfile = None
1278 1285 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1279 1286 backupfile = repair._bundle(repo, [parentctxnode], [topmost], root,
1280 1287 'histedit')
1281 1288 state.backupfile = backupfile
1282 1289
1283 1290 def _getsummary(ctx):
1284 1291 # a common pattern is to extract the summary but default to the empty
1285 1292 # string
1286 1293 summary = ctx.description() or ''
1287 1294 if summary:
1288 1295 summary = summary.splitlines()[0]
1289 1296 return summary
1290 1297
1291 1298 def bootstrapcontinue(ui, state, opts):
1292 1299 repo = state.repo
1293 1300
1294 1301 ms = mergemod.mergestate.read(repo)
1295 1302 mergeutil.checkunresolved(ms)
1296 1303
1297 1304 if state.actions:
1298 1305 actobj = state.actions.pop(0)
1299 1306
1300 1307 if _isdirtywc(repo):
1301 1308 actobj.continuedirty()
1302 1309 if _isdirtywc(repo):
1303 1310 abortdirty()
1304 1311
1305 1312 parentctx, replacements = actobj.continueclean()
1306 1313
1307 1314 state.parentctxnode = parentctx.node()
1308 1315 state.replacements.extend(replacements)
1309 1316
1310 1317 return state
1311 1318
1312 1319 def between(repo, old, new, keep):
1313 1320 """select and validate the set of revision to edit
1314 1321
1315 1322 When keep is false, the specified set can't have children."""
1316 1323 ctxs = list(repo.set('%n::%n', old, new))
1317 1324 if ctxs and not keep:
1318 1325 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1319 1326 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
1320 1327 raise error.Abort(_('can only histedit a changeset together '
1321 1328 'with all its descendants'))
1322 1329 if repo.revs('(%ld) and merge()', ctxs):
1323 1330 raise error.Abort(_('cannot edit history that contains merges'))
1324 1331 root = ctxs[0] # list is already sorted by repo.set
1325 1332 if not root.mutable():
1326 1333 raise error.Abort(_('cannot edit public changeset: %s') % root,
1327 1334 hint=_("see 'hg help phases' for details"))
1328 1335 return [c.node() for c in ctxs]
1329 1336
1330 1337 def ruleeditor(repo, ui, actions, editcomment=""):
1331 1338 """open an editor to edit rules
1332 1339
1333 1340 rules are in the format [ [act, ctx], ...] like in state.rules
1334 1341 """
1335 1342 if repo.ui.configbool("experimental", "histedit.autoverb"):
1336 1343 newact = util.sortdict()
1337 1344 for act in actions:
1338 1345 ctx = repo[act.node]
1339 1346 summary = _getsummary(ctx)
1340 1347 fword = summary.split(' ', 1)[0].lower()
1341 1348 added = False
1342 1349
1343 1350 # if it doesn't end with the special character '!' just skip this
1344 1351 if fword.endswith('!'):
1345 1352 fword = fword[:-1]
1346 1353 if fword in primaryactions | secondaryactions | tertiaryactions:
1347 1354 act.verb = fword
1348 1355 # get the target summary
1349 1356 tsum = summary[len(fword) + 1:].lstrip()
1350 1357 # safe but slow: reverse iterate over the actions so we
1351 1358 # don't clash on two commits having the same summary
1352 1359 for na, l in reversed(list(newact.iteritems())):
1353 1360 actx = repo[na.node]
1354 1361 asum = _getsummary(actx)
1355 1362 if asum == tsum:
1356 1363 added = True
1357 1364 l.append(act)
1358 1365 break
1359 1366
1360 1367 if not added:
1361 1368 newact[act] = []
1362 1369
1363 1370 # copy over and flatten the new list
1364 1371 actions = []
1365 1372 for na, l in newact.iteritems():
1366 1373 actions.append(na)
1367 1374 actions += l
1368 1375
1369 1376 rules = '\n'.join([act.torule() for act in actions])
1370 1377 rules += '\n\n'
1371 1378 rules += editcomment
1372 1379 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'},
1373 1380 repopath=repo.path, action='histedit')
1374 1381
1375 1382 # Save edit rules in .hg/histedit-last-edit.txt in case
1376 1383 # the user needs to ask for help after something
1377 1384 # surprising happens.
1378 1385 f = open(repo.vfs.join('histedit-last-edit.txt'), 'w')
1379 1386 f.write(rules)
1380 1387 f.close()
1381 1388
1382 1389 return rules
1383 1390
1384 1391 def parserules(rules, state):
1385 1392 """Read the histedit rules string and return list of action objects """
1386 1393 rules = [l for l in (r.strip() for r in rules.splitlines())
1387 1394 if l and not l.startswith('#')]
1388 1395 actions = []
1389 1396 for r in rules:
1390 1397 if ' ' not in r:
1391 1398 raise error.ParseError(_('malformed line "%s"') % r)
1392 1399 verb, rest = r.split(' ', 1)
1393 1400
1394 1401 if verb not in actiontable:
1395 1402 raise error.ParseError(_('unknown action "%s"') % verb)
1396 1403
1397 1404 action = actiontable[verb].fromrule(state, rest)
1398 1405 actions.append(action)
1399 1406 return actions
1400 1407
1401 1408 def warnverifyactions(ui, repo, actions, state, ctxs):
1402 1409 try:
1403 1410 verifyactions(actions, state, ctxs)
1404 1411 except error.ParseError:
1405 1412 if repo.vfs.exists('histedit-last-edit.txt'):
1406 1413 ui.warn(_('warning: histedit rules saved '
1407 1414 'to: .hg/histedit-last-edit.txt\n'))
1408 1415 raise
1409 1416
1410 1417 def verifyactions(actions, state, ctxs):
1411 1418 """Verify that there exists exactly one action per given changeset and
1412 1419 other constraints.
1413 1420
1414 1421 Will abort if there are to many or too few rules, a malformed rule,
1415 1422 or a rule on a changeset outside of the user-given range.
1416 1423 """
1417 1424 expected = set(c.node() for c in ctxs)
1418 1425 seen = set()
1419 1426 prev = None
1420 1427
1421 1428 if actions and actions[0].verb in ['roll', 'fold']:
1422 1429 raise error.ParseError(_('first changeset cannot use verb "%s"') %
1423 1430 actions[0].verb)
1424 1431
1425 1432 for action in actions:
1426 1433 action.verify(prev, expected, seen)
1427 1434 prev = action
1428 1435 if action.node is not None:
1429 1436 seen.add(action.node)
1430 1437 missing = sorted(expected - seen) # sort to stabilize output
1431 1438
1432 1439 if state.repo.ui.configbool('histedit', 'dropmissing'):
1433 1440 if len(actions) == 0:
1434 1441 raise error.ParseError(_('no rules provided'),
1435 1442 hint=_('use strip extension to remove commits'))
1436 1443
1437 1444 drops = [drop(state, n) for n in missing]
1438 1445 # put the in the beginning so they execute immediately and
1439 1446 # don't show in the edit-plan in the future
1440 1447 actions[:0] = drops
1441 1448 elif missing:
1442 1449 raise error.ParseError(_('missing rules for changeset %s') %
1443 1450 node.short(missing[0]),
1444 1451 hint=_('use "drop %s" to discard, see also: '
1445 1452 "'hg help -e histedit.config'")
1446 1453 % node.short(missing[0]))
1447 1454
1448 1455 def adjustreplacementsfrommarkers(repo, oldreplacements):
1449 1456 """Adjust replacements from obsolescence markers
1450 1457
1451 1458 Replacements structure is originally generated based on
1452 1459 histedit's state and does not account for changes that are
1453 1460 not recorded there. This function fixes that by adding
1454 1461 data read from obsolescence markers"""
1455 1462 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1456 1463 return oldreplacements
1457 1464
1458 1465 unfi = repo.unfiltered()
1459 1466 nm = unfi.changelog.nodemap
1460 1467 obsstore = repo.obsstore
1461 1468 newreplacements = list(oldreplacements)
1462 1469 oldsuccs = [r[1] for r in oldreplacements]
1463 1470 # successors that have already been added to succstocheck once
1464 1471 seensuccs = set().union(*oldsuccs) # create a set from an iterable of tuples
1465 1472 succstocheck = list(seensuccs)
1466 1473 while succstocheck:
1467 1474 n = succstocheck.pop()
1468 1475 missing = nm.get(n) is None
1469 1476 markers = obsstore.successors.get(n, ())
1470 1477 if missing and not markers:
1471 1478 # dead end, mark it as such
1472 1479 newreplacements.append((n, ()))
1473 1480 for marker in markers:
1474 1481 nsuccs = marker[1]
1475 1482 newreplacements.append((n, nsuccs))
1476 1483 for nsucc in nsuccs:
1477 1484 if nsucc not in seensuccs:
1478 1485 seensuccs.add(nsucc)
1479 1486 succstocheck.append(nsucc)
1480 1487
1481 1488 return newreplacements
1482 1489
1483 1490 def processreplacement(state):
1484 1491 """process the list of replacements to return
1485 1492
1486 1493 1) the final mapping between original and created nodes
1487 1494 2) the list of temporary node created by histedit
1488 1495 3) the list of new commit created by histedit"""
1489 1496 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
1490 1497 allsuccs = set()
1491 1498 replaced = set()
1492 1499 fullmapping = {}
1493 1500 # initialize basic set
1494 1501 # fullmapping records all operations recorded in replacement
1495 1502 for rep in replacements:
1496 1503 allsuccs.update(rep[1])
1497 1504 replaced.add(rep[0])
1498 1505 fullmapping.setdefault(rep[0], set()).update(rep[1])
1499 1506 new = allsuccs - replaced
1500 1507 tmpnodes = allsuccs & replaced
1501 1508 # Reduce content fullmapping into direct relation between original nodes
1502 1509 # and final node created during history edition
1503 1510 # Dropped changeset are replaced by an empty list
1504 1511 toproceed = set(fullmapping)
1505 1512 final = {}
1506 1513 while toproceed:
1507 1514 for x in list(toproceed):
1508 1515 succs = fullmapping[x]
1509 1516 for s in list(succs):
1510 1517 if s in toproceed:
1511 1518 # non final node with unknown closure
1512 1519 # We can't process this now
1513 1520 break
1514 1521 elif s in final:
1515 1522 # non final node, replace with closure
1516 1523 succs.remove(s)
1517 1524 succs.update(final[s])
1518 1525 else:
1519 1526 final[x] = succs
1520 1527 toproceed.remove(x)
1521 1528 # remove tmpnodes from final mapping
1522 1529 for n in tmpnodes:
1523 1530 del final[n]
1524 1531 # we expect all changes involved in final to exist in the repo
1525 1532 # turn `final` into list (topologically sorted)
1526 1533 nm = state.repo.changelog.nodemap
1527 1534 for prec, succs in final.items():
1528 1535 final[prec] = sorted(succs, key=nm.get)
1529 1536
1530 1537 # computed topmost element (necessary for bookmark)
1531 1538 if new:
1532 1539 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1533 1540 elif not final:
1534 1541 # Nothing rewritten at all. we won't need `newtopmost`
1535 1542 # It is the same as `oldtopmost` and `processreplacement` know it
1536 1543 newtopmost = None
1537 1544 else:
1538 1545 # every body died. The newtopmost is the parent of the root.
1539 1546 r = state.repo.changelog.rev
1540 1547 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1541 1548
1542 1549 return final, tmpnodes, new, newtopmost
1543 1550
1544 1551 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
1545 1552 """Move bookmark from oldtopmost to newly created topmost
1546 1553
1547 1554 This is arguably a feature and we may only want that for the active
1548 1555 bookmark. But the behavior is kept compatible with the old version for now.
1549 1556 """
1550 1557 if not oldtopmost or not newtopmost:
1551 1558 return
1552 1559 oldbmarks = repo.nodebookmarks(oldtopmost)
1553 1560 if oldbmarks:
1554 1561 with repo.lock(), repo.transaction('histedit') as tr:
1555 1562 marks = repo._bookmarks
1556 1563 changes = []
1557 1564 for name in oldbmarks:
1558 1565 changes.append((name, newtopmost))
1559 1566 marks.applychanges(repo, tr, changes)
1560 1567
1561 1568 def cleanupnode(ui, repo, nodes):
1562 1569 """strip a group of nodes from the repository
1563 1570
1564 1571 The set of node to strip may contains unknown nodes."""
1565 1572 with repo.lock():
1566 1573 # do not let filtering get in the way of the cleanse
1567 1574 # we should probably get rid of obsolescence marker created during the
1568 1575 # histedit, but we currently do not have such information.
1569 1576 repo = repo.unfiltered()
1570 1577 # Find all nodes that need to be stripped
1571 1578 # (we use %lr instead of %ln to silently ignore unknown items)
1572 1579 nm = repo.changelog.nodemap
1573 1580 nodes = sorted(n for n in nodes if n in nm)
1574 1581 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1575 1582 if roots:
1576 1583 repair.strip(ui, repo, roots)
1577 1584
1578 1585 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1579 1586 if isinstance(nodelist, str):
1580 1587 nodelist = [nodelist]
1581 1588 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1582 1589 state = histeditstate(repo)
1583 1590 state.read()
1584 1591 histedit_nodes = {action.node for action
1585 1592 in state.actions if action.node}
1586 1593 common_nodes = histedit_nodes & set(nodelist)
1587 1594 if common_nodes:
1588 1595 raise error.Abort(_("histedit in progress, can't strip %s")
1589 1596 % ', '.join(node.short(x) for x in common_nodes))
1590 1597 return orig(ui, repo, nodelist, *args, **kwargs)
1591 1598
1592 1599 extensions.wrapfunction(repair, 'strip', stripwrapper)
1593 1600
1594 1601 def summaryhook(ui, repo):
1595 1602 if not os.path.exists(repo.vfs.join('histedit-state')):
1596 1603 return
1597 1604 state = histeditstate(repo)
1598 1605 state.read()
1599 1606 if state.actions:
1600 1607 # i18n: column positioning for "hg summary"
1601 1608 ui.write(_('hist: %s (histedit --continue)\n') %
1602 1609 (ui.label(_('%d remaining'), 'histedit.remaining') %
1603 1610 len(state.actions)))
1604 1611
1605 1612 def extsetup(ui):
1606 1613 cmdutil.summaryhooks.add('histedit', summaryhook)
1607 1614 cmdutil.unfinishedstates.append(
1608 1615 ['histedit-state', False, True, _('histedit in progress'),
1609 1616 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
1610 1617 cmdutil.afterresolvedstates.append(
1611 1618 ['histedit-state', _('hg histedit --continue')])
1612 1619 if ui.configbool("experimental", "histeditng"):
1613 1620 globals()['base'] = action(['base', 'b'],
1614 1621 _('checkout changeset and apply further changesets from there')
1615 1622 )(base)
General Comments 0
You need to be logged in to leave comments. Login now