summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDan McGee <dan@archlinux.org>2012-02-18 19:16:43 -0600
committerDan McGee <dan@archlinux.org>2013-09-30 20:53:11 -0500
commit1dbfb0fd1b0024093f42268e168e65c9e10b8f1b (patch)
tree3affa85392991d690714c5089a6286c8f3d5a5ee
parent92136757bfd20563999b0e1cf3f05685b60da6bd (diff)
downloadarchweb-1dbfb0fd1b0024093f42268e168e65c9e10b8f1b.tar.gz
archweb-1dbfb0fd1b0024093f42268e168e65c9e10b8f1b.zip
WIP: SVN RA protocol work
-rw-r--r--packages/management/commands/populate_signoffs.py157
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 = {}