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