##// END OF EJS Templates
fsmonitor: dependencies for new experimental extension...
Martijn Pieters -
r28432:2377c4ac default
parent child Browse files
Show More
This diff has been collapsed as it changes many lines, (779 lines changed) Show them Hide them
@@ -0,0 +1,779 b''
1 # Copyright 2014-present Facebook, Inc.
2 # All rights reserved.
3 #
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are met:
6 #
7 # * Redistributions of source code must retain the above copyright notice,
8 # this list of conditions and the following disclaimer.
9 #
10 # * Redistributions in binary form must reproduce the above copyright notice,
11 # this list of conditions and the following disclaimer in the documentation
12 # and/or other materials provided with the distribution.
13 #
14 # * Neither the name Facebook nor the names of its contributors may be used to
15 # endorse or promote products derived from this software without specific
16 # prior written permission.
17 #
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22 # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24 # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26 # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29 import os
30 import errno
31 import math
32 import socket
33 import subprocess
34 import time
35
36 # Sometimes it's really hard to get Python extensions to compile,
37 # so fall back to a pure Python implementation.
38 try:
39 import bser
40 except ImportError:
41 import pybser as bser
42
43 import capabilities
44
45 if os.name == 'nt':
46 import ctypes
47 import ctypes.wintypes
48
49 wintypes = ctypes.wintypes
50 GENERIC_READ = 0x80000000
51 GENERIC_WRITE = 0x40000000
52 FILE_FLAG_OVERLAPPED = 0x40000000
53 OPEN_EXISTING = 3
54 INVALID_HANDLE_VALUE = -1
55 FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000
56 FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100
57 FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200
58 WAIT_TIMEOUT = 0x00000102
59 WAIT_OBJECT_0 = 0x00000000
60 ERROR_IO_PENDING = 997
61
62 class OVERLAPPED(ctypes.Structure):
63 _fields_ = [
64 ("Internal", wintypes.ULONG), ("InternalHigh", wintypes.ULONG),
65 ("Offset", wintypes.DWORD), ("OffsetHigh", wintypes.DWORD),
66 ("hEvent", wintypes.HANDLE)
67 ]
68
69 def __init__(self):
70 self.Offset = 0
71 self.OffsetHigh = 0
72 self.hEvent = 0
73
74 LPDWORD = ctypes.POINTER(wintypes.DWORD)
75
76 CreateFile = ctypes.windll.kernel32.CreateFileA
77 CreateFile.argtypes = [wintypes.LPSTR, wintypes.DWORD, wintypes.DWORD,
78 wintypes.LPVOID, wintypes.DWORD, wintypes.DWORD,
79 wintypes.HANDLE]
80 CreateFile.restype = wintypes.HANDLE
81
82 CloseHandle = ctypes.windll.kernel32.CloseHandle
83 CloseHandle.argtypes = [wintypes.HANDLE]
84 CloseHandle.restype = wintypes.BOOL
85
86 ReadFile = ctypes.windll.kernel32.ReadFile
87 ReadFile.argtypes = [wintypes.HANDLE, wintypes.LPVOID, wintypes.DWORD,
88 LPDWORD, ctypes.POINTER(OVERLAPPED)]
89 ReadFile.restype = wintypes.BOOL
90
91 WriteFile = ctypes.windll.kernel32.WriteFile
92 WriteFile.argtypes = [wintypes.HANDLE, wintypes.LPVOID, wintypes.DWORD,
93 LPDWORD, ctypes.POINTER(OVERLAPPED)]
94 WriteFile.restype = wintypes.BOOL
95
96 GetLastError = ctypes.windll.kernel32.GetLastError
97 GetLastError.argtypes = []
98 GetLastError.restype = wintypes.DWORD
99
100 FormatMessage = ctypes.windll.kernel32.FormatMessageA
101 FormatMessage.argtypes = [wintypes.DWORD, wintypes.LPVOID, wintypes.DWORD,
102 wintypes.DWORD, ctypes.POINTER(wintypes.LPSTR),
103 wintypes.DWORD, wintypes.LPVOID]
104 FormatMessage.restype = wintypes.DWORD
105
106 LocalFree = ctypes.windll.kernel32.LocalFree
107
108 GetOverlappedResultEx = ctypes.windll.kernel32.GetOverlappedResultEx
109 GetOverlappedResultEx.argtypes = [wintypes.HANDLE,
110 ctypes.POINTER(OVERLAPPED), LPDWORD,
111 wintypes.DWORD, wintypes.BOOL]
112 GetOverlappedResultEx.restype = wintypes.BOOL
113
114 CancelIoEx = ctypes.windll.kernel32.CancelIoEx
115 CancelIoEx.argtypes = [wintypes.HANDLE, ctypes.POINTER(OVERLAPPED)]
116 CancelIoEx.restype = wintypes.BOOL
117
118 # 2 bytes marker, 1 byte int size, 8 bytes int64 value
119 sniff_len = 13
120
121 # This is a helper for debugging the client.
122 _debugging = False
123 if _debugging:
124
125 def log(fmt, *args):
126 print('[%s] %s' %
127 (time.strftime("%a, %d %b %Y %H:%M:%S", time.gmtime()),
128 fmt % args[:]))
129 else:
130
131 def log(fmt, *args):
132 pass
133
134
135 class WatchmanError(Exception):
136 pass
137
138
139 class SocketTimeout(WatchmanError):
140 """A specialized exception raised for socket timeouts during communication to/from watchman.
141 This makes it easier to implement non-blocking loops as callers can easily distinguish
142 between a routine timeout and an actual error condition.
143
144 Note that catching WatchmanError will also catch this as it is a super-class, so backwards
145 compatibility in exception handling is preserved.
146 """
147
148
149 class CommandError(WatchmanError):
150 """error returned by watchman
151
152 self.msg is the message returned by watchman.
153 """
154
155 def __init__(self, msg, cmd=None):
156 self.msg = msg
157 self.cmd = cmd
158 super(CommandError, self).__init__('watchman command error: %s' % msg)
159
160 def setCommand(self, cmd):
161 self.cmd = cmd
162
163 def __str__(self):
164 if self.cmd:
165 return '%s, while executing %s' % (self.msg, self.cmd)
166 return self.msg
167
168
169 class Transport(object):
170 """ communication transport to the watchman server """
171 buf = None
172
173 def close(self):
174 """ tear it down """
175 raise NotImplementedError()
176
177 def readBytes(self, size):
178 """ read size bytes """
179 raise NotImplementedError()
180
181 def write(self, buf):
182 """ write some data """
183 raise NotImplementedError()
184
185 def setTimeout(self, value):
186 pass
187
188 def readLine(self):
189 """ read a line
190 Maintains its own buffer, callers of the transport should not mix
191 calls to readBytes and readLine.
192 """
193 if self.buf is None:
194 self.buf = []
195
196 # Buffer may already have a line if we've received unilateral
197 # response(s) from the server
198 if len(self.buf) == 1 and "\n" in self.buf[0]:
199 (line, b) = self.buf[0].split("\n", 1)
200 self.buf = [b]
201 return line
202
203 while True:
204 b = self.readBytes(4096)
205 if "\n" in b:
206 result = ''.join(self.buf)
207 (line, b) = b.split("\n", 1)
208 self.buf = [b]
209 return result + line
210 self.buf.append(b)
211
212
213 class Codec(object):
214 """ communication encoding for the watchman server """
215 transport = None
216
217 def __init__(self, transport):
218 self.transport = transport
219
220 def receive(self):
221 raise NotImplementedError()
222
223 def send(self, *args):
224 raise NotImplementedError()
225
226 def setTimeout(self, value):
227 self.transport.setTimeout(value)
228
229
230 class UnixSocketTransport(Transport):
231 """ local unix domain socket transport """
232 sock = None
233
234 def __init__(self, sockpath, timeout):
235 self.sockpath = sockpath
236 self.timeout = timeout
237
238 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
239 try:
240 sock.settimeout(self.timeout)
241 sock.connect(self.sockpath)
242 self.sock = sock
243 except socket.error as e:
244 raise WatchmanError('unable to connect to %s: %s' %
245 (self.sockpath, e))
246
247 def close(self):
248 self.sock.close()
249 self.sock = None
250
251 def setTimeout(self, value):
252 self.timeout = value
253 self.sock.settimeout(self.timeout)
254
255 def readBytes(self, size):
256 try:
257 buf = [self.sock.recv(size)]
258 if not buf[0]:
259 raise WatchmanError('empty watchman response')
260 return buf[0]
261 except socket.timeout:
262 raise SocketTimeout('timed out waiting for response')
263
264 def write(self, data):
265 try:
266 self.sock.sendall(data)
267 except socket.timeout:
268 raise SocketTimeout('timed out sending query command')
269
270
271 class WindowsNamedPipeTransport(Transport):
272 """ connect to a named pipe """
273
274 def __init__(self, sockpath, timeout):
275 self.sockpath = sockpath
276 self.timeout = int(math.ceil(timeout * 1000))
277 self._iobuf = None
278
279 self.pipe = CreateFile(sockpath, GENERIC_READ | GENERIC_WRITE, 0, None,
280 OPEN_EXISTING, FILE_FLAG_OVERLAPPED, None)
281
282 if self.pipe == INVALID_HANDLE_VALUE:
283 self.pipe = None
284 self._raise_win_err('failed to open pipe %s' % sockpath,
285 GetLastError())
286
287 def _win32_strerror(self, err):
288 """ expand a win32 error code into a human readable message """
289
290 # FormatMessage will allocate memory and assign it here
291 buf = ctypes.c_char_p()
292 FormatMessage(
293 FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ALLOCATE_BUFFER
294 | FORMAT_MESSAGE_IGNORE_INSERTS, None, err, 0, buf, 0, None)
295 try:
296 return buf.value
297 finally:
298 LocalFree(buf)
299
300 def _raise_win_err(self, msg, err):
301 raise IOError('%s win32 error code: %d %s' %
302 (msg, err, self._win32_strerror(err)))
303
304 def close(self):
305 if self.pipe:
306 CloseHandle(self.pipe)
307 self.pipe = None
308
309 def readBytes(self, size):
310 """ A read can block for an unbounded amount of time, even if the
311 kernel reports that the pipe handle is signalled, so we need to
312 always perform our reads asynchronously
313 """
314
315 # try to satisfy the read from any buffered data
316 if self._iobuf:
317 if size >= len(self._iobuf):
318 res = self._iobuf
319 self.buf = None
320 return res
321 res = self._iobuf[:size]
322 self._iobuf = self._iobuf[size:]
323 return res
324
325 # We need to initiate a read
326 buf = ctypes.create_string_buffer(size)
327 olap = OVERLAPPED()
328
329 log('made read buff of size %d', size)
330
331 # ReadFile docs warn against sending in the nread parameter for async
332 # operations, so we always collect it via GetOverlappedResultEx
333 immediate = ReadFile(self.pipe, buf, size, None, olap)
334
335 if not immediate:
336 err = GetLastError()
337 if err != ERROR_IO_PENDING:
338 self._raise_win_err('failed to read %d bytes' % size,
339 GetLastError())
340
341 nread = wintypes.DWORD()
342 if not GetOverlappedResultEx(self.pipe, olap, nread,
343 0 if immediate else self.timeout, True):
344 err = GetLastError()
345 CancelIoEx(self.pipe, olap)
346
347 if err == WAIT_TIMEOUT:
348 log('GetOverlappedResultEx timedout')
349 raise SocketTimeout('timed out after waiting %dms for read' %
350 self.timeout)
351
352 log('GetOverlappedResultEx reports error %d', err)
353 self._raise_win_err('error while waiting for read', err)
354
355 nread = nread.value
356 if nread == 0:
357 # Docs say that named pipes return 0 byte when the other end did
358 # a zero byte write. Since we don't ever do that, the only
359 # other way this shows up is if the client has gotten in a weird
360 # state, so let's bail out
361 CancelIoEx(self.pipe, olap)
362 raise IOError('Async read yielded 0 bytes; unpossible!')
363
364 # Holds precisely the bytes that we read from the prior request
365 buf = buf[:nread]
366
367 returned_size = min(nread, size)
368 if returned_size == nread:
369 return buf
370
371 # keep any left-overs around for a later read to consume
372 self._iobuf = buf[returned_size:]
373 return buf[:returned_size]
374
375 def write(self, data):
376 olap = OVERLAPPED()
377 immediate = WriteFile(self.pipe, ctypes.c_char_p(data), len(data),
378 None, olap)
379
380 if not immediate:
381 err = GetLastError()
382 if err != ERROR_IO_PENDING:
383 self._raise_win_err('failed to write %d bytes' % len(data),
384 GetLastError())
385
386 # Obtain results, waiting if needed
387 nwrote = wintypes.DWORD()
388 if GetOverlappedResultEx(self.pipe, olap, nwrote, 0 if immediate else
389 self.timeout, True):
390 return nwrote.value
391
392 err = GetLastError()
393
394 # It's potentially unsafe to allow the write to continue after
395 # we unwind, so let's make a best effort to avoid that happening
396 CancelIoEx(self.pipe, olap)
397
398 if err == WAIT_TIMEOUT:
399 raise SocketTimeout('timed out after waiting %dms for write' %
400 self.timeout)
401 self._raise_win_err('error while waiting for write of %d bytes' %
402 len(data), err)
403
404
405 class CLIProcessTransport(Transport):
406 """ open a pipe to the cli to talk to the service
407 This intended to be used only in the test harness!
408
409 The CLI is an oddball because we only support JSON input
410 and cannot send multiple commands through the same instance,
411 so we spawn a new process for each command.
412
413 We disable server spawning for this implementation, again, because
414 it is intended to be used only in our test harness. You really
415 should not need to use the CLI transport for anything real.
416
417 While the CLI can output in BSER, our Transport interface doesn't
418 support telling this instance that it should do so. That effectively
419 limits this implementation to JSON input and output only at this time.
420
421 It is the responsibility of the caller to set the send and
422 receive codecs appropriately.
423 """
424 proc = None
425 closed = True
426
427 def __init__(self, sockpath, timeout):
428 self.sockpath = sockpath
429 self.timeout = timeout
430
431 def close(self):
432 if self.proc:
433 self.proc.kill()
434 self.proc = None
435
436 def _connect(self):
437 if self.proc:
438 return self.proc
439 args = [
440 'watchman',
441 '--sockname={}'.format(self.sockpath),
442 '--logfile=/BOGUS',
443 '--statefile=/BOGUS',
444 '--no-spawn',
445 '--no-local',
446 '--no-pretty',
447 '-j',
448 ]
449 self.proc = subprocess.Popen(args,
450 stdin=subprocess.PIPE,
451 stdout=subprocess.PIPE)
452 return self.proc
453
454 def readBytes(self, size):
455 self._connect()
456 res = self.proc.stdout.read(size)
457 if res == '':
458 raise WatchmanError('EOF on CLI process transport')
459 return res
460
461 def write(self, data):
462 if self.closed:
463 self.closed = False
464 self.proc = None
465 self._connect()
466 res = self.proc.stdin.write(data)
467 self.proc.stdin.close()
468 self.closed = True
469 return res
470
471
472 class BserCodec(Codec):
473 """ use the BSER encoding. This is the default, preferred codec """
474
475 def _loads(self, response):
476 return bser.loads(response)
477
478 def receive(self):
479 buf = [self.transport.readBytes(sniff_len)]
480 if not buf[0]:
481 raise WatchmanError('empty watchman response')
482
483 elen = bser.pdu_len(buf[0])
484
485 rlen = len(buf[0])
486 while elen > rlen:
487 buf.append(self.transport.readBytes(elen - rlen))
488 rlen += len(buf[-1])
489
490 response = ''.join(buf)
491 try:
492 res = self._loads(response)
493 return res
494 except ValueError as e:
495 raise WatchmanError('watchman response decode error: %s' % e)
496
497 def send(self, *args):
498 cmd = bser.dumps(*args)
499 self.transport.write(cmd)
500
501
502 class ImmutableBserCodec(BserCodec):
503 """ use the BSER encoding, decoding values using the newer
504 immutable object support """
505
506 def _loads(self, response):
507 return bser.loads(response, False)
508
509
510 class JsonCodec(Codec):
511 """ Use json codec. This is here primarily for testing purposes """
512 json = None
513
514 def __init__(self, transport):
515 super(JsonCodec, self).__init__(transport)
516 # optional dep on json, only if JsonCodec is used
517 import json
518 self.json = json
519
520 def receive(self):
521 line = self.transport.readLine()
522 try:
523 return self.json.loads(line)
524 except Exception as e:
525 print(e, line)
526 raise
527
528 def send(self, *args):
529 cmd = self.json.dumps(*args)
530 self.transport.write(cmd + "\n")
531
532
533 class client(object):
534 """ Handles the communication with the watchman service """
535 sockpath = None
536 transport = None
537 sendCodec = None
538 recvCodec = None
539 sendConn = None
540 recvConn = None
541 subs = {} # Keyed by subscription name
542 sub_by_root = {} # Keyed by root, then by subscription name
543 logs = [] # When log level is raised
544 unilateral = ['log', 'subscription']
545 tport = None
546 useImmutableBser = None
547
548 def __init__(self,
549 sockpath=None,
550 timeout=1.0,
551 transport=None,
552 sendEncoding=None,
553 recvEncoding=None,
554 useImmutableBser=False):
555 self.sockpath = sockpath
556 self.timeout = timeout
557 self.useImmutableBser = useImmutableBser
558
559 transport = transport or os.getenv('WATCHMAN_TRANSPORT') or 'local'
560 if transport == 'local' and os.name == 'nt':
561 self.transport = WindowsNamedPipeTransport
562 elif transport == 'local':
563 self.transport = UnixSocketTransport
564 elif transport == 'cli':
565 self.transport = CLIProcessTransport
566 if sendEncoding is None:
567 sendEncoding = 'json'
568 if recvEncoding is None:
569 recvEncoding = sendEncoding
570 else:
571 raise WatchmanError('invalid transport %s' % transport)
572
573 sendEncoding = sendEncoding or os.getenv('WATCHMAN_ENCODING') or 'bser'
574 recvEncoding = recvEncoding or os.getenv('WATCHMAN_ENCODING') or 'bser'
575
576 self.recvCodec = self._parseEncoding(recvEncoding)
577 self.sendCodec = self._parseEncoding(sendEncoding)
578
579 def _parseEncoding(self, enc):
580 if enc == 'bser':
581 if self.useImmutableBser:
582 return ImmutableBserCodec
583 return BserCodec
584 elif enc == 'json':
585 return JsonCodec
586 else:
587 raise WatchmanError('invalid encoding %s' % enc)
588
589 def _hasprop(self, result, name):
590 if self.useImmutableBser:
591 return hasattr(result, name)
592 return name in result
593
594 def _resolvesockname(self):
595 # if invoked via a trigger, watchman will set this env var; we
596 # should use it unless explicitly set otherwise
597 path = os.getenv('WATCHMAN_SOCK')
598 if path:
599 return path
600
601 cmd = ['watchman', '--output-encoding=bser', 'get-sockname']
602 try:
603 p = subprocess.Popen(cmd,
604 stdout=subprocess.PIPE,
605 stderr=subprocess.PIPE,
606 close_fds=os.name != 'nt')
607 except OSError as e:
608 raise WatchmanError('"watchman" executable not in PATH (%s)', e)
609
610 stdout, stderr = p.communicate()
611 exitcode = p.poll()
612
613 if exitcode:
614 raise WatchmanError("watchman exited with code %d" % exitcode)
615
616 result = bser.loads(stdout)
617 if 'error' in result:
618 raise WatchmanError('get-sockname error: %s' % result['error'])
619
620 return result['sockname']
621
622 def _connect(self):
623 """ establish transport connection """
624
625 if self.recvConn:
626 return
627
628 if self.sockpath is None:
629 self.sockpath = self._resolvesockname()
630
631 self.tport = self.transport(self.sockpath, self.timeout)
632 self.sendConn = self.sendCodec(self.tport)
633 self.recvConn = self.recvCodec(self.tport)
634
635 def __del__(self):
636 self.close()
637
638 def close(self):
639 if self.tport:
640 self.tport.close()
641 self.tport = None
642 self.recvConn = None
643 self.sendConn = None
644
645 def receive(self):
646 """ receive the next PDU from the watchman service
647
648 If the client has activated subscriptions or logs then
649 this PDU may be a unilateral PDU sent by the service to
650 inform the client of a log event or subscription change.
651
652 It may also simply be the response portion of a request
653 initiated by query.
654
655 There are clients in production that subscribe and call
656 this in a loop to retrieve all subscription responses,
657 so care should be taken when making changes here.
658 """
659
660 self._connect()
661 result = self.recvConn.receive()
662 if self._hasprop(result, 'error'):
663 raise CommandError(result['error'])
664
665 if self._hasprop(result, 'log'):
666 self.logs.append(result['log'])
667
668 if self._hasprop(result, 'subscription'):
669 sub = result['subscription']
670 if not (sub in self.subs):
671 self.subs[sub] = []
672 self.subs[sub].append(result)
673
674 # also accumulate in {root,sub} keyed store
675 root = os.path.normcase(result['root'])
676 if not root in self.sub_by_root:
677 self.sub_by_root[root] = {}
678 if not sub in self.sub_by_root[root]:
679 self.sub_by_root[root][sub] = []
680 self.sub_by_root[root][sub].append(result)
681
682 return result
683
684 def isUnilateralResponse(self, res):
685 for k in self.unilateral:
686 if k in res:
687 return True
688 return False
689
690 def getLog(self, remove=True):
691 """ Retrieve buffered log data
692
693 If remove is true the data will be removed from the buffer.
694 Otherwise it will be left in the buffer
695 """
696 res = self.logs
697 if remove:
698 self.logs = []
699 return res
700
701 def getSubscription(self, name, remove=True, root=None):
702 """ Retrieve the data associated with a named subscription
703
704 If remove is True (the default), the subscription data is removed
705 from the buffer. Otherwise the data is returned but left in
706 the buffer.
707
708 Returns None if there is no data associated with `name`
709
710 If root is not None, then only return the subscription
711 data that matches both root and name. When used in this way,
712 remove processing impacts both the unscoped and scoped stores
713 for the subscription data.
714 """
715
716 if root is not None:
717 if not root in self.sub_by_root:
718 return None
719 if not name in self.sub_by_root[root]:
720 return None
721 sub = self.sub_by_root[root][name]
722 if remove:
723 del self.sub_by_root[root][name]
724 # don't let this grow unbounded
725 if name in self.subs:
726 del self.subs[name]
727 return sub
728
729 if not (name in self.subs):
730 return None
731 sub = self.subs[name]
732 if remove:
733 del self.subs[name]
734 return sub
735
736 def query(self, *args):
737 """ Send a query to the watchman service and return the response
738
739 This call will block until the response is returned.
740 If any unilateral responses are sent by the service in between
741 the request-response they will be buffered up in the client object
742 and NOT returned via this method.
743 """
744
745 log('calling client.query')
746 self._connect()
747 try:
748 self.sendConn.send(args)
749
750 res = self.receive()
751 while self.isUnilateralResponse(res):
752 res = self.receive()
753
754 return res
755 except CommandError as ex:
756 ex.setCommand(args)
757 raise ex
758
759 def capabilityCheck(self, optional=None, required=None):
760 """ Perform a server capability check """
761 res = self.query('version', {
762 'optional': optional or [],
763 'required': required or []
764 })
765
766 if not self._hasprop(res, 'capabilities'):
767 # Server doesn't support capabilities, so we need to
768 # synthesize the results based on the version
769 capabilities.synthesize(res, optional)
770 if 'error' in res:
771 raise CommandError(res['error'])
772
773 return res
774
775 def setTimeout(self, value):
776 self.recvConn.setTimeout(value)
777 self.sendConn.setTimeout(value)
778
779 # no-check-code -- this is a 3rd party library
This diff has been collapsed as it changes many lines, (950 lines changed) Show them Hide them
@@ -0,0 +1,950 b''
1 /*
2 Copyright (c) 2013-2015, Facebook, Inc.
3 All rights reserved.
4
5 Redistribution and use in source and binary forms, with or without
6 modification, are permitted provided that the following conditions are met:
7
8 * Redistributions of source code must retain the above copyright notice,
9 this list of conditions and the following disclaimer.
10
11 * Redistributions in binary form must reproduce the above copyright notice,
12 this list of conditions and the following disclaimer in the documentation
13 and/or other materials provided with the distribution.
14
15 * Neither the name Facebook nor the names of its contributors may be used to
16 endorse or promote products derived from this software without specific
17 prior written permission.
18
19 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23 FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24 DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25 SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26 CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27 OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30
31 #include <Python.h>
32 #ifdef _MSC_VER
33 #define inline __inline
34 #include <stdint.h>
35 #endif
36
37 /* Return the smallest size int that can store the value */
38 #define INT_SIZE(x) (((x) == ((int8_t)x)) ? 1 : \
39 ((x) == ((int16_t)x)) ? 2 : \
40 ((x) == ((int32_t)x)) ? 4 : 8)
41
42 #define BSER_ARRAY 0x00
43 #define BSER_OBJECT 0x01
44 #define BSER_STRING 0x02
45 #define BSER_INT8 0x03
46 #define BSER_INT16 0x04
47 #define BSER_INT32 0x05
48 #define BSER_INT64 0x06
49 #define BSER_REAL 0x07
50 #define BSER_TRUE 0x08
51 #define BSER_FALSE 0x09
52 #define BSER_NULL 0x0a
53 #define BSER_TEMPLATE 0x0b
54 #define BSER_SKIP 0x0c
55
56 // An immutable object representation of BSER_OBJECT.
57 // Rather than build a hash table, key -> value are obtained
58 // by walking the list of keys to determine the offset into
59 // the values array. The assumption is that the number of
60 // array elements will be typically small (~6 for the top
61 // level query result and typically 3-5 for the file entries)
62 // so that the time overhead for this is small compared to
63 // using a proper hash table. Even with this simplistic
64 // approach, this is still faster for the mercurial use case
65 // as it helps to eliminate creating N other objects to
66 // represent the stat information in the hgwatchman extension
67 typedef struct {
68 PyObject_HEAD
69 PyObject *keys; // tuple of field names
70 PyObject *values; // tuple of values
71 } bserObject;
72
73 static Py_ssize_t bserobj_tuple_length(PyObject *o) {
74 bserObject *obj = (bserObject*)o;
75
76 return PySequence_Length(obj->keys);
77 }
78
79 static PyObject *bserobj_tuple_item(PyObject *o, Py_ssize_t i) {
80 bserObject *obj = (bserObject*)o;
81
82 return PySequence_GetItem(obj->values, i);
83 }
84
85 static PySequenceMethods bserobj_sq = {
86 bserobj_tuple_length, /* sq_length */
87 0, /* sq_concat */
88 0, /* sq_repeat */
89 bserobj_tuple_item, /* sq_item */
90 0, /* sq_ass_item */
91 0, /* sq_contains */
92 0, /* sq_inplace_concat */
93 0 /* sq_inplace_repeat */
94 };
95
96 static void bserobj_dealloc(PyObject *o) {
97 bserObject *obj = (bserObject*)o;
98
99 Py_CLEAR(obj->keys);
100 Py_CLEAR(obj->values);
101 PyObject_Del(o);
102 }
103
104 static PyObject *bserobj_getattrro(PyObject *o, PyObject *name) {
105 bserObject *obj = (bserObject*)o;
106 Py_ssize_t i, n;
107 const char *namestr;
108
109 if (PyIndex_Check(name)) {
110 i = PyNumber_AsSsize_t(name, PyExc_IndexError);
111 if (i == -1 && PyErr_Occurred()) {
112 return NULL;
113 }
114 return PySequence_GetItem(obj->values, i);
115 }
116
117 // hack^Wfeature to allow mercurial to use "st_size" to reference "size"
118 namestr = PyString_AsString(name);
119 if (!strncmp(namestr, "st_", 3)) {
120 namestr += 3;
121 }
122
123 n = PyTuple_GET_SIZE(obj->keys);
124 for (i = 0; i < n; i++) {
125 const char *item_name = NULL;
126 PyObject *key = PyTuple_GET_ITEM(obj->keys, i);
127
128 item_name = PyString_AsString(key);
129 if (!strcmp(item_name, namestr)) {
130 return PySequence_GetItem(obj->values, i);
131 }
132 }
133 PyErr_Format(PyExc_AttributeError,
134 "bserobject has no attribute '%.400s'", namestr);
135 return NULL;
136 }
137
138 static PyMappingMethods bserobj_map = {
139 bserobj_tuple_length, /* mp_length */
140 bserobj_getattrro, /* mp_subscript */
141 0 /* mp_ass_subscript */
142 };
143
144 PyTypeObject bserObjectType = {
145 PyVarObject_HEAD_INIT(NULL, 0)
146 "bserobj_tuple", /* tp_name */
147 sizeof(bserObject), /* tp_basicsize */
148 0, /* tp_itemsize */
149 bserobj_dealloc, /* tp_dealloc */
150 0, /* tp_print */
151 0, /* tp_getattr */
152 0, /* tp_setattr */
153 0, /* tp_compare */
154 0, /* tp_repr */
155 0, /* tp_as_number */
156 &bserobj_sq, /* tp_as_sequence */
157 &bserobj_map, /* tp_as_mapping */
158 0, /* tp_hash */
159 0, /* tp_call */
160 0, /* tp_str */
161 bserobj_getattrro, /* tp_getattro */
162 0, /* tp_setattro */
163 0, /* tp_as_buffer */
164 Py_TPFLAGS_DEFAULT, /* tp_flags */
165 "bserobj tuple", /* tp_doc */
166 0, /* tp_traverse */
167 0, /* tp_clear */
168 0, /* tp_richcompare */
169 0, /* tp_weaklistoffset */
170 0, /* tp_iter */
171 0, /* tp_iternext */
172 0, /* tp_methods */
173 0, /* tp_members */
174 0, /* tp_getset */
175 0, /* tp_base */
176 0, /* tp_dict */
177 0, /* tp_descr_get */
178 0, /* tp_descr_set */
179 0, /* tp_dictoffset */
180 0, /* tp_init */
181 0, /* tp_alloc */
182 0, /* tp_new */
183 };
184
185
186 static PyObject *bser_loads_recursive(const char **ptr, const char *end,
187 int mutable);
188
189 static const char bser_true = BSER_TRUE;
190 static const char bser_false = BSER_FALSE;
191 static const char bser_null = BSER_NULL;
192 static const char bser_string_hdr = BSER_STRING;
193 static const char bser_array_hdr = BSER_ARRAY;
194 static const char bser_object_hdr = BSER_OBJECT;
195
196 static inline uint32_t next_power_2(uint32_t n)
197 {
198 n |= (n >> 16);
199 n |= (n >> 8);
200 n |= (n >> 4);
201 n |= (n >> 2);
202 n |= (n >> 1);
203 return n + 1;
204 }
205
206 // A buffer we use for building up the serialized result
207 struct bser_buffer {
208 char *buf;
209 int wpos, allocd;
210 };
211 typedef struct bser_buffer bser_t;
212
213 static int bser_append(bser_t *bser, const char *data, uint32_t len)
214 {
215 int newlen = next_power_2(bser->wpos + len);
216 if (newlen > bser->allocd) {
217 char *nbuf = realloc(bser->buf, newlen);
218 if (!nbuf) {
219 return 0;
220 }
221
222 bser->buf = nbuf;
223 bser->allocd = newlen;
224 }
225
226 memcpy(bser->buf + bser->wpos, data, len);
227 bser->wpos += len;
228 return 1;
229 }
230
231 static int bser_init(bser_t *bser)
232 {
233 bser->allocd = 8192;
234 bser->wpos = 0;
235 bser->buf = malloc(bser->allocd);
236
237 if (!bser->buf) {
238 return 0;
239 }
240
241 // Leave room for the serialization header, which includes
242 // our overall length. To make things simpler, we'll use an
243 // int32 for the header
244 #define EMPTY_HEADER "\x00\x01\x05\x00\x00\x00\x00"
245 bser_append(bser, EMPTY_HEADER, sizeof(EMPTY_HEADER)-1);
246
247 return 1;
248 }
249
250 static void bser_dtor(bser_t *bser)
251 {
252 free(bser->buf);
253 bser->buf = NULL;
254 }
255
256 static int bser_long(bser_t *bser, int64_t val)
257 {
258 int8_t i8;
259 int16_t i16;
260 int32_t i32;
261 int64_t i64;
262 char sz;
263 int size = INT_SIZE(val);
264 char *iptr;
265
266 switch (size) {
267 case 1:
268 sz = BSER_INT8;
269 i8 = (int8_t)val;
270 iptr = (char*)&i8;
271 break;
272 case 2:
273 sz = BSER_INT16;
274 i16 = (int16_t)val;
275 iptr = (char*)&i16;
276 break;
277 case 4:
278 sz = BSER_INT32;
279 i32 = (int32_t)val;
280 iptr = (char*)&i32;
281 break;
282 case 8:
283 sz = BSER_INT64;
284 i64 = (int64_t)val;
285 iptr = (char*)&i64;
286 break;
287 default:
288 PyErr_SetString(PyExc_RuntimeError,
289 "Cannot represent this long value!?");
290 return 0;
291 }
292
293 if (!bser_append(bser, &sz, sizeof(sz))) {
294 return 0;
295 }
296
297 return bser_append(bser, iptr, size);
298 }
299
300 static int bser_string(bser_t *bser, PyObject *sval)
301 {
302 char *buf = NULL;
303 Py_ssize_t len;
304 int res;
305 PyObject *utf = NULL;
306
307 if (PyUnicode_Check(sval)) {
308 utf = PyUnicode_AsEncodedString(sval, "utf-8", "ignore");
309 sval = utf;
310 }
311
312 res = PyString_AsStringAndSize(sval, &buf, &len);
313 if (res == -1) {
314 res = 0;
315 goto out;
316 }
317
318 if (!bser_append(bser, &bser_string_hdr, sizeof(bser_string_hdr))) {
319 res = 0;
320 goto out;
321 }
322
323 if (!bser_long(bser, len)) {
324 res = 0;
325 goto out;
326 }
327
328 if (len > UINT32_MAX) {
329 PyErr_Format(PyExc_ValueError, "string too big");
330 res = 0;
331 goto out;
332 }
333
334 res = bser_append(bser, buf, (uint32_t)len);
335
336 out:
337 if (utf) {
338 Py_DECREF(utf);
339 }
340
341 return res;
342 }
343
344 static int bser_recursive(bser_t *bser, PyObject *val)
345 {
346 if (PyBool_Check(val)) {
347 if (val == Py_True) {
348 return bser_append(bser, &bser_true, sizeof(bser_true));
349 }
350 return bser_append(bser, &bser_false, sizeof(bser_false));
351 }
352
353 if (val == Py_None) {
354 return bser_append(bser, &bser_null, sizeof(bser_null));
355 }
356
357 if (PyInt_Check(val)) {
358 return bser_long(bser, PyInt_AS_LONG(val));
359 }
360
361 if (PyLong_Check(val)) {
362 return bser_long(bser, PyLong_AsLongLong(val));
363 }
364
365 if (PyString_Check(val) || PyUnicode_Check(val)) {
366 return bser_string(bser, val);
367 }
368
369
370 if (PyFloat_Check(val)) {
371 double dval = PyFloat_AS_DOUBLE(val);
372 char sz = BSER_REAL;
373
374 if (!bser_append(bser, &sz, sizeof(sz))) {
375 return 0;
376 }
377
378 return bser_append(bser, (char*)&dval, sizeof(dval));
379 }
380
381 if (PyList_Check(val)) {
382 Py_ssize_t i, len = PyList_GET_SIZE(val);
383
384 if (!bser_append(bser, &bser_array_hdr, sizeof(bser_array_hdr))) {
385 return 0;
386 }
387
388 if (!bser_long(bser, len)) {
389 return 0;
390 }
391
392 for (i = 0; i < len; i++) {
393 PyObject *ele = PyList_GET_ITEM(val, i);
394
395 if (!bser_recursive(bser, ele)) {
396 return 0;
397 }
398 }
399
400 return 1;
401 }
402
403 if (PyTuple_Check(val)) {
404 Py_ssize_t i, len = PyTuple_GET_SIZE(val);
405
406 if (!bser_append(bser, &bser_array_hdr, sizeof(bser_array_hdr))) {
407 return 0;
408 }
409
410 if (!bser_long(bser, len)) {
411 return 0;
412 }
413
414 for (i = 0; i < len; i++) {
415 PyObject *ele = PyTuple_GET_ITEM(val, i);
416
417 if (!bser_recursive(bser, ele)) {
418 return 0;
419 }
420 }
421
422 return 1;
423 }
424
425 if (PyMapping_Check(val)) {
426 Py_ssize_t len = PyMapping_Length(val);
427 Py_ssize_t pos = 0;
428 PyObject *key, *ele;
429
430 if (!bser_append(bser, &bser_object_hdr, sizeof(bser_object_hdr))) {
431 return 0;
432 }
433
434 if (!bser_long(bser, len)) {
435 return 0;
436 }
437
438 while (PyDict_Next(val, &pos, &key, &ele)) {
439 if (!bser_string(bser, key)) {
440 return 0;
441 }
442 if (!bser_recursive(bser, ele)) {
443 return 0;
444 }
445 }
446
447 return 1;
448 }
449
450 PyErr_SetString(PyExc_ValueError, "Unsupported value type");
451 return 0;
452 }
453
454 static PyObject *bser_dumps(PyObject *self, PyObject *args)
455 {
456 PyObject *val = NULL, *res;
457 bser_t bser;
458 uint32_t len;
459
460 if (!PyArg_ParseTuple(args, "O", &val)) {
461 return NULL;
462 }
463
464 if (!bser_init(&bser)) {
465 return PyErr_NoMemory();
466 }
467
468 if (!bser_recursive(&bser, val)) {
469 bser_dtor(&bser);
470 if (errno == ENOMEM) {
471 return PyErr_NoMemory();
472 }
473 // otherwise, we've already set the error to something reasonable
474 return NULL;
475 }
476
477 // Now fill in the overall length
478 len = bser.wpos - (sizeof(EMPTY_HEADER) - 1);
479 memcpy(bser.buf + 3, &len, sizeof(len));
480
481 res = PyString_FromStringAndSize(bser.buf, bser.wpos);
482 bser_dtor(&bser);
483
484 return res;
485 }
486
487 int bunser_int(const char **ptr, const char *end, int64_t *val)
488 {
489 int needed;
490 const char *buf = *ptr;
491 int8_t i8;
492 int16_t i16;
493 int32_t i32;
494 int64_t i64;
495
496 switch (buf[0]) {
497 case BSER_INT8:
498 needed = 2;
499 break;
500 case BSER_INT16:
501 needed = 3;
502 break;
503 case BSER_INT32:
504 needed = 5;
505 break;
506 case BSER_INT64:
507 needed = 9;
508 break;
509 default:
510 PyErr_Format(PyExc_ValueError,
511 "invalid bser int encoding 0x%02x", buf[0]);
512 return 0;
513 }
514 if (end - buf < needed) {
515 PyErr_SetString(PyExc_ValueError, "input buffer to small for int encoding");
516 return 0;
517 }
518 *ptr = buf + needed;
519 switch (buf[0]) {
520 case BSER_INT8:
521 memcpy(&i8, buf + 1, sizeof(i8));
522 *val = i8;
523 return 1;
524 case BSER_INT16:
525 memcpy(&i16, buf + 1, sizeof(i16));
526 *val = i16;
527 return 1;
528 case BSER_INT32:
529 memcpy(&i32, buf + 1, sizeof(i32));
530 *val = i32;
531 return 1;
532 case BSER_INT64:
533 memcpy(&i64, buf + 1, sizeof(i64));
534 *val = i64;
535 return 1;
536 default:
537 return 0;
538 }
539 }
540
541 static int bunser_string(const char **ptr, const char *end,
542 const char **start, int64_t *len)
543 {
544 const char *buf = *ptr;
545
546 // skip string marker
547 buf++;
548 if (!bunser_int(&buf, end, len)) {
549 return 0;
550 }
551
552 if (buf + *len > end) {
553 PyErr_Format(PyExc_ValueError, "invalid string length in bser data");
554 return 0;
555 }
556
557 *ptr = buf + *len;
558 *start = buf;
559 return 1;
560 }
561
562 static PyObject *bunser_array(const char **ptr, const char *end, int mutable)
563 {
564 const char *buf = *ptr;
565 int64_t nitems, i;
566 PyObject *res;
567
568 // skip array header
569 buf++;
570 if (!bunser_int(&buf, end, &nitems)) {
571 return 0;
572 }
573 *ptr = buf;
574
575 if (nitems > LONG_MAX) {
576 PyErr_Format(PyExc_ValueError, "too many items for python array");
577 return NULL;
578 }
579
580 if (mutable) {
581 res = PyList_New((Py_ssize_t)nitems);
582 } else {
583 res = PyTuple_New((Py_ssize_t)nitems);
584 }
585
586 for (i = 0; i < nitems; i++) {
587 PyObject *ele = bser_loads_recursive(ptr, end, mutable);
588
589 if (!ele) {
590 Py_DECREF(res);
591 return NULL;
592 }
593
594 if (mutable) {
595 PyList_SET_ITEM(res, i, ele);
596 } else {
597 PyTuple_SET_ITEM(res, i, ele);
598 }
599 // DECREF(ele) not required as SET_ITEM steals the ref
600 }
601
602 return res;
603 }
604
605 static PyObject *bunser_object(const char **ptr, const char *end,
606 int mutable)
607 {
608 const char *buf = *ptr;
609 int64_t nitems, i;
610 PyObject *res;
611 bserObject *obj;
612
613 // skip array header
614 buf++;
615 if (!bunser_int(&buf, end, &nitems)) {
616 return 0;
617 }
618 *ptr = buf;
619
620 if (mutable) {
621 res = PyDict_New();
622 } else {
623 obj = PyObject_New(bserObject, &bserObjectType);
624 obj->keys = PyTuple_New((Py_ssize_t)nitems);
625 obj->values = PyTuple_New((Py_ssize_t)nitems);
626 res = (PyObject*)obj;
627 }
628
629 for (i = 0; i < nitems; i++) {
630 const char *keystr;
631 int64_t keylen;
632 PyObject *key;
633 PyObject *ele;
634
635 if (!bunser_string(ptr, end, &keystr, &keylen)) {
636 Py_DECREF(res);
637 return NULL;
638 }
639
640 if (keylen > LONG_MAX) {
641 PyErr_Format(PyExc_ValueError, "string too big for python");
642 Py_DECREF(res);
643 return NULL;
644 }
645
646 key = PyString_FromStringAndSize(keystr, (Py_ssize_t)keylen);
647 if (!key) {
648 Py_DECREF(res);
649 return NULL;
650 }
651
652 ele = bser_loads_recursive(ptr, end, mutable);
653
654 if (!ele) {
655 Py_DECREF(key);
656 Py_DECREF(res);
657 return NULL;
658 }
659
660 if (mutable) {
661 PyDict_SetItem(res, key, ele);
662 Py_DECREF(key);
663 Py_DECREF(ele);
664 } else {
665 /* PyTuple_SET_ITEM steals ele, key */
666 PyTuple_SET_ITEM(obj->values, i, ele);
667 PyTuple_SET_ITEM(obj->keys, i, key);
668 }
669 }
670
671 return res;
672 }
673
674 static PyObject *bunser_template(const char **ptr, const char *end,
675 int mutable)
676 {
677 const char *buf = *ptr;
678 int64_t nitems, i;
679 PyObject *arrval;
680 PyObject *keys;
681 Py_ssize_t numkeys, keyidx;
682
683 if (buf[1] != BSER_ARRAY) {
684 PyErr_Format(PyExc_ValueError, "Expect ARRAY to follow TEMPLATE");
685 return NULL;
686 }
687
688 // skip header
689 buf++;
690 *ptr = buf;
691
692 // Load template keys
693 keys = bunser_array(ptr, end, mutable);
694 if (!keys) {
695 return NULL;
696 }
697
698 numkeys = PySequence_Length(keys);
699
700 // Load number of array elements
701 if (!bunser_int(ptr, end, &nitems)) {
702 Py_DECREF(keys);
703 return 0;
704 }
705
706 if (nitems > LONG_MAX) {
707 PyErr_Format(PyExc_ValueError, "Too many items for python");
708 Py_DECREF(keys);
709 return NULL;
710 }
711
712 arrval = PyList_New((Py_ssize_t)nitems);
713 if (!arrval) {
714 Py_DECREF(keys);
715 return NULL;
716 }
717
718 for (i = 0; i < nitems; i++) {
719 PyObject *dict = NULL;
720 bserObject *obj = NULL;
721
722 if (mutable) {
723 dict = PyDict_New();
724 } else {
725 obj = PyObject_New(bserObject, &bserObjectType);
726 if (obj) {
727 obj->keys = keys;
728 Py_INCREF(obj->keys);
729 obj->values = PyTuple_New(numkeys);
730 }
731 dict = (PyObject*)obj;
732 }
733 if (!dict) {
734 fail:
735 Py_DECREF(keys);
736 Py_DECREF(arrval);
737 return NULL;
738 }
739
740 for (keyidx = 0; keyidx < numkeys; keyidx++) {
741 PyObject *key;
742 PyObject *ele;
743
744 if (**ptr == BSER_SKIP) {
745 *ptr = *ptr + 1;
746 ele = Py_None;
747 Py_INCREF(ele);
748 } else {
749 ele = bser_loads_recursive(ptr, end, mutable);
750 }
751
752 if (!ele) {
753 goto fail;
754 }
755
756 if (mutable) {
757 key = PyList_GET_ITEM(keys, keyidx);
758 PyDict_SetItem(dict, key, ele);
759 Py_DECREF(ele);
760 } else {
761 PyTuple_SET_ITEM(obj->values, keyidx, ele);
762 // DECREF(ele) not required as SET_ITEM steals the ref
763 }
764 }
765
766 PyList_SET_ITEM(arrval, i, dict);
767 // DECREF(obj) not required as SET_ITEM steals the ref
768 }
769
770 Py_DECREF(keys);
771
772 return arrval;
773 }
774
775 static PyObject *bser_loads_recursive(const char **ptr, const char *end,
776 int mutable)
777 {
778 const char *buf = *ptr;
779
780 switch (buf[0]) {
781 case BSER_INT8:
782 case BSER_INT16:
783 case BSER_INT32:
784 case BSER_INT64:
785 {
786 int64_t ival;
787 if (!bunser_int(ptr, end, &ival)) {
788 return NULL;
789 }
790 if (ival < LONG_MIN || ival > LONG_MAX) {
791 return PyLong_FromLongLong(ival);
792 }
793 return PyInt_FromSsize_t(Py_SAFE_DOWNCAST(ival, int64_t, Py_ssize_t));
794 }
795
796 case BSER_REAL:
797 {
798 double dval;
799 memcpy(&dval, buf + 1, sizeof(dval));
800 *ptr = buf + 1 + sizeof(double);
801 return PyFloat_FromDouble(dval);
802 }
803
804 case BSER_TRUE:
805 *ptr = buf + 1;
806 Py_INCREF(Py_True);
807 return Py_True;
808
809 case BSER_FALSE:
810 *ptr = buf + 1;
811 Py_INCREF(Py_False);
812 return Py_False;
813
814 case BSER_NULL:
815 *ptr = buf + 1;
816 Py_INCREF(Py_None);
817 return Py_None;
818
819 case BSER_STRING:
820 {
821 const char *start;
822 int64_t len;
823
824 if (!bunser_string(ptr, end, &start, &len)) {
825 return NULL;
826 }
827
828 if (len > LONG_MAX) {
829 PyErr_Format(PyExc_ValueError, "string too long for python");
830 return NULL;
831 }
832
833 return PyString_FromStringAndSize(start, (long)len);
834 }
835
836 case BSER_ARRAY:
837 return bunser_array(ptr, end, mutable);
838
839 case BSER_OBJECT:
840 return bunser_object(ptr, end, mutable);
841
842 case BSER_TEMPLATE:
843 return bunser_template(ptr, end, mutable);
844
845 default:
846 PyErr_Format(PyExc_ValueError, "unhandled bser opcode 0x%02x", buf[0]);
847 }
848
849 return NULL;
850 }
851
852 // Expected use case is to read a packet from the socket and
853 // then call bser.pdu_len on the packet. It returns the total
854 // length of the entire response that the peer is sending,
855 // including the bytes already received. This allows the client
856 // to compute the data size it needs to read before it can
857 // decode the data
858 static PyObject *bser_pdu_len(PyObject *self, PyObject *args)
859 {
860 const char *start = NULL;
861 const char *data = NULL;
862 int datalen = 0;
863 const char *end;
864 int64_t expected_len, total_len;
865
866 if (!PyArg_ParseTuple(args, "s#", &start, &datalen)) {
867 return NULL;
868 }
869 data = start;
870 end = data + datalen;
871
872 // Validate the header and length
873 if (memcmp(data, EMPTY_HEADER, 2) != 0) {
874 PyErr_SetString(PyExc_ValueError, "invalid bser header");
875 return NULL;
876 }
877
878 data += 2;
879
880 // Expect an integer telling us how big the rest of the data
881 // should be
882 if (!bunser_int(&data, end, &expected_len)) {
883 return NULL;
884 }
885
886 total_len = expected_len + (data - start);
887 if (total_len > LONG_MAX) {
888 return PyLong_FromLongLong(total_len);
889 }
890 return PyInt_FromLong((long)total_len);
891 }
892
893 static PyObject *bser_loads(PyObject *self, PyObject *args)
894 {
895 const char *data = NULL;
896 int datalen = 0;
897 const char *end;
898 int64_t expected_len;
899 int mutable = 1;
900 PyObject *mutable_obj = NULL;
901
902 if (!PyArg_ParseTuple(args, "s#|O:loads", &data, &datalen, &mutable_obj)) {
903 return NULL;
904 }
905 if (mutable_obj) {
906 mutable = PyObject_IsTrue(mutable_obj) > 0 ? 1 : 0;
907 }
908
909 end = data + datalen;
910
911 // Validate the header and length
912 if (memcmp(data, EMPTY_HEADER, 2) != 0) {
913 PyErr_SetString(PyExc_ValueError, "invalid bser header");
914 return NULL;
915 }
916
917 data += 2;
918
919 // Expect an integer telling us how big the rest of the data
920 // should be
921 if (!bunser_int(&data, end, &expected_len)) {
922 return NULL;
923 }
924
925 // Verify
926 if (expected_len + data != end) {
927 PyErr_SetString(PyExc_ValueError, "bser data len != header len");
928 return NULL;
929 }
930
931 return bser_loads_recursive(&data, end, mutable);
932 }
933
934 static PyMethodDef bser_methods[] = {
935 {"loads", bser_loads, METH_VARARGS, "Deserialize string."},
936 {"pdu_len", bser_pdu_len, METH_VARARGS, "Extract PDU length."},
937 {"dumps", bser_dumps, METH_VARARGS, "Serialize string."},
938 {NULL, NULL, 0, NULL}
939 };
940
941 PyMODINIT_FUNC initbser(void)
942 {
943 (void)Py_InitModule("bser", bser_methods);
944 PyType_Ready(&bserObjectType);
945 }
946
947 /* vim:ts=2:sw=2:et:
948 */
949
950 // no-check-code -- this is a 3rd party library
@@ -0,0 +1,69 b''
1 # Copyright 2015 Facebook, Inc.
2 # All rights reserved.
3 #
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are met:
6 #
7 # * Redistributions of source code must retain the above copyright notice,
8 # this list of conditions and the following disclaimer.
9 #
10 # * Redistributions in binary form must reproduce the above copyright notice,
11 # this list of conditions and the following disclaimer in the documentation
12 # and/or other materials provided with the distribution.
13 #
14 # * Neither the name Facebook nor the names of its contributors may be used to
15 # endorse or promote products derived from this software without specific
16 # prior written permission.
17 #
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22 # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24 # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26 # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29 import re
30
31 def parse_version(vstr):
32 res = 0
33 for n in vstr.split('.'):
34 res = res * 1000
35 res = res + int(n)
36 return res
37
38 cap_versions = {
39 "cmd-watch-del-all": "3.1.1",
40 "cmd-watch-project": "3.1",
41 "relative_root": "3.3",
42 "term-dirname": "3.1",
43 "term-idirname": "3.1",
44 "wildmatch": "3.7",
45 }
46
47 def check(version, name):
48 if name in cap_versions:
49 return version >= parse_version(cap_versions[name])
50 return False
51
52 def synthesize(vers, opts):
53 """ Synthesize a capability enabled version response
54 This is a very limited emulation for relatively recent feature sets
55 """
56 parsed_version = parse_version(vers['version'])
57 vers['capabilities'] = {}
58 for name in opts['optional']:
59 vers['capabilities'][name] = check(parsed_version, name)
60 failed = False
61 for name in opts['required']:
62 have = check(parsed_version, name)
63 vers['capabilities'][name] = have
64 if not have:
65 vers['error'] = 'client required capability `' + name + \
66 '` is not supported by this server'
67 return vers
68
69 # no-check-code -- this is a 3rd party library
@@ -0,0 +1,359 b''
1 # Copyright 2015 Facebook, Inc.
2 # All rights reserved.
3 #
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are met:
6 #
7 # * Redistributions of source code must retain the above copyright notice,
8 # this list of conditions and the following disclaimer.
9 #
10 # * Redistributions in binary form must reproduce the above copyright notice,
11 # this list of conditions and the following disclaimer in the documentation
12 # and/or other materials provided with the distribution.
13 #
14 # * Neither the name Facebook nor the names of its contributors may be used to
15 # endorse or promote products derived from this software without specific
16 # prior written permission.
17 #
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22 # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24 # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26 # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29 import collections
30 import ctypes
31 import struct
32 import sys
33
34 BSER_ARRAY = '\x00'
35 BSER_OBJECT = '\x01'
36 BSER_STRING = '\x02'
37 BSER_INT8 = '\x03'
38 BSER_INT16 = '\x04'
39 BSER_INT32 = '\x05'
40 BSER_INT64 = '\x06'
41 BSER_REAL = '\x07'
42 BSER_TRUE = '\x08'
43 BSER_FALSE = '\x09'
44 BSER_NULL = '\x0a'
45 BSER_TEMPLATE = '\x0b'
46 BSER_SKIP = '\x0c'
47
48 # Leave room for the serialization header, which includes
49 # our overall length. To make things simpler, we'll use an
50 # int32 for the header
51 EMPTY_HEADER = "\x00\x01\x05\x00\x00\x00\x00"
52
53 # Python 3 conditional for supporting Python 2's int/long types
54 if sys.version_info > (3,):
55 long = int
56
57 def _int_size(x):
58 """Return the smallest size int that can store the value"""
59 if -0x80 <= x <= 0x7F:
60 return 1
61 elif -0x8000 <= x <= 0x7FFF:
62 return 2
63 elif -0x80000000 <= x <= 0x7FFFFFFF:
64 return 4
65 elif long(-0x8000000000000000) <= x <= long(0x7FFFFFFFFFFFFFFF):
66 return 8
67 else:
68 raise RuntimeError('Cannot represent value: ' + str(x))
69
70
71 class _bser_buffer(object):
72
73 def __init__(self):
74 self.buf = ctypes.create_string_buffer(8192)
75 struct.pack_into(str(len(EMPTY_HEADER)) + 's', self.buf, 0, EMPTY_HEADER)
76 self.wpos = len(EMPTY_HEADER)
77
78 def ensure_size(self, size):
79 while ctypes.sizeof(self.buf) - self.wpos < size:
80 ctypes.resize(self.buf, ctypes.sizeof(self.buf) * 2)
81
82 def append_long(self, val):
83 size = _int_size(val)
84 to_write = size + 1
85 self.ensure_size(to_write)
86 if size == 1:
87 struct.pack_into('=cb', self.buf, self.wpos, BSER_INT8, val)
88 elif size == 2:
89 struct.pack_into('=ch', self.buf, self.wpos, BSER_INT16, val)
90 elif size == 4:
91 struct.pack_into('=ci', self.buf, self.wpos, BSER_INT32, val)
92 elif size == 8:
93 struct.pack_into('=cq', self.buf, self.wpos, BSER_INT64, val)
94 else:
95 raise RuntimeError('Cannot represent this long value')
96 self.wpos += to_write
97
98
99 def append_string(self, s):
100 if isinstance(s, unicode):
101 s = s.encode('utf-8')
102 s_len = len(s)
103 size = _int_size(s_len)
104 to_write = 2 + size + s_len
105 self.ensure_size(to_write)
106 if size == 1:
107 struct.pack_into('=ccb' + str(s_len) + 's', self.buf, self.wpos, BSER_STRING, BSER_INT8, s_len, s)
108 elif size == 2:
109 struct.pack_into('=cch' + str(s_len) + 's', self.buf, self.wpos, BSER_STRING, BSER_INT16, s_len, s)
110 elif size == 4:
111 struct.pack_into('=cci' + str(s_len) + 's', self.buf, self.wpos, BSER_STRING, BSER_INT32, s_len, s)
112 elif size == 8:
113 struct.pack_into('=ccq' + str(s_len) + 's', self.buf, self.wpos, BSER_STRING, BSER_INT64, s_len, s)
114 else:
115 raise RuntimeError('Cannot represent this string value')
116 self.wpos += to_write
117
118
119 def append_recursive(self, val):
120 if isinstance(val, bool):
121 needed = 1
122 self.ensure_size(needed)
123 if val:
124 to_encode = BSER_TRUE
125 else:
126 to_encode = BSER_FALSE
127 struct.pack_into('=c', self.buf, self.wpos, to_encode)
128 self.wpos += needed
129 elif val is None:
130 needed = 1
131 self.ensure_size(needed)
132 struct.pack_into('=c', self.buf, self.wpos, BSER_NULL)
133 self.wpos += needed
134 elif isinstance(val, (int, long)):
135 self.append_long(val)
136 elif isinstance(val, (str, unicode)):
137 self.append_string(val)
138 elif isinstance(val, float):
139 needed = 9
140 self.ensure_size(needed)
141 struct.pack_into('=cd', self.buf, self.wpos, BSER_REAL, val)
142 self.wpos += needed
143 elif isinstance(val, collections.Mapping) and isinstance(val, collections.Sized):
144 val_len = len(val)
145 size = _int_size(val_len)
146 needed = 2 + size
147 self.ensure_size(needed)
148 if size == 1:
149 struct.pack_into('=ccb', self.buf, self.wpos, BSER_OBJECT, BSER_INT8, val_len)
150 elif size == 2:
151 struct.pack_into('=cch', self.buf, self.wpos, BSER_OBJECT, BSER_INT16, val_len)
152 elif size == 4:
153 struct.pack_into('=cci', self.buf, self.wpos, BSER_OBJECT, BSER_INT32, val_len)
154 elif size == 8:
155 struct.pack_into('=ccq', self.buf, self.wpos, BSER_OBJECT, BSER_INT64, val_len)
156 else:
157 raise RuntimeError('Cannot represent this mapping value')
158 self.wpos += needed
159 for k, v in val.iteritems():
160 self.append_string(k)
161 self.append_recursive(v)
162 elif isinstance(val, collections.Iterable) and isinstance(val, collections.Sized):
163 val_len = len(val)
164 size = _int_size(val_len)
165 needed = 2 + size
166 self.ensure_size(needed)
167 if size == 1:
168 struct.pack_into('=ccb', self.buf, self.wpos, BSER_ARRAY, BSER_INT8, val_len)
169 elif size == 2:
170 struct.pack_into('=cch', self.buf, self.wpos, BSER_ARRAY, BSER_INT16, val_len)
171 elif size == 4:
172 struct.pack_into('=cci', self.buf, self.wpos, BSER_ARRAY, BSER_INT32, val_len)
173 elif size == 8:
174 struct.pack_into('=ccq', self.buf, self.wpos, BSER_ARRAY, BSER_INT64, val_len)
175 else:
176 raise RuntimeError('Cannot represent this sequence value')
177 self.wpos += needed
178 for v in val:
179 self.append_recursive(v)
180 else:
181 raise RuntimeError('Cannot represent unknown value type')
182
183
184 def dumps(obj):
185 bser_buf = _bser_buffer()
186 bser_buf.append_recursive(obj)
187 # Now fill in the overall length
188 obj_len = bser_buf.wpos - len(EMPTY_HEADER)
189 struct.pack_into('=i', bser_buf.buf, 3, obj_len)
190 return bser_buf.buf.raw[:bser_buf.wpos]
191
192
193 def _bunser_int(buf, pos):
194 try:
195 int_type = buf[pos]
196 except IndexError:
197 raise ValueError('Invalid bser int encoding, pos out of range')
198 if int_type == BSER_INT8:
199 needed = 2
200 fmt = '=b'
201 elif int_type == BSER_INT16:
202 needed = 3
203 fmt = '=h'
204 elif int_type == BSER_INT32:
205 needed = 5
206 fmt = '=i'
207 elif int_type == BSER_INT64:
208 needed = 9
209 fmt = '=q'
210 else:
211 raise ValueError('Invalid bser int encoding 0x%02x' % int(int_type))
212 int_val = struct.unpack_from(fmt, buf, pos + 1)[0]
213 return (int_val, pos + needed)
214
215
216 def _bunser_string(buf, pos):
217 str_len, pos = _bunser_int(buf, pos + 1)
218 str_val = struct.unpack_from(str(str_len) + 's', buf, pos)[0]
219 return (str_val, pos + str_len)
220
221
222 def _bunser_array(buf, pos, mutable=True):
223 arr_len, pos = _bunser_int(buf, pos + 1)
224 arr = []
225 for i in range(arr_len):
226 arr_item, pos = _bser_loads_recursive(buf, pos, mutable)
227 arr.append(arr_item)
228
229 if not mutable:
230 arr = tuple(arr)
231
232 return arr, pos
233
234
235 # This is a quack-alike with the bserObjectType in bser.c
236 # It provides by getattr accessors and getitem for both index
237 # and name.
238 class _BunserDict(object):
239 __slots__ = ('_keys', '_values')
240
241 def __init__(self, keys, values):
242 self._keys = keys
243 self._values = values
244
245 def __getattr__(self, name):
246 return self.__getitem__(name)
247
248 def __getitem__(self, key):
249 if isinstance(key, (int, long)):
250 return self._values[key]
251 elif key.startswith('st_'):
252 # hack^Wfeature to allow mercurial to use "st_size" to
253 # reference "size"
254 key = key[3:]
255 try:
256 return self._values[self._keys.index(key)]
257 except ValueError as ex:
258 raise KeyError('_BunserDict has no key %s' % key)
259
260 def __len__(self):
261 return len(self._keys)
262
263 def _bunser_object(buf, pos, mutable=True):
264 obj_len, pos = _bunser_int(buf, pos + 1)
265 if mutable:
266 obj = {}
267 else:
268 keys = []
269 vals = []
270
271 for i in range(obj_len):
272 key, pos = _bunser_string(buf, pos)
273 val, pos = _bser_loads_recursive(buf, pos, mutable)
274 if mutable:
275 obj[key] = val
276 else:
277 keys.append(key)
278 vals.append(val)
279
280 if not mutable:
281 obj = _BunserDict(keys, vals)
282
283 return obj, pos
284
285
286 def _bunser_template(buf, pos, mutable=True):
287 if buf[pos + 1] != BSER_ARRAY:
288 raise RuntimeError('Expect ARRAY to follow TEMPLATE')
289 keys, pos = _bunser_array(buf, pos + 1)
290 nitems, pos = _bunser_int(buf, pos)
291 arr = []
292 for i in range(nitems):
293 if mutable:
294 obj = {}
295 else:
296 vals = []
297
298 for keyidx in range(len(keys)):
299 if buf[pos] == BSER_SKIP:
300 pos += 1
301 ele = None
302 else:
303 ele, pos = _bser_loads_recursive(buf, pos, mutable)
304
305 if mutable:
306 key = keys[keyidx]
307 obj[key] = ele
308 else:
309 vals.append(ele)
310
311 if not mutable:
312 obj = _BunserDict(keys, vals)
313
314 arr.append(obj)
315 return arr, pos
316
317
318 def _bser_loads_recursive(buf, pos, mutable=True):
319 val_type = buf[pos]
320 if (val_type == BSER_INT8 or val_type == BSER_INT16 or
321 val_type == BSER_INT32 or val_type == BSER_INT64):
322 return _bunser_int(buf, pos)
323 elif val_type == BSER_REAL:
324 val = struct.unpack_from('=d', buf, pos + 1)[0]
325 return (val, pos + 9)
326 elif val_type == BSER_TRUE:
327 return (True, pos + 1)
328 elif val_type == BSER_FALSE:
329 return (False, pos + 1)
330 elif val_type == BSER_NULL:
331 return (None, pos + 1)
332 elif val_type == BSER_STRING:
333 return _bunser_string(buf, pos)
334 elif val_type == BSER_ARRAY:
335 return _bunser_array(buf, pos, mutable)
336 elif val_type == BSER_OBJECT:
337 return _bunser_object(buf, pos, mutable)
338 elif val_type == BSER_TEMPLATE:
339 return _bunser_template(buf, pos, mutable)
340 else:
341 raise RuntimeError('unhandled bser opcode 0x%02x' % (val_type,))
342
343
344 def pdu_len(buf):
345 if buf[0:2] != EMPTY_HEADER[0:2]:
346 raise RuntimeError('Invalid BSER header')
347 expected_len, pos = _bunser_int(buf, 2)
348 return expected_len + pos
349
350
351 def loads(buf, mutable=True):
352 if buf[0:2] != EMPTY_HEADER[0:2]:
353 raise RuntimeError('Invalid BSER header')
354 expected_len, pos = _bunser_int(buf, 2)
355 if len(buf) != expected_len + pos:
356 raise RuntimeError('bser data len != header len')
357 return _bser_loads_recursive(buf, pos, mutable)[0]
358
359 # no-check-code -- this is a 3rd party library
@@ -532,6 +532,8 b' extmodules = ['
532 Extension('mercurial.osutil', ['mercurial/osutil.c'],
532 Extension('mercurial.osutil', ['mercurial/osutil.c'],
533 extra_link_args=osutil_ldflags,
533 extra_link_args=osutil_ldflags,
534 depends=common_depends),
534 depends=common_depends),
535 Extension('hgext.fsmonitor.pywatchman.bser',
536 ['hgext/fsmonitor/pywatchman/bser.c']),
535 ]
537 ]
536
538
537 try:
539 try:
@@ -8,6 +8,10 b' New errors are not allowed. Warnings are'
8
8
9 $ hg locate | sed 's-\\-/-g' |
9 $ hg locate | sed 's-\\-/-g' |
10 > xargs "$check_code" --warnings --per-file=0 || false
10 > xargs "$check_code" --warnings --per-file=0 || false
11 Skipping hgext/fsmonitor/pywatchman/__init__.py it has no-che?k-code (glob)
12 Skipping hgext/fsmonitor/pywatchman/bser.c it has no-che?k-code (glob)
13 Skipping hgext/fsmonitor/pywatchman/capabilities.py it has no-che?k-code (glob)
14 Skipping hgext/fsmonitor/pywatchman/pybser.py it has no-che?k-code (glob)
11 Skipping i18n/polib.py it has no-che?k-code (glob)
15 Skipping i18n/polib.py it has no-che?k-code (glob)
12 Skipping mercurial/httpclient/__init__.py it has no-che?k-code (glob)
16 Skipping mercurial/httpclient/__init__.py it has no-che?k-code (glob)
13 Skipping mercurial/httpclient/_readers.py it has no-che?k-code (glob)
17 Skipping mercurial/httpclient/_readers.py it has no-che?k-code (glob)
@@ -25,6 +25,10 b''
25 hgext/extdiff.py not using absolute_import
25 hgext/extdiff.py not using absolute_import
26 hgext/factotum.py not using absolute_import
26 hgext/factotum.py not using absolute_import
27 hgext/fetch.py not using absolute_import
27 hgext/fetch.py not using absolute_import
28 hgext/fsmonitor/pywatchman/__init__.py not using absolute_import
29 hgext/fsmonitor/pywatchman/__init__.py requires print_function
30 hgext/fsmonitor/pywatchman/capabilities.py not using absolute_import
31 hgext/fsmonitor/pywatchman/pybser.py not using absolute_import
28 hgext/gpg.py not using absolute_import
32 hgext/gpg.py not using absolute_import
29 hgext/graphlog.py not using absolute_import
33 hgext/graphlog.py not using absolute_import
30 hgext/hgcia.py not using absolute_import
34 hgext/hgcia.py not using absolute_import
General Comments 0
You need to be logged in to leave comments. Login now