--- /dev/null
+include builddiff/templates/*.html
+
--- /dev/null
+
+__all__ = ['render', 'repo']
--- /dev/null
+from jinja2 import Environment, FileSystemLoader
+import os
+
+_SETTINGS = {
+ 'template_path': os.path.join(os.path.dirname(__file__), 'templates'),
+ }
+
+def output_html(template_name, **kwargs):
+ """Render a template file with given kwargs"""
+ env = Environment(
+ loader = FileSystemLoader(_SETTINGS['template_path']),
+ # auto_reload = True,
+ )
+ template = env.get_template(template_name)
+ return template.render(kwargs)
+
--- /dev/null
+from . import utils
+from .render import output_html
+
+import gzip
+import json
+import os
+import tempfile
+import urllib2
+import xml.etree.cElementTree as ET
+
+
+class RepoError(Exception):
+ """Local custom Exception class, handle Repo Errors"""
+ pass
+
+
+def _get_primary_md(url, workspace, name):
+ """Get primary.xml.gz file from remote repodata directory"""
+ tree = ET.ElementTree(file=_download(url + '/repodata/repomd.xml', \
+ workspace, name))
+ for elem in tree.iter(tag='{http://linux.duke.edu/metadata/repo}data'):
+ if elem.attrib['type'] == 'primary':
+ for c in elem:
+ if c.tag == '{http://linux.duke.edu/metadata/repo}location':
+ href = c.attrib['href']
+ if href:
+ return _download(url + href, workspace, \
+ href.split('/')[-1])
+ else:
+ raise RepoError('Repo primary metadata can\'t be found !')
+
+def _download(url, workspace, name):
+ """Download needed xml file to local by given url"""
+ xs_file = os.path.join(workspace, name)
+
+ try:
+ rf = urllib2.urlopen(url)
+ except urllib2.HTTPError:
+ xs_file = None
+ else:
+ with open(xs_file, 'wb') as xs:
+ xs.write(rf.read())
+
+ return xs_file
+
+
+class Repo(object):
+ """Stuff packages' info"""
+
+ def __init__(self, url):
+ workspace = tempfile.mkdtemp(dir='/var/tmp')
+ primary_md = _get_primary_md(url, workspace, 'repomd.xml')
+ self._et = utils.xml2obj(gzip.open(primary_md))
+
+ @property
+ def packages(self):
+ packages_info = {}
+ for _package in self._et.package:
+ packages_info[_package.name] = (packages_info.get(_package.name) \
+ or []) + [_package]
+ return packages_info
+
+def diff_to_JSON(old_url, new_url):
+ """Output diffs' json format"""
+
+ if not old_url or not new_url:
+ return
+
+ old, new = Repo(old_url).packages, Repo(new_url).packages
+
+ added, removed, modified, rebuilded = [], [], [], []
+ package_names = set(old.keys() + new.keys())
+
+ def _pair_old_new():
+ for name in package_names:
+ if old.get(name) is None:
+ for pkg in new[name]:
+ yield (None, pkg)
+ elif new.get(name) is None:
+ for pkg in old[name]:
+ yield (pkg, None)
+ else:
+ for old_pkg in old[name]:
+ for new_pkg in new[name]:
+ if old_pkg.version.ver == new_pkg.version.ver:
+ yield (old_pkg, new_pkg)
+ old[name].remove(old_pkg)
+ new[name].remove(new_pkg)
+ for pair in map(None, old[name], new[name]):
+ yield pair
+ for old_pkg, new_pkg in _pair_old_new():
+ if old_pkg is None:
+ added.append(new_pkg)
+ elif new_pkg is None:
+ removed.append(old_pkg)
+ elif old_pkg.version.ver == new_pkg.version.ver and \
+ old_pkg.version.vcs == new_pkg.version.vcs:
+ rebuilded.append((old_pkg, new_pkg))
+ else:
+ modified.append((old_pkg, new_pkg))
+
+ obj = {'repo': {'old': old_url, 'new': new_url},
+ 'diff': {
+ 'added': [{'oldpkg': None, 'newpkg': {'name': _new.name, \
+ 'version': {'epoch': _new.version.epoch, \
+ 'rel': _new.version.rel, 'ver': _new.version.rel}, \
+ 'vcs': _new.version.vcs}, 'codediff': None} \
+ for _new in added],
+ 'removed': [{'oldpkg': {'name': _old.name, \
+ 'version': {'epoch': _old.version.epoch, \
+ 'rel': _old.version.rel, 'ver': _old.version.ver}, \
+ 'vcs': _old.version.vcs}, 'newpkg': None, \
+ 'codediff': None} for _old in added],
+ 'modified': [{'oldpkg': {'name': _old.name, \
+ 'version': {'epoch': _old.version.epoch, \
+ 'rel': _old.version.rel, 'ver': _old.version.ver}, \
+ 'vcs': _old.version.vcs}, \
+ 'newpkg': {'name': _new.name, \
+ 'version': {'epoch': _new.version.epoch, \
+ 'rel': _new.version.rel, 'ver': _new.version.ver},
+ 'vcs': _new.version.vcs}, 'codediff': None} \
+ for _old, _new in modified],
+ 'rebuilded': [{'oldpkg': {'name': _old.name, \
+ 'version': {'epoch': _old.version.epoch, \
+ 'rel': _old.version.rel, 'ver': _old.version.ver}, \
+ 'vcs': _old.version.vcs}, \
+ 'newpkg': {'name': _new.name, \
+ 'version': {'epoch': _new.version.epoch, \
+ 'rel': _new.version.rel, 'ver': _new.version.ver},
+ 'vcs': _new.version.vcs}, 'codediff': None} \
+ for _old, _new in rebuilded],
+ }
+ }
+
+ return json.dumps(obj, indent=4)
+
+def diff_to_HTML(old_url, new_url):
+ """Output diffs' html format"""
+
+ data = json.loads(diff_to_JSON(old_url, new_url))
+
+ context = {'old_url': old_url,
+ 'new_url': new_url,
+ 'diff': data['diff'],
+ }
+
+ return output_html('diff.html', **context)
+
--- /dev/null
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+ <link href="theme/html-reset.css" media="screen" rel="stylesheet" type="text/css" />
+ <link href="theme/layout-liquid.css" media="screen" rel="stylesheet" type="text/css" />
+ <link href="theme/tizen.css" media="screen" rel="stylesheet" type="text/css" />
+ <link href="theme/style.css" media="screen" rel="stylesheet" type="text/css" />
+ <script type="text/javascript" src="theme/js/shCore.js"></script>
+ <script type="text/javascript" src="theme/js/shBrushDiff.js"></script>
+ <link href="theme/css/shCore.css" rel="stylesheet" type="text/css" />
+ <link href="theme/css/shThemeDefault.css" rel="stylesheet" type="text/css" />
+</head>
+<body>
+ <script type="text/javascript">SyntaxHighlighter.all()</script>
+ <div id="page-wrapper">
+ <div id="page">
+ <div id="header">
+ <div class="section clearfix">
+ <h1 id="logo">
+ <a href="http://www.tizen.org"><img alt="Tizen-logo" src="theme/tizen-logo.png" width="180px" height="50px"></a>
+ </h1>
+ </div>
+ </div>
+ </div>
+ </div>
+ <h1>
+ Repository Difference and Changelogs
+ </h1>
+ <a href="index.html">Go back</a>...<br>
+ <p>Difference between
+ <a href="{{old_url}}">{{ old_url }}</a> and <a href="{{new_url}}">{{ new_url }}</a>
+ </p>
+ <h3>Highlights</h3>
+ <ul>
+ <li><a href="#added">Added Packages: {{ diff['added']|count }}</a></li>
+ <li><a href="#removed">Removed Packages: {{ diff['removed']|count }}</a></li>
+ <li><a href="#modified">Modified packages: {{ diff['modified']|count }}</a></li>
+ <li><a href="#rebuilded">Packages with Rebuilds: {{ diff['rebuilded']|count }}</a></li>
+ </ul>
+</body>
+</html>
+
--- /dev/null
+import re
+import xml.sax.handler
+
+def xml2obj(src):
+ """
+ A simple function to converts XML data into native Python object,
+ from http://goo.gl/Ymc6Nl, Licensed under the PSF License.
+ """
+
+ non_id_char = re.compile('[^_0-9a-zA-Z]')
+ def _name_mangle(name):
+ return non_id_char.sub('_', name)
+
+ class DataNode(object):
+ def __init__(self):
+ self._attrs = {} # XML attributes and child elements
+ self.data = None # child text data
+ def __len__(self):
+ # treat single element as a list of 1
+ return 1
+ def __getitem__(self, key):
+ if isinstance(key, basestring):
+ return self._attrs.get(key, None)
+ else:
+ return [self][key]
+ def __contains__(self, name):
+ return self._attrs.has_key(name)
+ def __nonzero__(self):
+ return bool(self._attrs or self.data)
+ def __getattr__(self, name):
+ if name.startswith('__'):
+ # need to do this for Python special methods???
+ raise AttributeError(name)
+ return self._attrs.get(name, None)
+ def _add_xml_attr(self, name, value):
+ if name in self._attrs:
+ # multiple attribute of the same name are represented by a list
+ children = self._attrs[name]
+ if not isinstance(children, list):
+ children = [children]
+ self._attrs[name] = children
+ children.append(value)
+ else:
+ self._attrs[name] = value
+ def __str__(self):
+ return self.data or ''
+ def __repr__(self):
+ items = sorted(self._attrs.items())
+ if self.data:
+ items.append(('data', self.data))
+ return u'{%s}' % ', '.join([u'%s:%s' % (k, repr(v)) for k, v in items])
+
+ class TreeBuilder(xml.sax.handler.ContentHandler):
+ def __init__(self):
+ self.stack = []
+ self.root = DataNode()
+ self.current = self.root
+ self.text_parts = []
+ def startElement(self, name, attrs):
+ self.stack.append((self.current, self.text_parts))
+ self.current = DataNode()
+ self.text_parts = []
+ # xml attributes --> python attributes
+ for k, v in attrs.items():
+ self.current._add_xml_attr(_name_mangle(k), v)
+ def endElement(self, name):
+ text = ''.join(self.text_parts).strip()
+ if text:
+ self.current.data = text
+ if self.current._attrs:
+ obj = self.current
+ else:
+ # a text only node is simply represented by the string
+ obj = text or ''
+ self.current, self.text_parts = self.stack.pop()
+ self.current._add_xml_attr(_name_mangle(name), obj)
+ def characters(self, content):
+ self.text_parts.append(content)
+
+ builder = TreeBuilder()
+ if isinstance(src, basestring):
+ xml.sax.parseString(src, builder)
+ else:
+ xml.sax.parse(src, builder)
+ return builder.root._attrs.values()[0]
+
--- /dev/null
+* Wed Nov 20 2013 Lingchao Xin <lingchaox.xin@intel.com> - 0.1
+ - Initial packaging
--- /dev/null
+#
+# spec file for package builddiff
+#
+# Copyright (c) 2013 Lingchao Xin <lingchaox.xin@intel.com>.
+#
+
+Name: builddiff
+Version: 0.1
+Release: 0
+License: GPL-2.0+
+Summary: Generate image and repo diffs
+Url: http://www.tizen.org
+Group: Development/Tools/Other
+Source: %{name}-%{version}.tar.gz
+BuildRequires: python-devel
+Requires: python >= 2.7
+Requires: python-jinja2
+BuildRoot: %{_tmppath}/%{name}-%{version}-build
+BuildArch: noarch
+%{!?python_sitelib: %define python_sitelib %(python -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")}
+
+%description
+Builddiff is used to generate image and repo diffs.
+
+%prep
+%setup -q
+
+%build
+CFLAGS="%{optflags}" python setup.py build
+
+%install
+python setup.py install --prefix=%{_prefix} --root=%{buildroot}
+
+%clean
+rm -rf %{buildroot}
+
+%files
+%defattr(-,root,root)
+%{_bindir}/*
+%{python_sitelib}/*
+
--- /dev/null
+#!/usr/bin/env python
+
+try:
+ from setuptools import setup
+except ImportError:
+ from distutils.core import setup
+
+setup(name = 'builddiff',
+ version = '0.1',
+ description = 'Generate image and repo diffs',
+ author = 'Lingchao Xin',
+ author_email = 'lingchaox.xin@intel.com',
+ url = 'http://www.tizen.org/',
+ scripts = ['tools/repodiff',],
+ packages=['builddiff',],
+ include_package_data=True,
+ install_requires=['Jinja2>=2.6'],
+ zip_safe=False,
+)
+
--- /dev/null
+#!/usr/bin/env python
+
+from builddiff.repo import diff_to_JSON, diff_to_HTML
+
+import argparse
+import sys
+
+def main(argv):
+ description = 'Diff two repos with different urls'
+ parser = argparse.ArgumentParser(description=description)
+ parser.add_argument(dest='old', help='old repo')
+ parser.add_argument(dest='new', help='new repo')
+ parser.add_argument('--json', help='output json diffs', action='store_true')
+ args = parser.parse_args(argv)
+
+ if args.json:
+ print diff_to_JSON(args.old, args.new)
+ else:
+ print diff_to_HTML(args.old, args.new)
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))
+