diff options
author | Dan McGee <dan@archlinux.org> | 2012-02-18 19:16:43 -0600 |
---|---|---|
committer | Dan McGee <dan@archlinux.org> | 2013-09-30 20:53:11 -0500 |
commit | 1dbfb0fd1b0024093f42268e168e65c9e10b8f1b (patch) | |
tree | 3affa85392991d690714c5089a6286c8f3d5a5ee | |
parent | 92136757bfd20563999b0e1cf3f05685b60da6bd (diff) | |
download | archweb-1dbfb0fd1b0024093f42268e168e65c9e10b8f1b.tar.gz archweb-1dbfb0fd1b0024093f42268e168e65c9e10b8f1b.zip |
WIP: SVN RA protocol work
-rw-r--r-- | packages/management/commands/populate_signoffs.py | 157 |
1 files changed, 155 insertions, 2 deletions
diff --git a/packages/management/commands/populate_signoffs.py b/packages/management/commands/populate_signoffs.py index a9c1c81c..7c43c30d 100644 --- a/packages/management/commands/populate_signoffs.py +++ b/packages/management/commands/populate_signoffs.py @@ -10,8 +10,10 @@ Usage: ./manage.py populate_signoffs from datetime import datetime import logging +import socket import subprocess import sys +from urlparse import urlparse from xml.etree.ElementTree import XML from django.conf import settings @@ -43,6 +45,154 @@ is signoff-eligible and does not have an existing comment attached""" return add_signoff_comments() +class SvnProtocol(object): + '''A class that helps with the on-the-wire svn:// protocol for fetching + logs.''' + def __init__(self, url): + self.url = url + self.url_parts = urlparse(url) + if self.url_parts.scheme != 'svn': + raise Exception('We only support the svn:// scheme') + self.socket = None + + @staticmethod + def _stringify(value): + return '%d:%s' % (len(value), value) + + @staticmethod + def _parse_response(resp, stack=None): + '''Protocol response parser. This tracks state via a stack of parsed + tokens, and processes them accordingly depending on previously + processed items. Currently the behavior is undefined if we encounter a + malformed response. + If we are parsing and cannot fully process the input, we assume we were + passed only partial data and pass back the intermediate state of the + parser. Recalling the method with this state and further data allows + parsing to continue.''' + if stack is None: + stack = [[]] + i, length = 0, len(resp) + while i < length: + c = resp[i] + reading = type(stack[-1]) + if reading == list: + if c == '(': + stack.append([]) + elif c == ')': + val = stack.pop() + stack[-1].append(val) + elif c in ' \n': + pass + else: + stack.append(c) + elif reading == str: + if c in ' \n': + val = stack.pop() + # coerce to boolean, int, or leave as string + if val == 'true': + val = True + elif val == 'false': + val = False + elif val.isdigit(): + val = int(val) + stack[-1].append(val) + elif c == ':': + # string case; we can grab the whole thing at once + str_len = int(stack.pop()) + i += 1 + stack[-1].append(resp[i:i+str_len]) + i += str_len + else: + stack[-1] += c + i += 1 + + if len(stack) > 1: + # parsing was incomplete, we should try again with more data + return False, stack + return True, stack.pop() + + def issue_command(self, cmd, resp_ct, expect_auth=False): + self.socket.send(cmd + '\n') + return self.check_response(resp_ct, expect_auth) + + EMPTY_AUTH_RESPONSE = ['success', [[], '']] + + def check_response(self, resp_ct, expect_auth=False): + data = self.socket.recv(4096) + done, parsed = self._parse_response(data) + while not done or len(parsed) < resp_ct: + data = self.socket.recv(4096) + self._parse_response(data, parsed) + + if len(parsed) != resp_ct: + raise Exception('expected %d, got %d results' % ( + resp_ct, len(parsed))) + for result in parsed: + if (type(result) == list and len(result) > 0 and + type(result[0]) in (str, unicode) and + (result[0] == 'failure' or result[0] != 'success')): + raise Exception('unsuccessful result, %s' % result[0]) + if expect_auth and parsed[0] != self.EMPTY_AUTH_RESPONSE: + raise Exception('unexpected AUTH response') + return parsed + + def connect(self): + '''Connect to the remote svn:// protocol server. This must be called + before any other operation is performed.''' + if self.socket: + return + + # set up the socket connection + host = self.url_parts.hostname + port = self.url_parts.port or 3690 + self.socket = socket.socket() + self.socket.connect((host, port)) + # process initial message from server + self.check_response(1) + + # issue our connection setup commands + user_agent = 'archweb/0.1' + msg = '( 2 ( edit-pipeline log-revprops ) %s %s ( ) )' % ( + self._stringify(self.url), self._stringify(user_agent)) + self.issue_command(msg, 1) + + msg = '( ANONYMOUS ( %s ) )' % (self._stringify('anonymous')) + self.issue_command(msg, 2) + + def disconnect(self): + self.socket.close() + self.socket = None + + def log(self, path): + path = '%s%s' % (self.url, path) + msg = '( reparent ( %s ) )\n' % (self._stringify(path)) + self.issue_command(msg, 2, True) + + msg = '( log ( ( 0: ) ( ) ( 0 ) false false 1 ) )\n' + data = self.issue_command(msg, 4, True) + + # the single log entry we care about will be the second item in data; + # the various fields are hardcoded in position + date = datetime.strptime(data[1][3][0], '%Y-%m-%dT%H:%M:%S.%fZ') + + return { + 'revision': data[1][1], + 'date': date, + 'author': data[1][2][0], + 'message': data[1][4][0], + } + +def svn_native_log(pkgbase, repo): + path = '%s%s/' % (settings.SVN_BASE_URL, repo.svn_root) + if path in svn_native_log.cache: + svn = svn_native_log.cache[path] + else: + svn = SvnProtocol(path) + svn.connect() + svn_native_log.cache[path] = svn + return svn.log('%s/trunk/' % pkgbase) +svn_native_log.cache = {} + def svn_log(pkgbase, repo): '''Retrieve the most recent SVN log entry for the given pkgbase and repository. The configured setting SVN_BASE_URL is used along with the @@ -64,11 +214,14 @@ def svn_log(pkgbase, repo): def cached_svn_log(pkgbase, repo): '''Retrieve the cached version of the SVN log if possible, else delegate to - svn_log() to do the work and cache the result.''' + svn_log() or svn_native_log() to do the work and cache the result.''' key = (pkgbase, repo) if key in cached_svn_log.cache: return cached_svn_log.cache[key] - log = svn_log(pkgbase, repo) + if settings.SVN_BASE_URL.startswith('svn://'): + log = svn_native_log(pkgbase, repo) + else: + log = svn_log(pkgbase, repo) cached_svn_log.cache[key] = log return log cached_svn_log.cache = {} |