##// END OF EJS Templates
curses: do not setlocale() at import time (issue5261)...
Yuya Nishihara -
r43268:701341f5 stable
parent child Browse files
Show More
@@ -1,2326 +1,2324 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 import locale
204 205 import os
205 206 import struct
206 207
207 208 from mercurial.i18n import _
208 209 from mercurial import (
209 210 bundle2,
210 211 cmdutil,
211 212 context,
212 213 copies,
213 214 destutil,
214 215 discovery,
215 216 error,
216 217 exchange,
217 218 extensions,
218 219 hg,
219 220 logcmdutil,
220 221 merge as mergemod,
221 222 mergeutil,
222 223 node,
223 224 obsolete,
224 225 pycompat,
225 226 registrar,
226 227 repair,
227 228 scmutil,
228 229 state as statemod,
229 230 util,
230 231 )
231 232 from mercurial.utils import (
232 233 dateutil,
233 234 stringutil,
234 235 )
235 236
236 237 pickle = util.pickle
237 238 cmdtable = {}
238 239 command = registrar.command(cmdtable)
239 240
240 241 configtable = {}
241 242 configitem = registrar.configitem(configtable)
242 243 configitem('experimental', 'histedit.autoverb',
243 244 default=False,
244 245 )
245 246 configitem('histedit', 'defaultrev',
246 247 default=None,
247 248 )
248 249 configitem('histedit', 'dropmissing',
249 250 default=False,
250 251 )
251 252 configitem('histedit', 'linelen',
252 253 default=80,
253 254 )
254 255 configitem('histedit', 'singletransaction',
255 256 default=False,
256 257 )
257 258 configitem('ui', 'interface.histedit',
258 259 default=None,
259 260 )
260 261 configitem('histedit', 'summary-template',
261 262 default='{rev} {desc|firstline}')
262 263
263 264 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
264 265 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
265 266 # be specifying the version(s) of Mercurial they are tested with, or
266 267 # leave the attribute unspecified.
267 268 testedwith = 'ships-with-hg-core'
268 269
269 270 actiontable = {}
270 271 primaryactions = set()
271 272 secondaryactions = set()
272 273 tertiaryactions = set()
273 274 internalactions = set()
274 275
275 276 def geteditcomment(ui, first, last):
276 277 """ construct the editor comment
277 278 The comment includes::
278 279 - an intro
279 280 - sorted primary commands
280 281 - sorted short commands
281 282 - sorted long commands
282 283 - additional hints
283 284
284 285 Commands are only included once.
285 286 """
286 287 intro = _("""Edit history between %s and %s
287 288
288 289 Commits are listed from least to most recent
289 290
290 291 You can reorder changesets by reordering the lines
291 292
292 293 Commands:
293 294 """)
294 295 actions = []
295 296 def addverb(v):
296 297 a = actiontable[v]
297 298 lines = a.message.split("\n")
298 299 if len(a.verbs):
299 300 v = ', '.join(sorted(a.verbs, key=lambda v: len(v)))
300 301 actions.append(" %s = %s" % (v, lines[0]))
301 302 actions.extend([' %s' for l in lines[1:]])
302 303
303 304 for v in (
304 305 sorted(primaryactions) +
305 306 sorted(secondaryactions) +
306 307 sorted(tertiaryactions)
307 308 ):
308 309 addverb(v)
309 310 actions.append('')
310 311
311 312 hints = []
312 313 if ui.configbool('histedit', 'dropmissing'):
313 314 hints.append("Deleting a changeset from the list "
314 315 "will DISCARD it from the edited history!")
315 316
316 317 lines = (intro % (first, last)).split('\n') + actions + hints
317 318
318 319 return ''.join(['# %s\n' % l if l else '#\n' for l in lines])
319 320
320 321 class histeditstate(object):
321 322 def __init__(self, repo):
322 323 self.repo = repo
323 324 self.actions = None
324 325 self.keep = None
325 326 self.topmost = None
326 327 self.parentctxnode = None
327 328 self.lock = None
328 329 self.wlock = None
329 330 self.backupfile = None
330 331 self.stateobj = statemod.cmdstate(repo, 'histedit-state')
331 332 self.replacements = []
332 333
333 334 def read(self):
334 335 """Load histedit state from disk and set fields appropriately."""
335 336 if not self.stateobj.exists():
336 337 cmdutil.wrongtooltocontinue(self.repo, _('histedit'))
337 338
338 339 data = self._read()
339 340
340 341 self.parentctxnode = data['parentctxnode']
341 342 actions = parserules(data['rules'], self)
342 343 self.actions = actions
343 344 self.keep = data['keep']
344 345 self.topmost = data['topmost']
345 346 self.replacements = data['replacements']
346 347 self.backupfile = data['backupfile']
347 348
348 349 def _read(self):
349 350 fp = self.repo.vfs.read('histedit-state')
350 351 if fp.startswith('v1\n'):
351 352 data = self._load()
352 353 parentctxnode, rules, keep, topmost, replacements, backupfile = data
353 354 else:
354 355 data = pickle.loads(fp)
355 356 parentctxnode, rules, keep, topmost, replacements = data
356 357 backupfile = None
357 358 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
358 359
359 360 return {'parentctxnode': parentctxnode, "rules": rules, "keep": keep,
360 361 "topmost": topmost, "replacements": replacements,
361 362 "backupfile": backupfile}
362 363
363 364 def write(self, tr=None):
364 365 if tr:
365 366 tr.addfilegenerator('histedit-state', ('histedit-state',),
366 367 self._write, location='plain')
367 368 else:
368 369 with self.repo.vfs("histedit-state", "w") as f:
369 370 self._write(f)
370 371
371 372 def _write(self, fp):
372 373 fp.write('v1\n')
373 374 fp.write('%s\n' % node.hex(self.parentctxnode))
374 375 fp.write('%s\n' % node.hex(self.topmost))
375 376 fp.write('%s\n' % ('True' if self.keep else 'False'))
376 377 fp.write('%d\n' % len(self.actions))
377 378 for action in self.actions:
378 379 fp.write('%s\n' % action.tostate())
379 380 fp.write('%d\n' % len(self.replacements))
380 381 for replacement in self.replacements:
381 382 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
382 383 for r in replacement[1])))
383 384 backupfile = self.backupfile
384 385 if not backupfile:
385 386 backupfile = ''
386 387 fp.write('%s\n' % backupfile)
387 388
388 389 def _load(self):
389 390 fp = self.repo.vfs('histedit-state', 'r')
390 391 lines = [l[:-1] for l in fp.readlines()]
391 392
392 393 index = 0
393 394 lines[index] # version number
394 395 index += 1
395 396
396 397 parentctxnode = node.bin(lines[index])
397 398 index += 1
398 399
399 400 topmost = node.bin(lines[index])
400 401 index += 1
401 402
402 403 keep = lines[index] == 'True'
403 404 index += 1
404 405
405 406 # Rules
406 407 rules = []
407 408 rulelen = int(lines[index])
408 409 index += 1
409 410 for i in pycompat.xrange(rulelen):
410 411 ruleaction = lines[index]
411 412 index += 1
412 413 rule = lines[index]
413 414 index += 1
414 415 rules.append((ruleaction, rule))
415 416
416 417 # Replacements
417 418 replacements = []
418 419 replacementlen = int(lines[index])
419 420 index += 1
420 421 for i in pycompat.xrange(replacementlen):
421 422 replacement = lines[index]
422 423 original = node.bin(replacement[:40])
423 424 succ = [node.bin(replacement[i:i + 40]) for i in
424 425 range(40, len(replacement), 40)]
425 426 replacements.append((original, succ))
426 427 index += 1
427 428
428 429 backupfile = lines[index]
429 430 index += 1
430 431
431 432 fp.close()
432 433
433 434 return parentctxnode, rules, keep, topmost, replacements, backupfile
434 435
435 436 def clear(self):
436 437 if self.inprogress():
437 438 self.repo.vfs.unlink('histedit-state')
438 439
439 440 def inprogress(self):
440 441 return self.repo.vfs.exists('histedit-state')
441 442
442 443
443 444 class histeditaction(object):
444 445 def __init__(self, state, node):
445 446 self.state = state
446 447 self.repo = state.repo
447 448 self.node = node
448 449
449 450 @classmethod
450 451 def fromrule(cls, state, rule):
451 452 """Parses the given rule, returning an instance of the histeditaction.
452 453 """
453 454 ruleid = rule.strip().split(' ', 1)[0]
454 455 # ruleid can be anything from rev numbers, hashes, "bookmarks" etc
455 456 # Check for validation of rule ids and get the rulehash
456 457 try:
457 458 rev = node.bin(ruleid)
458 459 except TypeError:
459 460 try:
460 461 _ctx = scmutil.revsingle(state.repo, ruleid)
461 462 rulehash = _ctx.hex()
462 463 rev = node.bin(rulehash)
463 464 except error.RepoLookupError:
464 465 raise error.ParseError(_("invalid changeset %s") % ruleid)
465 466 return cls(state, rev)
466 467
467 468 def verify(self, prev, expected, seen):
468 469 """ Verifies semantic correctness of the rule"""
469 470 repo = self.repo
470 471 ha = node.hex(self.node)
471 472 self.node = scmutil.resolvehexnodeidprefix(repo, ha)
472 473 if self.node is None:
473 474 raise error.ParseError(_('unknown changeset %s listed') % ha[:12])
474 475 self._verifynodeconstraints(prev, expected, seen)
475 476
476 477 def _verifynodeconstraints(self, prev, expected, seen):
477 478 # by default command need a node in the edited list
478 479 if self.node not in expected:
479 480 raise error.ParseError(_('%s "%s" changeset was not a candidate')
480 481 % (self.verb, node.short(self.node)),
481 482 hint=_('only use listed changesets'))
482 483 # and only one command per node
483 484 if self.node in seen:
484 485 raise error.ParseError(_('duplicated command for changeset %s') %
485 486 node.short(self.node))
486 487
487 488 def torule(self):
488 489 """build a histedit rule line for an action
489 490
490 491 by default lines are in the form:
491 492 <hash> <rev> <summary>
492 493 """
493 494 ctx = self.repo[self.node]
494 495 ui = self.repo.ui
495 496 summary = cmdutil.rendertemplate(
496 497 ctx, ui.config('histedit', 'summary-template')) or ''
497 498 summary = summary.splitlines()[0]
498 499 line = '%s %s %s' % (self.verb, ctx, summary)
499 500 # trim to 75 columns by default so it's not stupidly wide in my editor
500 501 # (the 5 more are left for verb)
501 502 maxlen = self.repo.ui.configint('histedit', 'linelen')
502 503 maxlen = max(maxlen, 22) # avoid truncating hash
503 504 return stringutil.ellipsis(line, maxlen)
504 505
505 506 def tostate(self):
506 507 """Print an action in format used by histedit state files
507 508 (the first line is a verb, the remainder is the second)
508 509 """
509 510 return "%s\n%s" % (self.verb, node.hex(self.node))
510 511
511 512 def run(self):
512 513 """Runs the action. The default behavior is simply apply the action's
513 514 rulectx onto the current parentctx."""
514 515 self.applychange()
515 516 self.continuedirty()
516 517 return self.continueclean()
517 518
518 519 def applychange(self):
519 520 """Applies the changes from this action's rulectx onto the current
520 521 parentctx, but does not commit them."""
521 522 repo = self.repo
522 523 rulectx = repo[self.node]
523 524 repo.ui.pushbuffer(error=True, labeled=True)
524 525 hg.update(repo, self.state.parentctxnode, quietempty=True)
525 526 repo.ui.popbuffer()
526 527 stats = applychanges(repo.ui, repo, rulectx, {})
527 528 repo.dirstate.setbranch(rulectx.branch())
528 529 if stats.unresolvedcount:
529 530 raise error.InterventionRequired(
530 531 _('Fix up the change (%s %s)') %
531 532 (self.verb, node.short(self.node)),
532 533 hint=_('hg histedit --continue to resume'))
533 534
534 535 def continuedirty(self):
535 536 """Continues the action when changes have been applied to the working
536 537 copy. The default behavior is to commit the dirty changes."""
537 538 repo = self.repo
538 539 rulectx = repo[self.node]
539 540
540 541 editor = self.commiteditor()
541 542 commit = commitfuncfor(repo, rulectx)
542 543 if repo.ui.configbool('rewrite', 'update-timestamp'):
543 544 date = dateutil.makedate()
544 545 else:
545 546 date = rulectx.date()
546 547 commit(text=rulectx.description(), user=rulectx.user(),
547 548 date=date, extra=rulectx.extra(), editor=editor)
548 549
549 550 def commiteditor(self):
550 551 """The editor to be used to edit the commit message."""
551 552 return False
552 553
553 554 def continueclean(self):
554 555 """Continues the action when the working copy is clean. The default
555 556 behavior is to accept the current commit as the new version of the
556 557 rulectx."""
557 558 ctx = self.repo['.']
558 559 if ctx.node() == self.state.parentctxnode:
559 560 self.repo.ui.warn(_('%s: skipping changeset (no changes)\n') %
560 561 node.short(self.node))
561 562 return ctx, [(self.node, tuple())]
562 563 if ctx.node() == self.node:
563 564 # Nothing changed
564 565 return ctx, []
565 566 return ctx, [(self.node, (ctx.node(),))]
566 567
567 568 def commitfuncfor(repo, src):
568 569 """Build a commit function for the replacement of <src>
569 570
570 571 This function ensure we apply the same treatment to all changesets.
571 572
572 573 - Add a 'histedit_source' entry in extra.
573 574
574 575 Note that fold has its own separated logic because its handling is a bit
575 576 different and not easily factored out of the fold method.
576 577 """
577 578 phasemin = src.phase()
578 579 def commitfunc(**kwargs):
579 580 overrides = {('phases', 'new-commit'): phasemin}
580 581 with repo.ui.configoverride(overrides, 'histedit'):
581 582 extra = kwargs.get(r'extra', {}).copy()
582 583 extra['histedit_source'] = src.hex()
583 584 kwargs[r'extra'] = extra
584 585 return repo.commit(**kwargs)
585 586 return commitfunc
586 587
587 588 def applychanges(ui, repo, ctx, opts):
588 589 """Merge changeset from ctx (only) in the current working directory"""
589 590 wcpar = repo.dirstate.p1()
590 591 if ctx.p1().node() == wcpar:
591 592 # edits are "in place" we do not need to make any merge,
592 593 # just applies changes on parent for editing
593 594 ui.pushbuffer()
594 595 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
595 596 stats = mergemod.updateresult(0, 0, 0, 0)
596 597 ui.popbuffer()
597 598 else:
598 599 try:
599 600 # ui.forcemerge is an internal variable, do not document
600 601 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
601 602 'histedit')
602 603 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
603 604 finally:
604 605 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
605 606 return stats
606 607
607 608 def collapse(repo, firstctx, lastctx, commitopts, skipprompt=False):
608 609 """collapse the set of revisions from first to last as new one.
609 610
610 611 Expected commit options are:
611 612 - message
612 613 - date
613 614 - username
614 615 Commit message is edited in all cases.
615 616
616 617 This function works in memory."""
617 618 ctxs = list(repo.set('%d::%d', firstctx.rev(), lastctx.rev()))
618 619 if not ctxs:
619 620 return None
620 621 for c in ctxs:
621 622 if not c.mutable():
622 623 raise error.ParseError(
623 624 _("cannot fold into public change %s") % node.short(c.node()))
624 625 base = firstctx.p1()
625 626
626 627 # commit a new version of the old changeset, including the update
627 628 # collect all files which might be affected
628 629 files = set()
629 630 for ctx in ctxs:
630 631 files.update(ctx.files())
631 632
632 633 # Recompute copies (avoid recording a -> b -> a)
633 634 copied = copies.pathcopies(base, lastctx)
634 635
635 636 # prune files which were reverted by the updates
636 637 files = [f for f in files if not cmdutil.samefile(f, lastctx, base)]
637 638 # commit version of these files as defined by head
638 639 headmf = lastctx.manifest()
639 640 def filectxfn(repo, ctx, path):
640 641 if path in headmf:
641 642 fctx = lastctx[path]
642 643 flags = fctx.flags()
643 644 mctx = context.memfilectx(repo, ctx,
644 645 fctx.path(), fctx.data(),
645 646 islink='l' in flags,
646 647 isexec='x' in flags,
647 648 copysource=copied.get(path))
648 649 return mctx
649 650 return None
650 651
651 652 if commitopts.get('message'):
652 653 message = commitopts['message']
653 654 else:
654 655 message = firstctx.description()
655 656 user = commitopts.get('user')
656 657 date = commitopts.get('date')
657 658 extra = commitopts.get('extra')
658 659
659 660 parents = (firstctx.p1().node(), firstctx.p2().node())
660 661 editor = None
661 662 if not skipprompt:
662 663 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
663 664 new = context.memctx(repo,
664 665 parents=parents,
665 666 text=message,
666 667 files=files,
667 668 filectxfn=filectxfn,
668 669 user=user,
669 670 date=date,
670 671 extra=extra,
671 672 editor=editor)
672 673 return repo.commitctx(new)
673 674
674 675 def _isdirtywc(repo):
675 676 return repo[None].dirty(missing=True)
676 677
677 678 def abortdirty():
678 679 raise error.Abort(_('working copy has pending changes'),
679 680 hint=_('amend, commit, or revert them and run histedit '
680 681 '--continue, or abort with histedit --abort'))
681 682
682 683 def action(verbs, message, priority=False, internal=False):
683 684 def wrap(cls):
684 685 assert not priority or not internal
685 686 verb = verbs[0]
686 687 if priority:
687 688 primaryactions.add(verb)
688 689 elif internal:
689 690 internalactions.add(verb)
690 691 elif len(verbs) > 1:
691 692 secondaryactions.add(verb)
692 693 else:
693 694 tertiaryactions.add(verb)
694 695
695 696 cls.verb = verb
696 697 cls.verbs = verbs
697 698 cls.message = message
698 699 for verb in verbs:
699 700 actiontable[verb] = cls
700 701 return cls
701 702 return wrap
702 703
703 704 @action(['pick', 'p'],
704 705 _('use commit'),
705 706 priority=True)
706 707 class pick(histeditaction):
707 708 def run(self):
708 709 rulectx = self.repo[self.node]
709 710 if rulectx.p1().node() == self.state.parentctxnode:
710 711 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
711 712 return rulectx, []
712 713
713 714 return super(pick, self).run()
714 715
715 716 @action(['edit', 'e'],
716 717 _('use commit, but stop for amending'),
717 718 priority=True)
718 719 class edit(histeditaction):
719 720 def run(self):
720 721 repo = self.repo
721 722 rulectx = repo[self.node]
722 723 hg.update(repo, self.state.parentctxnode, quietempty=True)
723 724 applychanges(repo.ui, repo, rulectx, {})
724 725 raise error.InterventionRequired(
725 726 _('Editing (%s), you may commit or record as needed now.')
726 727 % node.short(self.node),
727 728 hint=_('hg histedit --continue to resume'))
728 729
729 730 def commiteditor(self):
730 731 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
731 732
732 733 @action(['fold', 'f'],
733 734 _('use commit, but combine it with the one above'))
734 735 class fold(histeditaction):
735 736 def verify(self, prev, expected, seen):
736 737 """ Verifies semantic correctness of the fold rule"""
737 738 super(fold, self).verify(prev, expected, seen)
738 739 repo = self.repo
739 740 if not prev:
740 741 c = repo[self.node].p1()
741 742 elif not prev.verb in ('pick', 'base'):
742 743 return
743 744 else:
744 745 c = repo[prev.node]
745 746 if not c.mutable():
746 747 raise error.ParseError(
747 748 _("cannot fold into public change %s") % node.short(c.node()))
748 749
749 750
750 751 def continuedirty(self):
751 752 repo = self.repo
752 753 rulectx = repo[self.node]
753 754
754 755 commit = commitfuncfor(repo, rulectx)
755 756 commit(text='fold-temp-revision %s' % node.short(self.node),
756 757 user=rulectx.user(), date=rulectx.date(),
757 758 extra=rulectx.extra())
758 759
759 760 def continueclean(self):
760 761 repo = self.repo
761 762 ctx = repo['.']
762 763 rulectx = repo[self.node]
763 764 parentctxnode = self.state.parentctxnode
764 765 if ctx.node() == parentctxnode:
765 766 repo.ui.warn(_('%s: empty changeset\n') %
766 767 node.short(self.node))
767 768 return ctx, [(self.node, (parentctxnode,))]
768 769
769 770 parentctx = repo[parentctxnode]
770 771 newcommits = set(c.node() for c in repo.set('(%d::. - %d)',
771 772 parentctx.rev(),
772 773 parentctx.rev()))
773 774 if not newcommits:
774 775 repo.ui.warn(_('%s: cannot fold - working copy is not a '
775 776 'descendant of previous commit %s\n') %
776 777 (node.short(self.node), node.short(parentctxnode)))
777 778 return ctx, [(self.node, (ctx.node(),))]
778 779
779 780 middlecommits = newcommits.copy()
780 781 middlecommits.discard(ctx.node())
781 782
782 783 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
783 784 middlecommits)
784 785
785 786 def skipprompt(self):
786 787 """Returns true if the rule should skip the message editor.
787 788
788 789 For example, 'fold' wants to show an editor, but 'rollup'
789 790 doesn't want to.
790 791 """
791 792 return False
792 793
793 794 def mergedescs(self):
794 795 """Returns true if the rule should merge messages of multiple changes.
795 796
796 797 This exists mainly so that 'rollup' rules can be a subclass of
797 798 'fold'.
798 799 """
799 800 return True
800 801
801 802 def firstdate(self):
802 803 """Returns true if the rule should preserve the date of the first
803 804 change.
804 805
805 806 This exists mainly so that 'rollup' rules can be a subclass of
806 807 'fold'.
807 808 """
808 809 return False
809 810
810 811 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
811 812 parent = ctx.p1().node()
812 813 hg.updaterepo(repo, parent, overwrite=False)
813 814 ### prepare new commit data
814 815 commitopts = {}
815 816 commitopts['user'] = ctx.user()
816 817 # commit message
817 818 if not self.mergedescs():
818 819 newmessage = ctx.description()
819 820 else:
820 821 newmessage = '\n***\n'.join(
821 822 [ctx.description()] +
822 823 [repo[r].description() for r in internalchanges] +
823 824 [oldctx.description()]) + '\n'
824 825 commitopts['message'] = newmessage
825 826 # date
826 827 if self.firstdate():
827 828 commitopts['date'] = ctx.date()
828 829 else:
829 830 commitopts['date'] = max(ctx.date(), oldctx.date())
830 831 # if date is to be updated to current
831 832 if ui.configbool('rewrite', 'update-timestamp'):
832 833 commitopts['date'] = dateutil.makedate()
833 834
834 835 extra = ctx.extra().copy()
835 836 # histedit_source
836 837 # note: ctx is likely a temporary commit but that the best we can do
837 838 # here. This is sufficient to solve issue3681 anyway.
838 839 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
839 840 commitopts['extra'] = extra
840 841 phasemin = max(ctx.phase(), oldctx.phase())
841 842 overrides = {('phases', 'new-commit'): phasemin}
842 843 with repo.ui.configoverride(overrides, 'histedit'):
843 844 n = collapse(repo, ctx, repo[newnode], commitopts,
844 845 skipprompt=self.skipprompt())
845 846 if n is None:
846 847 return ctx, []
847 848 hg.updaterepo(repo, n, overwrite=False)
848 849 replacements = [(oldctx.node(), (newnode,)),
849 850 (ctx.node(), (n,)),
850 851 (newnode, (n,)),
851 852 ]
852 853 for ich in internalchanges:
853 854 replacements.append((ich, (n,)))
854 855 return repo[n], replacements
855 856
856 857 @action(['base', 'b'],
857 858 _('checkout changeset and apply further changesets from there'))
858 859 class base(histeditaction):
859 860
860 861 def run(self):
861 862 if self.repo['.'].node() != self.node:
862 863 mergemod.update(self.repo, self.node, branchmerge=False, force=True)
863 864 return self.continueclean()
864 865
865 866 def continuedirty(self):
866 867 abortdirty()
867 868
868 869 def continueclean(self):
869 870 basectx = self.repo['.']
870 871 return basectx, []
871 872
872 873 def _verifynodeconstraints(self, prev, expected, seen):
873 874 # base can only be use with a node not in the edited set
874 875 if self.node in expected:
875 876 msg = _('%s "%s" changeset was an edited list candidate')
876 877 raise error.ParseError(
877 878 msg % (self.verb, node.short(self.node)),
878 879 hint=_('base must only use unlisted changesets'))
879 880
880 881 @action(['_multifold'],
881 882 _(
882 883 """fold subclass used for when multiple folds happen in a row
883 884
884 885 We only want to fire the editor for the folded message once when
885 886 (say) four changes are folded down into a single change. This is
886 887 similar to rollup, but we should preserve both messages so that
887 888 when the last fold operation runs we can show the user all the
888 889 commit messages in their editor.
889 890 """),
890 891 internal=True)
891 892 class _multifold(fold):
892 893 def skipprompt(self):
893 894 return True
894 895
895 896 @action(["roll", "r"],
896 897 _("like fold, but discard this commit's description and date"))
897 898 class rollup(fold):
898 899 def mergedescs(self):
899 900 return False
900 901
901 902 def skipprompt(self):
902 903 return True
903 904
904 905 def firstdate(self):
905 906 return True
906 907
907 908 @action(["drop", "d"],
908 909 _('remove commit from history'))
909 910 class drop(histeditaction):
910 911 def run(self):
911 912 parentctx = self.repo[self.state.parentctxnode]
912 913 return parentctx, [(self.node, tuple())]
913 914
914 915 @action(["mess", "m"],
915 916 _('edit commit message without changing commit content'),
916 917 priority=True)
917 918 class message(histeditaction):
918 919 def commiteditor(self):
919 920 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
920 921
921 922 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
922 923 """utility function to find the first outgoing changeset
923 924
924 925 Used by initialization code"""
925 926 if opts is None:
926 927 opts = {}
927 928 dest = ui.expandpath(remote or 'default-push', remote or 'default')
928 929 dest, branches = hg.parseurl(dest, None)[:2]
929 930 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
930 931
931 932 revs, checkout = hg.addbranchrevs(repo, repo, branches, None)
932 933 other = hg.peer(repo, opts, dest)
933 934
934 935 if revs:
935 936 revs = [repo.lookup(rev) for rev in revs]
936 937
937 938 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
938 939 if not outgoing.missing:
939 940 raise error.Abort(_('no outgoing ancestors'))
940 941 roots = list(repo.revs("roots(%ln)", outgoing.missing))
941 942 if len(roots) > 1:
942 943 msg = _('there are ambiguous outgoing revisions')
943 944 hint = _("see 'hg help histedit' for more detail")
944 945 raise error.Abort(msg, hint=hint)
945 946 return repo[roots[0]].node()
946 947
947 948 # Curses Support
948 949 try:
949 950 import curses
950
951 # Curses requires setting the locale or it will default to the C
952 # locale. This sets the locale to the user's default system
953 # locale.
954 import locale
955 locale.setlocale(locale.LC_ALL, r'')
956 951 except ImportError:
957 952 curses = None
958 953
959 954 KEY_LIST = ['pick', 'edit', 'fold', 'drop', 'mess', 'roll']
960 955 ACTION_LABELS = {
961 956 'fold': '^fold',
962 957 'roll': '^roll',
963 958 }
964 959
965 960 COLOR_HELP, COLOR_SELECTED, COLOR_OK, COLOR_WARN, COLOR_CURRENT = 1, 2, 3, 4, 5
966 961 COLOR_DIFF_ADD_LINE, COLOR_DIFF_DEL_LINE, COLOR_DIFF_OFFSET = 6, 7, 8
967 962
968 963 E_QUIT, E_HISTEDIT = 1, 2
969 964 E_PAGEDOWN, E_PAGEUP, E_LINEUP, E_LINEDOWN, E_RESIZE = 3, 4, 5, 6, 7
970 965 MODE_INIT, MODE_PATCH, MODE_RULES, MODE_HELP = 0, 1, 2, 3
971 966
972 967 KEYTABLE = {
973 968 'global': {
974 969 'h': 'next-action',
975 970 'KEY_RIGHT': 'next-action',
976 971 'l': 'prev-action',
977 972 'KEY_LEFT': 'prev-action',
978 973 'q': 'quit',
979 974 'c': 'histedit',
980 975 'C': 'histedit',
981 976 'v': 'showpatch',
982 977 '?': 'help',
983 978 },
984 979 MODE_RULES: {
985 980 'd': 'action-drop',
986 981 'e': 'action-edit',
987 982 'f': 'action-fold',
988 983 'm': 'action-mess',
989 984 'p': 'action-pick',
990 985 'r': 'action-roll',
991 986 ' ': 'select',
992 987 'j': 'down',
993 988 'k': 'up',
994 989 'KEY_DOWN': 'down',
995 990 'KEY_UP': 'up',
996 991 'J': 'move-down',
997 992 'K': 'move-up',
998 993 'KEY_NPAGE': 'move-down',
999 994 'KEY_PPAGE': 'move-up',
1000 995 '0': 'goto', # Used for 0..9
1001 996 },
1002 997 MODE_PATCH: {
1003 998 ' ': 'page-down',
1004 999 'KEY_NPAGE': 'page-down',
1005 1000 'KEY_PPAGE': 'page-up',
1006 1001 'j': 'line-down',
1007 1002 'k': 'line-up',
1008 1003 'KEY_DOWN': 'line-down',
1009 1004 'KEY_UP': 'line-up',
1010 1005 'J': 'down',
1011 1006 'K': 'up',
1012 1007 },
1013 1008 MODE_HELP: {
1014 1009 },
1015 1010 }
1016 1011
1017 1012 def screen_size():
1018 1013 return struct.unpack('hh', fcntl.ioctl(1, termios.TIOCGWINSZ, ' '))
1019 1014
1020 1015 class histeditrule(object):
1021 1016 def __init__(self, ctx, pos, action='pick'):
1022 1017 self.ctx = ctx
1023 1018 self.action = action
1024 1019 self.origpos = pos
1025 1020 self.pos = pos
1026 1021 self.conflicts = []
1027 1022
1028 1023 def __str__(self):
1029 1024 # Some actions ('fold' and 'roll') combine a patch with a previous one.
1030 1025 # Add a marker showing which patch they apply to, and also omit the
1031 1026 # description for 'roll' (since it will get discarded). Example display:
1032 1027 #
1033 1028 # #10 pick 316392:06a16c25c053 add option to skip tests
1034 1029 # #11 ^roll 316393:71313c964cc5
1035 1030 # #12 pick 316394:ab31f3973b0d include mfbt for mozilla-config.h
1036 1031 # #13 ^fold 316395:14ce5803f4c3 fix warnings
1037 1032 #
1038 1033 # The carets point to the changeset being folded into ("roll this
1039 1034 # changeset into the changeset above").
1040 1035 action = ACTION_LABELS.get(self.action, self.action)
1041 1036 h = self.ctx.hex()[0:12]
1042 1037 r = self.ctx.rev()
1043 1038 desc = self.ctx.description().splitlines()[0].strip()
1044 1039 if self.action == 'roll':
1045 1040 desc = ''
1046 1041 return "#{0:<2} {1:<6} {2}:{3} {4}".format(
1047 1042 self.origpos, action, r, h, desc)
1048 1043
1049 1044 def checkconflicts(self, other):
1050 1045 if other.pos > self.pos and other.origpos <= self.origpos:
1051 1046 if set(other.ctx.files()) & set(self.ctx.files()) != set():
1052 1047 self.conflicts.append(other)
1053 1048 return self.conflicts
1054 1049
1055 1050 if other in self.conflicts:
1056 1051 self.conflicts.remove(other)
1057 1052 return self.conflicts
1058 1053
1059 1054 # ============ EVENTS ===============
1060 1055 def movecursor(state, oldpos, newpos):
1061 1056 '''Change the rule/changeset that the cursor is pointing to, regardless of
1062 1057 current mode (you can switch between patches from the view patch window).'''
1063 1058 state['pos'] = newpos
1064 1059
1065 1060 mode, _ = state['mode']
1066 1061 if mode == MODE_RULES:
1067 1062 # Scroll through the list by updating the view for MODE_RULES, so that
1068 1063 # even if we are not currently viewing the rules, switching back will
1069 1064 # result in the cursor's rule being visible.
1070 1065 modestate = state['modes'][MODE_RULES]
1071 1066 if newpos < modestate['line_offset']:
1072 1067 modestate['line_offset'] = newpos
1073 1068 elif newpos > modestate['line_offset'] + state['page_height'] - 1:
1074 1069 modestate['line_offset'] = newpos - state['page_height'] + 1
1075 1070
1076 1071 # Reset the patch view region to the top of the new patch.
1077 1072 state['modes'][MODE_PATCH]['line_offset'] = 0
1078 1073
1079 1074 def changemode(state, mode):
1080 1075 curmode, _ = state['mode']
1081 1076 state['mode'] = (mode, curmode)
1082 1077 if mode == MODE_PATCH:
1083 1078 state['modes'][MODE_PATCH]['patchcontents'] = patchcontents(state)
1084 1079
1085 1080 def makeselection(state, pos):
1086 1081 state['selected'] = pos
1087 1082
1088 1083 def swap(state, oldpos, newpos):
1089 1084 """Swap two positions and calculate necessary conflicts in
1090 1085 O(|newpos-oldpos|) time"""
1091 1086
1092 1087 rules = state['rules']
1093 1088 assert 0 <= oldpos < len(rules) and 0 <= newpos < len(rules)
1094 1089
1095 1090 rules[oldpos], rules[newpos] = rules[newpos], rules[oldpos]
1096 1091
1097 1092 # TODO: swap should not know about histeditrule's internals
1098 1093 rules[newpos].pos = newpos
1099 1094 rules[oldpos].pos = oldpos
1100 1095
1101 1096 start = min(oldpos, newpos)
1102 1097 end = max(oldpos, newpos)
1103 1098 for r in pycompat.xrange(start, end + 1):
1104 1099 rules[newpos].checkconflicts(rules[r])
1105 1100 rules[oldpos].checkconflicts(rules[r])
1106 1101
1107 1102 if state['selected']:
1108 1103 makeselection(state, newpos)
1109 1104
1110 1105 def changeaction(state, pos, action):
1111 1106 """Change the action state on the given position to the new action"""
1112 1107 rules = state['rules']
1113 1108 assert 0 <= pos < len(rules)
1114 1109 rules[pos].action = action
1115 1110
1116 1111 def cycleaction(state, pos, next=False):
1117 1112 """Changes the action state the next or the previous action from
1118 1113 the action list"""
1119 1114 rules = state['rules']
1120 1115 assert 0 <= pos < len(rules)
1121 1116 current = rules[pos].action
1122 1117
1123 1118 assert current in KEY_LIST
1124 1119
1125 1120 index = KEY_LIST.index(current)
1126 1121 if next:
1127 1122 index += 1
1128 1123 else:
1129 1124 index -= 1
1130 1125 changeaction(state, pos, KEY_LIST[index % len(KEY_LIST)])
1131 1126
1132 1127 def changeview(state, delta, unit):
1133 1128 '''Change the region of whatever is being viewed (a patch or the list of
1134 1129 changesets). 'delta' is an amount (+/- 1) and 'unit' is 'page' or 'line'.'''
1135 1130 mode, _ = state['mode']
1136 1131 if mode != MODE_PATCH:
1137 1132 return
1138 1133 mode_state = state['modes'][mode]
1139 1134 num_lines = len(mode_state['patchcontents'])
1140 1135 page_height = state['page_height']
1141 1136 unit = page_height if unit == 'page' else 1
1142 1137 num_pages = 1 + (num_lines - 1) / page_height
1143 1138 max_offset = (num_pages - 1) * page_height
1144 1139 newline = mode_state['line_offset'] + delta * unit
1145 1140 mode_state['line_offset'] = max(0, min(max_offset, newline))
1146 1141
1147 1142 def event(state, ch):
1148 1143 """Change state based on the current character input
1149 1144
1150 1145 This takes the current state and based on the current character input from
1151 1146 the user we change the state.
1152 1147 """
1153 1148 selected = state['selected']
1154 1149 oldpos = state['pos']
1155 1150 rules = state['rules']
1156 1151
1157 1152 if ch in (curses.KEY_RESIZE, "KEY_RESIZE"):
1158 1153 return E_RESIZE
1159 1154
1160 1155 lookup_ch = ch
1161 1156 if '0' <= ch <= '9':
1162 1157 lookup_ch = '0'
1163 1158
1164 1159 curmode, prevmode = state['mode']
1165 1160 action = KEYTABLE[curmode].get(lookup_ch, KEYTABLE['global'].get(lookup_ch))
1166 1161 if action is None:
1167 1162 return
1168 1163 if action in ('down', 'move-down'):
1169 1164 newpos = min(oldpos + 1, len(rules) - 1)
1170 1165 movecursor(state, oldpos, newpos)
1171 1166 if selected is not None or action == 'move-down':
1172 1167 swap(state, oldpos, newpos)
1173 1168 elif action in ('up', 'move-up'):
1174 1169 newpos = max(0, oldpos - 1)
1175 1170 movecursor(state, oldpos, newpos)
1176 1171 if selected is not None or action == 'move-up':
1177 1172 swap(state, oldpos, newpos)
1178 1173 elif action == 'next-action':
1179 1174 cycleaction(state, oldpos, next=True)
1180 1175 elif action == 'prev-action':
1181 1176 cycleaction(state, oldpos, next=False)
1182 1177 elif action == 'select':
1183 1178 selected = oldpos if selected is None else None
1184 1179 makeselection(state, selected)
1185 1180 elif action == 'goto' and int(ch) < len(rules) and len(rules) <= 10:
1186 1181 newrule = next((r for r in rules if r.origpos == int(ch)))
1187 1182 movecursor(state, oldpos, newrule.pos)
1188 1183 if selected is not None:
1189 1184 swap(state, oldpos, newrule.pos)
1190 1185 elif action.startswith('action-'):
1191 1186 changeaction(state, oldpos, action[7:])
1192 1187 elif action == 'showpatch':
1193 1188 changemode(state, MODE_PATCH if curmode != MODE_PATCH else prevmode)
1194 1189 elif action == 'help':
1195 1190 changemode(state, MODE_HELP if curmode != MODE_HELP else prevmode)
1196 1191 elif action == 'quit':
1197 1192 return E_QUIT
1198 1193 elif action == 'histedit':
1199 1194 return E_HISTEDIT
1200 1195 elif action == 'page-down':
1201 1196 return E_PAGEDOWN
1202 1197 elif action == 'page-up':
1203 1198 return E_PAGEUP
1204 1199 elif action == 'line-down':
1205 1200 return E_LINEDOWN
1206 1201 elif action == 'line-up':
1207 1202 return E_LINEUP
1208 1203
1209 1204 def makecommands(rules):
1210 1205 """Returns a list of commands consumable by histedit --commands based on
1211 1206 our list of rules"""
1212 1207 commands = []
1213 1208 for rules in rules:
1214 1209 commands.append("{0} {1}\n".format(rules.action, rules.ctx))
1215 1210 return commands
1216 1211
1217 1212 def addln(win, y, x, line, color=None):
1218 1213 """Add a line to the given window left padding but 100% filled with
1219 1214 whitespace characters, so that the color appears on the whole line"""
1220 1215 maxy, maxx = win.getmaxyx()
1221 1216 length = maxx - 1 - x
1222 1217 line = ("{0:<%d}" % length).format(str(line).strip())[:length]
1223 1218 if y < 0:
1224 1219 y = maxy + y
1225 1220 if x < 0:
1226 1221 x = maxx + x
1227 1222 if color:
1228 1223 win.addstr(y, x, line, color)
1229 1224 else:
1230 1225 win.addstr(y, x, line)
1231 1226
1232 1227 def _trunc_head(line, n):
1233 1228 if len(line) <= n:
1234 1229 return line
1235 1230 return '> ' + line[-(n - 2):]
1236 1231 def _trunc_tail(line, n):
1237 1232 if len(line) <= n:
1238 1233 return line
1239 1234 return line[:n - 2] + ' >'
1240 1235
1241 1236 def patchcontents(state):
1242 1237 repo = state['repo']
1243 1238 rule = state['rules'][state['pos']]
1244 1239 displayer = logcmdutil.changesetdisplayer(repo.ui, repo, {
1245 1240 "patch": True, "template": "status"
1246 1241 }, buffered=True)
1247 1242 overrides = {('ui', 'verbose'): True}
1248 1243 with repo.ui.configoverride(overrides, source='histedit'):
1249 1244 displayer.show(rule.ctx)
1250 1245 displayer.close()
1251 1246 return displayer.hunk[rule.ctx.rev()].splitlines()
1252 1247
1253 1248 def _chisteditmain(repo, rules, stdscr):
1254 1249 try:
1255 1250 curses.use_default_colors()
1256 1251 except curses.error:
1257 1252 pass
1258 1253
1259 1254 # initialize color pattern
1260 1255 curses.init_pair(COLOR_HELP, curses.COLOR_WHITE, curses.COLOR_BLUE)
1261 1256 curses.init_pair(COLOR_SELECTED, curses.COLOR_BLACK, curses.COLOR_WHITE)
1262 1257 curses.init_pair(COLOR_WARN, curses.COLOR_BLACK, curses.COLOR_YELLOW)
1263 1258 curses.init_pair(COLOR_OK, curses.COLOR_BLACK, curses.COLOR_GREEN)
1264 1259 curses.init_pair(COLOR_CURRENT, curses.COLOR_WHITE, curses.COLOR_MAGENTA)
1265 1260 curses.init_pair(COLOR_DIFF_ADD_LINE, curses.COLOR_GREEN, -1)
1266 1261 curses.init_pair(COLOR_DIFF_DEL_LINE, curses.COLOR_RED, -1)
1267 1262 curses.init_pair(COLOR_DIFF_OFFSET, curses.COLOR_MAGENTA, -1)
1268 1263
1269 1264 # don't display the cursor
1270 1265 try:
1271 1266 curses.curs_set(0)
1272 1267 except curses.error:
1273 1268 pass
1274 1269
1275 1270 def rendercommit(win, state):
1276 1271 """Renders the commit window that shows the log of the current selected
1277 1272 commit"""
1278 1273 pos = state['pos']
1279 1274 rules = state['rules']
1280 1275 rule = rules[pos]
1281 1276
1282 1277 ctx = rule.ctx
1283 1278 win.box()
1284 1279
1285 1280 maxy, maxx = win.getmaxyx()
1286 1281 length = maxx - 3
1287 1282
1288 1283 line = "changeset: {0}:{1:<12}".format(ctx.rev(), ctx)
1289 1284 win.addstr(1, 1, line[:length])
1290 1285
1291 1286 line = "user: {0}".format(ctx.user())
1292 1287 win.addstr(2, 1, line[:length])
1293 1288
1294 1289 bms = repo.nodebookmarks(ctx.node())
1295 1290 line = "bookmark: {0}".format(' '.join(bms))
1296 1291 win.addstr(3, 1, line[:length])
1297 1292
1298 1293 line = "summary: {0}".format(ctx.description().splitlines()[0])
1299 1294 win.addstr(4, 1, line[:length])
1300 1295
1301 1296 line = "files: "
1302 1297 win.addstr(5, 1, line)
1303 1298 fnx = 1 + len(line)
1304 1299 fnmaxx = length - fnx + 1
1305 1300 y = 5
1306 1301 fnmaxn = maxy - (1 + y) - 1
1307 1302 files = ctx.files()
1308 1303 for i, line1 in enumerate(files):
1309 1304 if len(files) > fnmaxn and i == fnmaxn - 1:
1310 1305 win.addstr(y, fnx, _trunc_tail(','.join(files[i:]), fnmaxx))
1311 1306 y = y + 1
1312 1307 break
1313 1308 win.addstr(y, fnx, _trunc_head(line1, fnmaxx))
1314 1309 y = y + 1
1315 1310
1316 1311 conflicts = rule.conflicts
1317 1312 if len(conflicts) > 0:
1318 1313 conflictstr = ','.join(map(lambda r: str(r.ctx), conflicts))
1319 1314 conflictstr = "changed files overlap with {0}".format(conflictstr)
1320 1315 else:
1321 1316 conflictstr = 'no overlap'
1322 1317
1323 1318 win.addstr(y, 1, conflictstr[:length])
1324 1319 win.noutrefresh()
1325 1320
1326 1321 def helplines(mode):
1327 1322 if mode == MODE_PATCH:
1328 1323 help = """\
1329 1324 ?: help, k/up: line up, j/down: line down, v: stop viewing patch
1330 1325 pgup: prev page, space/pgdn: next page, c: commit, q: abort
1331 1326 """
1332 1327 else:
1333 1328 help = """\
1334 1329 ?: help, k/up: move up, j/down: move down, space: select, v: view patch
1335 1330 d: drop, e: edit, f: fold, m: mess, p: pick, r: roll
1336 1331 pgup/K: move patch up, pgdn/J: move patch down, c: commit, q: abort
1337 1332 """
1338 1333 return help.splitlines()
1339 1334
1340 1335 def renderhelp(win, state):
1341 1336 maxy, maxx = win.getmaxyx()
1342 1337 mode, _ = state['mode']
1343 1338 for y, line in enumerate(helplines(mode)):
1344 1339 if y >= maxy:
1345 1340 break
1346 1341 addln(win, y, 0, line, curses.color_pair(COLOR_HELP))
1347 1342 win.noutrefresh()
1348 1343
1349 1344 def renderrules(rulesscr, state):
1350 1345 rules = state['rules']
1351 1346 pos = state['pos']
1352 1347 selected = state['selected']
1353 1348 start = state['modes'][MODE_RULES]['line_offset']
1354 1349
1355 1350 conflicts = [r.ctx for r in rules if r.conflicts]
1356 1351 if len(conflicts) > 0:
1357 1352 line = "potential conflict in %s" % ','.join(map(str, conflicts))
1358 1353 addln(rulesscr, -1, 0, line, curses.color_pair(COLOR_WARN))
1359 1354
1360 1355 for y, rule in enumerate(rules[start:]):
1361 1356 if y >= state['page_height']:
1362 1357 break
1363 1358 if len(rule.conflicts) > 0:
1364 1359 rulesscr.addstr(y, 0, " ", curses.color_pair(COLOR_WARN))
1365 1360 else:
1366 1361 rulesscr.addstr(y, 0, " ", curses.COLOR_BLACK)
1367 1362 if y + start == selected:
1368 1363 addln(rulesscr, y, 2, rule, curses.color_pair(COLOR_SELECTED))
1369 1364 elif y + start == pos:
1370 1365 addln(rulesscr, y, 2, rule,
1371 1366 curses.color_pair(COLOR_CURRENT) | curses.A_BOLD)
1372 1367 else:
1373 1368 addln(rulesscr, y, 2, rule)
1374 1369 rulesscr.noutrefresh()
1375 1370
1376 1371 def renderstring(win, state, output, diffcolors=False):
1377 1372 maxy, maxx = win.getmaxyx()
1378 1373 length = min(maxy - 1, len(output))
1379 1374 for y in range(0, length):
1380 1375 line = output[y]
1381 1376 if diffcolors:
1382 1377 if line and line[0] == '+':
1383 1378 win.addstr(
1384 1379 y, 0, line, curses.color_pair(COLOR_DIFF_ADD_LINE))
1385 1380 elif line and line[0] == '-':
1386 1381 win.addstr(
1387 1382 y, 0, line, curses.color_pair(COLOR_DIFF_DEL_LINE))
1388 1383 elif line.startswith('@@ '):
1389 1384 win.addstr(
1390 1385 y, 0, line, curses.color_pair(COLOR_DIFF_OFFSET))
1391 1386 else:
1392 1387 win.addstr(y, 0, line)
1393 1388 else:
1394 1389 win.addstr(y, 0, line)
1395 1390 win.noutrefresh()
1396 1391
1397 1392 def renderpatch(win, state):
1398 1393 start = state['modes'][MODE_PATCH]['line_offset']
1399 1394 content = state['modes'][MODE_PATCH]['patchcontents']
1400 1395 renderstring(win, state, content[start:], diffcolors=True)
1401 1396
1402 1397 def layout(mode):
1403 1398 maxy, maxx = stdscr.getmaxyx()
1404 1399 helplen = len(helplines(mode))
1405 1400 return {
1406 1401 'commit': (12, maxx),
1407 1402 'help': (helplen, maxx),
1408 1403 'main': (maxy - helplen - 12, maxx),
1409 1404 }
1410 1405
1411 1406 def drawvertwin(size, y, x):
1412 1407 win = curses.newwin(size[0], size[1], y, x)
1413 1408 y += size[0]
1414 1409 return win, y, x
1415 1410
1416 1411 state = {
1417 1412 'pos': 0,
1418 1413 'rules': rules,
1419 1414 'selected': None,
1420 1415 'mode': (MODE_INIT, MODE_INIT),
1421 1416 'page_height': None,
1422 1417 'modes': {
1423 1418 MODE_RULES: {
1424 1419 'line_offset': 0,
1425 1420 },
1426 1421 MODE_PATCH: {
1427 1422 'line_offset': 0,
1428 1423 }
1429 1424 },
1430 1425 'repo': repo,
1431 1426 }
1432 1427
1433 1428 # eventloop
1434 1429 ch = None
1435 1430 stdscr.clear()
1436 1431 stdscr.refresh()
1437 1432 while True:
1438 1433 try:
1439 1434 oldmode, _ = state['mode']
1440 1435 if oldmode == MODE_INIT:
1441 1436 changemode(state, MODE_RULES)
1442 1437 e = event(state, ch)
1443 1438
1444 1439 if e == E_QUIT:
1445 1440 return False
1446 1441 if e == E_HISTEDIT:
1447 1442 return state['rules']
1448 1443 else:
1449 1444 if e == E_RESIZE:
1450 1445 size = screen_size()
1451 1446 if size != stdscr.getmaxyx():
1452 1447 curses.resizeterm(*size)
1453 1448
1454 1449 curmode, _ = state['mode']
1455 1450 sizes = layout(curmode)
1456 1451 if curmode != oldmode:
1457 1452 state['page_height'] = sizes['main'][0]
1458 1453 # Adjust the view to fit the current screen size.
1459 1454 movecursor(state, state['pos'], state['pos'])
1460 1455
1461 1456 # Pack the windows against the top, each pane spread across the
1462 1457 # full width of the screen.
1463 1458 y, x = (0, 0)
1464 1459 helpwin, y, x = drawvertwin(sizes['help'], y, x)
1465 1460 mainwin, y, x = drawvertwin(sizes['main'], y, x)
1466 1461 commitwin, y, x = drawvertwin(sizes['commit'], y, x)
1467 1462
1468 1463 if e in (E_PAGEDOWN, E_PAGEUP, E_LINEDOWN, E_LINEUP):
1469 1464 if e == E_PAGEDOWN:
1470 1465 changeview(state, +1, 'page')
1471 1466 elif e == E_PAGEUP:
1472 1467 changeview(state, -1, 'page')
1473 1468 elif e == E_LINEDOWN:
1474 1469 changeview(state, +1, 'line')
1475 1470 elif e == E_LINEUP:
1476 1471 changeview(state, -1, 'line')
1477 1472
1478 1473 # start rendering
1479 1474 commitwin.erase()
1480 1475 helpwin.erase()
1481 1476 mainwin.erase()
1482 1477 if curmode == MODE_PATCH:
1483 1478 renderpatch(mainwin, state)
1484 1479 elif curmode == MODE_HELP:
1485 1480 renderstring(mainwin, state, __doc__.strip().splitlines())
1486 1481 else:
1487 1482 renderrules(mainwin, state)
1488 1483 rendercommit(commitwin, state)
1489 1484 renderhelp(helpwin, state)
1490 1485 curses.doupdate()
1491 1486 # done rendering
1492 1487 ch = stdscr.getkey()
1493 1488 except curses.error:
1494 1489 pass
1495 1490
1496 1491 def _chistedit(ui, repo, *freeargs, **opts):
1497 1492 """interactively edit changeset history via a curses interface
1498 1493
1499 1494 Provides a ncurses interface to histedit. Press ? in chistedit mode
1500 1495 to see an extensive help. Requires python-curses to be installed."""
1501 1496
1502 1497 if curses is None:
1503 1498 raise error.Abort(_("Python curses library required"))
1504 1499
1505 1500 # disable color
1506 1501 ui._colormode = None
1507 1502
1508 1503 try:
1509 1504 keep = opts.get('keep')
1510 1505 revs = opts.get('rev', [])[:]
1511 1506 cmdutil.checkunfinished(repo)
1512 1507 cmdutil.bailifchanged(repo)
1513 1508
1514 1509 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1515 1510 raise error.Abort(_('history edit already in progress, try '
1516 1511 '--continue or --abort'))
1517 1512 revs.extend(freeargs)
1518 1513 if not revs:
1519 1514 defaultrev = destutil.desthistedit(ui, repo)
1520 1515 if defaultrev is not None:
1521 1516 revs.append(defaultrev)
1522 1517 if len(revs) != 1:
1523 1518 raise error.Abort(
1524 1519 _('histedit requires exactly one ancestor revision'))
1525 1520
1526 1521 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1527 1522 if len(rr) != 1:
1528 1523 raise error.Abort(_('The specified revisions must have '
1529 1524 'exactly one common root'))
1530 1525 root = rr[0].node()
1531 1526
1532 1527 topmost = repo.dirstate.p1()
1533 1528 revs = between(repo, root, topmost, keep)
1534 1529 if not revs:
1535 1530 raise error.Abort(_('%s is not an ancestor of working directory') %
1536 1531 node.short(root))
1537 1532
1538 1533 ctxs = []
1539 1534 for i, r in enumerate(revs):
1540 1535 ctxs.append(histeditrule(repo[r], i))
1536 # Curses requires setting the locale or it will default to the C
1537 # locale. This sets the locale to the user's default system
1538 # locale.
1539 locale.setlocale(locale.LC_ALL, r'')
1541 1540 rc = curses.wrapper(functools.partial(_chisteditmain, repo, ctxs))
1542 1541 curses.echo()
1543 1542 curses.endwin()
1544 1543 if rc is False:
1545 1544 ui.write(_("histedit aborted\n"))
1546 1545 return 0
1547 1546 if type(rc) is list:
1548 1547 ui.status(_("performing changes\n"))
1549 1548 rules = makecommands(rc)
1550 1549 filename = repo.vfs.join('chistedit')
1551 1550 with open(filename, 'w+') as fp:
1552 1551 for r in rules:
1553 1552 fp.write(r)
1554 1553 opts['commands'] = filename
1555 1554 return _texthistedit(ui, repo, *freeargs, **opts)
1556 1555 except KeyboardInterrupt:
1557 1556 pass
1558 1557 return -1
1559 1558
1560 1559 @command('histedit',
1561 1560 [('', 'commands', '',
1562 1561 _('read history edits from the specified file'), _('FILE')),
1563 1562 ('c', 'continue', False, _('continue an edit already in progress')),
1564 1563 ('', 'edit-plan', False, _('edit remaining actions list')),
1565 1564 ('k', 'keep', False,
1566 1565 _("don't strip old nodes after edit is complete")),
1567 1566 ('', 'abort', False, _('abort an edit in progress')),
1568 1567 ('o', 'outgoing', False, _('changesets not found in destination')),
1569 1568 ('f', 'force', False,
1570 1569 _('force outgoing even for unrelated repositories')),
1571 1570 ('r', 'rev', [], _('first revision to be edited'), _('REV'))] +
1572 1571 cmdutil.formatteropts,
1573 1572 _("[OPTIONS] ([ANCESTOR] | --outgoing [URL])"),
1574 1573 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT)
1575 1574 def histedit(ui, repo, *freeargs, **opts):
1576 1575 """interactively edit changeset history
1577 1576
1578 1577 This command lets you edit a linear series of changesets (up to
1579 1578 and including the working directory, which should be clean).
1580 1579 You can:
1581 1580
1582 1581 - `pick` to [re]order a changeset
1583 1582
1584 1583 - `drop` to omit changeset
1585 1584
1586 1585 - `mess` to reword the changeset commit message
1587 1586
1588 1587 - `fold` to combine it with the preceding changeset (using the later date)
1589 1588
1590 1589 - `roll` like fold, but discarding this commit's description and date
1591 1590
1592 1591 - `edit` to edit this changeset (preserving date)
1593 1592
1594 1593 - `base` to checkout changeset and apply further changesets from there
1595 1594
1596 1595 There are a number of ways to select the root changeset:
1597 1596
1598 1597 - Specify ANCESTOR directly
1599 1598
1600 1599 - Use --outgoing -- it will be the first linear changeset not
1601 1600 included in destination. (See :hg:`help config.paths.default-push`)
1602 1601
1603 1602 - Otherwise, the value from the "histedit.defaultrev" config option
1604 1603 is used as a revset to select the base revision when ANCESTOR is not
1605 1604 specified. The first revision returned by the revset is used. By
1606 1605 default, this selects the editable history that is unique to the
1607 1606 ancestry of the working directory.
1608 1607
1609 1608 .. container:: verbose
1610 1609
1611 1610 If you use --outgoing, this command will abort if there are ambiguous
1612 1611 outgoing revisions. For example, if there are multiple branches
1613 1612 containing outgoing revisions.
1614 1613
1615 1614 Use "min(outgoing() and ::.)" or similar revset specification
1616 1615 instead of --outgoing to specify edit target revision exactly in
1617 1616 such ambiguous situation. See :hg:`help revsets` for detail about
1618 1617 selecting revisions.
1619 1618
1620 1619 .. container:: verbose
1621 1620
1622 1621 Examples:
1623 1622
1624 1623 - A number of changes have been made.
1625 1624 Revision 3 is no longer needed.
1626 1625
1627 1626 Start history editing from revision 3::
1628 1627
1629 1628 hg histedit -r 3
1630 1629
1631 1630 An editor opens, containing the list of revisions,
1632 1631 with specific actions specified::
1633 1632
1634 1633 pick 5339bf82f0ca 3 Zworgle the foobar
1635 1634 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1636 1635 pick 0a9639fcda9d 5 Morgify the cromulancy
1637 1636
1638 1637 Additional information about the possible actions
1639 1638 to take appears below the list of revisions.
1640 1639
1641 1640 To remove revision 3 from the history,
1642 1641 its action (at the beginning of the relevant line)
1643 1642 is changed to 'drop'::
1644 1643
1645 1644 drop 5339bf82f0ca 3 Zworgle the foobar
1646 1645 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1647 1646 pick 0a9639fcda9d 5 Morgify the cromulancy
1648 1647
1649 1648 - A number of changes have been made.
1650 1649 Revision 2 and 4 need to be swapped.
1651 1650
1652 1651 Start history editing from revision 2::
1653 1652
1654 1653 hg histedit -r 2
1655 1654
1656 1655 An editor opens, containing the list of revisions,
1657 1656 with specific actions specified::
1658 1657
1659 1658 pick 252a1af424ad 2 Blorb a morgwazzle
1660 1659 pick 5339bf82f0ca 3 Zworgle the foobar
1661 1660 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1662 1661
1663 1662 To swap revision 2 and 4, its lines are swapped
1664 1663 in the editor::
1665 1664
1666 1665 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1667 1666 pick 5339bf82f0ca 3 Zworgle the foobar
1668 1667 pick 252a1af424ad 2 Blorb a morgwazzle
1669 1668
1670 1669 Returns 0 on success, 1 if user intervention is required (not only
1671 1670 for intentional "edit" command, but also for resolving unexpected
1672 1671 conflicts).
1673 1672 """
1674 1673 # kludge: _chistedit only works for starting an edit, not aborting
1675 1674 # or continuing, so fall back to regular _texthistedit for those
1676 1675 # operations.
1677 1676 if ui.interface('histedit') == 'curses' and _getgoal(
1678 1677 pycompat.byteskwargs(opts)) == goalnew:
1679 1678 return _chistedit(ui, repo, *freeargs, **opts)
1680 1679 return _texthistedit(ui, repo, *freeargs, **opts)
1681 1680
1682 1681 def _texthistedit(ui, repo, *freeargs, **opts):
1683 1682 state = histeditstate(repo)
1684 1683 with repo.wlock() as wlock, repo.lock() as lock:
1685 1684 state.wlock = wlock
1686 1685 state.lock = lock
1687 1686 _histedit(ui, repo, state, *freeargs, **opts)
1688 1687
1689 1688 goalcontinue = 'continue'
1690 1689 goalabort = 'abort'
1691 1690 goaleditplan = 'edit-plan'
1692 1691 goalnew = 'new'
1693 1692
1694 1693 def _getgoal(opts):
1695 1694 if opts.get(b'continue'):
1696 1695 return goalcontinue
1697 1696 if opts.get(b'abort'):
1698 1697 return goalabort
1699 1698 if opts.get(b'edit_plan'):
1700 1699 return goaleditplan
1701 1700 return goalnew
1702 1701
1703 1702 def _readfile(ui, path):
1704 1703 if path == '-':
1705 1704 with ui.timeblockedsection('histedit'):
1706 1705 return ui.fin.read()
1707 1706 else:
1708 1707 with open(path, 'rb') as f:
1709 1708 return f.read()
1710 1709
1711 1710 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1712 1711 # TODO only abort if we try to histedit mq patches, not just
1713 1712 # blanket if mq patches are applied somewhere
1714 1713 mq = getattr(repo, 'mq', None)
1715 1714 if mq and mq.applied:
1716 1715 raise error.Abort(_('source has mq patches applied'))
1717 1716
1718 1717 # basic argument incompatibility processing
1719 1718 outg = opts.get('outgoing')
1720 1719 editplan = opts.get('edit_plan')
1721 1720 abort = opts.get('abort')
1722 1721 force = opts.get('force')
1723 1722 if force and not outg:
1724 1723 raise error.Abort(_('--force only allowed with --outgoing'))
1725 1724 if goal == 'continue':
1726 1725 if any((outg, abort, revs, freeargs, rules, editplan)):
1727 1726 raise error.Abort(_('no arguments allowed with --continue'))
1728 1727 elif goal == 'abort':
1729 1728 if any((outg, revs, freeargs, rules, editplan)):
1730 1729 raise error.Abort(_('no arguments allowed with --abort'))
1731 1730 elif goal == 'edit-plan':
1732 1731 if any((outg, revs, freeargs)):
1733 1732 raise error.Abort(_('only --commands argument allowed with '
1734 1733 '--edit-plan'))
1735 1734 else:
1736 1735 if state.inprogress():
1737 1736 raise error.Abort(_('history edit already in progress, try '
1738 1737 '--continue or --abort'))
1739 1738 if outg:
1740 1739 if revs:
1741 1740 raise error.Abort(_('no revisions allowed with --outgoing'))
1742 1741 if len(freeargs) > 1:
1743 1742 raise error.Abort(
1744 1743 _('only one repo argument allowed with --outgoing'))
1745 1744 else:
1746 1745 revs.extend(freeargs)
1747 1746 if len(revs) == 0:
1748 1747 defaultrev = destutil.desthistedit(ui, repo)
1749 1748 if defaultrev is not None:
1750 1749 revs.append(defaultrev)
1751 1750
1752 1751 if len(revs) != 1:
1753 1752 raise error.Abort(
1754 1753 _('histedit requires exactly one ancestor revision'))
1755 1754
1756 1755 def _histedit(ui, repo, state, *freeargs, **opts):
1757 1756 opts = pycompat.byteskwargs(opts)
1758 1757 fm = ui.formatter('histedit', opts)
1759 1758 fm.startitem()
1760 1759 goal = _getgoal(opts)
1761 1760 revs = opts.get('rev', [])
1762 1761 nobackup = not ui.configbool('rewrite', 'backup-bundle')
1763 1762 rules = opts.get('commands', '')
1764 1763 state.keep = opts.get('keep', False)
1765 1764
1766 1765 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1767 1766
1768 1767 hastags = False
1769 1768 if revs:
1770 1769 revs = scmutil.revrange(repo, revs)
1771 1770 ctxs = [repo[rev] for rev in revs]
1772 1771 for ctx in ctxs:
1773 1772 tags = [tag for tag in ctx.tags() if tag != 'tip']
1774 1773 if not hastags:
1775 1774 hastags = len(tags)
1776 1775 if hastags:
1777 1776 if ui.promptchoice(_('warning: tags associated with the given'
1778 1777 ' changeset will be lost after histedit.\n'
1779 1778 'do you want to continue (yN)? $$ &Yes $$ &No'),
1780 1779 default=1):
1781 1780 raise error.Abort(_('histedit cancelled\n'))
1782 1781 # rebuild state
1783 1782 if goal == goalcontinue:
1784 1783 state.read()
1785 1784 state = bootstrapcontinue(ui, state, opts)
1786 1785 elif goal == goaleditplan:
1787 1786 _edithisteditplan(ui, repo, state, rules)
1788 1787 return
1789 1788 elif goal == goalabort:
1790 1789 _aborthistedit(ui, repo, state, nobackup=nobackup)
1791 1790 return
1792 1791 else:
1793 1792 # goal == goalnew
1794 1793 _newhistedit(ui, repo, state, revs, freeargs, opts)
1795 1794
1796 1795 _continuehistedit(ui, repo, state)
1797 1796 _finishhistedit(ui, repo, state, fm)
1798 1797 fm.end()
1799 1798
1800 1799 def _continuehistedit(ui, repo, state):
1801 1800 """This function runs after either:
1802 1801 - bootstrapcontinue (if the goal is 'continue')
1803 1802 - _newhistedit (if the goal is 'new')
1804 1803 """
1805 1804 # preprocess rules so that we can hide inner folds from the user
1806 1805 # and only show one editor
1807 1806 actions = state.actions[:]
1808 1807 for idx, (action, nextact) in enumerate(
1809 1808 zip(actions, actions[1:] + [None])):
1810 1809 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1811 1810 state.actions[idx].__class__ = _multifold
1812 1811
1813 1812 # Force an initial state file write, so the user can run --abort/continue
1814 1813 # even if there's an exception before the first transaction serialize.
1815 1814 state.write()
1816 1815
1817 1816 tr = None
1818 1817 # Don't use singletransaction by default since it rolls the entire
1819 1818 # transaction back if an unexpected exception happens (like a
1820 1819 # pretxncommit hook throws, or the user aborts the commit msg editor).
1821 1820 if ui.configbool("histedit", "singletransaction"):
1822 1821 # Don't use a 'with' for the transaction, since actions may close
1823 1822 # and reopen a transaction. For example, if the action executes an
1824 1823 # external process it may choose to commit the transaction first.
1825 1824 tr = repo.transaction('histedit')
1826 1825 progress = ui.makeprogress(_("editing"), unit=_('changes'),
1827 1826 total=len(state.actions))
1828 1827 with progress, util.acceptintervention(tr):
1829 1828 while state.actions:
1830 1829 state.write(tr=tr)
1831 1830 actobj = state.actions[0]
1832 1831 progress.increment(item=actobj.torule())
1833 1832 ui.debug('histedit: processing %s %s\n' % (actobj.verb,
1834 1833 actobj.torule()))
1835 1834 parentctx, replacement_ = actobj.run()
1836 1835 state.parentctxnode = parentctx.node()
1837 1836 state.replacements.extend(replacement_)
1838 1837 state.actions.pop(0)
1839 1838
1840 1839 state.write()
1841 1840
1842 1841 def _finishhistedit(ui, repo, state, fm):
1843 1842 """This action runs when histedit is finishing its session"""
1844 1843 hg.updaterepo(repo, state.parentctxnode, overwrite=False)
1845 1844
1846 1845 mapping, tmpnodes, created, ntm = processreplacement(state)
1847 1846 if mapping:
1848 1847 for prec, succs in mapping.iteritems():
1849 1848 if not succs:
1850 1849 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1851 1850 else:
1852 1851 ui.debug('histedit: %s is replaced by %s\n' % (
1853 1852 node.short(prec), node.short(succs[0])))
1854 1853 if len(succs) > 1:
1855 1854 m = 'histedit: %s'
1856 1855 for n in succs[1:]:
1857 1856 ui.debug(m % node.short(n))
1858 1857
1859 1858 if not state.keep:
1860 1859 if mapping:
1861 1860 movetopmostbookmarks(repo, state.topmost, ntm)
1862 1861 # TODO update mq state
1863 1862 else:
1864 1863 mapping = {}
1865 1864
1866 1865 for n in tmpnodes:
1867 1866 if n in repo:
1868 1867 mapping[n] = ()
1869 1868
1870 1869 # remove entries about unknown nodes
1871 1870 nodemap = repo.unfiltered().changelog.nodemap
1872 1871 mapping = {k: v for k, v in mapping.items()
1873 1872 if k in nodemap and all(n in nodemap for n in v)}
1874 1873 scmutil.cleanupnodes(repo, mapping, 'histedit')
1875 1874 hf = fm.hexfunc
1876 1875 fl = fm.formatlist
1877 1876 fd = fm.formatdict
1878 1877 nodechanges = fd({hf(oldn): fl([hf(n) for n in newn], name='node')
1879 1878 for oldn, newn in mapping.iteritems()},
1880 1879 key="oldnode", value="newnodes")
1881 1880 fm.data(nodechanges=nodechanges)
1882 1881
1883 1882 state.clear()
1884 1883 if os.path.exists(repo.sjoin('undo')):
1885 1884 os.unlink(repo.sjoin('undo'))
1886 1885 if repo.vfs.exists('histedit-last-edit.txt'):
1887 1886 repo.vfs.unlink('histedit-last-edit.txt')
1888 1887
1889 1888 def _aborthistedit(ui, repo, state, nobackup=False):
1890 1889 try:
1891 1890 state.read()
1892 1891 __, leafs, tmpnodes, __ = processreplacement(state)
1893 1892 ui.debug('restore wc to old parent %s\n'
1894 1893 % node.short(state.topmost))
1895 1894
1896 1895 # Recover our old commits if necessary
1897 1896 if not state.topmost in repo and state.backupfile:
1898 1897 backupfile = repo.vfs.join(state.backupfile)
1899 1898 f = hg.openpath(ui, backupfile)
1900 1899 gen = exchange.readbundle(ui, f, backupfile)
1901 1900 with repo.transaction('histedit.abort') as tr:
1902 1901 bundle2.applybundle(repo, gen, tr, source='histedit',
1903 1902 url='bundle:' + backupfile)
1904 1903
1905 1904 os.remove(backupfile)
1906 1905
1907 1906 # check whether we should update away
1908 1907 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1909 1908 state.parentctxnode, leafs | tmpnodes):
1910 1909 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1911 1910 cleanupnode(ui, repo, tmpnodes, nobackup=nobackup)
1912 1911 cleanupnode(ui, repo, leafs, nobackup=nobackup)
1913 1912 except Exception:
1914 1913 if state.inprogress():
1915 1914 ui.warn(_('warning: encountered an exception during histedit '
1916 1915 '--abort; the repository may not have been completely '
1917 1916 'cleaned up\n'))
1918 1917 raise
1919 1918 finally:
1920 1919 state.clear()
1921 1920
1922 1921 def hgaborthistedit(ui, repo):
1923 1922 state = histeditstate(repo)
1924 1923 nobackup = not ui.configbool('rewrite', 'backup-bundle')
1925 1924 with repo.wlock() as wlock, repo.lock() as lock:
1926 1925 state.wlock = wlock
1927 1926 state.lock = lock
1928 1927 _aborthistedit(ui, repo, state, nobackup=nobackup)
1929 1928
1930 1929 def _edithisteditplan(ui, repo, state, rules):
1931 1930 state.read()
1932 1931 if not rules:
1933 1932 comment = geteditcomment(ui,
1934 1933 node.short(state.parentctxnode),
1935 1934 node.short(state.topmost))
1936 1935 rules = ruleeditor(repo, ui, state.actions, comment)
1937 1936 else:
1938 1937 rules = _readfile(ui, rules)
1939 1938 actions = parserules(rules, state)
1940 1939 ctxs = [repo[act.node]
1941 1940 for act in state.actions if act.node]
1942 1941 warnverifyactions(ui, repo, actions, state, ctxs)
1943 1942 state.actions = actions
1944 1943 state.write()
1945 1944
1946 1945 def _newhistedit(ui, repo, state, revs, freeargs, opts):
1947 1946 outg = opts.get('outgoing')
1948 1947 rules = opts.get('commands', '')
1949 1948 force = opts.get('force')
1950 1949
1951 1950 cmdutil.checkunfinished(repo)
1952 1951 cmdutil.bailifchanged(repo)
1953 1952
1954 1953 topmost = repo.dirstate.p1()
1955 1954 if outg:
1956 1955 if freeargs:
1957 1956 remote = freeargs[0]
1958 1957 else:
1959 1958 remote = None
1960 1959 root = findoutgoing(ui, repo, remote, force, opts)
1961 1960 else:
1962 1961 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1963 1962 if len(rr) != 1:
1964 1963 raise error.Abort(_('The specified revisions must have '
1965 1964 'exactly one common root'))
1966 1965 root = rr[0].node()
1967 1966
1968 1967 revs = between(repo, root, topmost, state.keep)
1969 1968 if not revs:
1970 1969 raise error.Abort(_('%s is not an ancestor of working directory') %
1971 1970 node.short(root))
1972 1971
1973 1972 ctxs = [repo[r] for r in revs]
1974 1973 if not rules:
1975 1974 comment = geteditcomment(ui, node.short(root), node.short(topmost))
1976 1975 actions = [pick(state, r) for r in revs]
1977 1976 rules = ruleeditor(repo, ui, actions, comment)
1978 1977 else:
1979 1978 rules = _readfile(ui, rules)
1980 1979 actions = parserules(rules, state)
1981 1980 warnverifyactions(ui, repo, actions, state, ctxs)
1982 1981
1983 1982 parentctxnode = repo[root].p1().node()
1984 1983
1985 1984 state.parentctxnode = parentctxnode
1986 1985 state.actions = actions
1987 1986 state.topmost = topmost
1988 1987 state.replacements = []
1989 1988
1990 1989 ui.log("histedit", "%d actions to histedit\n", len(actions),
1991 1990 histedit_num_actions=len(actions))
1992 1991
1993 1992 # Create a backup so we can always abort completely.
1994 1993 backupfile = None
1995 1994 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1996 1995 backupfile = repair.backupbundle(repo, [parentctxnode],
1997 1996 [topmost], root, 'histedit')
1998 1997 state.backupfile = backupfile
1999 1998
2000 1999 def _getsummary(ctx):
2001 2000 # a common pattern is to extract the summary but default to the empty
2002 2001 # string
2003 2002 summary = ctx.description() or ''
2004 2003 if summary:
2005 2004 summary = summary.splitlines()[0]
2006 2005 return summary
2007 2006
2008 2007 def bootstrapcontinue(ui, state, opts):
2009 2008 repo = state.repo
2010 2009
2011 2010 ms = mergemod.mergestate.read(repo)
2012 2011 mergeutil.checkunresolved(ms)
2013 2012
2014 2013 if state.actions:
2015 2014 actobj = state.actions.pop(0)
2016 2015
2017 2016 if _isdirtywc(repo):
2018 2017 actobj.continuedirty()
2019 2018 if _isdirtywc(repo):
2020 2019 abortdirty()
2021 2020
2022 2021 parentctx, replacements = actobj.continueclean()
2023 2022
2024 2023 state.parentctxnode = parentctx.node()
2025 2024 state.replacements.extend(replacements)
2026 2025
2027 2026 return state
2028 2027
2029 2028 def between(repo, old, new, keep):
2030 2029 """select and validate the set of revision to edit
2031 2030
2032 2031 When keep is false, the specified set can't have children."""
2033 2032 revs = repo.revs('%n::%n', old, new)
2034 2033 if revs and not keep:
2035 2034 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
2036 2035 repo.revs('(%ld::) - (%ld)', revs, revs)):
2037 2036 raise error.Abort(_('can only histedit a changeset together '
2038 2037 'with all its descendants'))
2039 2038 if repo.revs('(%ld) and merge()', revs):
2040 2039 raise error.Abort(_('cannot edit history that contains merges'))
2041 2040 root = repo[revs.first()] # list is already sorted by repo.revs()
2042 2041 if not root.mutable():
2043 2042 raise error.Abort(_('cannot edit public changeset: %s') % root,
2044 2043 hint=_("see 'hg help phases' for details"))
2045 2044 return pycompat.maplist(repo.changelog.node, revs)
2046 2045
2047 2046 def ruleeditor(repo, ui, actions, editcomment=""):
2048 2047 """open an editor to edit rules
2049 2048
2050 2049 rules are in the format [ [act, ctx], ...] like in state.rules
2051 2050 """
2052 2051 if repo.ui.configbool("experimental", "histedit.autoverb"):
2053 2052 newact = util.sortdict()
2054 2053 for act in actions:
2055 2054 ctx = repo[act.node]
2056 2055 summary = _getsummary(ctx)
2057 2056 fword = summary.split(' ', 1)[0].lower()
2058 2057 added = False
2059 2058
2060 2059 # if it doesn't end with the special character '!' just skip this
2061 2060 if fword.endswith('!'):
2062 2061 fword = fword[:-1]
2063 2062 if fword in primaryactions | secondaryactions | tertiaryactions:
2064 2063 act.verb = fword
2065 2064 # get the target summary
2066 2065 tsum = summary[len(fword) + 1:].lstrip()
2067 2066 # safe but slow: reverse iterate over the actions so we
2068 2067 # don't clash on two commits having the same summary
2069 2068 for na, l in reversed(list(newact.iteritems())):
2070 2069 actx = repo[na.node]
2071 2070 asum = _getsummary(actx)
2072 2071 if asum == tsum:
2073 2072 added = True
2074 2073 l.append(act)
2075 2074 break
2076 2075
2077 2076 if not added:
2078 2077 newact[act] = []
2079 2078
2080 2079 # copy over and flatten the new list
2081 2080 actions = []
2082 2081 for na, l in newact.iteritems():
2083 2082 actions.append(na)
2084 2083 actions += l
2085 2084
2086 2085 rules = '\n'.join([act.torule() for act in actions])
2087 2086 rules += '\n\n'
2088 2087 rules += editcomment
2089 2088 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'},
2090 2089 repopath=repo.path, action='histedit')
2091 2090
2092 2091 # Save edit rules in .hg/histedit-last-edit.txt in case
2093 2092 # the user needs to ask for help after something
2094 2093 # surprising happens.
2095 2094 with repo.vfs('histedit-last-edit.txt', 'wb') as f:
2096 2095 f.write(rules)
2097 2096
2098 2097 return rules
2099 2098
2100 2099 def parserules(rules, state):
2101 2100 """Read the histedit rules string and return list of action objects """
2102 2101 rules = [l for l in (r.strip() for r in rules.splitlines())
2103 2102 if l and not l.startswith('#')]
2104 2103 actions = []
2105 2104 for r in rules:
2106 2105 if ' ' not in r:
2107 2106 raise error.ParseError(_('malformed line "%s"') % r)
2108 2107 verb, rest = r.split(' ', 1)
2109 2108
2110 2109 if verb not in actiontable:
2111 2110 raise error.ParseError(_('unknown action "%s"') % verb)
2112 2111
2113 2112 action = actiontable[verb].fromrule(state, rest)
2114 2113 actions.append(action)
2115 2114 return actions
2116 2115
2117 2116 def warnverifyactions(ui, repo, actions, state, ctxs):
2118 2117 try:
2119 2118 verifyactions(actions, state, ctxs)
2120 2119 except error.ParseError:
2121 2120 if repo.vfs.exists('histedit-last-edit.txt'):
2122 2121 ui.warn(_('warning: histedit rules saved '
2123 2122 'to: .hg/histedit-last-edit.txt\n'))
2124 2123 raise
2125 2124
2126 2125 def verifyactions(actions, state, ctxs):
2127 2126 """Verify that there exists exactly one action per given changeset and
2128 2127 other constraints.
2129 2128
2130 2129 Will abort if there are to many or too few rules, a malformed rule,
2131 2130 or a rule on a changeset outside of the user-given range.
2132 2131 """
2133 2132 expected = set(c.node() for c in ctxs)
2134 2133 seen = set()
2135 2134 prev = None
2136 2135
2137 2136 if actions and actions[0].verb in ['roll', 'fold']:
2138 2137 raise error.ParseError(_('first changeset cannot use verb "%s"') %
2139 2138 actions[0].verb)
2140 2139
2141 2140 for action in actions:
2142 2141 action.verify(prev, expected, seen)
2143 2142 prev = action
2144 2143 if action.node is not None:
2145 2144 seen.add(action.node)
2146 2145 missing = sorted(expected - seen) # sort to stabilize output
2147 2146
2148 2147 if state.repo.ui.configbool('histedit', 'dropmissing'):
2149 2148 if len(actions) == 0:
2150 2149 raise error.ParseError(_('no rules provided'),
2151 2150 hint=_('use strip extension to remove commits'))
2152 2151
2153 2152 drops = [drop(state, n) for n in missing]
2154 2153 # put the in the beginning so they execute immediately and
2155 2154 # don't show in the edit-plan in the future
2156 2155 actions[:0] = drops
2157 2156 elif missing:
2158 2157 raise error.ParseError(_('missing rules for changeset %s') %
2159 2158 node.short(missing[0]),
2160 2159 hint=_('use "drop %s" to discard, see also: '
2161 2160 "'hg help -e histedit.config'")
2162 2161 % node.short(missing[0]))
2163 2162
2164 2163 def adjustreplacementsfrommarkers(repo, oldreplacements):
2165 2164 """Adjust replacements from obsolescence markers
2166 2165
2167 2166 Replacements structure is originally generated based on
2168 2167 histedit's state and does not account for changes that are
2169 2168 not recorded there. This function fixes that by adding
2170 2169 data read from obsolescence markers"""
2171 2170 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
2172 2171 return oldreplacements
2173 2172
2174 2173 unfi = repo.unfiltered()
2175 2174 nm = unfi.changelog.nodemap
2176 2175 obsstore = repo.obsstore
2177 2176 newreplacements = list(oldreplacements)
2178 2177 oldsuccs = [r[1] for r in oldreplacements]
2179 2178 # successors that have already been added to succstocheck once
2180 2179 seensuccs = set().union(*oldsuccs) # create a set from an iterable of tuples
2181 2180 succstocheck = list(seensuccs)
2182 2181 while succstocheck:
2183 2182 n = succstocheck.pop()
2184 2183 missing = nm.get(n) is None
2185 2184 markers = obsstore.successors.get(n, ())
2186 2185 if missing and not markers:
2187 2186 # dead end, mark it as such
2188 2187 newreplacements.append((n, ()))
2189 2188 for marker in markers:
2190 2189 nsuccs = marker[1]
2191 2190 newreplacements.append((n, nsuccs))
2192 2191 for nsucc in nsuccs:
2193 2192 if nsucc not in seensuccs:
2194 2193 seensuccs.add(nsucc)
2195 2194 succstocheck.append(nsucc)
2196 2195
2197 2196 return newreplacements
2198 2197
2199 2198 def processreplacement(state):
2200 2199 """process the list of replacements to return
2201 2200
2202 2201 1) the final mapping between original and created nodes
2203 2202 2) the list of temporary node created by histedit
2204 2203 3) the list of new commit created by histedit"""
2205 2204 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
2206 2205 allsuccs = set()
2207 2206 replaced = set()
2208 2207 fullmapping = {}
2209 2208 # initialize basic set
2210 2209 # fullmapping records all operations recorded in replacement
2211 2210 for rep in replacements:
2212 2211 allsuccs.update(rep[1])
2213 2212 replaced.add(rep[0])
2214 2213 fullmapping.setdefault(rep[0], set()).update(rep[1])
2215 2214 new = allsuccs - replaced
2216 2215 tmpnodes = allsuccs & replaced
2217 2216 # Reduce content fullmapping into direct relation between original nodes
2218 2217 # and final node created during history edition
2219 2218 # Dropped changeset are replaced by an empty list
2220 2219 toproceed = set(fullmapping)
2221 2220 final = {}
2222 2221 while toproceed:
2223 2222 for x in list(toproceed):
2224 2223 succs = fullmapping[x]
2225 2224 for s in list(succs):
2226 2225 if s in toproceed:
2227 2226 # non final node with unknown closure
2228 2227 # We can't process this now
2229 2228 break
2230 2229 elif s in final:
2231 2230 # non final node, replace with closure
2232 2231 succs.remove(s)
2233 2232 succs.update(final[s])
2234 2233 else:
2235 2234 final[x] = succs
2236 2235 toproceed.remove(x)
2237 2236 # remove tmpnodes from final mapping
2238 2237 for n in tmpnodes:
2239 2238 del final[n]
2240 2239 # we expect all changes involved in final to exist in the repo
2241 2240 # turn `final` into list (topologically sorted)
2242 2241 nm = state.repo.changelog.nodemap
2243 2242 for prec, succs in final.items():
2244 2243 final[prec] = sorted(succs, key=nm.get)
2245 2244
2246 2245 # computed topmost element (necessary for bookmark)
2247 2246 if new:
2248 2247 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
2249 2248 elif not final:
2250 2249 # Nothing rewritten at all. we won't need `newtopmost`
2251 2250 # It is the same as `oldtopmost` and `processreplacement` know it
2252 2251 newtopmost = None
2253 2252 else:
2254 2253 # every body died. The newtopmost is the parent of the root.
2255 2254 r = state.repo.changelog.rev
2256 2255 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
2257 2256
2258 2257 return final, tmpnodes, new, newtopmost
2259 2258
2260 2259 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
2261 2260 """Move bookmark from oldtopmost to newly created topmost
2262 2261
2263 2262 This is arguably a feature and we may only want that for the active
2264 2263 bookmark. But the behavior is kept compatible with the old version for now.
2265 2264 """
2266 2265 if not oldtopmost or not newtopmost:
2267 2266 return
2268 2267 oldbmarks = repo.nodebookmarks(oldtopmost)
2269 2268 if oldbmarks:
2270 2269 with repo.lock(), repo.transaction('histedit') as tr:
2271 2270 marks = repo._bookmarks
2272 2271 changes = []
2273 2272 for name in oldbmarks:
2274 2273 changes.append((name, newtopmost))
2275 2274 marks.applychanges(repo, tr, changes)
2276 2275
2277 2276 def cleanupnode(ui, repo, nodes, nobackup=False):
2278 2277 """strip a group of nodes from the repository
2279 2278
2280 2279 The set of node to strip may contains unknown nodes."""
2281 2280 with repo.lock():
2282 2281 # do not let filtering get in the way of the cleanse
2283 2282 # we should probably get rid of obsolescence marker created during the
2284 2283 # histedit, but we currently do not have such information.
2285 2284 repo = repo.unfiltered()
2286 2285 # Find all nodes that need to be stripped
2287 2286 # (we use %lr instead of %ln to silently ignore unknown items)
2288 2287 nm = repo.changelog.nodemap
2289 2288 nodes = sorted(n for n in nodes if n in nm)
2290 2289 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
2291 2290 if roots:
2292 2291 backup = not nobackup
2293 2292 repair.strip(ui, repo, roots, backup=backup)
2294 2293
2295 2294 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
2296 2295 if isinstance(nodelist, str):
2297 2296 nodelist = [nodelist]
2298 2297 state = histeditstate(repo)
2299 2298 if state.inprogress():
2300 2299 state.read()
2301 2300 histedit_nodes = {action.node for action
2302 2301 in state.actions if action.node}
2303 2302 common_nodes = histedit_nodes & set(nodelist)
2304 2303 if common_nodes:
2305 2304 raise error.Abort(_("histedit in progress, can't strip %s")
2306 2305 % ', '.join(node.short(x) for x in common_nodes))
2307 2306 return orig(ui, repo, nodelist, *args, **kwargs)
2308 2307
2309 2308 extensions.wrapfunction(repair, 'strip', stripwrapper)
2310 2309
2311 2310 def summaryhook(ui, repo):
2312 2311 state = histeditstate(repo)
2313 2312 if not state.inprogress():
2314 2313 return
2315 2314 state.read()
2316 2315 if state.actions:
2317 2316 # i18n: column positioning for "hg summary"
2318 2317 ui.write(_('hist: %s (histedit --continue)\n') %
2319 2318 (ui.label(_('%d remaining'), 'histedit.remaining') %
2320 2319 len(state.actions)))
2321 2320
2322 2321 def extsetup(ui):
2323 2322 cmdutil.summaryhooks.add('histedit', summaryhook)
2324 2323 statemod.addunfinished('histedit', fname='histedit-state', allowcommit=True,
2325 2324 continueflag=True, abortfunc=hgaborthistedit)
2326
@@ -1,1916 +1,1915 b''
1 1 # stuff related specifically to patch manipulation / parsing
2 2 #
3 3 # Copyright 2008 Mark Edgington <edgimar@gmail.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 #
8 8 # This code is based on the Mark Edgington's crecord extension.
9 9 # (Itself based on Bryan O'Sullivan's record extension.)
10 10
11 11 from __future__ import absolute_import
12 12
13 13 import locale
14 14 import os
15 15 import re
16 16 import signal
17 17
18 18 from .i18n import _
19 19 from . import (
20 20 encoding,
21 21 error,
22 22 patch as patchmod,
23 23 pycompat,
24 24 scmutil,
25 25 util,
26 26 )
27 27 from .utils import (
28 28 stringutil,
29 29 )
30 30 stringio = util.stringio
31 31
32 # This is required for ncurses to display non-ASCII characters in default user
33 # locale encoding correctly. --immerrr
34 locale.setlocale(locale.LC_ALL, r'')
35
36 32 # patch comments based on the git one
37 33 diffhelptext = _("""# To remove '-' lines, make them ' ' lines (context).
38 34 # To remove '+' lines, delete them.
39 35 # Lines starting with # will be removed from the patch.
40 36 """)
41 37
42 38 hunkhelptext = _("""#
43 39 # If the patch applies cleanly, the edited hunk will immediately be
44 40 # added to the record list. If it does not apply cleanly, a rejects file
45 41 # will be generated. You can use that when you try again. If all lines
46 42 # of the hunk are removed, then the edit is aborted and the hunk is left
47 43 # unchanged.
48 44 """)
49 45
50 46 patchhelptext = _("""#
51 47 # If the patch applies cleanly, the edited patch will immediately
52 48 # be finalised. If it does not apply cleanly, rejects files will be
53 49 # generated. You can use those when you try again.
54 50 """)
55 51
56 52 try:
57 53 import curses
58 54 curses.error
59 55 except ImportError:
60 56 # I have no idea if wcurses works with crecord...
61 57 try:
62 58 import wcurses as curses
63 59 curses.error
64 60 except ImportError:
65 61 # wcurses is not shipped on Windows by default, or python is not
66 62 # compiled with curses
67 63 curses = False
68 64
69 65 class fallbackerror(error.Abort):
70 66 """Error that indicates the client should try to fallback to text mode."""
71 67 # Inherits from error.Abort so that existing behavior is preserved if the
72 68 # calling code does not know how to fallback.
73 69
74 70 def checkcurses(ui):
75 71 """Return True if the user wants to use curses
76 72
77 73 This method returns True if curses is found (and that python is built with
78 74 it) and that the user has the correct flag for the ui.
79 75 """
80 76 return curses and ui.interface("chunkselector") == "curses"
81 77
82 78 class patchnode(object):
83 79 """abstract class for patch graph nodes
84 80 (i.e. patchroot, header, hunk, hunkline)
85 81 """
86 82
87 83 def firstchild(self):
88 84 raise NotImplementedError("method must be implemented by subclass")
89 85
90 86 def lastchild(self):
91 87 raise NotImplementedError("method must be implemented by subclass")
92 88
93 89 def allchildren(self):
94 90 "Return a list of all of the direct children of this node"
95 91 raise NotImplementedError("method must be implemented by subclass")
96 92
97 93 def nextsibling(self):
98 94 """
99 95 Return the closest next item of the same type where there are no items
100 96 of different types between the current item and this closest item.
101 97 If no such item exists, return None.
102 98 """
103 99 raise NotImplementedError("method must be implemented by subclass")
104 100
105 101 def prevsibling(self):
106 102 """
107 103 Return the closest previous item of the same type where there are no
108 104 items of different types between the current item and this closest item.
109 105 If no such item exists, return None.
110 106 """
111 107 raise NotImplementedError("method must be implemented by subclass")
112 108
113 109 def parentitem(self):
114 110 raise NotImplementedError("method must be implemented by subclass")
115 111
116 112 def nextitem(self, skipfolded=True):
117 113 """
118 114 Try to return the next item closest to this item, regardless of item's
119 115 type (header, hunk, or hunkline).
120 116
121 117 If skipfolded == True, and the current item is folded, then the child
122 118 items that are hidden due to folding will be skipped when determining
123 119 the next item.
124 120
125 121 If it is not possible to get the next item, return None.
126 122 """
127 123 try:
128 124 itemfolded = self.folded
129 125 except AttributeError:
130 126 itemfolded = False
131 127 if skipfolded and itemfolded:
132 128 nextitem = self.nextsibling()
133 129 if nextitem is None:
134 130 try:
135 131 nextitem = self.parentitem().nextsibling()
136 132 except AttributeError:
137 133 nextitem = None
138 134 return nextitem
139 135 else:
140 136 # try child
141 137 item = self.firstchild()
142 138 if item is not None:
143 139 return item
144 140
145 141 # else try next sibling
146 142 item = self.nextsibling()
147 143 if item is not None:
148 144 return item
149 145
150 146 try:
151 147 # else try parent's next sibling
152 148 item = self.parentitem().nextsibling()
153 149 if item is not None:
154 150 return item
155 151
156 152 # else return grandparent's next sibling (or None)
157 153 return self.parentitem().parentitem().nextsibling()
158 154
159 155 except AttributeError: # parent and/or grandparent was None
160 156 return None
161 157
162 158 def previtem(self):
163 159 """
164 160 Try to return the previous item closest to this item, regardless of
165 161 item's type (header, hunk, or hunkline).
166 162
167 163 If it is not possible to get the previous item, return None.
168 164 """
169 165 # try previous sibling's last child's last child,
170 166 # else try previous sibling's last child, else try previous sibling
171 167 prevsibling = self.prevsibling()
172 168 if prevsibling is not None:
173 169 prevsiblinglastchild = prevsibling.lastchild()
174 170 if ((prevsiblinglastchild is not None) and
175 171 not prevsibling.folded):
176 172 prevsiblinglclc = prevsiblinglastchild.lastchild()
177 173 if ((prevsiblinglclc is not None) and
178 174 not prevsiblinglastchild.folded):
179 175 return prevsiblinglclc
180 176 else:
181 177 return prevsiblinglastchild
182 178 else:
183 179 return prevsibling
184 180
185 181 # try parent (or None)
186 182 return self.parentitem()
187 183
188 184 class patch(patchnode, list): # todo: rename patchroot
189 185 """
190 186 list of header objects representing the patch.
191 187 """
192 188 def __init__(self, headerlist):
193 189 self.extend(headerlist)
194 190 # add parent patch object reference to each header
195 191 for header in self:
196 192 header.patch = self
197 193
198 194 class uiheader(patchnode):
199 195 """patch header
200 196
201 197 xxx shouldn't we move this to mercurial/patch.py ?
202 198 """
203 199
204 200 def __init__(self, header):
205 201 self.nonuiheader = header
206 202 # flag to indicate whether to apply this chunk
207 203 self.applied = True
208 204 # flag which only affects the status display indicating if a node's
209 205 # children are partially applied (i.e. some applied, some not).
210 206 self.partial = False
211 207
212 208 # flag to indicate whether to display as folded/unfolded to user
213 209 self.folded = True
214 210
215 211 # list of all headers in patch
216 212 self.patch = None
217 213
218 214 # flag is False if this header was ever unfolded from initial state
219 215 self.neverunfolded = True
220 216 self.hunks = [uihunk(h, self) for h in self.hunks]
221 217
222 218 def prettystr(self):
223 219 x = stringio()
224 220 self.pretty(x)
225 221 return x.getvalue()
226 222
227 223 def nextsibling(self):
228 224 numheadersinpatch = len(self.patch)
229 225 indexofthisheader = self.patch.index(self)
230 226
231 227 if indexofthisheader < numheadersinpatch - 1:
232 228 nextheader = self.patch[indexofthisheader + 1]
233 229 return nextheader
234 230 else:
235 231 return None
236 232
237 233 def prevsibling(self):
238 234 indexofthisheader = self.patch.index(self)
239 235 if indexofthisheader > 0:
240 236 previousheader = self.patch[indexofthisheader - 1]
241 237 return previousheader
242 238 else:
243 239 return None
244 240
245 241 def parentitem(self):
246 242 """
247 243 there is no 'real' parent item of a header that can be selected,
248 244 so return None.
249 245 """
250 246 return None
251 247
252 248 def firstchild(self):
253 249 "return the first child of this item, if one exists. otherwise None."
254 250 if len(self.hunks) > 0:
255 251 return self.hunks[0]
256 252 else:
257 253 return None
258 254
259 255 def lastchild(self):
260 256 "return the last child of this item, if one exists. otherwise None."
261 257 if len(self.hunks) > 0:
262 258 return self.hunks[-1]
263 259 else:
264 260 return None
265 261
266 262 def allchildren(self):
267 263 "return a list of all of the direct children of this node"
268 264 return self.hunks
269 265
270 266 def __getattr__(self, name):
271 267 return getattr(self.nonuiheader, name)
272 268
273 269 class uihunkline(patchnode):
274 270 "represents a changed line in a hunk"
275 271 def __init__(self, linetext, hunk):
276 272 self.linetext = linetext
277 273 self.applied = True
278 274 # the parent hunk to which this line belongs
279 275 self.hunk = hunk
280 276 # folding lines currently is not used/needed, but this flag is needed
281 277 # in the previtem method.
282 278 self.folded = False
283 279
284 280 def prettystr(self):
285 281 return self.linetext
286 282
287 283 def nextsibling(self):
288 284 numlinesinhunk = len(self.hunk.changedlines)
289 285 indexofthisline = self.hunk.changedlines.index(self)
290 286
291 287 if (indexofthisline < numlinesinhunk - 1):
292 288 nextline = self.hunk.changedlines[indexofthisline + 1]
293 289 return nextline
294 290 else:
295 291 return None
296 292
297 293 def prevsibling(self):
298 294 indexofthisline = self.hunk.changedlines.index(self)
299 295 if indexofthisline > 0:
300 296 previousline = self.hunk.changedlines[indexofthisline - 1]
301 297 return previousline
302 298 else:
303 299 return None
304 300
305 301 def parentitem(self):
306 302 "return the parent to the current item"
307 303 return self.hunk
308 304
309 305 def firstchild(self):
310 306 "return the first child of this item, if one exists. otherwise None."
311 307 # hunk-lines don't have children
312 308 return None
313 309
314 310 def lastchild(self):
315 311 "return the last child of this item, if one exists. otherwise None."
316 312 # hunk-lines don't have children
317 313 return None
318 314
319 315 class uihunk(patchnode):
320 316 """ui patch hunk, wraps a hunk and keep track of ui behavior """
321 317 maxcontext = 3
322 318
323 319 def __init__(self, hunk, header):
324 320 self._hunk = hunk
325 321 self.changedlines = [uihunkline(line, self) for line in hunk.hunk]
326 322 self.header = header
327 323 # used at end for detecting how many removed lines were un-applied
328 324 self.originalremoved = self.removed
329 325
330 326 # flag to indicate whether to display as folded/unfolded to user
331 327 self.folded = True
332 328 # flag to indicate whether to apply this chunk
333 329 self.applied = True
334 330 # flag which only affects the status display indicating if a node's
335 331 # children are partially applied (i.e. some applied, some not).
336 332 self.partial = False
337 333
338 334 def nextsibling(self):
339 335 numhunksinheader = len(self.header.hunks)
340 336 indexofthishunk = self.header.hunks.index(self)
341 337
342 338 if (indexofthishunk < numhunksinheader - 1):
343 339 nexthunk = self.header.hunks[indexofthishunk + 1]
344 340 return nexthunk
345 341 else:
346 342 return None
347 343
348 344 def prevsibling(self):
349 345 indexofthishunk = self.header.hunks.index(self)
350 346 if indexofthishunk > 0:
351 347 previoushunk = self.header.hunks[indexofthishunk - 1]
352 348 return previoushunk
353 349 else:
354 350 return None
355 351
356 352 def parentitem(self):
357 353 "return the parent to the current item"
358 354 return self.header
359 355
360 356 def firstchild(self):
361 357 "return the first child of this item, if one exists. otherwise None."
362 358 if len(self.changedlines) > 0:
363 359 return self.changedlines[0]
364 360 else:
365 361 return None
366 362
367 363 def lastchild(self):
368 364 "return the last child of this item, if one exists. otherwise None."
369 365 if len(self.changedlines) > 0:
370 366 return self.changedlines[-1]
371 367 else:
372 368 return None
373 369
374 370 def allchildren(self):
375 371 "return a list of all of the direct children of this node"
376 372 return self.changedlines
377 373
378 374 def countchanges(self):
379 375 """changedlines -> (n+,n-)"""
380 376 add = len([l for l in self.changedlines if l.applied
381 377 and l.prettystr().startswith('+')])
382 378 rem = len([l for l in self.changedlines if l.applied
383 379 and l.prettystr().startswith('-')])
384 380 return add, rem
385 381
386 382 def getfromtoline(self):
387 383 # calculate the number of removed lines converted to context lines
388 384 removedconvertedtocontext = self.originalremoved - self.removed
389 385
390 386 contextlen = (len(self.before) + len(self.after) +
391 387 removedconvertedtocontext)
392 388 if self.after and self.after[-1] == '\\ No newline at end of file\n':
393 389 contextlen -= 1
394 390 fromlen = contextlen + self.removed
395 391 tolen = contextlen + self.added
396 392
397 393 # diffutils manual, section "2.2.2.2 detailed description of unified
398 394 # format": "an empty hunk is considered to end at the line that
399 395 # precedes the hunk."
400 396 #
401 397 # so, if either of hunks is empty, decrease its line start. --immerrr
402 398 # but only do this if fromline > 0, to avoid having, e.g fromline=-1.
403 399 fromline, toline = self.fromline, self.toline
404 400 if fromline != 0:
405 401 if fromlen == 0:
406 402 fromline -= 1
407 403 if tolen == 0 and toline > 0:
408 404 toline -= 1
409 405
410 406 fromtoline = '@@ -%d,%d +%d,%d @@%s\n' % (
411 407 fromline, fromlen, toline, tolen,
412 408 self.proc and (' ' + self.proc))
413 409 return fromtoline
414 410
415 411 def write(self, fp):
416 412 # updated self.added/removed, which are used by getfromtoline()
417 413 self.added, self.removed = self.countchanges()
418 414 fp.write(self.getfromtoline())
419 415
420 416 hunklinelist = []
421 417 # add the following to the list: (1) all applied lines, and
422 418 # (2) all unapplied removal lines (convert these to context lines)
423 419 for changedline in self.changedlines:
424 420 changedlinestr = changedline.prettystr()
425 421 if changedline.applied:
426 422 hunklinelist.append(changedlinestr)
427 423 elif changedlinestr.startswith("-"):
428 424 hunklinelist.append(" " + changedlinestr[1:])
429 425
430 426 fp.write(''.join(self.before + hunklinelist + self.after))
431 427
432 428 pretty = write
433 429
434 430 def prettystr(self):
435 431 x = stringio()
436 432 self.pretty(x)
437 433 return x.getvalue()
438 434
439 435 def reversehunk(self):
440 436 """return a recordhunk which is the reverse of the hunk
441 437
442 438 Assuming the displayed patch is diff(A, B) result. The returned hunk is
443 439 intended to be applied to B, instead of A.
444 440
445 441 For example, when A is "0\n1\n2\n6\n" and B is "0\n3\n4\n5\n6\n", and
446 442 the user made the following selection:
447 443
448 444 0
449 445 [x] -1 [x]: selected
450 446 [ ] -2 [ ]: not selected
451 447 [x] +3
452 448 [ ] +4
453 449 [x] +5
454 450 6
455 451
456 452 This function returns a hunk like:
457 453
458 454 0
459 455 -3
460 456 -4
461 457 -5
462 458 +1
463 459 +4
464 460 6
465 461
466 462 Note "4" was first deleted then added. That's because "4" exists in B
467 463 side and "-4" must exist between "-3" and "-5" to make the patch
468 464 applicable to B.
469 465 """
470 466 dels = []
471 467 adds = []
472 468 for line in self.changedlines:
473 469 text = line.linetext
474 470 if line.applied:
475 471 if text.startswith('+'):
476 472 dels.append(text[1:])
477 473 elif text.startswith('-'):
478 474 adds.append(text[1:])
479 475 elif text.startswith('+'):
480 476 dels.append(text[1:])
481 477 adds.append(text[1:])
482 478 hunk = ['-%s' % l for l in dels] + ['+%s' % l for l in adds]
483 479 h = self._hunk
484 480 return patchmod.recordhunk(h.header, h.toline, h.fromline, h.proc,
485 481 h.before, hunk, h.after)
486 482
487 483 def __getattr__(self, name):
488 484 return getattr(self._hunk, name)
489 485
490 486 def __repr__(self):
491 487 return r'<hunk %r@%d>' % (self.filename(), self.fromline)
492 488
493 489 def filterpatch(ui, chunks, chunkselector, operation=None):
494 490 """interactively filter patch chunks into applied-only chunks"""
495 491 chunks = list(chunks)
496 492 # convert chunks list into structure suitable for displaying/modifying
497 493 # with curses. create a list of headers only.
498 494 headers = [c for c in chunks if isinstance(c, patchmod.header)]
499 495
500 496 # if there are no changed files
501 497 if len(headers) == 0:
502 498 return [], {}
503 499 uiheaders = [uiheader(h) for h in headers]
504 500 # let user choose headers/hunks/lines, and mark their applied flags
505 501 # accordingly
506 502 ret = chunkselector(ui, uiheaders, operation=operation)
507 503 appliedhunklist = []
508 504 for hdr in uiheaders:
509 505 if (hdr.applied and
510 506 (hdr.special() or len([h for h in hdr.hunks if h.applied]) > 0)):
511 507 appliedhunklist.append(hdr)
512 508 fixoffset = 0
513 509 for hnk in hdr.hunks:
514 510 if hnk.applied:
515 511 appliedhunklist.append(hnk)
516 512 # adjust the 'to'-line offset of the hunk to be correct
517 513 # after de-activating some of the other hunks for this file
518 514 if fixoffset:
519 515 #hnk = copy.copy(hnk) # necessary??
520 516 hnk.toline += fixoffset
521 517 else:
522 518 fixoffset += hnk.removed - hnk.added
523 519
524 520 return (appliedhunklist, ret)
525 521
526 522 def chunkselector(ui, headerlist, operation=None):
527 523 """
528 524 curses interface to get selection of chunks, and mark the applied flags
529 525 of the chosen chunks.
530 526 """
531 527 ui.write(_('starting interactive selection\n'))
532 528 chunkselector = curseschunkselector(headerlist, ui, operation)
529 # This is required for ncurses to display non-ASCII characters in
530 # default user locale encoding correctly. --immerrr
531 locale.setlocale(locale.LC_ALL, r'')
533 532 origsigtstp = sentinel = object()
534 533 if util.safehasattr(signal, 'SIGTSTP'):
535 534 origsigtstp = signal.getsignal(signal.SIGTSTP)
536 535 try:
537 536 curses.wrapper(chunkselector.main)
538 537 if chunkselector.initexc is not None:
539 538 raise chunkselector.initexc
540 539 # ncurses does not restore signal handler for SIGTSTP
541 540 finally:
542 541 if origsigtstp is not sentinel:
543 542 signal.signal(signal.SIGTSTP, origsigtstp)
544 543 return chunkselector.opts
545 544
546 545 def testdecorator(testfn, f):
547 546 def u(*args, **kwargs):
548 547 return f(testfn, *args, **kwargs)
549 548 return u
550 549
551 550 def testchunkselector(testfn, ui, headerlist, operation=None):
552 551 """
553 552 test interface to get selection of chunks, and mark the applied flags
554 553 of the chosen chunks.
555 554 """
556 555 chunkselector = curseschunkselector(headerlist, ui, operation)
557 556
558 557 class dummystdscr(object):
559 558 def clear(self):
560 559 pass
561 560 def refresh(self):
562 561 pass
563 562
564 563 chunkselector.stdscr = dummystdscr()
565 564 if testfn and os.path.exists(testfn):
566 565 testf = open(testfn, 'rb')
567 566 testcommands = [x.rstrip('\n') for x in testf.readlines()]
568 567 testf.close()
569 568 while True:
570 569 if chunkselector.handlekeypressed(testcommands.pop(0), test=True):
571 570 break
572 571 return chunkselector.opts
573 572
574 573 _headermessages = { # {operation: text}
575 574 'apply': _('Select hunks to apply'),
576 575 'discard': _('Select hunks to discard'),
577 576 'keep': _('Select hunks to keep'),
578 577 None: _('Select hunks to record'),
579 578 }
580 579
581 580 class curseschunkselector(object):
582 581 def __init__(self, headerlist, ui, operation=None):
583 582 # put the headers into a patch object
584 583 self.headerlist = patch(headerlist)
585 584
586 585 self.ui = ui
587 586 self.opts = {}
588 587
589 588 self.errorstr = None
590 589 # list of all chunks
591 590 self.chunklist = []
592 591 for h in headerlist:
593 592 self.chunklist.append(h)
594 593 self.chunklist.extend(h.hunks)
595 594
596 595 # dictionary mapping (fgcolor, bgcolor) pairs to the
597 596 # corresponding curses color-pair value.
598 597 self.colorpairs = {}
599 598 # maps custom nicknames of color-pairs to curses color-pair values
600 599 self.colorpairnames = {}
601 600
602 601 # Honor color setting of ui section. Keep colored setup as
603 602 # long as not explicitly set to a falsy value - especially,
604 603 # when not set at all. This is to stay most compatible with
605 604 # previous (color only) behaviour.
606 605 uicolor = stringutil.parsebool(self.ui.config('ui', 'color'))
607 606 self.usecolor = uicolor is not False
608 607
609 608 # the currently selected header, hunk, or hunk-line
610 609 self.currentselecteditem = self.headerlist[0]
611 610 self.lastapplieditem = None
612 611
613 612 # updated when printing out patch-display -- the 'lines' here are the
614 613 # line positions *in the pad*, not on the screen.
615 614 self.selecteditemstartline = 0
616 615 self.selecteditemendline = None
617 616
618 617 # define indentation levels
619 618 self.headerindentnumchars = 0
620 619 self.hunkindentnumchars = 3
621 620 self.hunklineindentnumchars = 6
622 621
623 622 # the first line of the pad to print to the screen
624 623 self.firstlineofpadtoprint = 0
625 624
626 625 # keeps track of the number of lines in the pad
627 626 self.numpadlines = None
628 627
629 628 self.numstatuslines = 1
630 629
631 630 # keep a running count of the number of lines printed to the pad
632 631 # (used for determining when the selected item begins/ends)
633 632 self.linesprintedtopadsofar = 0
634 633
635 634 # the first line of the pad which is visible on the screen
636 635 self.firstlineofpadtoprint = 0
637 636
638 637 # stores optional text for a commit comment provided by the user
639 638 self.commenttext = ""
640 639
641 640 # if the last 'toggle all' command caused all changes to be applied
642 641 self.waslasttoggleallapplied = True
643 642
644 643 # affects some ui text
645 644 if operation not in _headermessages:
646 645 raise error.ProgrammingError('unexpected operation: %s' % operation)
647 646 self.operation = operation
648 647
649 648 def uparrowevent(self):
650 649 """
651 650 try to select the previous item to the current item that has the
652 651 most-indented level. for example, if a hunk is selected, try to select
653 652 the last hunkline of the hunk prior to the selected hunk. or, if
654 653 the first hunkline of a hunk is currently selected, then select the
655 654 hunk itself.
656 655 """
657 656 currentitem = self.currentselecteditem
658 657
659 658 nextitem = currentitem.previtem()
660 659
661 660 if nextitem is None:
662 661 # if no parent item (i.e. currentitem is the first header), then
663 662 # no change...
664 663 nextitem = currentitem
665 664
666 665 self.currentselecteditem = nextitem
667 666
668 667 def uparrowshiftevent(self):
669 668 """
670 669 select (if possible) the previous item on the same level as the
671 670 currently selected item. otherwise, select (if possible) the
672 671 parent-item of the currently selected item.
673 672 """
674 673 currentitem = self.currentselecteditem
675 674 nextitem = currentitem.prevsibling()
676 675 # if there's no previous sibling, try choosing the parent
677 676 if nextitem is None:
678 677 nextitem = currentitem.parentitem()
679 678 if nextitem is None:
680 679 # if no parent item (i.e. currentitem is the first header), then
681 680 # no change...
682 681 nextitem = currentitem
683 682
684 683 self.currentselecteditem = nextitem
685 684 self.recenterdisplayedarea()
686 685
687 686 def downarrowevent(self):
688 687 """
689 688 try to select the next item to the current item that has the
690 689 most-indented level. for example, if a hunk is selected, select
691 690 the first hunkline of the selected hunk. or, if the last hunkline of
692 691 a hunk is currently selected, then select the next hunk, if one exists,
693 692 or if not, the next header if one exists.
694 693 """
695 694 #self.startprintline += 1 #debug
696 695 currentitem = self.currentselecteditem
697 696
698 697 nextitem = currentitem.nextitem()
699 698 # if there's no next item, keep the selection as-is
700 699 if nextitem is None:
701 700 nextitem = currentitem
702 701
703 702 self.currentselecteditem = nextitem
704 703
705 704 def downarrowshiftevent(self):
706 705 """
707 706 select (if possible) the next item on the same level as the currently
708 707 selected item. otherwise, select (if possible) the next item on the
709 708 same level as the parent item of the currently selected item.
710 709 """
711 710 currentitem = self.currentselecteditem
712 711 nextitem = currentitem.nextsibling()
713 712 # if there's no next sibling, try choosing the parent's nextsibling
714 713 if nextitem is None:
715 714 try:
716 715 nextitem = currentitem.parentitem().nextsibling()
717 716 except AttributeError:
718 717 # parentitem returned None, so nextsibling() can't be called
719 718 nextitem = None
720 719 if nextitem is None:
721 720 # if parent has no next sibling, then no change...
722 721 nextitem = currentitem
723 722
724 723 self.currentselecteditem = nextitem
725 724 self.recenterdisplayedarea()
726 725
727 726 def nextsametype(self, test=False):
728 727 currentitem = self.currentselecteditem
729 728 sametype = lambda item: isinstance(item, type(currentitem))
730 729 nextitem = currentitem.nextitem()
731 730
732 731 while nextitem is not None and not sametype(nextitem):
733 732 nextitem = nextitem.nextitem()
734 733
735 734 if nextitem is None:
736 735 nextitem = currentitem
737 736 else:
738 737 parent = nextitem.parentitem()
739 738 if parent is not None and parent.folded:
740 739 self.togglefolded(parent)
741 740
742 741 self.currentselecteditem = nextitem
743 742 if not test:
744 743 self.recenterdisplayedarea()
745 744
746 745 def rightarrowevent(self):
747 746 """
748 747 select (if possible) the first of this item's child-items.
749 748 """
750 749 currentitem = self.currentselecteditem
751 750 nextitem = currentitem.firstchild()
752 751
753 752 # turn off folding if we want to show a child-item
754 753 if currentitem.folded:
755 754 self.togglefolded(currentitem)
756 755
757 756 if nextitem is None:
758 757 # if no next item on parent-level, then no change...
759 758 nextitem = currentitem
760 759
761 760 self.currentselecteditem = nextitem
762 761
763 762 def leftarrowevent(self):
764 763 """
765 764 if the current item can be folded (i.e. it is an unfolded header or
766 765 hunk), then fold it. otherwise try select (if possible) the parent
767 766 of this item.
768 767 """
769 768 currentitem = self.currentselecteditem
770 769
771 770 # try to fold the item
772 771 if not isinstance(currentitem, uihunkline):
773 772 if not currentitem.folded:
774 773 self.togglefolded(item=currentitem)
775 774 return
776 775
777 776 # if it can't be folded, try to select the parent item
778 777 nextitem = currentitem.parentitem()
779 778
780 779 if nextitem is None:
781 780 # if no item on parent-level, then no change...
782 781 nextitem = currentitem
783 782 if not nextitem.folded:
784 783 self.togglefolded(item=nextitem)
785 784
786 785 self.currentselecteditem = nextitem
787 786
788 787 def leftarrowshiftevent(self):
789 788 """
790 789 select the header of the current item (or fold current item if the
791 790 current item is already a header).
792 791 """
793 792 currentitem = self.currentselecteditem
794 793
795 794 if isinstance(currentitem, uiheader):
796 795 if not currentitem.folded:
797 796 self.togglefolded(item=currentitem)
798 797 return
799 798
800 799 # select the parent item recursively until we're at a header
801 800 while True:
802 801 nextitem = currentitem.parentitem()
803 802 if nextitem is None:
804 803 break
805 804 else:
806 805 currentitem = nextitem
807 806
808 807 self.currentselecteditem = currentitem
809 808
810 809 def updatescroll(self):
811 810 "scroll the screen to fully show the currently-selected"
812 811 selstart = self.selecteditemstartline
813 812 selend = self.selecteditemendline
814 813
815 814 padstart = self.firstlineofpadtoprint
816 815 padend = padstart + self.yscreensize - self.numstatuslines - 1
817 816 # 'buffered' pad start/end values which scroll with a certain
818 817 # top/bottom context margin
819 818 padstartbuffered = padstart + 3
820 819 padendbuffered = padend - 3
821 820
822 821 if selend > padendbuffered:
823 822 self.scrolllines(selend - padendbuffered)
824 823 elif selstart < padstartbuffered:
825 824 # negative values scroll in pgup direction
826 825 self.scrolllines(selstart - padstartbuffered)
827 826
828 827 def scrolllines(self, numlines):
829 828 "scroll the screen up (down) by numlines when numlines >0 (<0)."
830 829 self.firstlineofpadtoprint += numlines
831 830 if self.firstlineofpadtoprint < 0:
832 831 self.firstlineofpadtoprint = 0
833 832 if self.firstlineofpadtoprint > self.numpadlines - 1:
834 833 self.firstlineofpadtoprint = self.numpadlines - 1
835 834
836 835 def toggleapply(self, item=None):
837 836 """
838 837 toggle the applied flag of the specified item. if no item is specified,
839 838 toggle the flag of the currently selected item.
840 839 """
841 840 if item is None:
842 841 item = self.currentselecteditem
843 842 # Only set this when NOT using 'toggleall'
844 843 self.lastapplieditem = item
845 844
846 845 item.applied = not item.applied
847 846
848 847 if isinstance(item, uiheader):
849 848 item.partial = False
850 849 if item.applied:
851 850 # apply all its hunks
852 851 for hnk in item.hunks:
853 852 hnk.applied = True
854 853 # apply all their hunklines
855 854 for hunkline in hnk.changedlines:
856 855 hunkline.applied = True
857 856 else:
858 857 # un-apply all its hunks
859 858 for hnk in item.hunks:
860 859 hnk.applied = False
861 860 hnk.partial = False
862 861 # un-apply all their hunklines
863 862 for hunkline in hnk.changedlines:
864 863 hunkline.applied = False
865 864 elif isinstance(item, uihunk):
866 865 item.partial = False
867 866 # apply all it's hunklines
868 867 for hunkline in item.changedlines:
869 868 hunkline.applied = item.applied
870 869
871 870 siblingappliedstatus = [hnk.applied for hnk in item.header.hunks]
872 871 allsiblingsapplied = not (False in siblingappliedstatus)
873 872 nosiblingsapplied = not (True in siblingappliedstatus)
874 873
875 874 siblingspartialstatus = [hnk.partial for hnk in item.header.hunks]
876 875 somesiblingspartial = (True in siblingspartialstatus)
877 876
878 877 #cases where applied or partial should be removed from header
879 878
880 879 # if no 'sibling' hunks are applied (including this hunk)
881 880 if nosiblingsapplied:
882 881 if not item.header.special():
883 882 item.header.applied = False
884 883 item.header.partial = False
885 884 else: # some/all parent siblings are applied
886 885 item.header.applied = True
887 886 item.header.partial = (somesiblingspartial or
888 887 not allsiblingsapplied)
889 888
890 889 elif isinstance(item, uihunkline):
891 890 siblingappliedstatus = [ln.applied for ln in item.hunk.changedlines]
892 891 allsiblingsapplied = not (False in siblingappliedstatus)
893 892 nosiblingsapplied = not (True in siblingappliedstatus)
894 893
895 894 # if no 'sibling' lines are applied
896 895 if nosiblingsapplied:
897 896 item.hunk.applied = False
898 897 item.hunk.partial = False
899 898 elif allsiblingsapplied:
900 899 item.hunk.applied = True
901 900 item.hunk.partial = False
902 901 else: # some siblings applied
903 902 item.hunk.applied = True
904 903 item.hunk.partial = True
905 904
906 905 parentsiblingsapplied = [hnk.applied for hnk
907 906 in item.hunk.header.hunks]
908 907 noparentsiblingsapplied = not (True in parentsiblingsapplied)
909 908 allparentsiblingsapplied = not (False in parentsiblingsapplied)
910 909
911 910 parentsiblingspartial = [hnk.partial for hnk
912 911 in item.hunk.header.hunks]
913 912 someparentsiblingspartial = (True in parentsiblingspartial)
914 913
915 914 # if all parent hunks are not applied, un-apply header
916 915 if noparentsiblingsapplied:
917 916 if not item.hunk.header.special():
918 917 item.hunk.header.applied = False
919 918 item.hunk.header.partial = False
920 919 # set the applied and partial status of the header if needed
921 920 else: # some/all parent siblings are applied
922 921 item.hunk.header.applied = True
923 922 item.hunk.header.partial = (someparentsiblingspartial or
924 923 not allparentsiblingsapplied)
925 924
926 925 def toggleall(self):
927 926 "toggle the applied flag of all items."
928 927 if self.waslasttoggleallapplied: # then unapply them this time
929 928 for item in self.headerlist:
930 929 if item.applied:
931 930 self.toggleapply(item)
932 931 else:
933 932 for item in self.headerlist:
934 933 if not item.applied:
935 934 self.toggleapply(item)
936 935 self.waslasttoggleallapplied = not self.waslasttoggleallapplied
937 936
938 937 def toggleallbetween(self):
939 938 "toggle applied on or off for all items in range [lastapplied,current]."
940 939 if (not self.lastapplieditem or
941 940 self.currentselecteditem == self.lastapplieditem):
942 941 # Treat this like a normal 'x'/' '
943 942 self.toggleapply()
944 943 return
945 944
946 945 startitem = self.lastapplieditem
947 946 enditem = self.currentselecteditem
948 947 # Verify that enditem is "after" startitem, otherwise swap them.
949 948 for direction in ['forward', 'reverse']:
950 949 nextitem = startitem.nextitem()
951 950 while nextitem and nextitem != enditem:
952 951 nextitem = nextitem.nextitem()
953 952 if nextitem:
954 953 break
955 954 # Looks like we went the wrong direction :)
956 955 startitem, enditem = enditem, startitem
957 956
958 957 if not nextitem:
959 958 # We didn't find a path going either forward or backward? Don't know
960 959 # how this can happen, let's not crash though.
961 960 return
962 961
963 962 nextitem = startitem
964 963 # Switch all items to be the opposite state of the currently selected
965 964 # item. Specifically:
966 965 # [ ] startitem
967 966 # [x] middleitem
968 967 # [ ] enditem <-- currently selected
969 968 # This will turn all three on, since the currently selected item is off.
970 969 # This does *not* invert each item (i.e. middleitem stays marked/on)
971 970 desiredstate = not self.currentselecteditem.applied
972 971 while nextitem != enditem.nextitem():
973 972 if nextitem.applied != desiredstate:
974 973 self.toggleapply(item=nextitem)
975 974 nextitem = nextitem.nextitem()
976 975
977 976 def togglefolded(self, item=None, foldparent=False):
978 977 "toggle folded flag of specified item (defaults to currently selected)"
979 978 if item is None:
980 979 item = self.currentselecteditem
981 980 if foldparent or (isinstance(item, uiheader) and item.neverunfolded):
982 981 if not isinstance(item, uiheader):
983 982 # we need to select the parent item in this case
984 983 self.currentselecteditem = item = item.parentitem()
985 984 elif item.neverunfolded:
986 985 item.neverunfolded = False
987 986
988 987 # also fold any foldable children of the parent/current item
989 988 if isinstance(item, uiheader): # the original or 'new' item
990 989 for child in item.allchildren():
991 990 child.folded = not item.folded
992 991
993 992 if isinstance(item, (uiheader, uihunk)):
994 993 item.folded = not item.folded
995 994
996 995 def alignstring(self, instr, window):
997 996 """
998 997 add whitespace to the end of a string in order to make it fill
999 998 the screen in the x direction. the current cursor position is
1000 999 taken into account when making this calculation. the string can span
1001 1000 multiple lines.
1002 1001 """
1003 1002 y, xstart = window.getyx()
1004 1003 width = self.xscreensize
1005 1004 # turn tabs into spaces
1006 1005 instr = instr.expandtabs(4)
1007 1006 strwidth = encoding.colwidth(instr)
1008 1007 numspaces = (width - ((strwidth + xstart) % width))
1009 1008 return instr + " " * numspaces
1010 1009
1011 1010 def printstring(self, window, text, fgcolor=None, bgcolor=None, pair=None,
1012 1011 pairname=None, attrlist=None, towin=True, align=True, showwhtspc=False):
1013 1012 """
1014 1013 print the string, text, with the specified colors and attributes, to
1015 1014 the specified curses window object.
1016 1015
1017 1016 the foreground and background colors are of the form
1018 1017 curses.color_xxxx, where xxxx is one of: [black, blue, cyan, green,
1019 1018 magenta, red, white, yellow]. if pairname is provided, a color
1020 1019 pair will be looked up in the self.colorpairnames dictionary.
1021 1020
1022 1021 attrlist is a list containing text attributes in the form of
1023 1022 curses.a_xxxx, where xxxx can be: [bold, dim, normal, standout,
1024 1023 underline].
1025 1024
1026 1025 if align == True, whitespace is added to the printed string such that
1027 1026 the string stretches to the right border of the window.
1028 1027
1029 1028 if showwhtspc == True, trailing whitespace of a string is highlighted.
1030 1029 """
1031 1030 # preprocess the text, converting tabs to spaces
1032 1031 text = text.expandtabs(4)
1033 1032 # strip \n, and convert control characters to ^[char] representation
1034 1033 text = re.sub(br'[\x00-\x08\x0a-\x1f]',
1035 1034 lambda m:'^' + chr(ord(m.group()) + 64), text.strip('\n'))
1036 1035
1037 1036 if pair is not None:
1038 1037 colorpair = pair
1039 1038 elif pairname is not None:
1040 1039 colorpair = self.colorpairnames[pairname]
1041 1040 else:
1042 1041 if fgcolor is None:
1043 1042 fgcolor = -1
1044 1043 if bgcolor is None:
1045 1044 bgcolor = -1
1046 1045 if (fgcolor, bgcolor) in self.colorpairs:
1047 1046 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1048 1047 else:
1049 1048 colorpair = self.getcolorpair(fgcolor, bgcolor)
1050 1049 # add attributes if possible
1051 1050 if attrlist is None:
1052 1051 attrlist = []
1053 1052 if colorpair < 256:
1054 1053 # then it is safe to apply all attributes
1055 1054 for textattr in attrlist:
1056 1055 colorpair |= textattr
1057 1056 else:
1058 1057 # just apply a select few (safe?) attributes
1059 1058 for textattr in (curses.A_UNDERLINE, curses.A_BOLD):
1060 1059 if textattr in attrlist:
1061 1060 colorpair |= textattr
1062 1061
1063 1062 y, xstart = self.chunkpad.getyx()
1064 1063 t = "" # variable for counting lines printed
1065 1064 # if requested, show trailing whitespace
1066 1065 if showwhtspc:
1067 1066 origlen = len(text)
1068 1067 text = text.rstrip(' \n') # tabs have already been expanded
1069 1068 strippedlen = len(text)
1070 1069 numtrailingspaces = origlen - strippedlen
1071 1070
1072 1071 if towin:
1073 1072 window.addstr(text, colorpair)
1074 1073 t += text
1075 1074
1076 1075 if showwhtspc:
1077 1076 wscolorpair = colorpair | curses.A_REVERSE
1078 1077 if towin:
1079 1078 for i in range(numtrailingspaces):
1080 1079 window.addch(curses.ACS_CKBOARD, wscolorpair)
1081 1080 t += " " * numtrailingspaces
1082 1081
1083 1082 if align:
1084 1083 if towin:
1085 1084 extrawhitespace = self.alignstring("", window)
1086 1085 window.addstr(extrawhitespace, colorpair)
1087 1086 else:
1088 1087 # need to use t, since the x position hasn't incremented
1089 1088 extrawhitespace = self.alignstring(t, window)
1090 1089 t += extrawhitespace
1091 1090
1092 1091 # is reset to 0 at the beginning of printitem()
1093 1092
1094 1093 linesprinted = (xstart + len(t)) / self.xscreensize
1095 1094 self.linesprintedtopadsofar += linesprinted
1096 1095 return t
1097 1096
1098 1097 def _getstatuslinesegments(self):
1099 1098 """-> [str]. return segments"""
1100 1099 selected = self.currentselecteditem.applied
1101 1100 spaceselect = _('space/enter: select')
1102 1101 spacedeselect = _('space/enter: deselect')
1103 1102 # Format the selected label into a place as long as the longer of the
1104 1103 # two possible labels. This may vary by language.
1105 1104 spacelen = max(len(spaceselect), len(spacedeselect))
1106 1105 selectedlabel = '%-*s' % (spacelen,
1107 1106 spacedeselect if selected else spaceselect)
1108 1107 segments = [
1109 1108 _headermessages[self.operation],
1110 1109 '-',
1111 1110 _('[x]=selected **=collapsed'),
1112 1111 _('c: confirm'),
1113 1112 _('q: abort'),
1114 1113 _('arrow keys: move/expand/collapse'),
1115 1114 selectedlabel,
1116 1115 _('?: help'),
1117 1116 ]
1118 1117 return segments
1119 1118
1120 1119 def _getstatuslines(self):
1121 1120 """() -> [str]. return short help used in the top status window"""
1122 1121 if self.errorstr is not None:
1123 1122 lines = [self.errorstr, _('Press any key to continue')]
1124 1123 else:
1125 1124 # wrap segments to lines
1126 1125 segments = self._getstatuslinesegments()
1127 1126 width = self.xscreensize
1128 1127 lines = []
1129 1128 lastwidth = width
1130 1129 for s in segments:
1131 1130 w = encoding.colwidth(s)
1132 1131 sep = ' ' * (1 + (s and s[0] not in '-['))
1133 1132 if lastwidth + w + len(sep) >= width:
1134 1133 lines.append(s)
1135 1134 lastwidth = w
1136 1135 else:
1137 1136 lines[-1] += sep + s
1138 1137 lastwidth += w + len(sep)
1139 1138 if len(lines) != self.numstatuslines:
1140 1139 self.numstatuslines = len(lines)
1141 1140 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1142 1141 return [stringutil.ellipsis(l, self.xscreensize - 1) for l in lines]
1143 1142
1144 1143 def updatescreen(self):
1145 1144 self.statuswin.erase()
1146 1145 self.chunkpad.erase()
1147 1146
1148 1147 printstring = self.printstring
1149 1148
1150 1149 # print out the status lines at the top
1151 1150 try:
1152 1151 for line in self._getstatuslines():
1153 1152 printstring(self.statuswin, line, pairname="legend")
1154 1153 self.statuswin.refresh()
1155 1154 except curses.error:
1156 1155 pass
1157 1156 if self.errorstr is not None:
1158 1157 return
1159 1158
1160 1159 # print out the patch in the remaining part of the window
1161 1160 try:
1162 1161 self.printitem()
1163 1162 self.updatescroll()
1164 1163 self.chunkpad.refresh(self.firstlineofpadtoprint, 0,
1165 1164 self.numstatuslines, 0,
1166 1165 self.yscreensize - self.numstatuslines,
1167 1166 self.xscreensize)
1168 1167 except curses.error:
1169 1168 pass
1170 1169
1171 1170 def getstatusprefixstring(self, item):
1172 1171 """
1173 1172 create a string to prefix a line with which indicates whether 'item'
1174 1173 is applied and/or folded.
1175 1174 """
1176 1175
1177 1176 # create checkbox string
1178 1177 if item.applied:
1179 1178 if not isinstance(item, uihunkline) and item.partial:
1180 1179 checkbox = "[~]"
1181 1180 else:
1182 1181 checkbox = "[x]"
1183 1182 else:
1184 1183 checkbox = "[ ]"
1185 1184
1186 1185 try:
1187 1186 if item.folded:
1188 1187 checkbox += "**"
1189 1188 if isinstance(item, uiheader):
1190 1189 # one of "m", "a", or "d" (modified, added, deleted)
1191 1190 filestatus = item.changetype
1192 1191
1193 1192 checkbox += filestatus + " "
1194 1193 else:
1195 1194 checkbox += " "
1196 1195 if isinstance(item, uiheader):
1197 1196 # add two more spaces for headers
1198 1197 checkbox += " "
1199 1198 except AttributeError: # not foldable
1200 1199 checkbox += " "
1201 1200
1202 1201 return checkbox
1203 1202
1204 1203 def printheader(self, header, selected=False, towin=True,
1205 1204 ignorefolding=False):
1206 1205 """
1207 1206 print the header to the pad. if countlines is True, don't print
1208 1207 anything, but just count the number of lines which would be printed.
1209 1208 """
1210 1209
1211 1210 outstr = ""
1212 1211 text = header.prettystr()
1213 1212 chunkindex = self.chunklist.index(header)
1214 1213
1215 1214 if chunkindex != 0 and not header.folded:
1216 1215 # add separating line before headers
1217 1216 outstr += self.printstring(self.chunkpad, '_' * self.xscreensize,
1218 1217 towin=towin, align=False)
1219 1218 # select color-pair based on if the header is selected
1220 1219 colorpair = self.getcolorpair(name=selected and "selected" or "normal",
1221 1220 attrlist=[curses.A_BOLD])
1222 1221
1223 1222 # print out each line of the chunk, expanding it to screen width
1224 1223
1225 1224 # number of characters to indent lines on this level by
1226 1225 indentnumchars = 0
1227 1226 checkbox = self.getstatusprefixstring(header)
1228 1227 if not header.folded or ignorefolding:
1229 1228 textlist = text.split("\n")
1230 1229 linestr = checkbox + textlist[0]
1231 1230 else:
1232 1231 linestr = checkbox + header.filename()
1233 1232 outstr += self.printstring(self.chunkpad, linestr, pair=colorpair,
1234 1233 towin=towin)
1235 1234 if not header.folded or ignorefolding:
1236 1235 if len(textlist) > 1:
1237 1236 for line in textlist[1:]:
1238 1237 linestr = " "*(indentnumchars + len(checkbox)) + line
1239 1238 outstr += self.printstring(self.chunkpad, linestr,
1240 1239 pair=colorpair, towin=towin)
1241 1240
1242 1241 return outstr
1243 1242
1244 1243 def printhunklinesbefore(self, hunk, selected=False, towin=True,
1245 1244 ignorefolding=False):
1246 1245 "includes start/end line indicator"
1247 1246 outstr = ""
1248 1247 # where hunk is in list of siblings
1249 1248 hunkindex = hunk.header.hunks.index(hunk)
1250 1249
1251 1250 if hunkindex != 0:
1252 1251 # add separating line before headers
1253 1252 outstr += self.printstring(self.chunkpad, ' '*self.xscreensize,
1254 1253 towin=towin, align=False)
1255 1254
1256 1255 colorpair = self.getcolorpair(name=selected and "selected" or "normal",
1257 1256 attrlist=[curses.A_BOLD])
1258 1257
1259 1258 # print out from-to line with checkbox
1260 1259 checkbox = self.getstatusprefixstring(hunk)
1261 1260
1262 1261 lineprefix = " "*self.hunkindentnumchars + checkbox
1263 1262 frtoline = " " + hunk.getfromtoline().strip("\n")
1264 1263
1265 1264 outstr += self.printstring(self.chunkpad, lineprefix, towin=towin,
1266 1265 align=False) # add uncolored checkbox/indent
1267 1266 outstr += self.printstring(self.chunkpad, frtoline, pair=colorpair,
1268 1267 towin=towin)
1269 1268
1270 1269 if hunk.folded and not ignorefolding:
1271 1270 # skip remainder of output
1272 1271 return outstr
1273 1272
1274 1273 # print out lines of the chunk preceeding changed-lines
1275 1274 for line in hunk.before:
1276 1275 linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line
1277 1276 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1278 1277
1279 1278 return outstr
1280 1279
1281 1280 def printhunklinesafter(self, hunk, towin=True, ignorefolding=False):
1282 1281 outstr = ""
1283 1282 if hunk.folded and not ignorefolding:
1284 1283 return outstr
1285 1284
1286 1285 # a bit superfluous, but to avoid hard-coding indent amount
1287 1286 checkbox = self.getstatusprefixstring(hunk)
1288 1287 for line in hunk.after:
1289 1288 linestr = " "*(self.hunklineindentnumchars + len(checkbox)) + line
1290 1289 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1291 1290
1292 1291 return outstr
1293 1292
1294 1293 def printhunkchangedline(self, hunkline, selected=False, towin=True):
1295 1294 outstr = ""
1296 1295 checkbox = self.getstatusprefixstring(hunkline)
1297 1296
1298 1297 linestr = hunkline.prettystr().strip("\n")
1299 1298
1300 1299 # select color-pair based on whether line is an addition/removal
1301 1300 if selected:
1302 1301 colorpair = self.getcolorpair(name="selected")
1303 1302 elif linestr.startswith("+"):
1304 1303 colorpair = self.getcolorpair(name="addition")
1305 1304 elif linestr.startswith("-"):
1306 1305 colorpair = self.getcolorpair(name="deletion")
1307 1306 elif linestr.startswith("\\"):
1308 1307 colorpair = self.getcolorpair(name="normal")
1309 1308
1310 1309 lineprefix = " "*self.hunklineindentnumchars + checkbox
1311 1310 outstr += self.printstring(self.chunkpad, lineprefix, towin=towin,
1312 1311 align=False) # add uncolored checkbox/indent
1313 1312 outstr += self.printstring(self.chunkpad, linestr, pair=colorpair,
1314 1313 towin=towin, showwhtspc=True)
1315 1314 return outstr
1316 1315
1317 1316 def printitem(self, item=None, ignorefolding=False, recursechildren=True,
1318 1317 towin=True):
1319 1318 """
1320 1319 use __printitem() to print the the specified item.applied.
1321 1320 if item is not specified, then print the entire patch.
1322 1321 (hiding folded elements, etc. -- see __printitem() docstring)
1323 1322 """
1324 1323
1325 1324 if item is None:
1326 1325 item = self.headerlist
1327 1326 if recursechildren:
1328 1327 self.linesprintedtopadsofar = 0
1329 1328
1330 1329 outstr = []
1331 1330 self.__printitem(item, ignorefolding, recursechildren, outstr,
1332 1331 towin=towin)
1333 1332 return ''.join(outstr)
1334 1333
1335 1334 def outofdisplayedarea(self):
1336 1335 y, _ = self.chunkpad.getyx() # cursor location
1337 1336 # * 2 here works but an optimization would be the max number of
1338 1337 # consecutive non selectable lines
1339 1338 # i.e the max number of context line for any hunk in the patch
1340 1339 miny = min(0, self.firstlineofpadtoprint - self.yscreensize)
1341 1340 maxy = self.firstlineofpadtoprint + self.yscreensize * 2
1342 1341 return y < miny or y > maxy
1343 1342
1344 1343 def handleselection(self, item, recursechildren):
1345 1344 selected = (item is self.currentselecteditem)
1346 1345 if selected and recursechildren:
1347 1346 # assumes line numbering starting from line 0
1348 1347 self.selecteditemstartline = self.linesprintedtopadsofar
1349 1348 selecteditemlines = self.getnumlinesdisplayed(item,
1350 1349 recursechildren=False)
1351 1350 self.selecteditemendline = (self.selecteditemstartline +
1352 1351 selecteditemlines - 1)
1353 1352 return selected
1354 1353
1355 1354 def __printitem(self, item, ignorefolding, recursechildren, outstr,
1356 1355 towin=True):
1357 1356 """
1358 1357 recursive method for printing out patch/header/hunk/hunk-line data to
1359 1358 screen. also returns a string with all of the content of the displayed
1360 1359 patch (not including coloring, etc.).
1361 1360
1362 1361 if ignorefolding is True, then folded items are printed out.
1363 1362
1364 1363 if recursechildren is False, then only print the item without its
1365 1364 child items.
1366 1365 """
1367 1366
1368 1367 if towin and self.outofdisplayedarea():
1369 1368 return
1370 1369
1371 1370 selected = self.handleselection(item, recursechildren)
1372 1371
1373 1372 # patch object is a list of headers
1374 1373 if isinstance(item, patch):
1375 1374 if recursechildren:
1376 1375 for hdr in item:
1377 1376 self.__printitem(hdr, ignorefolding,
1378 1377 recursechildren, outstr, towin)
1379 1378 # todo: eliminate all isinstance() calls
1380 1379 if isinstance(item, uiheader):
1381 1380 outstr.append(self.printheader(item, selected, towin=towin,
1382 1381 ignorefolding=ignorefolding))
1383 1382 if recursechildren:
1384 1383 for hnk in item.hunks:
1385 1384 self.__printitem(hnk, ignorefolding,
1386 1385 recursechildren, outstr, towin)
1387 1386 elif (isinstance(item, uihunk) and
1388 1387 ((not item.header.folded) or ignorefolding)):
1389 1388 # print the hunk data which comes before the changed-lines
1390 1389 outstr.append(self.printhunklinesbefore(item, selected, towin=towin,
1391 1390 ignorefolding=ignorefolding))
1392 1391 if recursechildren:
1393 1392 for l in item.changedlines:
1394 1393 self.__printitem(l, ignorefolding,
1395 1394 recursechildren, outstr, towin)
1396 1395 outstr.append(self.printhunklinesafter(item, towin=towin,
1397 1396 ignorefolding=ignorefolding))
1398 1397 elif (isinstance(item, uihunkline) and
1399 1398 ((not item.hunk.folded) or ignorefolding)):
1400 1399 outstr.append(self.printhunkchangedline(item, selected,
1401 1400 towin=towin))
1402 1401
1403 1402 return outstr
1404 1403
1405 1404 def getnumlinesdisplayed(self, item=None, ignorefolding=False,
1406 1405 recursechildren=True):
1407 1406 """
1408 1407 return the number of lines which would be displayed if the item were
1409 1408 to be printed to the display. the item will not be printed to the
1410 1409 display (pad).
1411 1410 if no item is given, assume the entire patch.
1412 1411 if ignorefolding is True, folded items will be unfolded when counting
1413 1412 the number of lines.
1414 1413 """
1415 1414
1416 1415 # temporarily disable printing to windows by printstring
1417 1416 patchdisplaystring = self.printitem(item, ignorefolding,
1418 1417 recursechildren, towin=False)
1419 1418 numlines = len(patchdisplaystring) // self.xscreensize
1420 1419 return numlines
1421 1420
1422 1421 def sigwinchhandler(self, n, frame):
1423 1422 "handle window resizing"
1424 1423 try:
1425 1424 curses.endwin()
1426 1425 self.xscreensize, self.yscreensize = scmutil.termsize(self.ui)
1427 1426 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1428 1427 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1429 1428 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1430 1429 except curses.error:
1431 1430 pass
1432 1431
1433 1432 def getcolorpair(self, fgcolor=None, bgcolor=None, name=None,
1434 1433 attrlist=None):
1435 1434 """
1436 1435 get a curses color pair, adding it to self.colorpairs if it is not
1437 1436 already defined. an optional string, name, can be passed as a shortcut
1438 1437 for referring to the color-pair. by default, if no arguments are
1439 1438 specified, the white foreground / black background color-pair is
1440 1439 returned.
1441 1440
1442 1441 it is expected that this function will be used exclusively for
1443 1442 initializing color pairs, and not curses.init_pair().
1444 1443
1445 1444 attrlist is used to 'flavor' the returned color-pair. this information
1446 1445 is not stored in self.colorpairs. it contains attribute values like
1447 1446 curses.A_BOLD.
1448 1447 """
1449 1448
1450 1449 if (name is not None) and name in self.colorpairnames:
1451 1450 # then get the associated color pair and return it
1452 1451 colorpair = self.colorpairnames[name]
1453 1452 else:
1454 1453 if fgcolor is None:
1455 1454 fgcolor = -1
1456 1455 if bgcolor is None:
1457 1456 bgcolor = -1
1458 1457 if (fgcolor, bgcolor) in self.colorpairs:
1459 1458 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1460 1459 else:
1461 1460 pairindex = len(self.colorpairs) + 1
1462 1461 if self.usecolor:
1463 1462 curses.init_pair(pairindex, fgcolor, bgcolor)
1464 1463 colorpair = self.colorpairs[(fgcolor, bgcolor)] = (
1465 1464 curses.color_pair(pairindex))
1466 1465 if name is not None:
1467 1466 self.colorpairnames[name] = curses.color_pair(pairindex)
1468 1467 else:
1469 1468 cval = 0
1470 1469 if name is not None:
1471 1470 if name == 'selected':
1472 1471 cval = curses.A_REVERSE
1473 1472 self.colorpairnames[name] = cval
1474 1473 colorpair = self.colorpairs[(fgcolor, bgcolor)] = cval
1475 1474
1476 1475 # add attributes if possible
1477 1476 if attrlist is None:
1478 1477 attrlist = []
1479 1478 if colorpair < 256:
1480 1479 # then it is safe to apply all attributes
1481 1480 for textattr in attrlist:
1482 1481 colorpair |= textattr
1483 1482 else:
1484 1483 # just apply a select few (safe?) attributes
1485 1484 for textattrib in (curses.A_UNDERLINE, curses.A_BOLD):
1486 1485 if textattrib in attrlist:
1487 1486 colorpair |= textattrib
1488 1487 return colorpair
1489 1488
1490 1489 def initcolorpair(self, *args, **kwargs):
1491 1490 "same as getcolorpair."
1492 1491 self.getcolorpair(*args, **kwargs)
1493 1492
1494 1493 def helpwindow(self):
1495 1494 "print a help window to the screen. exit after any keypress."
1496 1495 helptext = _(
1497 1496 """ [press any key to return to the patch-display]
1498 1497
1499 1498 crecord allows you to interactively choose among the changes you have made,
1500 1499 and confirm only those changes you select for further processing by the command
1501 1500 you are running (commit/shelve/revert), after confirming the selected
1502 1501 changes, the unselected changes are still present in your working copy, so you
1503 1502 can use crecord multiple times to split large changes into smaller changesets.
1504 1503 the following are valid keystrokes:
1505 1504
1506 1505 x [space] : (un-)select item ([~]/[x] = partly/fully applied)
1507 1506 [enter] : (un-)select item and go to next item of same type
1508 1507 A : (un-)select all items
1509 1508 X : (un-)select all items between current and most-recent
1510 1509 up/down-arrow [k/j] : go to previous/next unfolded item
1511 1510 pgup/pgdn [K/J] : go to previous/next item of same type
1512 1511 right/left-arrow [l/h] : go to child item / parent item
1513 1512 shift-left-arrow [H] : go to parent header / fold selected header
1514 1513 g : go to the top
1515 1514 G : go to the bottom
1516 1515 f : fold / unfold item, hiding/revealing its children
1517 1516 F : fold / unfold parent item and all of its ancestors
1518 1517 ctrl-l : scroll the selected line to the top of the screen
1519 1518 m : edit / resume editing the commit message
1520 1519 e : edit the currently selected hunk
1521 1520 a : toggle amend mode, only with commit -i
1522 1521 c : confirm selected changes
1523 1522 r : review/edit and confirm selected changes
1524 1523 q : quit without confirming (no changes will be made)
1525 1524 ? : help (what you're currently reading)""")
1526 1525
1527 1526 helpwin = curses.newwin(self.yscreensize, 0, 0, 0)
1528 1527 helplines = helptext.split("\n")
1529 1528 helplines = helplines + [" "]*(
1530 1529 self.yscreensize - self.numstatuslines - len(helplines) - 1)
1531 1530 try:
1532 1531 for line in helplines:
1533 1532 self.printstring(helpwin, line, pairname="legend")
1534 1533 except curses.error:
1535 1534 pass
1536 1535 helpwin.refresh()
1537 1536 try:
1538 1537 with self.ui.timeblockedsection('crecord'):
1539 1538 helpwin.getkey()
1540 1539 except curses.error:
1541 1540 pass
1542 1541
1543 1542 def commitMessageWindow(self):
1544 1543 "Create a temporary commit message editing window on the screen."
1545 1544
1546 1545 curses.raw()
1547 1546 curses.def_prog_mode()
1548 1547 curses.endwin()
1549 1548 self.commenttext = self.ui.edit(self.commenttext, self.ui.username())
1550 1549 curses.cbreak()
1551 1550 self.stdscr.refresh()
1552 1551 self.stdscr.keypad(1) # allow arrow-keys to continue to function
1553 1552
1554 1553 def handlefirstlineevent(self):
1555 1554 """
1556 1555 Handle 'g' to navigate to the top most file in the ncurses window.
1557 1556 """
1558 1557 self.currentselecteditem = self.headerlist[0]
1559 1558 currentitem = self.currentselecteditem
1560 1559 # select the parent item recursively until we're at a header
1561 1560 while True:
1562 1561 nextitem = currentitem.parentitem()
1563 1562 if nextitem is None:
1564 1563 break
1565 1564 else:
1566 1565 currentitem = nextitem
1567 1566
1568 1567 self.currentselecteditem = currentitem
1569 1568
1570 1569 def handlelastlineevent(self):
1571 1570 """
1572 1571 Handle 'G' to navigate to the bottom most file/hunk/line depending
1573 1572 on the whether the fold is active or not.
1574 1573
1575 1574 If the bottom most file is folded, it navigates to that file and
1576 1575 stops there. If the bottom most file is unfolded, it navigates to
1577 1576 the bottom most hunk in that file and stops there. If the bottom most
1578 1577 hunk is unfolded, it navigates to the bottom most line in that hunk.
1579 1578 """
1580 1579 currentitem = self.currentselecteditem
1581 1580 nextitem = currentitem.nextitem()
1582 1581 # select the child item recursively until we're at a footer
1583 1582 while nextitem is not None:
1584 1583 nextitem = currentitem.nextitem()
1585 1584 if nextitem is None:
1586 1585 break
1587 1586 else:
1588 1587 currentitem = nextitem
1589 1588
1590 1589 self.currentselecteditem = currentitem
1591 1590 self.recenterdisplayedarea()
1592 1591
1593 1592 def confirmationwindow(self, windowtext):
1594 1593 "display an informational window, then wait for and return a keypress."
1595 1594
1596 1595 confirmwin = curses.newwin(self.yscreensize, 0, 0, 0)
1597 1596 try:
1598 1597 lines = windowtext.split("\n")
1599 1598 for line in lines:
1600 1599 self.printstring(confirmwin, line, pairname="selected")
1601 1600 except curses.error:
1602 1601 pass
1603 1602 self.stdscr.refresh()
1604 1603 confirmwin.refresh()
1605 1604 try:
1606 1605 with self.ui.timeblockedsection('crecord'):
1607 1606 response = chr(self.stdscr.getch())
1608 1607 except ValueError:
1609 1608 response = None
1610 1609
1611 1610 return response
1612 1611
1613 1612 def reviewcommit(self):
1614 1613 """ask for 'y' to be pressed to confirm selected. return True if
1615 1614 confirmed."""
1616 1615 confirmtext = _(
1617 1616 """If you answer yes to the following, your currently chosen patch chunks
1618 1617 will be loaded into an editor. To modify the patch, make the changes in your
1619 1618 editor and save. To accept the current patch as-is, close the editor without
1620 1619 saving.
1621 1620
1622 1621 note: don't add/remove lines unless you also modify the range information.
1623 1622 failing to follow this rule will result in the commit aborting.
1624 1623
1625 1624 are you sure you want to review/edit and confirm the selected changes [yn]?
1626 1625 """)
1627 1626 with self.ui.timeblockedsection('crecord'):
1628 1627 response = self.confirmationwindow(confirmtext)
1629 1628 if response is None:
1630 1629 response = "n"
1631 1630 if response.lower().startswith("y"):
1632 1631 return True
1633 1632 else:
1634 1633 return False
1635 1634
1636 1635 def toggleamend(self, opts, test):
1637 1636 """Toggle the amend flag.
1638 1637
1639 1638 When the amend flag is set, a commit will modify the most recently
1640 1639 committed changeset, instead of creating a new changeset. Otherwise, a
1641 1640 new changeset will be created (the normal commit behavior).
1642 1641 """
1643 1642
1644 1643 if opts.get('amend') is None:
1645 1644 opts['amend'] = True
1646 1645 msg = _("Amend option is turned on -- committing the currently "
1647 1646 "selected changes will not create a new changeset, but "
1648 1647 "instead update the most recently committed changeset.\n\n"
1649 1648 "Press any key to continue.")
1650 1649 elif opts.get('amend') is True:
1651 1650 opts['amend'] = None
1652 1651 msg = _("Amend option is turned off -- committing the currently "
1653 1652 "selected changes will create a new changeset.\n\n"
1654 1653 "Press any key to continue.")
1655 1654 if not test:
1656 1655 self.confirmationwindow(msg)
1657 1656
1658 1657 def recenterdisplayedarea(self):
1659 1658 """
1660 1659 once we scrolled with pg up pg down we can be pointing outside of the
1661 1660 display zone. we print the patch with towin=False to compute the
1662 1661 location of the selected item even though it is outside of the displayed
1663 1662 zone and then update the scroll.
1664 1663 """
1665 1664 self.printitem(towin=False)
1666 1665 self.updatescroll()
1667 1666
1668 1667 def toggleedit(self, item=None, test=False):
1669 1668 """
1670 1669 edit the currently selected chunk
1671 1670 """
1672 1671 def updateui(self):
1673 1672 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1674 1673 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1675 1674 self.updatescroll()
1676 1675 self.stdscr.refresh()
1677 1676 self.statuswin.refresh()
1678 1677 self.stdscr.keypad(1)
1679 1678
1680 1679 def editpatchwitheditor(self, chunk):
1681 1680 if chunk is None:
1682 1681 self.ui.write(_('cannot edit patch for whole file'))
1683 1682 self.ui.write("\n")
1684 1683 return None
1685 1684 if chunk.header.binary():
1686 1685 self.ui.write(_('cannot edit patch for binary file'))
1687 1686 self.ui.write("\n")
1688 1687 return None
1689 1688
1690 1689 # write the initial patch
1691 1690 patch = stringio()
1692 1691 patch.write(diffhelptext + hunkhelptext)
1693 1692 chunk.header.write(patch)
1694 1693 chunk.write(patch)
1695 1694
1696 1695 # start the editor and wait for it to complete
1697 1696 try:
1698 1697 patch = self.ui.edit(patch.getvalue(), "", action="diff")
1699 1698 except error.Abort as exc:
1700 1699 self.errorstr = str(exc)
1701 1700 return None
1702 1701 finally:
1703 1702 self.stdscr.clear()
1704 1703 self.stdscr.refresh()
1705 1704
1706 1705 # remove comment lines
1707 1706 patch = [line + '\n' for line in patch.splitlines()
1708 1707 if not line.startswith('#')]
1709 1708 return patchmod.parsepatch(patch)
1710 1709
1711 1710 if item is None:
1712 1711 item = self.currentselecteditem
1713 1712 if isinstance(item, uiheader):
1714 1713 return
1715 1714 if isinstance(item, uihunkline):
1716 1715 item = item.parentitem()
1717 1716 if not isinstance(item, uihunk):
1718 1717 return
1719 1718
1720 1719 # To go back to that hunk or its replacement at the end of the edit
1721 1720 itemindex = item.parentitem().hunks.index(item)
1722 1721
1723 1722 beforeadded, beforeremoved = item.added, item.removed
1724 1723 newpatches = editpatchwitheditor(self, item)
1725 1724 if newpatches is None:
1726 1725 if not test:
1727 1726 updateui(self)
1728 1727 return
1729 1728 header = item.header
1730 1729 editedhunkindex = header.hunks.index(item)
1731 1730 hunksbefore = header.hunks[:editedhunkindex]
1732 1731 hunksafter = header.hunks[editedhunkindex + 1:]
1733 1732 newpatchheader = newpatches[0]
1734 1733 newhunks = [uihunk(h, header) for h in newpatchheader.hunks]
1735 1734 newadded = sum([h.added for h in newhunks])
1736 1735 newremoved = sum([h.removed for h in newhunks])
1737 1736 offset = (newadded - beforeadded) - (newremoved - beforeremoved)
1738 1737
1739 1738 for h in hunksafter:
1740 1739 h.toline += offset
1741 1740 for h in newhunks:
1742 1741 h.folded = False
1743 1742 header.hunks = hunksbefore + newhunks + hunksafter
1744 1743 if self.emptypatch():
1745 1744 header.hunks = hunksbefore + [item] + hunksafter
1746 1745 self.currentselecteditem = header
1747 1746 if len(header.hunks) > itemindex:
1748 1747 self.currentselecteditem = header.hunks[itemindex]
1749 1748
1750 1749 if not test:
1751 1750 updateui(self)
1752 1751
1753 1752 def emptypatch(self):
1754 1753 item = self.headerlist
1755 1754 if not item:
1756 1755 return True
1757 1756 for header in item:
1758 1757 if header.hunks:
1759 1758 return False
1760 1759 return True
1761 1760
1762 1761 def handlekeypressed(self, keypressed, test=False):
1763 1762 """
1764 1763 Perform actions based on pressed keys.
1765 1764
1766 1765 Return true to exit the main loop.
1767 1766 """
1768 1767 keypressed = pycompat.bytestr(keypressed)
1769 1768 if keypressed in ["k", "KEY_UP"]:
1770 1769 self.uparrowevent()
1771 1770 elif keypressed in ["K", "KEY_PPAGE"]:
1772 1771 self.uparrowshiftevent()
1773 1772 elif keypressed in ["j", "KEY_DOWN"]:
1774 1773 self.downarrowevent()
1775 1774 elif keypressed in ["J", "KEY_NPAGE"]:
1776 1775 self.downarrowshiftevent()
1777 1776 elif keypressed in ["l", "KEY_RIGHT"]:
1778 1777 self.rightarrowevent()
1779 1778 elif keypressed in ["h", "KEY_LEFT"]:
1780 1779 self.leftarrowevent()
1781 1780 elif keypressed in ["H", "KEY_SLEFT"]:
1782 1781 self.leftarrowshiftevent()
1783 1782 elif keypressed in ["q"]:
1784 1783 raise error.Abort(_('user quit'))
1785 1784 elif keypressed in ['a']:
1786 1785 self.toggleamend(self.opts, test)
1787 1786 elif keypressed in ["c"]:
1788 1787 return True
1789 1788 elif keypressed in ["r"]:
1790 1789 if self.reviewcommit():
1791 1790 self.opts['review'] = True
1792 1791 return True
1793 1792 elif test and keypressed in ['R']:
1794 1793 self.opts['review'] = True
1795 1794 return True
1796 1795 elif keypressed in [' ', 'x']:
1797 1796 self.toggleapply()
1798 1797 elif keypressed in ['\n', 'KEY_ENTER']:
1799 1798 self.toggleapply()
1800 1799 self.nextsametype(test=test)
1801 1800 elif keypressed in ['X']:
1802 1801 self.toggleallbetween()
1803 1802 elif keypressed in ['A']:
1804 1803 self.toggleall()
1805 1804 elif keypressed in ['e']:
1806 1805 self.toggleedit(test=test)
1807 1806 elif keypressed in ["f"]:
1808 1807 self.togglefolded()
1809 1808 elif keypressed in ["F"]:
1810 1809 self.togglefolded(foldparent=True)
1811 1810 elif keypressed in ["m"]:
1812 1811 self.commitMessageWindow()
1813 1812 elif keypressed in ["g", "KEY_HOME"]:
1814 1813 self.handlefirstlineevent()
1815 1814 elif keypressed in ["G", "KEY_END"]:
1816 1815 self.handlelastlineevent()
1817 1816 elif keypressed in ["?"]:
1818 1817 self.helpwindow()
1819 1818 self.stdscr.clear()
1820 1819 self.stdscr.refresh()
1821 1820 elif curses.unctrl(keypressed) in ["^L"]:
1822 1821 # scroll the current line to the top of the screen, and redraw
1823 1822 # everything
1824 1823 self.scrolllines(self.selecteditemstartline)
1825 1824 self.stdscr.clear()
1826 1825 self.stdscr.refresh()
1827 1826
1828 1827 def main(self, stdscr):
1829 1828 """
1830 1829 method to be wrapped by curses.wrapper() for selecting chunks.
1831 1830 """
1832 1831
1833 1832 origsigwinch = sentinel = object()
1834 1833 if util.safehasattr(signal, 'SIGWINCH'):
1835 1834 origsigwinch = signal.signal(signal.SIGWINCH,
1836 1835 self.sigwinchhandler)
1837 1836 try:
1838 1837 return self._main(stdscr)
1839 1838 finally:
1840 1839 if origsigwinch is not sentinel:
1841 1840 signal.signal(signal.SIGWINCH, origsigwinch)
1842 1841
1843 1842 def _main(self, stdscr):
1844 1843 self.stdscr = stdscr
1845 1844 # error during initialization, cannot be printed in the curses
1846 1845 # interface, it should be printed by the calling code
1847 1846 self.initexc = None
1848 1847 self.yscreensize, self.xscreensize = self.stdscr.getmaxyx()
1849 1848
1850 1849 curses.start_color()
1851 1850 try:
1852 1851 curses.use_default_colors()
1853 1852 except curses.error:
1854 1853 self.usecolor = False
1855 1854
1856 1855 # In some situations we may have some cruft left on the "alternate
1857 1856 # screen" from another program (or previous iterations of ourself), and
1858 1857 # we won't clear it if the scroll region is small enough to comfortably
1859 1858 # fit on the terminal.
1860 1859 self.stdscr.clear()
1861 1860
1862 1861 # don't display the cursor
1863 1862 try:
1864 1863 curses.curs_set(0)
1865 1864 except curses.error:
1866 1865 pass
1867 1866
1868 1867 # available colors: black, blue, cyan, green, magenta, white, yellow
1869 1868 # init_pair(color_id, foreground_color, background_color)
1870 1869 self.initcolorpair(None, None, name="normal")
1871 1870 self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_MAGENTA,
1872 1871 name="selected")
1873 1872 self.initcolorpair(curses.COLOR_RED, None, name="deletion")
1874 1873 self.initcolorpair(curses.COLOR_GREEN, None, name="addition")
1875 1874 self.initcolorpair(curses.COLOR_WHITE, curses.COLOR_BLUE, name="legend")
1876 1875 # newwin([height, width,] begin_y, begin_x)
1877 1876 self.statuswin = curses.newwin(self.numstatuslines, 0, 0, 0)
1878 1877 self.statuswin.keypad(1) # interpret arrow-key, etc. esc sequences
1879 1878
1880 1879 # figure out how much space to allocate for the chunk-pad which is
1881 1880 # used for displaying the patch
1882 1881
1883 1882 # stupid hack to prevent getnumlinesdisplayed from failing
1884 1883 self.chunkpad = curses.newpad(1, self.xscreensize)
1885 1884
1886 1885 # add 1 so to account for last line text reaching end of line
1887 1886 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1888 1887
1889 1888 try:
1890 1889 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1891 1890 except curses.error:
1892 1891 self.initexc = fallbackerror(
1893 1892 _('this diff is too large to be displayed'))
1894 1893 return
1895 1894 # initialize selecteditemendline (initial start-line is 0)
1896 1895 self.selecteditemendline = self.getnumlinesdisplayed(
1897 1896 self.currentselecteditem, recursechildren=False)
1898 1897
1899 1898 while True:
1900 1899 self.updatescreen()
1901 1900 try:
1902 1901 with self.ui.timeblockedsection('crecord'):
1903 1902 keypressed = self.statuswin.getkey()
1904 1903 if self.errorstr is not None:
1905 1904 self.errorstr = None
1906 1905 continue
1907 1906 except curses.error:
1908 1907 keypressed = "foobar"
1909 1908 if self.handlekeypressed(keypressed):
1910 1909 break
1911 1910
1912 1911 if self.commenttext != "":
1913 1912 whitespaceremoved = re.sub(br"(?m)^\s.*(\n|$)", b"",
1914 1913 self.commenttext)
1915 1914 if whitespaceremoved != "":
1916 1915 self.opts['message'] = self.commenttext
General Comments 0
You need to be logged in to leave comments. Login now