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