diff options
Diffstat (limited to 'tools/hook-scripts/svn2feed.py')
-rwxr-xr-x | tools/hook-scripts/svn2feed.py | 466 |
1 files changed, 466 insertions, 0 deletions
diff --git a/tools/hook-scripts/svn2feed.py b/tools/hook-scripts/svn2feed.py new file mode 100755 index 0000000..0075cfc --- /dev/null +++ b/tools/hook-scripts/svn2feed.py @@ -0,0 +1,466 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# ==================================================================== +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ==================================================================== + +"""Usage: svn2feed.py [OPTION...] REPOS-PATH + +Generate an RSS 2.0 or Atom 1.0 feed file containing commit +information for the Subversion repository located at REPOS-PATH. Once +the maximum number of items is reached, older elements are removed. +The item title is the revision number, and the item description +contains the author, date, log messages and changed paths. + +Options: + + -h, --help Show this help message. + + -F, --format=FORMAT Required option. FORMAT must be one of: + 'rss' (RSS 2.0) + 'atom' (Atom 1.0) + to select the appropriate feed format. + + -f, --feed-file=PATH Store the feed in the file located at PATH, which will + be created if it does not exist, or overwritten if it + does. If not provided, the script will store the feed + in the current working directory, in a file named + REPOS_NAME.rss or REPOS_NAME.atom (where REPOS_NAME is + the basename of the REPOS_PATH command-line argument, + and the file extension depends on the selected + format). + + -r, --revision=X[:Y] Subversion revision (or revision range) to generate + info for. If not provided, info for the single + youngest revision in the repository will be generated. + + -m, --max-items=N Keep only N items in the feed file. By default, + 20 items are kept. + + -u, --item-url=URL Use URL as the basis for generating feed item links. + This value is appended with '?rev=REV_NUMBER' to form + the actual item links. + + -U, --feed-url=URL Use URL as the global link associated with the feed. + + -P, --svn-path=DIR Look in DIR for the svnlook binary. If not provided, + svnlook must be on the PATH. +""" + +# TODO: +# --item-url should support arbitrary formatting of the revision number, +# to be useful with web viewers other than ViewVC. +# Rather more than intended is being cached in the pickle file. Instead of +# only old items being drawn from the pickle, all the global feed metadata +# is actually set only on initial feed creation, and thereafter simply +# re-used from the pickle each time. + +# $HeadURL: http://svn.apache.org/repos/asf/subversion/branches/1.7.x/tools/hook-scripts/svn2feed.py $ +# $LastChangedDate: 2009-11-16 19:07:17 +0000 (Mon, 16 Nov 2009) $ +# $LastChangedBy: hwright $ +# $LastChangedRevision: 880911 $ + +import sys + +# Python 2.4 is required for subprocess +if sys.version_info < (2, 4): + sys.stderr.write("Error: Python 2.4 or higher required.\n") + sys.stderr.flush() + sys.exit(1) + +import getopt +import os +import subprocess +try: + # Python <3.0 + import cPickle as pickle +except ImportError: + # Python >=3.0 + import pickle +import datetime +import time + +def usage_and_exit(errmsg=None): + """Print a usage message, plus an ERRMSG (if provided), then exit. + If ERRMSG is provided, the usage message is printed to stderr and + the script exits with a non-zero error code. Otherwise, the usage + message goes to stdout, and the script exits with a zero + errorcode.""" + if errmsg is None: + stream = sys.stdout + else: + stream = sys.stderr + stream.write("%s\n" % __doc__) + stream.flush() + if errmsg: + stream.write("\nError: %s\n" % errmsg) + stream.flush() + sys.exit(2) + sys.exit(0) + +def check_url(url, opt): + """Verify that URL looks like a valid URL or option OPT.""" + if not (url.startswith('https://') \ + or url.startswith('http://') \ + or url.startswith('file://')): + usage_and_exit("svn2feed.py: Invalid url '%s' is specified for " \ + "'%s' option" % (url, opt)) + + +class Svn2Feed: + def __init__(self, svn_path, repos_path, item_url, feed_file, + max_items, feed_url): + self.repos_path = repos_path + self.item_url = item_url + self.feed_file = feed_file + self.max_items = max_items + self.feed_url = feed_url + self.svnlook_cmd = 'svnlook' + if svn_path is not None: + self.svnlook_cmd = os.path.join(svn_path, 'svnlook') + self.feed_title = ("%s's Subversion Commits Feed" + % (os.path.basename(os.path.abspath(self.repos_path)))) + self.feed_desc = "The latest Subversion commits" + + def _get_item_dict(self, revision): + revision = str(revision) + + cmd = [self.svnlook_cmd, 'info', '-r', revision, self.repos_path] + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE) + proc.wait() + info_lines = proc.stdout.readlines() + + cmd = [self.svnlook_cmd, 'changed', '-r', revision, self.repos_path] + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE) + proc.wait() + changed_data = proc.stdout.readlines() + + desc = ("\nRevision: %s\nLog: %sModified: \n%s" + % (revision, info_lines[3], changed_data)) + + item_dict = { + 'author': info_lines[0].strip('\n'), + 'title': "Revision %s" % revision, + 'link': self.item_url and "%s?rev=%s" % (self.item_url, revision), + 'date': self._format_updated_ts(info_lines[1]), + 'description': "<pre>" + desc + "</pre>", + } + + return item_dict + + def _format_updated_ts(self, revision_ts): + + # Get "2006-08-10 20:17:08" from + # "2006-07-28 20:17:18 +0530 (Fri, 28 Jul 2006) + date = revision_ts[0:19] + epoch = time.mktime(time.strptime(date, "%Y-%m-%d %H:%M:%S")) + return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(epoch)) + + +class Svn2RSS(Svn2Feed): + def __init__(self, svn_path, repos_path, item_url, feed_file, + max_items, feed_url): + Svn2Feed.__init__(self, svn_path, repos_path, item_url, feed_file, + max_items, feed_url) + try: + import PyRSS2Gen + except ImportError: + sys.stderr.write(""" +Error: Required PyRSS2Gen module not found. You can download the PyRSS2Gen +module from: + + http://www.dalkescientific.com/Python/PyRSS2Gen.html + +""") + sys.exit(1) + self.PyRSS2Gen = PyRSS2Gen + + (file, ext) = os.path.splitext(self.feed_file) + self.pickle_file = file + ".pickle" + if os.path.exists(self.pickle_file): + self.rss = pickle.load(open(self.pickle_file, "r")) + else: + self.rss = self.PyRSS2Gen.RSS2( + title = self.feed_title, + link = self.feed_url, + description = self.feed_desc, + lastBuildDate = datetime.datetime.now(), + items = []) + + @staticmethod + def get_default_file_extension(): + return ".rss" + + def add_revision_item(self, revision): + rss_item = self._make_rss_item(revision) + self.rss.items.insert(0, rss_item) + if len(self.rss.items) > self.max_items: + del self.rss.items[self.max_items:] + + def write_output(self): + s = pickle.dumps(self.rss) + f = open(self.pickle_file, "w") + f.write(s) + f.close() + + f = open(self.feed_file, "w") + self.rss.write_xml(f) + f.close() + + def _make_rss_item(self, revision): + info = self._get_item_dict(revision) + + rss_item = self.PyRSS2Gen.RSSItem( + author = info['author'], + title = info['title'], + link = info['link'], + description = info['description'], + guid = self.PyRSS2Gen.Guid(info['link']), + pubDate = info['date']) + return rss_item + + +class Svn2Atom(Svn2Feed): + def __init__(self, svn_path, repos_path, item_url, feed_file, + max_items, feed_url): + Svn2Feed.__init__(self, svn_path, repos_path, item_url, feed_file, + max_items, feed_url) + from xml.dom import getDOMImplementation + self.dom_impl = getDOMImplementation() + + self.pickle_file = self.feed_file + ".pickle" + if os.path.exists(self.pickle_file): + self.document = pickle.load(open(self.pickle_file, "r")) + self.feed = self.document.getElementsByTagName('feed')[0] + else: + self._init_atom_document() + + @staticmethod + def get_default_file_extension(): + return ".atom" + + def add_revision_item(self, revision): + item = self._make_atom_item(revision) + + total = 0 + for childNode in self.feed.childNodes: + if childNode.nodeName == 'entry': + if total == 0: + self.feed.insertBefore(item, childNode) + total += 1 + total += 1 + if total > self.max_items: + self.feed.removeChild(childNode) + if total == 0: + self.feed.appendChild(item) + + def write_output(self): + s = pickle.dumps(self.document) + f = open(self.pickle_file, "w") + f.write(s) + f.close() + + f = open(self.feed_file, "w") + f.write(self.document.toxml()) + f.close() + + def _make_atom_item(self, revision): + info = self._get_item_dict(revision) + + doc = self.document + entry = doc.createElement("entry") + + id = doc.createElement("id") + entry.appendChild(id) + id.appendChild(doc.createTextNode(info['link'])) + + title = doc.createElement("title") + entry.appendChild(title) + title.appendChild(doc.createTextNode(info['title'])) + + updated = doc.createElement("updated") + entry.appendChild(updated) + updated.appendChild(doc.createTextNode(info['date'])) + + link = doc.createElement("link") + entry.appendChild(link) + link.setAttribute("href", info['link']) + + summary = doc.createElement("summary") + entry.appendChild(summary) + summary.appendChild(doc.createTextNode(info['description'])) + + author = doc.createElement("author") + entry.appendChild(author) + aname = doc.createElement("name") + author.appendChild(aname) + aname.appendChild(doc.createTextNode(info['author'])) + + return entry + + def _init_atom_document(self): + doc = self.document = self.dom_impl.createDocument(None, None, None) + feed = self.feed = doc.createElement("feed") + doc.appendChild(feed) + + feed.setAttribute("xmlns", "http://www.w3.org/2005/Atom") + + title = doc.createElement("title") + feed.appendChild(title) + title.appendChild(doc.createTextNode(self.feed_title)) + + id = doc.createElement("id") + feed.appendChild(id) + id.appendChild(doc.createTextNode(self.feed_url)) + + updated = doc.createElement("updated") + feed.appendChild(updated) + now = datetime.datetime.now() + updated.appendChild(doc.createTextNode(self._format_date(now))) + + link = doc.createElement("link") + feed.appendChild(link) + link.setAttribute("href", self.feed_url) + + author = doc.createElement("author") + feed.appendChild(author) + aname = doc.createElement("name") + author.appendChild(aname) + aname.appendChild(doc.createTextNode("subversion")) + + def _format_date(self, dt): + """ input date must be in GMT """ + return ("%04d-%02d-%02dT%02d:%02d:%02d.%02dZ" + % (dt.year, dt.month, dt.day, dt.hour, dt.minute, + dt.second, dt.microsecond)) + + +def main(): + # Parse the command-line options and arguments. + try: + opts, args = getopt.gnu_getopt(sys.argv[1:], "hP:r:u:f:m:U:F:", + ["help", + "svn-path=", + "revision=", + "item-url=", + "feed-file=", + "max-items=", + "feed-url=", + "format=", + ]) + except getopt.GetoptError, msg: + usage_and_exit(msg) + + # Make sure required arguments are present. + if len(args) != 1: + usage_and_exit("You must specify a repository path.") + repos_path = os.path.abspath(args[0]) + + # Now deal with the options. + max_items = 20 + commit_rev = svn_path = None + item_url = feed_url = None + feed_file = None + feedcls = None + feed_classes = { 'rss': Svn2RSS, 'atom': Svn2Atom } + + for opt, arg in opts: + if opt in ("-h", "--help"): + usage_and_exit() + elif opt in ("-P", "--svn-path"): + svn_path = arg + elif opt in ("-r", "--revision"): + commit_rev = arg + elif opt in ("-u", "--item-url"): + item_url = arg + check_url(item_url, opt) + elif opt in ("-f", "--feed-file"): + feed_file = arg + elif opt in ("-m", "--max-items"): + try: + max_items = int(arg) + except ValueError, msg: + usage_and_exit("Invalid value '%s' for --max-items." % (arg)) + if max_items < 1: + usage_and_exit("Value for --max-items must be a positive " + "integer.") + elif opt in ("-U", "--feed-url"): + feed_url = arg + check_url(feed_url, opt) + elif opt in ("-F", "--format"): + try: + feedcls = feed_classes[arg] + except KeyError: + usage_and_exit("Invalid value '%s' for --format." % arg) + + if feedcls is None: + usage_and_exit("Option -F [--format] is required.") + + if item_url is None: + usage_and_exit("Option -u [--item-url] is required.") + + if feed_url is None: + usage_and_exit("Option -U [--feed-url] is required.") + + if commit_rev is None: + svnlook_cmd = 'svnlook' + if svn_path is not None: + svnlook_cmd = os.path.join(svn_path, 'svnlook') + cmd = [svnlook_cmd, 'youngest', repos_path] + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE) + proc.wait() + cmd_out = proc.stdout.readlines() + try: + revisions = [int(cmd_out[0])] + except IndexError, msg: + usage_and_exit("svn2feed.py: Invalid value '%s' for " \ + "REPOS-PATH" % (repos_path)) + else: + try: + rev_range = commit_rev.split(':') + len_rev_range = len(rev_range) + if len_rev_range == 1: + revisions = [int(commit_rev)] + elif len_rev_range == 2: + start, end = rev_range + start = int(start) + end = int(end) + if (start > end): + tmp = start + start = end + end = tmp + revisions = list(range(start, end + 1)[-max_items:]) + else: + raise ValueError() + except ValueError, msg: + usage_and_exit("svn2feed.py: Invalid value '%s' for --revision." \ + % (commit_rev)) + + if feed_file is None: + feed_file = (os.path.basename(repos_path) + + feedcls.get_default_file_extension()) + + feed = feedcls(svn_path, repos_path, item_url, feed_file, max_items, + feed_url) + for revision in revisions: + feed.add_revision_item(revision) + feed.write_output() + + +if __name__ == "__main__": + main() |