From bd797a85635d7643468c8e1378c24b3c73fcb35c Mon Sep 17 00:00:00 2001 From: Corentin Chary Date: Fri, 1 Apr 2011 17:18:21 +0200 Subject: [PATCH] Initial commit Signed-off-by: Corentin Chary --- euscan | 545 ++++++++++++++++++++++++++++++++++++++++++++++ web/euscan-update | 229 +++++++++++++++++++ web/euscan.sql | 54 +++++ web/index.php | 207 ++++++++++++++++++ web/sorttable.js | 493 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 1528 insertions(+) create mode 100755 euscan create mode 100755 web/euscan-update create mode 100644 web/euscan.sql create mode 100644 web/index.php create mode 100644 web/sorttable.js diff --git a/euscan b/euscan new file mode 100755 index 0000000..4310343 --- /dev/null +++ b/euscan @@ -0,0 +1,545 @@ +#!/usr/bin/python +############################################################################## +# $Header: $ +############################################################################## +# Distributed under the terms of the GNU General Public License, v2 or later +# Author: Corentin Chary + +# Gentoo new upstream release scan tool. + +import os +import sys +import re +import StringIO +from stat import * +from xml.sax import saxutils, make_parser, handler +from xml.sax.handler import feature_namespaces + +import urllib +import urllib2 + +import pkg_resources + +import portage +from portage.output import * +from portage.dbapi.porttree import _parse_uri_map +from portage.exception import InvalidDependString + +__version__ = "svn" + +settings = { + "brute-force-level" : 2, + "brute-force" : True, + "brute-force-crazy" : True, + "scan-dir" : True, + "format" : "pretty", + "verbose" : True, + "stop-when-found" : False, + "check-all-files" : False, +} + +output = EOutput() +output.quiet = not settings['verbose'] + +def cast_int_components(version): + for i, obj in enumerate(version): + try: + version[i] = int(obj) + except ValueError: + pass + return version + +def parse_version(version): + version = pkg_resources.parse_version(version) + #version = list(version) + #return cast_int_components(version) + return version + + +def template_from_url(url, version): + prefix, chunks = url.split('://') + chunks = chunks.split('/') + + for i in range(len(chunks)): + chunk = chunks[i] + + if not chunk: + continue + + # If it's the full version, it's easy + if version in chunk: + chunk = chunk.replace(version, '${PV}') + # For directories made from a part of the version + elif version.startswith(chunk): + full = split_version(version) + part = split_version(chunk) + + for j in range(min(len(full), len(part))): + + if part[j] != full[j]: + break + part[j] = '${%d}' % j + + chunk = join_version(part) + chunk = chunk.replace('}$', '}.$') + + chunks[i] = chunk + + return prefix + "://" + "/".join(chunks) + +def url_from_template(url, version): + components = split_version(version) + + url = url.replace('${PV}', version) + for i in range(len(components)): + url = url.replace('${%d}' % i, str(components[i])) + + return url + +# Stolen from distutils.LooseVersion +# Used for brute force to increment the version +def split_version(version): + component_re = re.compile(r'(\d+ | [a-z]+ | \.)', re.VERBOSE) + components = filter(lambda x: x and x != '.', component_re.split(version)) + for i in range(len(components)): + try: + components[i] = int(components[i]) + except ValueError: + pass + return components + +def join_version(components): + version = "" + for i in range(len(components)): + version += str(components[i]) + if i >= len(components) - 1: + break + if type(components[i]) != str and type(components[i + 1]) != str: + version += "." + return version + +def increment_version(components, level): + n = len(components) + + if level > n - 1 or level < 0: + raise Exception + + for i in range(n, level + 1, -1): + if type(components[i - 1]) == int: + components[i - 1] = 0 + + if type(components[level]) == int: + components[level] += 1 + + return components + +def gen_versions(components, level): + n = len(components) + depth = level + level = min(level, n) + + if not n: + return [] + + versions = [] + + for i in range(n, n - level, -1): + increment_version(components, i - 1) + for j in range(depth): + versions.append(list(components)) + increment_version(components, i - 1) + + return versions + +def tryurl(fileurl): + result = False + + output.ebegin("Trying: " + fileurl) + + try: + fp = urllib2.urlopen(fileurl, None, 5) + headers = fp.info() + + basename = os.path.basename(fileurl) + + if 'Content-disposition' in headers and basename not in headers['Content-disposition']: + result = False + elif 'Content-Length' in headers and headers['Content-Length'] == '0': + result = False + elif 'text/html' in headers['Content-Type']: + result = False + else: + result = True + except: + retult = False + + output.eend(errno.ENOENT if not result else 0) + + return result + +def regex_from_template(template): + template = re.escape(template) + template = template.replace('\$\{', '${') + template = template.replace('\}', '}') + template = template.replace('}\.$', '}.$') + template = re.sub(r'(\$\{\d+\}\.?)+', r'([\w\.\-]+?)', template) + #template = re.sub(r'(\$\{\d+\}\.+)+', '(.+?)\.', template) + #template = re.sub(r'(\$\{\d+\})+', '(.+?)', template) + template = template.replace('${PV}', r'([\w\.\-]+?)') + template = template + r'/?$' + return template + +def basedir_from_template(template): + idx = template.find('${') + if idx == -1: + return template + + idx = template[0:idx].rfind('/') + if idx == -1: + return "" + + return template[0:idx] + +def generate_scan_paths(url): + prefix, chunks = url.split('://') + chunks = chunks.split('/') + + steps = [] + + path = prefix + ":/" + for chunk in chunks: + if '${' in chunk: + steps.append((path, regex_from_template(chunk))) + path = "" + else: + path += "/" + path += chunk + return steps + +def scan_directory_recursive(url, steps, vmin, vmax): + if not steps: + return [] + + url += steps[0][0] + pattern = steps[0][1] + + steps = steps[1:] + + output.einfo("Scanning: %s" % url) + + try: + fp = urllib2.urlopen(url, None, 5) + except Exception, err: + return [] + + data = fp.read() + + results = [] + + if re.search("<\s*a\s+[^>]*href", data): + from BeautifulSoup import BeautifulSoup + + soup = BeautifulSoup(data) + + for link in soup.findAll('a'): + href = link.get("href") + if not href: + continue + if href.startswith(url): + href = href.replace(url, "", 1) + + match = re.match(pattern, href) + if match: + results.append((match.group(1), match.group(0))) + + elif url.startswith('ftp://'): # Probably a FTP Server + buf = StringIO.StringIO(data) + for line in buf.readlines(): + line = line.replace("\n", "").replace("\r", "") + match = re.search(pattern, line) + if match: + results.append((match.group(1), match.group(0))) + # add url + + versions = [] + + for version, path in results: + ver = parse_version(version) + if vmin and ver <= vmin: + continue + if vmax and ver >= vmax: + continue + + if not url.endswith('/') and not path.startswith('/'): + path = url + '/' + path + else: + path = url + path + + versions.append((path, version)) + if steps: + ret = scan_directory_recursive(path, steps, vmin, vmax) + versions.extend(ret) + return versions + +def scan_directory(cpv, fileurl, limit=None): + # Ftp: list dir + # Handle mirrors + if not settings["scan-dir"]: + return [] + + catpkg, ver, rev = portage.pkgsplit(cpv) + + template = template_from_url(fileurl, ver) + + if '${' not in template: + output.ewarn("Url doesn't seems to depend on version: %s not found in %s" + % (ver, fileurl)) + return [] + else: + output.einfo("Scanning: %s" % template) + + vmin = parse_version(ver) + + steps = generate_scan_paths(template) + return scan_directory_recursive("", steps, vmin, limit) + +def brute_force(cpv, fileurl, limit=None): + if not settings["brute-force"]: + return [] + + catpkg, ver, rev = portage.pkgsplit(cpv) + + components = split_version(ver) + versions = gen_versions(components, settings["brute-force-level"]) + + output.einfo("Generating version from " + ver) + + if not versions: + output.ewarn("Can't generate new versions from " + ver) + return [] + + template = template_from_url(fileurl, ver) + + if '${' not in template: + output.ewarn("Url doesn't seems to depend on version: %s not found in %s" + % (fileurl, ver)) + return [] + else: + output.einfo("Brute forcing: %s" % template) + + result = [] + + i = 0 + done = [] + while i < len(versions): + components = versions[i] + i += 1 + if components in done: + continue + done.append(tuple(components)) + + vstring = join_version(components) + version = parse_version(vstring) + + if limit and version >= limit: + continue + + url = url_from_template(template, vstring) + + if not tryurl(url): + continue + + result.append([url, vstring]) + + if settings["brute-force-crazy"]: + for v in gen_versions(components, settings["brute-force-level"]): + if v not in versions and tuple(v) not in done: + versions.append(v) + + if settings["stop-when-found"]: + break + + return result + +def euscan(cpv, portdir): + catpkg, ver, rev = portage.pkgsplit(cpv) + + if portdir: + portdb = portage.portdbapi(portdir) + else: + portdb = portage.portdbapi() + + src_uri, repo = portdb.aux_get(cpv, ['SRC_URI', 'repository']) + + metadata = { + "EAPI" : portage.settings["EAPI"], + "SRC_URI" : src_uri, + } + use = frozenset(portage.settings["PORTAGE_USE"].split()) + try: + alist = _parse_uri_map(cpv, metadata, use=use) + aalist = _parse_uri_map(cpv, metadata) + except InvalidDependString as e: + red("!!! %s\n" % str(e)) + red(_("!!! Invalid SRC_URI for '%s'.\n") % cpv) + del e + return + + if "mirror" in portage.settings.features: + fetchme = aalist + else: + fetchme = alist + + versions = [] + + for filename in fetchme: + for fileurl in fetchme[filename]: + if fileurl.startswith('mirror://'): + output.eerror('mirror:// scheme not supported (%s)' % fileurl) + continue + + # Try list dir + versions.extend(scan_directory(cpv, fileurl)) + + if versions and settings['stop-when-found']: + break + + # Try manual bump + versions.extend(brute_force(cpv, fileurl)) + + if versions and settings['stop-when-found']: + break + + if versions and not settings["check-all-files"]: + break + + newversions = {} + + for url, version in versions: + if version in newversions and len(url) < len(newversions[version]): + continue + newversions[version] = url + + for version in newversions: + print darkgreen("New Upstream Version: ") + green("%s" % version) + " %s" % newversions[version] + return versions + +class Metadata_XML(handler.ContentHandler): + _inside_herd="No" + _inside_maintainer="No" + _inside_email="No" + _inside_longdescription="No" + + _herd = [] + _maintainers = [] + _longdescription = "" + + def startElement(self, tag, attr): + if tag == "herd": + self._inside_herd="Yes" + if tag == "longdescription": + self._inside_longdescription="Yes" + if tag == "maintainer": + self._inside_maintainer="Yes" + if tag == "email": + self._inside_email="Yes" + + def endElement(self, tag): + if tag == "herd": + self._inside_herd="No" + if tag == "longdescription": + self._inside_longdescription="No" + if tag == "maintainer": + self._inside_maintainer="No" + if tag == "email": + self._inside_email="No" + + def characters(self, contents): + if self._inside_herd == "Yes": + self._herd.append(contents) + + if self._inside_longdescription == "Yes": + self._longdescription = contents + + if self._inside_maintainer=="Yes" and self._inside_email=="Yes": + self._maintainers.append(contents) + + +def check_metadata(cpv, portdir = None): + """Checks that the primary maintainer is still an active dev and list the herd the package belongs to""" + if not portdir: + portdb = portage.portdbapi() + repo, = portdb.aux_get(cpv, ['repository']) + portdir = portdb.getRepositoryPath(repo) + + metadata_file = portdir + "/" + portage.pkgsplit(cpv)[0] + "/metadata.xml" + + if not os.path.exists(metadata_file): + print darkgreen("Maintainer: ") + red("Error (Missing metadata.xml)") + return 1 + + parser = make_parser() + handler = Metadata_XML() + handler._maintainers = [] + parser.setContentHandler(handler) + parser.parse( metadata_file ) + + if handler._herd: + herds = ", ".join(handler._herd) + print darkgreen("Herd: ") + herds + else: + print darkgreen("Herd: ") + red("Error (No Herd)") + return 1 + + + if handler._maintainers: + print darkgreen("Maintainer: ") + ", ".join(handler._maintainers) + else: + print darkgreen("Maintainer: ") + "none" + + if len(handler._longdescription) > 1: + print darkgreen("Description: ") + handler._longdescription + print darkgreen("Location: ") + os.path.normpath(portdir + "/" + portage.pkgsplit(cpv)[0]) + + +def usage(code): + """Prints the uage information for this script""" + print green("euscan"), "(%s)" % __version__ + print + print "Usage: euscan [ebuild|[package-cat/]package[-version]]" + sys.exit(code) + + +# default color setup +if ( not sys.stdout.isatty() ) or ( portage.settings["NOCOLOR"] in ["yes","true"] ): + nocolor() + +def fc(x,y): + return cmp(y[0], x[0]) + +def main (): + if len( sys.argv ) < 2: + usage(1) + + for pkg in sys.argv[1:]: + #try: + if pkg.endswith('.ebuild'): + portdir = os.path.dirname(os.path.dirname(os.path.dirname(pkg))) + package_list = os.path.basname(pkg) + else: + portdir = None + print pkg + package_list = portage.portdb.xmatch("match-all", pkg) + + for cpv in package_list: + print darkgreen("Package: ") + cpv + #check_metadata(cpv, portdir) + euscan(cpv, portdir) + print "" + #except Exception, err: + # print red("Error: "+pkg+"\n") + # print err + + +if __name__ == '__main__': + main() diff --git a/web/euscan-update b/web/euscan-update new file mode 100755 index 0000000..23cc78f --- /dev/null +++ b/web/euscan-update @@ -0,0 +1,229 @@ +#!/usr/bin/env python + +import subprocess +import portage +import sqlite3 +import sys +import argparse + +def package_id_by_name(c, catpkg): + cat, pkg = catpkg.split('/') + + c.execute('SELECT id FROM packages WHERE category = ? AND package = ?', (cat, pkg)) + + row = c.fetchone() + + if row: + package_id = row[0] + else: + c.execute('INSERT OR IGNORE INTO packages (category, package) VALUES (?,?)', (cat, pkg)) + package_id = c.lastrowid + print '[e] %s/%s' % (cat, pkg) + + return package_id + +def store_package(c, cpv, slot): + catpkg, ver, rev = portage.pkgsplit(cpv) + cat, pkg = catpkg.split('/') + package_id = -1 + + package_id = package_id_by_name(c, catpkg) + + sql = 'INSERT OR IGNORE INTO versions (package_id, slot, revision, version, packaged) VALUES (?, ?, ?, ?, 1)' + c.execute(sql, (package_id, slot, rev, ver)) + + if c.lastrowid: + print '[v] %s:%s' % (cpv, slot) + +def portage_scan(db, package=None): + c = db.cursor() + + cmd = ['eix', '--format', '', '--pure-packages', '-x'] + if package: + cmd.append(package) + + output1 = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0] + + cmd = ['eix', '--format', '', '--pure-packages', '-x'] + if package: + cmd.append(package) + output2 = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0] + + output1 = output1.split('\n') + output2 = output2.split('\n') + + for i in range(0, len(output1)): + if not output1[i]: + continue + + cpv = output1[i] + slot = output2[i].split(':')[1] + store_package(c, cpv, slot) + + db.commit() + +def herd_id_by_name(cursor, herd): + cursor.execute('SELECT id FROM herds WHERE herd = ?', (herd,)) + + row = cursor.fetchone() + + if row: + herd_id = row[0] + else: + cursor.execute('INSERT INTO herds (herd) VALUES (?)', (herd,)) + herd_id = cursor.lastrowid + print '[h] %s' % (herd) + + return herd_id + +def maintainer_id_by_name(cursor, maintainer): + + cursor.execute('SELECT id FROM maintainers WHERE maintainer = ?', (maintainer,)) + + row = cursor.fetchone() + + if row: + maintainer_id = row[0] + else: + cursor.execute('INSERT INTO maintainers (maintainer) VALUES (?)', (maintainer,)) + maintainer_id = cursor.lastrowid + print '[m] %s' % (maintainer) + + return maintainer_id + + +def store_herd(cursor, catpkg, herd): + if herd == 'no-herd': + return + package_id = package_id_by_name(cursor, catpkg) + herd_id = herd_id_by_name(cursor, herd) + print catpkg, herd + cursor.execute('INSERT OR IGNORE INTO package_herds (herd_id, package_id) VALUES (?, ?)', (herd_id, package_id)) + +def store_maintainer(cursor, catpkg, maintainer): + if maintainer == 'None specified': + return + package_id = package_id_by_name(cursor, catpkg) + maintainer_id = maintainer_id_by_name(cursor, maintainer) + print catpkg, maintainer + cursor.execute('INSERT OR IGNORE INTO package_maintainers (maintainer_id, package_id) VALUES (?, ?)', (maintainer_id, package_id)) + +def metadata_scan_some(c, packages): + cmd = ['epkginfo'] + cmd.extend(packages[:-1]) + output = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0] + output = output.split('\n\n') + + for infos in output: + infos = infos.split('\n') + + package = packages.pop(0) + + for line in infos: + if line.startswith('Herd:'): + line = line.replace('Herd:', '').strip() + store_herd(c, package, line) + if line.startswith('Maintainer:'): + line = line.replace('Maintainer:', '').strip() + store_maintainer(c, package, line) + +def metadata_scan(db, package=None): + c = db.cursor() + + cmd = ['eix', '--only-names'] + + if package: + cmd.append(package) + output = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0] + packages = output.split('\n') + + tmp = [] + for package in packages: + tmp.append(package) + if len(tmp) > 10: + metadata_scan_some(c, tmp) + tmp = [] + + metadata_scan_some(c, tmp) + + db.commit() + +def version_id_by_version(cursor, package_id, version): + + cursor.execute('SELECT id, packaged FROM versions WHERE package_id = ? AND version = ?', (package_id, version)) + + row = cursor.fetchone() + + if row: + version_id = row[0] + packaged = row[1] + else: + cursor.execute('INSERT INTO versions (package_id, slot, revision, version, packaged) VALUES (?, ?, ?, ?, 0)', + (package_id, '', 'r0', version)) + version_id = cursor.lastrowid + packaged = 0 + + return version_id, packaged + +def upstream_scan(db, package): + c = db.cursor() + + cmd = ['eix', '--format', '', '--pure-packages'] + if package: + cmd.append(package) + output = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0] + packages = output.split('\n') + + for package in packages: + if not package.strip(): + continue + catpkg, ver, rev = portage.pkgsplit(package) + + cmd = ['../euscan', package] + output = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0] + output = output.split('\n') + for line in output: + if not line.startswith('New Upstream Version: '): + continue + line = line.replace('New Upstream Version: ', '').split(' ') + ver = line[0] + urls = line[1:] + + package_id = package_id_by_name(c, catpkg) + version_id, packaged= version_id_by_version(c, package_id, ver) + + if packaged: + continue + + print '[u] %s %s' % (ver, ' '.join(urls)) + + for url in urls: + c.execute('INSERT OR REPLACE INTO upstream_urls (version_id, url) VALUES (?, ?)', (version_id, url)) + db.commit() + + +def main(): + parser = argparse.ArgumentParser(description='Update euscan database.') + parser.add_argument('--skip-portage', action='store_true', help='Skip portage scan.') + parser.add_argument('--skip-metadata', action='store_true', help='Skip metadata scan.') + parser.add_argument('package', nargs='*', help='Only check updates for these packages') + + args = parser.parse_args() + + db = sqlite3.connect('euscan.db') + + if not args.package: + args.package = [None] + + for package in args.package: + if not args.skip_portage: + portage_scan(db, package) + if not args.skip_metadata: + metadata_scan(db, package) + + upstream_scan(db, package) + + db.close() + +if __name__ == '__main__': + main() diff --git a/web/euscan.sql b/web/euscan.sql new file mode 100644 index 0000000..a761d88 --- /dev/null +++ b/web/euscan.sql @@ -0,0 +1,54 @@ +CREATE TABLE IF NOT EXISTS "packages" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + "category" TEXT NOT NULL, + "package" TEXT NOT NULL +); + +CREATE UNIQUE INDEX IF NOT EXISTS "packages_catpkg" ON packages (category, package); + +CREATE TABLE IF NOT EXISTS "herds" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + "herd" TEXT NOT NULL +); + +CREATE INDEX IF NOT EXISTS "herds_herd" ON herds (herd); + +CREATE TABLE IF NOT EXISTS "maintainers" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + "maintainer" TEXT NOT NULL +); + +CREATE INDEX IF NOT EXISTS "maintainers_maintainer" ON maintainers (maintainer); + +CREATE TABLE IF NOT EXISTS "package_herds" ( + "herd_id" INTEGER NOT NULL, + "package_id" INTEGER NOT NULL, + PRIMARY KEY("herd_id", "package_id") +); + +CREATE TABLE IF NOT EXISTS "package_maintainers" ( + "maintainer_id" INTEGER NOT NULL, + "package_id" INTEGER NOT NULL, + PRIMARY KEY("maintainer_id", "package_id") +); + +CREATE TABLE IF NOT EXISTS "versions" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + "package_id" INTEGER NOT NULL, + "slot" TEXT NOT NULL, + "revision" TEXT NOT NULL, + "version" TEXT NOT NULL, + "packaged" INTEGER NOT NULL DEFAULT (0) +); + +CREATE INDEX IF NOT EXISTS "versions_packaged" on versions (package_id, packaged); +CREATE UNIQUE INDEX IF NOT EXISTS "versions_version" on versions (package_id, version, slot, revision); + +CREATE TABLE IF NOT EXISTS "upstream_urls" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + "version_id" INTEGER NOT NULL, + "url" TEXT NOT NULL +); + +CREATE INDEX IF NOT EXISTS "upstream_version" on upstream_urls (version_id); +CREATE UNIQUE INDEX IF NOT EXISTS "upstream_unique_urls" on upstream_urls (version_id, url); \ No newline at end of file diff --git a/web/index.php b/web/index.php new file mode 100644 index 0000000..d27ba45 --- /dev/null +++ b/web/index.php @@ -0,0 +1,207 @@ +' ?> + + + euscan + + + + + + +
+query($sql); + + echo ""; + while ($row = $results->fetchArray()) { + $new = $row['versions'] - $row['ebuilds']; + $color = $new == 0 ? 'green' : 'red'; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + } + echo "
CategoryEbuildsNew versions
{$row['category']}{$row['versions']}$new
"; +} else if ($act == "herds") { + $sql = "SELECT herd, COUNT(version) as versions, SUM(packaged) as ebuilds"; + $sql.= " FROM herds"; + $sql.= " JOIN package_herds ON herds.id = herd_id"; + $sql.= " JOIN packages ON package_herds.package_id = packages.id"; + $sql.= " JOIN versions ON versions.package_id = packages.id"; + $sql.= " GROUP BY herd"; + + $results = $db->query($sql); + + echo ""; + while ($row = $results->fetchArray()) { + $new = $row['versions'] - $row['ebuilds']; + $color = $new == 0 ? 'green' : 'red'; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + } + echo "
HerdEbuildsNew versions
{$row['herd']}{$row['versions']}$new
"; +} else if ($act == "maintainers") { + $sql = "SELECT maintainer, COUNT(version) as versions, SUM(packaged) as ebuilds"; + $sql.= " FROM maintainers"; + $sql.= " JOIN package_maintainers ON maintainers.id = maintainer_id"; + $sql.= " JOIN packages ON package_maintainers.package_id = packages.id"; + $sql.= " JOIN versions ON versions.package_id = packages.id"; + $sql.= " GROUP BY maintainer"; + + $results = $db->query($sql); + + echo ""; + while ($row = $results->fetchArray()) { + $new = $row['versions'] - $row['ebuilds']; + $color = $new == 0 ? 'green' : 'red'; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + } + echo "
MaintainerEbuildsNew versions
{$row['maintainer']}{$row['versions']}$new
"; +} else if ($act == "categories") { + $sql = "SELECT category, COUNT(version) as versions, SUM(packaged) as ebuilds"; + $sql.= " FROM packages JOIN versions ON package_id = packages.id"; + $sql.= " GROUP BY category"; + + $results = $db->query($sql); + + echo ""; + while ($row = $results->fetchArray()) { + $new = $row['versions'] - $row['ebuilds']; + $color = $new == 0 ? 'green' : 'red'; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + } + echo "
CategoryEbuildsNew versions
{$row['category']}{$row['versions']}$new
"; +} else if ($act == 'packages') { + $db_cat = $db->escapeString($cat); + $db_herd = $db->escapeString($herd); + $db_mtnr = $db->escapeString($mtnr); + + if ($db_cat) { + $sql = "SELECT category, package, COUNT(version) as versions, SUM(packaged) as ebuilds"; + $sql.= " FROM packages JOIN versions ON package_id = packages.id"; + $sql.= " WHERE category = '$db_cat'"; + $sql.= " GROUP BY category, package"; + } else if ($db_herd) { + $sql = "SELECT category, package, COUNT(version) as versions, SUM(packaged) as ebuilds"; + $sql.= " FROM herds"; + $sql.= " JOIN package_herds ON herds.id = herd_id"; + $sql.= " JOIN packages ON package_herds.package_id = packages.id"; + $sql.= " JOIN versions ON versions.package_id = packages.id"; + $sql.= " WHERE herd LIKE '$db_herd'"; + $sql.= " GROUP BY category, package"; + } else if ($db_mtnr) { + $sql = "SELECT category, package, COUNT(version) as versions, SUM(packaged) as ebuilds"; + $sql.= " FROM maintainers"; + $sql.= " JOIN package_maintainers ON maintainers.id = maintainer_id"; + $sql.= " JOIN packages ON package_maintainers.package_id = packages.id"; + $sql.= " JOIN versions ON versions.package_id = packages.id"; + $sql.= " WHERE maintainer LIKE '%$db_mtnr%'"; + $sql.= " GROUP BY category, package"; + } + + if ($sql) { + $results = $db->query($sql); + + echo ""; + while ($row = $results->fetchArray()) { + $new = $row['versions'] - $row['ebuilds']; + $catpkg = "{$row['category']}/{$row['package']}"; + $color = $new == 0 ? 'green' : 'red'; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + } + echo "
CategoryEbuildsNew versions
$catpkg{$row['versions']}$new
"; + } +} else if ($act == 'package') { + $pkg = explode("/", $pkg); + + if (count($pkg) == 2) { + $cat = $db->escapeString($pkg[0]); + $pkg = $db->escapeString($pkg[1]); + } else { + $cat = $pkg = ""; + } + + $sql = "SELECT * FROM packages WHERE category = '$cat' AND package = '$pkg'"; + $infos = $db->query($sql); + $infos = $infos->fetchArray(); + + if ($infos) { + $sql = "SELECT * FROM versions WHERE package_id = ${infos['id']} AND packaged = 1"; + $results = $db->query($sql); + + echo '

Packaged versions:

    '; + while ($version = $results->fetchArray()) { + echo "
  • ${version['version']}-${version['revision']}:${version['slot']}
  • "; + } + + $sql = "SELECT * FROM versions "; + $sql.= "JOIN upstream_urls ON versions.id = version_id "; + $sql.= "WHERE package_id = ${infos['id']} AND packaged = 0"; + $results = $db->query($sql); + + echo '
'; + echo '

Upstream versions:

    '; + + while ($version = $results->fetchArray()) { + echo "
  • ${version['version']} - ${version['url']}
  • "; + } + + echo '
'; + } else { + echo '
Invalid package
'; + } +} + +?> +
+ + + + + diff --git a/web/sorttable.js b/web/sorttable.js new file mode 100644 index 0000000..25bccb2 --- /dev/null +++ b/web/sorttable.js @@ -0,0 +1,493 @@ +/* + SortTable + version 2 + 7th April 2007 + Stuart Langridge, http://www.kryogenix.org/code/browser/sorttable/ + + Instructions: + Download this file + Add to your HTML + Add class="sortable" to any table you'd like to make sortable + Click on the headers to sort + + Thanks to many, many people for contributions and suggestions. + Licenced as X11: http://www.kryogenix.org/code/browser/licence.html + This basically means: do what you want with it. +*/ + + +var stIsIE = /*@cc_on!@*/false; + +sorttable = { + init: function() { + // quit if this function has already been called + if (arguments.callee.done) return; + // flag this function so we don't do the same thing twice + arguments.callee.done = true; + // kill the timer + if (_timer) clearInterval(_timer); + + if (!document.createElement || !document.getElementsByTagName) return; + + sorttable.DATE_RE = /^(\d\d?)[\/\.-](\d\d?)[\/\.-]((\d\d)?\d\d)$/; + + forEach(document.getElementsByTagName('table'), function(table) { + if (table.className.search(/\bsortable\b/) != -1) { + sorttable.makeSortable(table); + } + }); + + }, + + makeSortable: function(table) { + if (table.getElementsByTagName('thead').length == 0) { + // table doesn't have a tHead. Since it should have, create one and + // put the first table row in it. + the = document.createElement('thead'); + the.appendChild(table.rows[0]); + table.insertBefore(the,table.firstChild); + } + // Safari doesn't support table.tHead, sigh + if (table.tHead == null) table.tHead = table.getElementsByTagName('thead')[0]; + + if (table.tHead.rows.length != 1) return; // can't cope with two header rows + + // Sorttable v1 put rows with a class of "sortbottom" at the bottom (as + // "total" rows, for example). This is B&R, since what you're supposed + // to do is put them in a tfoot. So, if there are sortbottom rows, + // for backwards compatibility, move them to tfoot (creating it if needed). + sortbottomrows = []; + for (var i=0; i5' : ' ▴'; + this.appendChild(sortrevind); + return; + } + if (this.className.search(/\bsorttable_sorted_reverse\b/) != -1) { + // if we're already sorted by this column in reverse, just + // re-reverse the table, which is quicker + sorttable.reverse(this.sorttable_tbody); + this.className = this.className.replace('sorttable_sorted_reverse', + 'sorttable_sorted'); + this.removeChild(document.getElementById('sorttable_sortrevind')); + sortfwdind = document.createElement('span'); + sortfwdind.id = "sorttable_sortfwdind"; + sortfwdind.innerHTML = stIsIE ? ' 6' : ' ▾'; + this.appendChild(sortfwdind); + return; + } + + // remove sorttable_sorted classes + theadrow = this.parentNode; + forEach(theadrow.childNodes, function(cell) { + if (cell.nodeType == 1) { // an element + cell.className = cell.className.replace('sorttable_sorted_reverse',''); + cell.className = cell.className.replace('sorttable_sorted',''); + } + }); + sortfwdind = document.getElementById('sorttable_sortfwdind'); + if (sortfwdind) { sortfwdind.parentNode.removeChild(sortfwdind); } + sortrevind = document.getElementById('sorttable_sortrevind'); + if (sortrevind) { sortrevind.parentNode.removeChild(sortrevind); } + + this.className += ' sorttable_sorted'; + sortfwdind = document.createElement('span'); + sortfwdind.id = "sorttable_sortfwdind"; + sortfwdind.innerHTML = stIsIE ? ' 6' : ' ▾'; + this.appendChild(sortfwdind); + + // build an array to sort. This is a Schwartzian transform thing, + // i.e., we "decorate" each row with the actual sort key, + // sort based on the sort keys, and then put the rows back in order + // which is a lot faster because you only do getInnerText once per row + row_array = []; + col = this.sorttable_columnindex; + rows = this.sorttable_tbody.rows; + for (var j=0; j 12) { + // definitely dd/mm + return sorttable.sort_ddmm; + } else if (second > 12) { + return sorttable.sort_mmdd; + } else { + // looks like a date, but we can't tell which, so assume + // that it's dd/mm (English imperialism!) and keep looking + sortfn = sorttable.sort_ddmm; + } + } + } + } + return sortfn; + }, + + getInnerText: function(node) { + // gets the text we want to use for sorting for a cell. + // strips leading and trailing whitespace. + // this is *not* a generic getInnerText function; it's special to sorttable. + // for example, you can override the cell text with a customkey attribute. + // it also gets .value for fields. + + hasInputs = (typeof node.getElementsByTagName == 'function') && + node.getElementsByTagName('input').length; + + if (node.getAttribute("sorttable_customkey") != null) { + return node.getAttribute("sorttable_customkey"); + } + else if (typeof node.textContent != 'undefined' && !hasInputs) { + return node.textContent.replace(/^\s+|\s+$/g, ''); + } + else if (typeof node.innerText != 'undefined' && !hasInputs) { + return node.innerText.replace(/^\s+|\s+$/g, ''); + } + else if (typeof node.text != 'undefined' && !hasInputs) { + return node.text.replace(/^\s+|\s+$/g, ''); + } + else { + switch (node.nodeType) { + case 3: + if (node.nodeName.toLowerCase() == 'input') { + return node.value.replace(/^\s+|\s+$/g, ''); + } + case 4: + return node.nodeValue.replace(/^\s+|\s+$/g, ''); + break; + case 1: + case 11: + var innerText = ''; + for (var i = 0; i < node.childNodes.length; i++) { + innerText += sorttable.getInnerText(node.childNodes[i]); + } + return innerText.replace(/^\s+|\s+$/g, ''); + break; + default: + return ''; + } + } + }, + + reverse: function(tbody) { + // reverse the rows in a tbody + newrows = []; + for (var i=0; i=0; i--) { + tbody.appendChild(newrows[i]); + } + delete newrows; + }, + + /* sort functions + each sort function takes two parameters, a and b + you are comparing a[0] and b[0] */ + sort_numeric: function(a,b) { + aa = parseFloat(a[0].replace(/[^0-9.-]/g,'')); + if (isNaN(aa)) aa = 0; + bb = parseFloat(b[0].replace(/[^0-9.-]/g,'')); + if (isNaN(bb)) bb = 0; + return aa-bb; + }, + sort_alpha: function(a,b) { + if (a[0]==b[0]) return 0; + if (a[0] 0 ) { + var q = list[i]; list[i] = list[i+1]; list[i+1] = q; + swap = true; + } + } // for + t--; + + if (!swap) break; + + for(var i = t; i > b; --i) { + if ( comp_func(list[i], list[i-1]) < 0 ) { + var q = list[i]; list[i] = list[i-1]; list[i-1] = q; + swap = true; + } + } // for + b++; + + } // while(swap) + } +} + +/* ****************************************************************** + Supporting functions: bundled here to avoid depending on a library + ****************************************************************** */ + +// Dean Edwards/Matthias Miller/John Resig + +/* for Mozilla/Opera9 */ +if (document.addEventListener) { + document.addEventListener("DOMContentLoaded", sorttable.init, false); +} + +/* for Internet Explorer */ +/*@cc_on @*/ +/*@if (@_win32) + document.write("