2015-04-02 00:52:22 +00:00
#!/usr/bin/env python
#
# 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.
from glob import glob
from optparse import OptionParser
2015-05-06 17:33:56 +00:00
from time import gmtime , strftime
2015-07-06 22:49:03 +00:00
import pprint
2015-04-02 00:52:22 +00:00
import os
import re
import sys
import urllib
2015-07-20 16:47:46 +00:00
import urllib2
2015-04-02 00:52:22 +00:00
try :
import json
except ImportError :
import simplejson as json
releaseVersion = { }
namePattern = re . compile ( r ' \ ([0-9]+ \ ) ' )
2015-04-03 03:50:36 +00:00
asflicense = '''
< ! - - -
# 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.
- - >
'''
2015-04-02 00:52:22 +00:00
def clean ( str ) :
return tableclean ( re . sub ( namePattern , " " , str ) )
def formatComponents ( str ) :
str = re . sub ( namePattern , ' ' , str ) . replace ( " ' " , " " )
if str != " " :
ret = str
else :
# some markdown parsers don't like empty tables
ret = " . "
return clean ( ret )
# convert to utf-8
# protect some known md metachars
# or chars that screw up doxia
def tableclean ( str ) :
str = str . encode ( ' utf-8 ' )
str = str . replace ( " _ " , " \ _ " )
str = str . replace ( " \r " , " " )
str = str . rstrip ( )
return str
# same thing as tableclean,
# except table metachars are also
# escaped as well as more
# things we don't want doxia to
# screw up
def notableclean ( str ) :
str = tableclean ( str )
str = str . replace ( " | " , " \ | " )
str = str . replace ( " < " , " \ < " )
str = str . replace ( " > " , " \ > " )
2015-05-06 17:33:56 +00:00
str = str . replace ( " * " , " \ * " )
2015-04-02 00:52:22 +00:00
str = str . rstrip ( )
return str
2015-06-27 15:59:50 +00:00
# clean output dir
def cleanOutputDir ( dir ) :
files = os . listdir ( dir )
for name in files :
os . remove ( os . path . join ( dir , name ) )
os . rmdir ( dir )
2015-04-02 00:52:22 +00:00
def mstr ( obj ) :
2015-06-27 15:59:50 +00:00
if ( obj is None ) :
2015-04-02 00:52:22 +00:00
return " "
return unicode ( obj )
2015-07-06 22:49:03 +00:00
def buildindex ( title , license ) :
2015-04-02 00:52:22 +00:00
versions = reversed ( sorted ( glob ( " [0-9]*.[0-9]*.[0-9]* " ) ) )
with open ( " index.md " , " w " ) as indexfile :
2015-07-06 22:49:03 +00:00
if license is True :
indexfile . write ( asflicense )
2015-04-02 00:52:22 +00:00
for v in versions :
2015-07-06 22:49:03 +00:00
indexfile . write ( " * %s v %s \n " % ( title , v ) )
2015-04-02 00:52:22 +00:00
for k in ( " Changes " , " Release Notes " ) :
2015-07-06 22:49:03 +00:00
indexfile . write ( " * %s ( %s / %s . %s .html) \n " \
2015-04-02 00:52:22 +00:00
% ( k , v , k . upper ( ) . replace ( " " , " " ) , v ) )
indexfile . close ( )
2015-07-06 22:49:03 +00:00
class GetVersions :
""" yo """
def __init__ ( self , versions , projects ) :
versions = versions
projects = projects
self . newversions = [ ]
pp = pprint . PrettyPrinter ( indent = 4 )
at = 0
end = 1
count = 100
versions . sort ( )
print " Looking for %s through %s " % ( versions [ 0 ] , versions [ - 1 ] )
for p in projects :
2015-07-20 16:47:46 +00:00
resp = urllib2 . urlopen ( " https://issues.apache.org/jira/rest/api/2/project/ %s /versions " % p )
2015-07-06 22:49:03 +00:00
data = json . loads ( resp . read ( ) )
for d in data :
if d [ ' name ' ] [ 0 ] . isdigit and versions [ 0 ] < = d [ ' name ' ] and d [ ' name ' ] < = versions [ - 1 ] :
print " Adding %s to the list " % d [ ' name ' ]
self . newversions . append ( d [ ' name ' ] )
newlist = list ( set ( self . newversions ) )
self . newversions = newlist
def getlist ( self ) :
pp = pprint . PrettyPrinter ( indent = 4 )
return ( self . newversions )
2015-04-02 00:52:22 +00:00
class Version :
""" Represents a version number """
def __init__ ( self , data ) :
self . mod = False
self . data = data
found = re . match ( ' ^(( \ d+)( \ . \ d+)*).*$ ' , data )
if ( found ) :
self . parts = [ int ( p ) for p in found . group ( 1 ) . split ( ' . ' ) ]
else :
self . parts = [ ]
# backfill version with zeroes if missing parts
self . parts . extend ( ( 0 , ) * ( 3 - len ( self . parts ) ) )
def __str__ ( self ) :
if ( self . mod ) :
return ' . ' . join ( [ str ( p ) for p in self . parts ] )
return self . data
def __cmp__ ( self , other ) :
return cmp ( self . parts , other . parts )
class Jira :
""" A single JIRA """
def __init__ ( self , data , parent ) :
self . key = data [ ' key ' ]
self . fields = data [ ' fields ' ]
self . parent = parent
self . notes = None
self . incompat = None
self . reviewed = None
def getId ( self ) :
return mstr ( self . key )
def getDescription ( self ) :
return mstr ( self . fields [ ' description ' ] )
def getReleaseNote ( self ) :
2015-06-27 15:59:50 +00:00
if ( self . notes is None ) :
2015-04-02 00:52:22 +00:00
field = self . parent . fieldIdMap [ ' Release Note ' ]
if ( self . fields . has_key ( field ) ) :
self . notes = mstr ( self . fields [ field ] )
else :
self . notes = self . getDescription ( )
return self . notes
def getPriority ( self ) :
ret = " "
pri = self . fields [ ' priority ' ]
2015-06-27 15:59:50 +00:00
if ( pri is not None ) :
2015-04-02 00:52:22 +00:00
ret = pri [ ' name ' ]
return mstr ( ret )
def getAssignee ( self ) :
ret = " "
mid = self . fields [ ' assignee ' ]
2015-06-27 15:59:50 +00:00
if ( mid is not None ) :
2015-04-02 00:52:22 +00:00
ret = mid [ ' displayName ' ]
return mstr ( ret )
def getComponents ( self ) :
if ( len ( self . fields [ ' components ' ] ) > 0 ) :
return " , " . join ( [ comp [ ' name ' ] for comp in self . fields [ ' components ' ] ] )
else :
return " "
def getSummary ( self ) :
return self . fields [ ' summary ' ]
def getType ( self ) :
ret = " "
mid = self . fields [ ' issuetype ' ]
2015-06-27 15:59:50 +00:00
if ( mid is not None ) :
2015-04-02 00:52:22 +00:00
ret = mid [ ' name ' ]
return mstr ( ret )
def getReporter ( self ) :
ret = " "
mid = self . fields [ ' reporter ' ]
2015-06-27 15:59:50 +00:00
if ( mid is not None ) :
2015-04-02 00:52:22 +00:00
ret = mid [ ' displayName ' ]
return mstr ( ret )
def getProject ( self ) :
ret = " "
mid = self . fields [ ' project ' ]
2015-06-27 15:59:50 +00:00
if ( mid is not None ) :
2015-04-02 00:52:22 +00:00
ret = mid [ ' key ' ]
return mstr ( ret )
def __cmp__ ( self , other ) :
selfsplit = self . getId ( ) . split ( ' - ' )
othersplit = other . getId ( ) . split ( ' - ' )
v1 = cmp ( selfsplit [ 0 ] , othersplit [ 0 ] )
if ( v1 != 0 ) :
return v1
else :
if selfsplit [ 1 ] < othersplit [ 1 ] :
return True
elif selfsplit [ 1 ] > othersplit [ 1 ] :
return False
return False
def getIncompatibleChange ( self ) :
2015-06-27 15:59:50 +00:00
if ( self . incompat is None ) :
2015-04-02 00:52:22 +00:00
field = self . parent . fieldIdMap [ ' Hadoop Flags ' ]
self . reviewed = False
self . incompat = False
if ( self . fields . has_key ( field ) ) :
if self . fields [ field ] :
for hf in self . fields [ field ] :
if hf [ ' value ' ] == " Incompatible change " :
self . incompat = True
if hf [ ' value ' ] == " Reviewed " :
self . reviewed = True
return self . incompat
2015-06-27 15:59:50 +00:00
def checkMissingComponent ( self ) :
if ( len ( self . fields [ ' components ' ] ) > 0 ) :
return False
return True
def checkMissingAssignee ( self ) :
if ( self . fields [ ' assignee ' ] is not None ) :
return False
return True
def checkVersionString ( self ) :
field = self . parent . fieldIdMap [ ' Fix Version/s ' ]
for h in self . fields [ field ] :
found = re . match ( ' ^(( \ d+)( \ . \ d+)*).*$|^( \ w+ \ - \ d+)$ ' , h [ ' name ' ] )
if not found :
return True
return False
2015-04-02 00:52:22 +00:00
def getReleaseDate ( self , version ) :
for j in range ( len ( self . fields [ ' fixVersions ' ] ) ) :
if self . fields [ ' fixVersions ' ] [ j ] == version :
return ( self . fields [ ' fixVersions ' ] [ j ] [ ' releaseDate ' ] )
return None
class JiraIter :
""" An Iterator of JIRAs """
2015-07-06 22:49:03 +00:00
def __init__ ( self , version , projects ) :
self . version = version
self . projects = projects
v = str ( version ) . replace ( " -SNAPSHOT " , " " )
2015-04-02 00:52:22 +00:00
2015-07-20 16:47:46 +00:00
resp = urllib2 . urlopen ( " https://issues.apache.org/jira/rest/api/2/field " )
2015-04-02 00:52:22 +00:00
data = json . loads ( resp . read ( ) )
self . fieldIdMap = { }
for part in data :
self . fieldIdMap [ part [ ' name ' ] ] = part [ ' id ' ]
self . jiras = [ ]
at = 0
end = 1
count = 100
while ( at < end ) :
2015-07-06 22:49:03 +00:00
params = urllib . urlencode ( { ' jql ' : " project in ( ' " + " ' , ' " . join ( projects ) + " ' ) and fixVersion in ( ' " + v + " ' ) and resolution = Fixed " , ' startAt ' : at , ' maxResults ' : count } )
2015-07-20 16:47:46 +00:00
resp = urllib2 . urlopen ( " https://issues.apache.org/jira/rest/api/2/search? %s " % params )
2015-04-02 00:52:22 +00:00
data = json . loads ( resp . read ( ) )
if ( data . has_key ( ' errorMessages ' ) ) :
raise Exception ( data [ ' errorMessages ' ] )
at = data [ ' startAt ' ] + data [ ' maxResults ' ]
end = data [ ' total ' ]
self . jiras . extend ( data [ ' issues ' ] )
needaversion = False
2015-07-06 22:49:03 +00:00
if v not in releaseVersion :
needaversion = True
2015-04-02 00:52:22 +00:00
if needaversion is True :
for i in range ( len ( data [ ' issues ' ] ) ) :
for j in range ( len ( data [ ' issues ' ] [ i ] [ ' fields ' ] [ ' fixVersions ' ] ) ) :
if ' releaseDate ' in data [ ' issues ' ] [ i ] [ ' fields ' ] [ ' fixVersions ' ] [ j ] :
releaseVersion [ data [ ' issues ' ] [ i ] [ ' fields ' ] [ ' fixVersions ' ] [ j ] [ ' name ' ] ] = \
data [ ' issues ' ] [ i ] [ ' fields ' ] [ ' fixVersions ' ] [ j ] [ ' releaseDate ' ]
self . iter = self . jiras . __iter__ ( )
def __iter__ ( self ) :
return self
def next ( self ) :
data = self . iter . next ( )
j = Jira ( data , self )
return j
class Outputs :
""" Several different files to output to at the same time """
def __init__ ( self , base_file_name , file_name_pattern , keys , params = { } ) :
self . params = params
self . base = open ( base_file_name % params , ' w ' )
self . others = { }
for key in keys :
both = dict ( params )
both [ ' key ' ] = key
self . others [ key ] = open ( file_name_pattern % both , ' w ' )
def writeAll ( self , pattern ) :
both = dict ( self . params )
both [ ' key ' ] = ' '
self . base . write ( pattern % both )
for key in self . others . keys ( ) :
both = dict ( self . params )
both [ ' key ' ] = key
self . others [ key ] . write ( pattern % both )
def writeKeyRaw ( self , key , str ) :
self . base . write ( str )
if ( self . others . has_key ( key ) ) :
self . others [ key ] . write ( str )
def close ( self ) :
self . base . close ( )
for fd in self . others . values ( ) :
fd . close ( )
def writeList ( self , mylist ) :
for jira in sorted ( mylist ) :
line = ' | [ %s ](https://issues.apache.org/jira/browse/ %s ) | %s | %s | %s | %s | %s | \n ' \
% ( notableclean ( jira . getId ( ) ) , notableclean ( jira . getId ( ) ) ,
notableclean ( jira . getSummary ( ) ) ,
notableclean ( jira . getPriority ( ) ) ,
formatComponents ( jira . getComponents ( ) ) ,
notableclean ( jira . getReporter ( ) ) ,
notableclean ( jira . getAssignee ( ) ) )
self . writeKeyRaw ( jira . getProject ( ) , line )
def main ( ) :
2015-07-06 22:49:03 +00:00
parser = OptionParser ( usage = " usage: % prog --project PROJECT [--project PROJECT] --version VERSION [--version VERSION2 ...] " ,
2015-04-02 00:52:22 +00:00
epilog =
" Markdown-formatted CHANGES and RELEASENOTES files will be stored in a directory "
" named after the highest version provided. " )
parser . add_option ( " -i " , " --index " , dest = " index " , action = " store_true " ,
2015-07-06 22:49:03 +00:00
default = False , help = " build an index file " )
parser . add_option ( " -l " , " --license " , dest = " license " , action = " store_false " ,
default = True , help = " Add an ASF license " )
2015-06-27 15:59:50 +00:00
parser . add_option ( " -n " , " --lint " , dest = " lint " , action = " store_true " ,
help = " use lint flag to exit on failures " )
2015-07-06 22:49:03 +00:00
parser . add_option ( " -p " , " --project " , dest = " projects " ,
action = " append " , type = " string " ,
help = " projects in JIRA to include in releasenotes " , metavar = " PROJECT " )
parser . add_option ( " -r " , " --range " , dest = " range " , action = " store_true " ,
default = False , help = " Given versions are a range " )
parser . add_option ( " -t " , " --projecttitle " , dest = " title " ,
type = " string " ,
help = " Title to use for the project (default is Apache PROJECT) " )
parser . add_option ( " -u " , " --usetoday " , dest = " usetoday " , action = " store_true " ,
default = False , help = " use current date for unreleased versions " )
parser . add_option ( " -v " , " --version " , dest = " versions " ,
action = " append " , type = " string " ,
help = " versions in JIRA to include in releasenotes " , metavar = " VERSION " )
2015-04-02 00:52:22 +00:00
( options , args ) = parser . parse_args ( )
2015-06-27 15:59:50 +00:00
if ( options . versions is None ) :
2015-04-02 00:52:22 +00:00
options . versions = [ ]
if ( len ( args ) > 2 ) :
options . versions . append ( args [ 2 ] )
if ( len ( options . versions ) < = 0 ) :
parser . error ( " At least one version needs to be supplied " )
2015-07-20 16:47:46 +00:00
proxy = urllib2 . ProxyHandler ( )
opener = urllib2 . build_opener ( proxy )
urllib2 . install_opener ( opener )
2015-07-06 22:49:03 +00:00
projects = options . projects
if ( options . range is True ) :
versions = [ Version ( v ) for v in GetVersions ( options . versions , projects ) . getlist ( ) ]
else :
versions = [ Version ( v ) for v in options . versions ]
2015-04-02 00:52:22 +00:00
versions . sort ( ) ;
2015-07-06 22:49:03 +00:00
if ( options . title is None ) :
title = projects [ 0 ]
else :
title = options . title
2015-04-02 00:52:22 +00:00
2015-07-07 21:30:32 +00:00
haderrors = False
2015-07-06 22:49:03 +00:00
for v in versions :
vstr = str ( v )
jlist = JiraIter ( vstr , projects )
2015-04-02 00:52:22 +00:00
2015-07-06 22:49:03 +00:00
if vstr in releaseVersion :
reldate = releaseVersion [ vstr ]
elif options . usetoday :
reldate = strftime ( " % Y- % m- %d " , gmtime ( ) )
else :
reldate = " Unreleased "
2015-04-02 00:52:22 +00:00
2015-07-06 22:49:03 +00:00
if not os . path . exists ( vstr ) :
os . mkdir ( vstr )
2015-04-02 00:52:22 +00:00
reloutputs = Outputs ( " %(ver)s /RELEASENOTES. %(ver)s .md " ,
" %(ver)s /RELEASENOTES. %(key)s . %(ver)s .md " ,
2015-07-06 22:49:03 +00:00
[ ] , { " ver " : v , " date " : reldate , " title " : title } )
2015-04-02 00:52:22 +00:00
choutputs = Outputs ( " %(ver)s /CHANGES. %(ver)s .md " ,
" %(ver)s /CHANGES. %(key)s . %(ver)s .md " ,
2015-07-06 22:49:03 +00:00
[ ] , { " ver " : v , " date " : reldate , " title " : title } )
if ( options . license is True ) :
reloutputs . writeAll ( asflicense )
choutputs . writeAll ( asflicense )
relhead = ' # %(title)s %(key)s %(ver)s Release Notes \n \n ' \
' These release notes cover new developer and user-facing incompatibilities, features, and major improvements. \n \n '
chhead = ' # %(title)s Changelog \n \n ' \
' ## Release %(ver)s - %(date)s \n ' \
' \n '
reloutputs . writeAll ( relhead )
choutputs . writeAll ( chhead )
errorCount = 0
warningCount = 0
lintMessage = " "
incompatlist = [ ]
buglist = [ ]
improvementlist = [ ]
newfeaturelist = [ ]
subtasklist = [ ]
tasklist = [ ]
testlist = [ ]
otherlist = [ ]
for jira in sorted ( jlist ) :
if jira . getIncompatibleChange ( ) :
incompatlist . append ( jira )
elif jira . getType ( ) == " Bug " :
buglist . append ( jira )
elif jira . getType ( ) == " Improvement " :
improvementlist . append ( jira )
elif jira . getType ( ) == " New Feature " :
newfeaturelist . append ( jira )
elif jira . getType ( ) == " Sub-task " :
subtasklist . append ( jira )
elif jira . getType ( ) == " Task " :
tasklist . append ( jira )
elif jira . getType ( ) == " Test " :
testlist . append ( jira )
else :
otherlist . append ( jira )
line = ' * [ %s ](https://issues.apache.org/jira/browse/ %s ) | * %s * | ** %s ** \n ' \
% ( notableclean ( jira . getId ( ) ) , notableclean ( jira . getId ( ) ) , notableclean ( jira . getPriority ( ) ) ,
notableclean ( jira . getSummary ( ) ) )
if ( jira . getIncompatibleChange ( ) ) and ( len ( jira . getReleaseNote ( ) ) == 0 ) :
2015-07-07 21:30:32 +00:00
warningCount + = 1
2015-07-06 22:49:03 +00:00
reloutputs . writeKeyRaw ( jira . getProject ( ) , " \n --- \n \n " )
reloutputs . writeKeyRaw ( jira . getProject ( ) , line )
line = ' \n **WARNING: No release note provided for this incompatible change.** \n \n '
lintMessage + = " \n WARNING: incompatible change %s lacks release notes. " % ( notableclean ( jira . getId ( ) ) )
reloutputs . writeKeyRaw ( jira . getProject ( ) , line )
if jira . checkVersionString ( ) :
2015-07-07 21:30:32 +00:00
warningCount + = 1
2015-07-06 22:49:03 +00:00
lintMessage + = " \n WARNING: Version string problem for %s " % jira . getId ( )
if ( jira . checkMissingComponent ( ) or jira . checkMissingAssignee ( ) ) :
2015-07-07 21:30:32 +00:00
errorCount + = 1
2015-07-06 22:49:03 +00:00
errorMessage = [ ]
jira . checkMissingComponent ( ) and errorMessage . append ( " component " )
jira . checkMissingAssignee ( ) and errorMessage . append ( " assignee " )
lintMessage + = " \n ERROR: missing %s for %s " % ( " and " . join ( errorMessage ) , jira . getId ( ) )
if ( len ( jira . getReleaseNote ( ) ) > 0 ) :
reloutputs . writeKeyRaw ( jira . getProject ( ) , " \n --- \n \n " )
reloutputs . writeKeyRaw ( jira . getProject ( ) , line )
line = ' \n %s \n \n ' % ( tableclean ( jira . getReleaseNote ( ) ) )
reloutputs . writeKeyRaw ( jira . getProject ( ) , line )
if ( options . lint is True ) :
print lintMessage
print " ======================================= "
2015-07-07 21:30:32 +00:00
print " %s : Error: %d , Warning: %d \n " % ( vstr , errorCount , warningCount )
2015-07-06 22:49:03 +00:00
if ( errorCount > 0 ) :
2015-07-07 21:30:32 +00:00
haderrors = True
cleanOutputDir ( vstr )
continue
2015-07-06 22:49:03 +00:00
reloutputs . writeAll ( " \n \n " )
reloutputs . close ( )
choutputs . writeAll ( " ### INCOMPATIBLE CHANGES: \n \n " )
choutputs . writeAll ( " | JIRA | Summary | Priority | Component | Reporter | Contributor | \n " )
choutputs . writeAll ( " |:---- |:---- | :--- |:---- |:---- |:---- | \n " )
choutputs . writeList ( incompatlist )
choutputs . writeAll ( " \n \n ### NEW FEATURES: \n \n " )
choutputs . writeAll ( " | JIRA | Summary | Priority | Component | Reporter | Contributor | \n " )
choutputs . writeAll ( " |:---- |:---- | :--- |:---- |:---- |:---- | \n " )
choutputs . writeList ( newfeaturelist )
choutputs . writeAll ( " \n \n ### IMPROVEMENTS: \n \n " )
choutputs . writeAll ( " | JIRA | Summary | Priority | Component | Reporter | Contributor | \n " )
choutputs . writeAll ( " |:---- |:---- | :--- |:---- |:---- |:---- | \n " )
choutputs . writeList ( improvementlist )
choutputs . writeAll ( " \n \n ### BUG FIXES: \n \n " )
choutputs . writeAll ( " | JIRA | Summary | Priority | Component | Reporter | Contributor | \n " )
choutputs . writeAll ( " |:---- |:---- | :--- |:---- |:---- |:---- | \n " )
choutputs . writeList ( buglist )
choutputs . writeAll ( " \n \n ### TESTS: \n \n " )
choutputs . writeAll ( " | JIRA | Summary | Priority | Component | Reporter | Contributor | \n " )
choutputs . writeAll ( " |:---- |:---- | :--- |:---- |:---- |:---- | \n " )
choutputs . writeList ( testlist )
choutputs . writeAll ( " \n \n ### SUB-TASKS: \n \n " )
choutputs . writeAll ( " | JIRA | Summary | Priority | Component | Reporter | Contributor | \n " )
choutputs . writeAll ( " |:---- |:---- | :--- |:---- |:---- |:---- | \n " )
choutputs . writeList ( subtasklist )
choutputs . writeAll ( " \n \n ### OTHER: \n \n " )
choutputs . writeAll ( " | JIRA | Summary | Priority | Component | Reporter | Contributor | \n " )
choutputs . writeAll ( " |:---- |:---- | :--- |:---- |:---- |:---- | \n " )
choutputs . writeList ( otherlist )
choutputs . writeList ( tasklist )
choutputs . writeAll ( " \n \n " )
choutputs . close ( )
2015-04-02 00:52:22 +00:00
if options . index :
2015-07-06 22:49:03 +00:00
buildindex ( title , options . license )
2015-04-02 00:52:22 +00:00
2015-07-07 21:30:32 +00:00
if haderrors is True :
sys . exit ( 1 )
2015-04-02 00:52:22 +00:00
if __name__ == " __main__ " :
main ( )