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