##// END OF EJS Templates
histedit: add user input to warning message on editing tagged commits...
Navaneeth Suresh -
r41186:7b7e081f default
parent child Browse files
Show More
@@ -1,2246 +1,2246
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 # b, base = checkout changeset and apply further changesets from there
43 43 #
44 44
45 45 In this file, lines beginning with ``#`` are ignored. You must specify a rule
46 46 for each revision in your history. For example, if you had meant to add gamma
47 47 before beta, and then wanted to add delta in the same revision as beta, you
48 48 would reorganize the file to look like this::
49 49
50 50 pick 030b686bedc4 Add gamma
51 51 pick c561b4e977df Add beta
52 52 fold 7c2fd3b9020c Add delta
53 53
54 54 # Edit history between c561b4e977df and 7c2fd3b9020c
55 55 #
56 56 # Commits are listed from least to most recent
57 57 #
58 58 # Commands:
59 59 # p, pick = use commit
60 60 # e, edit = use commit, but stop for amending
61 61 # f, fold = use commit, but combine it with the one above
62 62 # r, roll = like fold, but discard this commit's description and date
63 63 # d, drop = remove commit from history
64 64 # m, mess = edit commit message without changing commit content
65 65 # b, base = checkout changeset and apply further changesets from there
66 66 #
67 67
68 68 At which point you close the editor and ``histedit`` starts working. When you
69 69 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
70 70 those revisions together, offering you a chance to clean up the commit message::
71 71
72 72 Add beta
73 73 ***
74 74 Add delta
75 75
76 76 Edit the commit message to your liking, then close the editor. The date used
77 77 for the commit will be the later of the two commits' dates. For this example,
78 78 let's assume that the commit message was changed to ``Add beta and delta.``
79 79 After histedit has run and had a chance to remove any old or temporary
80 80 revisions it needed, the history looks like this::
81 81
82 82 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
83 83 | Add beta and delta.
84 84 |
85 85 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
86 86 | Add gamma
87 87 |
88 88 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
89 89 Add alpha
90 90
91 91 Note that ``histedit`` does *not* remove any revisions (even its own temporary
92 92 ones) until after it has completed all the editing operations, so it will
93 93 probably perform several strip operations when it's done. For the above example,
94 94 it had to run strip twice. Strip can be slow depending on a variety of factors,
95 95 so you might need to be a little patient. You can choose to keep the original
96 96 revisions by passing the ``--keep`` flag.
97 97
98 98 The ``edit`` operation will drop you back to a command prompt,
99 99 allowing you to edit files freely, or even use ``hg record`` to commit
100 100 some changes as a separate commit. When you're done, any remaining
101 101 uncommitted changes will be committed as well. When done, run ``hg
102 102 histedit --continue`` to finish this step. If there are uncommitted
103 103 changes, you'll be prompted for a new commit message, but the default
104 104 commit message will be the original message for the ``edit`` ed
105 105 revision, and the date of the original commit will be preserved.
106 106
107 107 The ``message`` operation will give you a chance to revise a commit
108 108 message without changing the contents. It's a shortcut for doing
109 109 ``edit`` immediately followed by `hg histedit --continue``.
110 110
111 111 If ``histedit`` encounters a conflict when moving a revision (while
112 112 handling ``pick`` or ``fold``), it'll stop in a similar manner to
113 113 ``edit`` with the difference that it won't prompt you for a commit
114 114 message when done. If you decide at this point that you don't like how
115 115 much work it will be to rearrange history, or that you made a mistake,
116 116 you can use ``hg histedit --abort`` to abandon the new changes you
117 117 have made and return to the state before you attempted to edit your
118 118 history.
119 119
120 120 If we clone the histedit-ed example repository above and add four more
121 121 changes, such that we have the following history::
122 122
123 123 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
124 124 | Add theta
125 125 |
126 126 o 5 140988835471 2009-04-27 18:04 -0500 stefan
127 127 | Add eta
128 128 |
129 129 o 4 122930637314 2009-04-27 18:04 -0500 stefan
130 130 | Add zeta
131 131 |
132 132 o 3 836302820282 2009-04-27 18:04 -0500 stefan
133 133 | Add epsilon
134 134 |
135 135 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
136 136 | Add beta and delta.
137 137 |
138 138 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
139 139 | Add gamma
140 140 |
141 141 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
142 142 Add alpha
143 143
144 144 If you run ``hg histedit --outgoing`` on the clone then it is the same
145 145 as running ``hg histedit 836302820282``. If you need plan to push to a
146 146 repository that Mercurial does not detect to be related to the source
147 147 repo, you can add a ``--force`` option.
148 148
149 149 Config
150 150 ------
151 151
152 152 Histedit rule lines are truncated to 80 characters by default. You
153 153 can customize this behavior by setting a different length in your
154 154 configuration file::
155 155
156 156 [histedit]
157 157 linelen = 120 # truncate rule lines at 120 characters
158 158
159 159 ``hg histedit`` attempts to automatically choose an appropriate base
160 160 revision to use. To change which base revision is used, define a
161 161 revset in your configuration file::
162 162
163 163 [histedit]
164 164 defaultrev = only(.) & draft()
165 165
166 166 By default each edited revision needs to be present in histedit commands.
167 167 To remove revision you need to use ``drop`` operation. You can configure
168 168 the drop to be implicit for missing commits by adding::
169 169
170 170 [histedit]
171 171 dropmissing = True
172 172
173 173 By default, histedit will close the transaction after each action. For
174 174 performance purposes, you can configure histedit to use a single transaction
175 175 across the entire histedit. WARNING: This setting introduces a significant risk
176 176 of losing the work you've done in a histedit if the histedit aborts
177 177 unexpectedly::
178 178
179 179 [histedit]
180 180 singletransaction = True
181 181
182 182 """
183 183
184 184 from __future__ import absolute_import
185 185
186 186 # chistedit dependencies that are not available everywhere
187 187 try:
188 188 import fcntl
189 189 import termios
190 190 except ImportError:
191 191 fcntl = None
192 192 termios = None
193 193
194 194 import functools
195 195 import os
196 196 import struct
197 import time
198 197
199 198 from mercurial.i18n import _
200 199 from mercurial import (
201 200 bundle2,
202 201 cmdutil,
203 202 context,
204 203 copies,
205 204 destutil,
206 205 discovery,
207 206 error,
208 207 exchange,
209 208 extensions,
210 209 hg,
211 210 lock,
212 211 logcmdutil,
213 212 merge as mergemod,
214 213 mergeutil,
215 214 node,
216 215 obsolete,
217 216 pycompat,
218 217 registrar,
219 218 repair,
220 219 scmutil,
221 220 state as statemod,
222 221 util,
223 222 )
224 223 from mercurial.utils import (
225 224 stringutil,
226 225 )
227 226
228 227 pickle = util.pickle
229 228 release = lock.release
230 229 cmdtable = {}
231 230 command = registrar.command(cmdtable)
232 231
233 232 configtable = {}
234 233 configitem = registrar.configitem(configtable)
235 234 configitem('experimental', 'histedit.autoverb',
236 235 default=False,
237 236 )
238 237 configitem('histedit', 'defaultrev',
239 238 default=None,
240 239 )
241 240 configitem('histedit', 'dropmissing',
242 241 default=False,
243 242 )
244 243 configitem('histedit', 'linelen',
245 244 default=80,
246 245 )
247 246 configitem('histedit', 'singletransaction',
248 247 default=False,
249 248 )
250 249 configitem('ui', 'interface.histedit',
251 250 default=None,
252 251 )
253 252
254 253 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
255 254 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
256 255 # be specifying the version(s) of Mercurial they are tested with, or
257 256 # leave the attribute unspecified.
258 257 testedwith = 'ships-with-hg-core'
259 258
260 259 actiontable = {}
261 260 primaryactions = set()
262 261 secondaryactions = set()
263 262 tertiaryactions = set()
264 263 internalactions = set()
265 264
266 265 def geteditcomment(ui, first, last):
267 266 """ construct the editor comment
268 267 The comment includes::
269 268 - an intro
270 269 - sorted primary commands
271 270 - sorted short commands
272 271 - sorted long commands
273 272 - additional hints
274 273
275 274 Commands are only included once.
276 275 """
277 276 intro = _("""Edit history between %s and %s
278 277
279 278 Commits are listed from least to most recent
280 279
281 280 You can reorder changesets by reordering the lines
282 281
283 282 Commands:
284 283 """)
285 284 actions = []
286 285 def addverb(v):
287 286 a = actiontable[v]
288 287 lines = a.message.split("\n")
289 288 if len(a.verbs):
290 289 v = ', '.join(sorted(a.verbs, key=lambda v: len(v)))
291 290 actions.append(" %s = %s" % (v, lines[0]))
292 291 actions.extend([' %s' for l in lines[1:]])
293 292
294 293 for v in (
295 294 sorted(primaryactions) +
296 295 sorted(secondaryactions) +
297 296 sorted(tertiaryactions)
298 297 ):
299 298 addverb(v)
300 299 actions.append('')
301 300
302 301 hints = []
303 302 if ui.configbool('histedit', 'dropmissing'):
304 303 hints.append("Deleting a changeset from the list "
305 304 "will DISCARD it from the edited history!")
306 305
307 306 lines = (intro % (first, last)).split('\n') + actions + hints
308 307
309 308 return ''.join(['# %s\n' % l if l else '#\n' for l in lines])
310 309
311 310 class histeditstate(object):
312 311 def __init__(self, repo, parentctxnode=None, actions=None, keep=None,
313 312 topmost=None, replacements=None, lock=None, wlock=None):
314 313 self.repo = repo
315 314 self.actions = actions
316 315 self.keep = keep
317 316 self.topmost = topmost
318 317 self.parentctxnode = parentctxnode
319 318 self.lock = lock
320 319 self.wlock = wlock
321 320 self.backupfile = None
322 321 self.stateobj = statemod.cmdstate(repo, 'histedit-state')
323 322 if replacements is None:
324 323 self.replacements = []
325 324 else:
326 325 self.replacements = replacements
327 326
328 327 def read(self):
329 328 """Load histedit state from disk and set fields appropriately."""
330 329 if not self.stateobj.exists():
331 330 cmdutil.wrongtooltocontinue(self.repo, _('histedit'))
332 331
333 332 data = self._read()
334 333
335 334 self.parentctxnode = data['parentctxnode']
336 335 actions = parserules(data['rules'], self)
337 336 self.actions = actions
338 337 self.keep = data['keep']
339 338 self.topmost = data['topmost']
340 339 self.replacements = data['replacements']
341 340 self.backupfile = data['backupfile']
342 341
343 342 def _read(self):
344 343 fp = self.repo.vfs.read('histedit-state')
345 344 if fp.startswith('v1\n'):
346 345 data = self._load()
347 346 parentctxnode, rules, keep, topmost, replacements, backupfile = data
348 347 else:
349 348 data = pickle.loads(fp)
350 349 parentctxnode, rules, keep, topmost, replacements = data
351 350 backupfile = None
352 351 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
353 352
354 353 return {'parentctxnode': parentctxnode, "rules": rules, "keep": keep,
355 354 "topmost": topmost, "replacements": replacements,
356 355 "backupfile": backupfile}
357 356
358 357 def write(self, tr=None):
359 358 if tr:
360 359 tr.addfilegenerator('histedit-state', ('histedit-state',),
361 360 self._write, location='plain')
362 361 else:
363 362 with self.repo.vfs("histedit-state", "w") as f:
364 363 self._write(f)
365 364
366 365 def _write(self, fp):
367 366 fp.write('v1\n')
368 367 fp.write('%s\n' % node.hex(self.parentctxnode))
369 368 fp.write('%s\n' % node.hex(self.topmost))
370 369 fp.write('%s\n' % ('True' if self.keep else 'False'))
371 370 fp.write('%d\n' % len(self.actions))
372 371 for action in self.actions:
373 372 fp.write('%s\n' % action.tostate())
374 373 fp.write('%d\n' % len(self.replacements))
375 374 for replacement in self.replacements:
376 375 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
377 376 for r in replacement[1])))
378 377 backupfile = self.backupfile
379 378 if not backupfile:
380 379 backupfile = ''
381 380 fp.write('%s\n' % backupfile)
382 381
383 382 def _load(self):
384 383 fp = self.repo.vfs('histedit-state', 'r')
385 384 lines = [l[:-1] for l in fp.readlines()]
386 385
387 386 index = 0
388 387 lines[index] # version number
389 388 index += 1
390 389
391 390 parentctxnode = node.bin(lines[index])
392 391 index += 1
393 392
394 393 topmost = node.bin(lines[index])
395 394 index += 1
396 395
397 396 keep = lines[index] == 'True'
398 397 index += 1
399 398
400 399 # Rules
401 400 rules = []
402 401 rulelen = int(lines[index])
403 402 index += 1
404 403 for i in pycompat.xrange(rulelen):
405 404 ruleaction = lines[index]
406 405 index += 1
407 406 rule = lines[index]
408 407 index += 1
409 408 rules.append((ruleaction, rule))
410 409
411 410 # Replacements
412 411 replacements = []
413 412 replacementlen = int(lines[index])
414 413 index += 1
415 414 for i in pycompat.xrange(replacementlen):
416 415 replacement = lines[index]
417 416 original = node.bin(replacement[:40])
418 417 succ = [node.bin(replacement[i:i + 40]) for i in
419 418 range(40, len(replacement), 40)]
420 419 replacements.append((original, succ))
421 420 index += 1
422 421
423 422 backupfile = lines[index]
424 423 index += 1
425 424
426 425 fp.close()
427 426
428 427 return parentctxnode, rules, keep, topmost, replacements, backupfile
429 428
430 429 def clear(self):
431 430 if self.inprogress():
432 431 self.repo.vfs.unlink('histedit-state')
433 432
434 433 def inprogress(self):
435 434 return self.repo.vfs.exists('histedit-state')
436 435
437 436
438 437 class histeditaction(object):
439 438 def __init__(self, state, node):
440 439 self.state = state
441 440 self.repo = state.repo
442 441 self.node = node
443 442
444 443 @classmethod
445 444 def fromrule(cls, state, rule):
446 445 """Parses the given rule, returning an instance of the histeditaction.
447 446 """
448 447 ruleid = rule.strip().split(' ', 1)[0]
449 448 # ruleid can be anything from rev numbers, hashes, "bookmarks" etc
450 449 # Check for validation of rule ids and get the rulehash
451 450 try:
452 451 rev = node.bin(ruleid)
453 452 except TypeError:
454 453 try:
455 454 _ctx = scmutil.revsingle(state.repo, ruleid)
456 455 rulehash = _ctx.hex()
457 456 rev = node.bin(rulehash)
458 457 except error.RepoLookupError:
459 458 raise error.ParseError(_("invalid changeset %s") % ruleid)
460 459 return cls(state, rev)
461 460
462 461 def verify(self, prev, expected, seen):
463 462 """ Verifies semantic correctness of the rule"""
464 463 repo = self.repo
465 464 ha = node.hex(self.node)
466 465 self.node = scmutil.resolvehexnodeidprefix(repo, ha)
467 466 if self.node is None:
468 467 raise error.ParseError(_('unknown changeset %s listed') % ha[:12])
469 468 self._verifynodeconstraints(prev, expected, seen)
470 469
471 470 def _verifynodeconstraints(self, prev, expected, seen):
472 471 # by default command need a node in the edited list
473 472 if self.node not in expected:
474 473 raise error.ParseError(_('%s "%s" changeset was not a candidate')
475 474 % (self.verb, node.short(self.node)),
476 475 hint=_('only use listed changesets'))
477 476 # and only one command per node
478 477 if self.node in seen:
479 478 raise error.ParseError(_('duplicated command for changeset %s') %
480 479 node.short(self.node))
481 480
482 481 def torule(self):
483 482 """build a histedit rule line for an action
484 483
485 484 by default lines are in the form:
486 485 <hash> <rev> <summary>
487 486 """
488 487 ctx = self.repo[self.node]
489 488 summary = _getsummary(ctx)
490 489 line = '%s %s %d %s' % (self.verb, ctx, ctx.rev(), summary)
491 490 # trim to 75 columns by default so it's not stupidly wide in my editor
492 491 # (the 5 more are left for verb)
493 492 maxlen = self.repo.ui.configint('histedit', 'linelen')
494 493 maxlen = max(maxlen, 22) # avoid truncating hash
495 494 return stringutil.ellipsis(line, maxlen)
496 495
497 496 def tostate(self):
498 497 """Print an action in format used by histedit state files
499 498 (the first line is a verb, the remainder is the second)
500 499 """
501 500 return "%s\n%s" % (self.verb, node.hex(self.node))
502 501
503 502 def run(self):
504 503 """Runs the action. The default behavior is simply apply the action's
505 504 rulectx onto the current parentctx."""
506 505 self.applychange()
507 506 self.continuedirty()
508 507 return self.continueclean()
509 508
510 509 def applychange(self):
511 510 """Applies the changes from this action's rulectx onto the current
512 511 parentctx, but does not commit them."""
513 512 repo = self.repo
514 513 rulectx = repo[self.node]
515 514 repo.ui.pushbuffer(error=True, labeled=True)
516 515 hg.update(repo, self.state.parentctxnode, quietempty=True)
517 516 stats = applychanges(repo.ui, repo, rulectx, {})
518 517 repo.dirstate.setbranch(rulectx.branch())
519 518 if stats.unresolvedcount:
520 519 buf = repo.ui.popbuffer()
521 520 repo.ui.write(buf)
522 521 raise error.InterventionRequired(
523 522 _('Fix up the change (%s %s)') %
524 523 (self.verb, node.short(self.node)),
525 524 hint=_('hg histedit --continue to resume'))
526 525 else:
527 526 repo.ui.popbuffer()
528 527
529 528 def continuedirty(self):
530 529 """Continues the action when changes have been applied to the working
531 530 copy. The default behavior is to commit the dirty changes."""
532 531 repo = self.repo
533 532 rulectx = repo[self.node]
534 533
535 534 editor = self.commiteditor()
536 535 commit = commitfuncfor(repo, rulectx)
537 536
538 537 commit(text=rulectx.description(), user=rulectx.user(),
539 538 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
540 539
541 540 def commiteditor(self):
542 541 """The editor to be used to edit the commit message."""
543 542 return False
544 543
545 544 def continueclean(self):
546 545 """Continues the action when the working copy is clean. The default
547 546 behavior is to accept the current commit as the new version of the
548 547 rulectx."""
549 548 ctx = self.repo['.']
550 549 if ctx.node() == self.state.parentctxnode:
551 550 self.repo.ui.warn(_('%s: skipping changeset (no changes)\n') %
552 551 node.short(self.node))
553 552 return ctx, [(self.node, tuple())]
554 553 if ctx.node() == self.node:
555 554 # Nothing changed
556 555 return ctx, []
557 556 return ctx, [(self.node, (ctx.node(),))]
558 557
559 558 def commitfuncfor(repo, src):
560 559 """Build a commit function for the replacement of <src>
561 560
562 561 This function ensure we apply the same treatment to all changesets.
563 562
564 563 - Add a 'histedit_source' entry in extra.
565 564
566 565 Note that fold has its own separated logic because its handling is a bit
567 566 different and not easily factored out of the fold method.
568 567 """
569 568 phasemin = src.phase()
570 569 def commitfunc(**kwargs):
571 570 overrides = {('phases', 'new-commit'): phasemin}
572 571 with repo.ui.configoverride(overrides, 'histedit'):
573 572 extra = kwargs.get(r'extra', {}).copy()
574 573 extra['histedit_source'] = src.hex()
575 574 kwargs[r'extra'] = extra
576 575 return repo.commit(**kwargs)
577 576 return commitfunc
578 577
579 578 def applychanges(ui, repo, ctx, opts):
580 579 """Merge changeset from ctx (only) in the current working directory"""
581 580 wcpar = repo.dirstate.parents()[0]
582 581 if ctx.p1().node() == wcpar:
583 582 # edits are "in place" we do not need to make any merge,
584 583 # just applies changes on parent for editing
585 584 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
586 585 stats = mergemod.updateresult(0, 0, 0, 0)
587 586 else:
588 587 try:
589 588 # ui.forcemerge is an internal variable, do not document
590 589 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
591 590 'histedit')
592 591 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
593 592 finally:
594 593 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
595 594 return stats
596 595
597 596 def collapse(repo, firstctx, lastctx, commitopts, skipprompt=False):
598 597 """collapse the set of revisions from first to last as new one.
599 598
600 599 Expected commit options are:
601 600 - message
602 601 - date
603 602 - username
604 603 Commit message is edited in all cases.
605 604
606 605 This function works in memory."""
607 606 ctxs = list(repo.set('%d::%d', firstctx.rev(), lastctx.rev()))
608 607 if not ctxs:
609 608 return None
610 609 for c in ctxs:
611 610 if not c.mutable():
612 611 raise error.ParseError(
613 612 _("cannot fold into public change %s") % node.short(c.node()))
614 613 base = firstctx.parents()[0]
615 614
616 615 # commit a new version of the old changeset, including the update
617 616 # collect all files which might be affected
618 617 files = set()
619 618 for ctx in ctxs:
620 619 files.update(ctx.files())
621 620
622 621 # Recompute copies (avoid recording a -> b -> a)
623 622 copied = copies.pathcopies(base, lastctx)
624 623
625 624 # prune files which were reverted by the updates
626 625 files = [f for f in files if not cmdutil.samefile(f, lastctx, base)]
627 626 # commit version of these files as defined by head
628 627 headmf = lastctx.manifest()
629 628 def filectxfn(repo, ctx, path):
630 629 if path in headmf:
631 630 fctx = lastctx[path]
632 631 flags = fctx.flags()
633 632 mctx = context.memfilectx(repo, ctx,
634 633 fctx.path(), fctx.data(),
635 634 islink='l' in flags,
636 635 isexec='x' in flags,
637 636 copied=copied.get(path))
638 637 return mctx
639 638 return None
640 639
641 640 if commitopts.get('message'):
642 641 message = commitopts['message']
643 642 else:
644 643 message = firstctx.description()
645 644 user = commitopts.get('user')
646 645 date = commitopts.get('date')
647 646 extra = commitopts.get('extra')
648 647
649 648 parents = (firstctx.p1().node(), firstctx.p2().node())
650 649 editor = None
651 650 if not skipprompt:
652 651 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
653 652 new = context.memctx(repo,
654 653 parents=parents,
655 654 text=message,
656 655 files=files,
657 656 filectxfn=filectxfn,
658 657 user=user,
659 658 date=date,
660 659 extra=extra,
661 660 editor=editor)
662 661 return repo.commitctx(new)
663 662
664 663 def _isdirtywc(repo):
665 664 return repo[None].dirty(missing=True)
666 665
667 666 def abortdirty():
668 667 raise error.Abort(_('working copy has pending changes'),
669 668 hint=_('amend, commit, or revert them and run histedit '
670 669 '--continue, or abort with histedit --abort'))
671 670
672 671 def action(verbs, message, priority=False, internal=False):
673 672 def wrap(cls):
674 673 assert not priority or not internal
675 674 verb = verbs[0]
676 675 if priority:
677 676 primaryactions.add(verb)
678 677 elif internal:
679 678 internalactions.add(verb)
680 679 elif len(verbs) > 1:
681 680 secondaryactions.add(verb)
682 681 else:
683 682 tertiaryactions.add(verb)
684 683
685 684 cls.verb = verb
686 685 cls.verbs = verbs
687 686 cls.message = message
688 687 for verb in verbs:
689 688 actiontable[verb] = cls
690 689 return cls
691 690 return wrap
692 691
693 692 @action(['pick', 'p'],
694 693 _('use commit'),
695 694 priority=True)
696 695 class pick(histeditaction):
697 696 def run(self):
698 697 rulectx = self.repo[self.node]
699 698 if rulectx.parents()[0].node() == self.state.parentctxnode:
700 699 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
701 700 return rulectx, []
702 701
703 702 return super(pick, self).run()
704 703
705 704 @action(['edit', 'e'],
706 705 _('use commit, but stop for amending'),
707 706 priority=True)
708 707 class edit(histeditaction):
709 708 def run(self):
710 709 repo = self.repo
711 710 rulectx = repo[self.node]
712 711 hg.update(repo, self.state.parentctxnode, quietempty=True)
713 712 applychanges(repo.ui, repo, rulectx, {})
714 713 raise error.InterventionRequired(
715 714 _('Editing (%s), you may commit or record as needed now.')
716 715 % node.short(self.node),
717 716 hint=_('hg histedit --continue to resume'))
718 717
719 718 def commiteditor(self):
720 719 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
721 720
722 721 @action(['fold', 'f'],
723 722 _('use commit, but combine it with the one above'))
724 723 class fold(histeditaction):
725 724 def verify(self, prev, expected, seen):
726 725 """ Verifies semantic correctness of the fold rule"""
727 726 super(fold, self).verify(prev, expected, seen)
728 727 repo = self.repo
729 728 if not prev:
730 729 c = repo[self.node].parents()[0]
731 730 elif not prev.verb in ('pick', 'base'):
732 731 return
733 732 else:
734 733 c = repo[prev.node]
735 734 if not c.mutable():
736 735 raise error.ParseError(
737 736 _("cannot fold into public change %s") % node.short(c.node()))
738 737
739 738
740 739 def continuedirty(self):
741 740 repo = self.repo
742 741 rulectx = repo[self.node]
743 742
744 743 commit = commitfuncfor(repo, rulectx)
745 744 commit(text='fold-temp-revision %s' % node.short(self.node),
746 745 user=rulectx.user(), date=rulectx.date(),
747 746 extra=rulectx.extra())
748 747
749 748 def continueclean(self):
750 749 repo = self.repo
751 750 ctx = repo['.']
752 751 rulectx = repo[self.node]
753 752 parentctxnode = self.state.parentctxnode
754 753 if ctx.node() == parentctxnode:
755 754 repo.ui.warn(_('%s: empty changeset\n') %
756 755 node.short(self.node))
757 756 return ctx, [(self.node, (parentctxnode,))]
758 757
759 758 parentctx = repo[parentctxnode]
760 759 newcommits = set(c.node() for c in repo.set('(%d::. - %d)',
761 760 parentctx.rev(),
762 761 parentctx.rev()))
763 762 if not newcommits:
764 763 repo.ui.warn(_('%s: cannot fold - working copy is not a '
765 764 'descendant of previous commit %s\n') %
766 765 (node.short(self.node), node.short(parentctxnode)))
767 766 return ctx, [(self.node, (ctx.node(),))]
768 767
769 768 middlecommits = newcommits.copy()
770 769 middlecommits.discard(ctx.node())
771 770
772 771 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
773 772 middlecommits)
774 773
775 774 def skipprompt(self):
776 775 """Returns true if the rule should skip the message editor.
777 776
778 777 For example, 'fold' wants to show an editor, but 'rollup'
779 778 doesn't want to.
780 779 """
781 780 return False
782 781
783 782 def mergedescs(self):
784 783 """Returns true if the rule should merge messages of multiple changes.
785 784
786 785 This exists mainly so that 'rollup' rules can be a subclass of
787 786 'fold'.
788 787 """
789 788 return True
790 789
791 790 def firstdate(self):
792 791 """Returns true if the rule should preserve the date of the first
793 792 change.
794 793
795 794 This exists mainly so that 'rollup' rules can be a subclass of
796 795 'fold'.
797 796 """
798 797 return False
799 798
800 799 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
801 800 parent = ctx.parents()[0].node()
802 801 hg.updaterepo(repo, parent, overwrite=False)
803 802 ### prepare new commit data
804 803 commitopts = {}
805 804 commitopts['user'] = ctx.user()
806 805 # commit message
807 806 if not self.mergedescs():
808 807 newmessage = ctx.description()
809 808 else:
810 809 newmessage = '\n***\n'.join(
811 810 [ctx.description()] +
812 811 [repo[r].description() for r in internalchanges] +
813 812 [oldctx.description()]) + '\n'
814 813 commitopts['message'] = newmessage
815 814 # date
816 815 if self.firstdate():
817 816 commitopts['date'] = ctx.date()
818 817 else:
819 818 commitopts['date'] = max(ctx.date(), oldctx.date())
820 819 extra = ctx.extra().copy()
821 820 # histedit_source
822 821 # note: ctx is likely a temporary commit but that the best we can do
823 822 # here. This is sufficient to solve issue3681 anyway.
824 823 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
825 824 commitopts['extra'] = extra
826 825 phasemin = max(ctx.phase(), oldctx.phase())
827 826 overrides = {('phases', 'new-commit'): phasemin}
828 827 with repo.ui.configoverride(overrides, 'histedit'):
829 828 n = collapse(repo, ctx, repo[newnode], commitopts,
830 829 skipprompt=self.skipprompt())
831 830 if n is None:
832 831 return ctx, []
833 832 hg.updaterepo(repo, n, overwrite=False)
834 833 replacements = [(oldctx.node(), (newnode,)),
835 834 (ctx.node(), (n,)),
836 835 (newnode, (n,)),
837 836 ]
838 837 for ich in internalchanges:
839 838 replacements.append((ich, (n,)))
840 839 return repo[n], replacements
841 840
842 841 @action(['base', 'b'],
843 842 _('checkout changeset and apply further changesets from there'))
844 843 class base(histeditaction):
845 844
846 845 def run(self):
847 846 if self.repo['.'].node() != self.node:
848 847 mergemod.update(self.repo, self.node, branchmerge=False, force=True)
849 848 return self.continueclean()
850 849
851 850 def continuedirty(self):
852 851 abortdirty()
853 852
854 853 def continueclean(self):
855 854 basectx = self.repo['.']
856 855 return basectx, []
857 856
858 857 def _verifynodeconstraints(self, prev, expected, seen):
859 858 # base can only be use with a node not in the edited set
860 859 if self.node in expected:
861 860 msg = _('%s "%s" changeset was an edited list candidate')
862 861 raise error.ParseError(
863 862 msg % (self.verb, node.short(self.node)),
864 863 hint=_('base must only use unlisted changesets'))
865 864
866 865 @action(['_multifold'],
867 866 _(
868 867 """fold subclass used for when multiple folds happen in a row
869 868
870 869 We only want to fire the editor for the folded message once when
871 870 (say) four changes are folded down into a single change. This is
872 871 similar to rollup, but we should preserve both messages so that
873 872 when the last fold operation runs we can show the user all the
874 873 commit messages in their editor.
875 874 """),
876 875 internal=True)
877 876 class _multifold(fold):
878 877 def skipprompt(self):
879 878 return True
880 879
881 880 @action(["roll", "r"],
882 881 _("like fold, but discard this commit's description and date"))
883 882 class rollup(fold):
884 883 def mergedescs(self):
885 884 return False
886 885
887 886 def skipprompt(self):
888 887 return True
889 888
890 889 def firstdate(self):
891 890 return True
892 891
893 892 @action(["drop", "d"],
894 893 _('remove commit from history'))
895 894 class drop(histeditaction):
896 895 def run(self):
897 896 parentctx = self.repo[self.state.parentctxnode]
898 897 return parentctx, [(self.node, tuple())]
899 898
900 899 @action(["mess", "m"],
901 900 _('edit commit message without changing commit content'),
902 901 priority=True)
903 902 class message(histeditaction):
904 903 def commiteditor(self):
905 904 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
906 905
907 906 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
908 907 """utility function to find the first outgoing changeset
909 908
910 909 Used by initialization code"""
911 910 if opts is None:
912 911 opts = {}
913 912 dest = ui.expandpath(remote or 'default-push', remote or 'default')
914 913 dest, branches = hg.parseurl(dest, None)[:2]
915 914 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
916 915
917 916 revs, checkout = hg.addbranchrevs(repo, repo, branches, None)
918 917 other = hg.peer(repo, opts, dest)
919 918
920 919 if revs:
921 920 revs = [repo.lookup(rev) for rev in revs]
922 921
923 922 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
924 923 if not outgoing.missing:
925 924 raise error.Abort(_('no outgoing ancestors'))
926 925 roots = list(repo.revs("roots(%ln)", outgoing.missing))
927 926 if len(roots) > 1:
928 927 msg = _('there are ambiguous outgoing revisions')
929 928 hint = _("see 'hg help histedit' for more detail")
930 929 raise error.Abort(msg, hint=hint)
931 930 return repo[roots[0]].node()
932 931
933 932 # Curses Support
934 933 try:
935 934 import curses
936 935 except ImportError:
937 936 curses = None
938 937
939 938 KEY_LIST = ['pick', 'edit', 'fold', 'drop', 'mess', 'roll']
940 939 ACTION_LABELS = {
941 940 'fold': '^fold',
942 941 'roll': '^roll',
943 942 }
944 943
945 944 COLOR_HELP, COLOR_SELECTED, COLOR_OK, COLOR_WARN = 1, 2, 3, 4
946 945
947 946 E_QUIT, E_HISTEDIT = 1, 2
948 947 E_PAGEDOWN, E_PAGEUP, E_LINEUP, E_LINEDOWN, E_RESIZE = 3, 4, 5, 6, 7
949 948 MODE_INIT, MODE_PATCH, MODE_RULES, MODE_HELP = 0, 1, 2, 3
950 949
951 950 KEYTABLE = {
952 951 'global': {
953 952 'h': 'next-action',
954 953 'KEY_RIGHT': 'next-action',
955 954 'l': 'prev-action',
956 955 'KEY_LEFT': 'prev-action',
957 956 'q': 'quit',
958 957 'c': 'histedit',
959 958 'C': 'histedit',
960 959 'v': 'showpatch',
961 960 '?': 'help',
962 961 },
963 962 MODE_RULES: {
964 963 'd': 'action-drop',
965 964 'e': 'action-edit',
966 965 'f': 'action-fold',
967 966 'm': 'action-mess',
968 967 'p': 'action-pick',
969 968 'r': 'action-roll',
970 969 ' ': 'select',
971 970 'j': 'down',
972 971 'k': 'up',
973 972 'KEY_DOWN': 'down',
974 973 'KEY_UP': 'up',
975 974 'J': 'move-down',
976 975 'K': 'move-up',
977 976 'KEY_NPAGE': 'move-down',
978 977 'KEY_PPAGE': 'move-up',
979 978 '0': 'goto', # Used for 0..9
980 979 },
981 980 MODE_PATCH: {
982 981 ' ': 'page-down',
983 982 'KEY_NPAGE': 'page-down',
984 983 'KEY_PPAGE': 'page-up',
985 984 'j': 'line-down',
986 985 'k': 'line-up',
987 986 'KEY_DOWN': 'line-down',
988 987 'KEY_UP': 'line-up',
989 988 'J': 'down',
990 989 'K': 'up',
991 990 },
992 991 MODE_HELP: {
993 992 },
994 993 }
995 994
996 995 def screen_size():
997 996 return struct.unpack('hh', fcntl.ioctl(1, termios.TIOCGWINSZ, ' '))
998 997
999 998 class histeditrule(object):
1000 999 def __init__(self, ctx, pos, action='pick'):
1001 1000 self.ctx = ctx
1002 1001 self.action = action
1003 1002 self.origpos = pos
1004 1003 self.pos = pos
1005 1004 self.conflicts = []
1006 1005
1007 1006 def __str__(self):
1008 1007 # Some actions ('fold' and 'roll') combine a patch with a previous one.
1009 1008 # Add a marker showing which patch they apply to, and also omit the
1010 1009 # description for 'roll' (since it will get discarded). Example display:
1011 1010 #
1012 1011 # #10 pick 316392:06a16c25c053 add option to skip tests
1013 1012 # #11 ^roll 316393:71313c964cc5
1014 1013 # #12 pick 316394:ab31f3973b0d include mfbt for mozilla-config.h
1015 1014 # #13 ^fold 316395:14ce5803f4c3 fix warnings
1016 1015 #
1017 1016 # The carets point to the changeset being folded into ("roll this
1018 1017 # changeset into the changeset above").
1019 1018 action = ACTION_LABELS.get(self.action, self.action)
1020 1019 h = self.ctx.hex()[0:12]
1021 1020 r = self.ctx.rev()
1022 1021 desc = self.ctx.description().splitlines()[0].strip()
1023 1022 if self.action == 'roll':
1024 1023 desc = ''
1025 1024 return "#{0:<2} {1:<6} {2}:{3} {4}".format(
1026 1025 self.origpos, action, r, h, desc)
1027 1026
1028 1027 def checkconflicts(self, other):
1029 1028 if other.pos > self.pos and other.origpos <= self.origpos:
1030 1029 if set(other.ctx.files()) & set(self.ctx.files()) != set():
1031 1030 self.conflicts.append(other)
1032 1031 return self.conflicts
1033 1032
1034 1033 if other in self.conflicts:
1035 1034 self.conflicts.remove(other)
1036 1035 return self.conflicts
1037 1036
1038 1037 # ============ EVENTS ===============
1039 1038 def movecursor(state, oldpos, newpos):
1040 1039 '''Change the rule/changeset that the cursor is pointing to, regardless of
1041 1040 current mode (you can switch between patches from the view patch window).'''
1042 1041 state['pos'] = newpos
1043 1042
1044 1043 mode, _ = state['mode']
1045 1044 if mode == MODE_RULES:
1046 1045 # Scroll through the list by updating the view for MODE_RULES, so that
1047 1046 # even if we are not currently viewing the rules, switching back will
1048 1047 # result in the cursor's rule being visible.
1049 1048 modestate = state['modes'][MODE_RULES]
1050 1049 if newpos < modestate['line_offset']:
1051 1050 modestate['line_offset'] = newpos
1052 1051 elif newpos > modestate['line_offset'] + state['page_height'] - 1:
1053 1052 modestate['line_offset'] = newpos - state['page_height'] + 1
1054 1053
1055 1054 # Reset the patch view region to the top of the new patch.
1056 1055 state['modes'][MODE_PATCH]['line_offset'] = 0
1057 1056
1058 1057 def changemode(state, mode):
1059 1058 curmode, _ = state['mode']
1060 1059 state['mode'] = (mode, curmode)
1061 1060
1062 1061 def makeselection(state, pos):
1063 1062 state['selected'] = pos
1064 1063
1065 1064 def swap(state, oldpos, newpos):
1066 1065 """Swap two positions and calculate necessary conflicts in
1067 1066 O(|newpos-oldpos|) time"""
1068 1067
1069 1068 rules = state['rules']
1070 1069 assert 0 <= oldpos < len(rules) and 0 <= newpos < len(rules)
1071 1070
1072 1071 rules[oldpos], rules[newpos] = rules[newpos], rules[oldpos]
1073 1072
1074 1073 # TODO: swap should not know about histeditrule's internals
1075 1074 rules[newpos].pos = newpos
1076 1075 rules[oldpos].pos = oldpos
1077 1076
1078 1077 start = min(oldpos, newpos)
1079 1078 end = max(oldpos, newpos)
1080 1079 for r in pycompat.xrange(start, end + 1):
1081 1080 rules[newpos].checkconflicts(rules[r])
1082 1081 rules[oldpos].checkconflicts(rules[r])
1083 1082
1084 1083 if state['selected']:
1085 1084 makeselection(state, newpos)
1086 1085
1087 1086 def changeaction(state, pos, action):
1088 1087 """Change the action state on the given position to the new action"""
1089 1088 rules = state['rules']
1090 1089 assert 0 <= pos < len(rules)
1091 1090 rules[pos].action = action
1092 1091
1093 1092 def cycleaction(state, pos, next=False):
1094 1093 """Changes the action state the next or the previous action from
1095 1094 the action list"""
1096 1095 rules = state['rules']
1097 1096 assert 0 <= pos < len(rules)
1098 1097 current = rules[pos].action
1099 1098
1100 1099 assert current in KEY_LIST
1101 1100
1102 1101 index = KEY_LIST.index(current)
1103 1102 if next:
1104 1103 index += 1
1105 1104 else:
1106 1105 index -= 1
1107 1106 changeaction(state, pos, KEY_LIST[index % len(KEY_LIST)])
1108 1107
1109 1108 def changeview(state, delta, unit):
1110 1109 '''Change the region of whatever is being viewed (a patch or the list of
1111 1110 changesets). 'delta' is an amount (+/- 1) and 'unit' is 'page' or 'line'.'''
1112 1111 mode, _ = state['mode']
1113 1112 if mode != MODE_PATCH:
1114 1113 return
1115 1114 mode_state = state['modes'][mode]
1116 1115 num_lines = len(patchcontents(state))
1117 1116 page_height = state['page_height']
1118 1117 unit = page_height if unit == 'page' else 1
1119 1118 num_pages = 1 + (num_lines - 1) / page_height
1120 1119 max_offset = (num_pages - 1) * page_height
1121 1120 newline = mode_state['line_offset'] + delta * unit
1122 1121 mode_state['line_offset'] = max(0, min(max_offset, newline))
1123 1122
1124 1123 def event(state, ch):
1125 1124 """Change state based on the current character input
1126 1125
1127 1126 This takes the current state and based on the current character input from
1128 1127 the user we change the state.
1129 1128 """
1130 1129 selected = state['selected']
1131 1130 oldpos = state['pos']
1132 1131 rules = state['rules']
1133 1132
1134 1133 if ch in (curses.KEY_RESIZE, "KEY_RESIZE"):
1135 1134 return E_RESIZE
1136 1135
1137 1136 lookup_ch = ch
1138 1137 if '0' <= ch <= '9':
1139 1138 lookup_ch = '0'
1140 1139
1141 1140 curmode, prevmode = state['mode']
1142 1141 action = KEYTABLE[curmode].get(lookup_ch, KEYTABLE['global'].get(lookup_ch))
1143 1142 if action is None:
1144 1143 return
1145 1144 if action in ('down', 'move-down'):
1146 1145 newpos = min(oldpos + 1, len(rules) - 1)
1147 1146 movecursor(state, oldpos, newpos)
1148 1147 if selected is not None or action == 'move-down':
1149 1148 swap(state, oldpos, newpos)
1150 1149 elif action in ('up', 'move-up'):
1151 1150 newpos = max(0, oldpos - 1)
1152 1151 movecursor(state, oldpos, newpos)
1153 1152 if selected is not None or action == 'move-up':
1154 1153 swap(state, oldpos, newpos)
1155 1154 elif action == 'next-action':
1156 1155 cycleaction(state, oldpos, next=True)
1157 1156 elif action == 'prev-action':
1158 1157 cycleaction(state, oldpos, next=False)
1159 1158 elif action == 'select':
1160 1159 selected = oldpos if selected is None else None
1161 1160 makeselection(state, selected)
1162 1161 elif action == 'goto' and int(ch) < len(rules) and len(rules) <= 10:
1163 1162 newrule = next((r for r in rules if r.origpos == int(ch)))
1164 1163 movecursor(state, oldpos, newrule.pos)
1165 1164 if selected is not None:
1166 1165 swap(state, oldpos, newrule.pos)
1167 1166 elif action.startswith('action-'):
1168 1167 changeaction(state, oldpos, action[7:])
1169 1168 elif action == 'showpatch':
1170 1169 changemode(state, MODE_PATCH if curmode != MODE_PATCH else prevmode)
1171 1170 elif action == 'help':
1172 1171 changemode(state, MODE_HELP if curmode != MODE_HELP else prevmode)
1173 1172 elif action == 'quit':
1174 1173 return E_QUIT
1175 1174 elif action == 'histedit':
1176 1175 return E_HISTEDIT
1177 1176 elif action == 'page-down':
1178 1177 return E_PAGEDOWN
1179 1178 elif action == 'page-up':
1180 1179 return E_PAGEUP
1181 1180 elif action == 'line-down':
1182 1181 return E_LINEDOWN
1183 1182 elif action == 'line-up':
1184 1183 return E_LINEUP
1185 1184
1186 1185 def makecommands(rules):
1187 1186 """Returns a list of commands consumable by histedit --commands based on
1188 1187 our list of rules"""
1189 1188 commands = []
1190 1189 for rules in rules:
1191 1190 commands.append("{0} {1}\n".format(rules.action, rules.ctx))
1192 1191 return commands
1193 1192
1194 1193 def addln(win, y, x, line, color=None):
1195 1194 """Add a line to the given window left padding but 100% filled with
1196 1195 whitespace characters, so that the color appears on the whole line"""
1197 1196 maxy, maxx = win.getmaxyx()
1198 1197 length = maxx - 1 - x
1199 1198 line = ("{0:<%d}" % length).format(str(line).strip())[:length]
1200 1199 if y < 0:
1201 1200 y = maxy + y
1202 1201 if x < 0:
1203 1202 x = maxx + x
1204 1203 if color:
1205 1204 win.addstr(y, x, line, color)
1206 1205 else:
1207 1206 win.addstr(y, x, line)
1208 1207
1209 1208 def patchcontents(state):
1210 1209 repo = state['repo']
1211 1210 rule = state['rules'][state['pos']]
1212 1211 displayer = logcmdutil.changesetdisplayer(repo.ui, repo, {
1213 1212 'patch': True, 'verbose': True
1214 1213 }, buffered=True)
1215 1214 displayer.show(rule.ctx)
1216 1215 displayer.close()
1217 1216 return displayer.hunk[rule.ctx.rev()].splitlines()
1218 1217
1219 1218 def _chisteditmain(repo, rules, stdscr):
1220 1219 # initialize color pattern
1221 1220 curses.init_pair(COLOR_HELP, curses.COLOR_WHITE, curses.COLOR_BLUE)
1222 1221 curses.init_pair(COLOR_SELECTED, curses.COLOR_BLACK, curses.COLOR_WHITE)
1223 1222 curses.init_pair(COLOR_WARN, curses.COLOR_BLACK, curses.COLOR_YELLOW)
1224 1223 curses.init_pair(COLOR_OK, curses.COLOR_BLACK, curses.COLOR_GREEN)
1225 1224
1226 1225 # don't display the cursor
1227 1226 try:
1228 1227 curses.curs_set(0)
1229 1228 except curses.error:
1230 1229 pass
1231 1230
1232 1231 def rendercommit(win, state):
1233 1232 """Renders the commit window that shows the log of the current selected
1234 1233 commit"""
1235 1234 pos = state['pos']
1236 1235 rules = state['rules']
1237 1236 rule = rules[pos]
1238 1237
1239 1238 ctx = rule.ctx
1240 1239 win.box()
1241 1240
1242 1241 maxy, maxx = win.getmaxyx()
1243 1242 length = maxx - 3
1244 1243
1245 1244 line = "changeset: {0}:{1:<12}".format(ctx.rev(), ctx)
1246 1245 win.addstr(1, 1, line[:length])
1247 1246
1248 1247 line = "user: {0}".format(stringutil.shortuser(ctx.user()))
1249 1248 win.addstr(2, 1, line[:length])
1250 1249
1251 1250 bms = repo.nodebookmarks(ctx.node())
1252 1251 line = "bookmark: {0}".format(' '.join(bms))
1253 1252 win.addstr(3, 1, line[:length])
1254 1253
1255 1254 line = "files: {0}".format(','.join(ctx.files()))
1256 1255 win.addstr(4, 1, line[:length])
1257 1256
1258 1257 line = "summary: {0}".format(ctx.description().splitlines()[0])
1259 1258 win.addstr(5, 1, line[:length])
1260 1259
1261 1260 conflicts = rule.conflicts
1262 1261 if len(conflicts) > 0:
1263 1262 conflictstr = ','.join(map(lambda r: str(r.ctx), conflicts))
1264 1263 conflictstr = "changed files overlap with {0}".format(conflictstr)
1265 1264 else:
1266 1265 conflictstr = 'no overlap'
1267 1266
1268 1267 win.addstr(6, 1, conflictstr[:length])
1269 1268 win.noutrefresh()
1270 1269
1271 1270 def helplines(mode):
1272 1271 if mode == MODE_PATCH:
1273 1272 help = """\
1274 1273 ?: help, k/up: line up, j/down: line down, v: stop viewing patch
1275 1274 pgup: prev page, space/pgdn: next page, c: commit, q: abort
1276 1275 """
1277 1276 else:
1278 1277 help = """\
1279 1278 ?: help, k/up: move up, j/down: move down, space: select, v: view patch
1280 1279 d: drop, e: edit, f: fold, m: mess, p: pick, r: roll
1281 1280 pgup/K: move patch up, pgdn/J: move patch down, c: commit, q: abort
1282 1281 """
1283 1282 return help.splitlines()
1284 1283
1285 1284 def renderhelp(win, state):
1286 1285 maxy, maxx = win.getmaxyx()
1287 1286 mode, _ = state['mode']
1288 1287 for y, line in enumerate(helplines(mode)):
1289 1288 if y >= maxy:
1290 1289 break
1291 1290 addln(win, y, 0, line, curses.color_pair(COLOR_HELP))
1292 1291 win.noutrefresh()
1293 1292
1294 1293 def renderrules(rulesscr, state):
1295 1294 rules = state['rules']
1296 1295 pos = state['pos']
1297 1296 selected = state['selected']
1298 1297 start = state['modes'][MODE_RULES]['line_offset']
1299 1298
1300 1299 conflicts = [r.ctx for r in rules if r.conflicts]
1301 1300 if len(conflicts) > 0:
1302 1301 line = "potential conflict in %s" % ','.join(map(str, conflicts))
1303 1302 addln(rulesscr, -1, 0, line, curses.color_pair(COLOR_WARN))
1304 1303
1305 1304 for y, rule in enumerate(rules[start:]):
1306 1305 if y >= state['page_height']:
1307 1306 break
1308 1307 if len(rule.conflicts) > 0:
1309 1308 rulesscr.addstr(y, 0, " ", curses.color_pair(COLOR_WARN))
1310 1309 else:
1311 1310 rulesscr.addstr(y, 0, " ", curses.COLOR_BLACK)
1312 1311 if y + start == selected:
1313 1312 addln(rulesscr, y, 2, rule, curses.color_pair(COLOR_SELECTED))
1314 1313 elif y + start == pos:
1315 1314 addln(rulesscr, y, 2, rule, curses.A_BOLD)
1316 1315 else:
1317 1316 addln(rulesscr, y, 2, rule)
1318 1317 rulesscr.noutrefresh()
1319 1318
1320 1319 def renderstring(win, state, output):
1321 1320 maxy, maxx = win.getmaxyx()
1322 1321 length = min(maxy - 1, len(output))
1323 1322 for y in range(0, length):
1324 1323 win.addstr(y, 0, output[y])
1325 1324 win.noutrefresh()
1326 1325
1327 1326 def renderpatch(win, state):
1328 1327 start = state['modes'][MODE_PATCH]['line_offset']
1329 1328 renderstring(win, state, patchcontents(state)[start:])
1330 1329
1331 1330 def layout(mode):
1332 1331 maxy, maxx = stdscr.getmaxyx()
1333 1332 helplen = len(helplines(mode))
1334 1333 return {
1335 1334 'commit': (8, maxx),
1336 1335 'help': (helplen, maxx),
1337 1336 'main': (maxy - helplen - 8, maxx),
1338 1337 }
1339 1338
1340 1339 def drawvertwin(size, y, x):
1341 1340 win = curses.newwin(size[0], size[1], y, x)
1342 1341 y += size[0]
1343 1342 return win, y, x
1344 1343
1345 1344 state = {
1346 1345 'pos': 0,
1347 1346 'rules': rules,
1348 1347 'selected': None,
1349 1348 'mode': (MODE_INIT, MODE_INIT),
1350 1349 'page_height': None,
1351 1350 'modes': {
1352 1351 MODE_RULES: {
1353 1352 'line_offset': 0,
1354 1353 },
1355 1354 MODE_PATCH: {
1356 1355 'line_offset': 0,
1357 1356 }
1358 1357 },
1359 1358 'repo': repo,
1360 1359 }
1361 1360
1362 1361 # eventloop
1363 1362 ch = None
1364 1363 stdscr.clear()
1365 1364 stdscr.refresh()
1366 1365 while True:
1367 1366 try:
1368 1367 oldmode, _ = state['mode']
1369 1368 if oldmode == MODE_INIT:
1370 1369 changemode(state, MODE_RULES)
1371 1370 e = event(state, ch)
1372 1371
1373 1372 if e == E_QUIT:
1374 1373 return False
1375 1374 if e == E_HISTEDIT:
1376 1375 return state['rules']
1377 1376 else:
1378 1377 if e == E_RESIZE:
1379 1378 size = screen_size()
1380 1379 if size != stdscr.getmaxyx():
1381 1380 curses.resizeterm(*size)
1382 1381
1383 1382 curmode, _ = state['mode']
1384 1383 sizes = layout(curmode)
1385 1384 if curmode != oldmode:
1386 1385 state['page_height'] = sizes['main'][0]
1387 1386 # Adjust the view to fit the current screen size.
1388 1387 movecursor(state, state['pos'], state['pos'])
1389 1388
1390 1389 # Pack the windows against the top, each pane spread across the
1391 1390 # full width of the screen.
1392 1391 y, x = (0, 0)
1393 1392 helpwin, y, x = drawvertwin(sizes['help'], y, x)
1394 1393 mainwin, y, x = drawvertwin(sizes['main'], y, x)
1395 1394 commitwin, y, x = drawvertwin(sizes['commit'], y, x)
1396 1395
1397 1396 if e in (E_PAGEDOWN, E_PAGEUP, E_LINEDOWN, E_LINEUP):
1398 1397 if e == E_PAGEDOWN:
1399 1398 changeview(state, +1, 'page')
1400 1399 elif e == E_PAGEUP:
1401 1400 changeview(state, -1, 'page')
1402 1401 elif e == E_LINEDOWN:
1403 1402 changeview(state, +1, 'line')
1404 1403 elif e == E_LINEUP:
1405 1404 changeview(state, -1, 'line')
1406 1405
1407 1406 # start rendering
1408 1407 commitwin.erase()
1409 1408 helpwin.erase()
1410 1409 mainwin.erase()
1411 1410 if curmode == MODE_PATCH:
1412 1411 renderpatch(mainwin, state)
1413 1412 elif curmode == MODE_HELP:
1414 1413 renderstring(mainwin, state, __doc__.strip().splitlines())
1415 1414 else:
1416 1415 renderrules(mainwin, state)
1417 1416 rendercommit(commitwin, state)
1418 1417 renderhelp(helpwin, state)
1419 1418 curses.doupdate()
1420 1419 # done rendering
1421 1420 ch = stdscr.getkey()
1422 1421 except curses.error:
1423 1422 pass
1424 1423
1425 1424 def _chistedit(ui, repo, *freeargs, **opts):
1426 1425 """interactively edit changeset history via a curses interface
1427 1426
1428 1427 Provides a ncurses interface to histedit. Press ? in chistedit mode
1429 1428 to see an extensive help. Requires python-curses to be installed."""
1430 1429
1431 1430 if curses is None:
1432 1431 raise error.Abort(_("Python curses library required"))
1433 1432
1434 1433 # disable color
1435 1434 ui._colormode = None
1436 1435
1437 1436 try:
1438 1437 keep = opts.get('keep')
1439 1438 revs = opts.get('rev', [])[:]
1440 1439 cmdutil.checkunfinished(repo)
1441 1440 cmdutil.bailifchanged(repo)
1442 1441
1443 1442 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1444 1443 raise error.Abort(_('history edit already in progress, try '
1445 1444 '--continue or --abort'))
1446 1445 revs.extend(freeargs)
1447 1446 if not revs:
1448 1447 defaultrev = destutil.desthistedit(ui, repo)
1449 1448 if defaultrev is not None:
1450 1449 revs.append(defaultrev)
1451 1450 if len(revs) != 1:
1452 1451 raise error.Abort(
1453 1452 _('histedit requires exactly one ancestor revision'))
1454 1453
1455 1454 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1456 1455 if len(rr) != 1:
1457 1456 raise error.Abort(_('The specified revisions must have '
1458 1457 'exactly one common root'))
1459 1458 root = rr[0].node()
1460 1459
1461 1460 topmost, empty = repo.dirstate.parents()
1462 1461 revs = between(repo, root, topmost, keep)
1463 1462 if not revs:
1464 1463 raise error.Abort(_('%s is not an ancestor of working directory') %
1465 1464 node.short(root))
1466 1465
1467 1466 ctxs = []
1468 1467 for i, r in enumerate(revs):
1469 1468 ctxs.append(histeditrule(repo[r], i))
1470 1469 rc = curses.wrapper(functools.partial(_chisteditmain, repo, ctxs))
1471 1470 curses.echo()
1472 1471 curses.endwin()
1473 1472 if rc is False:
1474 1473 ui.write(_("chistedit aborted\n"))
1475 1474 return 0
1476 1475 if type(rc) is list:
1477 1476 ui.status(_("running histedit\n"))
1478 1477 rules = makecommands(rc)
1479 1478 filename = repo.vfs.join('chistedit')
1480 1479 with open(filename, 'w+') as fp:
1481 1480 for r in rules:
1482 1481 fp.write(r)
1483 1482 opts['commands'] = filename
1484 1483 return _texthistedit(ui, repo, *freeargs, **opts)
1485 1484 except KeyboardInterrupt:
1486 1485 pass
1487 1486 return -1
1488 1487
1489 1488 @command('histedit',
1490 1489 [('', 'commands', '',
1491 1490 _('read history edits from the specified file'), _('FILE')),
1492 1491 ('c', 'continue', False, _('continue an edit already in progress')),
1493 1492 ('', 'edit-plan', False, _('edit remaining actions list')),
1494 1493 ('k', 'keep', False,
1495 1494 _("don't strip old nodes after edit is complete")),
1496 1495 ('', 'abort', False, _('abort an edit in progress')),
1497 1496 ('o', 'outgoing', False, _('changesets not found in destination')),
1498 1497 ('f', 'force', False,
1499 1498 _('force outgoing even for unrelated repositories')),
1500 1499 ('r', 'rev', [], _('first revision to be edited'), _('REV'))] +
1501 1500 cmdutil.formatteropts,
1502 1501 _("[OPTIONS] ([ANCESTOR] | --outgoing [URL])"),
1503 1502 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT)
1504 1503 def histedit(ui, repo, *freeargs, **opts):
1505 1504 """interactively edit changeset history
1506 1505
1507 1506 This command lets you edit a linear series of changesets (up to
1508 1507 and including the working directory, which should be clean).
1509 1508 You can:
1510 1509
1511 1510 - `pick` to [re]order a changeset
1512 1511
1513 1512 - `drop` to omit changeset
1514 1513
1515 1514 - `mess` to reword the changeset commit message
1516 1515
1517 1516 - `fold` to combine it with the preceding changeset (using the later date)
1518 1517
1519 1518 - `roll` like fold, but discarding this commit's description and date
1520 1519
1521 1520 - `edit` to edit this changeset (preserving date)
1522 1521
1523 1522 - `base` to checkout changeset and apply further changesets from there
1524 1523
1525 1524 There are a number of ways to select the root changeset:
1526 1525
1527 1526 - Specify ANCESTOR directly
1528 1527
1529 1528 - Use --outgoing -- it will be the first linear changeset not
1530 1529 included in destination. (See :hg:`help config.paths.default-push`)
1531 1530
1532 1531 - Otherwise, the value from the "histedit.defaultrev" config option
1533 1532 is used as a revset to select the base revision when ANCESTOR is not
1534 1533 specified. The first revision returned by the revset is used. By
1535 1534 default, this selects the editable history that is unique to the
1536 1535 ancestry of the working directory.
1537 1536
1538 1537 .. container:: verbose
1539 1538
1540 1539 If you use --outgoing, this command will abort if there are ambiguous
1541 1540 outgoing revisions. For example, if there are multiple branches
1542 1541 containing outgoing revisions.
1543 1542
1544 1543 Use "min(outgoing() and ::.)" or similar revset specification
1545 1544 instead of --outgoing to specify edit target revision exactly in
1546 1545 such ambiguous situation. See :hg:`help revsets` for detail about
1547 1546 selecting revisions.
1548 1547
1549 1548 .. container:: verbose
1550 1549
1551 1550 Examples:
1552 1551
1553 1552 - A number of changes have been made.
1554 1553 Revision 3 is no longer needed.
1555 1554
1556 1555 Start history editing from revision 3::
1557 1556
1558 1557 hg histedit -r 3
1559 1558
1560 1559 An editor opens, containing the list of revisions,
1561 1560 with specific actions specified::
1562 1561
1563 1562 pick 5339bf82f0ca 3 Zworgle the foobar
1564 1563 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1565 1564 pick 0a9639fcda9d 5 Morgify the cromulancy
1566 1565
1567 1566 Additional information about the possible actions
1568 1567 to take appears below the list of revisions.
1569 1568
1570 1569 To remove revision 3 from the history,
1571 1570 its action (at the beginning of the relevant line)
1572 1571 is changed to 'drop'::
1573 1572
1574 1573 drop 5339bf82f0ca 3 Zworgle the foobar
1575 1574 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1576 1575 pick 0a9639fcda9d 5 Morgify the cromulancy
1577 1576
1578 1577 - A number of changes have been made.
1579 1578 Revision 2 and 4 need to be swapped.
1580 1579
1581 1580 Start history editing from revision 2::
1582 1581
1583 1582 hg histedit -r 2
1584 1583
1585 1584 An editor opens, containing the list of revisions,
1586 1585 with specific actions specified::
1587 1586
1588 1587 pick 252a1af424ad 2 Blorb a morgwazzle
1589 1588 pick 5339bf82f0ca 3 Zworgle the foobar
1590 1589 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1591 1590
1592 1591 To swap revision 2 and 4, its lines are swapped
1593 1592 in the editor::
1594 1593
1595 1594 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1596 1595 pick 5339bf82f0ca 3 Zworgle the foobar
1597 1596 pick 252a1af424ad 2 Blorb a morgwazzle
1598 1597
1599 1598 Returns 0 on success, 1 if user intervention is required (not only
1600 1599 for intentional "edit" command, but also for resolving unexpected
1601 1600 conflicts).
1602 1601 """
1603 1602 if ui.interface('histedit') == 'curses':
1604 1603 return _chistedit(ui, repo, *freeargs, **opts)
1605 1604 return _texthistedit(ui, repo, *freeargs, **opts)
1606 1605
1607 1606 def _texthistedit(ui, repo, *freeargs, **opts):
1608 1607 state = histeditstate(repo)
1609 1608 try:
1610 1609 state.wlock = repo.wlock()
1611 1610 state.lock = repo.lock()
1612 1611 _histedit(ui, repo, state, *freeargs, **opts)
1613 1612 finally:
1614 1613 release(state.lock, state.wlock)
1615 1614
1616 1615 goalcontinue = 'continue'
1617 1616 goalabort = 'abort'
1618 1617 goaleditplan = 'edit-plan'
1619 1618 goalnew = 'new'
1620 1619
1621 1620 def _getgoal(opts):
1622 1621 if opts.get('continue'):
1623 1622 return goalcontinue
1624 1623 if opts.get('abort'):
1625 1624 return goalabort
1626 1625 if opts.get('edit_plan'):
1627 1626 return goaleditplan
1628 1627 return goalnew
1629 1628
1630 1629 def _readfile(ui, path):
1631 1630 if path == '-':
1632 1631 with ui.timeblockedsection('histedit'):
1633 1632 return ui.fin.read()
1634 1633 else:
1635 1634 with open(path, 'rb') as f:
1636 1635 return f.read()
1637 1636
1638 1637 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1639 1638 # TODO only abort if we try to histedit mq patches, not just
1640 1639 # blanket if mq patches are applied somewhere
1641 1640 mq = getattr(repo, 'mq', None)
1642 1641 if mq and mq.applied:
1643 1642 raise error.Abort(_('source has mq patches applied'))
1644 1643
1645 1644 # basic argument incompatibility processing
1646 1645 outg = opts.get('outgoing')
1647 1646 editplan = opts.get('edit_plan')
1648 1647 abort = opts.get('abort')
1649 1648 force = opts.get('force')
1650 1649 if force and not outg:
1651 1650 raise error.Abort(_('--force only allowed with --outgoing'))
1652 1651 if goal == 'continue':
1653 1652 if any((outg, abort, revs, freeargs, rules, editplan)):
1654 1653 raise error.Abort(_('no arguments allowed with --continue'))
1655 1654 elif goal == 'abort':
1656 1655 if any((outg, revs, freeargs, rules, editplan)):
1657 1656 raise error.Abort(_('no arguments allowed with --abort'))
1658 1657 elif goal == 'edit-plan':
1659 1658 if any((outg, revs, freeargs)):
1660 1659 raise error.Abort(_('only --commands argument allowed with '
1661 1660 '--edit-plan'))
1662 1661 else:
1663 1662 if state.inprogress():
1664 1663 raise error.Abort(_('history edit already in progress, try '
1665 1664 '--continue or --abort'))
1666 1665 if outg:
1667 1666 if revs:
1668 1667 raise error.Abort(_('no revisions allowed with --outgoing'))
1669 1668 if len(freeargs) > 1:
1670 1669 raise error.Abort(
1671 1670 _('only one repo argument allowed with --outgoing'))
1672 1671 else:
1673 1672 revs.extend(freeargs)
1674 1673 if len(revs) == 0:
1675 1674 defaultrev = destutil.desthistedit(ui, repo)
1676 1675 if defaultrev is not None:
1677 1676 revs.append(defaultrev)
1678 1677
1679 1678 if len(revs) != 1:
1680 1679 raise error.Abort(
1681 1680 _('histedit requires exactly one ancestor revision'))
1682 1681
1683 1682 def _histedit(ui, repo, state, *freeargs, **opts):
1684 1683 opts = pycompat.byteskwargs(opts)
1685 1684 fm = ui.formatter('histedit', opts)
1686 1685 fm.startitem()
1687 1686 goal = _getgoal(opts)
1688 1687 revs = opts.get('rev', [])
1689 1688 # experimental config: ui.history-editing-backup
1690 1689 nobackup = not ui.configbool('ui', 'history-editing-backup')
1691 1690 rules = opts.get('commands', '')
1692 1691 state.keep = opts.get('keep', False)
1693 1692
1694 1693 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1695 1694
1696 1695 hastags = False
1697 1696 if revs:
1698 1697 revs = scmutil.revrange(repo, revs)
1699 1698 ctxs = [repo[rev] for rev in revs]
1700 1699 for ctx in ctxs:
1701 1700 tags = [tag for tag in ctx.tags() if tag != 'tip']
1702 1701 if not hastags:
1703 1702 hastags = len(tags)
1704 1703 if hastags:
1705 ui.warn(_('warning: tags associated with the given changeset '
1706 'will be lost after histedit \n'))
1707 time.sleep(1)
1704 if ui.promptchoice(_('warning: tags associated with the given'
1705 ' changeset will be lost after histedit. \n'
1706 'do you want to continue (yN)? $$ &Yes $$ &No'), default=1):
1707 raise error.Abort(_('histedit cancelled\n'))
1708 1708 # rebuild state
1709 1709 if goal == goalcontinue:
1710 1710 state.read()
1711 1711 state = bootstrapcontinue(ui, state, opts)
1712 1712 elif goal == goaleditplan:
1713 1713 _edithisteditplan(ui, repo, state, rules)
1714 1714 return
1715 1715 elif goal == goalabort:
1716 1716 _aborthistedit(ui, repo, state, nobackup=nobackup)
1717 1717 return
1718 1718 else:
1719 1719 # goal == goalnew
1720 1720 _newhistedit(ui, repo, state, revs, freeargs, opts)
1721 1721
1722 1722 _continuehistedit(ui, repo, state)
1723 1723 _finishhistedit(ui, repo, state, fm)
1724 1724 fm.end()
1725 1725
1726 1726 def _continuehistedit(ui, repo, state):
1727 1727 """This function runs after either:
1728 1728 - bootstrapcontinue (if the goal is 'continue')
1729 1729 - _newhistedit (if the goal is 'new')
1730 1730 """
1731 1731 # preprocess rules so that we can hide inner folds from the user
1732 1732 # and only show one editor
1733 1733 actions = state.actions[:]
1734 1734 for idx, (action, nextact) in enumerate(
1735 1735 zip(actions, actions[1:] + [None])):
1736 1736 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1737 1737 state.actions[idx].__class__ = _multifold
1738 1738
1739 1739 # Force an initial state file write, so the user can run --abort/continue
1740 1740 # even if there's an exception before the first transaction serialize.
1741 1741 state.write()
1742 1742
1743 1743 tr = None
1744 1744 # Don't use singletransaction by default since it rolls the entire
1745 1745 # transaction back if an unexpected exception happens (like a
1746 1746 # pretxncommit hook throws, or the user aborts the commit msg editor).
1747 1747 if ui.configbool("histedit", "singletransaction"):
1748 1748 # Don't use a 'with' for the transaction, since actions may close
1749 1749 # and reopen a transaction. For example, if the action executes an
1750 1750 # external process it may choose to commit the transaction first.
1751 1751 tr = repo.transaction('histedit')
1752 1752 progress = ui.makeprogress(_("editing"), unit=_('changes'),
1753 1753 total=len(state.actions))
1754 1754 with progress, util.acceptintervention(tr):
1755 1755 while state.actions:
1756 1756 state.write(tr=tr)
1757 1757 actobj = state.actions[0]
1758 1758 progress.increment(item=actobj.torule())
1759 1759 ui.debug('histedit: processing %s %s\n' % (actobj.verb,\
1760 1760 actobj.torule()))
1761 1761 parentctx, replacement_ = actobj.run()
1762 1762 state.parentctxnode = parentctx.node()
1763 1763 state.replacements.extend(replacement_)
1764 1764 state.actions.pop(0)
1765 1765
1766 1766 state.write()
1767 1767
1768 1768 def _finishhistedit(ui, repo, state, fm):
1769 1769 """This action runs when histedit is finishing its session"""
1770 1770 hg.updaterepo(repo, state.parentctxnode, overwrite=False)
1771 1771
1772 1772 mapping, tmpnodes, created, ntm = processreplacement(state)
1773 1773 if mapping:
1774 1774 for prec, succs in mapping.iteritems():
1775 1775 if not succs:
1776 1776 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1777 1777 else:
1778 1778 ui.debug('histedit: %s is replaced by %s\n' % (
1779 1779 node.short(prec), node.short(succs[0])))
1780 1780 if len(succs) > 1:
1781 1781 m = 'histedit: %s'
1782 1782 for n in succs[1:]:
1783 1783 ui.debug(m % node.short(n))
1784 1784
1785 1785 if not state.keep:
1786 1786 if mapping:
1787 1787 movetopmostbookmarks(repo, state.topmost, ntm)
1788 1788 # TODO update mq state
1789 1789 else:
1790 1790 mapping = {}
1791 1791
1792 1792 for n in tmpnodes:
1793 1793 if n in repo:
1794 1794 mapping[n] = ()
1795 1795
1796 1796 # remove entries about unknown nodes
1797 1797 nodemap = repo.unfiltered().changelog.nodemap
1798 1798 mapping = {k: v for k, v in mapping.items()
1799 1799 if k in nodemap and all(n in nodemap for n in v)}
1800 1800 scmutil.cleanupnodes(repo, mapping, 'histedit')
1801 1801 hf = fm.hexfunc
1802 1802 fl = fm.formatlist
1803 1803 fd = fm.formatdict
1804 1804 nodechanges = fd({hf(oldn): fl([hf(n) for n in newn], name='node')
1805 1805 for oldn, newn in mapping.iteritems()},
1806 1806 key="oldnode", value="newnodes")
1807 1807 fm.data(nodechanges=nodechanges)
1808 1808
1809 1809 state.clear()
1810 1810 if os.path.exists(repo.sjoin('undo')):
1811 1811 os.unlink(repo.sjoin('undo'))
1812 1812 if repo.vfs.exists('histedit-last-edit.txt'):
1813 1813 repo.vfs.unlink('histedit-last-edit.txt')
1814 1814
1815 1815 def _aborthistedit(ui, repo, state, nobackup=False):
1816 1816 try:
1817 1817 state.read()
1818 1818 __, leafs, tmpnodes, __ = processreplacement(state)
1819 1819 ui.debug('restore wc to old parent %s\n'
1820 1820 % node.short(state.topmost))
1821 1821
1822 1822 # Recover our old commits if necessary
1823 1823 if not state.topmost in repo and state.backupfile:
1824 1824 backupfile = repo.vfs.join(state.backupfile)
1825 1825 f = hg.openpath(ui, backupfile)
1826 1826 gen = exchange.readbundle(ui, f, backupfile)
1827 1827 with repo.transaction('histedit.abort') as tr:
1828 1828 bundle2.applybundle(repo, gen, tr, source='histedit',
1829 1829 url='bundle:' + backupfile)
1830 1830
1831 1831 os.remove(backupfile)
1832 1832
1833 1833 # check whether we should update away
1834 1834 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1835 1835 state.parentctxnode, leafs | tmpnodes):
1836 1836 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1837 1837 cleanupnode(ui, repo, tmpnodes, nobackup=nobackup)
1838 1838 cleanupnode(ui, repo, leafs, nobackup=nobackup)
1839 1839 except Exception:
1840 1840 if state.inprogress():
1841 1841 ui.warn(_('warning: encountered an exception during histedit '
1842 1842 '--abort; the repository may not have been completely '
1843 1843 'cleaned up\n'))
1844 1844 raise
1845 1845 finally:
1846 1846 state.clear()
1847 1847
1848 1848 def _edithisteditplan(ui, repo, state, rules):
1849 1849 state.read()
1850 1850 if not rules:
1851 1851 comment = geteditcomment(ui,
1852 1852 node.short(state.parentctxnode),
1853 1853 node.short(state.topmost))
1854 1854 rules = ruleeditor(repo, ui, state.actions, comment)
1855 1855 else:
1856 1856 rules = _readfile(ui, rules)
1857 1857 actions = parserules(rules, state)
1858 1858 ctxs = [repo[act.node] \
1859 1859 for act in state.actions if act.node]
1860 1860 warnverifyactions(ui, repo, actions, state, ctxs)
1861 1861 state.actions = actions
1862 1862 state.write()
1863 1863
1864 1864 def _newhistedit(ui, repo, state, revs, freeargs, opts):
1865 1865 outg = opts.get('outgoing')
1866 1866 rules = opts.get('commands', '')
1867 1867 force = opts.get('force')
1868 1868
1869 1869 cmdutil.checkunfinished(repo)
1870 1870 cmdutil.bailifchanged(repo)
1871 1871
1872 1872 topmost, empty = repo.dirstate.parents()
1873 1873 if outg:
1874 1874 if freeargs:
1875 1875 remote = freeargs[0]
1876 1876 else:
1877 1877 remote = None
1878 1878 root = findoutgoing(ui, repo, remote, force, opts)
1879 1879 else:
1880 1880 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1881 1881 if len(rr) != 1:
1882 1882 raise error.Abort(_('The specified revisions must have '
1883 1883 'exactly one common root'))
1884 1884 root = rr[0].node()
1885 1885
1886 1886 revs = between(repo, root, topmost, state.keep)
1887 1887 if not revs:
1888 1888 raise error.Abort(_('%s is not an ancestor of working directory') %
1889 1889 node.short(root))
1890 1890
1891 1891 ctxs = [repo[r] for r in revs]
1892 1892 if not rules:
1893 1893 comment = geteditcomment(ui, node.short(root), node.short(topmost))
1894 1894 actions = [pick(state, r) for r in revs]
1895 1895 rules = ruleeditor(repo, ui, actions, comment)
1896 1896 else:
1897 1897 rules = _readfile(ui, rules)
1898 1898 actions = parserules(rules, state)
1899 1899 warnverifyactions(ui, repo, actions, state, ctxs)
1900 1900
1901 1901 parentctxnode = repo[root].parents()[0].node()
1902 1902
1903 1903 state.parentctxnode = parentctxnode
1904 1904 state.actions = actions
1905 1905 state.topmost = topmost
1906 1906 state.replacements = []
1907 1907
1908 1908 ui.log("histedit", "%d actions to histedit", len(actions),
1909 1909 histedit_num_actions=len(actions))
1910 1910
1911 1911 # Create a backup so we can always abort completely.
1912 1912 backupfile = None
1913 1913 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1914 1914 backupfile = repair.backupbundle(repo, [parentctxnode],
1915 1915 [topmost], root, 'histedit')
1916 1916 state.backupfile = backupfile
1917 1917
1918 1918 def _getsummary(ctx):
1919 1919 # a common pattern is to extract the summary but default to the empty
1920 1920 # string
1921 1921 summary = ctx.description() or ''
1922 1922 if summary:
1923 1923 summary = summary.splitlines()[0]
1924 1924 return summary
1925 1925
1926 1926 def bootstrapcontinue(ui, state, opts):
1927 1927 repo = state.repo
1928 1928
1929 1929 ms = mergemod.mergestate.read(repo)
1930 1930 mergeutil.checkunresolved(ms)
1931 1931
1932 1932 if state.actions:
1933 1933 actobj = state.actions.pop(0)
1934 1934
1935 1935 if _isdirtywc(repo):
1936 1936 actobj.continuedirty()
1937 1937 if _isdirtywc(repo):
1938 1938 abortdirty()
1939 1939
1940 1940 parentctx, replacements = actobj.continueclean()
1941 1941
1942 1942 state.parentctxnode = parentctx.node()
1943 1943 state.replacements.extend(replacements)
1944 1944
1945 1945 return state
1946 1946
1947 1947 def between(repo, old, new, keep):
1948 1948 """select and validate the set of revision to edit
1949 1949
1950 1950 When keep is false, the specified set can't have children."""
1951 1951 revs = repo.revs('%n::%n', old, new)
1952 1952 if revs and not keep:
1953 1953 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1954 1954 repo.revs('(%ld::) - (%ld)', revs, revs)):
1955 1955 raise error.Abort(_('can only histedit a changeset together '
1956 1956 'with all its descendants'))
1957 1957 if repo.revs('(%ld) and merge()', revs):
1958 1958 raise error.Abort(_('cannot edit history that contains merges'))
1959 1959 root = repo[revs.first()] # list is already sorted by repo.revs()
1960 1960 if not root.mutable():
1961 1961 raise error.Abort(_('cannot edit public changeset: %s') % root,
1962 1962 hint=_("see 'hg help phases' for details"))
1963 1963 return pycompat.maplist(repo.changelog.node, revs)
1964 1964
1965 1965 def ruleeditor(repo, ui, actions, editcomment=""):
1966 1966 """open an editor to edit rules
1967 1967
1968 1968 rules are in the format [ [act, ctx], ...] like in state.rules
1969 1969 """
1970 1970 if repo.ui.configbool("experimental", "histedit.autoverb"):
1971 1971 newact = util.sortdict()
1972 1972 for act in actions:
1973 1973 ctx = repo[act.node]
1974 1974 summary = _getsummary(ctx)
1975 1975 fword = summary.split(' ', 1)[0].lower()
1976 1976 added = False
1977 1977
1978 1978 # if it doesn't end with the special character '!' just skip this
1979 1979 if fword.endswith('!'):
1980 1980 fword = fword[:-1]
1981 1981 if fword in primaryactions | secondaryactions | tertiaryactions:
1982 1982 act.verb = fword
1983 1983 # get the target summary
1984 1984 tsum = summary[len(fword) + 1:].lstrip()
1985 1985 # safe but slow: reverse iterate over the actions so we
1986 1986 # don't clash on two commits having the same summary
1987 1987 for na, l in reversed(list(newact.iteritems())):
1988 1988 actx = repo[na.node]
1989 1989 asum = _getsummary(actx)
1990 1990 if asum == tsum:
1991 1991 added = True
1992 1992 l.append(act)
1993 1993 break
1994 1994
1995 1995 if not added:
1996 1996 newact[act] = []
1997 1997
1998 1998 # copy over and flatten the new list
1999 1999 actions = []
2000 2000 for na, l in newact.iteritems():
2001 2001 actions.append(na)
2002 2002 actions += l
2003 2003
2004 2004 rules = '\n'.join([act.torule() for act in actions])
2005 2005 rules += '\n\n'
2006 2006 rules += editcomment
2007 2007 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'},
2008 2008 repopath=repo.path, action='histedit')
2009 2009
2010 2010 # Save edit rules in .hg/histedit-last-edit.txt in case
2011 2011 # the user needs to ask for help after something
2012 2012 # surprising happens.
2013 2013 with repo.vfs('histedit-last-edit.txt', 'wb') as f:
2014 2014 f.write(rules)
2015 2015
2016 2016 return rules
2017 2017
2018 2018 def parserules(rules, state):
2019 2019 """Read the histedit rules string and return list of action objects """
2020 2020 rules = [l for l in (r.strip() for r in rules.splitlines())
2021 2021 if l and not l.startswith('#')]
2022 2022 actions = []
2023 2023 for r in rules:
2024 2024 if ' ' not in r:
2025 2025 raise error.ParseError(_('malformed line "%s"') % r)
2026 2026 verb, rest = r.split(' ', 1)
2027 2027
2028 2028 if verb not in actiontable:
2029 2029 raise error.ParseError(_('unknown action "%s"') % verb)
2030 2030
2031 2031 action = actiontable[verb].fromrule(state, rest)
2032 2032 actions.append(action)
2033 2033 return actions
2034 2034
2035 2035 def warnverifyactions(ui, repo, actions, state, ctxs):
2036 2036 try:
2037 2037 verifyactions(actions, state, ctxs)
2038 2038 except error.ParseError:
2039 2039 if repo.vfs.exists('histedit-last-edit.txt'):
2040 2040 ui.warn(_('warning: histedit rules saved '
2041 2041 'to: .hg/histedit-last-edit.txt\n'))
2042 2042 raise
2043 2043
2044 2044 def verifyactions(actions, state, ctxs):
2045 2045 """Verify that there exists exactly one action per given changeset and
2046 2046 other constraints.
2047 2047
2048 2048 Will abort if there are to many or too few rules, a malformed rule,
2049 2049 or a rule on a changeset outside of the user-given range.
2050 2050 """
2051 2051 expected = set(c.node() for c in ctxs)
2052 2052 seen = set()
2053 2053 prev = None
2054 2054
2055 2055 if actions and actions[0].verb in ['roll', 'fold']:
2056 2056 raise error.ParseError(_('first changeset cannot use verb "%s"') %
2057 2057 actions[0].verb)
2058 2058
2059 2059 for action in actions:
2060 2060 action.verify(prev, expected, seen)
2061 2061 prev = action
2062 2062 if action.node is not None:
2063 2063 seen.add(action.node)
2064 2064 missing = sorted(expected - seen) # sort to stabilize output
2065 2065
2066 2066 if state.repo.ui.configbool('histedit', 'dropmissing'):
2067 2067 if len(actions) == 0:
2068 2068 raise error.ParseError(_('no rules provided'),
2069 2069 hint=_('use strip extension to remove commits'))
2070 2070
2071 2071 drops = [drop(state, n) for n in missing]
2072 2072 # put the in the beginning so they execute immediately and
2073 2073 # don't show in the edit-plan in the future
2074 2074 actions[:0] = drops
2075 2075 elif missing:
2076 2076 raise error.ParseError(_('missing rules for changeset %s') %
2077 2077 node.short(missing[0]),
2078 2078 hint=_('use "drop %s" to discard, see also: '
2079 2079 "'hg help -e histedit.config'")
2080 2080 % node.short(missing[0]))
2081 2081
2082 2082 def adjustreplacementsfrommarkers(repo, oldreplacements):
2083 2083 """Adjust replacements from obsolescence markers
2084 2084
2085 2085 Replacements structure is originally generated based on
2086 2086 histedit's state and does not account for changes that are
2087 2087 not recorded there. This function fixes that by adding
2088 2088 data read from obsolescence markers"""
2089 2089 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
2090 2090 return oldreplacements
2091 2091
2092 2092 unfi = repo.unfiltered()
2093 2093 nm = unfi.changelog.nodemap
2094 2094 obsstore = repo.obsstore
2095 2095 newreplacements = list(oldreplacements)
2096 2096 oldsuccs = [r[1] for r in oldreplacements]
2097 2097 # successors that have already been added to succstocheck once
2098 2098 seensuccs = set().union(*oldsuccs) # create a set from an iterable of tuples
2099 2099 succstocheck = list(seensuccs)
2100 2100 while succstocheck:
2101 2101 n = succstocheck.pop()
2102 2102 missing = nm.get(n) is None
2103 2103 markers = obsstore.successors.get(n, ())
2104 2104 if missing and not markers:
2105 2105 # dead end, mark it as such
2106 2106 newreplacements.append((n, ()))
2107 2107 for marker in markers:
2108 2108 nsuccs = marker[1]
2109 2109 newreplacements.append((n, nsuccs))
2110 2110 for nsucc in nsuccs:
2111 2111 if nsucc not in seensuccs:
2112 2112 seensuccs.add(nsucc)
2113 2113 succstocheck.append(nsucc)
2114 2114
2115 2115 return newreplacements
2116 2116
2117 2117 def processreplacement(state):
2118 2118 """process the list of replacements to return
2119 2119
2120 2120 1) the final mapping between original and created nodes
2121 2121 2) the list of temporary node created by histedit
2122 2122 3) the list of new commit created by histedit"""
2123 2123 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
2124 2124 allsuccs = set()
2125 2125 replaced = set()
2126 2126 fullmapping = {}
2127 2127 # initialize basic set
2128 2128 # fullmapping records all operations recorded in replacement
2129 2129 for rep in replacements:
2130 2130 allsuccs.update(rep[1])
2131 2131 replaced.add(rep[0])
2132 2132 fullmapping.setdefault(rep[0], set()).update(rep[1])
2133 2133 new = allsuccs - replaced
2134 2134 tmpnodes = allsuccs & replaced
2135 2135 # Reduce content fullmapping into direct relation between original nodes
2136 2136 # and final node created during history edition
2137 2137 # Dropped changeset are replaced by an empty list
2138 2138 toproceed = set(fullmapping)
2139 2139 final = {}
2140 2140 while toproceed:
2141 2141 for x in list(toproceed):
2142 2142 succs = fullmapping[x]
2143 2143 for s in list(succs):
2144 2144 if s in toproceed:
2145 2145 # non final node with unknown closure
2146 2146 # We can't process this now
2147 2147 break
2148 2148 elif s in final:
2149 2149 # non final node, replace with closure
2150 2150 succs.remove(s)
2151 2151 succs.update(final[s])
2152 2152 else:
2153 2153 final[x] = succs
2154 2154 toproceed.remove(x)
2155 2155 # remove tmpnodes from final mapping
2156 2156 for n in tmpnodes:
2157 2157 del final[n]
2158 2158 # we expect all changes involved in final to exist in the repo
2159 2159 # turn `final` into list (topologically sorted)
2160 2160 nm = state.repo.changelog.nodemap
2161 2161 for prec, succs in final.items():
2162 2162 final[prec] = sorted(succs, key=nm.get)
2163 2163
2164 2164 # computed topmost element (necessary for bookmark)
2165 2165 if new:
2166 2166 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
2167 2167 elif not final:
2168 2168 # Nothing rewritten at all. we won't need `newtopmost`
2169 2169 # It is the same as `oldtopmost` and `processreplacement` know it
2170 2170 newtopmost = None
2171 2171 else:
2172 2172 # every body died. The newtopmost is the parent of the root.
2173 2173 r = state.repo.changelog.rev
2174 2174 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
2175 2175
2176 2176 return final, tmpnodes, new, newtopmost
2177 2177
2178 2178 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
2179 2179 """Move bookmark from oldtopmost to newly created topmost
2180 2180
2181 2181 This is arguably a feature and we may only want that for the active
2182 2182 bookmark. But the behavior is kept compatible with the old version for now.
2183 2183 """
2184 2184 if not oldtopmost or not newtopmost:
2185 2185 return
2186 2186 oldbmarks = repo.nodebookmarks(oldtopmost)
2187 2187 if oldbmarks:
2188 2188 with repo.lock(), repo.transaction('histedit') as tr:
2189 2189 marks = repo._bookmarks
2190 2190 changes = []
2191 2191 for name in oldbmarks:
2192 2192 changes.append((name, newtopmost))
2193 2193 marks.applychanges(repo, tr, changes)
2194 2194
2195 2195 def cleanupnode(ui, repo, nodes, nobackup=False):
2196 2196 """strip a group of nodes from the repository
2197 2197
2198 2198 The set of node to strip may contains unknown nodes."""
2199 2199 with repo.lock():
2200 2200 # do not let filtering get in the way of the cleanse
2201 2201 # we should probably get rid of obsolescence marker created during the
2202 2202 # histedit, but we currently do not have such information.
2203 2203 repo = repo.unfiltered()
2204 2204 # Find all nodes that need to be stripped
2205 2205 # (we use %lr instead of %ln to silently ignore unknown items)
2206 2206 nm = repo.changelog.nodemap
2207 2207 nodes = sorted(n for n in nodes if n in nm)
2208 2208 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
2209 2209 if roots:
2210 2210 backup = not nobackup
2211 2211 repair.strip(ui, repo, roots, backup=backup)
2212 2212
2213 2213 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
2214 2214 if isinstance(nodelist, str):
2215 2215 nodelist = [nodelist]
2216 2216 state = histeditstate(repo)
2217 2217 if state.inprogress():
2218 2218 state.read()
2219 2219 histedit_nodes = {action.node for action
2220 2220 in state.actions if action.node}
2221 2221 common_nodes = histedit_nodes & set(nodelist)
2222 2222 if common_nodes:
2223 2223 raise error.Abort(_("histedit in progress, can't strip %s")
2224 2224 % ', '.join(node.short(x) for x in common_nodes))
2225 2225 return orig(ui, repo, nodelist, *args, **kwargs)
2226 2226
2227 2227 extensions.wrapfunction(repair, 'strip', stripwrapper)
2228 2228
2229 2229 def summaryhook(ui, repo):
2230 2230 state = histeditstate(repo)
2231 2231 if not state.inprogress():
2232 2232 return
2233 2233 state.read()
2234 2234 if state.actions:
2235 2235 # i18n: column positioning for "hg summary"
2236 2236 ui.write(_('hist: %s (histedit --continue)\n') %
2237 2237 (ui.label(_('%d remaining'), 'histedit.remaining') %
2238 2238 len(state.actions)))
2239 2239
2240 2240 def extsetup(ui):
2241 2241 cmdutil.summaryhooks.add('histedit', summaryhook)
2242 2242 cmdutil.unfinishedstates.append(
2243 2243 ['histedit-state', False, True, _('histedit in progress'),
2244 2244 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
2245 2245 cmdutil.afterresolvedstates.append(
2246 2246 ['histedit-state', _('hg histedit --continue')])
General Comments 0
You need to be logged in to leave comments. Login now