##// END OF EJS Templates
py3: catch AttributeError too with ImportError...
Pulkit Goyal -
r44736:d3f776c4 default
parent child Browse files
Show More
@@ -1,577 +1,577 b''
1 # utility for color output for Mercurial commands
1 # utility for color output for Mercurial commands
2 #
2 #
3 # Copyright (C) 2007 Kevin Christen <kevin.christen@gmail.com> and other
3 # Copyright (C) 2007 Kevin Christen <kevin.christen@gmail.com> and other
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 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import re
10 import re
11
11
12 from .i18n import _
12 from .i18n import _
13 from .pycompat import getattr
13 from .pycompat import getattr
14
14
15 from . import (
15 from . import (
16 encoding,
16 encoding,
17 pycompat,
17 pycompat,
18 )
18 )
19
19
20 from .utils import stringutil
20 from .utils import stringutil
21
21
22 try:
22 try:
23 import curses
23 import curses
24
24
25 # Mapping from effect name to terminfo attribute name (or raw code) or
25 # Mapping from effect name to terminfo attribute name (or raw code) or
26 # color number. This will also force-load the curses module.
26 # color number. This will also force-load the curses module.
27 _baseterminfoparams = {
27 _baseterminfoparams = {
28 b'none': (True, b'sgr0', b''),
28 b'none': (True, b'sgr0', b''),
29 b'standout': (True, b'smso', b''),
29 b'standout': (True, b'smso', b''),
30 b'underline': (True, b'smul', b''),
30 b'underline': (True, b'smul', b''),
31 b'reverse': (True, b'rev', b''),
31 b'reverse': (True, b'rev', b''),
32 b'inverse': (True, b'rev', b''),
32 b'inverse': (True, b'rev', b''),
33 b'blink': (True, b'blink', b''),
33 b'blink': (True, b'blink', b''),
34 b'dim': (True, b'dim', b''),
34 b'dim': (True, b'dim', b''),
35 b'bold': (True, b'bold', b''),
35 b'bold': (True, b'bold', b''),
36 b'invisible': (True, b'invis', b''),
36 b'invisible': (True, b'invis', b''),
37 b'italic': (True, b'sitm', b''),
37 b'italic': (True, b'sitm', b''),
38 b'black': (False, curses.COLOR_BLACK, b''),
38 b'black': (False, curses.COLOR_BLACK, b''),
39 b'red': (False, curses.COLOR_RED, b''),
39 b'red': (False, curses.COLOR_RED, b''),
40 b'green': (False, curses.COLOR_GREEN, b''),
40 b'green': (False, curses.COLOR_GREEN, b''),
41 b'yellow': (False, curses.COLOR_YELLOW, b''),
41 b'yellow': (False, curses.COLOR_YELLOW, b''),
42 b'blue': (False, curses.COLOR_BLUE, b''),
42 b'blue': (False, curses.COLOR_BLUE, b''),
43 b'magenta': (False, curses.COLOR_MAGENTA, b''),
43 b'magenta': (False, curses.COLOR_MAGENTA, b''),
44 b'cyan': (False, curses.COLOR_CYAN, b''),
44 b'cyan': (False, curses.COLOR_CYAN, b''),
45 b'white': (False, curses.COLOR_WHITE, b''),
45 b'white': (False, curses.COLOR_WHITE, b''),
46 }
46 }
47 except ImportError:
47 except (ImportError, AttributeError):
48 curses = None
48 curses = None
49 _baseterminfoparams = {}
49 _baseterminfoparams = {}
50
50
51 # start and stop parameters for effects
51 # start and stop parameters for effects
52 _effects = {
52 _effects = {
53 b'none': 0,
53 b'none': 0,
54 b'black': 30,
54 b'black': 30,
55 b'red': 31,
55 b'red': 31,
56 b'green': 32,
56 b'green': 32,
57 b'yellow': 33,
57 b'yellow': 33,
58 b'blue': 34,
58 b'blue': 34,
59 b'magenta': 35,
59 b'magenta': 35,
60 b'cyan': 36,
60 b'cyan': 36,
61 b'white': 37,
61 b'white': 37,
62 b'bold': 1,
62 b'bold': 1,
63 b'italic': 3,
63 b'italic': 3,
64 b'underline': 4,
64 b'underline': 4,
65 b'inverse': 7,
65 b'inverse': 7,
66 b'dim': 2,
66 b'dim': 2,
67 b'black_background': 40,
67 b'black_background': 40,
68 b'red_background': 41,
68 b'red_background': 41,
69 b'green_background': 42,
69 b'green_background': 42,
70 b'yellow_background': 43,
70 b'yellow_background': 43,
71 b'blue_background': 44,
71 b'blue_background': 44,
72 b'purple_background': 45,
72 b'purple_background': 45,
73 b'cyan_background': 46,
73 b'cyan_background': 46,
74 b'white_background': 47,
74 b'white_background': 47,
75 }
75 }
76
76
77 _defaultstyles = {
77 _defaultstyles = {
78 b'grep.match': b'red bold',
78 b'grep.match': b'red bold',
79 b'grep.linenumber': b'green',
79 b'grep.linenumber': b'green',
80 b'grep.rev': b'blue',
80 b'grep.rev': b'blue',
81 b'grep.sep': b'cyan',
81 b'grep.sep': b'cyan',
82 b'grep.filename': b'magenta',
82 b'grep.filename': b'magenta',
83 b'grep.user': b'magenta',
83 b'grep.user': b'magenta',
84 b'grep.date': b'magenta',
84 b'grep.date': b'magenta',
85 b'grep.inserted': b'green bold',
85 b'grep.inserted': b'green bold',
86 b'grep.deleted': b'red bold',
86 b'grep.deleted': b'red bold',
87 b'bookmarks.active': b'green',
87 b'bookmarks.active': b'green',
88 b'branches.active': b'none',
88 b'branches.active': b'none',
89 b'branches.closed': b'black bold',
89 b'branches.closed': b'black bold',
90 b'branches.current': b'green',
90 b'branches.current': b'green',
91 b'branches.inactive': b'none',
91 b'branches.inactive': b'none',
92 b'diff.changed': b'white',
92 b'diff.changed': b'white',
93 b'diff.deleted': b'red',
93 b'diff.deleted': b'red',
94 b'diff.deleted.changed': b'red bold underline',
94 b'diff.deleted.changed': b'red bold underline',
95 b'diff.deleted.unchanged': b'red',
95 b'diff.deleted.unchanged': b'red',
96 b'diff.diffline': b'bold',
96 b'diff.diffline': b'bold',
97 b'diff.extended': b'cyan bold',
97 b'diff.extended': b'cyan bold',
98 b'diff.file_a': b'red bold',
98 b'diff.file_a': b'red bold',
99 b'diff.file_b': b'green bold',
99 b'diff.file_b': b'green bold',
100 b'diff.hunk': b'magenta',
100 b'diff.hunk': b'magenta',
101 b'diff.inserted': b'green',
101 b'diff.inserted': b'green',
102 b'diff.inserted.changed': b'green bold underline',
102 b'diff.inserted.changed': b'green bold underline',
103 b'diff.inserted.unchanged': b'green',
103 b'diff.inserted.unchanged': b'green',
104 b'diff.tab': b'',
104 b'diff.tab': b'',
105 b'diff.trailingwhitespace': b'bold red_background',
105 b'diff.trailingwhitespace': b'bold red_background',
106 b'changeset.public': b'',
106 b'changeset.public': b'',
107 b'changeset.draft': b'',
107 b'changeset.draft': b'',
108 b'changeset.secret': b'',
108 b'changeset.secret': b'',
109 b'diffstat.deleted': b'red',
109 b'diffstat.deleted': b'red',
110 b'diffstat.inserted': b'green',
110 b'diffstat.inserted': b'green',
111 b'formatvariant.name.mismatchconfig': b'red',
111 b'formatvariant.name.mismatchconfig': b'red',
112 b'formatvariant.name.mismatchdefault': b'yellow',
112 b'formatvariant.name.mismatchdefault': b'yellow',
113 b'formatvariant.name.uptodate': b'green',
113 b'formatvariant.name.uptodate': b'green',
114 b'formatvariant.repo.mismatchconfig': b'red',
114 b'formatvariant.repo.mismatchconfig': b'red',
115 b'formatvariant.repo.mismatchdefault': b'yellow',
115 b'formatvariant.repo.mismatchdefault': b'yellow',
116 b'formatvariant.repo.uptodate': b'green',
116 b'formatvariant.repo.uptodate': b'green',
117 b'formatvariant.config.special': b'yellow',
117 b'formatvariant.config.special': b'yellow',
118 b'formatvariant.config.default': b'green',
118 b'formatvariant.config.default': b'green',
119 b'formatvariant.default': b'',
119 b'formatvariant.default': b'',
120 b'histedit.remaining': b'red bold',
120 b'histedit.remaining': b'red bold',
121 b'ui.addremove.added': b'green',
121 b'ui.addremove.added': b'green',
122 b'ui.addremove.removed': b'red',
122 b'ui.addremove.removed': b'red',
123 b'ui.error': b'red',
123 b'ui.error': b'red',
124 b'ui.prompt': b'yellow',
124 b'ui.prompt': b'yellow',
125 b'log.changeset': b'yellow',
125 b'log.changeset': b'yellow',
126 b'patchbomb.finalsummary': b'',
126 b'patchbomb.finalsummary': b'',
127 b'patchbomb.from': b'magenta',
127 b'patchbomb.from': b'magenta',
128 b'patchbomb.to': b'cyan',
128 b'patchbomb.to': b'cyan',
129 b'patchbomb.subject': b'green',
129 b'patchbomb.subject': b'green',
130 b'patchbomb.diffstats': b'',
130 b'patchbomb.diffstats': b'',
131 b'rebase.rebased': b'blue',
131 b'rebase.rebased': b'blue',
132 b'rebase.remaining': b'red bold',
132 b'rebase.remaining': b'red bold',
133 b'resolve.resolved': b'green bold',
133 b'resolve.resolved': b'green bold',
134 b'resolve.unresolved': b'red bold',
134 b'resolve.unresolved': b'red bold',
135 b'shelve.age': b'cyan',
135 b'shelve.age': b'cyan',
136 b'shelve.newest': b'green bold',
136 b'shelve.newest': b'green bold',
137 b'shelve.name': b'blue bold',
137 b'shelve.name': b'blue bold',
138 b'status.added': b'green bold',
138 b'status.added': b'green bold',
139 b'status.clean': b'none',
139 b'status.clean': b'none',
140 b'status.copied': b'none',
140 b'status.copied': b'none',
141 b'status.deleted': b'cyan bold underline',
141 b'status.deleted': b'cyan bold underline',
142 b'status.ignored': b'black bold',
142 b'status.ignored': b'black bold',
143 b'status.modified': b'blue bold',
143 b'status.modified': b'blue bold',
144 b'status.removed': b'red bold',
144 b'status.removed': b'red bold',
145 b'status.unknown': b'magenta bold underline',
145 b'status.unknown': b'magenta bold underline',
146 b'tags.normal': b'green',
146 b'tags.normal': b'green',
147 b'tags.local': b'black bold',
147 b'tags.local': b'black bold',
148 b'upgrade-repo.requirement.preserved': b'cyan',
148 b'upgrade-repo.requirement.preserved': b'cyan',
149 b'upgrade-repo.requirement.added': b'green',
149 b'upgrade-repo.requirement.added': b'green',
150 b'upgrade-repo.requirement.removed': b'red',
150 b'upgrade-repo.requirement.removed': b'red',
151 }
151 }
152
152
153
153
154 def loadcolortable(ui, extname, colortable):
154 def loadcolortable(ui, extname, colortable):
155 _defaultstyles.update(colortable)
155 _defaultstyles.update(colortable)
156
156
157
157
158 def _terminfosetup(ui, mode, formatted):
158 def _terminfosetup(ui, mode, formatted):
159 '''Initialize terminfo data and the terminal if we're in terminfo mode.'''
159 '''Initialize terminfo data and the terminal if we're in terminfo mode.'''
160
160
161 # If we failed to load curses, we go ahead and return.
161 # If we failed to load curses, we go ahead and return.
162 if curses is None:
162 if curses is None:
163 return
163 return
164 # Otherwise, see what the config file says.
164 # Otherwise, see what the config file says.
165 if mode not in (b'auto', b'terminfo'):
165 if mode not in (b'auto', b'terminfo'):
166 return
166 return
167 ui._terminfoparams.update(_baseterminfoparams)
167 ui._terminfoparams.update(_baseterminfoparams)
168
168
169 for key, val in ui.configitems(b'color'):
169 for key, val in ui.configitems(b'color'):
170 if key.startswith(b'color.'):
170 if key.startswith(b'color.'):
171 newval = (False, int(val), b'')
171 newval = (False, int(val), b'')
172 ui._terminfoparams[key[6:]] = newval
172 ui._terminfoparams[key[6:]] = newval
173 elif key.startswith(b'terminfo.'):
173 elif key.startswith(b'terminfo.'):
174 newval = (True, b'', val.replace(b'\\E', b'\x1b'))
174 newval = (True, b'', val.replace(b'\\E', b'\x1b'))
175 ui._terminfoparams[key[9:]] = newval
175 ui._terminfoparams[key[9:]] = newval
176 try:
176 try:
177 curses.setupterm()
177 curses.setupterm()
178 except curses.error:
178 except curses.error:
179 ui._terminfoparams.clear()
179 ui._terminfoparams.clear()
180 return
180 return
181
181
182 for key, (b, e, c) in ui._terminfoparams.copy().items():
182 for key, (b, e, c) in ui._terminfoparams.copy().items():
183 if not b:
183 if not b:
184 continue
184 continue
185 if not c and not curses.tigetstr(pycompat.sysstr(e)):
185 if not c and not curses.tigetstr(pycompat.sysstr(e)):
186 # Most terminals don't support dim, invis, etc, so don't be
186 # Most terminals don't support dim, invis, etc, so don't be
187 # noisy and use ui.debug().
187 # noisy and use ui.debug().
188 ui.debug(b"no terminfo entry for %s\n" % e)
188 ui.debug(b"no terminfo entry for %s\n" % e)
189 del ui._terminfoparams[key]
189 del ui._terminfoparams[key]
190 if not curses.tigetstr('setaf') or not curses.tigetstr('setab'):
190 if not curses.tigetstr('setaf') or not curses.tigetstr('setab'):
191 # Only warn about missing terminfo entries if we explicitly asked for
191 # Only warn about missing terminfo entries if we explicitly asked for
192 # terminfo mode and we're in a formatted terminal.
192 # terminfo mode and we're in a formatted terminal.
193 if mode == b"terminfo" and formatted:
193 if mode == b"terminfo" and formatted:
194 ui.warn(
194 ui.warn(
195 _(
195 _(
196 b"no terminfo entry for setab/setaf: reverting to "
196 b"no terminfo entry for setab/setaf: reverting to "
197 b"ECMA-48 color\n"
197 b"ECMA-48 color\n"
198 )
198 )
199 )
199 )
200 ui._terminfoparams.clear()
200 ui._terminfoparams.clear()
201
201
202
202
203 def setup(ui):
203 def setup(ui):
204 """configure color on a ui
204 """configure color on a ui
205
205
206 That function both set the colormode for the ui object and read
206 That function both set the colormode for the ui object and read
207 the configuration looking for custom colors and effect definitions."""
207 the configuration looking for custom colors and effect definitions."""
208 mode = _modesetup(ui)
208 mode = _modesetup(ui)
209 ui._colormode = mode
209 ui._colormode = mode
210 if mode and mode != b'debug':
210 if mode and mode != b'debug':
211 configstyles(ui)
211 configstyles(ui)
212
212
213
213
214 def _modesetup(ui):
214 def _modesetup(ui):
215 if ui.plain(b'color'):
215 if ui.plain(b'color'):
216 return None
216 return None
217 config = ui.config(b'ui', b'color')
217 config = ui.config(b'ui', b'color')
218 if config == b'debug':
218 if config == b'debug':
219 return b'debug'
219 return b'debug'
220
220
221 auto = config == b'auto'
221 auto = config == b'auto'
222 always = False
222 always = False
223 if not auto and stringutil.parsebool(config):
223 if not auto and stringutil.parsebool(config):
224 # We want the config to behave like a boolean, "on" is actually auto,
224 # We want the config to behave like a boolean, "on" is actually auto,
225 # but "always" value is treated as a special case to reduce confusion.
225 # but "always" value is treated as a special case to reduce confusion.
226 if (
226 if (
227 ui.configsource(b'ui', b'color') == b'--color'
227 ui.configsource(b'ui', b'color') == b'--color'
228 or config == b'always'
228 or config == b'always'
229 ):
229 ):
230 always = True
230 always = True
231 else:
231 else:
232 auto = True
232 auto = True
233
233
234 if not always and not auto:
234 if not always and not auto:
235 return None
235 return None
236
236
237 formatted = always or (
237 formatted = always or (
238 encoding.environ.get(b'TERM') != b'dumb' and ui.formatted()
238 encoding.environ.get(b'TERM') != b'dumb' and ui.formatted()
239 )
239 )
240
240
241 mode = ui.config(b'color', b'mode')
241 mode = ui.config(b'color', b'mode')
242
242
243 # If pager is active, color.pagermode overrides color.mode.
243 # If pager is active, color.pagermode overrides color.mode.
244 if getattr(ui, 'pageractive', False):
244 if getattr(ui, 'pageractive', False):
245 mode = ui.config(b'color', b'pagermode', mode)
245 mode = ui.config(b'color', b'pagermode', mode)
246
246
247 realmode = mode
247 realmode = mode
248 if pycompat.iswindows:
248 if pycompat.iswindows:
249 from . import win32
249 from . import win32
250
250
251 term = encoding.environ.get(b'TERM')
251 term = encoding.environ.get(b'TERM')
252 # TERM won't be defined in a vanilla cmd.exe environment.
252 # TERM won't be defined in a vanilla cmd.exe environment.
253
253
254 # UNIX-like environments on Windows such as Cygwin and MSYS will
254 # UNIX-like environments on Windows such as Cygwin and MSYS will
255 # set TERM. They appear to make a best effort attempt at setting it
255 # set TERM. They appear to make a best effort attempt at setting it
256 # to something appropriate. However, not all environments with TERM
256 # to something appropriate. However, not all environments with TERM
257 # defined support ANSI.
257 # defined support ANSI.
258 ansienviron = term and b'xterm' in term
258 ansienviron = term and b'xterm' in term
259
259
260 if mode == b'auto':
260 if mode == b'auto':
261 # Since "ansi" could result in terminal gibberish, we error on the
261 # Since "ansi" could result in terminal gibberish, we error on the
262 # side of selecting "win32". However, if w32effects is not defined,
262 # side of selecting "win32". However, if w32effects is not defined,
263 # we almost certainly don't support "win32", so don't even try.
263 # we almost certainly don't support "win32", so don't even try.
264 # w32effects is not populated when stdout is redirected, so checking
264 # w32effects is not populated when stdout is redirected, so checking
265 # it first avoids win32 calls in a state known to error out.
265 # it first avoids win32 calls in a state known to error out.
266 if ansienviron or not w32effects or win32.enablevtmode():
266 if ansienviron or not w32effects or win32.enablevtmode():
267 realmode = b'ansi'
267 realmode = b'ansi'
268 else:
268 else:
269 realmode = b'win32'
269 realmode = b'win32'
270 # An empty w32effects is a clue that stdout is redirected, and thus
270 # An empty w32effects is a clue that stdout is redirected, and thus
271 # cannot enable VT mode.
271 # cannot enable VT mode.
272 elif mode == b'ansi' and w32effects and not ansienviron:
272 elif mode == b'ansi' and w32effects and not ansienviron:
273 win32.enablevtmode()
273 win32.enablevtmode()
274 elif mode == b'auto':
274 elif mode == b'auto':
275 realmode = b'ansi'
275 realmode = b'ansi'
276
276
277 def modewarn():
277 def modewarn():
278 # only warn if color.mode was explicitly set and we're in
278 # only warn if color.mode was explicitly set and we're in
279 # a formatted terminal
279 # a formatted terminal
280 if mode == realmode and formatted:
280 if mode == realmode and formatted:
281 ui.warn(_(b'warning: failed to set color mode to %s\n') % mode)
281 ui.warn(_(b'warning: failed to set color mode to %s\n') % mode)
282
282
283 if realmode == b'win32':
283 if realmode == b'win32':
284 ui._terminfoparams.clear()
284 ui._terminfoparams.clear()
285 if not w32effects:
285 if not w32effects:
286 modewarn()
286 modewarn()
287 return None
287 return None
288 elif realmode == b'ansi':
288 elif realmode == b'ansi':
289 ui._terminfoparams.clear()
289 ui._terminfoparams.clear()
290 elif realmode == b'terminfo':
290 elif realmode == b'terminfo':
291 _terminfosetup(ui, mode, formatted)
291 _terminfosetup(ui, mode, formatted)
292 if not ui._terminfoparams:
292 if not ui._terminfoparams:
293 ## FIXME Shouldn't we return None in this case too?
293 ## FIXME Shouldn't we return None in this case too?
294 modewarn()
294 modewarn()
295 realmode = b'ansi'
295 realmode = b'ansi'
296 else:
296 else:
297 return None
297 return None
298
298
299 if always or (auto and formatted):
299 if always or (auto and formatted):
300 return realmode
300 return realmode
301 return None
301 return None
302
302
303
303
304 def configstyles(ui):
304 def configstyles(ui):
305 ui._styles.update(_defaultstyles)
305 ui._styles.update(_defaultstyles)
306 for status, cfgeffects in ui.configitems(b'color'):
306 for status, cfgeffects in ui.configitems(b'color'):
307 if b'.' not in status or status.startswith((b'color.', b'terminfo.')):
307 if b'.' not in status or status.startswith((b'color.', b'terminfo.')):
308 continue
308 continue
309 cfgeffects = ui.configlist(b'color', status)
309 cfgeffects = ui.configlist(b'color', status)
310 if cfgeffects:
310 if cfgeffects:
311 good = []
311 good = []
312 for e in cfgeffects:
312 for e in cfgeffects:
313 if valideffect(ui, e):
313 if valideffect(ui, e):
314 good.append(e)
314 good.append(e)
315 else:
315 else:
316 ui.warn(
316 ui.warn(
317 _(
317 _(
318 b"ignoring unknown color/effect %s "
318 b"ignoring unknown color/effect %s "
319 b"(configured in color.%s)\n"
319 b"(configured in color.%s)\n"
320 )
320 )
321 % (stringutil.pprint(e), status)
321 % (stringutil.pprint(e), status)
322 )
322 )
323 ui._styles[status] = b' '.join(good)
323 ui._styles[status] = b' '.join(good)
324
324
325
325
326 def _activeeffects(ui):
326 def _activeeffects(ui):
327 '''Return the effects map for the color mode set on the ui.'''
327 '''Return the effects map for the color mode set on the ui.'''
328 if ui._colormode == b'win32':
328 if ui._colormode == b'win32':
329 return w32effects
329 return w32effects
330 elif ui._colormode is not None:
330 elif ui._colormode is not None:
331 return _effects
331 return _effects
332 return {}
332 return {}
333
333
334
334
335 def valideffect(ui, effect):
335 def valideffect(ui, effect):
336 """Determine if the effect is valid or not."""
336 """Determine if the effect is valid or not."""
337 return (not ui._terminfoparams and effect in _activeeffects(ui)) or (
337 return (not ui._terminfoparams and effect in _activeeffects(ui)) or (
338 effect in ui._terminfoparams or effect[:-11] in ui._terminfoparams
338 effect in ui._terminfoparams or effect[:-11] in ui._terminfoparams
339 )
339 )
340
340
341
341
342 def _effect_str(ui, effect):
342 def _effect_str(ui, effect):
343 '''Helper function for render_effects().'''
343 '''Helper function for render_effects().'''
344
344
345 bg = False
345 bg = False
346 if effect.endswith(b'_background'):
346 if effect.endswith(b'_background'):
347 bg = True
347 bg = True
348 effect = effect[:-11]
348 effect = effect[:-11]
349 try:
349 try:
350 attr, val, termcode = ui._terminfoparams[effect]
350 attr, val, termcode = ui._terminfoparams[effect]
351 except KeyError:
351 except KeyError:
352 return b''
352 return b''
353 if attr:
353 if attr:
354 if termcode:
354 if termcode:
355 return termcode
355 return termcode
356 else:
356 else:
357 return curses.tigetstr(pycompat.sysstr(val))
357 return curses.tigetstr(pycompat.sysstr(val))
358 elif bg:
358 elif bg:
359 return curses.tparm(curses.tigetstr('setab'), val)
359 return curses.tparm(curses.tigetstr('setab'), val)
360 else:
360 else:
361 return curses.tparm(curses.tigetstr('setaf'), val)
361 return curses.tparm(curses.tigetstr('setaf'), val)
362
362
363
363
364 def _mergeeffects(text, start, stop):
364 def _mergeeffects(text, start, stop):
365 """Insert start sequence at every occurrence of stop sequence
365 """Insert start sequence at every occurrence of stop sequence
366
366
367 >>> s = _mergeeffects(b'cyan', b'[C]', b'|')
367 >>> s = _mergeeffects(b'cyan', b'[C]', b'|')
368 >>> s = _mergeeffects(s + b'yellow', b'[Y]', b'|')
368 >>> s = _mergeeffects(s + b'yellow', b'[Y]', b'|')
369 >>> s = _mergeeffects(b'ma' + s + b'genta', b'[M]', b'|')
369 >>> s = _mergeeffects(b'ma' + s + b'genta', b'[M]', b'|')
370 >>> s = _mergeeffects(b'red' + s, b'[R]', b'|')
370 >>> s = _mergeeffects(b'red' + s, b'[R]', b'|')
371 >>> s
371 >>> s
372 '[R]red[M]ma[Y][C]cyan|[R][M][Y]yellow|[R][M]genta|'
372 '[R]red[M]ma[Y][C]cyan|[R][M][Y]yellow|[R][M]genta|'
373 """
373 """
374 parts = []
374 parts = []
375 for t in text.split(stop):
375 for t in text.split(stop):
376 if not t:
376 if not t:
377 continue
377 continue
378 parts.extend([start, t, stop])
378 parts.extend([start, t, stop])
379 return b''.join(parts)
379 return b''.join(parts)
380
380
381
381
382 def _render_effects(ui, text, effects):
382 def _render_effects(ui, text, effects):
383 """Wrap text in commands to turn on each effect."""
383 """Wrap text in commands to turn on each effect."""
384 if not text:
384 if not text:
385 return text
385 return text
386 if ui._terminfoparams:
386 if ui._terminfoparams:
387 start = b''.join(
387 start = b''.join(
388 _effect_str(ui, effect) for effect in [b'none'] + effects.split()
388 _effect_str(ui, effect) for effect in [b'none'] + effects.split()
389 )
389 )
390 stop = _effect_str(ui, b'none')
390 stop = _effect_str(ui, b'none')
391 else:
391 else:
392 activeeffects = _activeeffects(ui)
392 activeeffects = _activeeffects(ui)
393 start = [
393 start = [
394 pycompat.bytestr(activeeffects[e])
394 pycompat.bytestr(activeeffects[e])
395 for e in [b'none'] + effects.split()
395 for e in [b'none'] + effects.split()
396 ]
396 ]
397 start = b'\033[' + b';'.join(start) + b'm'
397 start = b'\033[' + b';'.join(start) + b'm'
398 stop = b'\033[' + pycompat.bytestr(activeeffects[b'none']) + b'm'
398 stop = b'\033[' + pycompat.bytestr(activeeffects[b'none']) + b'm'
399 return _mergeeffects(text, start, stop)
399 return _mergeeffects(text, start, stop)
400
400
401
401
402 _ansieffectre = re.compile(br'\x1b\[[0-9;]*m')
402 _ansieffectre = re.compile(br'\x1b\[[0-9;]*m')
403
403
404
404
405 def stripeffects(text):
405 def stripeffects(text):
406 """Strip ANSI control codes which could be inserted by colorlabel()"""
406 """Strip ANSI control codes which could be inserted by colorlabel()"""
407 return _ansieffectre.sub(b'', text)
407 return _ansieffectre.sub(b'', text)
408
408
409
409
410 def colorlabel(ui, msg, label):
410 def colorlabel(ui, msg, label):
411 """add color control code according to the mode"""
411 """add color control code according to the mode"""
412 if ui._colormode == b'debug':
412 if ui._colormode == b'debug':
413 if label and msg:
413 if label and msg:
414 if msg.endswith(b'\n'):
414 if msg.endswith(b'\n'):
415 msg = b"[%s|%s]\n" % (label, msg[:-1])
415 msg = b"[%s|%s]\n" % (label, msg[:-1])
416 else:
416 else:
417 msg = b"[%s|%s]" % (label, msg)
417 msg = b"[%s|%s]" % (label, msg)
418 elif ui._colormode is not None:
418 elif ui._colormode is not None:
419 effects = []
419 effects = []
420 for l in label.split():
420 for l in label.split():
421 s = ui._styles.get(l, b'')
421 s = ui._styles.get(l, b'')
422 if s:
422 if s:
423 effects.append(s)
423 effects.append(s)
424 elif valideffect(ui, l):
424 elif valideffect(ui, l):
425 effects.append(l)
425 effects.append(l)
426 effects = b' '.join(effects)
426 effects = b' '.join(effects)
427 if effects:
427 if effects:
428 msg = b'\n'.join(
428 msg = b'\n'.join(
429 [
429 [
430 _render_effects(ui, line, effects)
430 _render_effects(ui, line, effects)
431 for line in msg.split(b'\n')
431 for line in msg.split(b'\n')
432 ]
432 ]
433 )
433 )
434 return msg
434 return msg
435
435
436
436
437 w32effects = None
437 w32effects = None
438 if pycompat.iswindows:
438 if pycompat.iswindows:
439 import ctypes
439 import ctypes
440
440
441 _kernel32 = ctypes.windll.kernel32 # pytype: disable=module-attr
441 _kernel32 = ctypes.windll.kernel32 # pytype: disable=module-attr
442
442
443 _WORD = ctypes.c_ushort
443 _WORD = ctypes.c_ushort
444
444
445 _INVALID_HANDLE_VALUE = -1
445 _INVALID_HANDLE_VALUE = -1
446
446
447 class _COORD(ctypes.Structure):
447 class _COORD(ctypes.Structure):
448 _fields_ = [('X', ctypes.c_short), ('Y', ctypes.c_short)]
448 _fields_ = [('X', ctypes.c_short), ('Y', ctypes.c_short)]
449
449
450 class _SMALL_RECT(ctypes.Structure):
450 class _SMALL_RECT(ctypes.Structure):
451 _fields_ = [
451 _fields_ = [
452 ('Left', ctypes.c_short),
452 ('Left', ctypes.c_short),
453 ('Top', ctypes.c_short),
453 ('Top', ctypes.c_short),
454 ('Right', ctypes.c_short),
454 ('Right', ctypes.c_short),
455 ('Bottom', ctypes.c_short),
455 ('Bottom', ctypes.c_short),
456 ]
456 ]
457
457
458 class _CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
458 class _CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
459 _fields_ = [
459 _fields_ = [
460 ('dwSize', _COORD),
460 ('dwSize', _COORD),
461 ('dwCursorPosition', _COORD),
461 ('dwCursorPosition', _COORD),
462 ('wAttributes', _WORD),
462 ('wAttributes', _WORD),
463 ('srWindow', _SMALL_RECT),
463 ('srWindow', _SMALL_RECT),
464 ('dwMaximumWindowSize', _COORD),
464 ('dwMaximumWindowSize', _COORD),
465 ]
465 ]
466
466
467 _STD_OUTPUT_HANDLE = 0xFFFFFFF5 # (DWORD)-11
467 _STD_OUTPUT_HANDLE = 0xFFFFFFF5 # (DWORD)-11
468 _STD_ERROR_HANDLE = 0xFFFFFFF4 # (DWORD)-12
468 _STD_ERROR_HANDLE = 0xFFFFFFF4 # (DWORD)-12
469
469
470 _FOREGROUND_BLUE = 0x0001
470 _FOREGROUND_BLUE = 0x0001
471 _FOREGROUND_GREEN = 0x0002
471 _FOREGROUND_GREEN = 0x0002
472 _FOREGROUND_RED = 0x0004
472 _FOREGROUND_RED = 0x0004
473 _FOREGROUND_INTENSITY = 0x0008
473 _FOREGROUND_INTENSITY = 0x0008
474
474
475 _BACKGROUND_BLUE = 0x0010
475 _BACKGROUND_BLUE = 0x0010
476 _BACKGROUND_GREEN = 0x0020
476 _BACKGROUND_GREEN = 0x0020
477 _BACKGROUND_RED = 0x0040
477 _BACKGROUND_RED = 0x0040
478 _BACKGROUND_INTENSITY = 0x0080
478 _BACKGROUND_INTENSITY = 0x0080
479
479
480 _COMMON_LVB_REVERSE_VIDEO = 0x4000
480 _COMMON_LVB_REVERSE_VIDEO = 0x4000
481 _COMMON_LVB_UNDERSCORE = 0x8000
481 _COMMON_LVB_UNDERSCORE = 0x8000
482
482
483 # http://msdn.microsoft.com/en-us/library/ms682088%28VS.85%29.aspx
483 # http://msdn.microsoft.com/en-us/library/ms682088%28VS.85%29.aspx
484 w32effects = {
484 w32effects = {
485 b'none': -1,
485 b'none': -1,
486 b'black': 0,
486 b'black': 0,
487 b'red': _FOREGROUND_RED,
487 b'red': _FOREGROUND_RED,
488 b'green': _FOREGROUND_GREEN,
488 b'green': _FOREGROUND_GREEN,
489 b'yellow': _FOREGROUND_RED | _FOREGROUND_GREEN,
489 b'yellow': _FOREGROUND_RED | _FOREGROUND_GREEN,
490 b'blue': _FOREGROUND_BLUE,
490 b'blue': _FOREGROUND_BLUE,
491 b'magenta': _FOREGROUND_BLUE | _FOREGROUND_RED,
491 b'magenta': _FOREGROUND_BLUE | _FOREGROUND_RED,
492 b'cyan': _FOREGROUND_BLUE | _FOREGROUND_GREEN,
492 b'cyan': _FOREGROUND_BLUE | _FOREGROUND_GREEN,
493 b'white': _FOREGROUND_RED | _FOREGROUND_GREEN | _FOREGROUND_BLUE,
493 b'white': _FOREGROUND_RED | _FOREGROUND_GREEN | _FOREGROUND_BLUE,
494 b'bold': _FOREGROUND_INTENSITY,
494 b'bold': _FOREGROUND_INTENSITY,
495 b'black_background': 0x100, # unused value > 0x0f
495 b'black_background': 0x100, # unused value > 0x0f
496 b'red_background': _BACKGROUND_RED,
496 b'red_background': _BACKGROUND_RED,
497 b'green_background': _BACKGROUND_GREEN,
497 b'green_background': _BACKGROUND_GREEN,
498 b'yellow_background': _BACKGROUND_RED | _BACKGROUND_GREEN,
498 b'yellow_background': _BACKGROUND_RED | _BACKGROUND_GREEN,
499 b'blue_background': _BACKGROUND_BLUE,
499 b'blue_background': _BACKGROUND_BLUE,
500 b'purple_background': _BACKGROUND_BLUE | _BACKGROUND_RED,
500 b'purple_background': _BACKGROUND_BLUE | _BACKGROUND_RED,
501 b'cyan_background': _BACKGROUND_BLUE | _BACKGROUND_GREEN,
501 b'cyan_background': _BACKGROUND_BLUE | _BACKGROUND_GREEN,
502 b'white_background': (
502 b'white_background': (
503 _BACKGROUND_RED | _BACKGROUND_GREEN | _BACKGROUND_BLUE
503 _BACKGROUND_RED | _BACKGROUND_GREEN | _BACKGROUND_BLUE
504 ),
504 ),
505 b'bold_background': _BACKGROUND_INTENSITY,
505 b'bold_background': _BACKGROUND_INTENSITY,
506 b'underline': _COMMON_LVB_UNDERSCORE, # double-byte charsets only
506 b'underline': _COMMON_LVB_UNDERSCORE, # double-byte charsets only
507 b'inverse': _COMMON_LVB_REVERSE_VIDEO, # double-byte charsets only
507 b'inverse': _COMMON_LVB_REVERSE_VIDEO, # double-byte charsets only
508 }
508 }
509
509
510 passthrough = {
510 passthrough = {
511 _FOREGROUND_INTENSITY,
511 _FOREGROUND_INTENSITY,
512 _BACKGROUND_INTENSITY,
512 _BACKGROUND_INTENSITY,
513 _COMMON_LVB_UNDERSCORE,
513 _COMMON_LVB_UNDERSCORE,
514 _COMMON_LVB_REVERSE_VIDEO,
514 _COMMON_LVB_REVERSE_VIDEO,
515 }
515 }
516
516
517 stdout = _kernel32.GetStdHandle(
517 stdout = _kernel32.GetStdHandle(
518 _STD_OUTPUT_HANDLE
518 _STD_OUTPUT_HANDLE
519 ) # don't close the handle returned
519 ) # don't close the handle returned
520 if stdout is None or stdout == _INVALID_HANDLE_VALUE:
520 if stdout is None or stdout == _INVALID_HANDLE_VALUE:
521 w32effects = None
521 w32effects = None
522 else:
522 else:
523 csbi = _CONSOLE_SCREEN_BUFFER_INFO()
523 csbi = _CONSOLE_SCREEN_BUFFER_INFO()
524 if not _kernel32.GetConsoleScreenBufferInfo(stdout, ctypes.byref(csbi)):
524 if not _kernel32.GetConsoleScreenBufferInfo(stdout, ctypes.byref(csbi)):
525 # stdout may not support GetConsoleScreenBufferInfo()
525 # stdout may not support GetConsoleScreenBufferInfo()
526 # when called from subprocess or redirected
526 # when called from subprocess or redirected
527 w32effects = None
527 w32effects = None
528 else:
528 else:
529 origattr = csbi.wAttributes
529 origattr = csbi.wAttributes
530 ansire = re.compile(
530 ansire = re.compile(
531 br'\033\[([^m]*)m([^\033]*)(.*)', re.MULTILINE | re.DOTALL
531 br'\033\[([^m]*)m([^\033]*)(.*)', re.MULTILINE | re.DOTALL
532 )
532 )
533
533
534 def win32print(ui, writefunc, text, **opts):
534 def win32print(ui, writefunc, text, **opts):
535 label = opts.get('label', b'')
535 label = opts.get('label', b'')
536 attr = origattr
536 attr = origattr
537
537
538 def mapcolor(val, attr):
538 def mapcolor(val, attr):
539 if val == -1:
539 if val == -1:
540 return origattr
540 return origattr
541 elif val in passthrough:
541 elif val in passthrough:
542 return attr | val
542 return attr | val
543 elif val > 0x0F:
543 elif val > 0x0F:
544 return (val & 0x70) | (attr & 0x8F)
544 return (val & 0x70) | (attr & 0x8F)
545 else:
545 else:
546 return (val & 0x07) | (attr & 0xF8)
546 return (val & 0x07) | (attr & 0xF8)
547
547
548 # determine console attributes based on labels
548 # determine console attributes based on labels
549 for l in label.split():
549 for l in label.split():
550 style = ui._styles.get(l, b'')
550 style = ui._styles.get(l, b'')
551 for effect in style.split():
551 for effect in style.split():
552 try:
552 try:
553 attr = mapcolor(w32effects[effect], attr)
553 attr = mapcolor(w32effects[effect], attr)
554 except KeyError:
554 except KeyError:
555 # w32effects could not have certain attributes so we skip
555 # w32effects could not have certain attributes so we skip
556 # them if not found
556 # them if not found
557 pass
557 pass
558 # hack to ensure regexp finds data
558 # hack to ensure regexp finds data
559 if not text.startswith(b'\033['):
559 if not text.startswith(b'\033['):
560 text = b'\033[m' + text
560 text = b'\033[m' + text
561
561
562 # Look for ANSI-like codes embedded in text
562 # Look for ANSI-like codes embedded in text
563 m = re.match(ansire, text)
563 m = re.match(ansire, text)
564
564
565 try:
565 try:
566 while m:
566 while m:
567 for sattr in m.group(1).split(b';'):
567 for sattr in m.group(1).split(b';'):
568 if sattr:
568 if sattr:
569 attr = mapcolor(int(sattr), attr)
569 attr = mapcolor(int(sattr), attr)
570 ui.flush()
570 ui.flush()
571 _kernel32.SetConsoleTextAttribute(stdout, attr)
571 _kernel32.SetConsoleTextAttribute(stdout, attr)
572 writefunc(m.group(2))
572 writefunc(m.group(2))
573 m = re.match(ansire, m.group(3))
573 m = re.match(ansire, m.group(3))
574 finally:
574 finally:
575 # Explicitly reset original attributes
575 # Explicitly reset original attributes
576 ui.flush()
576 ui.flush()
577 _kernel32.SetConsoleTextAttribute(stdout, origattr)
577 _kernel32.SetConsoleTextAttribute(stdout, origattr)
@@ -1,2037 +1,2037 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 .pycompat import (
19 from .pycompat import (
20 getattr,
20 getattr,
21 open,
21 open,
22 )
22 )
23 from . import (
23 from . import (
24 encoding,
24 encoding,
25 error,
25 error,
26 patch as patchmod,
26 patch as patchmod,
27 pycompat,
27 pycompat,
28 scmutil,
28 scmutil,
29 util,
29 util,
30 )
30 )
31 from .utils import stringutil
31 from .utils import stringutil
32
32
33 stringio = util.stringio
33 stringio = util.stringio
34
34
35 # patch comments based on the git one
35 # patch comments based on the git one
36 diffhelptext = _(
36 diffhelptext = _(
37 """# To remove '-' lines, make them ' ' lines (context).
37 """# To remove '-' lines, make them ' ' lines (context).
38 # To remove '+' lines, delete them.
38 # To remove '+' lines, delete them.
39 # Lines starting with # will be removed from the patch.
39 # Lines starting with # will be removed from the patch.
40 """
40 """
41 )
41 )
42
42
43 hunkhelptext = _(
43 hunkhelptext = _(
44 """#
44 """#
45 # If the patch applies cleanly, the edited hunk will immediately be
45 # If the patch applies cleanly, the edited hunk will immediately be
46 # added to the record list. If it does not apply cleanly, a rejects file
46 # added to the record list. If it does not apply cleanly, a rejects file
47 # will be generated. You can use that when you try again. If all lines
47 # will be generated. You can use that when you try again. If all lines
48 # of the hunk are removed, then the edit is aborted and the hunk is left
48 # of the hunk are removed, then the edit is aborted and the hunk is left
49 # unchanged.
49 # unchanged.
50 """
50 """
51 )
51 )
52
52
53 patchhelptext = _(
53 patchhelptext = _(
54 """#
54 """#
55 # If the patch applies cleanly, the edited patch will immediately
55 # If the patch applies cleanly, the edited patch will immediately
56 # be finalised. If it does not apply cleanly, rejects files will be
56 # be finalised. If it does not apply cleanly, rejects files will be
57 # generated. You can use those when you try again.
57 # generated. You can use those when you try again.
58 """
58 """
59 )
59 )
60
60
61 try:
61 try:
62 import curses
62 import curses
63 import curses.ascii
63 import curses.ascii
64
64
65 curses.error
65 curses.error
66 except ImportError:
66 except (ImportError, AttributeError):
67 # I have no idea if wcurses works with crecord...
67 # I have no idea if wcurses works with crecord...
68 try:
68 try:
69 import wcurses as curses
69 import wcurses as curses
70
70
71 curses.error
71 curses.error
72 except ImportError:
72 except (ImportError, AttributeError):
73 # wcurses is not shipped on Windows by default, or python is not
73 # wcurses is not shipped on Windows by default, or python is not
74 # compiled with curses
74 # compiled with curses
75 curses = False
75 curses = False
76
76
77
77
78 class fallbackerror(error.Abort):
78 class fallbackerror(error.Abort):
79 """Error that indicates the client should try to fallback to text mode."""
79 """Error that indicates the client should try to fallback to text mode."""
80
80
81 # Inherits from error.Abort so that existing behavior is preserved if the
81 # Inherits from error.Abort so that existing behavior is preserved if the
82 # calling code does not know how to fallback.
82 # calling code does not know how to fallback.
83
83
84
84
85 def checkcurses(ui):
85 def checkcurses(ui):
86 """Return True if the user wants to use curses
86 """Return True if the user wants to use curses
87
87
88 This method returns True if curses is found (and that python is built with
88 This method returns True if curses is found (and that python is built with
89 it) and that the user has the correct flag for the ui.
89 it) and that the user has the correct flag for the ui.
90 """
90 """
91 return curses and ui.interface(b"chunkselector") == b"curses"
91 return curses and ui.interface(b"chunkselector") == b"curses"
92
92
93
93
94 class patchnode(object):
94 class patchnode(object):
95 """abstract class for patch graph nodes
95 """abstract class for patch graph nodes
96 (i.e. patchroot, header, hunk, hunkline)
96 (i.e. patchroot, header, hunk, hunkline)
97 """
97 """
98
98
99 def firstchild(self):
99 def firstchild(self):
100 raise NotImplementedError(b"method must be implemented by subclass")
100 raise NotImplementedError(b"method must be implemented by subclass")
101
101
102 def lastchild(self):
102 def lastchild(self):
103 raise NotImplementedError(b"method must be implemented by subclass")
103 raise NotImplementedError(b"method must be implemented by subclass")
104
104
105 def allchildren(self):
105 def allchildren(self):
106 """Return a list of all of the direct children of this node"""
106 """Return a list of all of the direct children of this node"""
107 raise NotImplementedError(b"method must be implemented by subclass")
107 raise NotImplementedError(b"method must be implemented by subclass")
108
108
109 def nextsibling(self):
109 def nextsibling(self):
110 """
110 """
111 Return the closest next item of the same type where there are no items
111 Return the closest next item of the same type where there are no items
112 of different types between the current item and this closest item.
112 of different types between the current item and this closest item.
113 If no such item exists, return None.
113 If no such item exists, return None.
114 """
114 """
115 raise NotImplementedError(b"method must be implemented by subclass")
115 raise NotImplementedError(b"method must be implemented by subclass")
116
116
117 def prevsibling(self):
117 def prevsibling(self):
118 """
118 """
119 Return the closest previous item of the same type where there are no
119 Return the closest previous item of the same type where there are no
120 items of different types between the current item and this closest item.
120 items of different types between the current item and this closest item.
121 If no such item exists, return None.
121 If no such item exists, return None.
122 """
122 """
123 raise NotImplementedError(b"method must be implemented by subclass")
123 raise NotImplementedError(b"method must be implemented by subclass")
124
124
125 def parentitem(self):
125 def parentitem(self):
126 raise NotImplementedError(b"method must be implemented by subclass")
126 raise NotImplementedError(b"method must be implemented by subclass")
127
127
128 def nextitem(self, skipfolded=True):
128 def nextitem(self, skipfolded=True):
129 """
129 """
130 Try to return the next item closest to this item, regardless of item's
130 Try to return the next item closest to this item, regardless of item's
131 type (header, hunk, or hunkline).
131 type (header, hunk, or hunkline).
132
132
133 If skipfolded == True, and the current item is folded, then the child
133 If skipfolded == True, and the current item is folded, then the child
134 items that are hidden due to folding will be skipped when determining
134 items that are hidden due to folding will be skipped when determining
135 the next item.
135 the next item.
136
136
137 If it is not possible to get the next item, return None.
137 If it is not possible to get the next item, return None.
138 """
138 """
139 try:
139 try:
140 itemfolded = self.folded
140 itemfolded = self.folded
141 except AttributeError:
141 except AttributeError:
142 itemfolded = False
142 itemfolded = False
143 if skipfolded and itemfolded:
143 if skipfolded and itemfolded:
144 nextitem = self.nextsibling()
144 nextitem = self.nextsibling()
145 if nextitem is None:
145 if nextitem is None:
146 try:
146 try:
147 nextitem = self.parentitem().nextsibling()
147 nextitem = self.parentitem().nextsibling()
148 except AttributeError:
148 except AttributeError:
149 nextitem = None
149 nextitem = None
150 return nextitem
150 return nextitem
151 else:
151 else:
152 # try child
152 # try child
153 item = self.firstchild()
153 item = self.firstchild()
154 if item is not None:
154 if item is not None:
155 return item
155 return item
156
156
157 # else try next sibling
157 # else try next sibling
158 item = self.nextsibling()
158 item = self.nextsibling()
159 if item is not None:
159 if item is not None:
160 return item
160 return item
161
161
162 try:
162 try:
163 # else try parent's next sibling
163 # else try parent's next sibling
164 item = self.parentitem().nextsibling()
164 item = self.parentitem().nextsibling()
165 if item is not None:
165 if item is not None:
166 return item
166 return item
167
167
168 # else return grandparent's next sibling (or None)
168 # else return grandparent's next sibling (or None)
169 return self.parentitem().parentitem().nextsibling()
169 return self.parentitem().parentitem().nextsibling()
170
170
171 except AttributeError: # parent and/or grandparent was None
171 except AttributeError: # parent and/or grandparent was None
172 return None
172 return None
173
173
174 def previtem(self):
174 def previtem(self):
175 """
175 """
176 Try to return the previous item closest to this item, regardless of
176 Try to return the previous item closest to this item, regardless of
177 item's type (header, hunk, or hunkline).
177 item's type (header, hunk, or hunkline).
178
178
179 If it is not possible to get the previous item, return None.
179 If it is not possible to get the previous item, return None.
180 """
180 """
181 # try previous sibling's last child's last child,
181 # try previous sibling's last child's last child,
182 # else try previous sibling's last child, else try previous sibling
182 # else try previous sibling's last child, else try previous sibling
183 prevsibling = self.prevsibling()
183 prevsibling = self.prevsibling()
184 if prevsibling is not None:
184 if prevsibling is not None:
185 prevsiblinglastchild = prevsibling.lastchild()
185 prevsiblinglastchild = prevsibling.lastchild()
186 if (prevsiblinglastchild is not None) and not prevsibling.folded:
186 if (prevsiblinglastchild is not None) and not prevsibling.folded:
187 prevsiblinglclc = prevsiblinglastchild.lastchild()
187 prevsiblinglclc = prevsiblinglastchild.lastchild()
188 if (
188 if (
189 prevsiblinglclc is not None
189 prevsiblinglclc is not None
190 ) and not prevsiblinglastchild.folded:
190 ) and not prevsiblinglastchild.folded:
191 return prevsiblinglclc
191 return prevsiblinglclc
192 else:
192 else:
193 return prevsiblinglastchild
193 return prevsiblinglastchild
194 else:
194 else:
195 return prevsibling
195 return prevsibling
196
196
197 # try parent (or None)
197 # try parent (or None)
198 return self.parentitem()
198 return self.parentitem()
199
199
200
200
201 class patch(patchnode, list): # todo: rename patchroot
201 class patch(patchnode, list): # todo: rename patchroot
202 """
202 """
203 list of header objects representing the patch.
203 list of header objects representing the patch.
204 """
204 """
205
205
206 def __init__(self, headerlist):
206 def __init__(self, headerlist):
207 self.extend(headerlist)
207 self.extend(headerlist)
208 # add parent patch object reference to each header
208 # add parent patch object reference to each header
209 for header in self:
209 for header in self:
210 header.patch = self
210 header.patch = self
211
211
212
212
213 class uiheader(patchnode):
213 class uiheader(patchnode):
214 """patch header
214 """patch header
215
215
216 xxx shouldn't we move this to mercurial/patch.py ?
216 xxx shouldn't we move this to mercurial/patch.py ?
217 """
217 """
218
218
219 def __init__(self, header):
219 def __init__(self, header):
220 self.nonuiheader = header
220 self.nonuiheader = header
221 # flag to indicate whether to apply this chunk
221 # flag to indicate whether to apply this chunk
222 self.applied = True
222 self.applied = True
223 # flag which only affects the status display indicating if a node's
223 # flag which only affects the status display indicating if a node's
224 # children are partially applied (i.e. some applied, some not).
224 # children are partially applied (i.e. some applied, some not).
225 self.partial = False
225 self.partial = False
226
226
227 # flag to indicate whether to display as folded/unfolded to user
227 # flag to indicate whether to display as folded/unfolded to user
228 self.folded = True
228 self.folded = True
229
229
230 # list of all headers in patch
230 # list of all headers in patch
231 self.patch = None
231 self.patch = None
232
232
233 # flag is False if this header was ever unfolded from initial state
233 # flag is False if this header was ever unfolded from initial state
234 self.neverunfolded = True
234 self.neverunfolded = True
235 self.hunks = [uihunk(h, self) for h in self.hunks]
235 self.hunks = [uihunk(h, self) for h in self.hunks]
236
236
237 def prettystr(self):
237 def prettystr(self):
238 x = stringio()
238 x = stringio()
239 self.pretty(x)
239 self.pretty(x)
240 return x.getvalue()
240 return x.getvalue()
241
241
242 def nextsibling(self):
242 def nextsibling(self):
243 numheadersinpatch = len(self.patch)
243 numheadersinpatch = len(self.patch)
244 indexofthisheader = self.patch.index(self)
244 indexofthisheader = self.patch.index(self)
245
245
246 if indexofthisheader < numheadersinpatch - 1:
246 if indexofthisheader < numheadersinpatch - 1:
247 nextheader = self.patch[indexofthisheader + 1]
247 nextheader = self.patch[indexofthisheader + 1]
248 return nextheader
248 return nextheader
249 else:
249 else:
250 return None
250 return None
251
251
252 def prevsibling(self):
252 def prevsibling(self):
253 indexofthisheader = self.patch.index(self)
253 indexofthisheader = self.patch.index(self)
254 if indexofthisheader > 0:
254 if indexofthisheader > 0:
255 previousheader = self.patch[indexofthisheader - 1]
255 previousheader = self.patch[indexofthisheader - 1]
256 return previousheader
256 return previousheader
257 else:
257 else:
258 return None
258 return None
259
259
260 def parentitem(self):
260 def parentitem(self):
261 """
261 """
262 there is no 'real' parent item of a header that can be selected,
262 there is no 'real' parent item of a header that can be selected,
263 so return None.
263 so return None.
264 """
264 """
265 return None
265 return None
266
266
267 def firstchild(self):
267 def firstchild(self):
268 """return the first child of this item, if one exists. otherwise
268 """return the first child of this item, if one exists. otherwise
269 None."""
269 None."""
270 if len(self.hunks) > 0:
270 if len(self.hunks) > 0:
271 return self.hunks[0]
271 return self.hunks[0]
272 else:
272 else:
273 return None
273 return None
274
274
275 def lastchild(self):
275 def lastchild(self):
276 """return the last child of this item, if one exists. otherwise
276 """return the last child of this item, if one exists. otherwise
277 None."""
277 None."""
278 if len(self.hunks) > 0:
278 if len(self.hunks) > 0:
279 return self.hunks[-1]
279 return self.hunks[-1]
280 else:
280 else:
281 return None
281 return None
282
282
283 def allchildren(self):
283 def allchildren(self):
284 """return a list of all of the direct children of this node"""
284 """return a list of all of the direct children of this node"""
285 return self.hunks
285 return self.hunks
286
286
287 def __getattr__(self, name):
287 def __getattr__(self, name):
288 return getattr(self.nonuiheader, name)
288 return getattr(self.nonuiheader, name)
289
289
290
290
291 class uihunkline(patchnode):
291 class uihunkline(patchnode):
292 """represents a changed line in a hunk"""
292 """represents a changed line in a hunk"""
293
293
294 def __init__(self, linetext, hunk):
294 def __init__(self, linetext, hunk):
295 self.linetext = linetext
295 self.linetext = linetext
296 self.applied = True
296 self.applied = True
297 # the parent hunk to which this line belongs
297 # the parent hunk to which this line belongs
298 self.hunk = hunk
298 self.hunk = hunk
299 # folding lines currently is not used/needed, but this flag is needed
299 # folding lines currently is not used/needed, but this flag is needed
300 # in the previtem method.
300 # in the previtem method.
301 self.folded = False
301 self.folded = False
302
302
303 def prettystr(self):
303 def prettystr(self):
304 return self.linetext
304 return self.linetext
305
305
306 def nextsibling(self):
306 def nextsibling(self):
307 numlinesinhunk = len(self.hunk.changedlines)
307 numlinesinhunk = len(self.hunk.changedlines)
308 indexofthisline = self.hunk.changedlines.index(self)
308 indexofthisline = self.hunk.changedlines.index(self)
309
309
310 if indexofthisline < numlinesinhunk - 1:
310 if indexofthisline < numlinesinhunk - 1:
311 nextline = self.hunk.changedlines[indexofthisline + 1]
311 nextline = self.hunk.changedlines[indexofthisline + 1]
312 return nextline
312 return nextline
313 else:
313 else:
314 return None
314 return None
315
315
316 def prevsibling(self):
316 def prevsibling(self):
317 indexofthisline = self.hunk.changedlines.index(self)
317 indexofthisline = self.hunk.changedlines.index(self)
318 if indexofthisline > 0:
318 if indexofthisline > 0:
319 previousline = self.hunk.changedlines[indexofthisline - 1]
319 previousline = self.hunk.changedlines[indexofthisline - 1]
320 return previousline
320 return previousline
321 else:
321 else:
322 return None
322 return None
323
323
324 def parentitem(self):
324 def parentitem(self):
325 """return the parent to the current item"""
325 """return the parent to the current item"""
326 return self.hunk
326 return self.hunk
327
327
328 def firstchild(self):
328 def firstchild(self):
329 """return the first child of this item, if one exists. otherwise
329 """return the first child of this item, if one exists. otherwise
330 None."""
330 None."""
331 # hunk-lines don't have children
331 # hunk-lines don't have children
332 return None
332 return None
333
333
334 def lastchild(self):
334 def lastchild(self):
335 """return the last child of this item, if one exists. otherwise
335 """return the last child of this item, if one exists. otherwise
336 None."""
336 None."""
337 # hunk-lines don't have children
337 # hunk-lines don't have children
338 return None
338 return None
339
339
340
340
341 class uihunk(patchnode):
341 class uihunk(patchnode):
342 """ui patch hunk, wraps a hunk and keep track of ui behavior """
342 """ui patch hunk, wraps a hunk and keep track of ui behavior """
343
343
344 maxcontext = 3
344 maxcontext = 3
345
345
346 def __init__(self, hunk, header):
346 def __init__(self, hunk, header):
347 self._hunk = hunk
347 self._hunk = hunk
348 self.changedlines = [uihunkline(line, self) for line in hunk.hunk]
348 self.changedlines = [uihunkline(line, self) for line in hunk.hunk]
349 self.header = header
349 self.header = header
350 # used at end for detecting how many removed lines were un-applied
350 # used at end for detecting how many removed lines were un-applied
351 self.originalremoved = self.removed
351 self.originalremoved = self.removed
352
352
353 # flag to indicate whether to display as folded/unfolded to user
353 # flag to indicate whether to display as folded/unfolded to user
354 self.folded = True
354 self.folded = True
355 # flag to indicate whether to apply this chunk
355 # flag to indicate whether to apply this chunk
356 self.applied = True
356 self.applied = True
357 # flag which only affects the status display indicating if a node's
357 # flag which only affects the status display indicating if a node's
358 # children are partially applied (i.e. some applied, some not).
358 # children are partially applied (i.e. some applied, some not).
359 self.partial = False
359 self.partial = False
360
360
361 def nextsibling(self):
361 def nextsibling(self):
362 numhunksinheader = len(self.header.hunks)
362 numhunksinheader = len(self.header.hunks)
363 indexofthishunk = self.header.hunks.index(self)
363 indexofthishunk = self.header.hunks.index(self)
364
364
365 if indexofthishunk < numhunksinheader - 1:
365 if indexofthishunk < numhunksinheader - 1:
366 nexthunk = self.header.hunks[indexofthishunk + 1]
366 nexthunk = self.header.hunks[indexofthishunk + 1]
367 return nexthunk
367 return nexthunk
368 else:
368 else:
369 return None
369 return None
370
370
371 def prevsibling(self):
371 def prevsibling(self):
372 indexofthishunk = self.header.hunks.index(self)
372 indexofthishunk = self.header.hunks.index(self)
373 if indexofthishunk > 0:
373 if indexofthishunk > 0:
374 previoushunk = self.header.hunks[indexofthishunk - 1]
374 previoushunk = self.header.hunks[indexofthishunk - 1]
375 return previoushunk
375 return previoushunk
376 else:
376 else:
377 return None
377 return None
378
378
379 def parentitem(self):
379 def parentitem(self):
380 """return the parent to the current item"""
380 """return the parent to the current item"""
381 return self.header
381 return self.header
382
382
383 def firstchild(self):
383 def firstchild(self):
384 """return the first child of this item, if one exists. otherwise
384 """return the first child of this item, if one exists. otherwise
385 None."""
385 None."""
386 if len(self.changedlines) > 0:
386 if len(self.changedlines) > 0:
387 return self.changedlines[0]
387 return self.changedlines[0]
388 else:
388 else:
389 return None
389 return None
390
390
391 def lastchild(self):
391 def lastchild(self):
392 """return the last child of this item, if one exists. otherwise
392 """return the last child of this item, if one exists. otherwise
393 None."""
393 None."""
394 if len(self.changedlines) > 0:
394 if len(self.changedlines) > 0:
395 return self.changedlines[-1]
395 return self.changedlines[-1]
396 else:
396 else:
397 return None
397 return None
398
398
399 def allchildren(self):
399 def allchildren(self):
400 """return a list of all of the direct children of this node"""
400 """return a list of all of the direct children of this node"""
401 return self.changedlines
401 return self.changedlines
402
402
403 def countchanges(self):
403 def countchanges(self):
404 """changedlines -> (n+,n-)"""
404 """changedlines -> (n+,n-)"""
405 add = len(
405 add = len(
406 [
406 [
407 l
407 l
408 for l in self.changedlines
408 for l in self.changedlines
409 if l.applied and l.prettystr().startswith(b'+')
409 if l.applied and l.prettystr().startswith(b'+')
410 ]
410 ]
411 )
411 )
412 rem = len(
412 rem = len(
413 [
413 [
414 l
414 l
415 for l in self.changedlines
415 for l in self.changedlines
416 if l.applied and l.prettystr().startswith(b'-')
416 if l.applied and l.prettystr().startswith(b'-')
417 ]
417 ]
418 )
418 )
419 return add, rem
419 return add, rem
420
420
421 def getfromtoline(self):
421 def getfromtoline(self):
422 # calculate the number of removed lines converted to context lines
422 # calculate the number of removed lines converted to context lines
423 removedconvertedtocontext = self.originalremoved - self.removed
423 removedconvertedtocontext = self.originalremoved - self.removed
424
424
425 contextlen = (
425 contextlen = (
426 len(self.before) + len(self.after) + removedconvertedtocontext
426 len(self.before) + len(self.after) + removedconvertedtocontext
427 )
427 )
428 if self.after and self.after[-1] == b'\\ No newline at end of file\n':
428 if self.after and self.after[-1] == b'\\ No newline at end of file\n':
429 contextlen -= 1
429 contextlen -= 1
430 fromlen = contextlen + self.removed
430 fromlen = contextlen + self.removed
431 tolen = contextlen + self.added
431 tolen = contextlen + self.added
432
432
433 # diffutils manual, section "2.2.2.2 detailed description of unified
433 # diffutils manual, section "2.2.2.2 detailed description of unified
434 # format": "an empty hunk is considered to end at the line that
434 # format": "an empty hunk is considered to end at the line that
435 # precedes the hunk."
435 # precedes the hunk."
436 #
436 #
437 # so, if either of hunks is empty, decrease its line start. --immerrr
437 # so, if either of hunks is empty, decrease its line start. --immerrr
438 # but only do this if fromline > 0, to avoid having, e.g fromline=-1.
438 # but only do this if fromline > 0, to avoid having, e.g fromline=-1.
439 fromline, toline = self.fromline, self.toline
439 fromline, toline = self.fromline, self.toline
440 if fromline != 0:
440 if fromline != 0:
441 if fromlen == 0:
441 if fromlen == 0:
442 fromline -= 1
442 fromline -= 1
443 if tolen == 0 and toline > 0:
443 if tolen == 0 and toline > 0:
444 toline -= 1
444 toline -= 1
445
445
446 fromtoline = b'@@ -%d,%d +%d,%d @@%s\n' % (
446 fromtoline = b'@@ -%d,%d +%d,%d @@%s\n' % (
447 fromline,
447 fromline,
448 fromlen,
448 fromlen,
449 toline,
449 toline,
450 tolen,
450 tolen,
451 self.proc and (b' ' + self.proc),
451 self.proc and (b' ' + self.proc),
452 )
452 )
453 return fromtoline
453 return fromtoline
454
454
455 def write(self, fp):
455 def write(self, fp):
456 # updated self.added/removed, which are used by getfromtoline()
456 # updated self.added/removed, which are used by getfromtoline()
457 self.added, self.removed = self.countchanges()
457 self.added, self.removed = self.countchanges()
458 fp.write(self.getfromtoline())
458 fp.write(self.getfromtoline())
459
459
460 hunklinelist = []
460 hunklinelist = []
461 # add the following to the list: (1) all applied lines, and
461 # add the following to the list: (1) all applied lines, and
462 # (2) all unapplied removal lines (convert these to context lines)
462 # (2) all unapplied removal lines (convert these to context lines)
463 for changedline in self.changedlines:
463 for changedline in self.changedlines:
464 changedlinestr = changedline.prettystr()
464 changedlinestr = changedline.prettystr()
465 if changedline.applied:
465 if changedline.applied:
466 hunklinelist.append(changedlinestr)
466 hunklinelist.append(changedlinestr)
467 elif changedlinestr.startswith(b"-"):
467 elif changedlinestr.startswith(b"-"):
468 hunklinelist.append(b" " + changedlinestr[1:])
468 hunklinelist.append(b" " + changedlinestr[1:])
469
469
470 fp.write(b''.join(self.before + hunklinelist + self.after))
470 fp.write(b''.join(self.before + hunklinelist + self.after))
471
471
472 pretty = write
472 pretty = write
473
473
474 def prettystr(self):
474 def prettystr(self):
475 x = stringio()
475 x = stringio()
476 self.pretty(x)
476 self.pretty(x)
477 return x.getvalue()
477 return x.getvalue()
478
478
479 def reversehunk(self):
479 def reversehunk(self):
480 """return a recordhunk which is the reverse of the hunk
480 """return a recordhunk which is the reverse of the hunk
481
481
482 Assuming the displayed patch is diff(A, B) result. The returned hunk is
482 Assuming the displayed patch is diff(A, B) result. The returned hunk is
483 intended to be applied to B, instead of A.
483 intended to be applied to B, instead of A.
484
484
485 For example, when A is "0\n1\n2\n6\n" and B is "0\n3\n4\n5\n6\n", and
485 For example, when A is "0\n1\n2\n6\n" and B is "0\n3\n4\n5\n6\n", and
486 the user made the following selection:
486 the user made the following selection:
487
487
488 0
488 0
489 [x] -1 [x]: selected
489 [x] -1 [x]: selected
490 [ ] -2 [ ]: not selected
490 [ ] -2 [ ]: not selected
491 [x] +3
491 [x] +3
492 [ ] +4
492 [ ] +4
493 [x] +5
493 [x] +5
494 6
494 6
495
495
496 This function returns a hunk like:
496 This function returns a hunk like:
497
497
498 0
498 0
499 -3
499 -3
500 -4
500 -4
501 -5
501 -5
502 +1
502 +1
503 +4
503 +4
504 6
504 6
505
505
506 Note "4" was first deleted then added. That's because "4" exists in B
506 Note "4" was first deleted then added. That's because "4" exists in B
507 side and "-4" must exist between "-3" and "-5" to make the patch
507 side and "-4" must exist between "-3" and "-5" to make the patch
508 applicable to B.
508 applicable to B.
509 """
509 """
510 dels = []
510 dels = []
511 adds = []
511 adds = []
512 for line in self.changedlines:
512 for line in self.changedlines:
513 text = line.linetext
513 text = line.linetext
514 if line.applied:
514 if line.applied:
515 if text.startswith(b'+'):
515 if text.startswith(b'+'):
516 dels.append(text[1:])
516 dels.append(text[1:])
517 elif text.startswith(b'-'):
517 elif text.startswith(b'-'):
518 adds.append(text[1:])
518 adds.append(text[1:])
519 elif text.startswith(b'+'):
519 elif text.startswith(b'+'):
520 dels.append(text[1:])
520 dels.append(text[1:])
521 adds.append(text[1:])
521 adds.append(text[1:])
522 hunk = [b'-%s' % l for l in dels] + [b'+%s' % l for l in adds]
522 hunk = [b'-%s' % l for l in dels] + [b'+%s' % l for l in adds]
523 h = self._hunk
523 h = self._hunk
524 return patchmod.recordhunk(
524 return patchmod.recordhunk(
525 h.header, h.toline, h.fromline, h.proc, h.before, hunk, h.after
525 h.header, h.toline, h.fromline, h.proc, h.before, hunk, h.after
526 )
526 )
527
527
528 def __getattr__(self, name):
528 def __getattr__(self, name):
529 return getattr(self._hunk, name)
529 return getattr(self._hunk, name)
530
530
531 def __repr__(self):
531 def __repr__(self):
532 return '<hunk %r@%d>' % (self.filename(), self.fromline)
532 return '<hunk %r@%d>' % (self.filename(), self.fromline)
533
533
534
534
535 def filterpatch(ui, chunks, chunkselector, operation=None):
535 def filterpatch(ui, chunks, chunkselector, operation=None):
536 """interactively filter patch chunks into applied-only chunks"""
536 """interactively filter patch chunks into applied-only chunks"""
537 chunks = list(chunks)
537 chunks = list(chunks)
538 # convert chunks list into structure suitable for displaying/modifying
538 # convert chunks list into structure suitable for displaying/modifying
539 # with curses. create a list of headers only.
539 # with curses. create a list of headers only.
540 headers = [c for c in chunks if isinstance(c, patchmod.header)]
540 headers = [c for c in chunks if isinstance(c, patchmod.header)]
541
541
542 # if there are no changed files
542 # if there are no changed files
543 if len(headers) == 0:
543 if len(headers) == 0:
544 return [], {}
544 return [], {}
545 uiheaders = [uiheader(h) for h in headers]
545 uiheaders = [uiheader(h) for h in headers]
546 # let user choose headers/hunks/lines, and mark their applied flags
546 # let user choose headers/hunks/lines, and mark their applied flags
547 # accordingly
547 # accordingly
548 ret = chunkselector(ui, uiheaders, operation=operation)
548 ret = chunkselector(ui, uiheaders, operation=operation)
549 appliedhunklist = []
549 appliedhunklist = []
550 for hdr in uiheaders:
550 for hdr in uiheaders:
551 if hdr.applied and (
551 if hdr.applied and (
552 hdr.special() or len([h for h in hdr.hunks if h.applied]) > 0
552 hdr.special() or len([h for h in hdr.hunks if h.applied]) > 0
553 ):
553 ):
554 appliedhunklist.append(hdr)
554 appliedhunklist.append(hdr)
555 fixoffset = 0
555 fixoffset = 0
556 for hnk in hdr.hunks:
556 for hnk in hdr.hunks:
557 if hnk.applied:
557 if hnk.applied:
558 appliedhunklist.append(hnk)
558 appliedhunklist.append(hnk)
559 # adjust the 'to'-line offset of the hunk to be correct
559 # adjust the 'to'-line offset of the hunk to be correct
560 # after de-activating some of the other hunks for this file
560 # after de-activating some of the other hunks for this file
561 if fixoffset:
561 if fixoffset:
562 # hnk = copy.copy(hnk) # necessary??
562 # hnk = copy.copy(hnk) # necessary??
563 hnk.toline += fixoffset
563 hnk.toline += fixoffset
564 else:
564 else:
565 fixoffset += hnk.removed - hnk.added
565 fixoffset += hnk.removed - hnk.added
566
566
567 return (appliedhunklist, ret)
567 return (appliedhunklist, ret)
568
568
569
569
570 def chunkselector(ui, headerlist, operation=None):
570 def chunkselector(ui, headerlist, operation=None):
571 """
571 """
572 curses interface to get selection of chunks, and mark the applied flags
572 curses interface to get selection of chunks, and mark the applied flags
573 of the chosen chunks.
573 of the chosen chunks.
574 """
574 """
575 ui.write(_(b'starting interactive selection\n'))
575 ui.write(_(b'starting interactive selection\n'))
576 chunkselector = curseschunkselector(headerlist, ui, operation)
576 chunkselector = curseschunkselector(headerlist, ui, operation)
577 # This is required for ncurses to display non-ASCII characters in
577 # This is required for ncurses to display non-ASCII characters in
578 # default user locale encoding correctly. --immerrr
578 # default user locale encoding correctly. --immerrr
579 locale.setlocale(locale.LC_ALL, '')
579 locale.setlocale(locale.LC_ALL, '')
580 origsigtstp = sentinel = object()
580 origsigtstp = sentinel = object()
581 if util.safehasattr(signal, b'SIGTSTP'):
581 if util.safehasattr(signal, b'SIGTSTP'):
582 origsigtstp = signal.getsignal(signal.SIGTSTP)
582 origsigtstp = signal.getsignal(signal.SIGTSTP)
583 try:
583 try:
584 curses.wrapper(chunkselector.main)
584 curses.wrapper(chunkselector.main)
585 if chunkselector.initexc is not None:
585 if chunkselector.initexc is not None:
586 raise chunkselector.initexc
586 raise chunkselector.initexc
587 # ncurses does not restore signal handler for SIGTSTP
587 # ncurses does not restore signal handler for SIGTSTP
588 finally:
588 finally:
589 if origsigtstp is not sentinel:
589 if origsigtstp is not sentinel:
590 signal.signal(signal.SIGTSTP, origsigtstp)
590 signal.signal(signal.SIGTSTP, origsigtstp)
591 return chunkselector.opts
591 return chunkselector.opts
592
592
593
593
594 def testdecorator(testfn, f):
594 def testdecorator(testfn, f):
595 def u(*args, **kwargs):
595 def u(*args, **kwargs):
596 return f(testfn, *args, **kwargs)
596 return f(testfn, *args, **kwargs)
597
597
598 return u
598 return u
599
599
600
600
601 def testchunkselector(testfn, ui, headerlist, operation=None):
601 def testchunkselector(testfn, ui, headerlist, operation=None):
602 """
602 """
603 test interface to get selection of chunks, and mark the applied flags
603 test interface to get selection of chunks, and mark the applied flags
604 of the chosen chunks.
604 of the chosen chunks.
605 """
605 """
606 chunkselector = curseschunkselector(headerlist, ui, operation)
606 chunkselector = curseschunkselector(headerlist, ui, operation)
607
607
608 class dummystdscr(object):
608 class dummystdscr(object):
609 def clear(self):
609 def clear(self):
610 pass
610 pass
611
611
612 def refresh(self):
612 def refresh(self):
613 pass
613 pass
614
614
615 chunkselector.stdscr = dummystdscr()
615 chunkselector.stdscr = dummystdscr()
616 if testfn and os.path.exists(testfn):
616 if testfn and os.path.exists(testfn):
617 testf = open(testfn, 'r')
617 testf = open(testfn, 'r')
618 testcommands = [x.rstrip('\n') for x in testf.readlines()]
618 testcommands = [x.rstrip('\n') for x in testf.readlines()]
619 testf.close()
619 testf.close()
620 while True:
620 while True:
621 if chunkselector.handlekeypressed(testcommands.pop(0), test=True):
621 if chunkselector.handlekeypressed(testcommands.pop(0), test=True):
622 break
622 break
623 return chunkselector.opts
623 return chunkselector.opts
624
624
625
625
626 _headermessages = { # {operation: text}
626 _headermessages = { # {operation: text}
627 b'apply': _(b'Select hunks to apply'),
627 b'apply': _(b'Select hunks to apply'),
628 b'discard': _(b'Select hunks to discard'),
628 b'discard': _(b'Select hunks to discard'),
629 b'keep': _(b'Select hunks to keep'),
629 b'keep': _(b'Select hunks to keep'),
630 None: _(b'Select hunks to record'),
630 None: _(b'Select hunks to record'),
631 }
631 }
632
632
633
633
634 class curseschunkselector(object):
634 class curseschunkselector(object):
635 def __init__(self, headerlist, ui, operation=None):
635 def __init__(self, headerlist, ui, operation=None):
636 # put the headers into a patch object
636 # put the headers into a patch object
637 self.headerlist = patch(headerlist)
637 self.headerlist = patch(headerlist)
638
638
639 self.ui = ui
639 self.ui = ui
640 self.opts = {}
640 self.opts = {}
641
641
642 self.errorstr = None
642 self.errorstr = None
643 # list of all chunks
643 # list of all chunks
644 self.chunklist = []
644 self.chunklist = []
645 for h in headerlist:
645 for h in headerlist:
646 self.chunklist.append(h)
646 self.chunklist.append(h)
647 self.chunklist.extend(h.hunks)
647 self.chunklist.extend(h.hunks)
648
648
649 # dictionary mapping (fgcolor, bgcolor) pairs to the
649 # dictionary mapping (fgcolor, bgcolor) pairs to the
650 # corresponding curses color-pair value.
650 # corresponding curses color-pair value.
651 self.colorpairs = {}
651 self.colorpairs = {}
652 # maps custom nicknames of color-pairs to curses color-pair values
652 # maps custom nicknames of color-pairs to curses color-pair values
653 self.colorpairnames = {}
653 self.colorpairnames = {}
654
654
655 # Honor color setting of ui section. Keep colored setup as
655 # Honor color setting of ui section. Keep colored setup as
656 # long as not explicitly set to a falsy value - especially,
656 # long as not explicitly set to a falsy value - especially,
657 # when not set at all. This is to stay most compatible with
657 # when not set at all. This is to stay most compatible with
658 # previous (color only) behaviour.
658 # previous (color only) behaviour.
659 uicolor = stringutil.parsebool(self.ui.config(b'ui', b'color'))
659 uicolor = stringutil.parsebool(self.ui.config(b'ui', b'color'))
660 self.usecolor = uicolor is not False
660 self.usecolor = uicolor is not False
661
661
662 # the currently selected header, hunk, or hunk-line
662 # the currently selected header, hunk, or hunk-line
663 self.currentselecteditem = self.headerlist[0]
663 self.currentselecteditem = self.headerlist[0]
664 self.lastapplieditem = None
664 self.lastapplieditem = None
665
665
666 # updated when printing out patch-display -- the 'lines' here are the
666 # updated when printing out patch-display -- the 'lines' here are the
667 # line positions *in the pad*, not on the screen.
667 # line positions *in the pad*, not on the screen.
668 self.selecteditemstartline = 0
668 self.selecteditemstartline = 0
669 self.selecteditemendline = None
669 self.selecteditemendline = None
670
670
671 # define indentation levels
671 # define indentation levels
672 self.headerindentnumchars = 0
672 self.headerindentnumchars = 0
673 self.hunkindentnumchars = 3
673 self.hunkindentnumchars = 3
674 self.hunklineindentnumchars = 6
674 self.hunklineindentnumchars = 6
675
675
676 # the first line of the pad to print to the screen
676 # the first line of the pad to print to the screen
677 self.firstlineofpadtoprint = 0
677 self.firstlineofpadtoprint = 0
678
678
679 # keeps track of the number of lines in the pad
679 # keeps track of the number of lines in the pad
680 self.numpadlines = None
680 self.numpadlines = None
681
681
682 self.numstatuslines = 1
682 self.numstatuslines = 1
683
683
684 # keep a running count of the number of lines printed to the pad
684 # keep a running count of the number of lines printed to the pad
685 # (used for determining when the selected item begins/ends)
685 # (used for determining when the selected item begins/ends)
686 self.linesprintedtopadsofar = 0
686 self.linesprintedtopadsofar = 0
687
687
688 # stores optional text for a commit comment provided by the user
688 # stores optional text for a commit comment provided by the user
689 self.commenttext = b""
689 self.commenttext = b""
690
690
691 # if the last 'toggle all' command caused all changes to be applied
691 # if the last 'toggle all' command caused all changes to be applied
692 self.waslasttoggleallapplied = True
692 self.waslasttoggleallapplied = True
693
693
694 # affects some ui text
694 # affects some ui text
695 if operation not in _headermessages:
695 if operation not in _headermessages:
696 raise error.ProgrammingError(
696 raise error.ProgrammingError(
697 b'unexpected operation: %s' % operation
697 b'unexpected operation: %s' % operation
698 )
698 )
699 self.operation = operation
699 self.operation = operation
700
700
701 def uparrowevent(self):
701 def uparrowevent(self):
702 """
702 """
703 try to select the previous item to the current item that has the
703 try to select the previous item to the current item that has the
704 most-indented level. for example, if a hunk is selected, try to select
704 most-indented level. for example, if a hunk is selected, try to select
705 the last hunkline of the hunk prior to the selected hunk. or, if
705 the last hunkline of the hunk prior to the selected hunk. or, if
706 the first hunkline of a hunk is currently selected, then select the
706 the first hunkline of a hunk is currently selected, then select the
707 hunk itself.
707 hunk itself.
708 """
708 """
709 currentitem = self.currentselecteditem
709 currentitem = self.currentselecteditem
710
710
711 nextitem = currentitem.previtem()
711 nextitem = currentitem.previtem()
712
712
713 if nextitem is None:
713 if nextitem is None:
714 # if no parent item (i.e. currentitem is the first header), then
714 # if no parent item (i.e. currentitem is the first header), then
715 # no change...
715 # no change...
716 nextitem = currentitem
716 nextitem = currentitem
717
717
718 self.currentselecteditem = nextitem
718 self.currentselecteditem = nextitem
719
719
720 def uparrowshiftevent(self):
720 def uparrowshiftevent(self):
721 """
721 """
722 select (if possible) the previous item on the same level as the
722 select (if possible) the previous item on the same level as the
723 currently selected item. otherwise, select (if possible) the
723 currently selected item. otherwise, select (if possible) the
724 parent-item of the currently selected item.
724 parent-item of the currently selected item.
725 """
725 """
726 currentitem = self.currentselecteditem
726 currentitem = self.currentselecteditem
727 nextitem = currentitem.prevsibling()
727 nextitem = currentitem.prevsibling()
728 # if there's no previous sibling, try choosing the parent
728 # if there's no previous sibling, try choosing the parent
729 if nextitem is None:
729 if nextitem is None:
730 nextitem = currentitem.parentitem()
730 nextitem = currentitem.parentitem()
731 if nextitem is None:
731 if nextitem is None:
732 # if no parent item (i.e. currentitem is the first header), then
732 # if no parent item (i.e. currentitem is the first header), then
733 # no change...
733 # no change...
734 nextitem = currentitem
734 nextitem = currentitem
735
735
736 self.currentselecteditem = nextitem
736 self.currentselecteditem = nextitem
737 self.recenterdisplayedarea()
737 self.recenterdisplayedarea()
738
738
739 def downarrowevent(self):
739 def downarrowevent(self):
740 """
740 """
741 try to select the next item to the current item that has the
741 try to select the next item to the current item that has the
742 most-indented level. for example, if a hunk is selected, select
742 most-indented level. for example, if a hunk is selected, select
743 the first hunkline of the selected hunk. or, if the last hunkline of
743 the first hunkline of the selected hunk. or, if the last hunkline of
744 a hunk is currently selected, then select the next hunk, if one exists,
744 a hunk is currently selected, then select the next hunk, if one exists,
745 or if not, the next header if one exists.
745 or if not, the next header if one exists.
746 """
746 """
747 # self.startprintline += 1 #debug
747 # self.startprintline += 1 #debug
748 currentitem = self.currentselecteditem
748 currentitem = self.currentselecteditem
749
749
750 nextitem = currentitem.nextitem()
750 nextitem = currentitem.nextitem()
751 # if there's no next item, keep the selection as-is
751 # if there's no next item, keep the selection as-is
752 if nextitem is None:
752 if nextitem is None:
753 nextitem = currentitem
753 nextitem = currentitem
754
754
755 self.currentselecteditem = nextitem
755 self.currentselecteditem = nextitem
756
756
757 def downarrowshiftevent(self):
757 def downarrowshiftevent(self):
758 """
758 """
759 select (if possible) the next item on the same level as the currently
759 select (if possible) the next item on the same level as the currently
760 selected item. otherwise, select (if possible) the next item on the
760 selected item. otherwise, select (if possible) the next item on the
761 same level as the parent item of the currently selected item.
761 same level as the parent item of the currently selected item.
762 """
762 """
763 currentitem = self.currentselecteditem
763 currentitem = self.currentselecteditem
764 nextitem = currentitem.nextsibling()
764 nextitem = currentitem.nextsibling()
765 # if there's no next sibling, try choosing the parent's nextsibling
765 # if there's no next sibling, try choosing the parent's nextsibling
766 if nextitem is None:
766 if nextitem is None:
767 try:
767 try:
768 nextitem = currentitem.parentitem().nextsibling()
768 nextitem = currentitem.parentitem().nextsibling()
769 except AttributeError:
769 except AttributeError:
770 # parentitem returned None, so nextsibling() can't be called
770 # parentitem returned None, so nextsibling() can't be called
771 nextitem = None
771 nextitem = None
772 if nextitem is None:
772 if nextitem is None:
773 # if parent has no next sibling, then no change...
773 # if parent has no next sibling, then no change...
774 nextitem = currentitem
774 nextitem = currentitem
775
775
776 self.currentselecteditem = nextitem
776 self.currentselecteditem = nextitem
777 self.recenterdisplayedarea()
777 self.recenterdisplayedarea()
778
778
779 def nextsametype(self, test=False):
779 def nextsametype(self, test=False):
780 currentitem = self.currentselecteditem
780 currentitem = self.currentselecteditem
781 sametype = lambda item: isinstance(item, type(currentitem))
781 sametype = lambda item: isinstance(item, type(currentitem))
782 nextitem = currentitem.nextitem()
782 nextitem = currentitem.nextitem()
783
783
784 while nextitem is not None and not sametype(nextitem):
784 while nextitem is not None and not sametype(nextitem):
785 nextitem = nextitem.nextitem()
785 nextitem = nextitem.nextitem()
786
786
787 if nextitem is None:
787 if nextitem is None:
788 nextitem = currentitem
788 nextitem = currentitem
789 else:
789 else:
790 parent = nextitem.parentitem()
790 parent = nextitem.parentitem()
791 if parent is not None and parent.folded:
791 if parent is not None and parent.folded:
792 self.togglefolded(parent)
792 self.togglefolded(parent)
793
793
794 self.currentselecteditem = nextitem
794 self.currentselecteditem = nextitem
795 if not test:
795 if not test:
796 self.recenterdisplayedarea()
796 self.recenterdisplayedarea()
797
797
798 def rightarrowevent(self):
798 def rightarrowevent(self):
799 """
799 """
800 select (if possible) the first of this item's child-items.
800 select (if possible) the first of this item's child-items.
801 """
801 """
802 currentitem = self.currentselecteditem
802 currentitem = self.currentselecteditem
803 nextitem = currentitem.firstchild()
803 nextitem = currentitem.firstchild()
804
804
805 # turn off folding if we want to show a child-item
805 # turn off folding if we want to show a child-item
806 if currentitem.folded:
806 if currentitem.folded:
807 self.togglefolded(currentitem)
807 self.togglefolded(currentitem)
808
808
809 if nextitem is None:
809 if nextitem is None:
810 # if no next item on parent-level, then no change...
810 # if no next item on parent-level, then no change...
811 nextitem = currentitem
811 nextitem = currentitem
812
812
813 self.currentselecteditem = nextitem
813 self.currentselecteditem = nextitem
814
814
815 def leftarrowevent(self):
815 def leftarrowevent(self):
816 """
816 """
817 if the current item can be folded (i.e. it is an unfolded header or
817 if the current item can be folded (i.e. it is an unfolded header or
818 hunk), then fold it. otherwise try select (if possible) the parent
818 hunk), then fold it. otherwise try select (if possible) the parent
819 of this item.
819 of this item.
820 """
820 """
821 currentitem = self.currentselecteditem
821 currentitem = self.currentselecteditem
822
822
823 # try to fold the item
823 # try to fold the item
824 if not isinstance(currentitem, uihunkline):
824 if not isinstance(currentitem, uihunkline):
825 if not currentitem.folded:
825 if not currentitem.folded:
826 self.togglefolded(item=currentitem)
826 self.togglefolded(item=currentitem)
827 return
827 return
828
828
829 # if it can't be folded, try to select the parent item
829 # if it can't be folded, try to select the parent item
830 nextitem = currentitem.parentitem()
830 nextitem = currentitem.parentitem()
831
831
832 if nextitem is None:
832 if nextitem is None:
833 # if no item on parent-level, then no change...
833 # if no item on parent-level, then no change...
834 nextitem = currentitem
834 nextitem = currentitem
835 if not nextitem.folded:
835 if not nextitem.folded:
836 self.togglefolded(item=nextitem)
836 self.togglefolded(item=nextitem)
837
837
838 self.currentselecteditem = nextitem
838 self.currentselecteditem = nextitem
839
839
840 def leftarrowshiftevent(self):
840 def leftarrowshiftevent(self):
841 """
841 """
842 select the header of the current item (or fold current item if the
842 select the header of the current item (or fold current item if the
843 current item is already a header).
843 current item is already a header).
844 """
844 """
845 currentitem = self.currentselecteditem
845 currentitem = self.currentselecteditem
846
846
847 if isinstance(currentitem, uiheader):
847 if isinstance(currentitem, uiheader):
848 if not currentitem.folded:
848 if not currentitem.folded:
849 self.togglefolded(item=currentitem)
849 self.togglefolded(item=currentitem)
850 return
850 return
851
851
852 # select the parent item recursively until we're at a header
852 # select the parent item recursively until we're at a header
853 while True:
853 while True:
854 nextitem = currentitem.parentitem()
854 nextitem = currentitem.parentitem()
855 if nextitem is None:
855 if nextitem is None:
856 break
856 break
857 else:
857 else:
858 currentitem = nextitem
858 currentitem = nextitem
859
859
860 self.currentselecteditem = currentitem
860 self.currentselecteditem = currentitem
861
861
862 def updatescroll(self):
862 def updatescroll(self):
863 """scroll the screen to fully show the currently-selected"""
863 """scroll the screen to fully show the currently-selected"""
864 selstart = self.selecteditemstartline
864 selstart = self.selecteditemstartline
865 selend = self.selecteditemendline
865 selend = self.selecteditemendline
866
866
867 padstart = self.firstlineofpadtoprint
867 padstart = self.firstlineofpadtoprint
868 padend = padstart + self.yscreensize - self.numstatuslines - 1
868 padend = padstart + self.yscreensize - self.numstatuslines - 1
869 # 'buffered' pad start/end values which scroll with a certain
869 # 'buffered' pad start/end values which scroll with a certain
870 # top/bottom context margin
870 # top/bottom context margin
871 padstartbuffered = padstart + 3
871 padstartbuffered = padstart + 3
872 padendbuffered = padend - 3
872 padendbuffered = padend - 3
873
873
874 if selend > padendbuffered:
874 if selend > padendbuffered:
875 self.scrolllines(selend - padendbuffered)
875 self.scrolllines(selend - padendbuffered)
876 elif selstart < padstartbuffered:
876 elif selstart < padstartbuffered:
877 # negative values scroll in pgup direction
877 # negative values scroll in pgup direction
878 self.scrolllines(selstart - padstartbuffered)
878 self.scrolllines(selstart - padstartbuffered)
879
879
880 def scrolllines(self, numlines):
880 def scrolllines(self, numlines):
881 """scroll the screen up (down) by numlines when numlines >0 (<0)."""
881 """scroll the screen up (down) by numlines when numlines >0 (<0)."""
882 self.firstlineofpadtoprint += numlines
882 self.firstlineofpadtoprint += numlines
883 if self.firstlineofpadtoprint < 0:
883 if self.firstlineofpadtoprint < 0:
884 self.firstlineofpadtoprint = 0
884 self.firstlineofpadtoprint = 0
885 if self.firstlineofpadtoprint > self.numpadlines - 1:
885 if self.firstlineofpadtoprint > self.numpadlines - 1:
886 self.firstlineofpadtoprint = self.numpadlines - 1
886 self.firstlineofpadtoprint = self.numpadlines - 1
887
887
888 def toggleapply(self, item=None):
888 def toggleapply(self, item=None):
889 """
889 """
890 toggle the applied flag of the specified item. if no item is specified,
890 toggle the applied flag of the specified item. if no item is specified,
891 toggle the flag of the currently selected item.
891 toggle the flag of the currently selected item.
892 """
892 """
893 if item is None:
893 if item is None:
894 item = self.currentselecteditem
894 item = self.currentselecteditem
895 # Only set this when NOT using 'toggleall'
895 # Only set this when NOT using 'toggleall'
896 self.lastapplieditem = item
896 self.lastapplieditem = item
897
897
898 item.applied = not item.applied
898 item.applied = not item.applied
899
899
900 if isinstance(item, uiheader):
900 if isinstance(item, uiheader):
901 item.partial = False
901 item.partial = False
902 if item.applied:
902 if item.applied:
903 # apply all its hunks
903 # apply all its hunks
904 for hnk in item.hunks:
904 for hnk in item.hunks:
905 hnk.applied = True
905 hnk.applied = True
906 # apply all their hunklines
906 # apply all their hunklines
907 for hunkline in hnk.changedlines:
907 for hunkline in hnk.changedlines:
908 hunkline.applied = True
908 hunkline.applied = True
909 else:
909 else:
910 # un-apply all its hunks
910 # un-apply all its hunks
911 for hnk in item.hunks:
911 for hnk in item.hunks:
912 hnk.applied = False
912 hnk.applied = False
913 hnk.partial = False
913 hnk.partial = False
914 # un-apply all their hunklines
914 # un-apply all their hunklines
915 for hunkline in hnk.changedlines:
915 for hunkline in hnk.changedlines:
916 hunkline.applied = False
916 hunkline.applied = False
917 elif isinstance(item, uihunk):
917 elif isinstance(item, uihunk):
918 item.partial = False
918 item.partial = False
919 # apply all it's hunklines
919 # apply all it's hunklines
920 for hunkline in item.changedlines:
920 for hunkline in item.changedlines:
921 hunkline.applied = item.applied
921 hunkline.applied = item.applied
922
922
923 siblingappliedstatus = [hnk.applied for hnk in item.header.hunks]
923 siblingappliedstatus = [hnk.applied for hnk in item.header.hunks]
924 allsiblingsapplied = not (False in siblingappliedstatus)
924 allsiblingsapplied = not (False in siblingappliedstatus)
925 nosiblingsapplied = not (True in siblingappliedstatus)
925 nosiblingsapplied = not (True in siblingappliedstatus)
926
926
927 siblingspartialstatus = [hnk.partial for hnk in item.header.hunks]
927 siblingspartialstatus = [hnk.partial for hnk in item.header.hunks]
928 somesiblingspartial = True in siblingspartialstatus
928 somesiblingspartial = True in siblingspartialstatus
929
929
930 # cases where applied or partial should be removed from header
930 # cases where applied or partial should be removed from header
931
931
932 # if no 'sibling' hunks are applied (including this hunk)
932 # if no 'sibling' hunks are applied (including this hunk)
933 if nosiblingsapplied:
933 if nosiblingsapplied:
934 if not item.header.special():
934 if not item.header.special():
935 item.header.applied = False
935 item.header.applied = False
936 item.header.partial = False
936 item.header.partial = False
937 else: # some/all parent siblings are applied
937 else: # some/all parent siblings are applied
938 item.header.applied = True
938 item.header.applied = True
939 item.header.partial = (
939 item.header.partial = (
940 somesiblingspartial or not allsiblingsapplied
940 somesiblingspartial or not allsiblingsapplied
941 )
941 )
942
942
943 elif isinstance(item, uihunkline):
943 elif isinstance(item, uihunkline):
944 siblingappliedstatus = [ln.applied for ln in item.hunk.changedlines]
944 siblingappliedstatus = [ln.applied for ln in item.hunk.changedlines]
945 allsiblingsapplied = not (False in siblingappliedstatus)
945 allsiblingsapplied = not (False in siblingappliedstatus)
946 nosiblingsapplied = not (True in siblingappliedstatus)
946 nosiblingsapplied = not (True in siblingappliedstatus)
947
947
948 # if no 'sibling' lines are applied
948 # if no 'sibling' lines are applied
949 if nosiblingsapplied:
949 if nosiblingsapplied:
950 item.hunk.applied = False
950 item.hunk.applied = False
951 item.hunk.partial = False
951 item.hunk.partial = False
952 elif allsiblingsapplied:
952 elif allsiblingsapplied:
953 item.hunk.applied = True
953 item.hunk.applied = True
954 item.hunk.partial = False
954 item.hunk.partial = False
955 else: # some siblings applied
955 else: # some siblings applied
956 item.hunk.applied = True
956 item.hunk.applied = True
957 item.hunk.partial = True
957 item.hunk.partial = True
958
958
959 parentsiblingsapplied = [
959 parentsiblingsapplied = [
960 hnk.applied for hnk in item.hunk.header.hunks
960 hnk.applied for hnk in item.hunk.header.hunks
961 ]
961 ]
962 noparentsiblingsapplied = not (True in parentsiblingsapplied)
962 noparentsiblingsapplied = not (True in parentsiblingsapplied)
963 allparentsiblingsapplied = not (False in parentsiblingsapplied)
963 allparentsiblingsapplied = not (False in parentsiblingsapplied)
964
964
965 parentsiblingspartial = [
965 parentsiblingspartial = [
966 hnk.partial for hnk in item.hunk.header.hunks
966 hnk.partial for hnk in item.hunk.header.hunks
967 ]
967 ]
968 someparentsiblingspartial = True in parentsiblingspartial
968 someparentsiblingspartial = True in parentsiblingspartial
969
969
970 # if all parent hunks are not applied, un-apply header
970 # if all parent hunks are not applied, un-apply header
971 if noparentsiblingsapplied:
971 if noparentsiblingsapplied:
972 if not item.hunk.header.special():
972 if not item.hunk.header.special():
973 item.hunk.header.applied = False
973 item.hunk.header.applied = False
974 item.hunk.header.partial = False
974 item.hunk.header.partial = False
975 # set the applied and partial status of the header if needed
975 # set the applied and partial status of the header if needed
976 else: # some/all parent siblings are applied
976 else: # some/all parent siblings are applied
977 item.hunk.header.applied = True
977 item.hunk.header.applied = True
978 item.hunk.header.partial = (
978 item.hunk.header.partial = (
979 someparentsiblingspartial or not allparentsiblingsapplied
979 someparentsiblingspartial or not allparentsiblingsapplied
980 )
980 )
981
981
982 def toggleall(self):
982 def toggleall(self):
983 """toggle the applied flag of all items."""
983 """toggle the applied flag of all items."""
984 if self.waslasttoggleallapplied: # then unapply them this time
984 if self.waslasttoggleallapplied: # then unapply them this time
985 for item in self.headerlist:
985 for item in self.headerlist:
986 if item.applied:
986 if item.applied:
987 self.toggleapply(item)
987 self.toggleapply(item)
988 else:
988 else:
989 for item in self.headerlist:
989 for item in self.headerlist:
990 if not item.applied:
990 if not item.applied:
991 self.toggleapply(item)
991 self.toggleapply(item)
992 self.waslasttoggleallapplied = not self.waslasttoggleallapplied
992 self.waslasttoggleallapplied = not self.waslasttoggleallapplied
993
993
994 def flipselections(self):
994 def flipselections(self):
995 """
995 """
996 Flip all selections. Every selected line is unselected and vice
996 Flip all selections. Every selected line is unselected and vice
997 versa.
997 versa.
998 """
998 """
999 for header in self.headerlist:
999 for header in self.headerlist:
1000 for hunk in header.allchildren():
1000 for hunk in header.allchildren():
1001 for line in hunk.allchildren():
1001 for line in hunk.allchildren():
1002 self.toggleapply(line)
1002 self.toggleapply(line)
1003
1003
1004 def toggleallbetween(self):
1004 def toggleallbetween(self):
1005 """toggle applied on or off for all items in range [lastapplied,
1005 """toggle applied on or off for all items in range [lastapplied,
1006 current]. """
1006 current]. """
1007 if (
1007 if (
1008 not self.lastapplieditem
1008 not self.lastapplieditem
1009 or self.currentselecteditem == self.lastapplieditem
1009 or self.currentselecteditem == self.lastapplieditem
1010 ):
1010 ):
1011 # Treat this like a normal 'x'/' '
1011 # Treat this like a normal 'x'/' '
1012 self.toggleapply()
1012 self.toggleapply()
1013 return
1013 return
1014
1014
1015 startitem = self.lastapplieditem
1015 startitem = self.lastapplieditem
1016 enditem = self.currentselecteditem
1016 enditem = self.currentselecteditem
1017 # Verify that enditem is "after" startitem, otherwise swap them.
1017 # Verify that enditem is "after" startitem, otherwise swap them.
1018 for direction in [b'forward', b'reverse']:
1018 for direction in [b'forward', b'reverse']:
1019 nextitem = startitem.nextitem()
1019 nextitem = startitem.nextitem()
1020 while nextitem and nextitem != enditem:
1020 while nextitem and nextitem != enditem:
1021 nextitem = nextitem.nextitem()
1021 nextitem = nextitem.nextitem()
1022 if nextitem:
1022 if nextitem:
1023 break
1023 break
1024 # Looks like we went the wrong direction :)
1024 # Looks like we went the wrong direction :)
1025 startitem, enditem = enditem, startitem
1025 startitem, enditem = enditem, startitem
1026
1026
1027 if not nextitem:
1027 if not nextitem:
1028 # We didn't find a path going either forward or backward? Don't know
1028 # We didn't find a path going either forward or backward? Don't know
1029 # how this can happen, let's not crash though.
1029 # how this can happen, let's not crash though.
1030 return
1030 return
1031
1031
1032 nextitem = startitem
1032 nextitem = startitem
1033 # Switch all items to be the opposite state of the currently selected
1033 # Switch all items to be the opposite state of the currently selected
1034 # item. Specifically:
1034 # item. Specifically:
1035 # [ ] startitem
1035 # [ ] startitem
1036 # [x] middleitem
1036 # [x] middleitem
1037 # [ ] enditem <-- currently selected
1037 # [ ] enditem <-- currently selected
1038 # This will turn all three on, since the currently selected item is off.
1038 # This will turn all three on, since the currently selected item is off.
1039 # This does *not* invert each item (i.e. middleitem stays marked/on)
1039 # This does *not* invert each item (i.e. middleitem stays marked/on)
1040 desiredstate = not self.currentselecteditem.applied
1040 desiredstate = not self.currentselecteditem.applied
1041 while nextitem != enditem.nextitem():
1041 while nextitem != enditem.nextitem():
1042 if nextitem.applied != desiredstate:
1042 if nextitem.applied != desiredstate:
1043 self.toggleapply(item=nextitem)
1043 self.toggleapply(item=nextitem)
1044 nextitem = nextitem.nextitem()
1044 nextitem = nextitem.nextitem()
1045
1045
1046 def togglefolded(self, item=None, foldparent=False):
1046 def togglefolded(self, item=None, foldparent=False):
1047 """toggle folded flag of specified item (defaults to currently
1047 """toggle folded flag of specified item (defaults to currently
1048 selected)"""
1048 selected)"""
1049 if item is None:
1049 if item is None:
1050 item = self.currentselecteditem
1050 item = self.currentselecteditem
1051 if foldparent or (isinstance(item, uiheader) and item.neverunfolded):
1051 if foldparent or (isinstance(item, uiheader) and item.neverunfolded):
1052 if not isinstance(item, uiheader):
1052 if not isinstance(item, uiheader):
1053 # we need to select the parent item in this case
1053 # we need to select the parent item in this case
1054 self.currentselecteditem = item = item.parentitem()
1054 self.currentselecteditem = item = item.parentitem()
1055 elif item.neverunfolded:
1055 elif item.neverunfolded:
1056 item.neverunfolded = False
1056 item.neverunfolded = False
1057
1057
1058 # also fold any foldable children of the parent/current item
1058 # also fold any foldable children of the parent/current item
1059 if isinstance(item, uiheader): # the original or 'new' item
1059 if isinstance(item, uiheader): # the original or 'new' item
1060 for child in item.allchildren():
1060 for child in item.allchildren():
1061 child.folded = not item.folded
1061 child.folded = not item.folded
1062
1062
1063 if isinstance(item, (uiheader, uihunk)):
1063 if isinstance(item, (uiheader, uihunk)):
1064 item.folded = not item.folded
1064 item.folded = not item.folded
1065
1065
1066 def alignstring(self, instr, window):
1066 def alignstring(self, instr, window):
1067 """
1067 """
1068 add whitespace to the end of a string in order to make it fill
1068 add whitespace to the end of a string in order to make it fill
1069 the screen in the x direction. the current cursor position is
1069 the screen in the x direction. the current cursor position is
1070 taken into account when making this calculation. the string can span
1070 taken into account when making this calculation. the string can span
1071 multiple lines.
1071 multiple lines.
1072 """
1072 """
1073 y, xstart = window.getyx()
1073 y, xstart = window.getyx()
1074 width = self.xscreensize
1074 width = self.xscreensize
1075 # turn tabs into spaces
1075 # turn tabs into spaces
1076 instr = instr.expandtabs(4)
1076 instr = instr.expandtabs(4)
1077 strwidth = encoding.colwidth(instr)
1077 strwidth = encoding.colwidth(instr)
1078 numspaces = width - ((strwidth + xstart) % width)
1078 numspaces = width - ((strwidth + xstart) % width)
1079 return instr + b" " * numspaces
1079 return instr + b" " * numspaces
1080
1080
1081 def printstring(
1081 def printstring(
1082 self,
1082 self,
1083 window,
1083 window,
1084 text,
1084 text,
1085 fgcolor=None,
1085 fgcolor=None,
1086 bgcolor=None,
1086 bgcolor=None,
1087 pair=None,
1087 pair=None,
1088 pairname=None,
1088 pairname=None,
1089 attrlist=None,
1089 attrlist=None,
1090 towin=True,
1090 towin=True,
1091 align=True,
1091 align=True,
1092 showwhtspc=False,
1092 showwhtspc=False,
1093 ):
1093 ):
1094 """
1094 """
1095 print the string, text, with the specified colors and attributes, to
1095 print the string, text, with the specified colors and attributes, to
1096 the specified curses window object.
1096 the specified curses window object.
1097
1097
1098 the foreground and background colors are of the form
1098 the foreground and background colors are of the form
1099 curses.color_xxxx, where xxxx is one of: [black, blue, cyan, green,
1099 curses.color_xxxx, where xxxx is one of: [black, blue, cyan, green,
1100 magenta, red, white, yellow]. if pairname is provided, a color
1100 magenta, red, white, yellow]. if pairname is provided, a color
1101 pair will be looked up in the self.colorpairnames dictionary.
1101 pair will be looked up in the self.colorpairnames dictionary.
1102
1102
1103 attrlist is a list containing text attributes in the form of
1103 attrlist is a list containing text attributes in the form of
1104 curses.a_xxxx, where xxxx can be: [bold, dim, normal, standout,
1104 curses.a_xxxx, where xxxx can be: [bold, dim, normal, standout,
1105 underline].
1105 underline].
1106
1106
1107 if align == True, whitespace is added to the printed string such that
1107 if align == True, whitespace is added to the printed string such that
1108 the string stretches to the right border of the window.
1108 the string stretches to the right border of the window.
1109
1109
1110 if showwhtspc == True, trailing whitespace of a string is highlighted.
1110 if showwhtspc == True, trailing whitespace of a string is highlighted.
1111 """
1111 """
1112 # preprocess the text, converting tabs to spaces
1112 # preprocess the text, converting tabs to spaces
1113 text = text.expandtabs(4)
1113 text = text.expandtabs(4)
1114 # strip \n, and convert control characters to ^[char] representation
1114 # strip \n, and convert control characters to ^[char] representation
1115 text = re.sub(
1115 text = re.sub(
1116 br'[\x00-\x08\x0a-\x1f]',
1116 br'[\x00-\x08\x0a-\x1f]',
1117 lambda m: b'^' + pycompat.sysbytes(chr(ord(m.group()) + 64)),
1117 lambda m: b'^' + pycompat.sysbytes(chr(ord(m.group()) + 64)),
1118 text.strip(b'\n'),
1118 text.strip(b'\n'),
1119 )
1119 )
1120
1120
1121 if pair is not None:
1121 if pair is not None:
1122 colorpair = pair
1122 colorpair = pair
1123 elif pairname is not None:
1123 elif pairname is not None:
1124 colorpair = self.colorpairnames[pairname]
1124 colorpair = self.colorpairnames[pairname]
1125 else:
1125 else:
1126 if fgcolor is None:
1126 if fgcolor is None:
1127 fgcolor = -1
1127 fgcolor = -1
1128 if bgcolor is None:
1128 if bgcolor is None:
1129 bgcolor = -1
1129 bgcolor = -1
1130 if (fgcolor, bgcolor) in self.colorpairs:
1130 if (fgcolor, bgcolor) in self.colorpairs:
1131 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1131 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1132 else:
1132 else:
1133 colorpair = self.getcolorpair(fgcolor, bgcolor)
1133 colorpair = self.getcolorpair(fgcolor, bgcolor)
1134 # add attributes if possible
1134 # add attributes if possible
1135 if attrlist is None:
1135 if attrlist is None:
1136 attrlist = []
1136 attrlist = []
1137 if colorpair < 256:
1137 if colorpair < 256:
1138 # then it is safe to apply all attributes
1138 # then it is safe to apply all attributes
1139 for textattr in attrlist:
1139 for textattr in attrlist:
1140 colorpair |= textattr
1140 colorpair |= textattr
1141 else:
1141 else:
1142 # just apply a select few (safe?) attributes
1142 # just apply a select few (safe?) attributes
1143 for textattr in (curses.A_UNDERLINE, curses.A_BOLD):
1143 for textattr in (curses.A_UNDERLINE, curses.A_BOLD):
1144 if textattr in attrlist:
1144 if textattr in attrlist:
1145 colorpair |= textattr
1145 colorpair |= textattr
1146
1146
1147 y, xstart = self.chunkpad.getyx()
1147 y, xstart = self.chunkpad.getyx()
1148 t = b"" # variable for counting lines printed
1148 t = b"" # variable for counting lines printed
1149 # if requested, show trailing whitespace
1149 # if requested, show trailing whitespace
1150 if showwhtspc:
1150 if showwhtspc:
1151 origlen = len(text)
1151 origlen = len(text)
1152 text = text.rstrip(b' \n') # tabs have already been expanded
1152 text = text.rstrip(b' \n') # tabs have already been expanded
1153 strippedlen = len(text)
1153 strippedlen = len(text)
1154 numtrailingspaces = origlen - strippedlen
1154 numtrailingspaces = origlen - strippedlen
1155
1155
1156 if towin:
1156 if towin:
1157 window.addstr(text, colorpair)
1157 window.addstr(text, colorpair)
1158 t += text
1158 t += text
1159
1159
1160 if showwhtspc:
1160 if showwhtspc:
1161 wscolorpair = colorpair | curses.A_REVERSE
1161 wscolorpair = colorpair | curses.A_REVERSE
1162 if towin:
1162 if towin:
1163 for i in range(numtrailingspaces):
1163 for i in range(numtrailingspaces):
1164 window.addch(curses.ACS_CKBOARD, wscolorpair)
1164 window.addch(curses.ACS_CKBOARD, wscolorpair)
1165 t += b" " * numtrailingspaces
1165 t += b" " * numtrailingspaces
1166
1166
1167 if align:
1167 if align:
1168 if towin:
1168 if towin:
1169 extrawhitespace = self.alignstring(b"", window)
1169 extrawhitespace = self.alignstring(b"", window)
1170 window.addstr(extrawhitespace, colorpair)
1170 window.addstr(extrawhitespace, colorpair)
1171 else:
1171 else:
1172 # need to use t, since the x position hasn't incremented
1172 # need to use t, since the x position hasn't incremented
1173 extrawhitespace = self.alignstring(t, window)
1173 extrawhitespace = self.alignstring(t, window)
1174 t += extrawhitespace
1174 t += extrawhitespace
1175
1175
1176 # is reset to 0 at the beginning of printitem()
1176 # is reset to 0 at the beginning of printitem()
1177
1177
1178 linesprinted = (xstart + len(t)) // self.xscreensize
1178 linesprinted = (xstart + len(t)) // self.xscreensize
1179 self.linesprintedtopadsofar += linesprinted
1179 self.linesprintedtopadsofar += linesprinted
1180 return t
1180 return t
1181
1181
1182 def _getstatuslinesegments(self):
1182 def _getstatuslinesegments(self):
1183 """-> [str]. return segments"""
1183 """-> [str]. return segments"""
1184 selected = self.currentselecteditem.applied
1184 selected = self.currentselecteditem.applied
1185 spaceselect = _(b'space/enter: select')
1185 spaceselect = _(b'space/enter: select')
1186 spacedeselect = _(b'space/enter: deselect')
1186 spacedeselect = _(b'space/enter: deselect')
1187 # Format the selected label into a place as long as the longer of the
1187 # Format the selected label into a place as long as the longer of the
1188 # two possible labels. This may vary by language.
1188 # two possible labels. This may vary by language.
1189 spacelen = max(len(spaceselect), len(spacedeselect))
1189 spacelen = max(len(spaceselect), len(spacedeselect))
1190 selectedlabel = b'%-*s' % (
1190 selectedlabel = b'%-*s' % (
1191 spacelen,
1191 spacelen,
1192 spacedeselect if selected else spaceselect,
1192 spacedeselect if selected else spaceselect,
1193 )
1193 )
1194 segments = [
1194 segments = [
1195 _headermessages[self.operation],
1195 _headermessages[self.operation],
1196 b'-',
1196 b'-',
1197 _(b'[x]=selected **=collapsed'),
1197 _(b'[x]=selected **=collapsed'),
1198 _(b'c: confirm'),
1198 _(b'c: confirm'),
1199 _(b'q: abort'),
1199 _(b'q: abort'),
1200 _(b'arrow keys: move/expand/collapse'),
1200 _(b'arrow keys: move/expand/collapse'),
1201 selectedlabel,
1201 selectedlabel,
1202 _(b'?: help'),
1202 _(b'?: help'),
1203 ]
1203 ]
1204 return segments
1204 return segments
1205
1205
1206 def _getstatuslines(self):
1206 def _getstatuslines(self):
1207 """() -> [str]. return short help used in the top status window"""
1207 """() -> [str]. return short help used in the top status window"""
1208 if self.errorstr is not None:
1208 if self.errorstr is not None:
1209 lines = [self.errorstr, _(b'Press any key to continue')]
1209 lines = [self.errorstr, _(b'Press any key to continue')]
1210 else:
1210 else:
1211 # wrap segments to lines
1211 # wrap segments to lines
1212 segments = self._getstatuslinesegments()
1212 segments = self._getstatuslinesegments()
1213 width = self.xscreensize
1213 width = self.xscreensize
1214 lines = []
1214 lines = []
1215 lastwidth = width
1215 lastwidth = width
1216 for s in segments:
1216 for s in segments:
1217 w = encoding.colwidth(s)
1217 w = encoding.colwidth(s)
1218 sep = b' ' * (1 + (s and s[0] not in b'-['))
1218 sep = b' ' * (1 + (s and s[0] not in b'-['))
1219 if lastwidth + w + len(sep) >= width:
1219 if lastwidth + w + len(sep) >= width:
1220 lines.append(s)
1220 lines.append(s)
1221 lastwidth = w
1221 lastwidth = w
1222 else:
1222 else:
1223 lines[-1] += sep + s
1223 lines[-1] += sep + s
1224 lastwidth += w + len(sep)
1224 lastwidth += w + len(sep)
1225 if len(lines) != self.numstatuslines:
1225 if len(lines) != self.numstatuslines:
1226 self.numstatuslines = len(lines)
1226 self.numstatuslines = len(lines)
1227 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1227 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1228 return [stringutil.ellipsis(l, self.xscreensize - 1) for l in lines]
1228 return [stringutil.ellipsis(l, self.xscreensize - 1) for l in lines]
1229
1229
1230 def updatescreen(self):
1230 def updatescreen(self):
1231 self.statuswin.erase()
1231 self.statuswin.erase()
1232 self.chunkpad.erase()
1232 self.chunkpad.erase()
1233
1233
1234 printstring = self.printstring
1234 printstring = self.printstring
1235
1235
1236 # print out the status lines at the top
1236 # print out the status lines at the top
1237 try:
1237 try:
1238 for line in self._getstatuslines():
1238 for line in self._getstatuslines():
1239 printstring(self.statuswin, line, pairname=b"legend")
1239 printstring(self.statuswin, line, pairname=b"legend")
1240 self.statuswin.refresh()
1240 self.statuswin.refresh()
1241 except curses.error:
1241 except curses.error:
1242 pass
1242 pass
1243 if self.errorstr is not None:
1243 if self.errorstr is not None:
1244 return
1244 return
1245
1245
1246 # print out the patch in the remaining part of the window
1246 # print out the patch in the remaining part of the window
1247 try:
1247 try:
1248 self.printitem()
1248 self.printitem()
1249 self.updatescroll()
1249 self.updatescroll()
1250 self.chunkpad.refresh(
1250 self.chunkpad.refresh(
1251 self.firstlineofpadtoprint,
1251 self.firstlineofpadtoprint,
1252 0,
1252 0,
1253 self.numstatuslines,
1253 self.numstatuslines,
1254 0,
1254 0,
1255 self.yscreensize - self.numstatuslines,
1255 self.yscreensize - self.numstatuslines,
1256 self.xscreensize,
1256 self.xscreensize,
1257 )
1257 )
1258 except curses.error:
1258 except curses.error:
1259 pass
1259 pass
1260
1260
1261 def getstatusprefixstring(self, item):
1261 def getstatusprefixstring(self, item):
1262 """
1262 """
1263 create a string to prefix a line with which indicates whether 'item'
1263 create a string to prefix a line with which indicates whether 'item'
1264 is applied and/or folded.
1264 is applied and/or folded.
1265 """
1265 """
1266
1266
1267 # create checkbox string
1267 # create checkbox string
1268 if item.applied:
1268 if item.applied:
1269 if not isinstance(item, uihunkline) and item.partial:
1269 if not isinstance(item, uihunkline) and item.partial:
1270 checkbox = b"[~]"
1270 checkbox = b"[~]"
1271 else:
1271 else:
1272 checkbox = b"[x]"
1272 checkbox = b"[x]"
1273 else:
1273 else:
1274 checkbox = b"[ ]"
1274 checkbox = b"[ ]"
1275
1275
1276 try:
1276 try:
1277 if item.folded:
1277 if item.folded:
1278 checkbox += b"**"
1278 checkbox += b"**"
1279 if isinstance(item, uiheader):
1279 if isinstance(item, uiheader):
1280 # one of "m", "a", or "d" (modified, added, deleted)
1280 # one of "m", "a", or "d" (modified, added, deleted)
1281 filestatus = item.changetype
1281 filestatus = item.changetype
1282
1282
1283 checkbox += filestatus + b" "
1283 checkbox += filestatus + b" "
1284 else:
1284 else:
1285 checkbox += b" "
1285 checkbox += b" "
1286 if isinstance(item, uiheader):
1286 if isinstance(item, uiheader):
1287 # add two more spaces for headers
1287 # add two more spaces for headers
1288 checkbox += b" "
1288 checkbox += b" "
1289 except AttributeError: # not foldable
1289 except AttributeError: # not foldable
1290 checkbox += b" "
1290 checkbox += b" "
1291
1291
1292 return checkbox
1292 return checkbox
1293
1293
1294 def printheader(
1294 def printheader(
1295 self, header, selected=False, towin=True, ignorefolding=False
1295 self, header, selected=False, towin=True, ignorefolding=False
1296 ):
1296 ):
1297 """
1297 """
1298 print the header to the pad. if countlines is True, don't print
1298 print the header to the pad. if countlines is True, don't print
1299 anything, but just count the number of lines which would be printed.
1299 anything, but just count the number of lines which would be printed.
1300 """
1300 """
1301
1301
1302 outstr = b""
1302 outstr = b""
1303 text = header.prettystr()
1303 text = header.prettystr()
1304 chunkindex = self.chunklist.index(header)
1304 chunkindex = self.chunklist.index(header)
1305
1305
1306 if chunkindex != 0 and not header.folded:
1306 if chunkindex != 0 and not header.folded:
1307 # add separating line before headers
1307 # add separating line before headers
1308 outstr += self.printstring(
1308 outstr += self.printstring(
1309 self.chunkpad, b'_' * self.xscreensize, towin=towin, align=False
1309 self.chunkpad, b'_' * self.xscreensize, towin=towin, align=False
1310 )
1310 )
1311 # select color-pair based on if the header is selected
1311 # select color-pair based on if the header is selected
1312 colorpair = self.getcolorpair(
1312 colorpair = self.getcolorpair(
1313 name=selected and b"selected" or b"normal", attrlist=[curses.A_BOLD]
1313 name=selected and b"selected" or b"normal", attrlist=[curses.A_BOLD]
1314 )
1314 )
1315
1315
1316 # print out each line of the chunk, expanding it to screen width
1316 # print out each line of the chunk, expanding it to screen width
1317
1317
1318 # number of characters to indent lines on this level by
1318 # number of characters to indent lines on this level by
1319 indentnumchars = 0
1319 indentnumchars = 0
1320 checkbox = self.getstatusprefixstring(header)
1320 checkbox = self.getstatusprefixstring(header)
1321 if not header.folded or ignorefolding:
1321 if not header.folded or ignorefolding:
1322 textlist = text.split(b"\n")
1322 textlist = text.split(b"\n")
1323 linestr = checkbox + textlist[0]
1323 linestr = checkbox + textlist[0]
1324 else:
1324 else:
1325 linestr = checkbox + header.filename()
1325 linestr = checkbox + header.filename()
1326 outstr += self.printstring(
1326 outstr += self.printstring(
1327 self.chunkpad, linestr, pair=colorpair, towin=towin
1327 self.chunkpad, linestr, pair=colorpair, towin=towin
1328 )
1328 )
1329 if not header.folded or ignorefolding:
1329 if not header.folded or ignorefolding:
1330 if len(textlist) > 1:
1330 if len(textlist) > 1:
1331 for line in textlist[1:]:
1331 for line in textlist[1:]:
1332 linestr = b" " * (indentnumchars + len(checkbox)) + line
1332 linestr = b" " * (indentnumchars + len(checkbox)) + line
1333 outstr += self.printstring(
1333 outstr += self.printstring(
1334 self.chunkpad, linestr, pair=colorpair, towin=towin
1334 self.chunkpad, linestr, pair=colorpair, towin=towin
1335 )
1335 )
1336
1336
1337 return outstr
1337 return outstr
1338
1338
1339 def printhunklinesbefore(
1339 def printhunklinesbefore(
1340 self, hunk, selected=False, towin=True, ignorefolding=False
1340 self, hunk, selected=False, towin=True, ignorefolding=False
1341 ):
1341 ):
1342 """includes start/end line indicator"""
1342 """includes start/end line indicator"""
1343 outstr = b""
1343 outstr = b""
1344 # where hunk is in list of siblings
1344 # where hunk is in list of siblings
1345 hunkindex = hunk.header.hunks.index(hunk)
1345 hunkindex = hunk.header.hunks.index(hunk)
1346
1346
1347 if hunkindex != 0:
1347 if hunkindex != 0:
1348 # add separating line before headers
1348 # add separating line before headers
1349 outstr += self.printstring(
1349 outstr += self.printstring(
1350 self.chunkpad, b' ' * self.xscreensize, towin=towin, align=False
1350 self.chunkpad, b' ' * self.xscreensize, towin=towin, align=False
1351 )
1351 )
1352
1352
1353 colorpair = self.getcolorpair(
1353 colorpair = self.getcolorpair(
1354 name=selected and b"selected" or b"normal", attrlist=[curses.A_BOLD]
1354 name=selected and b"selected" or b"normal", attrlist=[curses.A_BOLD]
1355 )
1355 )
1356
1356
1357 # print out from-to line with checkbox
1357 # print out from-to line with checkbox
1358 checkbox = self.getstatusprefixstring(hunk)
1358 checkbox = self.getstatusprefixstring(hunk)
1359
1359
1360 lineprefix = b" " * self.hunkindentnumchars + checkbox
1360 lineprefix = b" " * self.hunkindentnumchars + checkbox
1361 frtoline = b" " + hunk.getfromtoline().strip(b"\n")
1361 frtoline = b" " + hunk.getfromtoline().strip(b"\n")
1362
1362
1363 outstr += self.printstring(
1363 outstr += self.printstring(
1364 self.chunkpad, lineprefix, towin=towin, align=False
1364 self.chunkpad, lineprefix, towin=towin, align=False
1365 ) # add uncolored checkbox/indent
1365 ) # add uncolored checkbox/indent
1366 outstr += self.printstring(
1366 outstr += self.printstring(
1367 self.chunkpad, frtoline, pair=colorpair, towin=towin
1367 self.chunkpad, frtoline, pair=colorpair, towin=towin
1368 )
1368 )
1369
1369
1370 if hunk.folded and not ignorefolding:
1370 if hunk.folded and not ignorefolding:
1371 # skip remainder of output
1371 # skip remainder of output
1372 return outstr
1372 return outstr
1373
1373
1374 # print out lines of the chunk preceeding changed-lines
1374 # print out lines of the chunk preceeding changed-lines
1375 for line in hunk.before:
1375 for line in hunk.before:
1376 linestr = (
1376 linestr = (
1377 b" " * (self.hunklineindentnumchars + len(checkbox)) + line
1377 b" " * (self.hunklineindentnumchars + len(checkbox)) + line
1378 )
1378 )
1379 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1379 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1380
1380
1381 return outstr
1381 return outstr
1382
1382
1383 def printhunklinesafter(self, hunk, towin=True, ignorefolding=False):
1383 def printhunklinesafter(self, hunk, towin=True, ignorefolding=False):
1384 outstr = b""
1384 outstr = b""
1385 if hunk.folded and not ignorefolding:
1385 if hunk.folded and not ignorefolding:
1386 return outstr
1386 return outstr
1387
1387
1388 # a bit superfluous, but to avoid hard-coding indent amount
1388 # a bit superfluous, but to avoid hard-coding indent amount
1389 checkbox = self.getstatusprefixstring(hunk)
1389 checkbox = self.getstatusprefixstring(hunk)
1390 for line in hunk.after:
1390 for line in hunk.after:
1391 linestr = (
1391 linestr = (
1392 b" " * (self.hunklineindentnumchars + len(checkbox)) + line
1392 b" " * (self.hunklineindentnumchars + len(checkbox)) + line
1393 )
1393 )
1394 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1394 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1395
1395
1396 return outstr
1396 return outstr
1397
1397
1398 def printhunkchangedline(self, hunkline, selected=False, towin=True):
1398 def printhunkchangedline(self, hunkline, selected=False, towin=True):
1399 outstr = b""
1399 outstr = b""
1400 checkbox = self.getstatusprefixstring(hunkline)
1400 checkbox = self.getstatusprefixstring(hunkline)
1401
1401
1402 linestr = hunkline.prettystr().strip(b"\n")
1402 linestr = hunkline.prettystr().strip(b"\n")
1403
1403
1404 # select color-pair based on whether line is an addition/removal
1404 # select color-pair based on whether line is an addition/removal
1405 if selected:
1405 if selected:
1406 colorpair = self.getcolorpair(name=b"selected")
1406 colorpair = self.getcolorpair(name=b"selected")
1407 elif linestr.startswith(b"+"):
1407 elif linestr.startswith(b"+"):
1408 colorpair = self.getcolorpair(name=b"addition")
1408 colorpair = self.getcolorpair(name=b"addition")
1409 elif linestr.startswith(b"-"):
1409 elif linestr.startswith(b"-"):
1410 colorpair = self.getcolorpair(name=b"deletion")
1410 colorpair = self.getcolorpair(name=b"deletion")
1411 elif linestr.startswith(b"\\"):
1411 elif linestr.startswith(b"\\"):
1412 colorpair = self.getcolorpair(name=b"normal")
1412 colorpair = self.getcolorpair(name=b"normal")
1413
1413
1414 lineprefix = b" " * self.hunklineindentnumchars + checkbox
1414 lineprefix = b" " * self.hunklineindentnumchars + checkbox
1415 outstr += self.printstring(
1415 outstr += self.printstring(
1416 self.chunkpad, lineprefix, towin=towin, align=False
1416 self.chunkpad, lineprefix, towin=towin, align=False
1417 ) # add uncolored checkbox/indent
1417 ) # add uncolored checkbox/indent
1418 outstr += self.printstring(
1418 outstr += self.printstring(
1419 self.chunkpad, linestr, pair=colorpair, towin=towin, showwhtspc=True
1419 self.chunkpad, linestr, pair=colorpair, towin=towin, showwhtspc=True
1420 )
1420 )
1421 return outstr
1421 return outstr
1422
1422
1423 def printitem(
1423 def printitem(
1424 self, item=None, ignorefolding=False, recursechildren=True, towin=True
1424 self, item=None, ignorefolding=False, recursechildren=True, towin=True
1425 ):
1425 ):
1426 """
1426 """
1427 use __printitem() to print the the specified item.applied.
1427 use __printitem() to print the the specified item.applied.
1428 if item is not specified, then print the entire patch.
1428 if item is not specified, then print the entire patch.
1429 (hiding folded elements, etc. -- see __printitem() docstring)
1429 (hiding folded elements, etc. -- see __printitem() docstring)
1430 """
1430 """
1431
1431
1432 if item is None:
1432 if item is None:
1433 item = self.headerlist
1433 item = self.headerlist
1434 if recursechildren:
1434 if recursechildren:
1435 self.linesprintedtopadsofar = 0
1435 self.linesprintedtopadsofar = 0
1436
1436
1437 outstr = []
1437 outstr = []
1438 self.__printitem(
1438 self.__printitem(
1439 item, ignorefolding, recursechildren, outstr, towin=towin
1439 item, ignorefolding, recursechildren, outstr, towin=towin
1440 )
1440 )
1441 return b''.join(outstr)
1441 return b''.join(outstr)
1442
1442
1443 def outofdisplayedarea(self):
1443 def outofdisplayedarea(self):
1444 y, _ = self.chunkpad.getyx() # cursor location
1444 y, _ = self.chunkpad.getyx() # cursor location
1445 # * 2 here works but an optimization would be the max number of
1445 # * 2 here works but an optimization would be the max number of
1446 # consecutive non selectable lines
1446 # consecutive non selectable lines
1447 # i.e the max number of context line for any hunk in the patch
1447 # i.e the max number of context line for any hunk in the patch
1448 miny = min(0, self.firstlineofpadtoprint - self.yscreensize)
1448 miny = min(0, self.firstlineofpadtoprint - self.yscreensize)
1449 maxy = self.firstlineofpadtoprint + self.yscreensize * 2
1449 maxy = self.firstlineofpadtoprint + self.yscreensize * 2
1450 return y < miny or y > maxy
1450 return y < miny or y > maxy
1451
1451
1452 def handleselection(self, item, recursechildren):
1452 def handleselection(self, item, recursechildren):
1453 selected = item is self.currentselecteditem
1453 selected = item is self.currentselecteditem
1454 if selected and recursechildren:
1454 if selected and recursechildren:
1455 # assumes line numbering starting from line 0
1455 # assumes line numbering starting from line 0
1456 self.selecteditemstartline = self.linesprintedtopadsofar
1456 self.selecteditemstartline = self.linesprintedtopadsofar
1457 selecteditemlines = self.getnumlinesdisplayed(
1457 selecteditemlines = self.getnumlinesdisplayed(
1458 item, recursechildren=False
1458 item, recursechildren=False
1459 )
1459 )
1460 self.selecteditemendline = (
1460 self.selecteditemendline = (
1461 self.selecteditemstartline + selecteditemlines - 1
1461 self.selecteditemstartline + selecteditemlines - 1
1462 )
1462 )
1463 return selected
1463 return selected
1464
1464
1465 def __printitem(
1465 def __printitem(
1466 self, item, ignorefolding, recursechildren, outstr, towin=True
1466 self, item, ignorefolding, recursechildren, outstr, towin=True
1467 ):
1467 ):
1468 """
1468 """
1469 recursive method for printing out patch/header/hunk/hunk-line data to
1469 recursive method for printing out patch/header/hunk/hunk-line data to
1470 screen. also returns a string with all of the content of the displayed
1470 screen. also returns a string with all of the content of the displayed
1471 patch (not including coloring, etc.).
1471 patch (not including coloring, etc.).
1472
1472
1473 if ignorefolding is True, then folded items are printed out.
1473 if ignorefolding is True, then folded items are printed out.
1474
1474
1475 if recursechildren is False, then only print the item without its
1475 if recursechildren is False, then only print the item without its
1476 child items.
1476 child items.
1477 """
1477 """
1478
1478
1479 if towin and self.outofdisplayedarea():
1479 if towin and self.outofdisplayedarea():
1480 return
1480 return
1481
1481
1482 selected = self.handleselection(item, recursechildren)
1482 selected = self.handleselection(item, recursechildren)
1483
1483
1484 # patch object is a list of headers
1484 # patch object is a list of headers
1485 if isinstance(item, patch):
1485 if isinstance(item, patch):
1486 if recursechildren:
1486 if recursechildren:
1487 for hdr in item:
1487 for hdr in item:
1488 self.__printitem(
1488 self.__printitem(
1489 hdr, ignorefolding, recursechildren, outstr, towin
1489 hdr, ignorefolding, recursechildren, outstr, towin
1490 )
1490 )
1491 # todo: eliminate all isinstance() calls
1491 # todo: eliminate all isinstance() calls
1492 if isinstance(item, uiheader):
1492 if isinstance(item, uiheader):
1493 outstr.append(
1493 outstr.append(
1494 self.printheader(
1494 self.printheader(
1495 item, selected, towin=towin, ignorefolding=ignorefolding
1495 item, selected, towin=towin, ignorefolding=ignorefolding
1496 )
1496 )
1497 )
1497 )
1498 if recursechildren:
1498 if recursechildren:
1499 for hnk in item.hunks:
1499 for hnk in item.hunks:
1500 self.__printitem(
1500 self.__printitem(
1501 hnk, ignorefolding, recursechildren, outstr, towin
1501 hnk, ignorefolding, recursechildren, outstr, towin
1502 )
1502 )
1503 elif isinstance(item, uihunk) and (
1503 elif isinstance(item, uihunk) and (
1504 (not item.header.folded) or ignorefolding
1504 (not item.header.folded) or ignorefolding
1505 ):
1505 ):
1506 # print the hunk data which comes before the changed-lines
1506 # print the hunk data which comes before the changed-lines
1507 outstr.append(
1507 outstr.append(
1508 self.printhunklinesbefore(
1508 self.printhunklinesbefore(
1509 item, selected, towin=towin, ignorefolding=ignorefolding
1509 item, selected, towin=towin, ignorefolding=ignorefolding
1510 )
1510 )
1511 )
1511 )
1512 if recursechildren:
1512 if recursechildren:
1513 for l in item.changedlines:
1513 for l in item.changedlines:
1514 self.__printitem(
1514 self.__printitem(
1515 l, ignorefolding, recursechildren, outstr, towin
1515 l, ignorefolding, recursechildren, outstr, towin
1516 )
1516 )
1517 outstr.append(
1517 outstr.append(
1518 self.printhunklinesafter(
1518 self.printhunklinesafter(
1519 item, towin=towin, ignorefolding=ignorefolding
1519 item, towin=towin, ignorefolding=ignorefolding
1520 )
1520 )
1521 )
1521 )
1522 elif isinstance(item, uihunkline) and (
1522 elif isinstance(item, uihunkline) and (
1523 (not item.hunk.folded) or ignorefolding
1523 (not item.hunk.folded) or ignorefolding
1524 ):
1524 ):
1525 outstr.append(
1525 outstr.append(
1526 self.printhunkchangedline(item, selected, towin=towin)
1526 self.printhunkchangedline(item, selected, towin=towin)
1527 )
1527 )
1528
1528
1529 return outstr
1529 return outstr
1530
1530
1531 def getnumlinesdisplayed(
1531 def getnumlinesdisplayed(
1532 self, item=None, ignorefolding=False, recursechildren=True
1532 self, item=None, ignorefolding=False, recursechildren=True
1533 ):
1533 ):
1534 """
1534 """
1535 return the number of lines which would be displayed if the item were
1535 return the number of lines which would be displayed if the item were
1536 to be printed to the display. the item will not be printed to the
1536 to be printed to the display. the item will not be printed to the
1537 display (pad).
1537 display (pad).
1538 if no item is given, assume the entire patch.
1538 if no item is given, assume the entire patch.
1539 if ignorefolding is True, folded items will be unfolded when counting
1539 if ignorefolding is True, folded items will be unfolded when counting
1540 the number of lines.
1540 the number of lines.
1541 """
1541 """
1542
1542
1543 # temporarily disable printing to windows by printstring
1543 # temporarily disable printing to windows by printstring
1544 patchdisplaystring = self.printitem(
1544 patchdisplaystring = self.printitem(
1545 item, ignorefolding, recursechildren, towin=False
1545 item, ignorefolding, recursechildren, towin=False
1546 )
1546 )
1547 numlines = len(patchdisplaystring) // self.xscreensize
1547 numlines = len(patchdisplaystring) // self.xscreensize
1548 return numlines
1548 return numlines
1549
1549
1550 def sigwinchhandler(self, n, frame):
1550 def sigwinchhandler(self, n, frame):
1551 """handle window resizing"""
1551 """handle window resizing"""
1552 try:
1552 try:
1553 curses.endwin()
1553 curses.endwin()
1554 self.xscreensize, self.yscreensize = scmutil.termsize(self.ui)
1554 self.xscreensize, self.yscreensize = scmutil.termsize(self.ui)
1555 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1555 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1556 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1556 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1557 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1557 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1558 except curses.error:
1558 except curses.error:
1559 pass
1559 pass
1560
1560
1561 def getcolorpair(
1561 def getcolorpair(
1562 self, fgcolor=None, bgcolor=None, name=None, attrlist=None
1562 self, fgcolor=None, bgcolor=None, name=None, attrlist=None
1563 ):
1563 ):
1564 """
1564 """
1565 get a curses color pair, adding it to self.colorpairs if it is not
1565 get a curses color pair, adding it to self.colorpairs if it is not
1566 already defined. an optional string, name, can be passed as a shortcut
1566 already defined. an optional string, name, can be passed as a shortcut
1567 for referring to the color-pair. by default, if no arguments are
1567 for referring to the color-pair. by default, if no arguments are
1568 specified, the white foreground / black background color-pair is
1568 specified, the white foreground / black background color-pair is
1569 returned.
1569 returned.
1570
1570
1571 it is expected that this function will be used exclusively for
1571 it is expected that this function will be used exclusively for
1572 initializing color pairs, and not curses.init_pair().
1572 initializing color pairs, and not curses.init_pair().
1573
1573
1574 attrlist is used to 'flavor' the returned color-pair. this information
1574 attrlist is used to 'flavor' the returned color-pair. this information
1575 is not stored in self.colorpairs. it contains attribute values like
1575 is not stored in self.colorpairs. it contains attribute values like
1576 curses.A_BOLD.
1576 curses.A_BOLD.
1577 """
1577 """
1578
1578
1579 if (name is not None) and name in self.colorpairnames:
1579 if (name is not None) and name in self.colorpairnames:
1580 # then get the associated color pair and return it
1580 # then get the associated color pair and return it
1581 colorpair = self.colorpairnames[name]
1581 colorpair = self.colorpairnames[name]
1582 else:
1582 else:
1583 if fgcolor is None:
1583 if fgcolor is None:
1584 fgcolor = -1
1584 fgcolor = -1
1585 if bgcolor is None:
1585 if bgcolor is None:
1586 bgcolor = -1
1586 bgcolor = -1
1587 if (fgcolor, bgcolor) in self.colorpairs:
1587 if (fgcolor, bgcolor) in self.colorpairs:
1588 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1588 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1589 else:
1589 else:
1590 pairindex = len(self.colorpairs) + 1
1590 pairindex = len(self.colorpairs) + 1
1591 if self.usecolor:
1591 if self.usecolor:
1592 curses.init_pair(pairindex, fgcolor, bgcolor)
1592 curses.init_pair(pairindex, fgcolor, bgcolor)
1593 colorpair = self.colorpairs[
1593 colorpair = self.colorpairs[
1594 (fgcolor, bgcolor)
1594 (fgcolor, bgcolor)
1595 ] = curses.color_pair(pairindex)
1595 ] = curses.color_pair(pairindex)
1596 if name is not None:
1596 if name is not None:
1597 self.colorpairnames[name] = curses.color_pair(pairindex)
1597 self.colorpairnames[name] = curses.color_pair(pairindex)
1598 else:
1598 else:
1599 cval = 0
1599 cval = 0
1600 if name is not None:
1600 if name is not None:
1601 if name == b'selected':
1601 if name == b'selected':
1602 cval = curses.A_REVERSE
1602 cval = curses.A_REVERSE
1603 self.colorpairnames[name] = cval
1603 self.colorpairnames[name] = cval
1604 colorpair = self.colorpairs[(fgcolor, bgcolor)] = cval
1604 colorpair = self.colorpairs[(fgcolor, bgcolor)] = cval
1605
1605
1606 # add attributes if possible
1606 # add attributes if possible
1607 if attrlist is None:
1607 if attrlist is None:
1608 attrlist = []
1608 attrlist = []
1609 if colorpair < 256:
1609 if colorpair < 256:
1610 # then it is safe to apply all attributes
1610 # then it is safe to apply all attributes
1611 for textattr in attrlist:
1611 for textattr in attrlist:
1612 colorpair |= textattr
1612 colorpair |= textattr
1613 else:
1613 else:
1614 # just apply a select few (safe?) attributes
1614 # just apply a select few (safe?) attributes
1615 for textattrib in (curses.A_UNDERLINE, curses.A_BOLD):
1615 for textattrib in (curses.A_UNDERLINE, curses.A_BOLD):
1616 if textattrib in attrlist:
1616 if textattrib in attrlist:
1617 colorpair |= textattrib
1617 colorpair |= textattrib
1618 return colorpair
1618 return colorpair
1619
1619
1620 def initcolorpair(self, *args, **kwargs):
1620 def initcolorpair(self, *args, **kwargs):
1621 """same as getcolorpair."""
1621 """same as getcolorpair."""
1622 self.getcolorpair(*args, **kwargs)
1622 self.getcolorpair(*args, **kwargs)
1623
1623
1624 def helpwindow(self):
1624 def helpwindow(self):
1625 """print a help window to the screen. exit after any keypress."""
1625 """print a help window to the screen. exit after any keypress."""
1626 helptext = _(
1626 helptext = _(
1627 """ [press any key to return to the patch-display]
1627 """ [press any key to return to the patch-display]
1628
1628
1629 The curses hunk selector allows you to interactively choose among the
1629 The curses hunk selector allows you to interactively choose among the
1630 changes you have made, and confirm only those changes you select for
1630 changes you have made, and confirm only those changes you select for
1631 further processing by the command you are running (such as commit,
1631 further processing by the command you are running (such as commit,
1632 shelve, or revert). After confirming the selected changes, the
1632 shelve, or revert). After confirming the selected changes, the
1633 unselected changes are still present in your working copy, so you can
1633 unselected changes are still present in your working copy, so you can
1634 use the hunk selector multiple times to split large changes into
1634 use the hunk selector multiple times to split large changes into
1635 smaller changesets. the following are valid keystrokes:
1635 smaller changesets. the following are valid keystrokes:
1636
1636
1637 x [space] : (un-)select item ([~]/[x] = partly/fully applied)
1637 x [space] : (un-)select item ([~]/[x] = partly/fully applied)
1638 [enter] : (un-)select item and go to next item of same type
1638 [enter] : (un-)select item and go to next item of same type
1639 A : (un-)select all items
1639 A : (un-)select all items
1640 X : (un-)select all items between current and most-recent
1640 X : (un-)select all items between current and most-recent
1641 up/down-arrow [k/j] : go to previous/next unfolded item
1641 up/down-arrow [k/j] : go to previous/next unfolded item
1642 pgup/pgdn [K/J] : go to previous/next item of same type
1642 pgup/pgdn [K/J] : go to previous/next item of same type
1643 right/left-arrow [l/h] : go to child item / parent item
1643 right/left-arrow [l/h] : go to child item / parent item
1644 shift-left-arrow [H] : go to parent header / fold selected header
1644 shift-left-arrow [H] : go to parent header / fold selected header
1645 g : go to the top
1645 g : go to the top
1646 G : go to the bottom
1646 G : go to the bottom
1647 f : fold / unfold item, hiding/revealing its children
1647 f : fold / unfold item, hiding/revealing its children
1648 F : fold / unfold parent item and all of its ancestors
1648 F : fold / unfold parent item and all of its ancestors
1649 ctrl-l : scroll the selected line to the top of the screen
1649 ctrl-l : scroll the selected line to the top of the screen
1650 m : edit / resume editing the commit message
1650 m : edit / resume editing the commit message
1651 e : edit the currently selected hunk
1651 e : edit the currently selected hunk
1652 a : toggle all selections
1652 a : toggle all selections
1653 c : confirm selected changes
1653 c : confirm selected changes
1654 r : review/edit and confirm selected changes
1654 r : review/edit and confirm selected changes
1655 q : quit without confirming (no changes will be made)
1655 q : quit without confirming (no changes will be made)
1656 ? : help (what you're currently reading)"""
1656 ? : help (what you're currently reading)"""
1657 )
1657 )
1658
1658
1659 helpwin = curses.newwin(self.yscreensize, 0, 0, 0)
1659 helpwin = curses.newwin(self.yscreensize, 0, 0, 0)
1660 helplines = helptext.split(b"\n")
1660 helplines = helptext.split(b"\n")
1661 helplines = helplines + [b" "] * (
1661 helplines = helplines + [b" "] * (
1662 self.yscreensize - self.numstatuslines - len(helplines) - 1
1662 self.yscreensize - self.numstatuslines - len(helplines) - 1
1663 )
1663 )
1664 try:
1664 try:
1665 for line in helplines:
1665 for line in helplines:
1666 self.printstring(helpwin, line, pairname=b"legend")
1666 self.printstring(helpwin, line, pairname=b"legend")
1667 except curses.error:
1667 except curses.error:
1668 pass
1668 pass
1669 helpwin.refresh()
1669 helpwin.refresh()
1670 try:
1670 try:
1671 with self.ui.timeblockedsection(b'crecord'):
1671 with self.ui.timeblockedsection(b'crecord'):
1672 helpwin.getkey()
1672 helpwin.getkey()
1673 except curses.error:
1673 except curses.error:
1674 pass
1674 pass
1675
1675
1676 def commitMessageWindow(self):
1676 def commitMessageWindow(self):
1677 """Create a temporary commit message editing window on the screen."""
1677 """Create a temporary commit message editing window on the screen."""
1678
1678
1679 curses.raw()
1679 curses.raw()
1680 curses.def_prog_mode()
1680 curses.def_prog_mode()
1681 curses.endwin()
1681 curses.endwin()
1682 self.commenttext = self.ui.edit(self.commenttext, self.ui.username())
1682 self.commenttext = self.ui.edit(self.commenttext, self.ui.username())
1683 curses.cbreak()
1683 curses.cbreak()
1684 self.stdscr.refresh()
1684 self.stdscr.refresh()
1685 self.stdscr.keypad(1) # allow arrow-keys to continue to function
1685 self.stdscr.keypad(1) # allow arrow-keys to continue to function
1686
1686
1687 def handlefirstlineevent(self):
1687 def handlefirstlineevent(self):
1688 """
1688 """
1689 Handle 'g' to navigate to the top most file in the ncurses window.
1689 Handle 'g' to navigate to the top most file in the ncurses window.
1690 """
1690 """
1691 self.currentselecteditem = self.headerlist[0]
1691 self.currentselecteditem = self.headerlist[0]
1692 currentitem = self.currentselecteditem
1692 currentitem = self.currentselecteditem
1693 # select the parent item recursively until we're at a header
1693 # select the parent item recursively until we're at a header
1694 while True:
1694 while True:
1695 nextitem = currentitem.parentitem()
1695 nextitem = currentitem.parentitem()
1696 if nextitem is None:
1696 if nextitem is None:
1697 break
1697 break
1698 else:
1698 else:
1699 currentitem = nextitem
1699 currentitem = nextitem
1700
1700
1701 self.currentselecteditem = currentitem
1701 self.currentselecteditem = currentitem
1702
1702
1703 def handlelastlineevent(self):
1703 def handlelastlineevent(self):
1704 """
1704 """
1705 Handle 'G' to navigate to the bottom most file/hunk/line depending
1705 Handle 'G' to navigate to the bottom most file/hunk/line depending
1706 on the whether the fold is active or not.
1706 on the whether the fold is active or not.
1707
1707
1708 If the bottom most file is folded, it navigates to that file and
1708 If the bottom most file is folded, it navigates to that file and
1709 stops there. If the bottom most file is unfolded, it navigates to
1709 stops there. If the bottom most file is unfolded, it navigates to
1710 the bottom most hunk in that file and stops there. If the bottom most
1710 the bottom most hunk in that file and stops there. If the bottom most
1711 hunk is unfolded, it navigates to the bottom most line in that hunk.
1711 hunk is unfolded, it navigates to the bottom most line in that hunk.
1712 """
1712 """
1713 currentitem = self.currentselecteditem
1713 currentitem = self.currentselecteditem
1714 nextitem = currentitem.nextitem()
1714 nextitem = currentitem.nextitem()
1715 # select the child item recursively until we're at a footer
1715 # select the child item recursively until we're at a footer
1716 while nextitem is not None:
1716 while nextitem is not None:
1717 nextitem = currentitem.nextitem()
1717 nextitem = currentitem.nextitem()
1718 if nextitem is None:
1718 if nextitem is None:
1719 break
1719 break
1720 else:
1720 else:
1721 currentitem = nextitem
1721 currentitem = nextitem
1722
1722
1723 self.currentselecteditem = currentitem
1723 self.currentselecteditem = currentitem
1724 self.recenterdisplayedarea()
1724 self.recenterdisplayedarea()
1725
1725
1726 def confirmationwindow(self, windowtext):
1726 def confirmationwindow(self, windowtext):
1727 """display an informational window, then wait for and return a
1727 """display an informational window, then wait for and return a
1728 keypress."""
1728 keypress."""
1729
1729
1730 confirmwin = curses.newwin(self.yscreensize, 0, 0, 0)
1730 confirmwin = curses.newwin(self.yscreensize, 0, 0, 0)
1731 try:
1731 try:
1732 lines = windowtext.split(b"\n")
1732 lines = windowtext.split(b"\n")
1733 for line in lines:
1733 for line in lines:
1734 self.printstring(confirmwin, line, pairname=b"selected")
1734 self.printstring(confirmwin, line, pairname=b"selected")
1735 except curses.error:
1735 except curses.error:
1736 pass
1736 pass
1737 self.stdscr.refresh()
1737 self.stdscr.refresh()
1738 confirmwin.refresh()
1738 confirmwin.refresh()
1739 try:
1739 try:
1740 with self.ui.timeblockedsection(b'crecord'):
1740 with self.ui.timeblockedsection(b'crecord'):
1741 response = chr(self.stdscr.getch())
1741 response = chr(self.stdscr.getch())
1742 except ValueError:
1742 except ValueError:
1743 response = None
1743 response = None
1744
1744
1745 return response
1745 return response
1746
1746
1747 def reviewcommit(self):
1747 def reviewcommit(self):
1748 """ask for 'y' to be pressed to confirm selected. return True if
1748 """ask for 'y' to be pressed to confirm selected. return True if
1749 confirmed."""
1749 confirmed."""
1750 confirmtext = _(
1750 confirmtext = _(
1751 """If you answer yes to the following, your currently chosen patch chunks
1751 """If you answer yes to the following, your currently chosen patch chunks
1752 will be loaded into an editor. To modify the patch, make the changes in your
1752 will be loaded into an editor. To modify the patch, make the changes in your
1753 editor and save. To accept the current patch as-is, close the editor without
1753 editor and save. To accept the current patch as-is, close the editor without
1754 saving.
1754 saving.
1755
1755
1756 note: don't add/remove lines unless you also modify the range information.
1756 note: don't add/remove lines unless you also modify the range information.
1757 failing to follow this rule will result in the commit aborting.
1757 failing to follow this rule will result in the commit aborting.
1758
1758
1759 are you sure you want to review/edit and confirm the selected changes [yn]?
1759 are you sure you want to review/edit and confirm the selected changes [yn]?
1760 """
1760 """
1761 )
1761 )
1762 with self.ui.timeblockedsection(b'crecord'):
1762 with self.ui.timeblockedsection(b'crecord'):
1763 response = self.confirmationwindow(confirmtext)
1763 response = self.confirmationwindow(confirmtext)
1764 if response is None:
1764 if response is None:
1765 response = "n"
1765 response = "n"
1766 if response.lower().startswith("y"):
1766 if response.lower().startswith("y"):
1767 return True
1767 return True
1768 else:
1768 else:
1769 return False
1769 return False
1770
1770
1771 def recenterdisplayedarea(self):
1771 def recenterdisplayedarea(self):
1772 """
1772 """
1773 once we scrolled with pg up pg down we can be pointing outside of the
1773 once we scrolled with pg up pg down we can be pointing outside of the
1774 display zone. we print the patch with towin=False to compute the
1774 display zone. we print the patch with towin=False to compute the
1775 location of the selected item even though it is outside of the displayed
1775 location of the selected item even though it is outside of the displayed
1776 zone and then update the scroll.
1776 zone and then update the scroll.
1777 """
1777 """
1778 self.printitem(towin=False)
1778 self.printitem(towin=False)
1779 self.updatescroll()
1779 self.updatescroll()
1780
1780
1781 def toggleedit(self, item=None, test=False):
1781 def toggleedit(self, item=None, test=False):
1782 """
1782 """
1783 edit the currently selected chunk
1783 edit the currently selected chunk
1784 """
1784 """
1785
1785
1786 def updateui(self):
1786 def updateui(self):
1787 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1787 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1788 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1788 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1789 self.updatescroll()
1789 self.updatescroll()
1790 self.stdscr.refresh()
1790 self.stdscr.refresh()
1791 self.statuswin.refresh()
1791 self.statuswin.refresh()
1792 self.stdscr.keypad(1)
1792 self.stdscr.keypad(1)
1793
1793
1794 def editpatchwitheditor(self, chunk):
1794 def editpatchwitheditor(self, chunk):
1795 if chunk is None:
1795 if chunk is None:
1796 self.ui.write(_(b'cannot edit patch for whole file'))
1796 self.ui.write(_(b'cannot edit patch for whole file'))
1797 self.ui.write(b"\n")
1797 self.ui.write(b"\n")
1798 return None
1798 return None
1799 if chunk.header.binary():
1799 if chunk.header.binary():
1800 self.ui.write(_(b'cannot edit patch for binary file'))
1800 self.ui.write(_(b'cannot edit patch for binary file'))
1801 self.ui.write(b"\n")
1801 self.ui.write(b"\n")
1802 return None
1802 return None
1803
1803
1804 # write the initial patch
1804 # write the initial patch
1805 patch = stringio()
1805 patch = stringio()
1806 patch.write(diffhelptext + hunkhelptext)
1806 patch.write(diffhelptext + hunkhelptext)
1807 chunk.header.write(patch)
1807 chunk.header.write(patch)
1808 chunk.write(patch)
1808 chunk.write(patch)
1809
1809
1810 # start the editor and wait for it to complete
1810 # start the editor and wait for it to complete
1811 try:
1811 try:
1812 patch = self.ui.edit(patch.getvalue(), b"", action=b"diff")
1812 patch = self.ui.edit(patch.getvalue(), b"", action=b"diff")
1813 except error.Abort as exc:
1813 except error.Abort as exc:
1814 self.errorstr = stringutil.forcebytestr(exc)
1814 self.errorstr = stringutil.forcebytestr(exc)
1815 return None
1815 return None
1816 finally:
1816 finally:
1817 self.stdscr.clear()
1817 self.stdscr.clear()
1818 self.stdscr.refresh()
1818 self.stdscr.refresh()
1819
1819
1820 # remove comment lines
1820 # remove comment lines
1821 patch = [
1821 patch = [
1822 line + b'\n'
1822 line + b'\n'
1823 for line in patch.splitlines()
1823 for line in patch.splitlines()
1824 if not line.startswith(b'#')
1824 if not line.startswith(b'#')
1825 ]
1825 ]
1826 return patchmod.parsepatch(patch)
1826 return patchmod.parsepatch(patch)
1827
1827
1828 if item is None:
1828 if item is None:
1829 item = self.currentselecteditem
1829 item = self.currentselecteditem
1830 if isinstance(item, uiheader):
1830 if isinstance(item, uiheader):
1831 return
1831 return
1832 if isinstance(item, uihunkline):
1832 if isinstance(item, uihunkline):
1833 item = item.parentitem()
1833 item = item.parentitem()
1834 if not isinstance(item, uihunk):
1834 if not isinstance(item, uihunk):
1835 return
1835 return
1836
1836
1837 # To go back to that hunk or its replacement at the end of the edit
1837 # To go back to that hunk or its replacement at the end of the edit
1838 itemindex = item.parentitem().hunks.index(item)
1838 itemindex = item.parentitem().hunks.index(item)
1839
1839
1840 beforeadded, beforeremoved = item.added, item.removed
1840 beforeadded, beforeremoved = item.added, item.removed
1841 newpatches = editpatchwitheditor(self, item)
1841 newpatches = editpatchwitheditor(self, item)
1842 if newpatches is None:
1842 if newpatches is None:
1843 if not test:
1843 if not test:
1844 updateui(self)
1844 updateui(self)
1845 return
1845 return
1846 header = item.header
1846 header = item.header
1847 editedhunkindex = header.hunks.index(item)
1847 editedhunkindex = header.hunks.index(item)
1848 hunksbefore = header.hunks[:editedhunkindex]
1848 hunksbefore = header.hunks[:editedhunkindex]
1849 hunksafter = header.hunks[editedhunkindex + 1 :]
1849 hunksafter = header.hunks[editedhunkindex + 1 :]
1850 newpatchheader = newpatches[0]
1850 newpatchheader = newpatches[0]
1851 newhunks = [uihunk(h, header) for h in newpatchheader.hunks]
1851 newhunks = [uihunk(h, header) for h in newpatchheader.hunks]
1852 newadded = sum([h.added for h in newhunks])
1852 newadded = sum([h.added for h in newhunks])
1853 newremoved = sum([h.removed for h in newhunks])
1853 newremoved = sum([h.removed for h in newhunks])
1854 offset = (newadded - beforeadded) - (newremoved - beforeremoved)
1854 offset = (newadded - beforeadded) - (newremoved - beforeremoved)
1855
1855
1856 for h in hunksafter:
1856 for h in hunksafter:
1857 h.toline += offset
1857 h.toline += offset
1858 for h in newhunks:
1858 for h in newhunks:
1859 h.folded = False
1859 h.folded = False
1860 header.hunks = hunksbefore + newhunks + hunksafter
1860 header.hunks = hunksbefore + newhunks + hunksafter
1861 if self.emptypatch():
1861 if self.emptypatch():
1862 header.hunks = hunksbefore + [item] + hunksafter
1862 header.hunks = hunksbefore + [item] + hunksafter
1863 self.currentselecteditem = header
1863 self.currentselecteditem = header
1864 if len(header.hunks) > itemindex:
1864 if len(header.hunks) > itemindex:
1865 self.currentselecteditem = header.hunks[itemindex]
1865 self.currentselecteditem = header.hunks[itemindex]
1866
1866
1867 if not test:
1867 if not test:
1868 updateui(self)
1868 updateui(self)
1869
1869
1870 def emptypatch(self):
1870 def emptypatch(self):
1871 item = self.headerlist
1871 item = self.headerlist
1872 if not item:
1872 if not item:
1873 return True
1873 return True
1874 for header in item:
1874 for header in item:
1875 if header.hunks:
1875 if header.hunks:
1876 return False
1876 return False
1877 return True
1877 return True
1878
1878
1879 def handlekeypressed(self, keypressed, test=False):
1879 def handlekeypressed(self, keypressed, test=False):
1880 """
1880 """
1881 Perform actions based on pressed keys.
1881 Perform actions based on pressed keys.
1882
1882
1883 Return true to exit the main loop.
1883 Return true to exit the main loop.
1884 """
1884 """
1885 if keypressed in ["k", "KEY_UP"]:
1885 if keypressed in ["k", "KEY_UP"]:
1886 self.uparrowevent()
1886 self.uparrowevent()
1887 elif keypressed in ["K", "KEY_PPAGE"]:
1887 elif keypressed in ["K", "KEY_PPAGE"]:
1888 self.uparrowshiftevent()
1888 self.uparrowshiftevent()
1889 elif keypressed in ["j", "KEY_DOWN"]:
1889 elif keypressed in ["j", "KEY_DOWN"]:
1890 self.downarrowevent()
1890 self.downarrowevent()
1891 elif keypressed in ["J", "KEY_NPAGE"]:
1891 elif keypressed in ["J", "KEY_NPAGE"]:
1892 self.downarrowshiftevent()
1892 self.downarrowshiftevent()
1893 elif keypressed in ["l", "KEY_RIGHT"]:
1893 elif keypressed in ["l", "KEY_RIGHT"]:
1894 self.rightarrowevent()
1894 self.rightarrowevent()
1895 elif keypressed in ["h", "KEY_LEFT"]:
1895 elif keypressed in ["h", "KEY_LEFT"]:
1896 self.leftarrowevent()
1896 self.leftarrowevent()
1897 elif keypressed in ["H", "KEY_SLEFT"]:
1897 elif keypressed in ["H", "KEY_SLEFT"]:
1898 self.leftarrowshiftevent()
1898 self.leftarrowshiftevent()
1899 elif keypressed in ["q"]:
1899 elif keypressed in ["q"]:
1900 raise error.Abort(_(b'user quit'))
1900 raise error.Abort(_(b'user quit'))
1901 elif keypressed in ['a']:
1901 elif keypressed in ['a']:
1902 self.flipselections()
1902 self.flipselections()
1903 elif keypressed in ["c"]:
1903 elif keypressed in ["c"]:
1904 return True
1904 return True
1905 elif keypressed in ["r"]:
1905 elif keypressed in ["r"]:
1906 if self.reviewcommit():
1906 if self.reviewcommit():
1907 self.opts[b'review'] = True
1907 self.opts[b'review'] = True
1908 return True
1908 return True
1909 elif test and keypressed in ["R"]:
1909 elif test and keypressed in ["R"]:
1910 self.opts[b'review'] = True
1910 self.opts[b'review'] = True
1911 return True
1911 return True
1912 elif keypressed in [" ", "x"]:
1912 elif keypressed in [" ", "x"]:
1913 self.toggleapply()
1913 self.toggleapply()
1914 elif keypressed in ["\n", "KEY_ENTER"]:
1914 elif keypressed in ["\n", "KEY_ENTER"]:
1915 self.toggleapply()
1915 self.toggleapply()
1916 self.nextsametype(test=test)
1916 self.nextsametype(test=test)
1917 elif keypressed in ["X"]:
1917 elif keypressed in ["X"]:
1918 self.toggleallbetween()
1918 self.toggleallbetween()
1919 elif keypressed in ["A"]:
1919 elif keypressed in ["A"]:
1920 self.toggleall()
1920 self.toggleall()
1921 elif keypressed in ["e"]:
1921 elif keypressed in ["e"]:
1922 self.toggleedit(test=test)
1922 self.toggleedit(test=test)
1923 elif keypressed in ["f"]:
1923 elif keypressed in ["f"]:
1924 self.togglefolded()
1924 self.togglefolded()
1925 elif keypressed in ["F"]:
1925 elif keypressed in ["F"]:
1926 self.togglefolded(foldparent=True)
1926 self.togglefolded(foldparent=True)
1927 elif keypressed in ["m"]:
1927 elif keypressed in ["m"]:
1928 self.commitMessageWindow()
1928 self.commitMessageWindow()
1929 elif keypressed in ["g", "KEY_HOME"]:
1929 elif keypressed in ["g", "KEY_HOME"]:
1930 self.handlefirstlineevent()
1930 self.handlefirstlineevent()
1931 elif keypressed in ["G", "KEY_END"]:
1931 elif keypressed in ["G", "KEY_END"]:
1932 self.handlelastlineevent()
1932 self.handlelastlineevent()
1933 elif keypressed in ["?"]:
1933 elif keypressed in ["?"]:
1934 self.helpwindow()
1934 self.helpwindow()
1935 self.stdscr.clear()
1935 self.stdscr.clear()
1936 self.stdscr.refresh()
1936 self.stdscr.refresh()
1937 elif keypressed in [curses.ascii.ctrl("L")]:
1937 elif keypressed in [curses.ascii.ctrl("L")]:
1938 # scroll the current line to the top of the screen, and redraw
1938 # scroll the current line to the top of the screen, and redraw
1939 # everything
1939 # everything
1940 self.scrolllines(self.selecteditemstartline)
1940 self.scrolllines(self.selecteditemstartline)
1941 self.stdscr.clear()
1941 self.stdscr.clear()
1942 self.stdscr.refresh()
1942 self.stdscr.refresh()
1943
1943
1944 def main(self, stdscr):
1944 def main(self, stdscr):
1945 """
1945 """
1946 method to be wrapped by curses.wrapper() for selecting chunks.
1946 method to be wrapped by curses.wrapper() for selecting chunks.
1947 """
1947 """
1948
1948
1949 origsigwinch = sentinel = object()
1949 origsigwinch = sentinel = object()
1950 if util.safehasattr(signal, b'SIGWINCH'):
1950 if util.safehasattr(signal, b'SIGWINCH'):
1951 origsigwinch = signal.signal(signal.SIGWINCH, self.sigwinchhandler)
1951 origsigwinch = signal.signal(signal.SIGWINCH, self.sigwinchhandler)
1952 try:
1952 try:
1953 return self._main(stdscr)
1953 return self._main(stdscr)
1954 finally:
1954 finally:
1955 if origsigwinch is not sentinel:
1955 if origsigwinch is not sentinel:
1956 signal.signal(signal.SIGWINCH, origsigwinch)
1956 signal.signal(signal.SIGWINCH, origsigwinch)
1957
1957
1958 def _main(self, stdscr):
1958 def _main(self, stdscr):
1959 self.stdscr = stdscr
1959 self.stdscr = stdscr
1960 # error during initialization, cannot be printed in the curses
1960 # error during initialization, cannot be printed in the curses
1961 # interface, it should be printed by the calling code
1961 # interface, it should be printed by the calling code
1962 self.initexc = None
1962 self.initexc = None
1963 self.yscreensize, self.xscreensize = self.stdscr.getmaxyx()
1963 self.yscreensize, self.xscreensize = self.stdscr.getmaxyx()
1964
1964
1965 curses.start_color()
1965 curses.start_color()
1966 try:
1966 try:
1967 curses.use_default_colors()
1967 curses.use_default_colors()
1968 except curses.error:
1968 except curses.error:
1969 self.usecolor = False
1969 self.usecolor = False
1970
1970
1971 # In some situations we may have some cruft left on the "alternate
1971 # In some situations we may have some cruft left on the "alternate
1972 # screen" from another program (or previous iterations of ourself), and
1972 # screen" from another program (or previous iterations of ourself), and
1973 # we won't clear it if the scroll region is small enough to comfortably
1973 # we won't clear it if the scroll region is small enough to comfortably
1974 # fit on the terminal.
1974 # fit on the terminal.
1975 self.stdscr.clear()
1975 self.stdscr.clear()
1976
1976
1977 # don't display the cursor
1977 # don't display the cursor
1978 try:
1978 try:
1979 curses.curs_set(0)
1979 curses.curs_set(0)
1980 except curses.error:
1980 except curses.error:
1981 pass
1981 pass
1982
1982
1983 # available colors: black, blue, cyan, green, magenta, white, yellow
1983 # available colors: black, blue, cyan, green, magenta, white, yellow
1984 # init_pair(color_id, foreground_color, background_color)
1984 # init_pair(color_id, foreground_color, background_color)
1985 self.initcolorpair(None, None, name=b"normal")
1985 self.initcolorpair(None, None, name=b"normal")
1986 self.initcolorpair(
1986 self.initcolorpair(
1987 curses.COLOR_WHITE, curses.COLOR_MAGENTA, name=b"selected"
1987 curses.COLOR_WHITE, curses.COLOR_MAGENTA, name=b"selected"
1988 )
1988 )
1989 self.initcolorpair(curses.COLOR_RED, None, name=b"deletion")
1989 self.initcolorpair(curses.COLOR_RED, None, name=b"deletion")
1990 self.initcolorpair(curses.COLOR_GREEN, None, name=b"addition")
1990 self.initcolorpair(curses.COLOR_GREEN, None, name=b"addition")
1991 self.initcolorpair(
1991 self.initcolorpair(
1992 curses.COLOR_WHITE, curses.COLOR_BLUE, name=b"legend"
1992 curses.COLOR_WHITE, curses.COLOR_BLUE, name=b"legend"
1993 )
1993 )
1994 # newwin([height, width,] begin_y, begin_x)
1994 # newwin([height, width,] begin_y, begin_x)
1995 self.statuswin = curses.newwin(self.numstatuslines, 0, 0, 0)
1995 self.statuswin = curses.newwin(self.numstatuslines, 0, 0, 0)
1996 self.statuswin.keypad(1) # interpret arrow-key, etc. esc sequences
1996 self.statuswin.keypad(1) # interpret arrow-key, etc. esc sequences
1997
1997
1998 # figure out how much space to allocate for the chunk-pad which is
1998 # figure out how much space to allocate for the chunk-pad which is
1999 # used for displaying the patch
1999 # used for displaying the patch
2000
2000
2001 # stupid hack to prevent getnumlinesdisplayed from failing
2001 # stupid hack to prevent getnumlinesdisplayed from failing
2002 self.chunkpad = curses.newpad(1, self.xscreensize)
2002 self.chunkpad = curses.newpad(1, self.xscreensize)
2003
2003
2004 # add 1 so to account for last line text reaching end of line
2004 # add 1 so to account for last line text reaching end of line
2005 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
2005 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
2006
2006
2007 try:
2007 try:
2008 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
2008 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
2009 except curses.error:
2009 except curses.error:
2010 self.initexc = fallbackerror(
2010 self.initexc = fallbackerror(
2011 _(b'this diff is too large to be displayed')
2011 _(b'this diff is too large to be displayed')
2012 )
2012 )
2013 return
2013 return
2014 # initialize selecteditemendline (initial start-line is 0)
2014 # initialize selecteditemendline (initial start-line is 0)
2015 self.selecteditemendline = self.getnumlinesdisplayed(
2015 self.selecteditemendline = self.getnumlinesdisplayed(
2016 self.currentselecteditem, recursechildren=False
2016 self.currentselecteditem, recursechildren=False
2017 )
2017 )
2018
2018
2019 while True:
2019 while True:
2020 self.updatescreen()
2020 self.updatescreen()
2021 try:
2021 try:
2022 with self.ui.timeblockedsection(b'crecord'):
2022 with self.ui.timeblockedsection(b'crecord'):
2023 keypressed = self.statuswin.getkey()
2023 keypressed = self.statuswin.getkey()
2024 if self.errorstr is not None:
2024 if self.errorstr is not None:
2025 self.errorstr = None
2025 self.errorstr = None
2026 continue
2026 continue
2027 except curses.error:
2027 except curses.error:
2028 keypressed = b"foobar"
2028 keypressed = b"foobar"
2029 if self.handlekeypressed(keypressed):
2029 if self.handlekeypressed(keypressed):
2030 break
2030 break
2031
2031
2032 if self.commenttext != b"":
2032 if self.commenttext != b"":
2033 whitespaceremoved = re.sub(
2033 whitespaceremoved = re.sub(
2034 br"(?m)^\s.*(\n|$)", b"", self.commenttext
2034 br"(?m)^\s.*(\n|$)", b"", self.commenttext
2035 )
2035 )
2036 if whitespaceremoved != b"":
2036 if whitespaceremoved != b"":
2037 self.opts[b'message'] = self.commenttext
2037 self.opts[b'message'] = self.commenttext
@@ -1,1041 +1,1041 b''
1 from __future__ import absolute_import, print_function
1 from __future__ import absolute_import, print_function
2
2
3 import distutils.version
3 import distutils.version
4 import os
4 import os
5 import re
5 import re
6 import socket
6 import socket
7 import stat
7 import stat
8 import subprocess
8 import subprocess
9 import sys
9 import sys
10 import tempfile
10 import tempfile
11
11
12 tempprefix = 'hg-hghave-'
12 tempprefix = 'hg-hghave-'
13
13
14 checks = {
14 checks = {
15 "true": (lambda: True, "yak shaving"),
15 "true": (lambda: True, "yak shaving"),
16 "false": (lambda: False, "nail clipper"),
16 "false": (lambda: False, "nail clipper"),
17 }
17 }
18
18
19 try:
19 try:
20 import msvcrt
20 import msvcrt
21
21
22 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
22 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
23 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
23 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
24 except ImportError:
24 except ImportError:
25 pass
25 pass
26
26
27 stdout = getattr(sys.stdout, 'buffer', sys.stdout)
27 stdout = getattr(sys.stdout, 'buffer', sys.stdout)
28 stderr = getattr(sys.stderr, 'buffer', sys.stderr)
28 stderr = getattr(sys.stderr, 'buffer', sys.stderr)
29
29
30 if sys.version_info[0] >= 3:
30 if sys.version_info[0] >= 3:
31
31
32 def _bytespath(p):
32 def _bytespath(p):
33 if p is None:
33 if p is None:
34 return p
34 return p
35 return p.encode('utf-8')
35 return p.encode('utf-8')
36
36
37 def _strpath(p):
37 def _strpath(p):
38 if p is None:
38 if p is None:
39 return p
39 return p
40 return p.decode('utf-8')
40 return p.decode('utf-8')
41
41
42
42
43 else:
43 else:
44
44
45 def _bytespath(p):
45 def _bytespath(p):
46 return p
46 return p
47
47
48 _strpath = _bytespath
48 _strpath = _bytespath
49
49
50
50
51 def check(name, desc):
51 def check(name, desc):
52 """Registers a check function for a feature."""
52 """Registers a check function for a feature."""
53
53
54 def decorator(func):
54 def decorator(func):
55 checks[name] = (func, desc)
55 checks[name] = (func, desc)
56 return func
56 return func
57
57
58 return decorator
58 return decorator
59
59
60
60
61 def checkvers(name, desc, vers):
61 def checkvers(name, desc, vers):
62 """Registers a check function for each of a series of versions.
62 """Registers a check function for each of a series of versions.
63
63
64 vers can be a list or an iterator.
64 vers can be a list or an iterator.
65
65
66 Produces a series of feature checks that have the form <name><vers> without
66 Produces a series of feature checks that have the form <name><vers> without
67 any punctuation (even if there's punctuation in 'vers'; i.e. this produces
67 any punctuation (even if there's punctuation in 'vers'; i.e. this produces
68 'py38', not 'py3.8' or 'py-38')."""
68 'py38', not 'py3.8' or 'py-38')."""
69
69
70 def decorator(func):
70 def decorator(func):
71 def funcv(v):
71 def funcv(v):
72 def f():
72 def f():
73 return func(v)
73 return func(v)
74
74
75 return f
75 return f
76
76
77 for v in vers:
77 for v in vers:
78 v = str(v)
78 v = str(v)
79 f = funcv(v)
79 f = funcv(v)
80 checks['%s%s' % (name, v.replace('.', ''))] = (f, desc % v)
80 checks['%s%s' % (name, v.replace('.', ''))] = (f, desc % v)
81 return func
81 return func
82
82
83 return decorator
83 return decorator
84
84
85
85
86 def checkfeatures(features):
86 def checkfeatures(features):
87 result = {
87 result = {
88 'error': [],
88 'error': [],
89 'missing': [],
89 'missing': [],
90 'skipped': [],
90 'skipped': [],
91 }
91 }
92
92
93 for feature in features:
93 for feature in features:
94 negate = feature.startswith('no-')
94 negate = feature.startswith('no-')
95 if negate:
95 if negate:
96 feature = feature[3:]
96 feature = feature[3:]
97
97
98 if feature not in checks:
98 if feature not in checks:
99 result['missing'].append(feature)
99 result['missing'].append(feature)
100 continue
100 continue
101
101
102 check, desc = checks[feature]
102 check, desc = checks[feature]
103 try:
103 try:
104 available = check()
104 available = check()
105 except Exception:
105 except Exception:
106 result['error'].append('hghave check failed: %s' % feature)
106 result['error'].append('hghave check failed: %s' % feature)
107 continue
107 continue
108
108
109 if not negate and not available:
109 if not negate and not available:
110 result['skipped'].append('missing feature: %s' % desc)
110 result['skipped'].append('missing feature: %s' % desc)
111 elif negate and available:
111 elif negate and available:
112 result['skipped'].append('system supports %s' % desc)
112 result['skipped'].append('system supports %s' % desc)
113
113
114 return result
114 return result
115
115
116
116
117 def require(features):
117 def require(features):
118 """Require that features are available, exiting if not."""
118 """Require that features are available, exiting if not."""
119 result = checkfeatures(features)
119 result = checkfeatures(features)
120
120
121 for missing in result['missing']:
121 for missing in result['missing']:
122 stderr.write(
122 stderr.write(
123 ('skipped: unknown feature: %s\n' % missing).encode('utf-8')
123 ('skipped: unknown feature: %s\n' % missing).encode('utf-8')
124 )
124 )
125 for msg in result['skipped']:
125 for msg in result['skipped']:
126 stderr.write(('skipped: %s\n' % msg).encode('utf-8'))
126 stderr.write(('skipped: %s\n' % msg).encode('utf-8'))
127 for msg in result['error']:
127 for msg in result['error']:
128 stderr.write(('%s\n' % msg).encode('utf-8'))
128 stderr.write(('%s\n' % msg).encode('utf-8'))
129
129
130 if result['missing']:
130 if result['missing']:
131 sys.exit(2)
131 sys.exit(2)
132
132
133 if result['skipped'] or result['error']:
133 if result['skipped'] or result['error']:
134 sys.exit(1)
134 sys.exit(1)
135
135
136
136
137 def matchoutput(cmd, regexp, ignorestatus=False):
137 def matchoutput(cmd, regexp, ignorestatus=False):
138 """Return the match object if cmd executes successfully and its output
138 """Return the match object if cmd executes successfully and its output
139 is matched by the supplied regular expression.
139 is matched by the supplied regular expression.
140 """
140 """
141 r = re.compile(regexp)
141 r = re.compile(regexp)
142 p = subprocess.Popen(
142 p = subprocess.Popen(
143 cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
143 cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
144 )
144 )
145 s = p.communicate()[0]
145 s = p.communicate()[0]
146 ret = p.returncode
146 ret = p.returncode
147 return (ignorestatus or not ret) and r.search(s)
147 return (ignorestatus or not ret) and r.search(s)
148
148
149
149
150 @check("baz", "GNU Arch baz client")
150 @check("baz", "GNU Arch baz client")
151 def has_baz():
151 def has_baz():
152 return matchoutput('baz --version 2>&1', br'baz Bazaar version')
152 return matchoutput('baz --version 2>&1', br'baz Bazaar version')
153
153
154
154
155 @check("bzr", "Canonical's Bazaar client")
155 @check("bzr", "Canonical's Bazaar client")
156 def has_bzr():
156 def has_bzr():
157 try:
157 try:
158 import bzrlib
158 import bzrlib
159 import bzrlib.bzrdir
159 import bzrlib.bzrdir
160 import bzrlib.errors
160 import bzrlib.errors
161 import bzrlib.revision
161 import bzrlib.revision
162 import bzrlib.revisionspec
162 import bzrlib.revisionspec
163
163
164 bzrlib.revisionspec.RevisionSpec
164 bzrlib.revisionspec.RevisionSpec
165 return bzrlib.__doc__ is not None
165 return bzrlib.__doc__ is not None
166 except (AttributeError, ImportError):
166 except (AttributeError, ImportError):
167 return False
167 return False
168
168
169
169
170 @checkvers("bzr", "Canonical's Bazaar client >= %s", (1.14,))
170 @checkvers("bzr", "Canonical's Bazaar client >= %s", (1.14,))
171 def has_bzr_range(v):
171 def has_bzr_range(v):
172 major, minor = v.split('rc')[0].split('.')[0:2]
172 major, minor = v.split('rc')[0].split('.')[0:2]
173 try:
173 try:
174 import bzrlib
174 import bzrlib
175
175
176 return bzrlib.__doc__ is not None and bzrlib.version_info[:2] >= (
176 return bzrlib.__doc__ is not None and bzrlib.version_info[:2] >= (
177 int(major),
177 int(major),
178 int(minor),
178 int(minor),
179 )
179 )
180 except ImportError:
180 except ImportError:
181 return False
181 return False
182
182
183
183
184 @check("chg", "running with chg")
184 @check("chg", "running with chg")
185 def has_chg():
185 def has_chg():
186 return 'CHGHG' in os.environ
186 return 'CHGHG' in os.environ
187
187
188
188
189 @check("cvs", "cvs client/server")
189 @check("cvs", "cvs client/server")
190 def has_cvs():
190 def has_cvs():
191 re = br'Concurrent Versions System.*?server'
191 re = br'Concurrent Versions System.*?server'
192 return matchoutput('cvs --version 2>&1', re) and not has_msys()
192 return matchoutput('cvs --version 2>&1', re) and not has_msys()
193
193
194
194
195 @check("cvs112", "cvs client/server 1.12.* (not cvsnt)")
195 @check("cvs112", "cvs client/server 1.12.* (not cvsnt)")
196 def has_cvs112():
196 def has_cvs112():
197 re = br'Concurrent Versions System \(CVS\) 1.12.*?server'
197 re = br'Concurrent Versions System \(CVS\) 1.12.*?server'
198 return matchoutput('cvs --version 2>&1', re) and not has_msys()
198 return matchoutput('cvs --version 2>&1', re) and not has_msys()
199
199
200
200
201 @check("cvsnt", "cvsnt client/server")
201 @check("cvsnt", "cvsnt client/server")
202 def has_cvsnt():
202 def has_cvsnt():
203 re = br'Concurrent Versions System \(CVSNT\) (\d+).(\d+).*\(client/server\)'
203 re = br'Concurrent Versions System \(CVSNT\) (\d+).(\d+).*\(client/server\)'
204 return matchoutput('cvsnt --version 2>&1', re)
204 return matchoutput('cvsnt --version 2>&1', re)
205
205
206
206
207 @check("darcs", "darcs client")
207 @check("darcs", "darcs client")
208 def has_darcs():
208 def has_darcs():
209 return matchoutput('darcs --version', br'\b2\.([2-9]|\d{2})', True)
209 return matchoutput('darcs --version', br'\b2\.([2-9]|\d{2})', True)
210
210
211
211
212 @check("mtn", "monotone client (>= 1.0)")
212 @check("mtn", "monotone client (>= 1.0)")
213 def has_mtn():
213 def has_mtn():
214 return matchoutput('mtn --version', br'monotone', True) and not matchoutput(
214 return matchoutput('mtn --version', br'monotone', True) and not matchoutput(
215 'mtn --version', br'monotone 0\.', True
215 'mtn --version', br'monotone 0\.', True
216 )
216 )
217
217
218
218
219 @check("eol-in-paths", "end-of-lines in paths")
219 @check("eol-in-paths", "end-of-lines in paths")
220 def has_eol_in_paths():
220 def has_eol_in_paths():
221 try:
221 try:
222 fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix, suffix='\n\r')
222 fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix, suffix='\n\r')
223 os.close(fd)
223 os.close(fd)
224 os.remove(path)
224 os.remove(path)
225 return True
225 return True
226 except (IOError, OSError):
226 except (IOError, OSError):
227 return False
227 return False
228
228
229
229
230 @check("execbit", "executable bit")
230 @check("execbit", "executable bit")
231 def has_executablebit():
231 def has_executablebit():
232 try:
232 try:
233 EXECFLAGS = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
233 EXECFLAGS = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
234 fh, fn = tempfile.mkstemp(dir='.', prefix=tempprefix)
234 fh, fn = tempfile.mkstemp(dir='.', prefix=tempprefix)
235 try:
235 try:
236 os.close(fh)
236 os.close(fh)
237 m = os.stat(fn).st_mode & 0o777
237 m = os.stat(fn).st_mode & 0o777
238 new_file_has_exec = m & EXECFLAGS
238 new_file_has_exec = m & EXECFLAGS
239 os.chmod(fn, m ^ EXECFLAGS)
239 os.chmod(fn, m ^ EXECFLAGS)
240 exec_flags_cannot_flip = (os.stat(fn).st_mode & 0o777) == m
240 exec_flags_cannot_flip = (os.stat(fn).st_mode & 0o777) == m
241 finally:
241 finally:
242 os.unlink(fn)
242 os.unlink(fn)
243 except (IOError, OSError):
243 except (IOError, OSError):
244 # we don't care, the user probably won't be able to commit anyway
244 # we don't care, the user probably won't be able to commit anyway
245 return False
245 return False
246 return not (new_file_has_exec or exec_flags_cannot_flip)
246 return not (new_file_has_exec or exec_flags_cannot_flip)
247
247
248
248
249 @check("icasefs", "case insensitive file system")
249 @check("icasefs", "case insensitive file system")
250 def has_icasefs():
250 def has_icasefs():
251 # Stolen from mercurial.util
251 # Stolen from mercurial.util
252 fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix)
252 fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix)
253 os.close(fd)
253 os.close(fd)
254 try:
254 try:
255 s1 = os.stat(path)
255 s1 = os.stat(path)
256 d, b = os.path.split(path)
256 d, b = os.path.split(path)
257 p2 = os.path.join(d, b.upper())
257 p2 = os.path.join(d, b.upper())
258 if path == p2:
258 if path == p2:
259 p2 = os.path.join(d, b.lower())
259 p2 = os.path.join(d, b.lower())
260 try:
260 try:
261 s2 = os.stat(p2)
261 s2 = os.stat(p2)
262 return s2 == s1
262 return s2 == s1
263 except OSError:
263 except OSError:
264 return False
264 return False
265 finally:
265 finally:
266 os.remove(path)
266 os.remove(path)
267
267
268
268
269 @check("fifo", "named pipes")
269 @check("fifo", "named pipes")
270 def has_fifo():
270 def has_fifo():
271 if getattr(os, "mkfifo", None) is None:
271 if getattr(os, "mkfifo", None) is None:
272 return False
272 return False
273 name = tempfile.mktemp(dir='.', prefix=tempprefix)
273 name = tempfile.mktemp(dir='.', prefix=tempprefix)
274 try:
274 try:
275 os.mkfifo(name)
275 os.mkfifo(name)
276 os.unlink(name)
276 os.unlink(name)
277 return True
277 return True
278 except OSError:
278 except OSError:
279 return False
279 return False
280
280
281
281
282 @check("killdaemons", 'killdaemons.py support')
282 @check("killdaemons", 'killdaemons.py support')
283 def has_killdaemons():
283 def has_killdaemons():
284 return True
284 return True
285
285
286
286
287 @check("cacheable", "cacheable filesystem")
287 @check("cacheable", "cacheable filesystem")
288 def has_cacheable_fs():
288 def has_cacheable_fs():
289 from mercurial import util
289 from mercurial import util
290
290
291 fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix)
291 fd, path = tempfile.mkstemp(dir='.', prefix=tempprefix)
292 os.close(fd)
292 os.close(fd)
293 try:
293 try:
294 return util.cachestat(path).cacheable()
294 return util.cachestat(path).cacheable()
295 finally:
295 finally:
296 os.remove(path)
296 os.remove(path)
297
297
298
298
299 @check("lsprof", "python lsprof module")
299 @check("lsprof", "python lsprof module")
300 def has_lsprof():
300 def has_lsprof():
301 try:
301 try:
302 import _lsprof
302 import _lsprof
303
303
304 _lsprof.Profiler # silence unused import warning
304 _lsprof.Profiler # silence unused import warning
305 return True
305 return True
306 except ImportError:
306 except ImportError:
307 return False
307 return False
308
308
309
309
310 def gethgversion():
310 def gethgversion():
311 m = matchoutput('hg --version --quiet 2>&1', br'(\d+)\.(\d+)')
311 m = matchoutput('hg --version --quiet 2>&1', br'(\d+)\.(\d+)')
312 if not m:
312 if not m:
313 return (0, 0)
313 return (0, 0)
314 return (int(m.group(1)), int(m.group(2)))
314 return (int(m.group(1)), int(m.group(2)))
315
315
316
316
317 @checkvers(
317 @checkvers(
318 "hg", "Mercurial >= %s", list([(1.0 * x) / 10 for x in range(9, 99)])
318 "hg", "Mercurial >= %s", list([(1.0 * x) / 10 for x in range(9, 99)])
319 )
319 )
320 def has_hg_range(v):
320 def has_hg_range(v):
321 major, minor = v.split('.')[0:2]
321 major, minor = v.split('.')[0:2]
322 return gethgversion() >= (int(major), int(minor))
322 return gethgversion() >= (int(major), int(minor))
323
323
324
324
325 @check("hg08", "Mercurial >= 0.8")
325 @check("hg08", "Mercurial >= 0.8")
326 def has_hg08():
326 def has_hg08():
327 if checks["hg09"][0]():
327 if checks["hg09"][0]():
328 return True
328 return True
329 return matchoutput('hg help annotate 2>&1', '--date')
329 return matchoutput('hg help annotate 2>&1', '--date')
330
330
331
331
332 @check("hg07", "Mercurial >= 0.7")
332 @check("hg07", "Mercurial >= 0.7")
333 def has_hg07():
333 def has_hg07():
334 if checks["hg08"][0]():
334 if checks["hg08"][0]():
335 return True
335 return True
336 return matchoutput('hg --version --quiet 2>&1', 'Mercurial Distributed SCM')
336 return matchoutput('hg --version --quiet 2>&1', 'Mercurial Distributed SCM')
337
337
338
338
339 @check("hg06", "Mercurial >= 0.6")
339 @check("hg06", "Mercurial >= 0.6")
340 def has_hg06():
340 def has_hg06():
341 if checks["hg07"][0]():
341 if checks["hg07"][0]():
342 return True
342 return True
343 return matchoutput('hg --version --quiet 2>&1', 'Mercurial version')
343 return matchoutput('hg --version --quiet 2>&1', 'Mercurial version')
344
344
345
345
346 @check("gettext", "GNU Gettext (msgfmt)")
346 @check("gettext", "GNU Gettext (msgfmt)")
347 def has_gettext():
347 def has_gettext():
348 return matchoutput('msgfmt --version', br'GNU gettext-tools')
348 return matchoutput('msgfmt --version', br'GNU gettext-tools')
349
349
350
350
351 @check("git", "git command line client")
351 @check("git", "git command line client")
352 def has_git():
352 def has_git():
353 return matchoutput('git --version 2>&1', br'^git version')
353 return matchoutput('git --version 2>&1', br'^git version')
354
354
355
355
356 def getgitversion():
356 def getgitversion():
357 m = matchoutput('git --version 2>&1', br'git version (\d+)\.(\d+)')
357 m = matchoutput('git --version 2>&1', br'git version (\d+)\.(\d+)')
358 if not m:
358 if not m:
359 return (0, 0)
359 return (0, 0)
360 return (int(m.group(1)), int(m.group(2)))
360 return (int(m.group(1)), int(m.group(2)))
361
361
362
362
363 # https://github.com/git-lfs/lfs-test-server
363 # https://github.com/git-lfs/lfs-test-server
364 @check("lfs-test-server", "git-lfs test server")
364 @check("lfs-test-server", "git-lfs test server")
365 def has_lfsserver():
365 def has_lfsserver():
366 exe = 'lfs-test-server'
366 exe = 'lfs-test-server'
367 if has_windows():
367 if has_windows():
368 exe = 'lfs-test-server.exe'
368 exe = 'lfs-test-server.exe'
369 return any(
369 return any(
370 os.access(os.path.join(path, exe), os.X_OK)
370 os.access(os.path.join(path, exe), os.X_OK)
371 for path in os.environ["PATH"].split(os.pathsep)
371 for path in os.environ["PATH"].split(os.pathsep)
372 )
372 )
373
373
374
374
375 @checkvers("git", "git client (with ext::sh support) version >= %s", (1.9,))
375 @checkvers("git", "git client (with ext::sh support) version >= %s", (1.9,))
376 def has_git_range(v):
376 def has_git_range(v):
377 major, minor = v.split('.')[0:2]
377 major, minor = v.split('.')[0:2]
378 return getgitversion() >= (int(major), int(minor))
378 return getgitversion() >= (int(major), int(minor))
379
379
380
380
381 @check("docutils", "Docutils text processing library")
381 @check("docutils", "Docutils text processing library")
382 def has_docutils():
382 def has_docutils():
383 try:
383 try:
384 import docutils.core
384 import docutils.core
385
385
386 docutils.core.publish_cmdline # silence unused import
386 docutils.core.publish_cmdline # silence unused import
387 return True
387 return True
388 except ImportError:
388 except ImportError:
389 return False
389 return False
390
390
391
391
392 def getsvnversion():
392 def getsvnversion():
393 m = matchoutput('svn --version --quiet 2>&1', br'^(\d+)\.(\d+)')
393 m = matchoutput('svn --version --quiet 2>&1', br'^(\d+)\.(\d+)')
394 if not m:
394 if not m:
395 return (0, 0)
395 return (0, 0)
396 return (int(m.group(1)), int(m.group(2)))
396 return (int(m.group(1)), int(m.group(2)))
397
397
398
398
399 @checkvers("svn", "subversion client and admin tools >= %s", (1.3, 1.5))
399 @checkvers("svn", "subversion client and admin tools >= %s", (1.3, 1.5))
400 def has_svn_range(v):
400 def has_svn_range(v):
401 major, minor = v.split('.')[0:2]
401 major, minor = v.split('.')[0:2]
402 return getsvnversion() >= (int(major), int(minor))
402 return getsvnversion() >= (int(major), int(minor))
403
403
404
404
405 @check("svn", "subversion client and admin tools")
405 @check("svn", "subversion client and admin tools")
406 def has_svn():
406 def has_svn():
407 return matchoutput('svn --version 2>&1', br'^svn, version') and matchoutput(
407 return matchoutput('svn --version 2>&1', br'^svn, version') and matchoutput(
408 'svnadmin --version 2>&1', br'^svnadmin, version'
408 'svnadmin --version 2>&1', br'^svnadmin, version'
409 )
409 )
410
410
411
411
412 @check("svn-bindings", "subversion python bindings")
412 @check("svn-bindings", "subversion python bindings")
413 def has_svn_bindings():
413 def has_svn_bindings():
414 try:
414 try:
415 import svn.core
415 import svn.core
416
416
417 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
417 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
418 if version < (1, 4):
418 if version < (1, 4):
419 return False
419 return False
420 return True
420 return True
421 except ImportError:
421 except ImportError:
422 return False
422 return False
423
423
424
424
425 @check("p4", "Perforce server and client")
425 @check("p4", "Perforce server and client")
426 def has_p4():
426 def has_p4():
427 return matchoutput('p4 -V', br'Rev\. P4/') and matchoutput(
427 return matchoutput('p4 -V', br'Rev\. P4/') and matchoutput(
428 'p4d -V', br'Rev\. P4D/'
428 'p4d -V', br'Rev\. P4D/'
429 )
429 )
430
430
431
431
432 @check("symlink", "symbolic links")
432 @check("symlink", "symbolic links")
433 def has_symlink():
433 def has_symlink():
434 # mercurial.windows.checklink() is a hard 'no' at the moment
434 # mercurial.windows.checklink() is a hard 'no' at the moment
435 if os.name == 'nt' or getattr(os, "symlink", None) is None:
435 if os.name == 'nt' or getattr(os, "symlink", None) is None:
436 return False
436 return False
437 name = tempfile.mktemp(dir='.', prefix=tempprefix)
437 name = tempfile.mktemp(dir='.', prefix=tempprefix)
438 try:
438 try:
439 os.symlink(".", name)
439 os.symlink(".", name)
440 os.unlink(name)
440 os.unlink(name)
441 return True
441 return True
442 except (OSError, AttributeError):
442 except (OSError, AttributeError):
443 return False
443 return False
444
444
445
445
446 @check("hardlink", "hardlinks")
446 @check("hardlink", "hardlinks")
447 def has_hardlink():
447 def has_hardlink():
448 from mercurial import util
448 from mercurial import util
449
449
450 fh, fn = tempfile.mkstemp(dir='.', prefix=tempprefix)
450 fh, fn = tempfile.mkstemp(dir='.', prefix=tempprefix)
451 os.close(fh)
451 os.close(fh)
452 name = tempfile.mktemp(dir='.', prefix=tempprefix)
452 name = tempfile.mktemp(dir='.', prefix=tempprefix)
453 try:
453 try:
454 util.oslink(_bytespath(fn), _bytespath(name))
454 util.oslink(_bytespath(fn), _bytespath(name))
455 os.unlink(name)
455 os.unlink(name)
456 return True
456 return True
457 except OSError:
457 except OSError:
458 return False
458 return False
459 finally:
459 finally:
460 os.unlink(fn)
460 os.unlink(fn)
461
461
462
462
463 @check("hardlink-whitelisted", "hardlinks on whitelisted filesystems")
463 @check("hardlink-whitelisted", "hardlinks on whitelisted filesystems")
464 def has_hardlink_whitelisted():
464 def has_hardlink_whitelisted():
465 from mercurial import util
465 from mercurial import util
466
466
467 try:
467 try:
468 fstype = util.getfstype(b'.')
468 fstype = util.getfstype(b'.')
469 except OSError:
469 except OSError:
470 return False
470 return False
471 return fstype in util._hardlinkfswhitelist
471 return fstype in util._hardlinkfswhitelist
472
472
473
473
474 @check("rmcwd", "can remove current working directory")
474 @check("rmcwd", "can remove current working directory")
475 def has_rmcwd():
475 def has_rmcwd():
476 ocwd = os.getcwd()
476 ocwd = os.getcwd()
477 temp = tempfile.mkdtemp(dir='.', prefix=tempprefix)
477 temp = tempfile.mkdtemp(dir='.', prefix=tempprefix)
478 try:
478 try:
479 os.chdir(temp)
479 os.chdir(temp)
480 # On Linux, 'rmdir .' isn't allowed, but the other names are okay.
480 # On Linux, 'rmdir .' isn't allowed, but the other names are okay.
481 # On Solaris and Windows, the cwd can't be removed by any names.
481 # On Solaris and Windows, the cwd can't be removed by any names.
482 os.rmdir(os.getcwd())
482 os.rmdir(os.getcwd())
483 return True
483 return True
484 except OSError:
484 except OSError:
485 return False
485 return False
486 finally:
486 finally:
487 os.chdir(ocwd)
487 os.chdir(ocwd)
488 # clean up temp dir on platforms where cwd can't be removed
488 # clean up temp dir on platforms where cwd can't be removed
489 try:
489 try:
490 os.rmdir(temp)
490 os.rmdir(temp)
491 except OSError:
491 except OSError:
492 pass
492 pass
493
493
494
494
495 @check("tla", "GNU Arch tla client")
495 @check("tla", "GNU Arch tla client")
496 def has_tla():
496 def has_tla():
497 return matchoutput('tla --version 2>&1', br'The GNU Arch Revision')
497 return matchoutput('tla --version 2>&1', br'The GNU Arch Revision')
498
498
499
499
500 @check("gpg", "gpg client")
500 @check("gpg", "gpg client")
501 def has_gpg():
501 def has_gpg():
502 return matchoutput('gpg --version 2>&1', br'GnuPG')
502 return matchoutput('gpg --version 2>&1', br'GnuPG')
503
503
504
504
505 @check("gpg2", "gpg client v2")
505 @check("gpg2", "gpg client v2")
506 def has_gpg2():
506 def has_gpg2():
507 return matchoutput('gpg --version 2>&1', br'GnuPG[^0-9]+2\.')
507 return matchoutput('gpg --version 2>&1', br'GnuPG[^0-9]+2\.')
508
508
509
509
510 @check("gpg21", "gpg client v2.1+")
510 @check("gpg21", "gpg client v2.1+")
511 def has_gpg21():
511 def has_gpg21():
512 return matchoutput('gpg --version 2>&1', br'GnuPG[^0-9]+2\.(?!0)')
512 return matchoutput('gpg --version 2>&1', br'GnuPG[^0-9]+2\.(?!0)')
513
513
514
514
515 @check("unix-permissions", "unix-style permissions")
515 @check("unix-permissions", "unix-style permissions")
516 def has_unix_permissions():
516 def has_unix_permissions():
517 d = tempfile.mkdtemp(dir='.', prefix=tempprefix)
517 d = tempfile.mkdtemp(dir='.', prefix=tempprefix)
518 try:
518 try:
519 fname = os.path.join(d, 'foo')
519 fname = os.path.join(d, 'foo')
520 for umask in (0o77, 0o07, 0o22):
520 for umask in (0o77, 0o07, 0o22):
521 os.umask(umask)
521 os.umask(umask)
522 f = open(fname, 'w')
522 f = open(fname, 'w')
523 f.close()
523 f.close()
524 mode = os.stat(fname).st_mode
524 mode = os.stat(fname).st_mode
525 os.unlink(fname)
525 os.unlink(fname)
526 if mode & 0o777 != ~umask & 0o666:
526 if mode & 0o777 != ~umask & 0o666:
527 return False
527 return False
528 return True
528 return True
529 finally:
529 finally:
530 os.rmdir(d)
530 os.rmdir(d)
531
531
532
532
533 @check("unix-socket", "AF_UNIX socket family")
533 @check("unix-socket", "AF_UNIX socket family")
534 def has_unix_socket():
534 def has_unix_socket():
535 return getattr(socket, 'AF_UNIX', None) is not None
535 return getattr(socket, 'AF_UNIX', None) is not None
536
536
537
537
538 @check("root", "root permissions")
538 @check("root", "root permissions")
539 def has_root():
539 def has_root():
540 return getattr(os, 'geteuid', None) and os.geteuid() == 0
540 return getattr(os, 'geteuid', None) and os.geteuid() == 0
541
541
542
542
543 @check("pyflakes", "Pyflakes python linter")
543 @check("pyflakes", "Pyflakes python linter")
544 def has_pyflakes():
544 def has_pyflakes():
545 return matchoutput(
545 return matchoutput(
546 "sh -c \"echo 'import re' 2>&1 | pyflakes\"",
546 "sh -c \"echo 'import re' 2>&1 | pyflakes\"",
547 br"<stdin>:1: 're' imported but unused",
547 br"<stdin>:1: 're' imported but unused",
548 True,
548 True,
549 )
549 )
550
550
551
551
552 @check("pylint", "Pylint python linter")
552 @check("pylint", "Pylint python linter")
553 def has_pylint():
553 def has_pylint():
554 return matchoutput("pylint --help", br"Usage: pylint", True)
554 return matchoutput("pylint --help", br"Usage: pylint", True)
555
555
556
556
557 @check("clang-format", "clang-format C code formatter")
557 @check("clang-format", "clang-format C code formatter")
558 def has_clang_format():
558 def has_clang_format():
559 m = matchoutput('clang-format --version', br'clang-format version (\d)')
559 m = matchoutput('clang-format --version', br'clang-format version (\d)')
560 # style changed somewhere between 4.x and 6.x
560 # style changed somewhere between 4.x and 6.x
561 return m and int(m.group(1)) >= 6
561 return m and int(m.group(1)) >= 6
562
562
563
563
564 @check("jshint", "JSHint static code analysis tool")
564 @check("jshint", "JSHint static code analysis tool")
565 def has_jshint():
565 def has_jshint():
566 return matchoutput("jshint --version 2>&1", br"jshint v")
566 return matchoutput("jshint --version 2>&1", br"jshint v")
567
567
568
568
569 @check("pygments", "Pygments source highlighting library")
569 @check("pygments", "Pygments source highlighting library")
570 def has_pygments():
570 def has_pygments():
571 try:
571 try:
572 import pygments
572 import pygments
573
573
574 pygments.highlight # silence unused import warning
574 pygments.highlight # silence unused import warning
575 return True
575 return True
576 except ImportError:
576 except ImportError:
577 return False
577 return False
578
578
579
579
580 @check("pygments25", "Pygments version >= 2.5")
580 @check("pygments25", "Pygments version >= 2.5")
581 def pygments25():
581 def pygments25():
582 try:
582 try:
583 import pygments
583 import pygments
584
584
585 v = pygments.__version__
585 v = pygments.__version__
586 except ImportError:
586 except ImportError:
587 return False
587 return False
588
588
589 parts = v.split(".")
589 parts = v.split(".")
590 major = int(parts[0])
590 major = int(parts[0])
591 minor = int(parts[1])
591 minor = int(parts[1])
592
592
593 return (major, minor) >= (2, 5)
593 return (major, minor) >= (2, 5)
594
594
595
595
596 @check("outer-repo", "outer repo")
596 @check("outer-repo", "outer repo")
597 def has_outer_repo():
597 def has_outer_repo():
598 # failing for other reasons than 'no repo' imply that there is a repo
598 # failing for other reasons than 'no repo' imply that there is a repo
599 return not matchoutput('hg root 2>&1', br'abort: no repository found', True)
599 return not matchoutput('hg root 2>&1', br'abort: no repository found', True)
600
600
601
601
602 @check("ssl", "ssl module available")
602 @check("ssl", "ssl module available")
603 def has_ssl():
603 def has_ssl():
604 try:
604 try:
605 import ssl
605 import ssl
606
606
607 ssl.CERT_NONE
607 ssl.CERT_NONE
608 return True
608 return True
609 except ImportError:
609 except ImportError:
610 return False
610 return False
611
611
612
612
613 @check("sslcontext", "python >= 2.7.9 ssl")
613 @check("sslcontext", "python >= 2.7.9 ssl")
614 def has_sslcontext():
614 def has_sslcontext():
615 try:
615 try:
616 import ssl
616 import ssl
617
617
618 ssl.SSLContext
618 ssl.SSLContext
619 return True
619 return True
620 except (ImportError, AttributeError):
620 except (ImportError, AttributeError):
621 return False
621 return False
622
622
623
623
624 @check("defaultcacerts", "can verify SSL certs by system's CA certs store")
624 @check("defaultcacerts", "can verify SSL certs by system's CA certs store")
625 def has_defaultcacerts():
625 def has_defaultcacerts():
626 from mercurial import sslutil, ui as uimod
626 from mercurial import sslutil, ui as uimod
627
627
628 ui = uimod.ui.load()
628 ui = uimod.ui.load()
629 return sslutil._defaultcacerts(ui) or sslutil._canloaddefaultcerts
629 return sslutil._defaultcacerts(ui) or sslutil._canloaddefaultcerts
630
630
631
631
632 @check("defaultcacertsloaded", "detected presence of loaded system CA certs")
632 @check("defaultcacertsloaded", "detected presence of loaded system CA certs")
633 def has_defaultcacertsloaded():
633 def has_defaultcacertsloaded():
634 import ssl
634 import ssl
635 from mercurial import sslutil, ui as uimod
635 from mercurial import sslutil, ui as uimod
636
636
637 if not has_defaultcacerts():
637 if not has_defaultcacerts():
638 return False
638 return False
639 if not has_sslcontext():
639 if not has_sslcontext():
640 return False
640 return False
641
641
642 ui = uimod.ui.load()
642 ui = uimod.ui.load()
643 cafile = sslutil._defaultcacerts(ui)
643 cafile = sslutil._defaultcacerts(ui)
644 ctx = ssl.create_default_context()
644 ctx = ssl.create_default_context()
645 if cafile:
645 if cafile:
646 ctx.load_verify_locations(cafile=cafile)
646 ctx.load_verify_locations(cafile=cafile)
647 else:
647 else:
648 ctx.load_default_certs()
648 ctx.load_default_certs()
649
649
650 return len(ctx.get_ca_certs()) > 0
650 return len(ctx.get_ca_certs()) > 0
651
651
652
652
653 @check("tls1.2", "TLS 1.2 protocol support")
653 @check("tls1.2", "TLS 1.2 protocol support")
654 def has_tls1_2():
654 def has_tls1_2():
655 from mercurial import sslutil
655 from mercurial import sslutil
656
656
657 return b'tls1.2' in sslutil.supportedprotocols
657 return b'tls1.2' in sslutil.supportedprotocols
658
658
659
659
660 @check("windows", "Windows")
660 @check("windows", "Windows")
661 def has_windows():
661 def has_windows():
662 return os.name == 'nt'
662 return os.name == 'nt'
663
663
664
664
665 @check("system-sh", "system() uses sh")
665 @check("system-sh", "system() uses sh")
666 def has_system_sh():
666 def has_system_sh():
667 return os.name != 'nt'
667 return os.name != 'nt'
668
668
669
669
670 @check("serve", "platform and python can manage 'hg serve -d'")
670 @check("serve", "platform and python can manage 'hg serve -d'")
671 def has_serve():
671 def has_serve():
672 return True
672 return True
673
673
674
674
675 @check("test-repo", "running tests from repository")
675 @check("test-repo", "running tests from repository")
676 def has_test_repo():
676 def has_test_repo():
677 t = os.environ["TESTDIR"]
677 t = os.environ["TESTDIR"]
678 return os.path.isdir(os.path.join(t, "..", ".hg"))
678 return os.path.isdir(os.path.join(t, "..", ".hg"))
679
679
680
680
681 @check("tic", "terminfo compiler and curses module")
681 @check("tic", "terminfo compiler and curses module")
682 def has_tic():
682 def has_tic():
683 try:
683 try:
684 import curses
684 import curses
685
685
686 curses.COLOR_BLUE
686 curses.COLOR_BLUE
687 return matchoutput('test -x "`which tic`"', br'')
687 return matchoutput('test -x "`which tic`"', br'')
688 except ImportError:
688 except (ImportError, AttributeError):
689 return False
689 return False
690
690
691
691
692 @check("xz", "xz compression utility")
692 @check("xz", "xz compression utility")
693 def has_xz():
693 def has_xz():
694 # When Windows invokes a subprocess in shell mode, it uses `cmd.exe`, which
694 # When Windows invokes a subprocess in shell mode, it uses `cmd.exe`, which
695 # only knows `where`, not `which`. So invoke MSYS shell explicitly.
695 # only knows `where`, not `which`. So invoke MSYS shell explicitly.
696 return matchoutput("sh -c 'test -x \"`which xz`\"'", b'')
696 return matchoutput("sh -c 'test -x \"`which xz`\"'", b'')
697
697
698
698
699 @check("msys", "Windows with MSYS")
699 @check("msys", "Windows with MSYS")
700 def has_msys():
700 def has_msys():
701 return os.getenv('MSYSTEM')
701 return os.getenv('MSYSTEM')
702
702
703
703
704 @check("aix", "AIX")
704 @check("aix", "AIX")
705 def has_aix():
705 def has_aix():
706 return sys.platform.startswith("aix")
706 return sys.platform.startswith("aix")
707
707
708
708
709 @check("osx", "OS X")
709 @check("osx", "OS X")
710 def has_osx():
710 def has_osx():
711 return sys.platform == 'darwin'
711 return sys.platform == 'darwin'
712
712
713
713
714 @check("osxpackaging", "OS X packaging tools")
714 @check("osxpackaging", "OS X packaging tools")
715 def has_osxpackaging():
715 def has_osxpackaging():
716 try:
716 try:
717 return (
717 return (
718 matchoutput('pkgbuild', br'Usage: pkgbuild ', ignorestatus=1)
718 matchoutput('pkgbuild', br'Usage: pkgbuild ', ignorestatus=1)
719 and matchoutput(
719 and matchoutput(
720 'productbuild', br'Usage: productbuild ', ignorestatus=1
720 'productbuild', br'Usage: productbuild ', ignorestatus=1
721 )
721 )
722 and matchoutput('lsbom', br'Usage: lsbom', ignorestatus=1)
722 and matchoutput('lsbom', br'Usage: lsbom', ignorestatus=1)
723 and matchoutput('xar --help', br'Usage: xar', ignorestatus=1)
723 and matchoutput('xar --help', br'Usage: xar', ignorestatus=1)
724 )
724 )
725 except ImportError:
725 except ImportError:
726 return False
726 return False
727
727
728
728
729 @check('linuxormacos', 'Linux or MacOS')
729 @check('linuxormacos', 'Linux or MacOS')
730 def has_linuxormacos():
730 def has_linuxormacos():
731 # This isn't a perfect test for MacOS. But it is sufficient for our needs.
731 # This isn't a perfect test for MacOS. But it is sufficient for our needs.
732 return sys.platform.startswith(('linux', 'darwin'))
732 return sys.platform.startswith(('linux', 'darwin'))
733
733
734
734
735 @check("docker", "docker support")
735 @check("docker", "docker support")
736 def has_docker():
736 def has_docker():
737 pat = br'A self-sufficient runtime for'
737 pat = br'A self-sufficient runtime for'
738 if matchoutput('docker --help', pat):
738 if matchoutput('docker --help', pat):
739 if 'linux' not in sys.platform:
739 if 'linux' not in sys.platform:
740 # TODO: in theory we should be able to test docker-based
740 # TODO: in theory we should be able to test docker-based
741 # package creation on non-linux using boot2docker, but in
741 # package creation on non-linux using boot2docker, but in
742 # practice that requires extra coordination to make sure
742 # practice that requires extra coordination to make sure
743 # $TESTTEMP is going to be visible at the same path to the
743 # $TESTTEMP is going to be visible at the same path to the
744 # boot2docker VM. If we figure out how to verify that, we
744 # boot2docker VM. If we figure out how to verify that, we
745 # can use the following instead of just saying False:
745 # can use the following instead of just saying False:
746 # return 'DOCKER_HOST' in os.environ
746 # return 'DOCKER_HOST' in os.environ
747 return False
747 return False
748
748
749 return True
749 return True
750 return False
750 return False
751
751
752
752
753 @check("debhelper", "debian packaging tools")
753 @check("debhelper", "debian packaging tools")
754 def has_debhelper():
754 def has_debhelper():
755 # Some versions of dpkg say `dpkg', some say 'dpkg' (` vs ' on the first
755 # Some versions of dpkg say `dpkg', some say 'dpkg' (` vs ' on the first
756 # quote), so just accept anything in that spot.
756 # quote), so just accept anything in that spot.
757 dpkg = matchoutput(
757 dpkg = matchoutput(
758 'dpkg --version', br"Debian .dpkg' package management program"
758 'dpkg --version', br"Debian .dpkg' package management program"
759 )
759 )
760 dh = matchoutput(
760 dh = matchoutput(
761 'dh --help', br'dh is a part of debhelper.', ignorestatus=True
761 'dh --help', br'dh is a part of debhelper.', ignorestatus=True
762 )
762 )
763 dh_py2 = matchoutput(
763 dh_py2 = matchoutput(
764 'dh_python2 --help', br'other supported Python versions'
764 'dh_python2 --help', br'other supported Python versions'
765 )
765 )
766 # debuild comes from the 'devscripts' package, though you might want
766 # debuild comes from the 'devscripts' package, though you might want
767 # the 'build-debs' package instead, which has a dependency on devscripts.
767 # the 'build-debs' package instead, which has a dependency on devscripts.
768 debuild = matchoutput(
768 debuild = matchoutput(
769 'debuild --help', br'to run debian/rules with given parameter'
769 'debuild --help', br'to run debian/rules with given parameter'
770 )
770 )
771 return dpkg and dh and dh_py2 and debuild
771 return dpkg and dh and dh_py2 and debuild
772
772
773
773
774 @check(
774 @check(
775 "debdeps", "debian build dependencies (run dpkg-checkbuilddeps in contrib/)"
775 "debdeps", "debian build dependencies (run dpkg-checkbuilddeps in contrib/)"
776 )
776 )
777 def has_debdeps():
777 def has_debdeps():
778 # just check exit status (ignoring output)
778 # just check exit status (ignoring output)
779 path = '%s/../contrib/packaging/debian/control' % os.environ['TESTDIR']
779 path = '%s/../contrib/packaging/debian/control' % os.environ['TESTDIR']
780 return matchoutput('dpkg-checkbuilddeps %s' % path, br'')
780 return matchoutput('dpkg-checkbuilddeps %s' % path, br'')
781
781
782
782
783 @check("demandimport", "demandimport enabled")
783 @check("demandimport", "demandimport enabled")
784 def has_demandimport():
784 def has_demandimport():
785 # chg disables demandimport intentionally for performance wins.
785 # chg disables demandimport intentionally for performance wins.
786 return (not has_chg()) and os.environ.get('HGDEMANDIMPORT') != 'disable'
786 return (not has_chg()) and os.environ.get('HGDEMANDIMPORT') != 'disable'
787
787
788
788
789 # Add "py27", "py35", ... as possible feature checks. Note that there's no
789 # Add "py27", "py35", ... as possible feature checks. Note that there's no
790 # punctuation here.
790 # punctuation here.
791 @checkvers("py", "Python >= %s", (2.7, 3.5, 3.6, 3.7, 3.8, 3.9))
791 @checkvers("py", "Python >= %s", (2.7, 3.5, 3.6, 3.7, 3.8, 3.9))
792 def has_python_range(v):
792 def has_python_range(v):
793 major, minor = v.split('.')[0:2]
793 major, minor = v.split('.')[0:2]
794 py_major, py_minor = sys.version_info.major, sys.version_info.minor
794 py_major, py_minor = sys.version_info.major, sys.version_info.minor
795
795
796 return (py_major, py_minor) >= (int(major), int(minor))
796 return (py_major, py_minor) >= (int(major), int(minor))
797
797
798
798
799 @check("py3", "running with Python 3.x")
799 @check("py3", "running with Python 3.x")
800 def has_py3():
800 def has_py3():
801 return 3 == sys.version_info[0]
801 return 3 == sys.version_info[0]
802
802
803
803
804 @check("py3exe", "a Python 3.x interpreter is available")
804 @check("py3exe", "a Python 3.x interpreter is available")
805 def has_python3exe():
805 def has_python3exe():
806 return matchoutput('python3 -V', br'^Python 3.(5|6|7|8|9)')
806 return matchoutput('python3 -V', br'^Python 3.(5|6|7|8|9)')
807
807
808
808
809 @check("pure", "running with pure Python code")
809 @check("pure", "running with pure Python code")
810 def has_pure():
810 def has_pure():
811 return any(
811 return any(
812 [
812 [
813 os.environ.get("HGMODULEPOLICY") == "py",
813 os.environ.get("HGMODULEPOLICY") == "py",
814 os.environ.get("HGTEST_RUN_TESTS_PURE") == "--pure",
814 os.environ.get("HGTEST_RUN_TESTS_PURE") == "--pure",
815 ]
815 ]
816 )
816 )
817
817
818
818
819 @check("slow", "allow slow tests (use --allow-slow-tests)")
819 @check("slow", "allow slow tests (use --allow-slow-tests)")
820 def has_slow():
820 def has_slow():
821 return os.environ.get('HGTEST_SLOW') == 'slow'
821 return os.environ.get('HGTEST_SLOW') == 'slow'
822
822
823
823
824 @check("hypothesis", "Hypothesis automated test generation")
824 @check("hypothesis", "Hypothesis automated test generation")
825 def has_hypothesis():
825 def has_hypothesis():
826 try:
826 try:
827 import hypothesis
827 import hypothesis
828
828
829 hypothesis.given
829 hypothesis.given
830 return True
830 return True
831 except ImportError:
831 except ImportError:
832 return False
832 return False
833
833
834
834
835 @check("unziplinks", "unzip(1) understands and extracts symlinks")
835 @check("unziplinks", "unzip(1) understands and extracts symlinks")
836 def unzip_understands_symlinks():
836 def unzip_understands_symlinks():
837 return matchoutput('unzip --help', br'Info-ZIP')
837 return matchoutput('unzip --help', br'Info-ZIP')
838
838
839
839
840 @check("zstd", "zstd Python module available")
840 @check("zstd", "zstd Python module available")
841 def has_zstd():
841 def has_zstd():
842 try:
842 try:
843 import mercurial.zstd
843 import mercurial.zstd
844
844
845 mercurial.zstd.__version__
845 mercurial.zstd.__version__
846 return True
846 return True
847 except ImportError:
847 except ImportError:
848 return False
848 return False
849
849
850
850
851 @check("devfull", "/dev/full special file")
851 @check("devfull", "/dev/full special file")
852 def has_dev_full():
852 def has_dev_full():
853 return os.path.exists('/dev/full')
853 return os.path.exists('/dev/full')
854
854
855
855
856 @check("ensurepip", "ensurepip module")
856 @check("ensurepip", "ensurepip module")
857 def has_ensurepip():
857 def has_ensurepip():
858 try:
858 try:
859 import ensurepip
859 import ensurepip
860
860
861 ensurepip.bootstrap
861 ensurepip.bootstrap
862 return True
862 return True
863 except ImportError:
863 except ImportError:
864 return False
864 return False
865
865
866
866
867 @check("virtualenv", "Python virtualenv support")
867 @check("virtualenv", "Python virtualenv support")
868 def has_virtualenv():
868 def has_virtualenv():
869 try:
869 try:
870 import virtualenv
870 import virtualenv
871
871
872 virtualenv.ACTIVATE_SH
872 virtualenv.ACTIVATE_SH
873 return True
873 return True
874 except ImportError:
874 except ImportError:
875 return False
875 return False
876
876
877
877
878 @check("fsmonitor", "running tests with fsmonitor")
878 @check("fsmonitor", "running tests with fsmonitor")
879 def has_fsmonitor():
879 def has_fsmonitor():
880 return 'HGFSMONITOR_TESTS' in os.environ
880 return 'HGFSMONITOR_TESTS' in os.environ
881
881
882
882
883 @check("fuzzywuzzy", "Fuzzy string matching library")
883 @check("fuzzywuzzy", "Fuzzy string matching library")
884 def has_fuzzywuzzy():
884 def has_fuzzywuzzy():
885 try:
885 try:
886 import fuzzywuzzy
886 import fuzzywuzzy
887
887
888 fuzzywuzzy.__version__
888 fuzzywuzzy.__version__
889 return True
889 return True
890 except ImportError:
890 except ImportError:
891 return False
891 return False
892
892
893
893
894 @check("clang-libfuzzer", "clang new enough to include libfuzzer")
894 @check("clang-libfuzzer", "clang new enough to include libfuzzer")
895 def has_clang_libfuzzer():
895 def has_clang_libfuzzer():
896 mat = matchoutput('clang --version', br'clang version (\d)')
896 mat = matchoutput('clang --version', br'clang version (\d)')
897 if mat:
897 if mat:
898 # libfuzzer is new in clang 6
898 # libfuzzer is new in clang 6
899 return int(mat.group(1)) > 5
899 return int(mat.group(1)) > 5
900 return False
900 return False
901
901
902
902
903 @check("clang-6.0", "clang 6.0 with version suffix (libfuzzer included)")
903 @check("clang-6.0", "clang 6.0 with version suffix (libfuzzer included)")
904 def has_clang60():
904 def has_clang60():
905 return matchoutput('clang-6.0 --version', br'clang version 6\.')
905 return matchoutput('clang-6.0 --version', br'clang version 6\.')
906
906
907
907
908 @check("xdiff", "xdiff algorithm")
908 @check("xdiff", "xdiff algorithm")
909 def has_xdiff():
909 def has_xdiff():
910 try:
910 try:
911 from mercurial import policy
911 from mercurial import policy
912
912
913 bdiff = policy.importmod('bdiff')
913 bdiff = policy.importmod('bdiff')
914 return bdiff.xdiffblocks(b'', b'') == [(0, 0, 0, 0)]
914 return bdiff.xdiffblocks(b'', b'') == [(0, 0, 0, 0)]
915 except (ImportError, AttributeError):
915 except (ImportError, AttributeError):
916 return False
916 return False
917
917
918
918
919 @check('extraextensions', 'whether tests are running with extra extensions')
919 @check('extraextensions', 'whether tests are running with extra extensions')
920 def has_extraextensions():
920 def has_extraextensions():
921 return 'HGTESTEXTRAEXTENSIONS' in os.environ
921 return 'HGTESTEXTRAEXTENSIONS' in os.environ
922
922
923
923
924 def getrepofeatures():
924 def getrepofeatures():
925 """Obtain set of repository features in use.
925 """Obtain set of repository features in use.
926
926
927 HGREPOFEATURES can be used to define or remove features. It contains
927 HGREPOFEATURES can be used to define or remove features. It contains
928 a space-delimited list of feature strings. Strings beginning with ``-``
928 a space-delimited list of feature strings. Strings beginning with ``-``
929 mean to remove.
929 mean to remove.
930 """
930 """
931 # Default list provided by core.
931 # Default list provided by core.
932 features = {
932 features = {
933 'bundlerepo',
933 'bundlerepo',
934 'revlogstore',
934 'revlogstore',
935 'fncache',
935 'fncache',
936 }
936 }
937
937
938 # Features that imply other features.
938 # Features that imply other features.
939 implies = {
939 implies = {
940 'simplestore': ['-revlogstore', '-bundlerepo', '-fncache'],
940 'simplestore': ['-revlogstore', '-bundlerepo', '-fncache'],
941 }
941 }
942
942
943 for override in os.environ.get('HGREPOFEATURES', '').split(' '):
943 for override in os.environ.get('HGREPOFEATURES', '').split(' '):
944 if not override:
944 if not override:
945 continue
945 continue
946
946
947 if override.startswith('-'):
947 if override.startswith('-'):
948 if override[1:] in features:
948 if override[1:] in features:
949 features.remove(override[1:])
949 features.remove(override[1:])
950 else:
950 else:
951 features.add(override)
951 features.add(override)
952
952
953 for imply in implies.get(override, []):
953 for imply in implies.get(override, []):
954 if imply.startswith('-'):
954 if imply.startswith('-'):
955 if imply[1:] in features:
955 if imply[1:] in features:
956 features.remove(imply[1:])
956 features.remove(imply[1:])
957 else:
957 else:
958 features.add(imply)
958 features.add(imply)
959
959
960 return features
960 return features
961
961
962
962
963 @check('reporevlogstore', 'repository using the default revlog store')
963 @check('reporevlogstore', 'repository using the default revlog store')
964 def has_reporevlogstore():
964 def has_reporevlogstore():
965 return 'revlogstore' in getrepofeatures()
965 return 'revlogstore' in getrepofeatures()
966
966
967
967
968 @check('reposimplestore', 'repository using simple storage extension')
968 @check('reposimplestore', 'repository using simple storage extension')
969 def has_reposimplestore():
969 def has_reposimplestore():
970 return 'simplestore' in getrepofeatures()
970 return 'simplestore' in getrepofeatures()
971
971
972
972
973 @check('repobundlerepo', 'whether we can open bundle files as repos')
973 @check('repobundlerepo', 'whether we can open bundle files as repos')
974 def has_repobundlerepo():
974 def has_repobundlerepo():
975 return 'bundlerepo' in getrepofeatures()
975 return 'bundlerepo' in getrepofeatures()
976
976
977
977
978 @check('repofncache', 'repository has an fncache')
978 @check('repofncache', 'repository has an fncache')
979 def has_repofncache():
979 def has_repofncache():
980 return 'fncache' in getrepofeatures()
980 return 'fncache' in getrepofeatures()
981
981
982
982
983 @check('sqlite', 'sqlite3 module is available')
983 @check('sqlite', 'sqlite3 module is available')
984 def has_sqlite():
984 def has_sqlite():
985 try:
985 try:
986 import sqlite3
986 import sqlite3
987
987
988 version = sqlite3.sqlite_version_info
988 version = sqlite3.sqlite_version_info
989 except ImportError:
989 except ImportError:
990 return False
990 return False
991
991
992 if version < (3, 8, 3):
992 if version < (3, 8, 3):
993 # WITH clause not supported
993 # WITH clause not supported
994 return False
994 return False
995
995
996 return matchoutput('sqlite3 -version', br'^3\.\d+')
996 return matchoutput('sqlite3 -version', br'^3\.\d+')
997
997
998
998
999 @check('vcr', 'vcr http mocking library')
999 @check('vcr', 'vcr http mocking library')
1000 def has_vcr():
1000 def has_vcr():
1001 try:
1001 try:
1002 import vcr
1002 import vcr
1003
1003
1004 vcr.VCR
1004 vcr.VCR
1005 return True
1005 return True
1006 except (ImportError, AttributeError):
1006 except (ImportError, AttributeError):
1007 pass
1007 pass
1008 return False
1008 return False
1009
1009
1010
1010
1011 @check('emacs', 'GNU Emacs')
1011 @check('emacs', 'GNU Emacs')
1012 def has_emacs():
1012 def has_emacs():
1013 # Our emacs lisp uses `with-eval-after-load` which is new in emacs
1013 # Our emacs lisp uses `with-eval-after-load` which is new in emacs
1014 # 24.4, so we allow emacs 24.4, 24.5, and 25+ (24.5 was the last
1014 # 24.4, so we allow emacs 24.4, 24.5, and 25+ (24.5 was the last
1015 # 24 release)
1015 # 24 release)
1016 return matchoutput('emacs --version', b'GNU Emacs 2(4.4|4.5|5|6|7|8|9)')
1016 return matchoutput('emacs --version', b'GNU Emacs 2(4.4|4.5|5|6|7|8|9)')
1017
1017
1018
1018
1019 @check('black', 'the black formatter for python')
1019 @check('black', 'the black formatter for python')
1020 def has_black():
1020 def has_black():
1021 blackcmd = 'black --version'
1021 blackcmd = 'black --version'
1022 version_regex = b'black, version ([0-9a-b.]+)'
1022 version_regex = b'black, version ([0-9a-b.]+)'
1023 version = matchoutput(blackcmd, version_regex)
1023 version = matchoutput(blackcmd, version_regex)
1024 sv = distutils.version.StrictVersion
1024 sv = distutils.version.StrictVersion
1025 return version and sv(_strpath(version.group(1))) >= sv('19.10b0')
1025 return version and sv(_strpath(version.group(1))) >= sv('19.10b0')
1026
1026
1027
1027
1028 @check('pytype', 'the pytype type checker')
1028 @check('pytype', 'the pytype type checker')
1029 def has_pytype():
1029 def has_pytype():
1030 pytypecmd = 'pytype --version'
1030 pytypecmd = 'pytype --version'
1031 version = matchoutput(pytypecmd, b'[0-9a-b.]+')
1031 version = matchoutput(pytypecmd, b'[0-9a-b.]+')
1032 sv = distutils.version.StrictVersion
1032 sv = distutils.version.StrictVersion
1033 return version and sv(_strpath(version.group(0))) >= sv('2019.10.17')
1033 return version and sv(_strpath(version.group(0))) >= sv('2019.10.17')
1034
1034
1035
1035
1036 @check("rustfmt", "rustfmt tool")
1036 @check("rustfmt", "rustfmt tool")
1037 def has_rustfmt():
1037 def has_rustfmt():
1038 # We use Nightly's rustfmt due to current unstable config options.
1038 # We use Nightly's rustfmt due to current unstable config options.
1039 return matchoutput(
1039 return matchoutput(
1040 '`rustup which --toolchain nightly rustfmt` --version', b'rustfmt'
1040 '`rustup which --toolchain nightly rustfmt` --version', b'rustfmt'
1041 )
1041 )
General Comments 0
You need to be logged in to leave comments. Login now