Initial commit.

This commit is contained in:
Dan Prince 2011-11-20 21:59:29 -05:00
commit 2f441a5a7e
30 changed files with 458 additions and 0 deletions

19
README.md Normal file
View File

@ -0,0 +1,19 @@
# ReviewDay
HTML report generator for OpenStack code reviews. Launchpad meets SmokeStack and Gerrit. Based on and inspired by 'reviewlist' by Thierry Carez.
## Description
Its early in the morning and you just got an email from Soren. Oh no... Today is your review day!
Gerrit got you down?
Launchpad too slow?
Unit tests taking too long?
Just want to see the big picture?
What would happen if Launchpad met SmokeStack and Gerrit?
REVIEWDAY!

24
bin/reviewday Executable file
View File

@ -0,0 +1,24 @@
#!/usr/bin/env python
from datetime import datetime
import reviewday
lp = reviewday.LaunchPad()
smoker = reviewday.SmokeStack('http://smokestack.openstack.org/jobs.json?limit=10000')
projects = {}
for project in ['nova', 'glance', 'keystone']:
if project not in projects:
projects[project] = []
for review in reviewday.reviews(project):
try:
mp = reviewday.MergeProp(lp, smoker, review)
projects[project].append(mp)
except:
print 'Error creating merge prop %s' % review
raise
dts = str(datetime.utcnow())[0:19]
name_space = {"projects": projects, "dts": dts}
reviewday.create_report(name_space)

5
reviewday/__init__.py Normal file
View File

@ -0,0 +1,5 @@
from gerrit import reviews
from util import create_report
from launchpad import LaunchPad
from mergeprop import MergeProp
from smokestack import SmokeStack

19
reviewday/gerrit.py Normal file
View File

@ -0,0 +1,19 @@
import subprocess
import json
def reviews(project, status="open", branch="master"):
arr = []
cmd = 'ssh review gerrit' \
' query "status: %s project: openstack/%s branch: %s"' \
' --current-patch-set --format JSON' \
% (status, project, branch)
p = subprocess.Popen([cmd], shell=True, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout = p.stdout
for line in stdout.readlines():
review = json.loads(line)
if 'project' in review:
arr.append(review)
return arr

16
reviewday/html_helper.py Normal file
View File

@ -0,0 +1,16 @@
# Helper functions to help generate the HTML report
def job_data_for_type(jobs, job_type):
""" Return a reference to the first job of the specified type. """
for job in jobs:
for jt, data in job.iteritems():
if jt == job_type:
return data
def fail_status(job_data):
""" Return a reference to the first job of the specified type. """
if job_data['status'] == 'Failed':
return '<font style="color: #FF0000;">(Fail)</font>'
return ''

27
reviewday/launchpad.py Normal file
View File

@ -0,0 +1,27 @@
from launchpadlib.launchpad import Launchpad
class LaunchPad(object):
def __init__(self):
self.lp = Launchpad.login_anonymously('reviewday', 'production',
'~/.launchpadlib-reviewday', version="devel")
self.spec_cache = {}
def bug(self, id):
return self.lp.bugs[id]
def project(self, id):
return self.lp.projects[id]
def specifications(self, project):
if project not in self.spec_cache:
specs = self.project(project).valid_specifications
self.spec_cache[project] = specs
return self.spec_cache[project]
def specification(self, project, spec_name):
specs = self.specifications(project)
for spec in specs:
if spec.name == spec_name:
return spec

56
reviewday/mergeprop.py Normal file
View File

@ -0,0 +1,56 @@
class MergeProp(object):
def _calc_score(self, lp, project, topic):
cause = 'No link'
try:
if topic.find('bug/') == 0:
bug = lp.bug(topic[4:])
#FIXME: bug.importance doesn't seem to work but it should?
cause = '%s bugfix' % bug.bug_tasks[0].importance
elif topic.find('bp/') == 0:
spec = lp.specification(project, topic[3:])
if spec:
cause = '%s feature' % spec.priority
else:
spec = lp.specification(project, topic)
if spec:
cause = '%s feature' % spec.priority
except:
print 'WARNING: unabled to find cause for %s' % topic
cause = 'No link'
cause_score = {
'Regression hotfix': 350,
'Critical bugfix': 340,
'Essential feature': 330,
'High feature': 230,
'Medium feature': 180,
'High bugfix': 130,
'Low feature': 100,
'Medium bugfix': 70,
'Low bugfix': 50,
'Undefined feature': 40,
'Wishlist bugfix': 35,
'Undecided bugfix': 30,
'Untargeted feature': 10,
'No link': 0,
}
return (cause, cause_score[cause])
def __init__(self, lp, smoker, review):
self.owner_name = review['owner']['name']
self.url = review['url']
self.subject = review['subject']
self.project = review['project'][10:]
if 'topic' in review:
self.topic = review['topic']
else:
self.topic = ''
self.revision = review['currentPatchSet']['revision']
self.refspec = review['currentPatchSet']['ref']
self.number = review['number']
cause, score = self._calc_score(lp, self.project, self.topic)
self.score = score
self.cause = cause
self.jobs = smoker.jobs(self.revision[:7])

115
reviewday/report.html Normal file
View File

@ -0,0 +1,115 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" dir="ltr">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<base href="."/>
<title>OpenStack branch reviews</title>
<link rel="shortcut icon"
href="https://blueprints.launchpad.net/@@/launchpad.png"/>
<link type="text/css" rel="stylesheet" media="screen,print" href="combo.css"/>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.5.1/jquery.min.js" type="text/javascript"></script>
<script type="text/javascript" src="sorting.js"></script>
</head>
<body id="document" class="tab-specifications">
<div class="yui-d0">
#for $project, $mergeprops in $projects.iteritems():
<a name="$project"></a>
<div class="flowed-block wide">
<h1>$project branch reviews</h1>
<ol class="breadcrumbs">
<li>Page refreshed at $dts</li>
<li>$len($mergeprops) active reviews</li>
</ol><br/>
</div>
<table class="listing sortable" summary="$project reviews">
<thead>
<tr>
<th><a href="#" class="sortheader"
onclick="ts_resortTable(this); return false;">Type / Subject<img
class="sortarrow" src="arrowBlank" height="6" width="9"/></a></th>
<!--
<th><a href="#" class="sortheader"
onclick="ts_resortTable(this); return false;">Patchsize<img
class="sortarrow" src="arrowBlank" height="6" width="9"></a></th>
<th><a href="#" class="sortheader"
onclick="ts_resortTable(this); return false;">Age<img
class="sortarrow" src="arrowBlank" height="6" width="9"></a></th>
-->
<th><a href="#" class="sortheader"
onclick="ts_resortTable(this); return false;">Registrant<img
class="sortarrow" src="arrowBlank" height="6" width="9"/></a></th>
<th><a href="#" class="sortheader" id="$project-sortscore"
onclick="ts_resortTable(this); return false;">Score<img
class="sortarrow" src="arrowBlank" height="6" width="9"/></a></th>
<th><a href="#" class="sortheader"
onclick="ts_resortTable(this); return false;">SmokeStack Test Results<img
class="sortarrow" src="arrowBlank" height="6" width="9"/></a></th>
</tr>
</thead>
<tbody>
#for $mp in $mergeprops
<tr>
<td>
<span class="sortkey">12</span>
<img src="${mp.cause.replace(' ', '').upper()}.png" title="$mp.cause"/>
<a href="$mp.url" title="$mp.subject">$mp.subject[:60]</a>
</td>
<!--
<td>
<span class="sortkey">3176</span>
3176 lines
</td>
<td>
<span class="sortkey">60</span>
60 days
</td>
-->
<td>
$mp.owner_name
</td>
<td>
<span class="sortkey">$mp.score</span>
$mp.score
</td>
<td>
#set $unit_data = $helper.job_data_for_type($mp.jobs, "job_unit_tester")
#if $unit_data
<a href="http://smokestack.openstack.org/?go=/jobs/$unit_data['id']" title="$unit_data['msg']">Unit$helper.fail_status($unit_data)</a>&nbsp;
#end if
#set $libvirt_data = $helper.job_data_for_type($mp.jobs, "job_vpc")
#if $libvirt_data
<a href="http://smokestack.openstack.org/?go=/jobs/$libvirt_data['id']" title="$libvirt_data['msg']">Libvirt$helper.fail_status($libvirt_data)</a>&nbsp;
#end if
#set $xenserver_data = $helper.job_data_for_type($mp.jobs, "job_xen_hybrid")
#if $xenserver_data
<a href="http://smokestack.openstack.org/?go=/jobs/$xenserver_data['id']" title="$xenserver_data['msg']">XenServer$helper.fail_status($xenserver_data)</a>&nbsp;
#end if
</td>
</tr>
#end for
</tbody>
</table>
<script type="text/javascript">
ts_resortTable(document.getElementById("$project-sortscore"))
ts_resortTable(document.getElementById("$project-sortscore"))
</script>
#end for
</div>
</body></html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 691 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 707 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 615 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 703 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 B

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,45 @@
// sorttable/sorttable-min.js
var SORT_COLUMN_INDEX;var arrowUp="arrowUp";var arrowDown="arrowDown";var arrowBlank="arrowBlank";function trim(str){return str.replace(/^\s*|\s*$/g,"");}
function sortables_init(){if(!document.getElementsByTagName)return;tbls=document.getElementsByTagName("table");for(ti=0;ti<tbls.length;ti++){thisTbl=tbls[ti];if(((' '+thisTbl.className+' ').indexOf(" sortable ")!=-1)&&(thisTbl.id)){ts_makeSortable(thisTbl);}}}
function ts_makeSortable(table){if(table.tHead&&table.tHead.rows&&table.tHead.rows.length>0){var firstRow=table.tHead.rows[0];}else if(table.rows&&table.rows.length>0){var firstRow=table.rows[0];}
if(!firstRow)return;for(var i=0;i<firstRow.cells.length;i++){var cell=firstRow.cells[i];var txt=ts_getInnerText(cell);cell.innerHTML='<a href="#" class="sortheader" onclick="ts_resortTable(this); return false;">'
+txt+'<img class="sortarrow" src="'+arrowBlank+'" height="6" width="9"></a>';}
for(var i=0;i<firstRow.cells.length;i++){var cell=firstRow.cells[i];var lnk=ts_firstChildByName(cell,'A');var img=ts_firstChildByName(lnk,'IMG')
if((' '+cell.className+' ').indexOf(" default-sort ")!=-1){ts_arrowDown(img);}
if((' '+cell.className+' ').indexOf(" default-revsort ")!=-1){ts_arrowUp(img);}
if((' '+cell.className+' ').indexOf(" initial-sort ")!=-1){ts_resortTable(lnk);}}}
function ts_getInnerText(el){if(typeof el=="string")return el;if(typeof el=="undefined"){return el};/*if(el.innerText)return el.innerText*/;var str="";var cs=el.childNodes;var l=cs.length;for(var i=0;i<l;i++){node=cs[i];switch(node.nodeType){case 1:if(node.className=="sortkey"){return ts_getInnerText(node);}else if(node.className=="revsortkey"){return"-"+ts_getInnerText(node);}else{str+=ts_getInnerText(node);break;}
case 3:str+=node.nodeValue;break;}}
return str;}
function ts_firstChildByName(el,name){for(var ci=0;ci<el.childNodes.length;ci++){if(el.childNodes[ci].tagName&&el.childNodes[ci].tagName.toLowerCase()==name.toLowerCase())
return el.childNodes[ci];}}
function ts_arrowUp(img){img.setAttribute('sortdir','up');img.src=arrowUp;}
function ts_arrowDown(img){img.setAttribute('sortdir','down');img.src=arrowDown;}
function ts_resortTable(lnk){var img=ts_firstChildByName(lnk,'IMG')
var td=lnk.parentNode;var column=td.cellIndex;var table=getParent(td,'TABLE');if(table.rows.length<=1)return;SORT_COLUMN_INDEX=column;while(td.previousSibling!=null){td=td.previousSibling;if(td.nodeType!=1){continue}
colspan=td.getAttribute("colspan");if(colspan){SORT_COLUMN_INDEX+=parseInt(colspan)-1;}}
var itm=ts_getInnerText(table.rows[1].cells[SORT_COLUMN_INDEX]);itm=trim(itm);sortfn=ts_sort_caseinsensitive;if(itm.match(/^\d\d[\/-]\d\d[\/-]\d\d\d\d$/))sortfn=ts_sort_date;if(itm.match(/^\d\d[\/-]\d\d[\/-]\d\d$/))sortfn=ts_sort_date;if(itm.match(/^[£$]/))sortfn=ts_sort_currency;if(itm.match(/^-?[\d\.]+$/))sortfn=ts_sort_numeric;var firstRow=new Array();var newRows=new Array();for(i=0;i<table.rows[0].length;i++){firstRow[i]=table.rows[0][i];}
for(j=1;j<table.rows.length;j++){newRows[j-1]=table.rows[j];newRows[j-1].oldPosition=j-1;}
newRows.sort(ts_stableSort(sortfn));if(img.getAttribute("sortdir")=='down'){newRows.reverse();ts_arrowUp(img);}else{ts_arrowDown(img);}
for(i=0;i<newRows.length;i++){if(!newRows[i].className||(newRows[i].className&&(newRows[i].className.indexOf('sortbottom')==-1)))
table.tBodies[0].appendChild(newRows[i]);}
for(i=0;i<newRows.length;i++){if(newRows[i].className&&(newRows[i].className.indexOf('sortbottom')!=-1))
table.tBodies[0].appendChild(newRows[i]);}
var allimgs=document.getElementsByTagName("img");for(var ci=0;ci<allimgs.length;ci++){var one_img=allimgs[ci];if(one_img!=img&&one_img.className=='sortarrow'&&getParent(one_img,"table")==getParent(lnk,"table")){one_img.src=arrowBlank;one_img.setAttribute('sortdir','');}}}
function getParent(el,pTagName){if(el==null)
return null;else if(el.nodeType==1&&el.tagName.toLowerCase()==pTagName.toLowerCase())
return el;else
return getParent(el.parentNode,pTagName);}
function ts_stableSort(sortfn){function stableSort(a,b){var cmp=sortfn(a,b);if(cmp!=0){return cmp;}else{return a.oldPosition-b.oldPosition;}}
return stableSort;}
function ts_sort_date(a,b){aa=trim(ts_getInnerText(a.cells[SORT_COLUMN_INDEX]));bb=trim(ts_getInnerText(b.cells[SORT_COLUMN_INDEX]));if(aa.length==10){dt1=aa.substr(6,4)+aa.substr(3,2)+aa.substr(0,2);}else{yr=aa.substr(6,2);if(parseInt(yr)<50){yr='20'+yr;}else{yr='19'+yr;}
dt1=yr+aa.substr(3,2)+aa.substr(0,2);}
if(bb.length==10){dt2=bb.substr(6,4)+bb.substr(3,2)+bb.substr(0,2);}else{yr=bb.substr(6,2);if(parseInt(yr)<50){yr='20'+yr;}else{yr='19'+yr;}
dt2=yr+bb.substr(3,2)+bb.substr(0,2);}
if(dt1==dt2)return 0;if(dt1<dt2)return-1;return 1;}
function ts_sort_currency(a,b){aa=ts_getInnerText(a.cells[SORT_COLUMN_INDEX]).replace(/[^0-9.]/g,'');bb=ts_getInnerText(b.cells[SORT_COLUMN_INDEX]).replace(/[^0-9.]/g,'');return parseFloat(aa)-parseFloat(bb);}
function ts_sort_numeric(a,b){aa=parseFloat(ts_getInnerText(a.cells[SORT_COLUMN_INDEX]));if(isNaN(aa))aa=0;bb=parseFloat(ts_getInnerText(b.cells[SORT_COLUMN_INDEX]));if(isNaN(bb))bb=0;return aa-bb;}
function ts_sort_caseinsensitive(a,b){aa=ts_getInnerText(a.cells[SORT_COLUMN_INDEX]).toLowerCase();bb=ts_getInnerText(b.cells[SORT_COLUMN_INDEX]).toLowerCase();if(aa==bb)return 0;if(aa<bb)return-1;return 1;}
function ts_sort_default(a,b){aa=ts_getInnerText(a.cells[SORT_COLUMN_INDEX]);bb=ts_getInnerText(b.cells[SORT_COLUMN_INDEX]);if(aa==bb)return 0;if(aa<bb)return-1;return 1;}
function addEvent(elm,evType,fn,useCapture){if(elm.addEventListener){elm.addEventListener(evType,fn,useCapture);return true;}else if(elm.attachEvent){var r=elm.attachEvent("on"+evType,fn);return r;}else{alert("Handler could not be removed");}}

26
reviewday/smokestack.py Normal file
View File

@ -0,0 +1,26 @@
import json
import httplib2
class SmokeStack(object):
def __init__(self, url):
self._jobs = None
self.url = url
def jobs(self, git_hash=None):
if not self._jobs:
h = httplib2.Http()
resp, content = h.request(self.url, "GET")
self._jobs = json.loads(content)
if git_hash:
jobs_with_hash = []
for job in self._jobs:
for job_type, data in job.iteritems():
if data['nova_revision'] == git_hash or \
data['glance_revision'] == git_hash or \
data['keystone_revision'] == git_hash:
jobs_with_hash.append(job)
return jobs_with_hash
else:
return self._jobs

27
reviewday/util.py Normal file
View File

@ -0,0 +1,27 @@
import os
import shutil
import html_helper
from Cheetah.Template import Template
def prep_out_dir(out_dir='out_report'):
src_dir = os.path.dirname(__file__)
report_files_dir = os.path.join(src_dir, 'report_files')
if os.path.exists(out_dir):
print 'WARNING: output directory "%s" already exists' % out_dir
else:
shutil.copytree(report_files_dir, out_dir)
def create_report(name_space={}):
filename = os.path.join(os.path.dirname(__file__), 'report.html')
report_text = open(filename).read()
name_space['helper'] = html_helper
t = Template(report_text, searchList=[name_space])
out_dir = 'out_report'
prep_out_dir(out_dir)
out_file = open(os.path.join(out_dir, 'index.html'), "w")
out_file.write(str(t))
out_file.close()

53
setup.py Normal file
View File

@ -0,0 +1,53 @@
import os
from setuptools import setup
def read(fname):
return open(os.path.join(os.path.dirname(__file__), fname)).read()
setup(
name = "reviewday",
version = "0.1.0",
author = "Dan Prince",
author_email = "dan.prince@rackspace.com",
description = ("Report generator for OpenStack code reviews."),
license = "BSD",
keywords = "OpenStack HTML report generator",
url = "https://github/dprince/reviewday",
packages = ['reviewday'],
long_description = read('README.md'),
classifiers = [
"Development Status :: 4 - Beta",
"Topic :: Utilities",
"License :: OSI Approved :: BSD License",
],
scripts = ['bin/reviewday'],
data_files = [
('reviewday', ['reviewday/report.html']),
('reviewday/report_files', [
'reviewday/report_files/arrowBlank',
'reviewday/report_files/arrowDown',
'reviewday/report_files/arrowUp',
'reviewday/report_files/combo.css',
'reviewday/report_files/CRITICALBUGFIX.png',
'reviewday/report_files/ESSENTIALFEATURE.png',
'reviewday/report_files/HIGHBUGFIX.png',
'reviewday/report_files/HIGHFEATURE.png',
'reviewday/report_files/LOWBUGFIX.png',
'reviewday/report_files/LOWFEATURE.png',
'reviewday/report_files/MEDIUMBUGFIX.png',
'reviewday/report_files/MEDIUMFEATURE.png',
'reviewday/report_files/NOLINK.png',
'reviewday/report_files/REGRESSIONHOTFIX.png',
'reviewday/report_files/RELEASECRITICALBUG.png',
'reviewday/report_files/sorting.js',
'reviewday/report_files/UNDECIDEDBUGFIX.png',
'reviewday/report_files/UNTARGETEDFEATURE.png',
'reviewday/report_files/WISHLISTBUGFIX.png',
])
],
install_requires = [
"launchpadlib",
"cheetah",
],
)