storm-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From bo...@apache.org
Subject [1/3] storm git commit: STORM-931: Python Scritps to Produce Formatted JIRA and GitHub Joint Reports
Date Wed, 15 Jul 2015 21:30:12 GMT
Repository: storm
Updated Branches:
  refs/heads/master cddd2531c -> 413389973


STORM-931: Python Scritps to Produce Formatted JIRA and GitHub Joint Reports


Project: http://git-wip-us.apache.org/repos/asf/storm/repo
Commit: http://git-wip-us.apache.org/repos/asf/storm/commit/61dceffb
Tree: http://git-wip-us.apache.org/repos/asf/storm/tree/61dceffb
Diff: http://git-wip-us.apache.org/repos/asf/storm/diff/61dceffb

Branch: refs/heads/master
Commit: 61dceffb622e70dd419096f0fa0ad5979d99d710
Parents: f75cf7c
Author: Hugo Louro <hmclouro@gmail.com>
Authored: Fri Jun 19 18:48:12 2015 -0700
Committer: Hugo Louro <hmclouro@gmail.com>
Committed: Thu Jul 9 19:35:33 2015 -0700

----------------------------------------------------------------------
 dev-tools/github/__init__.py       | 205 ++++++++-------
 dev-tools/jira-github-join.py      |  77 ++----
 dev-tools/jira/__init__.py         | 435 ++++++++++++++++++--------------
 dev-tools/report/__init__.py       |  14 +
 dev-tools/report/formatter.py      |  68 +++++
 dev-tools/report/report.py         | 252 ++++++++++++++++++
 dev-tools/report/report_builder.py |  86 +++++++
 dev-tools/storm-merge.py           |   2 +-
 8 files changed, 795 insertions(+), 344 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/storm/blob/61dceffb/dev-tools/github/__init__.py
----------------------------------------------------------------------
diff --git a/dev-tools/github/__init__.py b/dev-tools/github/__init__.py
index 2c533d0..48f397b 100755
--- a/dev-tools/github/__init__.py
+++ b/dev-tools/github/__init__.py
@@ -13,108 +13,127 @@
 
 import getpass
 import base64
-import urllib
 import urllib2
 from datetime import datetime
+import re
+
 try:
-	import json
+    import json
 except ImportError:
-	import simplejson as json
+    import simplejson as json
+
 
 def mstr(obj):
-	if (obj == None):
-		return ""
-	return unicode(obj)
+    if obj is None:
+        return ""
+    return unicode(obj)
+
+
+def git_time(obj):
+    if obj is None:
+        return None
+    return datetime.strptime(obj[0:19], "%Y-%m-%dT%H:%M:%S")
 
-def gittime(obj):
-	if (obj == None):
-		return None
-	return datetime.strptime(obj[0:19], "%Y-%m-%dT%H:%M:%S")
 
 class GitPullRequest:
-	"""Pull Request from Git"""
-	def __init__(self, data, parent):
-		self.data = data
-		self.parent = parent
-	
-	def html_url(self):
-		return self.data["html_url"]
-	
-	def title(self):
-		return self.data["title"]
-	
-	def number(self):
-		return self.data["number"]
-	
-	#TODO def review_comments
-	
-	def user(self):
-		return mstr(self.data["user"]["login"])
-	
-	def fromBranch(self):
-		return mstr(self.data["head"]["ref"])
-	
-	def fromRepo(self):
-		return mstr(self.data["head"]["repo"]["clone_url"])
-
-	def merged(self):
-		return self.data["merged_at"] != None
-		
-	def raw(self):
-		return self.data
-
-	def created_at(self):
-		return gittime(self.data["created_at"])
-
-	def updated_at(self):
-		return gittime(self.data["updated_at"])
-
-	def merged_at(self):
-		return gittime(self.data["merged_at"])	
-	
-	def __str__(self):
-		return self.html_url()
-	
-	def __repr__(self):
-		return self.html_url()
+    """Pull Request from Git"""
+
+    storm_jira_number = re.compile("STORM-[0-9]+", re.I)
+
+    def __init__(self, data, parent):
+        self.data = data
+        self.parent = parent
+
+    def html_url(self):
+        return self.data["html_url"]
+
+    def title(self):
+        return self.data["title"]
+
+    def trimmed_title(self):
+        limit = 40
+        title = self.data["title"]
+        return title if len(title) < limit else title[0:limit] + "..."
+
+    def number(self):
+        return self.data["number"]
+
+        # TODO def review_comments
+
+    def user(self):
+        return mstr(self.data["user"]["login"])
+
+    def from_branch(self):
+        return mstr(self.data["head"]["ref"])
+
+    def from_repo(self):
+        return mstr(self.data["head"]["repo"]["clone_url"])
+
+    def merged(self):
+        return self.data["merged_at"] is not None
+
+    def raw(self):
+        return self.data
+
+    def created_at(self):
+        return git_time(self.data["created_at"])
+
+    def updated_at(self):
+        return git_time(self.data["updated_at"])
+
+    def merged_at(self):
+        return git_time(self.data["merged_at"])
+
+    def has_jira_id(self):
+        return GitPullRequest.storm_jira_number.search(self.title())
+
+    def jira_id(self):
+        return GitPullRequest.storm_jira_number.search(self.title()).group(0).upper()
+
+    def __str__(self):
+        return self.html_url()
+
+    def __repr__(self):
+        return self.html_url()
+
 
 class GitHub:
-	"""Github API"""
-	def __init__(self, options):
-		self.headers = {}
-		if options.gituser:
-			gitpassword = getpass.getpass("github.com user " + options.gituser+":")
-			authstr = base64.encodestring('%s:%s' % (options.gituser, gitpassword)).replace('\n',
'')
-			self.headers["Authorization"] = "Basic "+authstr
-	
-	def pulls(self, user, repo, type="all"):
-		page=1
-		ret = []
-		while True:
-			url = "https://api.github.com/repos/"+user+"/"+repo+"/pulls?state="+type+"&page="+str(page)
-		
-			req = urllib2.Request(url,None,self.headers)
-			result = urllib2.urlopen(req)
-			contents = result.read()
-			if result.getcode() != 200:
-				raise Exception(result.getcode() + " != 200 "+ contents)
-			got = json.loads(contents)
-			for part in got:
-				ret.append(GitPullRequest(part, self))
-			if len(got) == 0:
-				return ret
-			page = page + 1
-			
-	def openPulls(self, user, repo):
-		return self.pulls(user, repo, "open")
-
-	def pull(self, user, repo, number):
-		url = "https://api.github.com/repos/"+user+"/"+repo+"/pulls/"+number
-		req = urllib2.Request(url,None,self.headers)
-		result = urllib2.urlopen(req)
-		contents = result.read()
-		if result.getcode() != 200:
-			raise Exception(result.getcode() + " != 200 "+ contents)
-		got = json.loads(contents)
-		return GitPullRequest(got, self)
+    """Github API"""
+
+    def __init__(self, options):
+        self.headers = {}
+        if options.gituser:
+            gitpassword = getpass.getpass("github.com user " + options.gituser + ":")
+            authstr = base64.encodestring('%s:%s' % (options.gituser, gitpassword)).replace('\n',
'')
+            self.headers["Authorization"] = "Basic " + authstr
+
+    def pulls(self, user, repo, type="all"):
+        page = 1
+        ret = []
+        while True:
+            url = "https://api.github.com/repos/" + user + "/" + repo + "/pulls?state=" +
type + "&page=" + str(page)
+
+            req = urllib2.Request(url, None, self.headers)
+            result = urllib2.urlopen(req)
+            contents = result.read()
+            if result.getcode() != 200:
+                raise Exception(result.getcode() + " != 200 " + contents)
+            got = json.loads(contents)
+            for part in got:
+                ret.append(GitPullRequest(part, self))
+            if len(got) == 0:
+                return ret
+            page = page + 1
+
+    def open_pulls(self, user, repo):
+        return self.pulls(user, repo, "open")
 
+    def pull(self, user, repo, number):
+        url = "https://api.github.com/repos/" + user + "/" + repo + "/pulls/" + number
+        req = urllib2.Request(url, None, self.headers)
+        result = urllib2.urlopen(req)
+        contents = result.read()
+        if result.getcode() != 200:
+            raise Exception(result.getcode() + " != 200 " + contents)
+        got = json.loads(contents)
+        return GitPullRequest(got, self)

http://git-wip-us.apache.org/repos/asf/storm/blob/61dceffb/dev-tools/jira-github-join.py
----------------------------------------------------------------------
diff --git a/dev-tools/jira-github-join.py b/dev-tools/jira-github-join.py
index d2526e6..fe0daf3 100755
--- a/dev-tools/jira-github-join.py
+++ b/dev-tools/jira-github-join.py
@@ -12,69 +12,28 @@
 #  See the License for the specific language governing permissions and
 #  limitations under the License.
 
-from jira import JiraRepo
-from github import GitHub, mstr
-import re
 from optparse import OptionParser
 from datetime import datetime
+from github import GitHub
+from jira import JiraRepo
+from report.report_builder import CompleteReportBuilder
 
-def daydiff(a, b):
-	return (a - b).days
 
 def main():
-	parser = OptionParser(usage="usage: %prog [options]")
-	parser.add_option("-g", "--github-user", dest="gituser",
-			type="string", help="github user, if not supplied no auth is used", metavar="USER")
-	
-	(options, args) = parser.parse_args()
-	
-	jrepo = JiraRepo("https://issues.apache.org/jira/rest/api/2")
-	github = GitHub(options)
-	
-	openPullRequests = github.openPulls("apache","storm")
-	stormJiraNumber = re.compile("STORM-[0-9]+", re.I)
-	openJiras = jrepo.openJiras("STORM")
-	
-	jira2Pulls = {}
-	pullWithoutJira = []
-	pullWithBadJira = []
-	
-	for pull in openPullRequests:
-		found = stormJiraNumber.search(pull.title())
-		if found:
-			jiraNum = found.group(0).upper()
-			if not (jiraNum in openJiras):
-				pullWithBadJira.append(pull)
-			else:
-				if jira2Pulls.get(jiraNum) == None:
-					jira2Pulls[jiraNum] = []
-				jira2Pulls[jiraNum].append(pull)
-		else:
-			pullWithoutJira.append(pull);
-	
-	now = datetime.utcnow()
-	print "Pull requests that need a JIRA:"
-	print "Pull URL\tPull Title\tPull Age\tPull Update Age"
-	for pull in pullWithoutJira:
-		print ("%s\t%s\t%s\t%s"%(pull.html_url(), pull.title(), daydiff(now, pull.created_at()),
daydiff(now, pull.updated_at()))).encode("UTF-8")
-	
-	print "\nPull with bad or closed JIRA:"
-	print "Pull URL\tPull Title\tPull Age\tPull Update Age"
-	for pull in pullWithBadJira:
-		print ("%s\t%s\t%s\t%s"%(pull.html_url(), pull.title(), daydiff(now, pull.created_at()),
daydiff(now, pull.updated_at()))).encode("UTF-8")
-	
-	print "\nOpen JIRA to Pull Requests and Possible Votes, vote detection is very approximate:"
-	print "JIRA\tPull Requests\tJira Summary\tJIRA Age\tPull Age\tJIRA Update Age\tPull Update
Age"
-	print "\tComment Vote\tComment Author\tPull URL\tComment Age"
-	for key, value in jira2Pulls.items():
-		print ("%s\t%s\t%s\t%s\t%s\t%s\t%s"%(key, mstr(value), openJiras[key].getSummary(),
-			 daydiff(now, openJiras[key].getCreated()), daydiff(now, value[0].created_at()),
-			 daydiff(now, openJiras[key].getUpdated()), daydiff(now, value[0].updated_at()))).encode("UTF-8")
-		for comment in openJiras[key].getComments():
-			#print comment.raw()
-			if comment.hasVote():
-				print (("\t%s\t%s\t%s\t%s")%(comment.getVote(), comment.getAuthor(), comment.getPull(),
daydiff(now, comment.getCreated()))).encode("UTF-8")
+    parser = OptionParser(usage="usage: %prog [options]")
+    parser.add_option("-g", "--github-user", dest="gituser",
+                      type="string", help="github User, if not supplied no auth is used",
metavar="USER")
 
-if __name__ == "__main__":
-	main()
+    (options, args) = parser.parse_args()
+
+    jira_repo = JiraRepo("https://issues.apache.org/jira/rest/api/2")
+    github_repo = GitHub(options)
 
+    print "Report generated on: %s (GMT)" % (datetime.strftime(datetime.utcnow(), "%Y-%m-%d
%H:%M:%S"))
+
+    report_builder = CompleteReportBuilder(jira_repo, github_repo)
+    report_builder.report.print_all()
+
+
+if __name__ == "__main__":
+    main()

http://git-wip-us.apache.org/repos/asf/storm/blob/61dceffb/dev-tools/jira/__init__.py
----------------------------------------------------------------------
diff --git a/dev-tools/jira/__init__.py b/dev-tools/jira/__init__.py
index 15380aa..c98ae31 100755
--- a/dev-tools/jira/__init__.py
+++ b/dev-tools/jira/__init__.py
@@ -15,218 +15,271 @@ import re
 import urllib
 import urllib2
 from datetime import datetime
+
 try:
-	import json
+    import json
 except ImportError:
-	import simplejson as json
+    import simplejson as json
+
 
 def mstr(obj):
-	if (obj == None):
-		return ""
-	return unicode(obj)
+    if obj is None:
+        return ""
+    return unicode(obj)
+
 
 def jiratime(obj):
-	if (obj == None):
-		return None
-	return datetime.strptime(obj[0:19], "%Y-%m-%dT%H:%M:%S")
+    if obj is None:
+        return None
+    return datetime.strptime(obj[0:19], "%Y-%m-%dT%H:%M:%S")
+
+# Regex pattern definitions
+github_user = re.compile("Git[Hh]ub user ([\w-]+)")
+github_pull = re.compile("https://github.com/[^/\s]+/[^/\s]+/pull/[0-9]+")
+has_vote = re.compile("\s+([-+][01])\s*")
+is_diff = re.compile("--- End diff --")
+
 
-githubUser = re.compile("Git[Hh]ub user ([\w-]+)")
-githubPull = re.compile("https://github.com/[^/\s]+/[^/\s]+/pull/[0-9]+")
-hasVote = re.compile("\s+([-+][01])\s*")
-isDiff = re.compile("--- End diff --")
+def search_group(reg, txt, group):
+    m = reg.search(txt)
+    if m is None:
+        return None
+    return m.group(group)
 
-def searchGroup(reg, txt, group):
-	m = reg.search(txt)
-	if m == None:
-		return None
-	return m.group(group)
 
 class JiraComment:
-	"""A comment on a JIRA"""
+    """A comment on a JIRA"""
 
-	def __init__(self, data):
-		self.data = data
-		self.author = mstr(self.data['author']['name'])
-		self.githubAuthor = None
-		self.githubPull = None
-		self.githubComment = (self.author == "githubbot")
-		body = self.getBody()
-		if isDiff.search(body) != None:
-			self.vote = None
-		else:
-			self.vote = searchGroup(hasVote, body, 1)
+    def __init__(self, data):
+        self.data = data
+        self.author = mstr(self.data['author']['name'])
+        self.github_author = None
+        self.githubPull = None
+        self.githubComment = (self.author == "githubbot")
+        body = self.get_body()
+        if is_diff.search(body) is not None:
+            self.vote = None
+        else:
+            self.vote = search_group(has_vote, body, 1)
 
-		if self.githubComment:
-			self.githubAuthor = searchGroup(githubUser, body, 1)
-			self.githubPull = searchGroup(githubPull, body, 0)
-			
+        if self.githubComment:
+            self.github_author = search_group(github_user, body, 1)
+            self.githubPull = search_group(github_pull, body, 0)
 
-	def getAuthor(self):
-		if self.githubAuthor != None:
-			return self.githubAuthor
-		return self.author
+    def get_author(self):
+        if self.github_author is not None:
+            return self.github_author
+        return self.author
 
-	def getBody(self):
-		return mstr(self.data['body'])
+    def get_body(self):
+        return mstr(self.data['body'])
 
-	def getPull(self):
-		return self.githubPull
+    def get_pull(self):
+        return self.githubPull
 
-	def raw(self):
-		return self.data
+    def has_github_pull(self):
+        return self.githubPull is not None
 
-	def hasVote(self):
-		return self.vote != None
+    def raw(self):
+        return self.data
 
-	def getVote(self):
-		return self.vote
+    def has_vote(self):
+        return self.vote is not None
+
+    def get_vote(self):
+        return self.vote
+
+    def get_created(self):
+        return jiratime(self.data['created'])
 
-	def getCreated(self):
-		return jiratime(self.data['created'])
 
 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.comments = None
-	
-	def getId(self):
-		return mstr(self.key)
-	
-	def getDescription(self):
-		return mstr(self.fields['description'])
-	
-	def getReleaseNote(self):
-		if (self.notes == None):
-			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']
-		if(pri != None):
-			ret = pri['name']
-		return mstr(ret)
-	
-	def getAssigneeEmail(self):
-		ret = ""
-		mid = self.fields['assignee']
-		if mid != None:
-			ret = mid['emailAddress']
-		return mstr(ret)
-
-	
-	def getAssignee(self):
-		ret = ""
-		mid = self.fields['assignee']
-		if(mid != None):
-			ret = mid['displayName']
-		return mstr(ret)
-	
-	def getComponents(self):
-		return " , ".join([ comp['name'] for comp in self.fields['components'] ])
-	
-	def getSummary(self):
-		return self.fields['summary']
-	
-	def getType(self):
-		ret = ""
-		mid = self.fields['issuetype']
-		if(mid != None):
-			ret = mid['name']
-		return mstr(ret)
-	
-	def getReporter(self):
-		ret = ""
-		mid = self.fields['reporter']
-		if(mid != None):
-			ret = mid['displayName']
-		return mstr(ret)
-	
-	def getProject(self):
-		ret = ""
-		mid = self.fields['project']
-		if(mid != None):
-			ret = mid['key']
-		return mstr(ret)
-
-	def getCreated(self):
-		return jiratime(self.fields['created'])
-
-	def getUpdated(self):
-		return jiratime(self.fields['updated'])
-	
-	def getComments(self):
-		if self.comments == None:
-			jiraId = self.getId()
-			comments = []
-			at=0
-			end=1
-			count=100
-			while (at < end):
-				params = urllib.urlencode({'startAt':at, 'maxResults':count})
-				resp = urllib2.urlopen(self.parent.baseUrl+"/issue/"+jiraId+"/comment?"+params)
-				data = json.loads(resp.read())
-				if (data.has_key('errorMessages')):
-					raise Exception(data['errorMessages'])
-				at = data['startAt'] + data['maxResults']
-				end = data['total']
-				for item in data['comments']:
-					j = JiraComment(item)
-					comments.append(j)
-			self.comments = comments
-		return self.comments
-	
-	def raw(self):
-		return self.fields
+    """A single JIRA"""
+
+    def __init__(self, data, parent):
+        self.key = data['key']
+        self.fields = data['fields']
+        self.parent = parent
+        self.notes = None
+        self.comments = None
+
+    def get_id(self):
+        return mstr(self.key)
+
+    def get_description(self):
+        return mstr(self.fields['description'])
+
+    def getReleaseNote(self):
+        if self.notes is None:
+            field = self.parent.fieldIdMap['Release Note']
+            if self.fields.has_key(field):
+                self.notes = mstr(self.fields[field])
+            else:
+                self.notes = self.get_description()
+        return self.notes
+
+    def get_status(self):
+        ret = ""
+        status = self.fields['status']
+        if status is not None:
+            ret = status['name']
+        return mstr(ret)
+
+    def get_priority(self):
+        ret = ""
+        pri = self.fields['priority']
+        if pri is not None:
+            ret = pri['name']
+        return mstr(ret)
+
+    def get_assignee_email(self):
+        ret = ""
+        mid = self.fields['assignee']
+        if mid is not None:
+            ret = mid['emailAddress']
+        return mstr(ret)
+
+    def get_assignee(self):
+        ret = ""
+        mid = self.fields['assignee']
+        if mid is not None:
+            ret = mid['displayName']
+        return mstr(ret)
+
+    def get_components(self):
+        return " , ".join([comp['name'] for comp in self.fields['components']])
+
+    def get_summary(self):
+        return self.fields['summary']
+
+    def get_trimmed_summary(self):
+        limit = 40
+        summary = self.fields['summary']
+        return summary if len(summary) < limit else summary[0:limit] + "..."
+
+    def get_type(self):
+        ret = ""
+        mid = self.fields['issuetype']
+        if mid is not None:
+            ret = mid['name']
+        return mstr(ret)
+
+    def get_reporter(self):
+        ret = ""
+        mid = self.fields['reporter']
+        if mid is not None:
+            ret = mid['displayName']
+        return mstr(ret)
+
+    def get_project(self):
+        ret = ""
+        mid = self.fields['project']
+        if mid is not None:
+            ret = mid['key']
+        return mstr(ret)
+
+    def get_created(self):
+        return jiratime(self.fields['created'])
+
+    def get_updated(self):
+        return jiratime(self.fields['updated'])
+
+    def get_comments(self):
+        if self.comments is None:
+            jiraId = self.get_id()
+            comments = []
+            at = 0
+            end = 1
+            count = 100
+            while (at < end):
+                params = urllib.urlencode({'startAt': at, 'maxResults': count})
+                resp = urllib2.urlopen(self.parent.baseUrl + "/issue/" + jiraId + "/comment?"
+ params)
+                data = json.loads(resp.read())
+                if (data.has_key('errorMessages')):
+                    raise Exception(data['errorMessages'])
+                at = data['startAt'] + data['maxResults']
+                end = data['total']
+                for item in data['comments']:
+                    j = JiraComment(item)
+                    comments.append(j)
+            self.comments = comments
+        return self.comments
+
+    def has_voted_comment(self):
+        for comment in self.get_comments():
+            if comment.has_vote():
+                return True
+        return False
+
+    def get_trimmed_comments(self, limit=40):
+        comments = self.get_comments()
+        return comments if len(comments) < limit else comments[0:limit] + "..."
+
+    def raw(self):
+        return self.fields
+
+    def storm_jira_cmp(x, y):
+        xn = x.get_id().split("-")[1]
+        yn = y.get_id().split("-")[1]
+        return int(xn) - int(yn)
+
 
 class JiraRepo:
-	"""A Repository for JIRAs"""
-	
-	def __init__(self, baseUrl):
-		self.baseUrl = baseUrl
-		resp = urllib2.urlopen(baseUrl+"/field")
-		data = json.loads(resp.read())
-		
-		self.fieldIdMap = {}
-		for part in data:
-			self.fieldIdMap[part['name']] = part['id']
-	
-	def get(self, id):
-		resp = urllib2.urlopen(self.baseUrl+"/issue/"+id)
-		data = json.loads(resp.read())
-		if (data.has_key('errorMessages')):
-			raise Exception(data['errorMessages'])
-		j = Jira(data, self)
-		return j
-	
-	def query(self, query):
-		jiras = {}
-		at=0
-		end=1
-		count=100
-		while (at < end):
-			params = urllib.urlencode({'jql': query, 'startAt':at, 'maxResults':count})
-			#print params
-			resp = urllib2.urlopen(self.baseUrl+"/search?%s"%params)
-			data = json.loads(resp.read())
-			if (data.has_key('errorMessages')):
-				raise Exception(data['errorMessages'])
-			at = data['startAt'] + data['maxResults']
-			end = data['total']
-			for item in data['issues']:
-					j = Jira(item, self)
-					jiras[j.getId()] = j
-		return jiras
-	
-	def openJiras(self, project):
-		return self.query("project = "+project+" AND resolution = Unresolved");
+    """A Repository for JIRAs"""
+
+    def __init__(self, baseUrl):
+        self.baseUrl = baseUrl
+        resp = urllib2.urlopen(baseUrl + "/field")
+        data = json.loads(resp.read())
+
+        self.fieldIdMap = {}
+        for part in data:
+            self.fieldIdMap[part['name']] = part['id']
+
+    def get(self, id):
+        resp = urllib2.urlopen(self.baseUrl + "/issue/" + id)
+        data = json.loads(resp.read())
+        if (data.has_key('errorMessages')):
+            raise Exception(data['errorMessages'])
+        j = Jira(data, self)
+        return j
+
+    def query(self, query):
+        jiras = {}
+        at = 0
+        end = 1
+        count = 100
+        while (at < end):
+            params = urllib.urlencode({'jql': query, 'startAt': at, 'maxResults': count})
+            # print params
+            resp = urllib2.urlopen(self.baseUrl + "/search?%s" % params)
+            data = json.loads(resp.read())
+            if (data.has_key('errorMessages')):
+                raise Exception(data['errorMessages'])
+            at = data['startAt'] + data['maxResults']
+            end = data['total']
+            for item in data['issues']:
+                j = Jira(item, self)
+                jiras[j.get_id()] = j
+        return jiras
+
+    def unresolved_jiras(self, project):
+        """
+        :param project: The JIRA project to search for unresolved issues
+        :return: All JIRA issues that have the field resolution = Unresolved
+        """
+        return self.query("project = " + project + " AND resolution = Unresolved")
+
+    def open_jiras(self, project):
+        """
+        :param project: The JIRA project to search for open issues
+        :return: All JIRA issues that have the field status = Open
+        """
+        return self.query("project = " + project + " AND status = Open")
 
+    def in_progress_jiras(self, project):
+        """
+        :param project: The JIRA project to search for In Progress issues
+        :return: All JIRA issues that have the field status = 'In Progress'
+        """
+        return self.query("project = " + project + " AND status = 'In Progress'")

http://git-wip-us.apache.org/repos/asf/storm/blob/61dceffb/dev-tools/report/__init__.py
----------------------------------------------------------------------
diff --git a/dev-tools/report/__init__.py b/dev-tools/report/__init__.py
new file mode 100644
index 0000000..e931fe0
--- /dev/null
+++ b/dev-tools/report/__init__.py
@@ -0,0 +1,14 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#  Licensed 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.
+

http://git-wip-us.apache.org/repos/asf/storm/blob/61dceffb/dev-tools/report/formatter.py
----------------------------------------------------------------------
diff --git a/dev-tools/report/formatter.py b/dev-tools/report/formatter.py
new file mode 100644
index 0000000..81f574d
--- /dev/null
+++ b/dev-tools/report/formatter.py
@@ -0,0 +1,68 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#  Licensed 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.
+
+def encode(obj, encoding='UTF-8'):
+    """
+    Check if the object supports encode() method, and if so, encodes it.
+    Encoding defaults to UTF-8.
+    For example objects of type 'int' do not support encode
+    """
+    return obj.encode(encoding) if 'encode' in dir(obj) else obj
+
+class Formatter:
+    def __init__(self, fields_tuple=(), row_tuple=(), min_width_tuple=None):
+        # Format to pass as first argument to the print function, e.g. '%s%s%s'
+        self.format = ""
+        # data_format will be of the form ['{!s:43}'],'{!s:39}','{!s:11}','{!s:25}']
+        # the widths are determined from the data in order to print output with nice format
+        # Each entry of the data_format list will be used by the advanced string formatter:
+        # "{!s:43}".format("Text")
+        # Advanced string formatter as detailed in here: https://www.python.org/dev/peps/pep-3101/
+        self.data_format = []
+        Formatter._assert(fields_tuple, row_tuple, min_width_tuple)
+        self._build_format_tuples(fields_tuple, row_tuple, min_width_tuple)
+
+    @staticmethod
+    def _assert(o1, o2, o3):
+        if len(o1) != len(o2) and (o3 is not None and len(o2) != len(o3)):
+            raise RuntimeError("Object collections must have the same length. "
+                               "len(o1)={0}, len(o2)={1}, len(o3)={2}"
+                               .format(len(o1), len(o2), -1 if o3 is None else len(o3)))
+
+    # determines the widths from the data in order to print output with nice format
+    @staticmethod
+    def _find_sizes(fields_tuple, row_tuple, min_width_tuple):
+        sizes = []
+        padding = 3
+        for i in range(0, len(row_tuple)):
+            max_len = max(len(encode(fields_tuple[i])), len(str(encode(row_tuple[i]))))
+            if min_width_tuple is not None:
+                max_len = max(max_len, min_width_tuple[i])
+            sizes += [max_len + padding]
+        return sizes
+
+    def _build_format_tuples(self, fields_tuple, row_tuple, min_width_tuple):
+        sizes = Formatter._find_sizes(fields_tuple, row_tuple, min_width_tuple)
+
+        for i in range(0, len(row_tuple)):
+            self.format += "%s"
+            self.data_format += ["{!s:" + str(sizes[i]) + "}"]
+
+    # Returns a tuple where each entry has a string that is the result of
+    # statements with the pattern "{!s:43}".format("Text")
+    def row_str_format(self, row_tuple):
+        format_with_values = [str(self.data_format[0].format(encode(row_tuple[0])))]
+        for i in range(1, len(row_tuple)):
+            format_with_values += [str(self.data_format[i].format(encode(row_tuple[i])))]
+        return tuple(format_with_values)

http://git-wip-us.apache.org/repos/asf/storm/blob/61dceffb/dev-tools/report/report.py
----------------------------------------------------------------------
diff --git a/dev-tools/report/report.py b/dev-tools/report/report.py
new file mode 100644
index 0000000..46ec175
--- /dev/null
+++ b/dev-tools/report/report.py
@@ -0,0 +1,252 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#  Licensed 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 datetime import datetime
+from github import mstr
+from jira import Jira
+from formatter import Formatter, encode
+
+
+def daydiff(a, b):
+    return (a - b).days
+
+
+class Report:
+    now = datetime.utcnow()
+    def __init__(self, header=''):
+        self.header = header
+
+    # if padding starts with - it puts padding before contents, otherwise after
+    @staticmethod
+    def _build_tuple(contents, padding=''):
+        if padding is not '':
+            out = []
+            for i in range(len(contents)):
+                out += [padding[1:] + str(contents[i])] if padding[0] is '-' else [str(contents[i])
+ padding]
+            return tuple(out)
+        return contents
+
+    # calls the native print function with the following format. Text1,Text2,... has the
correct spacing
+    # print ("%s%s%s" % ("Text1, Text2, Text3))
+    def print_(self, formatter, row_tuple):
+        print (formatter.format % formatter.row_str_format(row_tuple))
+
+class JiraReport(Report):
+    def __init__(self, issues, header=''):
+        Report.__init__(self, header)
+        self.issues = issues
+
+    def view(self, excluded):
+        issues_view = dict(self.issues)
+        for key in excluded:
+            issues_view.pop(key, None)
+        return issues_view
+
+    def keys_view(self, excluded):
+        return self.view(excluded).keys().sort(Jira.storm_jira_cmp, reverse=True)
+
+    def values_view(self, excluded=None):
+        temp_dic = dict(self.issues) if excluded is None else self.view(excluded)
+        values = temp_dic.values()
+        values.sort(Jira.storm_jira_cmp, reverse=True)
+        return values
+
+    @staticmethod
+    def _row_tuple(jira):
+        return (jira.get_id(), jira.get_trimmed_summary(), daydiff(Report.now, jira.get_created()),
+                daydiff(Report.now, jira.get_updated()))
+
+    def _min_width_tuple(self):
+        return -1, 43, -1, -1
+
+    def print_report(self):
+        print "%s (Count = %s) " % (self.header, len(self.issues))
+        jiras = self.values_view()
+        fields_tuple = ('Jira Id', 'Summary', 'Created', 'Last Updated (Days)')
+        row_tuple = self._row_tuple(jiras[0])
+
+        formatter = Formatter(fields_tuple, row_tuple, self._min_width_tuple())
+
+        self.print_(formatter, fields_tuple)
+
+        for jira in jiras:
+            row_tuple = self._row_tuple(jira)
+            self.print_(formatter, row_tuple)
+
+    @staticmethod
+    def build_jira_url(jira_id):
+        BASE_URL = "https://issues.apache.org/jira/browse/"
+        return BASE_URL + jira_id
+
+
+class GitHubReport(Report):
+    def __init__(self, pull_requests=None, header=''):
+        Report.__init__(self, header)
+
+        if pull_requests is None:
+            self.pull_requests = []
+            self.type = ''
+        else:
+            self.pull_requests = pull_requests
+            self.type = type
+
+    def _row_tuple(self, pull):
+        return self._build_tuple(
+            (pull.html_url(), pull.trimmed_title(), daydiff(Report.now, pull.created_at()),
+             daydiff(Report.now, pull.updated_at()), pull.user()), '')
+
+    def _min_width_tuple(self):
+        return -1, 43, -1, -1, -1
+
+    def print_report(self):
+        print "%s (Count = %s) " % (self.header, len(self.pull_requests))
+
+        fields_tuple = self._build_tuple(('URL', 'Title', 'Created', 'Last Updated (Days)',
'User'), '')
+        row_tuple = self._row_tuple(self.pull_requests[0])
+
+        formatter = Formatter(fields_tuple, row_tuple, self._min_width_tuple())
+
+        self.print_(formatter, fields_tuple)
+        for pull in self.pull_requests:
+            row_tuple = self._row_tuple(pull)
+            self.print_(formatter, row_tuple)
+
+    def jira_ids(self):
+        """
+        :return: sorted list of JIRA ids present in Git pull requests
+        """
+        jira_ids = list()
+        for pull in self.pull_requests:
+            jira_ids.append(pull.jira_id())
+        return sorted(jira_ids)
+
+class JiraGitHubCombinedReport(Report):
+    def __init__(self, jira_report, github_report, header='', print_comments=False):
+        Report.__init__(self, header)
+        self.jira_report = jira_report
+        self.github_report = github_report
+        self.print_comments = print_comments
+
+    def _jira_comments(self, jira_id):
+        return None if jira_id is None else self.jira_report.issues[jira_id].get_comments()
+
+    def _idx_1st_comment_with_vote(self):
+        g = 0
+        for pull in self.github_report.pull_requests:
+            c = 0
+            for comment in self._jira_comments(pull.jira_id()):
+                if comment.has_vote():
+                    return(g,) + (c,)
+                c += 1
+            g += 1
+
+    def _pull_request(self, pull_idx):
+        pull = self.github_report.pull_requests[pull_idx]
+        return pull
+
+    def _jira_id(self, pull_idx):
+        pull = self._pull_request(pull_idx)
+        return encode(pull.jira_id())
+
+    def _jira_issue(self, jira_id):
+        return self.jira_report.issues[jira_id]
+
+    def _row_tuple(self, pull_idx):
+        pull = self._pull_request(pull_idx)
+        jira_id = self._jira_id(pull_idx)
+        jira_issue = self._jira_issue(jira_id)
+
+        return (jira_id, mstr(pull), jira_issue.get_trimmed_summary(),
+                daydiff(Report.now, jira_issue.get_created()),
+                daydiff(Report.now, pull.created_at()),
+                daydiff(Report.now, jira_issue.get_updated()),
+                daydiff(Report.now, pull.updated_at()),
+                jira_issue.get_status(), pull.user())
+
+    def _row_tuple_1(self, pull_idx, comment_idx):
+        row_tuple_1 = None
+        jira_id = self._jira_id(pull_idx)
+        jira_comments = self._jira_comments(jira_id)
+        comment = jira_comments[comment_idx]
+        if comment.has_vote():
+            row_tuple_1 = (comment.get_vote(), comment.get_author(), comment.get_pull(),
+                           daydiff(Report.now, comment.get_created()))
+
+        return row_tuple_1
+
+    # variables and method names ending with _1 correspond to the comments part
+    def print_report(self, print_comments=False):
+        print "%s (Count = %s) " % (self.header, len(self.github_report.pull_requests))
+
+        fields_tuple = ('JIRA ID', 'Pull Request', 'Jira Summary', 'JIRA Age',
+                        'Pull Age', 'JIRA Update Age', 'Pull Update Age (Days)',
+                        'JIRA Status', 'GitHub user')
+        row_tuple = self._row_tuple(0)
+        formatter = Formatter(fields_tuple, row_tuple)
+        self.print_(formatter, fields_tuple)
+
+        row_tuple_1 = ()
+        formatter_1 = Formatter()
+
+        if print_comments or self.print_comments:
+            fields_tuple_1 = self._build_tuple(('Comment Vote', 'Comment Author', 'Pull URL',
'Comment Age'), '-\t\t')
+            row_tuple_1 = self._build_tuple(self._row_tuple_1(*self._idx_1st_comment_with_vote()),
'-\t\t')
+            formatter_1 = Formatter(fields_tuple_1, row_tuple_1)
+            self.print_(formatter_1, fields_tuple_1)
+            print ''
+
+        for p in range(0, len(self.github_report.pull_requests)):
+            row_tuple = self._row_tuple(p)
+            self.print_(formatter, row_tuple)
+
+            if print_comments or self.print_comments:
+                has_vote = False
+                comments = self._jira_comments(self._jira_id(p))
+                for c in range(len(comments)):     # Check cleaner way
+                    comment = comments[c]
+                    if comment.has_vote():
+                        row_tuple_1 = self._build_tuple(self._row_tuple_1(p, c), '-\t\t')
+                        if row_tuple_1 is not None:
+                            self.print_(formatter_1, row_tuple_1)
+                            has_vote = True
+                if has_vote:
+                    print ''
+
+
+class CompleteReport(Report):
+    def __init__(self, header=''):
+        Report.__init__(self, header)
+        self.jira_reports = []
+        self.github_reports = []
+        self.jira_github_combined_reports = []
+
+    def print_all(self):
+        if self.header is not '':
+            print self.header
+
+        self._print_github_reports()
+        self._print_jira_github_combined_reports()
+        self._print_jira_reports()
+
+    def _print_jira_reports(self):
+        for jira in self.jira_reports:
+            jira.print_report()
+
+    def _print_github_reports(self):
+        for github in self.github_reports:
+            github.print_report()
+
+    def _print_jira_github_combined_reports(self):
+        for jira_github_combined in self.jira_github_combined_reports:
+            jira_github_combined.print_report()
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/storm/blob/61dceffb/dev-tools/report/report_builder.py
----------------------------------------------------------------------
diff --git a/dev-tools/report/report_builder.py b/dev-tools/report/report_builder.py
new file mode 100644
index 0000000..4b8a468
--- /dev/null
+++ b/dev-tools/report/report_builder.py
@@ -0,0 +1,86 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#  Licensed 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 report import CompleteReport, GitHubReport, JiraReport, JiraGitHubCombinedReport
+
+
+class ReportBuilder:
+    def __init__(self, jira_repo=None, github_repo=None):
+        self.jira_repo = jira_repo
+        self.github_repo = github_repo
+
+    def build(self):
+        pass
+
+
+class CompleteReportBuilder(ReportBuilder):
+    def __init__(self, jira_repo=None, github_repo=None):
+        ReportBuilder.__init__(self, jira_repo, github_repo)
+        self.report = CompleteReport()
+        self.build()
+
+    def build(self):
+        # all open github pull requests
+        github_open = GitHubReport(self.github_repo.open_pulls("apache", "storm"))
+        github_bad_jira = GitHubReport(None, "\nGITHUB PULL REQUESTS WITH BAD OR CLOSED JIRA
ID")
+        github_without_jira = GitHubReport(None, "\nGITHUB PULL REQUESTS WITHOUT A JIRA ID")
+        github_unresolved_jira = GitHubReport(None, "\nGITHUB PULL REQUESTS WITH UNRESOLVED
JIRA ID")
+        github_unresolved_jira_voted = GitHubReport(None, "\nGITHUB PULL REQUESTS WITH VOTES
FOR UNRESOLVED JIRAS")
+        github_open_jira = GitHubReport(None, "\nGITHUB PULL REQUESTS WITH OPEN JIRA ID")
+        github_unresolved_not_open_jira = GitHubReport(None, "\nGITHUB PULL REQUESTS WITH
UNRESOLVED BUT NOT OPEN JIRA ID")
+
+        # all unresolved JIRA issues
+        jira_unresolved = JiraReport(self.jira_repo.unresolved_jiras("STORM"))
+        jira_open = JiraReport(dict((x, y) for x, y in self.jira_repo.unresolved_jiras("STORM").items()
if y.get_status() == 'Open'))
+        jira_in_progress = JiraReport(dict((x, y) for x, y in self.jira_repo.in_progress_jiras("STORM").items()
if y.get_status() == 'In Progress'),
+                                      "\nIN PROGRESS JIRA ISSUES")
+
+        for pull in github_open.pull_requests:
+            if pull.has_jira_id():
+                pull_jira_id = pull.jira_id()
+                if pull_jira_id not in jira_unresolved.issues:
+                    github_bad_jira.pull_requests.append(pull)
+                else:
+                    github_unresolved_jira.pull_requests.append(pull)
+                    if jira_unresolved.issues[pull_jira_id].has_voted_comment():
+                        github_unresolved_jira_voted.pull_requests.append(pull)
+                    if pull_jira_id in jira_open.issues:
+                        github_open_jira.pull_requests.append(pull)
+                    else:
+                        github_unresolved_not_open_jira.pull_requests.append(pull)
+            else:
+                github_without_jira.pull_requests.append(pull)
+
+        jira_github_open = JiraGitHubCombinedReport(jira_open, github_open_jira,
+                                                    "\nOPEN JIRA ISSUES THAT HAVE GITHUB
PULL REQUESTS")
+        jira_github_unresolved_not_open = JiraGitHubCombinedReport(jira_unresolved, github_unresolved_not_open_jira,
+                                                                   "\nIN PROGRESS OR REOPENED
JIRA ISSUES THAT HAVE GITHUB PULL REQUESTS")
+        jira_github_unresolved_voted = JiraGitHubCombinedReport(jira_unresolved, github_unresolved_jira_voted,
+                                                                "\nGITHUB PULL REQUESTS WITH
VOTES FOR UNRESOLVED JIRAS", True)
+        # jira_github_unresolved = JiraGitHubCombinedReport(jira_unresolved, github_unresolved_jira,
+        #                                                   "\nUnresolved JIRA issues with
GitHub pull requests")
+
+        jira_open_no_pull = JiraReport(jira_open.view(github_open_jira.jira_ids()),
+                                       "\nOPEN JIRA ISSUES THAT DON'T HAVE GITHUB PULL REQUESTS")
+
+        # build complete report
+        self.report.jira_reports.append(jira_in_progress)
+        self.report.jira_reports.append(jira_open_no_pull)
+
+        self.report.github_reports.append(github_bad_jira)
+        self.report.github_reports.append(github_without_jira)
+
+        self.report.jira_github_combined_reports.append(jira_github_open)
+        self.report.jira_github_combined_reports.append(jira_github_unresolved_voted)
+        self.report.jira_github_combined_reports.append(jira_github_unresolved_not_open)
+        # self.report.jira_github_combined_reports.append(jira_github_unresolved)

http://git-wip-us.apache.org/repos/asf/storm/blob/61dceffb/dev-tools/storm-merge.py
----------------------------------------------------------------------
diff --git a/dev-tools/storm-merge.py b/dev-tools/storm-merge.py
index 06ae25f..ed06216 100755
--- a/dev-tools/storm-merge.py
+++ b/dev-tools/storm-merge.py
@@ -24,7 +24,7 @@ def main():
 
         for pullNumber in args:
 		pull = github.pull("apache", "storm", pullNumber)
-		print "git pull "+pull.fromRepo()+" "+pull.fromBranch()
+		print "git pull "+pull.from_repo()+" "+pull.from_branch()
 
 if __name__ == "__main__":
 	main()


Mime
View raw message