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 <[email protected]>
Authored: Fri Jun 19 18:48:12 2015 -0700
Committer: Hugo Louro <[email protected]>
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()

Reply via email to