##// END OF EJS Templates
py3: replace os.environ with encoding.environ (part 5 of 5)
Pulkit Goyal -
r30638:1c5cbf28 default
parent child Browse files
Show More
@@ -1,717 +1,719 b''
1 # color.py color output for Mercurial commands
1 # color.py color output for Mercurial commands
2 #
2 #
3 # Copyright (C) 2007 Kevin Christen <kevin.christen@gmail.com>
3 # Copyright (C) 2007 Kevin Christen <kevin.christen@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 '''colorize output from some commands
8 '''colorize output from some commands
9
9
10 The color extension colorizes output from several Mercurial commands.
10 The color extension colorizes output from several Mercurial commands.
11 For example, the diff command shows additions in green and deletions
11 For example, the diff command shows additions in green and deletions
12 in red, while the status command shows modified files in magenta. Many
12 in red, while the status command shows modified files in magenta. Many
13 other commands have analogous colors. It is possible to customize
13 other commands have analogous colors. It is possible to customize
14 these colors.
14 these colors.
15
15
16 Effects
16 Effects
17 -------
17 -------
18
18
19 Other effects in addition to color, like bold and underlined text, are
19 Other effects in addition to color, like bold and underlined text, are
20 also available. By default, the terminfo database is used to find the
20 also available. By default, the terminfo database is used to find the
21 terminal codes used to change color and effect. If terminfo is not
21 terminal codes used to change color and effect. If terminfo is not
22 available, then effects are rendered with the ECMA-48 SGR control
22 available, then effects are rendered with the ECMA-48 SGR control
23 function (aka ANSI escape codes).
23 function (aka ANSI escape codes).
24
24
25 The available effects in terminfo mode are 'blink', 'bold', 'dim',
25 The available effects in terminfo mode are 'blink', 'bold', 'dim',
26 'inverse', 'invisible', 'italic', 'standout', and 'underline'; in
26 'inverse', 'invisible', 'italic', 'standout', and 'underline'; in
27 ECMA-48 mode, the options are 'bold', 'inverse', 'italic', and
27 ECMA-48 mode, the options are 'bold', 'inverse', 'italic', and
28 'underline'. How each is rendered depends on the terminal emulator.
28 'underline'. How each is rendered depends on the terminal emulator.
29 Some may not be available for a given terminal type, and will be
29 Some may not be available for a given terminal type, and will be
30 silently ignored.
30 silently ignored.
31
31
32 If the terminfo entry for your terminal is missing codes for an effect
32 If the terminfo entry for your terminal is missing codes for an effect
33 or has the wrong codes, you can add or override those codes in your
33 or has the wrong codes, you can add or override those codes in your
34 configuration::
34 configuration::
35
35
36 [color]
36 [color]
37 terminfo.dim = \E[2m
37 terminfo.dim = \E[2m
38
38
39 where '\E' is substituted with an escape character.
39 where '\E' is substituted with an escape character.
40
40
41 Labels
41 Labels
42 ------
42 ------
43
43
44 Text receives color effects depending on the labels that it has. Many
44 Text receives color effects depending on the labels that it has. Many
45 default Mercurial commands emit labelled text. You can also define
45 default Mercurial commands emit labelled text. You can also define
46 your own labels in templates using the label function, see :hg:`help
46 your own labels in templates using the label function, see :hg:`help
47 templates`. A single portion of text may have more than one label. In
47 templates`. A single portion of text may have more than one label. In
48 that case, effects given to the last label will override any other
48 that case, effects given to the last label will override any other
49 effects. This includes the special "none" effect, which nullifies
49 effects. This includes the special "none" effect, which nullifies
50 other effects.
50 other effects.
51
51
52 Labels are normally invisible. In order to see these labels and their
52 Labels are normally invisible. In order to see these labels and their
53 position in the text, use the global --color=debug option. The same
53 position in the text, use the global --color=debug option. The same
54 anchor text may be associated to multiple labels, e.g.
54 anchor text may be associated to multiple labels, e.g.
55
55
56 [log.changeset changeset.secret|changeset: 22611:6f0a53c8f587]
56 [log.changeset changeset.secret|changeset: 22611:6f0a53c8f587]
57
57
58 The following are the default effects for some default labels. Default
58 The following are the default effects for some default labels. Default
59 effects may be overridden from your configuration file::
59 effects may be overridden from your configuration file::
60
60
61 [color]
61 [color]
62 status.modified = blue bold underline red_background
62 status.modified = blue bold underline red_background
63 status.added = green bold
63 status.added = green bold
64 status.removed = red bold blue_background
64 status.removed = red bold blue_background
65 status.deleted = cyan bold underline
65 status.deleted = cyan bold underline
66 status.unknown = magenta bold underline
66 status.unknown = magenta bold underline
67 status.ignored = black bold
67 status.ignored = black bold
68
68
69 # 'none' turns off all effects
69 # 'none' turns off all effects
70 status.clean = none
70 status.clean = none
71 status.copied = none
71 status.copied = none
72
72
73 qseries.applied = blue bold underline
73 qseries.applied = blue bold underline
74 qseries.unapplied = black bold
74 qseries.unapplied = black bold
75 qseries.missing = red bold
75 qseries.missing = red bold
76
76
77 diff.diffline = bold
77 diff.diffline = bold
78 diff.extended = cyan bold
78 diff.extended = cyan bold
79 diff.file_a = red bold
79 diff.file_a = red bold
80 diff.file_b = green bold
80 diff.file_b = green bold
81 diff.hunk = magenta
81 diff.hunk = magenta
82 diff.deleted = red
82 diff.deleted = red
83 diff.inserted = green
83 diff.inserted = green
84 diff.changed = white
84 diff.changed = white
85 diff.tab =
85 diff.tab =
86 diff.trailingwhitespace = bold red_background
86 diff.trailingwhitespace = bold red_background
87
87
88 # Blank so it inherits the style of the surrounding label
88 # Blank so it inherits the style of the surrounding label
89 changeset.public =
89 changeset.public =
90 changeset.draft =
90 changeset.draft =
91 changeset.secret =
91 changeset.secret =
92
92
93 resolve.unresolved = red bold
93 resolve.unresolved = red bold
94 resolve.resolved = green bold
94 resolve.resolved = green bold
95
95
96 bookmarks.active = green
96 bookmarks.active = green
97
97
98 branches.active = none
98 branches.active = none
99 branches.closed = black bold
99 branches.closed = black bold
100 branches.current = green
100 branches.current = green
101 branches.inactive = none
101 branches.inactive = none
102
102
103 tags.normal = green
103 tags.normal = green
104 tags.local = black bold
104 tags.local = black bold
105
105
106 rebase.rebased = blue
106 rebase.rebased = blue
107 rebase.remaining = red bold
107 rebase.remaining = red bold
108
108
109 shelve.age = cyan
109 shelve.age = cyan
110 shelve.newest = green bold
110 shelve.newest = green bold
111 shelve.name = blue bold
111 shelve.name = blue bold
112
112
113 histedit.remaining = red bold
113 histedit.remaining = red bold
114
114
115 Custom colors
115 Custom colors
116 -------------
116 -------------
117
117
118 Because there are only eight standard colors, this module allows you
118 Because there are only eight standard colors, this module allows you
119 to define color names for other color slots which might be available
119 to define color names for other color slots which might be available
120 for your terminal type, assuming terminfo mode. For instance::
120 for your terminal type, assuming terminfo mode. For instance::
121
121
122 color.brightblue = 12
122 color.brightblue = 12
123 color.pink = 207
123 color.pink = 207
124 color.orange = 202
124 color.orange = 202
125
125
126 to set 'brightblue' to color slot 12 (useful for 16 color terminals
126 to set 'brightblue' to color slot 12 (useful for 16 color terminals
127 that have brighter colors defined in the upper eight) and, 'pink' and
127 that have brighter colors defined in the upper eight) and, 'pink' and
128 'orange' to colors in 256-color xterm's default color cube. These
128 'orange' to colors in 256-color xterm's default color cube. These
129 defined colors may then be used as any of the pre-defined eight,
129 defined colors may then be used as any of the pre-defined eight,
130 including appending '_background' to set the background to that color.
130 including appending '_background' to set the background to that color.
131
131
132 Modes
132 Modes
133 -----
133 -----
134
134
135 By default, the color extension will use ANSI mode (or win32 mode on
135 By default, the color extension will use ANSI mode (or win32 mode on
136 Windows) if it detects a terminal. To override auto mode (to enable
136 Windows) if it detects a terminal. To override auto mode (to enable
137 terminfo mode, for example), set the following configuration option::
137 terminfo mode, for example), set the following configuration option::
138
138
139 [color]
139 [color]
140 mode = terminfo
140 mode = terminfo
141
141
142 Any value other than 'ansi', 'win32', 'terminfo', or 'auto' will
142 Any value other than 'ansi', 'win32', 'terminfo', or 'auto' will
143 disable color.
143 disable color.
144
144
145 Note that on some systems, terminfo mode may cause problems when using
145 Note that on some systems, terminfo mode may cause problems when using
146 color with the pager extension and less -R. less with the -R option
146 color with the pager extension and less -R. less with the -R option
147 will only display ECMA-48 color codes, and terminfo mode may sometimes
147 will only display ECMA-48 color codes, and terminfo mode may sometimes
148 emit codes that less doesn't understand. You can work around this by
148 emit codes that less doesn't understand. You can work around this by
149 either using ansi mode (or auto mode), or by using less -r (which will
149 either using ansi mode (or auto mode), or by using less -r (which will
150 pass through all terminal control codes, not just color control
150 pass through all terminal control codes, not just color control
151 codes).
151 codes).
152
152
153 On some systems (such as MSYS in Windows), the terminal may support
153 On some systems (such as MSYS in Windows), the terminal may support
154 a different color mode than the pager (activated via the "pager"
154 a different color mode than the pager (activated via the "pager"
155 extension). It is possible to define separate modes depending on whether
155 extension). It is possible to define separate modes depending on whether
156 the pager is active::
156 the pager is active::
157
157
158 [color]
158 [color]
159 mode = auto
159 mode = auto
160 pagermode = ansi
160 pagermode = ansi
161
161
162 If ``pagermode`` is not defined, the ``mode`` will be used.
162 If ``pagermode`` is not defined, the ``mode`` will be used.
163 '''
163 '''
164
164
165 from __future__ import absolute_import
165 from __future__ import absolute_import
166
166
167 import os
167 import os
168
168
169 from mercurial.i18n import _
169 from mercurial.i18n import _
170 from mercurial import (
170 from mercurial import (
171 cmdutil,
171 cmdutil,
172 commands,
172 commands,
173 dispatch,
173 dispatch,
174 encoding,
174 extensions,
175 extensions,
175 subrepo,
176 subrepo,
176 ui as uimod,
177 ui as uimod,
177 util,
178 util,
178 )
179 )
179
180
180 cmdtable = {}
181 cmdtable = {}
181 command = cmdutil.command(cmdtable)
182 command = cmdutil.command(cmdtable)
182 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
183 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
183 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
184 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
184 # be specifying the version(s) of Mercurial they are tested with, or
185 # be specifying the version(s) of Mercurial they are tested with, or
185 # leave the attribute unspecified.
186 # leave the attribute unspecified.
186 testedwith = 'ships-with-hg-core'
187 testedwith = 'ships-with-hg-core'
187
188
188 # start and stop parameters for effects
189 # start and stop parameters for effects
189 _effects = {'none': 0, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33,
190 _effects = {'none': 0, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33,
190 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37, 'bold': 1,
191 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37, 'bold': 1,
191 'italic': 3, 'underline': 4, 'inverse': 7, 'dim': 2,
192 'italic': 3, 'underline': 4, 'inverse': 7, 'dim': 2,
192 'black_background': 40, 'red_background': 41,
193 'black_background': 40, 'red_background': 41,
193 'green_background': 42, 'yellow_background': 43,
194 'green_background': 42, 'yellow_background': 43,
194 'blue_background': 44, 'purple_background': 45,
195 'blue_background': 44, 'purple_background': 45,
195 'cyan_background': 46, 'white_background': 47}
196 'cyan_background': 46, 'white_background': 47}
196
197
197 def _terminfosetup(ui, mode):
198 def _terminfosetup(ui, mode):
198 '''Initialize terminfo data and the terminal if we're in terminfo mode.'''
199 '''Initialize terminfo data and the terminal if we're in terminfo mode.'''
199
200
200 global _terminfo_params
201 global _terminfo_params
201 # If we failed to load curses, we go ahead and return.
202 # If we failed to load curses, we go ahead and return.
202 if not _terminfo_params:
203 if not _terminfo_params:
203 return
204 return
204 # Otherwise, see what the config file says.
205 # Otherwise, see what the config file says.
205 if mode not in ('auto', 'terminfo'):
206 if mode not in ('auto', 'terminfo'):
206 return
207 return
207
208
208 _terminfo_params.update((key[6:], (False, int(val), ''))
209 _terminfo_params.update((key[6:], (False, int(val), ''))
209 for key, val in ui.configitems('color')
210 for key, val in ui.configitems('color')
210 if key.startswith('color.'))
211 if key.startswith('color.'))
211 _terminfo_params.update((key[9:], (True, '', val.replace('\\E', '\x1b')))
212 _terminfo_params.update((key[9:], (True, '', val.replace('\\E', '\x1b')))
212 for key, val in ui.configitems('color')
213 for key, val in ui.configitems('color')
213 if key.startswith('terminfo.'))
214 if key.startswith('terminfo.'))
214
215
215 try:
216 try:
216 curses.setupterm()
217 curses.setupterm()
217 except curses.error as e:
218 except curses.error as e:
218 _terminfo_params = {}
219 _terminfo_params = {}
219 return
220 return
220
221
221 for key, (b, e, c) in _terminfo_params.items():
222 for key, (b, e, c) in _terminfo_params.items():
222 if not b:
223 if not b:
223 continue
224 continue
224 if not c and not curses.tigetstr(e):
225 if not c and not curses.tigetstr(e):
225 # Most terminals don't support dim, invis, etc, so don't be
226 # Most terminals don't support dim, invis, etc, so don't be
226 # noisy and use ui.debug().
227 # noisy and use ui.debug().
227 ui.debug("no terminfo entry for %s\n" % e)
228 ui.debug("no terminfo entry for %s\n" % e)
228 del _terminfo_params[key]
229 del _terminfo_params[key]
229 if not curses.tigetstr('setaf') or not curses.tigetstr('setab'):
230 if not curses.tigetstr('setaf') or not curses.tigetstr('setab'):
230 # Only warn about missing terminfo entries if we explicitly asked for
231 # Only warn about missing terminfo entries if we explicitly asked for
231 # terminfo mode.
232 # terminfo mode.
232 if mode == "terminfo":
233 if mode == "terminfo":
233 ui.warn(_("no terminfo entry for setab/setaf: reverting to "
234 ui.warn(_("no terminfo entry for setab/setaf: reverting to "
234 "ECMA-48 color\n"))
235 "ECMA-48 color\n"))
235 _terminfo_params = {}
236 _terminfo_params = {}
236
237
237 def _modesetup(ui, coloropt):
238 def _modesetup(ui, coloropt):
238 global _terminfo_params
239 global _terminfo_params
239
240
240 if coloropt == 'debug':
241 if coloropt == 'debug':
241 return 'debug'
242 return 'debug'
242
243
243 auto = (coloropt == 'auto')
244 auto = (coloropt == 'auto')
244 always = not auto and util.parsebool(coloropt)
245 always = not auto and util.parsebool(coloropt)
245 if not always and not auto:
246 if not always and not auto:
246 return None
247 return None
247
248
248 formatted = always or (os.environ.get('TERM') != 'dumb' and ui.formatted())
249 formatted = (always or (encoding.environ.get('TERM') != 'dumb'
250 and ui.formatted()))
249
251
250 mode = ui.config('color', 'mode', 'auto')
252 mode = ui.config('color', 'mode', 'auto')
251
253
252 # If pager is active, color.pagermode overrides color.mode.
254 # If pager is active, color.pagermode overrides color.mode.
253 if getattr(ui, 'pageractive', False):
255 if getattr(ui, 'pageractive', False):
254 mode = ui.config('color', 'pagermode', mode)
256 mode = ui.config('color', 'pagermode', mode)
255
257
256 realmode = mode
258 realmode = mode
257 if mode == 'auto':
259 if mode == 'auto':
258 if os.name == 'nt':
260 if os.name == 'nt':
259 term = os.environ.get('TERM')
261 term = encoding.environ.get('TERM')
260 # TERM won't be defined in a vanilla cmd.exe environment.
262 # TERM won't be defined in a vanilla cmd.exe environment.
261
263
262 # UNIX-like environments on Windows such as Cygwin and MSYS will
264 # UNIX-like environments on Windows such as Cygwin and MSYS will
263 # set TERM. They appear to make a best effort attempt at setting it
265 # set TERM. They appear to make a best effort attempt at setting it
264 # to something appropriate. However, not all environments with TERM
266 # to something appropriate. However, not all environments with TERM
265 # defined support ANSI. Since "ansi" could result in terminal
267 # defined support ANSI. Since "ansi" could result in terminal
266 # gibberish, we error on the side of selecting "win32". However, if
268 # gibberish, we error on the side of selecting "win32". However, if
267 # w32effects is not defined, we almost certainly don't support
269 # w32effects is not defined, we almost certainly don't support
268 # "win32", so don't even try.
270 # "win32", so don't even try.
269 if (term and 'xterm' in term) or not w32effects:
271 if (term and 'xterm' in term) or not w32effects:
270 realmode = 'ansi'
272 realmode = 'ansi'
271 else:
273 else:
272 realmode = 'win32'
274 realmode = 'win32'
273 else:
275 else:
274 realmode = 'ansi'
276 realmode = 'ansi'
275
277
276 def modewarn():
278 def modewarn():
277 # only warn if color.mode was explicitly set and we're in
279 # only warn if color.mode was explicitly set and we're in
278 # a formatted terminal
280 # a formatted terminal
279 if mode == realmode and ui.formatted():
281 if mode == realmode and ui.formatted():
280 ui.warn(_('warning: failed to set color mode to %s\n') % mode)
282 ui.warn(_('warning: failed to set color mode to %s\n') % mode)
281
283
282 if realmode == 'win32':
284 if realmode == 'win32':
283 _terminfo_params = {}
285 _terminfo_params = {}
284 if not w32effects:
286 if not w32effects:
285 modewarn()
287 modewarn()
286 return None
288 return None
287 _effects.update(w32effects)
289 _effects.update(w32effects)
288 elif realmode == 'ansi':
290 elif realmode == 'ansi':
289 _terminfo_params = {}
291 _terminfo_params = {}
290 elif realmode == 'terminfo':
292 elif realmode == 'terminfo':
291 _terminfosetup(ui, mode)
293 _terminfosetup(ui, mode)
292 if not _terminfo_params:
294 if not _terminfo_params:
293 ## FIXME Shouldn't we return None in this case too?
295 ## FIXME Shouldn't we return None in this case too?
294 modewarn()
296 modewarn()
295 realmode = 'ansi'
297 realmode = 'ansi'
296 else:
298 else:
297 return None
299 return None
298
300
299 if always or (auto and formatted):
301 if always or (auto and formatted):
300 return realmode
302 return realmode
301 return None
303 return None
302
304
303 try:
305 try:
304 import curses
306 import curses
305 # Mapping from effect name to terminfo attribute name (or raw code) or
307 # Mapping from effect name to terminfo attribute name (or raw code) or
306 # color number. This will also force-load the curses module.
308 # color number. This will also force-load the curses module.
307 _terminfo_params = {'none': (True, 'sgr0', ''),
309 _terminfo_params = {'none': (True, 'sgr0', ''),
308 'standout': (True, 'smso', ''),
310 'standout': (True, 'smso', ''),
309 'underline': (True, 'smul', ''),
311 'underline': (True, 'smul', ''),
310 'reverse': (True, 'rev', ''),
312 'reverse': (True, 'rev', ''),
311 'inverse': (True, 'rev', ''),
313 'inverse': (True, 'rev', ''),
312 'blink': (True, 'blink', ''),
314 'blink': (True, 'blink', ''),
313 'dim': (True, 'dim', ''),
315 'dim': (True, 'dim', ''),
314 'bold': (True, 'bold', ''),
316 'bold': (True, 'bold', ''),
315 'invisible': (True, 'invis', ''),
317 'invisible': (True, 'invis', ''),
316 'italic': (True, 'sitm', ''),
318 'italic': (True, 'sitm', ''),
317 'black': (False, curses.COLOR_BLACK, ''),
319 'black': (False, curses.COLOR_BLACK, ''),
318 'red': (False, curses.COLOR_RED, ''),
320 'red': (False, curses.COLOR_RED, ''),
319 'green': (False, curses.COLOR_GREEN, ''),
321 'green': (False, curses.COLOR_GREEN, ''),
320 'yellow': (False, curses.COLOR_YELLOW, ''),
322 'yellow': (False, curses.COLOR_YELLOW, ''),
321 'blue': (False, curses.COLOR_BLUE, ''),
323 'blue': (False, curses.COLOR_BLUE, ''),
322 'magenta': (False, curses.COLOR_MAGENTA, ''),
324 'magenta': (False, curses.COLOR_MAGENTA, ''),
323 'cyan': (False, curses.COLOR_CYAN, ''),
325 'cyan': (False, curses.COLOR_CYAN, ''),
324 'white': (False, curses.COLOR_WHITE, '')}
326 'white': (False, curses.COLOR_WHITE, '')}
325 except ImportError:
327 except ImportError:
326 _terminfo_params = {}
328 _terminfo_params = {}
327
329
328 _styles = {'grep.match': 'red bold',
330 _styles = {'grep.match': 'red bold',
329 'grep.linenumber': 'green',
331 'grep.linenumber': 'green',
330 'grep.rev': 'green',
332 'grep.rev': 'green',
331 'grep.change': 'green',
333 'grep.change': 'green',
332 'grep.sep': 'cyan',
334 'grep.sep': 'cyan',
333 'grep.filename': 'magenta',
335 'grep.filename': 'magenta',
334 'grep.user': 'magenta',
336 'grep.user': 'magenta',
335 'grep.date': 'magenta',
337 'grep.date': 'magenta',
336 'bookmarks.active': 'green',
338 'bookmarks.active': 'green',
337 'branches.active': 'none',
339 'branches.active': 'none',
338 'branches.closed': 'black bold',
340 'branches.closed': 'black bold',
339 'branches.current': 'green',
341 'branches.current': 'green',
340 'branches.inactive': 'none',
342 'branches.inactive': 'none',
341 'diff.changed': 'white',
343 'diff.changed': 'white',
342 'diff.deleted': 'red',
344 'diff.deleted': 'red',
343 'diff.diffline': 'bold',
345 'diff.diffline': 'bold',
344 'diff.extended': 'cyan bold',
346 'diff.extended': 'cyan bold',
345 'diff.file_a': 'red bold',
347 'diff.file_a': 'red bold',
346 'diff.file_b': 'green bold',
348 'diff.file_b': 'green bold',
347 'diff.hunk': 'magenta',
349 'diff.hunk': 'magenta',
348 'diff.inserted': 'green',
350 'diff.inserted': 'green',
349 'diff.tab': '',
351 'diff.tab': '',
350 'diff.trailingwhitespace': 'bold red_background',
352 'diff.trailingwhitespace': 'bold red_background',
351 'changeset.public' : '',
353 'changeset.public' : '',
352 'changeset.draft' : '',
354 'changeset.draft' : '',
353 'changeset.secret' : '',
355 'changeset.secret' : '',
354 'diffstat.deleted': 'red',
356 'diffstat.deleted': 'red',
355 'diffstat.inserted': 'green',
357 'diffstat.inserted': 'green',
356 'histedit.remaining': 'red bold',
358 'histedit.remaining': 'red bold',
357 'ui.prompt': 'yellow',
359 'ui.prompt': 'yellow',
358 'log.changeset': 'yellow',
360 'log.changeset': 'yellow',
359 'patchbomb.finalsummary': '',
361 'patchbomb.finalsummary': '',
360 'patchbomb.from': 'magenta',
362 'patchbomb.from': 'magenta',
361 'patchbomb.to': 'cyan',
363 'patchbomb.to': 'cyan',
362 'patchbomb.subject': 'green',
364 'patchbomb.subject': 'green',
363 'patchbomb.diffstats': '',
365 'patchbomb.diffstats': '',
364 'rebase.rebased': 'blue',
366 'rebase.rebased': 'blue',
365 'rebase.remaining': 'red bold',
367 'rebase.remaining': 'red bold',
366 'resolve.resolved': 'green bold',
368 'resolve.resolved': 'green bold',
367 'resolve.unresolved': 'red bold',
369 'resolve.unresolved': 'red bold',
368 'shelve.age': 'cyan',
370 'shelve.age': 'cyan',
369 'shelve.newest': 'green bold',
371 'shelve.newest': 'green bold',
370 'shelve.name': 'blue bold',
372 'shelve.name': 'blue bold',
371 'status.added': 'green bold',
373 'status.added': 'green bold',
372 'status.clean': 'none',
374 'status.clean': 'none',
373 'status.copied': 'none',
375 'status.copied': 'none',
374 'status.deleted': 'cyan bold underline',
376 'status.deleted': 'cyan bold underline',
375 'status.ignored': 'black bold',
377 'status.ignored': 'black bold',
376 'status.modified': 'blue bold',
378 'status.modified': 'blue bold',
377 'status.removed': 'red bold',
379 'status.removed': 'red bold',
378 'status.unknown': 'magenta bold underline',
380 'status.unknown': 'magenta bold underline',
379 'tags.normal': 'green',
381 'tags.normal': 'green',
380 'tags.local': 'black bold'}
382 'tags.local': 'black bold'}
381
383
382
384
383 def _effect_str(effect):
385 def _effect_str(effect):
384 '''Helper function for render_effects().'''
386 '''Helper function for render_effects().'''
385
387
386 bg = False
388 bg = False
387 if effect.endswith('_background'):
389 if effect.endswith('_background'):
388 bg = True
390 bg = True
389 effect = effect[:-11]
391 effect = effect[:-11]
390 try:
392 try:
391 attr, val, termcode = _terminfo_params[effect]
393 attr, val, termcode = _terminfo_params[effect]
392 except KeyError:
394 except KeyError:
393 return ''
395 return ''
394 if attr:
396 if attr:
395 if termcode:
397 if termcode:
396 return termcode
398 return termcode
397 else:
399 else:
398 return curses.tigetstr(val)
400 return curses.tigetstr(val)
399 elif bg:
401 elif bg:
400 return curses.tparm(curses.tigetstr('setab'), val)
402 return curses.tparm(curses.tigetstr('setab'), val)
401 else:
403 else:
402 return curses.tparm(curses.tigetstr('setaf'), val)
404 return curses.tparm(curses.tigetstr('setaf'), val)
403
405
404 def render_effects(text, effects):
406 def render_effects(text, effects):
405 'Wrap text in commands to turn on each effect.'
407 'Wrap text in commands to turn on each effect.'
406 if not text:
408 if not text:
407 return text
409 return text
408 if not _terminfo_params:
410 if not _terminfo_params:
409 start = [str(_effects[e]) for e in ['none'] + effects.split()]
411 start = [str(_effects[e]) for e in ['none'] + effects.split()]
410 start = '\033[' + ';'.join(start) + 'm'
412 start = '\033[' + ';'.join(start) + 'm'
411 stop = '\033[' + str(_effects['none']) + 'm'
413 stop = '\033[' + str(_effects['none']) + 'm'
412 else:
414 else:
413 start = ''.join(_effect_str(effect)
415 start = ''.join(_effect_str(effect)
414 for effect in ['none'] + effects.split())
416 for effect in ['none'] + effects.split())
415 stop = _effect_str('none')
417 stop = _effect_str('none')
416 return ''.join([start, text, stop])
418 return ''.join([start, text, stop])
417
419
418 def extstyles():
420 def extstyles():
419 for name, ext in extensions.extensions():
421 for name, ext in extensions.extensions():
420 _styles.update(getattr(ext, 'colortable', {}))
422 _styles.update(getattr(ext, 'colortable', {}))
421
423
422 def valideffect(effect):
424 def valideffect(effect):
423 'Determine if the effect is valid or not.'
425 'Determine if the effect is valid or not.'
424 good = False
426 good = False
425 if not _terminfo_params and effect in _effects:
427 if not _terminfo_params and effect in _effects:
426 good = True
428 good = True
427 elif effect in _terminfo_params or effect[:-11] in _terminfo_params:
429 elif effect in _terminfo_params or effect[:-11] in _terminfo_params:
428 good = True
430 good = True
429 return good
431 return good
430
432
431 def configstyles(ui):
433 def configstyles(ui):
432 for status, cfgeffects in ui.configitems('color'):
434 for status, cfgeffects in ui.configitems('color'):
433 if '.' not in status or status.startswith(('color.', 'terminfo.')):
435 if '.' not in status or status.startswith(('color.', 'terminfo.')):
434 continue
436 continue
435 cfgeffects = ui.configlist('color', status)
437 cfgeffects = ui.configlist('color', status)
436 if cfgeffects:
438 if cfgeffects:
437 good = []
439 good = []
438 for e in cfgeffects:
440 for e in cfgeffects:
439 if valideffect(e):
441 if valideffect(e):
440 good.append(e)
442 good.append(e)
441 else:
443 else:
442 ui.warn(_("ignoring unknown color/effect %r "
444 ui.warn(_("ignoring unknown color/effect %r "
443 "(configured in color.%s)\n")
445 "(configured in color.%s)\n")
444 % (e, status))
446 % (e, status))
445 _styles[status] = ' '.join(good)
447 _styles[status] = ' '.join(good)
446
448
447 class colorui(uimod.ui):
449 class colorui(uimod.ui):
448 _colormode = 'ansi'
450 _colormode = 'ansi'
449 def write(self, *args, **opts):
451 def write(self, *args, **opts):
450 if self._colormode is None:
452 if self._colormode is None:
451 return super(colorui, self).write(*args, **opts)
453 return super(colorui, self).write(*args, **opts)
452
454
453 label = opts.get('label', '')
455 label = opts.get('label', '')
454 if self._buffers and not opts.get('prompt', False):
456 if self._buffers and not opts.get('prompt', False):
455 if self._bufferapplylabels:
457 if self._bufferapplylabels:
456 self._buffers[-1].extend(self.label(a, label) for a in args)
458 self._buffers[-1].extend(self.label(a, label) for a in args)
457 else:
459 else:
458 self._buffers[-1].extend(args)
460 self._buffers[-1].extend(args)
459 elif self._colormode == 'win32':
461 elif self._colormode == 'win32':
460 for a in args:
462 for a in args:
461 win32print(a, super(colorui, self).write, **opts)
463 win32print(a, super(colorui, self).write, **opts)
462 else:
464 else:
463 return super(colorui, self).write(
465 return super(colorui, self).write(
464 *[self.label(a, label) for a in args], **opts)
466 *[self.label(a, label) for a in args], **opts)
465
467
466 def write_err(self, *args, **opts):
468 def write_err(self, *args, **opts):
467 if self._colormode is None:
469 if self._colormode is None:
468 return super(colorui, self).write_err(*args, **opts)
470 return super(colorui, self).write_err(*args, **opts)
469
471
470 label = opts.get('label', '')
472 label = opts.get('label', '')
471 if self._bufferstates and self._bufferstates[-1][0]:
473 if self._bufferstates and self._bufferstates[-1][0]:
472 return self.write(*args, **opts)
474 return self.write(*args, **opts)
473 if self._colormode == 'win32':
475 if self._colormode == 'win32':
474 for a in args:
476 for a in args:
475 win32print(a, super(colorui, self).write_err, **opts)
477 win32print(a, super(colorui, self).write_err, **opts)
476 else:
478 else:
477 return super(colorui, self).write_err(
479 return super(colorui, self).write_err(
478 *[self.label(a, label) for a in args], **opts)
480 *[self.label(a, label) for a in args], **opts)
479
481
480 def showlabel(self, msg, label):
482 def showlabel(self, msg, label):
481 if label and msg:
483 if label and msg:
482 if msg[-1] == '\n':
484 if msg[-1] == '\n':
483 return "[%s|%s]\n" % (label, msg[:-1])
485 return "[%s|%s]\n" % (label, msg[:-1])
484 else:
486 else:
485 return "[%s|%s]" % (label, msg)
487 return "[%s|%s]" % (label, msg)
486 else:
488 else:
487 return msg
489 return msg
488
490
489 def label(self, msg, label):
491 def label(self, msg, label):
490 if self._colormode is None:
492 if self._colormode is None:
491 return super(colorui, self).label(msg, label)
493 return super(colorui, self).label(msg, label)
492
494
493 if self._colormode == 'debug':
495 if self._colormode == 'debug':
494 return self.showlabel(msg, label)
496 return self.showlabel(msg, label)
495
497
496 effects = []
498 effects = []
497 for l in label.split():
499 for l in label.split():
498 s = _styles.get(l, '')
500 s = _styles.get(l, '')
499 if s:
501 if s:
500 effects.append(s)
502 effects.append(s)
501 elif valideffect(l):
503 elif valideffect(l):
502 effects.append(l)
504 effects.append(l)
503 effects = ' '.join(effects)
505 effects = ' '.join(effects)
504 if effects:
506 if effects:
505 return '\n'.join([render_effects(line, effects)
507 return '\n'.join([render_effects(line, effects)
506 for line in msg.split('\n')])
508 for line in msg.split('\n')])
507 return msg
509 return msg
508
510
509 def uisetup(ui):
511 def uisetup(ui):
510 if ui.plain():
512 if ui.plain():
511 return
513 return
512 if not isinstance(ui, colorui):
514 if not isinstance(ui, colorui):
513 colorui.__bases__ = (ui.__class__,)
515 colorui.__bases__ = (ui.__class__,)
514 ui.__class__ = colorui
516 ui.__class__ = colorui
515 def colorcmd(orig, ui_, opts, cmd, cmdfunc):
517 def colorcmd(orig, ui_, opts, cmd, cmdfunc):
516 mode = _modesetup(ui_, opts['color'])
518 mode = _modesetup(ui_, opts['color'])
517 colorui._colormode = mode
519 colorui._colormode = mode
518 if mode and mode != 'debug':
520 if mode and mode != 'debug':
519 extstyles()
521 extstyles()
520 configstyles(ui_)
522 configstyles(ui_)
521 return orig(ui_, opts, cmd, cmdfunc)
523 return orig(ui_, opts, cmd, cmdfunc)
522 def colorgit(orig, gitsub, commands, env=None, stream=False, cwd=None):
524 def colorgit(orig, gitsub, commands, env=None, stream=False, cwd=None):
523 if gitsub.ui._colormode and len(commands) and commands[0] == "diff":
525 if gitsub.ui._colormode and len(commands) and commands[0] == "diff":
524 # insert the argument in the front,
526 # insert the argument in the front,
525 # the end of git diff arguments is used for paths
527 # the end of git diff arguments is used for paths
526 commands.insert(1, '--color')
528 commands.insert(1, '--color')
527 return orig(gitsub, commands, env, stream, cwd)
529 return orig(gitsub, commands, env, stream, cwd)
528 extensions.wrapfunction(dispatch, '_runcommand', colorcmd)
530 extensions.wrapfunction(dispatch, '_runcommand', colorcmd)
529 extensions.wrapfunction(subrepo.gitsubrepo, '_gitnodir', colorgit)
531 extensions.wrapfunction(subrepo.gitsubrepo, '_gitnodir', colorgit)
530
532
531 def extsetup(ui):
533 def extsetup(ui):
532 commands.globalopts.append(
534 commands.globalopts.append(
533 ('', 'color', 'auto',
535 ('', 'color', 'auto',
534 # i18n: 'always', 'auto', 'never', and 'debug' are keywords
536 # i18n: 'always', 'auto', 'never', and 'debug' are keywords
535 # and should not be translated
537 # and should not be translated
536 _("when to colorize (boolean, always, auto, never, or debug)"),
538 _("when to colorize (boolean, always, auto, never, or debug)"),
537 _('TYPE')))
539 _('TYPE')))
538
540
539 @command('debugcolor',
541 @command('debugcolor',
540 [('', 'style', None, _('show all configured styles'))],
542 [('', 'style', None, _('show all configured styles'))],
541 'hg debugcolor')
543 'hg debugcolor')
542 def debugcolor(ui, repo, **opts):
544 def debugcolor(ui, repo, **opts):
543 """show available color, effects or style"""
545 """show available color, effects or style"""
544 ui.write(('color mode: %s\n') % ui._colormode)
546 ui.write(('color mode: %s\n') % ui._colormode)
545 if opts.get('style'):
547 if opts.get('style'):
546 return _debugdisplaystyle(ui)
548 return _debugdisplaystyle(ui)
547 else:
549 else:
548 return _debugdisplaycolor(ui)
550 return _debugdisplaycolor(ui)
549
551
550 def _debugdisplaycolor(ui):
552 def _debugdisplaycolor(ui):
551 global _styles
553 global _styles
552 oldstyle = _styles
554 oldstyle = _styles
553 try:
555 try:
554 _styles = {}
556 _styles = {}
555 for effect in _effects.keys():
557 for effect in _effects.keys():
556 _styles[effect] = effect
558 _styles[effect] = effect
557 if _terminfo_params:
559 if _terminfo_params:
558 for k, v in ui.configitems('color'):
560 for k, v in ui.configitems('color'):
559 if k.startswith('color.'):
561 if k.startswith('color.'):
560 _styles[k] = k[6:]
562 _styles[k] = k[6:]
561 elif k.startswith('terminfo.'):
563 elif k.startswith('terminfo.'):
562 _styles[k] = k[9:]
564 _styles[k] = k[9:]
563 ui.write(_('available colors:\n'))
565 ui.write(_('available colors:\n'))
564 # sort label with a '_' after the other to group '_background' entry.
566 # sort label with a '_' after the other to group '_background' entry.
565 items = sorted(_styles.items(),
567 items = sorted(_styles.items(),
566 key=lambda i: ('_' in i[0], i[0], i[1]))
568 key=lambda i: ('_' in i[0], i[0], i[1]))
567 for colorname, label in items:
569 for colorname, label in items:
568 ui.write(('%s\n') % colorname, label=label)
570 ui.write(('%s\n') % colorname, label=label)
569 finally:
571 finally:
570 _styles = oldstyle
572 _styles = oldstyle
571
573
572 def _debugdisplaystyle(ui):
574 def _debugdisplaystyle(ui):
573 ui.write(_('available style:\n'))
575 ui.write(_('available style:\n'))
574 width = max(len(s) for s in _styles)
576 width = max(len(s) for s in _styles)
575 for label, effects in sorted(_styles.items()):
577 for label, effects in sorted(_styles.items()):
576 ui.write('%s' % label, label=label)
578 ui.write('%s' % label, label=label)
577 if effects:
579 if effects:
578 # 50
580 # 50
579 ui.write(': ')
581 ui.write(': ')
580 ui.write(' ' * (max(0, width - len(label))))
582 ui.write(' ' * (max(0, width - len(label))))
581 ui.write(', '.join(ui.label(e, e) for e in effects.split()))
583 ui.write(', '.join(ui.label(e, e) for e in effects.split()))
582 ui.write('\n')
584 ui.write('\n')
583
585
584 if os.name != 'nt':
586 if os.name != 'nt':
585 w32effects = None
587 w32effects = None
586 else:
588 else:
587 import ctypes
589 import ctypes
588 import re
590 import re
589
591
590 _kernel32 = ctypes.windll.kernel32
592 _kernel32 = ctypes.windll.kernel32
591
593
592 _WORD = ctypes.c_ushort
594 _WORD = ctypes.c_ushort
593
595
594 _INVALID_HANDLE_VALUE = -1
596 _INVALID_HANDLE_VALUE = -1
595
597
596 class _COORD(ctypes.Structure):
598 class _COORD(ctypes.Structure):
597 _fields_ = [('X', ctypes.c_short),
599 _fields_ = [('X', ctypes.c_short),
598 ('Y', ctypes.c_short)]
600 ('Y', ctypes.c_short)]
599
601
600 class _SMALL_RECT(ctypes.Structure):
602 class _SMALL_RECT(ctypes.Structure):
601 _fields_ = [('Left', ctypes.c_short),
603 _fields_ = [('Left', ctypes.c_short),
602 ('Top', ctypes.c_short),
604 ('Top', ctypes.c_short),
603 ('Right', ctypes.c_short),
605 ('Right', ctypes.c_short),
604 ('Bottom', ctypes.c_short)]
606 ('Bottom', ctypes.c_short)]
605
607
606 class _CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
608 class _CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
607 _fields_ = [('dwSize', _COORD),
609 _fields_ = [('dwSize', _COORD),
608 ('dwCursorPosition', _COORD),
610 ('dwCursorPosition', _COORD),
609 ('wAttributes', _WORD),
611 ('wAttributes', _WORD),
610 ('srWindow', _SMALL_RECT),
612 ('srWindow', _SMALL_RECT),
611 ('dwMaximumWindowSize', _COORD)]
613 ('dwMaximumWindowSize', _COORD)]
612
614
613 _STD_OUTPUT_HANDLE = 0xfffffff5 # (DWORD)-11
615 _STD_OUTPUT_HANDLE = 0xfffffff5 # (DWORD)-11
614 _STD_ERROR_HANDLE = 0xfffffff4 # (DWORD)-12
616 _STD_ERROR_HANDLE = 0xfffffff4 # (DWORD)-12
615
617
616 _FOREGROUND_BLUE = 0x0001
618 _FOREGROUND_BLUE = 0x0001
617 _FOREGROUND_GREEN = 0x0002
619 _FOREGROUND_GREEN = 0x0002
618 _FOREGROUND_RED = 0x0004
620 _FOREGROUND_RED = 0x0004
619 _FOREGROUND_INTENSITY = 0x0008
621 _FOREGROUND_INTENSITY = 0x0008
620
622
621 _BACKGROUND_BLUE = 0x0010
623 _BACKGROUND_BLUE = 0x0010
622 _BACKGROUND_GREEN = 0x0020
624 _BACKGROUND_GREEN = 0x0020
623 _BACKGROUND_RED = 0x0040
625 _BACKGROUND_RED = 0x0040
624 _BACKGROUND_INTENSITY = 0x0080
626 _BACKGROUND_INTENSITY = 0x0080
625
627
626 _COMMON_LVB_REVERSE_VIDEO = 0x4000
628 _COMMON_LVB_REVERSE_VIDEO = 0x4000
627 _COMMON_LVB_UNDERSCORE = 0x8000
629 _COMMON_LVB_UNDERSCORE = 0x8000
628
630
629 # http://msdn.microsoft.com/en-us/library/ms682088%28VS.85%29.aspx
631 # http://msdn.microsoft.com/en-us/library/ms682088%28VS.85%29.aspx
630 w32effects = {
632 w32effects = {
631 'none': -1,
633 'none': -1,
632 'black': 0,
634 'black': 0,
633 'red': _FOREGROUND_RED,
635 'red': _FOREGROUND_RED,
634 'green': _FOREGROUND_GREEN,
636 'green': _FOREGROUND_GREEN,
635 'yellow': _FOREGROUND_RED | _FOREGROUND_GREEN,
637 'yellow': _FOREGROUND_RED | _FOREGROUND_GREEN,
636 'blue': _FOREGROUND_BLUE,
638 'blue': _FOREGROUND_BLUE,
637 'magenta': _FOREGROUND_BLUE | _FOREGROUND_RED,
639 'magenta': _FOREGROUND_BLUE | _FOREGROUND_RED,
638 'cyan': _FOREGROUND_BLUE | _FOREGROUND_GREEN,
640 'cyan': _FOREGROUND_BLUE | _FOREGROUND_GREEN,
639 'white': _FOREGROUND_RED | _FOREGROUND_GREEN | _FOREGROUND_BLUE,
641 'white': _FOREGROUND_RED | _FOREGROUND_GREEN | _FOREGROUND_BLUE,
640 'bold': _FOREGROUND_INTENSITY,
642 'bold': _FOREGROUND_INTENSITY,
641 'black_background': 0x100, # unused value > 0x0f
643 'black_background': 0x100, # unused value > 0x0f
642 'red_background': _BACKGROUND_RED,
644 'red_background': _BACKGROUND_RED,
643 'green_background': _BACKGROUND_GREEN,
645 'green_background': _BACKGROUND_GREEN,
644 'yellow_background': _BACKGROUND_RED | _BACKGROUND_GREEN,
646 'yellow_background': _BACKGROUND_RED | _BACKGROUND_GREEN,
645 'blue_background': _BACKGROUND_BLUE,
647 'blue_background': _BACKGROUND_BLUE,
646 'purple_background': _BACKGROUND_BLUE | _BACKGROUND_RED,
648 'purple_background': _BACKGROUND_BLUE | _BACKGROUND_RED,
647 'cyan_background': _BACKGROUND_BLUE | _BACKGROUND_GREEN,
649 'cyan_background': _BACKGROUND_BLUE | _BACKGROUND_GREEN,
648 'white_background': (_BACKGROUND_RED | _BACKGROUND_GREEN |
650 'white_background': (_BACKGROUND_RED | _BACKGROUND_GREEN |
649 _BACKGROUND_BLUE),
651 _BACKGROUND_BLUE),
650 'bold_background': _BACKGROUND_INTENSITY,
652 'bold_background': _BACKGROUND_INTENSITY,
651 'underline': _COMMON_LVB_UNDERSCORE, # double-byte charsets only
653 'underline': _COMMON_LVB_UNDERSCORE, # double-byte charsets only
652 'inverse': _COMMON_LVB_REVERSE_VIDEO, # double-byte charsets only
654 'inverse': _COMMON_LVB_REVERSE_VIDEO, # double-byte charsets only
653 }
655 }
654
656
655 passthrough = set([_FOREGROUND_INTENSITY,
657 passthrough = set([_FOREGROUND_INTENSITY,
656 _BACKGROUND_INTENSITY,
658 _BACKGROUND_INTENSITY,
657 _COMMON_LVB_UNDERSCORE,
659 _COMMON_LVB_UNDERSCORE,
658 _COMMON_LVB_REVERSE_VIDEO])
660 _COMMON_LVB_REVERSE_VIDEO])
659
661
660 stdout = _kernel32.GetStdHandle(
662 stdout = _kernel32.GetStdHandle(
661 _STD_OUTPUT_HANDLE) # don't close the handle returned
663 _STD_OUTPUT_HANDLE) # don't close the handle returned
662 if stdout is None or stdout == _INVALID_HANDLE_VALUE:
664 if stdout is None or stdout == _INVALID_HANDLE_VALUE:
663 w32effects = None
665 w32effects = None
664 else:
666 else:
665 csbi = _CONSOLE_SCREEN_BUFFER_INFO()
667 csbi = _CONSOLE_SCREEN_BUFFER_INFO()
666 if not _kernel32.GetConsoleScreenBufferInfo(
668 if not _kernel32.GetConsoleScreenBufferInfo(
667 stdout, ctypes.byref(csbi)):
669 stdout, ctypes.byref(csbi)):
668 # stdout may not support GetConsoleScreenBufferInfo()
670 # stdout may not support GetConsoleScreenBufferInfo()
669 # when called from subprocess or redirected
671 # when called from subprocess or redirected
670 w32effects = None
672 w32effects = None
671 else:
673 else:
672 origattr = csbi.wAttributes
674 origattr = csbi.wAttributes
673 ansire = re.compile('\033\[([^m]*)m([^\033]*)(.*)',
675 ansire = re.compile('\033\[([^m]*)m([^\033]*)(.*)',
674 re.MULTILINE | re.DOTALL)
676 re.MULTILINE | re.DOTALL)
675
677
676 def win32print(text, orig, **opts):
678 def win32print(text, orig, **opts):
677 label = opts.get('label', '')
679 label = opts.get('label', '')
678 attr = origattr
680 attr = origattr
679
681
680 def mapcolor(val, attr):
682 def mapcolor(val, attr):
681 if val == -1:
683 if val == -1:
682 return origattr
684 return origattr
683 elif val in passthrough:
685 elif val in passthrough:
684 return attr | val
686 return attr | val
685 elif val > 0x0f:
687 elif val > 0x0f:
686 return (val & 0x70) | (attr & 0x8f)
688 return (val & 0x70) | (attr & 0x8f)
687 else:
689 else:
688 return (val & 0x07) | (attr & 0xf8)
690 return (val & 0x07) | (attr & 0xf8)
689
691
690 # determine console attributes based on labels
692 # determine console attributes based on labels
691 for l in label.split():
693 for l in label.split():
692 style = _styles.get(l, '')
694 style = _styles.get(l, '')
693 for effect in style.split():
695 for effect in style.split():
694 try:
696 try:
695 attr = mapcolor(w32effects[effect], attr)
697 attr = mapcolor(w32effects[effect], attr)
696 except KeyError:
698 except KeyError:
697 # w32effects could not have certain attributes so we skip
699 # w32effects could not have certain attributes so we skip
698 # them if not found
700 # them if not found
699 pass
701 pass
700 # hack to ensure regexp finds data
702 # hack to ensure regexp finds data
701 if not text.startswith('\033['):
703 if not text.startswith('\033['):
702 text = '\033[m' + text
704 text = '\033[m' + text
703
705
704 # Look for ANSI-like codes embedded in text
706 # Look for ANSI-like codes embedded in text
705 m = re.match(ansire, text)
707 m = re.match(ansire, text)
706
708
707 try:
709 try:
708 while m:
710 while m:
709 for sattr in m.group(1).split(';'):
711 for sattr in m.group(1).split(';'):
710 if sattr:
712 if sattr:
711 attr = mapcolor(int(sattr), attr)
713 attr = mapcolor(int(sattr), attr)
712 _kernel32.SetConsoleTextAttribute(stdout, attr)
714 _kernel32.SetConsoleTextAttribute(stdout, attr)
713 orig(m.group(2), **opts)
715 orig(m.group(2), **opts)
714 m = re.match(ansire, m.group(3))
716 m = re.match(ansire, m.group(3))
715 finally:
717 finally:
716 # Explicitly reset original attributes
718 # Explicitly reset original attributes
717 _kernel32.SetConsoleTextAttribute(stdout, origattr)
719 _kernel32.SetConsoleTextAttribute(stdout, origattr)
@@ -1,297 +1,297 b''
1 # cvs.py: CVS conversion code inspired by hg-cvs-import and git-cvsimport
1 # cvs.py: CVS conversion code inspired by hg-cvs-import and git-cvsimport
2 #
2 #
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
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 from __future__ import absolute_import
7 from __future__ import absolute_import
8
8
9 import errno
9 import errno
10 import os
10 import os
11 import re
11 import re
12 import socket
12 import socket
13
13
14 from mercurial.i18n import _
14 from mercurial.i18n import _
15 from mercurial import (
15 from mercurial import (
16 encoding,
16 encoding,
17 error,
17 error,
18 pycompat,
18 pycompat,
19 util,
19 util,
20 )
20 )
21
21
22 from . import (
22 from . import (
23 common,
23 common,
24 cvsps,
24 cvsps,
25 )
25 )
26
26
27 stringio = util.stringio
27 stringio = util.stringio
28 checktool = common.checktool
28 checktool = common.checktool
29 commit = common.commit
29 commit = common.commit
30 converter_source = common.converter_source
30 converter_source = common.converter_source
31 makedatetimestamp = common.makedatetimestamp
31 makedatetimestamp = common.makedatetimestamp
32 NoRepo = common.NoRepo
32 NoRepo = common.NoRepo
33
33
34 class convert_cvs(converter_source):
34 class convert_cvs(converter_source):
35 def __init__(self, ui, path, revs=None):
35 def __init__(self, ui, path, revs=None):
36 super(convert_cvs, self).__init__(ui, path, revs=revs)
36 super(convert_cvs, self).__init__(ui, path, revs=revs)
37
37
38 cvs = os.path.join(path, "CVS")
38 cvs = os.path.join(path, "CVS")
39 if not os.path.exists(cvs):
39 if not os.path.exists(cvs):
40 raise NoRepo(_("%s does not look like a CVS checkout") % path)
40 raise NoRepo(_("%s does not look like a CVS checkout") % path)
41
41
42 checktool('cvs')
42 checktool('cvs')
43
43
44 self.changeset = None
44 self.changeset = None
45 self.files = {}
45 self.files = {}
46 self.tags = {}
46 self.tags = {}
47 self.lastbranch = {}
47 self.lastbranch = {}
48 self.socket = None
48 self.socket = None
49 self.cvsroot = open(os.path.join(cvs, "Root")).read()[:-1]
49 self.cvsroot = open(os.path.join(cvs, "Root")).read()[:-1]
50 self.cvsrepo = open(os.path.join(cvs, "Repository")).read()[:-1]
50 self.cvsrepo = open(os.path.join(cvs, "Repository")).read()[:-1]
51 self.encoding = encoding.encoding
51 self.encoding = encoding.encoding
52
52
53 self._connect()
53 self._connect()
54
54
55 def _parse(self):
55 def _parse(self):
56 if self.changeset is not None:
56 if self.changeset is not None:
57 return
57 return
58 self.changeset = {}
58 self.changeset = {}
59
59
60 maxrev = 0
60 maxrev = 0
61 if self.revs:
61 if self.revs:
62 if len(self.revs) > 1:
62 if len(self.revs) > 1:
63 raise error.Abort(_('cvs source does not support specifying '
63 raise error.Abort(_('cvs source does not support specifying '
64 'multiple revs'))
64 'multiple revs'))
65 # TODO: handle tags
65 # TODO: handle tags
66 try:
66 try:
67 # patchset number?
67 # patchset number?
68 maxrev = int(self.revs[0])
68 maxrev = int(self.revs[0])
69 except ValueError:
69 except ValueError:
70 raise error.Abort(_('revision %s is not a patchset number')
70 raise error.Abort(_('revision %s is not a patchset number')
71 % self.revs[0])
71 % self.revs[0])
72
72
73 d = pycompat.getcwd()
73 d = pycompat.getcwd()
74 try:
74 try:
75 os.chdir(self.path)
75 os.chdir(self.path)
76 id = None
76 id = None
77
77
78 cache = 'update'
78 cache = 'update'
79 if not self.ui.configbool('convert', 'cvsps.cache', True):
79 if not self.ui.configbool('convert', 'cvsps.cache', True):
80 cache = None
80 cache = None
81 db = cvsps.createlog(self.ui, cache=cache)
81 db = cvsps.createlog(self.ui, cache=cache)
82 db = cvsps.createchangeset(self.ui, db,
82 db = cvsps.createchangeset(self.ui, db,
83 fuzz=int(self.ui.config('convert', 'cvsps.fuzz', 60)),
83 fuzz=int(self.ui.config('convert', 'cvsps.fuzz', 60)),
84 mergeto=self.ui.config('convert', 'cvsps.mergeto', None),
84 mergeto=self.ui.config('convert', 'cvsps.mergeto', None),
85 mergefrom=self.ui.config('convert', 'cvsps.mergefrom', None))
85 mergefrom=self.ui.config('convert', 'cvsps.mergefrom', None))
86
86
87 for cs in db:
87 for cs in db:
88 if maxrev and cs.id > maxrev:
88 if maxrev and cs.id > maxrev:
89 break
89 break
90 id = str(cs.id)
90 id = str(cs.id)
91 cs.author = self.recode(cs.author)
91 cs.author = self.recode(cs.author)
92 self.lastbranch[cs.branch] = id
92 self.lastbranch[cs.branch] = id
93 cs.comment = self.recode(cs.comment)
93 cs.comment = self.recode(cs.comment)
94 if self.ui.configbool('convert', 'localtimezone'):
94 if self.ui.configbool('convert', 'localtimezone'):
95 cs.date = makedatetimestamp(cs.date[0])
95 cs.date = makedatetimestamp(cs.date[0])
96 date = util.datestr(cs.date, '%Y-%m-%d %H:%M:%S %1%2')
96 date = util.datestr(cs.date, '%Y-%m-%d %H:%M:%S %1%2')
97 self.tags.update(dict.fromkeys(cs.tags, id))
97 self.tags.update(dict.fromkeys(cs.tags, id))
98
98
99 files = {}
99 files = {}
100 for f in cs.entries:
100 for f in cs.entries:
101 files[f.file] = "%s%s" % ('.'.join([str(x)
101 files[f.file] = "%s%s" % ('.'.join([str(x)
102 for x in f.revision]),
102 for x in f.revision]),
103 ['', '(DEAD)'][f.dead])
103 ['', '(DEAD)'][f.dead])
104
104
105 # add current commit to set
105 # add current commit to set
106 c = commit(author=cs.author, date=date,
106 c = commit(author=cs.author, date=date,
107 parents=[str(p.id) for p in cs.parents],
107 parents=[str(p.id) for p in cs.parents],
108 desc=cs.comment, branch=cs.branch or '')
108 desc=cs.comment, branch=cs.branch or '')
109 self.changeset[id] = c
109 self.changeset[id] = c
110 self.files[id] = files
110 self.files[id] = files
111
111
112 self.heads = self.lastbranch.values()
112 self.heads = self.lastbranch.values()
113 finally:
113 finally:
114 os.chdir(d)
114 os.chdir(d)
115
115
116 def _connect(self):
116 def _connect(self):
117 root = self.cvsroot
117 root = self.cvsroot
118 conntype = None
118 conntype = None
119 user, host = None, None
119 user, host = None, None
120 cmd = ['cvs', 'server']
120 cmd = ['cvs', 'server']
121
121
122 self.ui.status(_("connecting to %s\n") % root)
122 self.ui.status(_("connecting to %s\n") % root)
123
123
124 if root.startswith(":pserver:"):
124 if root.startswith(":pserver:"):
125 root = root[9:]
125 root = root[9:]
126 m = re.match(r'(?:(.*?)(?::(.*?))?@)?([^:\/]*)(?::(\d*))?(.*)',
126 m = re.match(r'(?:(.*?)(?::(.*?))?@)?([^:\/]*)(?::(\d*))?(.*)',
127 root)
127 root)
128 if m:
128 if m:
129 conntype = "pserver"
129 conntype = "pserver"
130 user, passw, serv, port, root = m.groups()
130 user, passw, serv, port, root = m.groups()
131 if not user:
131 if not user:
132 user = "anonymous"
132 user = "anonymous"
133 if not port:
133 if not port:
134 port = 2401
134 port = 2401
135 else:
135 else:
136 port = int(port)
136 port = int(port)
137 format0 = ":pserver:%s@%s:%s" % (user, serv, root)
137 format0 = ":pserver:%s@%s:%s" % (user, serv, root)
138 format1 = ":pserver:%s@%s:%d%s" % (user, serv, port, root)
138 format1 = ":pserver:%s@%s:%d%s" % (user, serv, port, root)
139
139
140 if not passw:
140 if not passw:
141 passw = "A"
141 passw = "A"
142 cvspass = os.path.expanduser("~/.cvspass")
142 cvspass = os.path.expanduser("~/.cvspass")
143 try:
143 try:
144 pf = open(cvspass)
144 pf = open(cvspass)
145 for line in pf.read().splitlines():
145 for line in pf.read().splitlines():
146 part1, part2 = line.split(' ', 1)
146 part1, part2 = line.split(' ', 1)
147 # /1 :pserver:user@example.com:2401/cvsroot/foo
147 # /1 :pserver:user@example.com:2401/cvsroot/foo
148 # Ah<Z
148 # Ah<Z
149 if part1 == '/1':
149 if part1 == '/1':
150 part1, part2 = part2.split(' ', 1)
150 part1, part2 = part2.split(' ', 1)
151 format = format1
151 format = format1
152 # :pserver:user@example.com:/cvsroot/foo Ah<Z
152 # :pserver:user@example.com:/cvsroot/foo Ah<Z
153 else:
153 else:
154 format = format0
154 format = format0
155 if part1 == format:
155 if part1 == format:
156 passw = part2
156 passw = part2
157 break
157 break
158 pf.close()
158 pf.close()
159 except IOError as inst:
159 except IOError as inst:
160 if inst.errno != errno.ENOENT:
160 if inst.errno != errno.ENOENT:
161 if not getattr(inst, 'filename', None):
161 if not getattr(inst, 'filename', None):
162 inst.filename = cvspass
162 inst.filename = cvspass
163 raise
163 raise
164
164
165 sck = socket.socket()
165 sck = socket.socket()
166 sck.connect((serv, port))
166 sck.connect((serv, port))
167 sck.send("\n".join(["BEGIN AUTH REQUEST", root, user, passw,
167 sck.send("\n".join(["BEGIN AUTH REQUEST", root, user, passw,
168 "END AUTH REQUEST", ""]))
168 "END AUTH REQUEST", ""]))
169 if sck.recv(128) != "I LOVE YOU\n":
169 if sck.recv(128) != "I LOVE YOU\n":
170 raise error.Abort(_("CVS pserver authentication failed"))
170 raise error.Abort(_("CVS pserver authentication failed"))
171
171
172 self.writep = self.readp = sck.makefile('r+')
172 self.writep = self.readp = sck.makefile('r+')
173
173
174 if not conntype and root.startswith(":local:"):
174 if not conntype and root.startswith(":local:"):
175 conntype = "local"
175 conntype = "local"
176 root = root[7:]
176 root = root[7:]
177
177
178 if not conntype:
178 if not conntype:
179 # :ext:user@host/home/user/path/to/cvsroot
179 # :ext:user@host/home/user/path/to/cvsroot
180 if root.startswith(":ext:"):
180 if root.startswith(":ext:"):
181 root = root[5:]
181 root = root[5:]
182 m = re.match(r'(?:([^@:/]+)@)?([^:/]+):?(.*)', root)
182 m = re.match(r'(?:([^@:/]+)@)?([^:/]+):?(.*)', root)
183 # Do not take Windows path "c:\foo\bar" for a connection strings
183 # Do not take Windows path "c:\foo\bar" for a connection strings
184 if os.path.isdir(root) or not m:
184 if os.path.isdir(root) or not m:
185 conntype = "local"
185 conntype = "local"
186 else:
186 else:
187 conntype = "rsh"
187 conntype = "rsh"
188 user, host, root = m.group(1), m.group(2), m.group(3)
188 user, host, root = m.group(1), m.group(2), m.group(3)
189
189
190 if conntype != "pserver":
190 if conntype != "pserver":
191 if conntype == "rsh":
191 if conntype == "rsh":
192 rsh = os.environ.get("CVS_RSH") or "ssh"
192 rsh = encoding.environ.get("CVS_RSH") or "ssh"
193 if user:
193 if user:
194 cmd = [rsh, '-l', user, host] + cmd
194 cmd = [rsh, '-l', user, host] + cmd
195 else:
195 else:
196 cmd = [rsh, host] + cmd
196 cmd = [rsh, host] + cmd
197
197
198 # popen2 does not support argument lists under Windows
198 # popen2 does not support argument lists under Windows
199 cmd = [util.shellquote(arg) for arg in cmd]
199 cmd = [util.shellquote(arg) for arg in cmd]
200 cmd = util.quotecommand(' '.join(cmd))
200 cmd = util.quotecommand(' '.join(cmd))
201 self.writep, self.readp = util.popen2(cmd)
201 self.writep, self.readp = util.popen2(cmd)
202
202
203 self.realroot = root
203 self.realroot = root
204
204
205 self.writep.write("Root %s\n" % root)
205 self.writep.write("Root %s\n" % root)
206 self.writep.write("Valid-responses ok error Valid-requests Mode"
206 self.writep.write("Valid-responses ok error Valid-requests Mode"
207 " M Mbinary E Checked-in Created Updated"
207 " M Mbinary E Checked-in Created Updated"
208 " Merged Removed\n")
208 " Merged Removed\n")
209 self.writep.write("valid-requests\n")
209 self.writep.write("valid-requests\n")
210 self.writep.flush()
210 self.writep.flush()
211 r = self.readp.readline()
211 r = self.readp.readline()
212 if not r.startswith("Valid-requests"):
212 if not r.startswith("Valid-requests"):
213 raise error.Abort(_('unexpected response from CVS server '
213 raise error.Abort(_('unexpected response from CVS server '
214 '(expected "Valid-requests", but got %r)')
214 '(expected "Valid-requests", but got %r)')
215 % r)
215 % r)
216 if "UseUnchanged" in r:
216 if "UseUnchanged" in r:
217 self.writep.write("UseUnchanged\n")
217 self.writep.write("UseUnchanged\n")
218 self.writep.flush()
218 self.writep.flush()
219 r = self.readp.readline()
219 r = self.readp.readline()
220
220
221 def getheads(self):
221 def getheads(self):
222 self._parse()
222 self._parse()
223 return self.heads
223 return self.heads
224
224
225 def getfile(self, name, rev):
225 def getfile(self, name, rev):
226
226
227 def chunkedread(fp, count):
227 def chunkedread(fp, count):
228 # file-objects returned by socket.makefile() do not handle
228 # file-objects returned by socket.makefile() do not handle
229 # large read() requests very well.
229 # large read() requests very well.
230 chunksize = 65536
230 chunksize = 65536
231 output = stringio()
231 output = stringio()
232 while count > 0:
232 while count > 0:
233 data = fp.read(min(count, chunksize))
233 data = fp.read(min(count, chunksize))
234 if not data:
234 if not data:
235 raise error.Abort(_("%d bytes missing from remote file")
235 raise error.Abort(_("%d bytes missing from remote file")
236 % count)
236 % count)
237 count -= len(data)
237 count -= len(data)
238 output.write(data)
238 output.write(data)
239 return output.getvalue()
239 return output.getvalue()
240
240
241 self._parse()
241 self._parse()
242 if rev.endswith("(DEAD)"):
242 if rev.endswith("(DEAD)"):
243 return None, None
243 return None, None
244
244
245 args = ("-N -P -kk -r %s --" % rev).split()
245 args = ("-N -P -kk -r %s --" % rev).split()
246 args.append(self.cvsrepo + '/' + name)
246 args.append(self.cvsrepo + '/' + name)
247 for x in args:
247 for x in args:
248 self.writep.write("Argument %s\n" % x)
248 self.writep.write("Argument %s\n" % x)
249 self.writep.write("Directory .\n%s\nco\n" % self.realroot)
249 self.writep.write("Directory .\n%s\nco\n" % self.realroot)
250 self.writep.flush()
250 self.writep.flush()
251
251
252 data = ""
252 data = ""
253 mode = None
253 mode = None
254 while True:
254 while True:
255 line = self.readp.readline()
255 line = self.readp.readline()
256 if line.startswith("Created ") or line.startswith("Updated "):
256 if line.startswith("Created ") or line.startswith("Updated "):
257 self.readp.readline() # path
257 self.readp.readline() # path
258 self.readp.readline() # entries
258 self.readp.readline() # entries
259 mode = self.readp.readline()[:-1]
259 mode = self.readp.readline()[:-1]
260 count = int(self.readp.readline()[:-1])
260 count = int(self.readp.readline()[:-1])
261 data = chunkedread(self.readp, count)
261 data = chunkedread(self.readp, count)
262 elif line.startswith(" "):
262 elif line.startswith(" "):
263 data += line[1:]
263 data += line[1:]
264 elif line.startswith("M "):
264 elif line.startswith("M "):
265 pass
265 pass
266 elif line.startswith("Mbinary "):
266 elif line.startswith("Mbinary "):
267 count = int(self.readp.readline()[:-1])
267 count = int(self.readp.readline()[:-1])
268 data = chunkedread(self.readp, count)
268 data = chunkedread(self.readp, count)
269 else:
269 else:
270 if line == "ok\n":
270 if line == "ok\n":
271 if mode is None:
271 if mode is None:
272 raise error.Abort(_('malformed response from CVS'))
272 raise error.Abort(_('malformed response from CVS'))
273 return (data, "x" in mode and "x" or "")
273 return (data, "x" in mode and "x" or "")
274 elif line.startswith("E "):
274 elif line.startswith("E "):
275 self.ui.warn(_("cvs server: %s\n") % line[2:])
275 self.ui.warn(_("cvs server: %s\n") % line[2:])
276 elif line.startswith("Remove"):
276 elif line.startswith("Remove"):
277 self.readp.readline()
277 self.readp.readline()
278 else:
278 else:
279 raise error.Abort(_("unknown CVS response: %s") % line)
279 raise error.Abort(_("unknown CVS response: %s") % line)
280
280
281 def getchanges(self, rev, full):
281 def getchanges(self, rev, full):
282 if full:
282 if full:
283 raise error.Abort(_("convert from cvs does not support --full"))
283 raise error.Abort(_("convert from cvs does not support --full"))
284 self._parse()
284 self._parse()
285 return sorted(self.files[rev].iteritems()), {}, set()
285 return sorted(self.files[rev].iteritems()), {}, set()
286
286
287 def getcommit(self, rev):
287 def getcommit(self, rev):
288 self._parse()
288 self._parse()
289 return self.changeset[rev]
289 return self.changeset[rev]
290
290
291 def gettags(self):
291 def gettags(self):
292 self._parse()
292 self._parse()
293 return self.tags
293 return self.tags
294
294
295 def getchangedfiles(self, rev, i):
295 def getchangedfiles(self, rev, i):
296 self._parse()
296 self._parse()
297 return sorted(self.files[rev])
297 return sorted(self.files[rev])
@@ -1,920 +1,921 b''
1 # Mercurial built-in replacement for cvsps.
1 # Mercurial built-in replacement for cvsps.
2 #
2 #
3 # Copyright 2008, Frank Kingswood <frank@kingswood-consulting.co.uk>
3 # Copyright 2008, Frank Kingswood <frank@kingswood-consulting.co.uk>
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 from __future__ import absolute_import
7 from __future__ import absolute_import
8
8
9 import os
9 import os
10 import re
10 import re
11
11
12 from mercurial.i18n import _
12 from mercurial.i18n import _
13 from mercurial import (
13 from mercurial import (
14 encoding,
14 hook,
15 hook,
15 pycompat,
16 pycompat,
16 util,
17 util,
17 )
18 )
18
19
19 pickle = util.pickle
20 pickle = util.pickle
20
21
21 class logentry(object):
22 class logentry(object):
22 '''Class logentry has the following attributes:
23 '''Class logentry has the following attributes:
23 .author - author name as CVS knows it
24 .author - author name as CVS knows it
24 .branch - name of branch this revision is on
25 .branch - name of branch this revision is on
25 .branches - revision tuple of branches starting at this revision
26 .branches - revision tuple of branches starting at this revision
26 .comment - commit message
27 .comment - commit message
27 .commitid - CVS commitid or None
28 .commitid - CVS commitid or None
28 .date - the commit date as a (time, tz) tuple
29 .date - the commit date as a (time, tz) tuple
29 .dead - true if file revision is dead
30 .dead - true if file revision is dead
30 .file - Name of file
31 .file - Name of file
31 .lines - a tuple (+lines, -lines) or None
32 .lines - a tuple (+lines, -lines) or None
32 .parent - Previous revision of this entry
33 .parent - Previous revision of this entry
33 .rcs - name of file as returned from CVS
34 .rcs - name of file as returned from CVS
34 .revision - revision number as tuple
35 .revision - revision number as tuple
35 .tags - list of tags on the file
36 .tags - list of tags on the file
36 .synthetic - is this a synthetic "file ... added on ..." revision?
37 .synthetic - is this a synthetic "file ... added on ..." revision?
37 .mergepoint - the branch that has been merged from (if present in
38 .mergepoint - the branch that has been merged from (if present in
38 rlog output) or None
39 rlog output) or None
39 .branchpoints - the branches that start at the current entry or empty
40 .branchpoints - the branches that start at the current entry or empty
40 '''
41 '''
41 def __init__(self, **entries):
42 def __init__(self, **entries):
42 self.synthetic = False
43 self.synthetic = False
43 self.__dict__.update(entries)
44 self.__dict__.update(entries)
44
45
45 def __repr__(self):
46 def __repr__(self):
46 items = ("%s=%r"%(k, self.__dict__[k]) for k in sorted(self.__dict__))
47 items = ("%s=%r"%(k, self.__dict__[k]) for k in sorted(self.__dict__))
47 return "%s(%s)"%(type(self).__name__, ", ".join(items))
48 return "%s(%s)"%(type(self).__name__, ", ".join(items))
48
49
49 class logerror(Exception):
50 class logerror(Exception):
50 pass
51 pass
51
52
52 def getrepopath(cvspath):
53 def getrepopath(cvspath):
53 """Return the repository path from a CVS path.
54 """Return the repository path from a CVS path.
54
55
55 >>> getrepopath('/foo/bar')
56 >>> getrepopath('/foo/bar')
56 '/foo/bar'
57 '/foo/bar'
57 >>> getrepopath('c:/foo/bar')
58 >>> getrepopath('c:/foo/bar')
58 '/foo/bar'
59 '/foo/bar'
59 >>> getrepopath(':pserver:10/foo/bar')
60 >>> getrepopath(':pserver:10/foo/bar')
60 '/foo/bar'
61 '/foo/bar'
61 >>> getrepopath(':pserver:10c:/foo/bar')
62 >>> getrepopath(':pserver:10c:/foo/bar')
62 '/foo/bar'
63 '/foo/bar'
63 >>> getrepopath(':pserver:/foo/bar')
64 >>> getrepopath(':pserver:/foo/bar')
64 '/foo/bar'
65 '/foo/bar'
65 >>> getrepopath(':pserver:c:/foo/bar')
66 >>> getrepopath(':pserver:c:/foo/bar')
66 '/foo/bar'
67 '/foo/bar'
67 >>> getrepopath(':pserver:truc@foo.bar:/foo/bar')
68 >>> getrepopath(':pserver:truc@foo.bar:/foo/bar')
68 '/foo/bar'
69 '/foo/bar'
69 >>> getrepopath(':pserver:truc@foo.bar:c:/foo/bar')
70 >>> getrepopath(':pserver:truc@foo.bar:c:/foo/bar')
70 '/foo/bar'
71 '/foo/bar'
71 >>> getrepopath('user@server/path/to/repository')
72 >>> getrepopath('user@server/path/to/repository')
72 '/path/to/repository'
73 '/path/to/repository'
73 """
74 """
74 # According to CVS manual, CVS paths are expressed like:
75 # According to CVS manual, CVS paths are expressed like:
75 # [:method:][[user][:password]@]hostname[:[port]]/path/to/repository
76 # [:method:][[user][:password]@]hostname[:[port]]/path/to/repository
76 #
77 #
77 # CVSpath is splitted into parts and then position of the first occurrence
78 # CVSpath is splitted into parts and then position of the first occurrence
78 # of the '/' char after the '@' is located. The solution is the rest of the
79 # of the '/' char after the '@' is located. The solution is the rest of the
79 # string after that '/' sign including it
80 # string after that '/' sign including it
80
81
81 parts = cvspath.split(':')
82 parts = cvspath.split(':')
82 atposition = parts[-1].find('@')
83 atposition = parts[-1].find('@')
83 start = 0
84 start = 0
84
85
85 if atposition != -1:
86 if atposition != -1:
86 start = atposition
87 start = atposition
87
88
88 repopath = parts[-1][parts[-1].find('/', start):]
89 repopath = parts[-1][parts[-1].find('/', start):]
89 return repopath
90 return repopath
90
91
91 def createlog(ui, directory=None, root="", rlog=True, cache=None):
92 def createlog(ui, directory=None, root="", rlog=True, cache=None):
92 '''Collect the CVS rlog'''
93 '''Collect the CVS rlog'''
93
94
94 # Because we store many duplicate commit log messages, reusing strings
95 # Because we store many duplicate commit log messages, reusing strings
95 # saves a lot of memory and pickle storage space.
96 # saves a lot of memory and pickle storage space.
96 _scache = {}
97 _scache = {}
97 def scache(s):
98 def scache(s):
98 "return a shared version of a string"
99 "return a shared version of a string"
99 return _scache.setdefault(s, s)
100 return _scache.setdefault(s, s)
100
101
101 ui.status(_('collecting CVS rlog\n'))
102 ui.status(_('collecting CVS rlog\n'))
102
103
103 log = [] # list of logentry objects containing the CVS state
104 log = [] # list of logentry objects containing the CVS state
104
105
105 # patterns to match in CVS (r)log output, by state of use
106 # patterns to match in CVS (r)log output, by state of use
106 re_00 = re.compile('RCS file: (.+)$')
107 re_00 = re.compile('RCS file: (.+)$')
107 re_01 = re.compile('cvs \\[r?log aborted\\]: (.+)$')
108 re_01 = re.compile('cvs \\[r?log aborted\\]: (.+)$')
108 re_02 = re.compile('cvs (r?log|server): (.+)\n$')
109 re_02 = re.compile('cvs (r?log|server): (.+)\n$')
109 re_03 = re.compile("(Cannot access.+CVSROOT)|"
110 re_03 = re.compile("(Cannot access.+CVSROOT)|"
110 "(can't create temporary directory.+)$")
111 "(can't create temporary directory.+)$")
111 re_10 = re.compile('Working file: (.+)$')
112 re_10 = re.compile('Working file: (.+)$')
112 re_20 = re.compile('symbolic names:')
113 re_20 = re.compile('symbolic names:')
113 re_30 = re.compile('\t(.+): ([\\d.]+)$')
114 re_30 = re.compile('\t(.+): ([\\d.]+)$')
114 re_31 = re.compile('----------------------------$')
115 re_31 = re.compile('----------------------------$')
115 re_32 = re.compile('======================================='
116 re_32 = re.compile('======================================='
116 '======================================$')
117 '======================================$')
117 re_50 = re.compile('revision ([\\d.]+)(\s+locked by:\s+.+;)?$')
118 re_50 = re.compile('revision ([\\d.]+)(\s+locked by:\s+.+;)?$')
118 re_60 = re.compile(r'date:\s+(.+);\s+author:\s+(.+);\s+state:\s+(.+?);'
119 re_60 = re.compile(r'date:\s+(.+);\s+author:\s+(.+);\s+state:\s+(.+?);'
119 r'(\s+lines:\s+(\+\d+)?\s+(-\d+)?;)?'
120 r'(\s+lines:\s+(\+\d+)?\s+(-\d+)?;)?'
120 r'(\s+commitid:\s+([^;]+);)?'
121 r'(\s+commitid:\s+([^;]+);)?'
121 r'(.*mergepoint:\s+([^;]+);)?')
122 r'(.*mergepoint:\s+([^;]+);)?')
122 re_70 = re.compile('branches: (.+);$')
123 re_70 = re.compile('branches: (.+);$')
123
124
124 file_added_re = re.compile(r'file [^/]+ was (initially )?added on branch')
125 file_added_re = re.compile(r'file [^/]+ was (initially )?added on branch')
125
126
126 prefix = '' # leading path to strip of what we get from CVS
127 prefix = '' # leading path to strip of what we get from CVS
127
128
128 if directory is None:
129 if directory is None:
129 # Current working directory
130 # Current working directory
130
131
131 # Get the real directory in the repository
132 # Get the real directory in the repository
132 try:
133 try:
133 prefix = open(os.path.join('CVS','Repository')).read().strip()
134 prefix = open(os.path.join('CVS','Repository')).read().strip()
134 directory = prefix
135 directory = prefix
135 if prefix == ".":
136 if prefix == ".":
136 prefix = ""
137 prefix = ""
137 except IOError:
138 except IOError:
138 raise logerror(_('not a CVS sandbox'))
139 raise logerror(_('not a CVS sandbox'))
139
140
140 if prefix and not prefix.endswith(pycompat.ossep):
141 if prefix and not prefix.endswith(pycompat.ossep):
141 prefix += pycompat.ossep
142 prefix += pycompat.ossep
142
143
143 # Use the Root file in the sandbox, if it exists
144 # Use the Root file in the sandbox, if it exists
144 try:
145 try:
145 root = open(os.path.join('CVS','Root')).read().strip()
146 root = open(os.path.join('CVS','Root')).read().strip()
146 except IOError:
147 except IOError:
147 pass
148 pass
148
149
149 if not root:
150 if not root:
150 root = os.environ.get('CVSROOT', '')
151 root = encoding.environ.get('CVSROOT', '')
151
152
152 # read log cache if one exists
153 # read log cache if one exists
153 oldlog = []
154 oldlog = []
154 date = None
155 date = None
155
156
156 if cache:
157 if cache:
157 cachedir = os.path.expanduser('~/.hg.cvsps')
158 cachedir = os.path.expanduser('~/.hg.cvsps')
158 if not os.path.exists(cachedir):
159 if not os.path.exists(cachedir):
159 os.mkdir(cachedir)
160 os.mkdir(cachedir)
160
161
161 # The cvsps cache pickle needs a uniquified name, based on the
162 # The cvsps cache pickle needs a uniquified name, based on the
162 # repository location. The address may have all sort of nasties
163 # repository location. The address may have all sort of nasties
163 # in it, slashes, colons and such. So here we take just the
164 # in it, slashes, colons and such. So here we take just the
164 # alphanumeric characters, concatenated in a way that does not
165 # alphanumeric characters, concatenated in a way that does not
165 # mix up the various components, so that
166 # mix up the various components, so that
166 # :pserver:user@server:/path
167 # :pserver:user@server:/path
167 # and
168 # and
168 # /pserver/user/server/path
169 # /pserver/user/server/path
169 # are mapped to different cache file names.
170 # are mapped to different cache file names.
170 cachefile = root.split(":") + [directory, "cache"]
171 cachefile = root.split(":") + [directory, "cache"]
171 cachefile = ['-'.join(re.findall(r'\w+', s)) for s in cachefile if s]
172 cachefile = ['-'.join(re.findall(r'\w+', s)) for s in cachefile if s]
172 cachefile = os.path.join(cachedir,
173 cachefile = os.path.join(cachedir,
173 '.'.join([s for s in cachefile if s]))
174 '.'.join([s for s in cachefile if s]))
174
175
175 if cache == 'update':
176 if cache == 'update':
176 try:
177 try:
177 ui.note(_('reading cvs log cache %s\n') % cachefile)
178 ui.note(_('reading cvs log cache %s\n') % cachefile)
178 oldlog = pickle.load(open(cachefile))
179 oldlog = pickle.load(open(cachefile))
179 for e in oldlog:
180 for e in oldlog:
180 if not (util.safehasattr(e, 'branchpoints') and
181 if not (util.safehasattr(e, 'branchpoints') and
181 util.safehasattr(e, 'commitid') and
182 util.safehasattr(e, 'commitid') and
182 util.safehasattr(e, 'mergepoint')):
183 util.safehasattr(e, 'mergepoint')):
183 ui.status(_('ignoring old cache\n'))
184 ui.status(_('ignoring old cache\n'))
184 oldlog = []
185 oldlog = []
185 break
186 break
186
187
187 ui.note(_('cache has %d log entries\n') % len(oldlog))
188 ui.note(_('cache has %d log entries\n') % len(oldlog))
188 except Exception as e:
189 except Exception as e:
189 ui.note(_('error reading cache: %r\n') % e)
190 ui.note(_('error reading cache: %r\n') % e)
190
191
191 if oldlog:
192 if oldlog:
192 date = oldlog[-1].date # last commit date as a (time,tz) tuple
193 date = oldlog[-1].date # last commit date as a (time,tz) tuple
193 date = util.datestr(date, '%Y/%m/%d %H:%M:%S %1%2')
194 date = util.datestr(date, '%Y/%m/%d %H:%M:%S %1%2')
194
195
195 # build the CVS commandline
196 # build the CVS commandline
196 cmd = ['cvs', '-q']
197 cmd = ['cvs', '-q']
197 if root:
198 if root:
198 cmd.append('-d%s' % root)
199 cmd.append('-d%s' % root)
199 p = util.normpath(getrepopath(root))
200 p = util.normpath(getrepopath(root))
200 if not p.endswith('/'):
201 if not p.endswith('/'):
201 p += '/'
202 p += '/'
202 if prefix:
203 if prefix:
203 # looks like normpath replaces "" by "."
204 # looks like normpath replaces "" by "."
204 prefix = p + util.normpath(prefix)
205 prefix = p + util.normpath(prefix)
205 else:
206 else:
206 prefix = p
207 prefix = p
207 cmd.append(['log', 'rlog'][rlog])
208 cmd.append(['log', 'rlog'][rlog])
208 if date:
209 if date:
209 # no space between option and date string
210 # no space between option and date string
210 cmd.append('-d>%s' % date)
211 cmd.append('-d>%s' % date)
211 cmd.append(directory)
212 cmd.append(directory)
212
213
213 # state machine begins here
214 # state machine begins here
214 tags = {} # dictionary of revisions on current file with their tags
215 tags = {} # dictionary of revisions on current file with their tags
215 branchmap = {} # mapping between branch names and revision numbers
216 branchmap = {} # mapping between branch names and revision numbers
216 rcsmap = {}
217 rcsmap = {}
217 state = 0
218 state = 0
218 store = False # set when a new record can be appended
219 store = False # set when a new record can be appended
219
220
220 cmd = [util.shellquote(arg) for arg in cmd]
221 cmd = [util.shellquote(arg) for arg in cmd]
221 ui.note(_("running %s\n") % (' '.join(cmd)))
222 ui.note(_("running %s\n") % (' '.join(cmd)))
222 ui.debug("prefix=%r directory=%r root=%r\n" % (prefix, directory, root))
223 ui.debug("prefix=%r directory=%r root=%r\n" % (prefix, directory, root))
223
224
224 pfp = util.popen(' '.join(cmd))
225 pfp = util.popen(' '.join(cmd))
225 peek = pfp.readline()
226 peek = pfp.readline()
226 while True:
227 while True:
227 line = peek
228 line = peek
228 if line == '':
229 if line == '':
229 break
230 break
230 peek = pfp.readline()
231 peek = pfp.readline()
231 if line.endswith('\n'):
232 if line.endswith('\n'):
232 line = line[:-1]
233 line = line[:-1]
233 #ui.debug('state=%d line=%r\n' % (state, line))
234 #ui.debug('state=%d line=%r\n' % (state, line))
234
235
235 if state == 0:
236 if state == 0:
236 # initial state, consume input until we see 'RCS file'
237 # initial state, consume input until we see 'RCS file'
237 match = re_00.match(line)
238 match = re_00.match(line)
238 if match:
239 if match:
239 rcs = match.group(1)
240 rcs = match.group(1)
240 tags = {}
241 tags = {}
241 if rlog:
242 if rlog:
242 filename = util.normpath(rcs[:-2])
243 filename = util.normpath(rcs[:-2])
243 if filename.startswith(prefix):
244 if filename.startswith(prefix):
244 filename = filename[len(prefix):]
245 filename = filename[len(prefix):]
245 if filename.startswith('/'):
246 if filename.startswith('/'):
246 filename = filename[1:]
247 filename = filename[1:]
247 if filename.startswith('Attic/'):
248 if filename.startswith('Attic/'):
248 filename = filename[6:]
249 filename = filename[6:]
249 else:
250 else:
250 filename = filename.replace('/Attic/', '/')
251 filename = filename.replace('/Attic/', '/')
251 state = 2
252 state = 2
252 continue
253 continue
253 state = 1
254 state = 1
254 continue
255 continue
255 match = re_01.match(line)
256 match = re_01.match(line)
256 if match:
257 if match:
257 raise logerror(match.group(1))
258 raise logerror(match.group(1))
258 match = re_02.match(line)
259 match = re_02.match(line)
259 if match:
260 if match:
260 raise logerror(match.group(2))
261 raise logerror(match.group(2))
261 if re_03.match(line):
262 if re_03.match(line):
262 raise logerror(line)
263 raise logerror(line)
263
264
264 elif state == 1:
265 elif state == 1:
265 # expect 'Working file' (only when using log instead of rlog)
266 # expect 'Working file' (only when using log instead of rlog)
266 match = re_10.match(line)
267 match = re_10.match(line)
267 assert match, _('RCS file must be followed by working file')
268 assert match, _('RCS file must be followed by working file')
268 filename = util.normpath(match.group(1))
269 filename = util.normpath(match.group(1))
269 state = 2
270 state = 2
270
271
271 elif state == 2:
272 elif state == 2:
272 # expect 'symbolic names'
273 # expect 'symbolic names'
273 if re_20.match(line):
274 if re_20.match(line):
274 branchmap = {}
275 branchmap = {}
275 state = 3
276 state = 3
276
277
277 elif state == 3:
278 elif state == 3:
278 # read the symbolic names and store as tags
279 # read the symbolic names and store as tags
279 match = re_30.match(line)
280 match = re_30.match(line)
280 if match:
281 if match:
281 rev = [int(x) for x in match.group(2).split('.')]
282 rev = [int(x) for x in match.group(2).split('.')]
282
283
283 # Convert magic branch number to an odd-numbered one
284 # Convert magic branch number to an odd-numbered one
284 revn = len(rev)
285 revn = len(rev)
285 if revn > 3 and (revn % 2) == 0 and rev[-2] == 0:
286 if revn > 3 and (revn % 2) == 0 and rev[-2] == 0:
286 rev = rev[:-2] + rev[-1:]
287 rev = rev[:-2] + rev[-1:]
287 rev = tuple(rev)
288 rev = tuple(rev)
288
289
289 if rev not in tags:
290 if rev not in tags:
290 tags[rev] = []
291 tags[rev] = []
291 tags[rev].append(match.group(1))
292 tags[rev].append(match.group(1))
292 branchmap[match.group(1)] = match.group(2)
293 branchmap[match.group(1)] = match.group(2)
293
294
294 elif re_31.match(line):
295 elif re_31.match(line):
295 state = 5
296 state = 5
296 elif re_32.match(line):
297 elif re_32.match(line):
297 state = 0
298 state = 0
298
299
299 elif state == 4:
300 elif state == 4:
300 # expecting '------' separator before first revision
301 # expecting '------' separator before first revision
301 if re_31.match(line):
302 if re_31.match(line):
302 state = 5
303 state = 5
303 else:
304 else:
304 assert not re_32.match(line), _('must have at least '
305 assert not re_32.match(line), _('must have at least '
305 'some revisions')
306 'some revisions')
306
307
307 elif state == 5:
308 elif state == 5:
308 # expecting revision number and possibly (ignored) lock indication
309 # expecting revision number and possibly (ignored) lock indication
309 # we create the logentry here from values stored in states 0 to 4,
310 # we create the logentry here from values stored in states 0 to 4,
310 # as this state is re-entered for subsequent revisions of a file.
311 # as this state is re-entered for subsequent revisions of a file.
311 match = re_50.match(line)
312 match = re_50.match(line)
312 assert match, _('expected revision number')
313 assert match, _('expected revision number')
313 e = logentry(rcs=scache(rcs),
314 e = logentry(rcs=scache(rcs),
314 file=scache(filename),
315 file=scache(filename),
315 revision=tuple([int(x) for x in
316 revision=tuple([int(x) for x in
316 match.group(1).split('.')]),
317 match.group(1).split('.')]),
317 branches=[],
318 branches=[],
318 parent=None,
319 parent=None,
319 commitid=None,
320 commitid=None,
320 mergepoint=None,
321 mergepoint=None,
321 branchpoints=set())
322 branchpoints=set())
322
323
323 state = 6
324 state = 6
324
325
325 elif state == 6:
326 elif state == 6:
326 # expecting date, author, state, lines changed
327 # expecting date, author, state, lines changed
327 match = re_60.match(line)
328 match = re_60.match(line)
328 assert match, _('revision must be followed by date line')
329 assert match, _('revision must be followed by date line')
329 d = match.group(1)
330 d = match.group(1)
330 if d[2] == '/':
331 if d[2] == '/':
331 # Y2K
332 # Y2K
332 d = '19' + d
333 d = '19' + d
333
334
334 if len(d.split()) != 3:
335 if len(d.split()) != 3:
335 # cvs log dates always in GMT
336 # cvs log dates always in GMT
336 d = d + ' UTC'
337 d = d + ' UTC'
337 e.date = util.parsedate(d, ['%y/%m/%d %H:%M:%S',
338 e.date = util.parsedate(d, ['%y/%m/%d %H:%M:%S',
338 '%Y/%m/%d %H:%M:%S',
339 '%Y/%m/%d %H:%M:%S',
339 '%Y-%m-%d %H:%M:%S'])
340 '%Y-%m-%d %H:%M:%S'])
340 e.author = scache(match.group(2))
341 e.author = scache(match.group(2))
341 e.dead = match.group(3).lower() == 'dead'
342 e.dead = match.group(3).lower() == 'dead'
342
343
343 if match.group(5):
344 if match.group(5):
344 if match.group(6):
345 if match.group(6):
345 e.lines = (int(match.group(5)), int(match.group(6)))
346 e.lines = (int(match.group(5)), int(match.group(6)))
346 else:
347 else:
347 e.lines = (int(match.group(5)), 0)
348 e.lines = (int(match.group(5)), 0)
348 elif match.group(6):
349 elif match.group(6):
349 e.lines = (0, int(match.group(6)))
350 e.lines = (0, int(match.group(6)))
350 else:
351 else:
351 e.lines = None
352 e.lines = None
352
353
353 if match.group(7): # cvs 1.12 commitid
354 if match.group(7): # cvs 1.12 commitid
354 e.commitid = match.group(8)
355 e.commitid = match.group(8)
355
356
356 if match.group(9): # cvsnt mergepoint
357 if match.group(9): # cvsnt mergepoint
357 myrev = match.group(10).split('.')
358 myrev = match.group(10).split('.')
358 if len(myrev) == 2: # head
359 if len(myrev) == 2: # head
359 e.mergepoint = 'HEAD'
360 e.mergepoint = 'HEAD'
360 else:
361 else:
361 myrev = '.'.join(myrev[:-2] + ['0', myrev[-2]])
362 myrev = '.'.join(myrev[:-2] + ['0', myrev[-2]])
362 branches = [b for b in branchmap if branchmap[b] == myrev]
363 branches = [b for b in branchmap if branchmap[b] == myrev]
363 assert len(branches) == 1, ('unknown branch: %s'
364 assert len(branches) == 1, ('unknown branch: %s'
364 % e.mergepoint)
365 % e.mergepoint)
365 e.mergepoint = branches[0]
366 e.mergepoint = branches[0]
366
367
367 e.comment = []
368 e.comment = []
368 state = 7
369 state = 7
369
370
370 elif state == 7:
371 elif state == 7:
371 # read the revision numbers of branches that start at this revision
372 # read the revision numbers of branches that start at this revision
372 # or store the commit log message otherwise
373 # or store the commit log message otherwise
373 m = re_70.match(line)
374 m = re_70.match(line)
374 if m:
375 if m:
375 e.branches = [tuple([int(y) for y in x.strip().split('.')])
376 e.branches = [tuple([int(y) for y in x.strip().split('.')])
376 for x in m.group(1).split(';')]
377 for x in m.group(1).split(';')]
377 state = 8
378 state = 8
378 elif re_31.match(line) and re_50.match(peek):
379 elif re_31.match(line) and re_50.match(peek):
379 state = 5
380 state = 5
380 store = True
381 store = True
381 elif re_32.match(line):
382 elif re_32.match(line):
382 state = 0
383 state = 0
383 store = True
384 store = True
384 else:
385 else:
385 e.comment.append(line)
386 e.comment.append(line)
386
387
387 elif state == 8:
388 elif state == 8:
388 # store commit log message
389 # store commit log message
389 if re_31.match(line):
390 if re_31.match(line):
390 cpeek = peek
391 cpeek = peek
391 if cpeek.endswith('\n'):
392 if cpeek.endswith('\n'):
392 cpeek = cpeek[:-1]
393 cpeek = cpeek[:-1]
393 if re_50.match(cpeek):
394 if re_50.match(cpeek):
394 state = 5
395 state = 5
395 store = True
396 store = True
396 else:
397 else:
397 e.comment.append(line)
398 e.comment.append(line)
398 elif re_32.match(line):
399 elif re_32.match(line):
399 state = 0
400 state = 0
400 store = True
401 store = True
401 else:
402 else:
402 e.comment.append(line)
403 e.comment.append(line)
403
404
404 # When a file is added on a branch B1, CVS creates a synthetic
405 # When a file is added on a branch B1, CVS creates a synthetic
405 # dead trunk revision 1.1 so that the branch has a root.
406 # dead trunk revision 1.1 so that the branch has a root.
406 # Likewise, if you merge such a file to a later branch B2 (one
407 # Likewise, if you merge such a file to a later branch B2 (one
407 # that already existed when the file was added on B1), CVS
408 # that already existed when the file was added on B1), CVS
408 # creates a synthetic dead revision 1.1.x.1 on B2. Don't drop
409 # creates a synthetic dead revision 1.1.x.1 on B2. Don't drop
409 # these revisions now, but mark them synthetic so
410 # these revisions now, but mark them synthetic so
410 # createchangeset() can take care of them.
411 # createchangeset() can take care of them.
411 if (store and
412 if (store and
412 e.dead and
413 e.dead and
413 e.revision[-1] == 1 and # 1.1 or 1.1.x.1
414 e.revision[-1] == 1 and # 1.1 or 1.1.x.1
414 len(e.comment) == 1 and
415 len(e.comment) == 1 and
415 file_added_re.match(e.comment[0])):
416 file_added_re.match(e.comment[0])):
416 ui.debug('found synthetic revision in %s: %r\n'
417 ui.debug('found synthetic revision in %s: %r\n'
417 % (e.rcs, e.comment[0]))
418 % (e.rcs, e.comment[0]))
418 e.synthetic = True
419 e.synthetic = True
419
420
420 if store:
421 if store:
421 # clean up the results and save in the log.
422 # clean up the results and save in the log.
422 store = False
423 store = False
423 e.tags = sorted([scache(x) for x in tags.get(e.revision, [])])
424 e.tags = sorted([scache(x) for x in tags.get(e.revision, [])])
424 e.comment = scache('\n'.join(e.comment))
425 e.comment = scache('\n'.join(e.comment))
425
426
426 revn = len(e.revision)
427 revn = len(e.revision)
427 if revn > 3 and (revn % 2) == 0:
428 if revn > 3 and (revn % 2) == 0:
428 e.branch = tags.get(e.revision[:-1], [None])[0]
429 e.branch = tags.get(e.revision[:-1], [None])[0]
429 else:
430 else:
430 e.branch = None
431 e.branch = None
431
432
432 # find the branches starting from this revision
433 # find the branches starting from this revision
433 branchpoints = set()
434 branchpoints = set()
434 for branch, revision in branchmap.iteritems():
435 for branch, revision in branchmap.iteritems():
435 revparts = tuple([int(i) for i in revision.split('.')])
436 revparts = tuple([int(i) for i in revision.split('.')])
436 if len(revparts) < 2: # bad tags
437 if len(revparts) < 2: # bad tags
437 continue
438 continue
438 if revparts[-2] == 0 and revparts[-1] % 2 == 0:
439 if revparts[-2] == 0 and revparts[-1] % 2 == 0:
439 # normal branch
440 # normal branch
440 if revparts[:-2] == e.revision:
441 if revparts[:-2] == e.revision:
441 branchpoints.add(branch)
442 branchpoints.add(branch)
442 elif revparts == (1, 1, 1): # vendor branch
443 elif revparts == (1, 1, 1): # vendor branch
443 if revparts in e.branches:
444 if revparts in e.branches:
444 branchpoints.add(branch)
445 branchpoints.add(branch)
445 e.branchpoints = branchpoints
446 e.branchpoints = branchpoints
446
447
447 log.append(e)
448 log.append(e)
448
449
449 rcsmap[e.rcs.replace('/Attic/', '/')] = e.rcs
450 rcsmap[e.rcs.replace('/Attic/', '/')] = e.rcs
450
451
451 if len(log) % 100 == 0:
452 if len(log) % 100 == 0:
452 ui.status(util.ellipsis('%d %s' % (len(log), e.file), 80)+'\n')
453 ui.status(util.ellipsis('%d %s' % (len(log), e.file), 80)+'\n')
453
454
454 log.sort(key=lambda x: (x.rcs, x.revision))
455 log.sort(key=lambda x: (x.rcs, x.revision))
455
456
456 # find parent revisions of individual files
457 # find parent revisions of individual files
457 versions = {}
458 versions = {}
458 for e in sorted(oldlog, key=lambda x: (x.rcs, x.revision)):
459 for e in sorted(oldlog, key=lambda x: (x.rcs, x.revision)):
459 rcs = e.rcs.replace('/Attic/', '/')
460 rcs = e.rcs.replace('/Attic/', '/')
460 if rcs in rcsmap:
461 if rcs in rcsmap:
461 e.rcs = rcsmap[rcs]
462 e.rcs = rcsmap[rcs]
462 branch = e.revision[:-1]
463 branch = e.revision[:-1]
463 versions[(e.rcs, branch)] = e.revision
464 versions[(e.rcs, branch)] = e.revision
464
465
465 for e in log:
466 for e in log:
466 branch = e.revision[:-1]
467 branch = e.revision[:-1]
467 p = versions.get((e.rcs, branch), None)
468 p = versions.get((e.rcs, branch), None)
468 if p is None:
469 if p is None:
469 p = e.revision[:-2]
470 p = e.revision[:-2]
470 e.parent = p
471 e.parent = p
471 versions[(e.rcs, branch)] = e.revision
472 versions[(e.rcs, branch)] = e.revision
472
473
473 # update the log cache
474 # update the log cache
474 if cache:
475 if cache:
475 if log:
476 if log:
476 # join up the old and new logs
477 # join up the old and new logs
477 log.sort(key=lambda x: x.date)
478 log.sort(key=lambda x: x.date)
478
479
479 if oldlog and oldlog[-1].date >= log[0].date:
480 if oldlog and oldlog[-1].date >= log[0].date:
480 raise logerror(_('log cache overlaps with new log entries,'
481 raise logerror(_('log cache overlaps with new log entries,'
481 ' re-run without cache.'))
482 ' re-run without cache.'))
482
483
483 log = oldlog + log
484 log = oldlog + log
484
485
485 # write the new cachefile
486 # write the new cachefile
486 ui.note(_('writing cvs log cache %s\n') % cachefile)
487 ui.note(_('writing cvs log cache %s\n') % cachefile)
487 pickle.dump(log, open(cachefile, 'w'))
488 pickle.dump(log, open(cachefile, 'w'))
488 else:
489 else:
489 log = oldlog
490 log = oldlog
490
491
491 ui.status(_('%d log entries\n') % len(log))
492 ui.status(_('%d log entries\n') % len(log))
492
493
493 hook.hook(ui, None, "cvslog", True, log=log)
494 hook.hook(ui, None, "cvslog", True, log=log)
494
495
495 return log
496 return log
496
497
497
498
498 class changeset(object):
499 class changeset(object):
499 '''Class changeset has the following attributes:
500 '''Class changeset has the following attributes:
500 .id - integer identifying this changeset (list index)
501 .id - integer identifying this changeset (list index)
501 .author - author name as CVS knows it
502 .author - author name as CVS knows it
502 .branch - name of branch this changeset is on, or None
503 .branch - name of branch this changeset is on, or None
503 .comment - commit message
504 .comment - commit message
504 .commitid - CVS commitid or None
505 .commitid - CVS commitid or None
505 .date - the commit date as a (time,tz) tuple
506 .date - the commit date as a (time,tz) tuple
506 .entries - list of logentry objects in this changeset
507 .entries - list of logentry objects in this changeset
507 .parents - list of one or two parent changesets
508 .parents - list of one or two parent changesets
508 .tags - list of tags on this changeset
509 .tags - list of tags on this changeset
509 .synthetic - from synthetic revision "file ... added on branch ..."
510 .synthetic - from synthetic revision "file ... added on branch ..."
510 .mergepoint- the branch that has been merged from or None
511 .mergepoint- the branch that has been merged from or None
511 .branchpoints- the branches that start at the current entry or empty
512 .branchpoints- the branches that start at the current entry or empty
512 '''
513 '''
513 def __init__(self, **entries):
514 def __init__(self, **entries):
514 self.id = None
515 self.id = None
515 self.synthetic = False
516 self.synthetic = False
516 self.__dict__.update(entries)
517 self.__dict__.update(entries)
517
518
518 def __repr__(self):
519 def __repr__(self):
519 items = ("%s=%r"%(k, self.__dict__[k]) for k in sorted(self.__dict__))
520 items = ("%s=%r"%(k, self.__dict__[k]) for k in sorted(self.__dict__))
520 return "%s(%s)"%(type(self).__name__, ", ".join(items))
521 return "%s(%s)"%(type(self).__name__, ", ".join(items))
521
522
522 def createchangeset(ui, log, fuzz=60, mergefrom=None, mergeto=None):
523 def createchangeset(ui, log, fuzz=60, mergefrom=None, mergeto=None):
523 '''Convert log into changesets.'''
524 '''Convert log into changesets.'''
524
525
525 ui.status(_('creating changesets\n'))
526 ui.status(_('creating changesets\n'))
526
527
527 # try to order commitids by date
528 # try to order commitids by date
528 mindate = {}
529 mindate = {}
529 for e in log:
530 for e in log:
530 if e.commitid:
531 if e.commitid:
531 mindate[e.commitid] = min(e.date, mindate.get(e.commitid))
532 mindate[e.commitid] = min(e.date, mindate.get(e.commitid))
532
533
533 # Merge changesets
534 # Merge changesets
534 log.sort(key=lambda x: (mindate.get(x.commitid), x.commitid, x.comment,
535 log.sort(key=lambda x: (mindate.get(x.commitid), x.commitid, x.comment,
535 x.author, x.branch, x.date, x.branchpoints))
536 x.author, x.branch, x.date, x.branchpoints))
536
537
537 changesets = []
538 changesets = []
538 files = set()
539 files = set()
539 c = None
540 c = None
540 for i, e in enumerate(log):
541 for i, e in enumerate(log):
541
542
542 # Check if log entry belongs to the current changeset or not.
543 # Check if log entry belongs to the current changeset or not.
543
544
544 # Since CVS is file-centric, two different file revisions with
545 # Since CVS is file-centric, two different file revisions with
545 # different branchpoints should be treated as belonging to two
546 # different branchpoints should be treated as belonging to two
546 # different changesets (and the ordering is important and not
547 # different changesets (and the ordering is important and not
547 # honoured by cvsps at this point).
548 # honoured by cvsps at this point).
548 #
549 #
549 # Consider the following case:
550 # Consider the following case:
550 # foo 1.1 branchpoints: [MYBRANCH]
551 # foo 1.1 branchpoints: [MYBRANCH]
551 # bar 1.1 branchpoints: [MYBRANCH, MYBRANCH2]
552 # bar 1.1 branchpoints: [MYBRANCH, MYBRANCH2]
552 #
553 #
553 # Here foo is part only of MYBRANCH, but not MYBRANCH2, e.g. a
554 # Here foo is part only of MYBRANCH, but not MYBRANCH2, e.g. a
554 # later version of foo may be in MYBRANCH2, so foo should be the
555 # later version of foo may be in MYBRANCH2, so foo should be the
555 # first changeset and bar the next and MYBRANCH and MYBRANCH2
556 # first changeset and bar the next and MYBRANCH and MYBRANCH2
556 # should both start off of the bar changeset. No provisions are
557 # should both start off of the bar changeset. No provisions are
557 # made to ensure that this is, in fact, what happens.
558 # made to ensure that this is, in fact, what happens.
558 if not (c and e.branchpoints == c.branchpoints and
559 if not (c and e.branchpoints == c.branchpoints and
559 (# cvs commitids
560 (# cvs commitids
560 (e.commitid is not None and e.commitid == c.commitid) or
561 (e.commitid is not None and e.commitid == c.commitid) or
561 (# no commitids, use fuzzy commit detection
562 (# no commitids, use fuzzy commit detection
562 (e.commitid is None or c.commitid is None) and
563 (e.commitid is None or c.commitid is None) and
563 e.comment == c.comment and
564 e.comment == c.comment and
564 e.author == c.author and
565 e.author == c.author and
565 e.branch == c.branch and
566 e.branch == c.branch and
566 ((c.date[0] + c.date[1]) <=
567 ((c.date[0] + c.date[1]) <=
567 (e.date[0] + e.date[1]) <=
568 (e.date[0] + e.date[1]) <=
568 (c.date[0] + c.date[1]) + fuzz) and
569 (c.date[0] + c.date[1]) + fuzz) and
569 e.file not in files))):
570 e.file not in files))):
570 c = changeset(comment=e.comment, author=e.author,
571 c = changeset(comment=e.comment, author=e.author,
571 branch=e.branch, date=e.date,
572 branch=e.branch, date=e.date,
572 entries=[], mergepoint=e.mergepoint,
573 entries=[], mergepoint=e.mergepoint,
573 branchpoints=e.branchpoints, commitid=e.commitid)
574 branchpoints=e.branchpoints, commitid=e.commitid)
574 changesets.append(c)
575 changesets.append(c)
575
576
576 files = set()
577 files = set()
577 if len(changesets) % 100 == 0:
578 if len(changesets) % 100 == 0:
578 t = '%d %s' % (len(changesets), repr(e.comment)[1:-1])
579 t = '%d %s' % (len(changesets), repr(e.comment)[1:-1])
579 ui.status(util.ellipsis(t, 80) + '\n')
580 ui.status(util.ellipsis(t, 80) + '\n')
580
581
581 c.entries.append(e)
582 c.entries.append(e)
582 files.add(e.file)
583 files.add(e.file)
583 c.date = e.date # changeset date is date of latest commit in it
584 c.date = e.date # changeset date is date of latest commit in it
584
585
585 # Mark synthetic changesets
586 # Mark synthetic changesets
586
587
587 for c in changesets:
588 for c in changesets:
588 # Synthetic revisions always get their own changeset, because
589 # Synthetic revisions always get their own changeset, because
589 # the log message includes the filename. E.g. if you add file3
590 # the log message includes the filename. E.g. if you add file3
590 # and file4 on a branch, you get four log entries and three
591 # and file4 on a branch, you get four log entries and three
591 # changesets:
592 # changesets:
592 # "File file3 was added on branch ..." (synthetic, 1 entry)
593 # "File file3 was added on branch ..." (synthetic, 1 entry)
593 # "File file4 was added on branch ..." (synthetic, 1 entry)
594 # "File file4 was added on branch ..." (synthetic, 1 entry)
594 # "Add file3 and file4 to fix ..." (real, 2 entries)
595 # "Add file3 and file4 to fix ..." (real, 2 entries)
595 # Hence the check for 1 entry here.
596 # Hence the check for 1 entry here.
596 c.synthetic = len(c.entries) == 1 and c.entries[0].synthetic
597 c.synthetic = len(c.entries) == 1 and c.entries[0].synthetic
597
598
598 # Sort files in each changeset
599 # Sort files in each changeset
599
600
600 def entitycompare(l, r):
601 def entitycompare(l, r):
601 'Mimic cvsps sorting order'
602 'Mimic cvsps sorting order'
602 l = l.file.split('/')
603 l = l.file.split('/')
603 r = r.file.split('/')
604 r = r.file.split('/')
604 nl = len(l)
605 nl = len(l)
605 nr = len(r)
606 nr = len(r)
606 n = min(nl, nr)
607 n = min(nl, nr)
607 for i in range(n):
608 for i in range(n):
608 if i + 1 == nl and nl < nr:
609 if i + 1 == nl and nl < nr:
609 return -1
610 return -1
610 elif i + 1 == nr and nl > nr:
611 elif i + 1 == nr and nl > nr:
611 return +1
612 return +1
612 elif l[i] < r[i]:
613 elif l[i] < r[i]:
613 return -1
614 return -1
614 elif l[i] > r[i]:
615 elif l[i] > r[i]:
615 return +1
616 return +1
616 return 0
617 return 0
617
618
618 for c in changesets:
619 for c in changesets:
619 c.entries.sort(entitycompare)
620 c.entries.sort(entitycompare)
620
621
621 # Sort changesets by date
622 # Sort changesets by date
622
623
623 odd = set()
624 odd = set()
624 def cscmp(l, r, odd=odd):
625 def cscmp(l, r, odd=odd):
625 d = sum(l.date) - sum(r.date)
626 d = sum(l.date) - sum(r.date)
626 if d:
627 if d:
627 return d
628 return d
628
629
629 # detect vendor branches and initial commits on a branch
630 # detect vendor branches and initial commits on a branch
630 le = {}
631 le = {}
631 for e in l.entries:
632 for e in l.entries:
632 le[e.rcs] = e.revision
633 le[e.rcs] = e.revision
633 re = {}
634 re = {}
634 for e in r.entries:
635 for e in r.entries:
635 re[e.rcs] = e.revision
636 re[e.rcs] = e.revision
636
637
637 d = 0
638 d = 0
638 for e in l.entries:
639 for e in l.entries:
639 if re.get(e.rcs, None) == e.parent:
640 if re.get(e.rcs, None) == e.parent:
640 assert not d
641 assert not d
641 d = 1
642 d = 1
642 break
643 break
643
644
644 for e in r.entries:
645 for e in r.entries:
645 if le.get(e.rcs, None) == e.parent:
646 if le.get(e.rcs, None) == e.parent:
646 if d:
647 if d:
647 odd.add((l, r))
648 odd.add((l, r))
648 d = -1
649 d = -1
649 break
650 break
650 # By this point, the changesets are sufficiently compared that
651 # By this point, the changesets are sufficiently compared that
651 # we don't really care about ordering. However, this leaves
652 # we don't really care about ordering. However, this leaves
652 # some race conditions in the tests, so we compare on the
653 # some race conditions in the tests, so we compare on the
653 # number of files modified, the files contained in each
654 # number of files modified, the files contained in each
654 # changeset, and the branchpoints in the change to ensure test
655 # changeset, and the branchpoints in the change to ensure test
655 # output remains stable.
656 # output remains stable.
656
657
657 # recommended replacement for cmp from
658 # recommended replacement for cmp from
658 # https://docs.python.org/3.0/whatsnew/3.0.html
659 # https://docs.python.org/3.0/whatsnew/3.0.html
659 c = lambda x, y: (x > y) - (x < y)
660 c = lambda x, y: (x > y) - (x < y)
660 # Sort bigger changes first.
661 # Sort bigger changes first.
661 if not d:
662 if not d:
662 d = c(len(l.entries), len(r.entries))
663 d = c(len(l.entries), len(r.entries))
663 # Try sorting by filename in the change.
664 # Try sorting by filename in the change.
664 if not d:
665 if not d:
665 d = c([e.file for e in l.entries], [e.file for e in r.entries])
666 d = c([e.file for e in l.entries], [e.file for e in r.entries])
666 # Try and put changes without a branch point before ones with
667 # Try and put changes without a branch point before ones with
667 # a branch point.
668 # a branch point.
668 if not d:
669 if not d:
669 d = c(len(l.branchpoints), len(r.branchpoints))
670 d = c(len(l.branchpoints), len(r.branchpoints))
670 return d
671 return d
671
672
672 changesets.sort(cscmp)
673 changesets.sort(cscmp)
673
674
674 # Collect tags
675 # Collect tags
675
676
676 globaltags = {}
677 globaltags = {}
677 for c in changesets:
678 for c in changesets:
678 for e in c.entries:
679 for e in c.entries:
679 for tag in e.tags:
680 for tag in e.tags:
680 # remember which is the latest changeset to have this tag
681 # remember which is the latest changeset to have this tag
681 globaltags[tag] = c
682 globaltags[tag] = c
682
683
683 for c in changesets:
684 for c in changesets:
684 tags = set()
685 tags = set()
685 for e in c.entries:
686 for e in c.entries:
686 tags.update(e.tags)
687 tags.update(e.tags)
687 # remember tags only if this is the latest changeset to have it
688 # remember tags only if this is the latest changeset to have it
688 c.tags = sorted(tag for tag in tags if globaltags[tag] is c)
689 c.tags = sorted(tag for tag in tags if globaltags[tag] is c)
689
690
690 # Find parent changesets, handle {{mergetobranch BRANCHNAME}}
691 # Find parent changesets, handle {{mergetobranch BRANCHNAME}}
691 # by inserting dummy changesets with two parents, and handle
692 # by inserting dummy changesets with two parents, and handle
692 # {{mergefrombranch BRANCHNAME}} by setting two parents.
693 # {{mergefrombranch BRANCHNAME}} by setting two parents.
693
694
694 if mergeto is None:
695 if mergeto is None:
695 mergeto = r'{{mergetobranch ([-\w]+)}}'
696 mergeto = r'{{mergetobranch ([-\w]+)}}'
696 if mergeto:
697 if mergeto:
697 mergeto = re.compile(mergeto)
698 mergeto = re.compile(mergeto)
698
699
699 if mergefrom is None:
700 if mergefrom is None:
700 mergefrom = r'{{mergefrombranch ([-\w]+)}}'
701 mergefrom = r'{{mergefrombranch ([-\w]+)}}'
701 if mergefrom:
702 if mergefrom:
702 mergefrom = re.compile(mergefrom)
703 mergefrom = re.compile(mergefrom)
703
704
704 versions = {} # changeset index where we saw any particular file version
705 versions = {} # changeset index where we saw any particular file version
705 branches = {} # changeset index where we saw a branch
706 branches = {} # changeset index where we saw a branch
706 n = len(changesets)
707 n = len(changesets)
707 i = 0
708 i = 0
708 while i < n:
709 while i < n:
709 c = changesets[i]
710 c = changesets[i]
710
711
711 for f in c.entries:
712 for f in c.entries:
712 versions[(f.rcs, f.revision)] = i
713 versions[(f.rcs, f.revision)] = i
713
714
714 p = None
715 p = None
715 if c.branch in branches:
716 if c.branch in branches:
716 p = branches[c.branch]
717 p = branches[c.branch]
717 else:
718 else:
718 # first changeset on a new branch
719 # first changeset on a new branch
719 # the parent is a changeset with the branch in its
720 # the parent is a changeset with the branch in its
720 # branchpoints such that it is the latest possible
721 # branchpoints such that it is the latest possible
721 # commit without any intervening, unrelated commits.
722 # commit without any intervening, unrelated commits.
722
723
723 for candidate in xrange(i):
724 for candidate in xrange(i):
724 if c.branch not in changesets[candidate].branchpoints:
725 if c.branch not in changesets[candidate].branchpoints:
725 if p is not None:
726 if p is not None:
726 break
727 break
727 continue
728 continue
728 p = candidate
729 p = candidate
729
730
730 c.parents = []
731 c.parents = []
731 if p is not None:
732 if p is not None:
732 p = changesets[p]
733 p = changesets[p]
733
734
734 # Ensure no changeset has a synthetic changeset as a parent.
735 # Ensure no changeset has a synthetic changeset as a parent.
735 while p.synthetic:
736 while p.synthetic:
736 assert len(p.parents) <= 1, \
737 assert len(p.parents) <= 1, \
737 _('synthetic changeset cannot have multiple parents')
738 _('synthetic changeset cannot have multiple parents')
738 if p.parents:
739 if p.parents:
739 p = p.parents[0]
740 p = p.parents[0]
740 else:
741 else:
741 p = None
742 p = None
742 break
743 break
743
744
744 if p is not None:
745 if p is not None:
745 c.parents.append(p)
746 c.parents.append(p)
746
747
747 if c.mergepoint:
748 if c.mergepoint:
748 if c.mergepoint == 'HEAD':
749 if c.mergepoint == 'HEAD':
749 c.mergepoint = None
750 c.mergepoint = None
750 c.parents.append(changesets[branches[c.mergepoint]])
751 c.parents.append(changesets[branches[c.mergepoint]])
751
752
752 if mergefrom:
753 if mergefrom:
753 m = mergefrom.search(c.comment)
754 m = mergefrom.search(c.comment)
754 if m:
755 if m:
755 m = m.group(1)
756 m = m.group(1)
756 if m == 'HEAD':
757 if m == 'HEAD':
757 m = None
758 m = None
758 try:
759 try:
759 candidate = changesets[branches[m]]
760 candidate = changesets[branches[m]]
760 except KeyError:
761 except KeyError:
761 ui.warn(_("warning: CVS commit message references "
762 ui.warn(_("warning: CVS commit message references "
762 "non-existent branch %r:\n%s\n")
763 "non-existent branch %r:\n%s\n")
763 % (m, c.comment))
764 % (m, c.comment))
764 if m in branches and c.branch != m and not candidate.synthetic:
765 if m in branches and c.branch != m and not candidate.synthetic:
765 c.parents.append(candidate)
766 c.parents.append(candidate)
766
767
767 if mergeto:
768 if mergeto:
768 m = mergeto.search(c.comment)
769 m = mergeto.search(c.comment)
769 if m:
770 if m:
770 if m.groups():
771 if m.groups():
771 m = m.group(1)
772 m = m.group(1)
772 if m == 'HEAD':
773 if m == 'HEAD':
773 m = None
774 m = None
774 else:
775 else:
775 m = None # if no group found then merge to HEAD
776 m = None # if no group found then merge to HEAD
776 if m in branches and c.branch != m:
777 if m in branches and c.branch != m:
777 # insert empty changeset for merge
778 # insert empty changeset for merge
778 cc = changeset(
779 cc = changeset(
779 author=c.author, branch=m, date=c.date,
780 author=c.author, branch=m, date=c.date,
780 comment='convert-repo: CVS merge from branch %s'
781 comment='convert-repo: CVS merge from branch %s'
781 % c.branch,
782 % c.branch,
782 entries=[], tags=[],
783 entries=[], tags=[],
783 parents=[changesets[branches[m]], c])
784 parents=[changesets[branches[m]], c])
784 changesets.insert(i + 1, cc)
785 changesets.insert(i + 1, cc)
785 branches[m] = i + 1
786 branches[m] = i + 1
786
787
787 # adjust our loop counters now we have inserted a new entry
788 # adjust our loop counters now we have inserted a new entry
788 n += 1
789 n += 1
789 i += 2
790 i += 2
790 continue
791 continue
791
792
792 branches[c.branch] = i
793 branches[c.branch] = i
793 i += 1
794 i += 1
794
795
795 # Drop synthetic changesets (safe now that we have ensured no other
796 # Drop synthetic changesets (safe now that we have ensured no other
796 # changesets can have them as parents).
797 # changesets can have them as parents).
797 i = 0
798 i = 0
798 while i < len(changesets):
799 while i < len(changesets):
799 if changesets[i].synthetic:
800 if changesets[i].synthetic:
800 del changesets[i]
801 del changesets[i]
801 else:
802 else:
802 i += 1
803 i += 1
803
804
804 # Number changesets
805 # Number changesets
805
806
806 for i, c in enumerate(changesets):
807 for i, c in enumerate(changesets):
807 c.id = i + 1
808 c.id = i + 1
808
809
809 if odd:
810 if odd:
810 for l, r in odd:
811 for l, r in odd:
811 if l.id is not None and r.id is not None:
812 if l.id is not None and r.id is not None:
812 ui.warn(_('changeset %d is both before and after %d\n')
813 ui.warn(_('changeset %d is both before and after %d\n')
813 % (l.id, r.id))
814 % (l.id, r.id))
814
815
815 ui.status(_('%d changeset entries\n') % len(changesets))
816 ui.status(_('%d changeset entries\n') % len(changesets))
816
817
817 hook.hook(ui, None, "cvschangesets", True, changesets=changesets)
818 hook.hook(ui, None, "cvschangesets", True, changesets=changesets)
818
819
819 return changesets
820 return changesets
820
821
821
822
822 def debugcvsps(ui, *args, **opts):
823 def debugcvsps(ui, *args, **opts):
823 '''Read CVS rlog for current directory or named path in
824 '''Read CVS rlog for current directory or named path in
824 repository, and convert the log to changesets based on matching
825 repository, and convert the log to changesets based on matching
825 commit log entries and dates.
826 commit log entries and dates.
826 '''
827 '''
827 if opts["new_cache"]:
828 if opts["new_cache"]:
828 cache = "write"
829 cache = "write"
829 elif opts["update_cache"]:
830 elif opts["update_cache"]:
830 cache = "update"
831 cache = "update"
831 else:
832 else:
832 cache = None
833 cache = None
833
834
834 revisions = opts["revisions"]
835 revisions = opts["revisions"]
835
836
836 try:
837 try:
837 if args:
838 if args:
838 log = []
839 log = []
839 for d in args:
840 for d in args:
840 log += createlog(ui, d, root=opts["root"], cache=cache)
841 log += createlog(ui, d, root=opts["root"], cache=cache)
841 else:
842 else:
842 log = createlog(ui, root=opts["root"], cache=cache)
843 log = createlog(ui, root=opts["root"], cache=cache)
843 except logerror as e:
844 except logerror as e:
844 ui.write("%r\n"%e)
845 ui.write("%r\n"%e)
845 return
846 return
846
847
847 changesets = createchangeset(ui, log, opts["fuzz"])
848 changesets = createchangeset(ui, log, opts["fuzz"])
848 del log
849 del log
849
850
850 # Print changesets (optionally filtered)
851 # Print changesets (optionally filtered)
851
852
852 off = len(revisions)
853 off = len(revisions)
853 branches = {} # latest version number in each branch
854 branches = {} # latest version number in each branch
854 ancestors = {} # parent branch
855 ancestors = {} # parent branch
855 for cs in changesets:
856 for cs in changesets:
856
857
857 if opts["ancestors"]:
858 if opts["ancestors"]:
858 if cs.branch not in branches and cs.parents and cs.parents[0].id:
859 if cs.branch not in branches and cs.parents and cs.parents[0].id:
859 ancestors[cs.branch] = (changesets[cs.parents[0].id - 1].branch,
860 ancestors[cs.branch] = (changesets[cs.parents[0].id - 1].branch,
860 cs.parents[0].id)
861 cs.parents[0].id)
861 branches[cs.branch] = cs.id
862 branches[cs.branch] = cs.id
862
863
863 # limit by branches
864 # limit by branches
864 if opts["branches"] and (cs.branch or 'HEAD') not in opts["branches"]:
865 if opts["branches"] and (cs.branch or 'HEAD') not in opts["branches"]:
865 continue
866 continue
866
867
867 if not off:
868 if not off:
868 # Note: trailing spaces on several lines here are needed to have
869 # Note: trailing spaces on several lines here are needed to have
869 # bug-for-bug compatibility with cvsps.
870 # bug-for-bug compatibility with cvsps.
870 ui.write('---------------------\n')
871 ui.write('---------------------\n')
871 ui.write(('PatchSet %d \n' % cs.id))
872 ui.write(('PatchSet %d \n' % cs.id))
872 ui.write(('Date: %s\n' % util.datestr(cs.date,
873 ui.write(('Date: %s\n' % util.datestr(cs.date,
873 '%Y/%m/%d %H:%M:%S %1%2')))
874 '%Y/%m/%d %H:%M:%S %1%2')))
874 ui.write(('Author: %s\n' % cs.author))
875 ui.write(('Author: %s\n' % cs.author))
875 ui.write(('Branch: %s\n' % (cs.branch or 'HEAD')))
876 ui.write(('Branch: %s\n' % (cs.branch or 'HEAD')))
876 ui.write(('Tag%s: %s \n' % (['', 's'][len(cs.tags) > 1],
877 ui.write(('Tag%s: %s \n' % (['', 's'][len(cs.tags) > 1],
877 ','.join(cs.tags) or '(none)')))
878 ','.join(cs.tags) or '(none)')))
878 if cs.branchpoints:
879 if cs.branchpoints:
879 ui.write(('Branchpoints: %s \n') %
880 ui.write(('Branchpoints: %s \n') %
880 ', '.join(sorted(cs.branchpoints)))
881 ', '.join(sorted(cs.branchpoints)))
881 if opts["parents"] and cs.parents:
882 if opts["parents"] and cs.parents:
882 if len(cs.parents) > 1:
883 if len(cs.parents) > 1:
883 ui.write(('Parents: %s\n' %
884 ui.write(('Parents: %s\n' %
884 (','.join([str(p.id) for p in cs.parents]))))
885 (','.join([str(p.id) for p in cs.parents]))))
885 else:
886 else:
886 ui.write(('Parent: %d\n' % cs.parents[0].id))
887 ui.write(('Parent: %d\n' % cs.parents[0].id))
887
888
888 if opts["ancestors"]:
889 if opts["ancestors"]:
889 b = cs.branch
890 b = cs.branch
890 r = []
891 r = []
891 while b:
892 while b:
892 b, c = ancestors[b]
893 b, c = ancestors[b]
893 r.append('%s:%d:%d' % (b or "HEAD", c, branches[b]))
894 r.append('%s:%d:%d' % (b or "HEAD", c, branches[b]))
894 if r:
895 if r:
895 ui.write(('Ancestors: %s\n' % (','.join(r))))
896 ui.write(('Ancestors: %s\n' % (','.join(r))))
896
897
897 ui.write(('Log:\n'))
898 ui.write(('Log:\n'))
898 ui.write('%s\n\n' % cs.comment)
899 ui.write('%s\n\n' % cs.comment)
899 ui.write(('Members: \n'))
900 ui.write(('Members: \n'))
900 for f in cs.entries:
901 for f in cs.entries:
901 fn = f.file
902 fn = f.file
902 if fn.startswith(opts["prefix"]):
903 if fn.startswith(opts["prefix"]):
903 fn = fn[len(opts["prefix"]):]
904 fn = fn[len(opts["prefix"]):]
904 ui.write('\t%s:%s->%s%s \n' % (
905 ui.write('\t%s:%s->%s%s \n' % (
905 fn, '.'.join([str(x) for x in f.parent]) or 'INITIAL',
906 fn, '.'.join([str(x) for x in f.parent]) or 'INITIAL',
906 '.'.join([str(x) for x in f.revision]),
907 '.'.join([str(x) for x in f.revision]),
907 ['', '(DEAD)'][f.dead]))
908 ['', '(DEAD)'][f.dead]))
908 ui.write('\n')
909 ui.write('\n')
909
910
910 # have we seen the start tag?
911 # have we seen the start tag?
911 if revisions and off:
912 if revisions and off:
912 if revisions[0] == str(cs.id) or \
913 if revisions[0] == str(cs.id) or \
913 revisions[0] in cs.tags:
914 revisions[0] in cs.tags:
914 off = False
915 off = False
915
916
916 # see if we reached the end tag
917 # see if we reached the end tag
917 if len(revisions) > 1 and not off:
918 if len(revisions) > 1 and not off:
918 if revisions[1] == str(cs.id) or \
919 if revisions[1] == str(cs.id) or \
919 revisions[1] in cs.tags:
920 revisions[1] in cs.tags:
920 break
921 break
@@ -1,129 +1,131 b''
1 # logtoprocess.py - send ui.log() data to a subprocess
1 # logtoprocess.py - send ui.log() data to a subprocess
2 #
2 #
3 # Copyright 2016 Facebook, Inc.
3 # Copyright 2016 Facebook, Inc.
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 """Send ui.log() data to a subprocess (EXPERIMENTAL)
7 """Send ui.log() data to a subprocess (EXPERIMENTAL)
8
8
9 This extension lets you specify a shell command per ui.log() event,
9 This extension lets you specify a shell command per ui.log() event,
10 sending all remaining arguments to as environment variables to that command.
10 sending all remaining arguments to as environment variables to that command.
11
11
12 Each positional argument to the method results in a `MSG[N]` key in the
12 Each positional argument to the method results in a `MSG[N]` key in the
13 environment, starting at 1 (so `MSG1`, `MSG2`, etc.). Each keyword argument
13 environment, starting at 1 (so `MSG1`, `MSG2`, etc.). Each keyword argument
14 is set as a `OPT_UPPERCASE_KEY` variable (so the key is uppercased, and
14 is set as a `OPT_UPPERCASE_KEY` variable (so the key is uppercased, and
15 prefixed with `OPT_`). The original event name is passed in the `EVENT`
15 prefixed with `OPT_`). The original event name is passed in the `EVENT`
16 environment variable, and the process ID of mercurial is given in `HGPID`.
16 environment variable, and the process ID of mercurial is given in `HGPID`.
17
17
18 So given a call `ui.log('foo', 'bar', 'baz', spam='eggs'), a script configured
18 So given a call `ui.log('foo', 'bar', 'baz', spam='eggs'), a script configured
19 for the `foo` event can expect an environment with `MSG1=bar`, `MSG2=baz`, and
19 for the `foo` event can expect an environment with `MSG1=bar`, `MSG2=baz`, and
20 `OPT_SPAM=eggs`.
20 `OPT_SPAM=eggs`.
21
21
22 Scripts are configured in the `[logtoprocess]` section, each key an event name.
22 Scripts are configured in the `[logtoprocess]` section, each key an event name.
23 For example::
23 For example::
24
24
25 [logtoprocess]
25 [logtoprocess]
26 commandexception = echo "$MSG2$MSG3" > /var/log/mercurial_exceptions.log
26 commandexception = echo "$MSG2$MSG3" > /var/log/mercurial_exceptions.log
27
27
28 would log the warning message and traceback of any failed command dispatch.
28 would log the warning message and traceback of any failed command dispatch.
29
29
30 Scripts are run asynchronously as detached daemon processes; mercurial will
30 Scripts are run asynchronously as detached daemon processes; mercurial will
31 not ensure that they exit cleanly.
31 not ensure that they exit cleanly.
32
32
33 """
33 """
34
34
35 from __future__ import absolute_import
35 from __future__ import absolute_import
36
36
37 import itertools
37 import itertools
38 import os
38 import os
39 import platform
39 import platform
40 import subprocess
40 import subprocess
41 import sys
41 import sys
42
42
43 from mercurial import encoding
44
43 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
45 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
44 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
46 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
45 # be specifying the version(s) of Mercurial they are tested with, or
47 # be specifying the version(s) of Mercurial they are tested with, or
46 # leave the attribute unspecified.
48 # leave the attribute unspecified.
47 testedwith = 'ships-with-hg-core'
49 testedwith = 'ships-with-hg-core'
48
50
49 def uisetup(ui):
51 def uisetup(ui):
50 if platform.system() == 'Windows':
52 if platform.system() == 'Windows':
51 # no fork on Windows, but we can create a detached process
53 # no fork on Windows, but we can create a detached process
52 # https://msdn.microsoft.com/en-us/library/windows/desktop/ms684863.aspx
54 # https://msdn.microsoft.com/en-us/library/windows/desktop/ms684863.aspx
53 # No stdlib constant exists for this value
55 # No stdlib constant exists for this value
54 DETACHED_PROCESS = 0x00000008
56 DETACHED_PROCESS = 0x00000008
55 _creationflags = DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
57 _creationflags = DETACHED_PROCESS | subprocess.CREATE_NEW_PROCESS_GROUP
56
58
57 def runshellcommand(script, env):
59 def runshellcommand(script, env):
58 # we can't use close_fds *and* redirect stdin. I'm not sure that we
60 # we can't use close_fds *and* redirect stdin. I'm not sure that we
59 # need to because the detached process has no console connection.
61 # need to because the detached process has no console connection.
60 subprocess.Popen(
62 subprocess.Popen(
61 script, shell=True, env=env, close_fds=True,
63 script, shell=True, env=env, close_fds=True,
62 creationflags=_creationflags)
64 creationflags=_creationflags)
63 else:
65 else:
64 def runshellcommand(script, env):
66 def runshellcommand(script, env):
65 # double-fork to completely detach from the parent process
67 # double-fork to completely detach from the parent process
66 # based on http://code.activestate.com/recipes/278731
68 # based on http://code.activestate.com/recipes/278731
67 pid = os.fork()
69 pid = os.fork()
68 if pid:
70 if pid:
69 # parent
71 # parent
70 return
72 return
71 # subprocess.Popen() forks again, all we need to add is
73 # subprocess.Popen() forks again, all we need to add is
72 # flag the new process as a new session.
74 # flag the new process as a new session.
73 if sys.version_info < (3, 2):
75 if sys.version_info < (3, 2):
74 newsession = {'preexec_fn': os.setsid}
76 newsession = {'preexec_fn': os.setsid}
75 else:
77 else:
76 newsession = {'start_new_session': True}
78 newsession = {'start_new_session': True}
77 try:
79 try:
78 # connect stdin to devnull to make sure the subprocess can't
80 # connect stdin to devnull to make sure the subprocess can't
79 # muck up that stream for mercurial.
81 # muck up that stream for mercurial.
80 subprocess.Popen(
82 subprocess.Popen(
81 script, shell=True, stdin=open(os.devnull, 'r'), env=env,
83 script, shell=True, stdin=open(os.devnull, 'r'), env=env,
82 close_fds=True, **newsession)
84 close_fds=True, **newsession)
83 finally:
85 finally:
84 # mission accomplished, this child needs to exit and not
86 # mission accomplished, this child needs to exit and not
85 # continue the hg process here.
87 # continue the hg process here.
86 os._exit(0)
88 os._exit(0)
87
89
88 class logtoprocessui(ui.__class__):
90 class logtoprocessui(ui.__class__):
89 def log(self, event, *msg, **opts):
91 def log(self, event, *msg, **opts):
90 """Map log events to external commands
92 """Map log events to external commands
91
93
92 Arguments are passed on as environment variables.
94 Arguments are passed on as environment variables.
93
95
94 """
96 """
95 script = self.config('logtoprocess', event)
97 script = self.config('logtoprocess', event)
96 if script:
98 if script:
97 if msg:
99 if msg:
98 # try to format the log message given the remaining
100 # try to format the log message given the remaining
99 # arguments
101 # arguments
100 try:
102 try:
101 # Python string formatting with % either uses a
103 # Python string formatting with % either uses a
102 # dictionary *or* tuple, but not both. If we have
104 # dictionary *or* tuple, but not both. If we have
103 # keyword options, assume we need a mapping.
105 # keyword options, assume we need a mapping.
104 formatted = msg[0] % (opts or msg[1:])
106 formatted = msg[0] % (opts or msg[1:])
105 except (TypeError, KeyError):
107 except (TypeError, KeyError):
106 # Failed to apply the arguments, ignore
108 # Failed to apply the arguments, ignore
107 formatted = msg[0]
109 formatted = msg[0]
108 messages = (formatted,) + msg[1:]
110 messages = (formatted,) + msg[1:]
109 else:
111 else:
110 messages = msg
112 messages = msg
111 # positional arguments are listed as MSG[N] keys in the
113 # positional arguments are listed as MSG[N] keys in the
112 # environment
114 # environment
113 msgpairs = (
115 msgpairs = (
114 ('MSG{0:d}'.format(i), str(m))
116 ('MSG{0:d}'.format(i), str(m))
115 for i, m in enumerate(messages, 1))
117 for i, m in enumerate(messages, 1))
116 # keyword arguments get prefixed with OPT_ and uppercased
118 # keyword arguments get prefixed with OPT_ and uppercased
117 optpairs = (
119 optpairs = (
118 ('OPT_{0}'.format(key.upper()), str(value))
120 ('OPT_{0}'.format(key.upper()), str(value))
119 for key, value in opts.iteritems())
121 for key, value in opts.iteritems())
120 env = dict(itertools.chain(os.environ.items(),
122 env = dict(itertools.chain(encoding.environ.items(),
121 msgpairs, optpairs),
123 msgpairs, optpairs),
122 EVENT=event, HGPID=str(os.getpid()))
124 EVENT=event, HGPID=str(os.getpid()))
123 # Connect stdin to /dev/null to prevent child processes messing
125 # Connect stdin to /dev/null to prevent child processes messing
124 # with mercurial's stdin.
126 # with mercurial's stdin.
125 runshellcommand(script, env)
127 runshellcommand(script, env)
126 return super(logtoprocessui, self).log(event, *msg, **opts)
128 return super(logtoprocessui, self).log(event, *msg, **opts)
127
129
128 # Replace the class for this instance and all clones created from it:
130 # Replace the class for this instance and all clones created from it:
129 ui.__class__ = logtoprocessui
131 ui.__class__ = logtoprocessui
General Comments 0
You need to be logged in to leave comments. Login now