1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3# Licensed to the Apache Software Foundation (ASF) under one or more 4# contributor license agreements. See the NOTICE file distributed with 5# this work for additional information regarding copyright ownership. 6# The ASF licenses this file to You under the Apache License, Version 2.0 7# (the "License"); you may not use this file except in compliance with 8# the License. You may obtain a copy of the License at 9# 10# http://www.apache.org/licenses/LICENSE-2.0 11# 12# Unless required by applicable law or agreed to in writing, software 13# distributed under the License is distributed on an "AS IS" BASIS, 14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15# See the License for the specific language governing permissions and 16# limitations under the License. 17 18""" 19Simple script that queries GitHub for all open PRs, then finds the ones without 20issue number in title, and the ones where the linked JIRA is already closed 21""" 22 23import os 24import sys 25sys.path.append(os.path.dirname(__file__)) 26import argparse 27import json 28import re 29from github import Github 30from jira import JIRA 31from datetime import datetime 32from time import strftime 33try: 34 from jinja2 import Environment, BaseLoader 35 can_do_html = True 36except: 37 can_do_html = False 38 39def read_config(): 40 parser = argparse.ArgumentParser(description='Find open Pull Requests that need attention') 41 parser.add_argument('--json', action='store_true', default=False, help='Output as json') 42 parser.add_argument('--html', action='store_true', default=False, help='Output as html') 43 parser.add_argument('--token', help='Github access token in case you query too often anonymously') 44 newconf = parser.parse_args() 45 return newconf 46 47 48def out(text): 49 global conf 50 if not (conf.json or conf.html): 51 print(text) 52 53def make_html(dict): 54 if not can_do_html: 55 print ("ERROR: Cannot generate HTML. Please install jinja2") 56 sys.exit(1) 57 global conf 58 template = Environment(loader=BaseLoader).from_string(""" 59 <h1>Lucene Github PR report</h1> 60 61 <p>Number of open Pull Requests: {{ open_count }}</p> 62 63 <h2>PRs lacking JIRA reference in title ({{ no_jira_count }})</h2> 64 <ul> 65 {% for pr in no_jira %} 66 <li><a href="https://github.com/apache/lucene/pull/{{ pr.number }}">#{{ pr.number }}: {{ pr.created }} {{ pr.title }}</a> ({{ pr.user }})</li> 67 {%- endfor %} 68 </ul> 69 70 <h2>Open PRs with a resolved JIRA ({{ closed_jira_count }})</h2> 71 <ul> 72 {% for pr in closed_jira %} 73 <li><a href="https://github.com/apache/lucene/pull/{{ pr.pr_number }}">#{{ pr.pr_number }}</a>: <a href="https://issues.apache.org/jira/browse/{{ pr.issue_key }}">{{ pr.status }} {{ pr.resolution_date }} {{ pr.issue_key}}: {{ pr.issue_summary }}</a> ({{ pr.assignee }})</li> 74 {%- endfor %} 75 </ul> 76 """) 77 return template.render(dict) 78 79def main(): 80 global conf 81 conf = read_config() 82 token = conf.token if conf.token is not None else None 83 if token: 84 gh = Github(token) 85 else: 86 gh = Github() 87 jira = JIRA('https://issues.apache.org/jira') 88 result = {} 89 repo = gh.get_repo('apache/lucene') 90 open_prs = repo.get_pulls(state='open') 91 out("Lucene Github PR report") 92 out("============================") 93 out("Number of open Pull Requests: %s" % open_prs.totalCount) 94 result['open_count'] = open_prs.totalCount 95 96 lack_jira = list(filter(lambda x: not re.match(r'.*\b(LUCENE)-\d{3,6}\b', x.title), open_prs)) 97 result['no_jira_count'] = len(lack_jira) 98 lack_jira_list = [] 99 for pr in lack_jira: 100 lack_jira_list.append({'title': pr.title, 'number': pr.number, 'user': pr.user.login, 'created': pr.created_at.strftime("%Y-%m-%d")}) 101 result['no_jira'] = lack_jira_list 102 out("\nPRs lacking JIRA reference in title") 103 for pr in lack_jira_list: 104 out(" #%s: %s %s (%s)" % (pr['number'], pr['created'], pr['title'], pr['user'] )) 105 106 out("\nOpen PRs with a resolved JIRA") 107 has_jira = list(filter(lambda x: re.match(r'.*\b(LUCENE)-\d{3,6}\b', x.title), open_prs)) 108 109 issue_ids = [] 110 issue_to_pr = {} 111 for pr in has_jira: 112 jira_issue_str = re.match(r'.*\b((LUCENE)-\d{3,6})\b', pr.title).group(1) 113 issue_ids.append(jira_issue_str) 114 issue_to_pr[jira_issue_str] = pr 115 116 resolved_jiras = jira.search_issues(jql_str="key in (%s) AND status in ('Closed', 'Resolved')" % ", ".join(issue_ids)) 117 closed_jiras = [] 118 for issue in resolved_jiras: 119 pr_title = issue_to_pr[issue.key].title 120 pr_number = issue_to_pr[issue.key].number 121 assignee = issue.fields.assignee.name if issue.fields.assignee else None 122 closed_jiras.append({ 'issue_key': issue.key, 123 'status': issue.fields.status.name, 124 'resolution': issue.fields.resolution.name, 125 'resolution_date': issue.fields.resolutiondate[:10], 126 'pr_number': pr_number, 127 'pr_title': pr_title, 128 'issue_summary': issue.fields.summary, 129 'assignee': assignee}) 130 131 closed_jiras.sort(key=lambda r: r['pr_number'], reverse=True) 132 for issue in closed_jiras: 133 out(" #%s: %s %s %s: %s (%s)" % (issue['pr_number'], 134 issue['status'], 135 issue['resolution_date'], 136 issue['issue_key'], 137 issue['issue_summary'], 138 issue['assignee']) 139 ) 140 result['closed_jira_count'] = len(resolved_jiras) 141 result['closed_jira'] = closed_jiras 142 143 if conf.json: 144 print(json.dumps(result, indent=4)) 145 146 if conf.html: 147 print(make_html(result)) 148 149if __name__ == '__main__': 150 try: 151 main() 152 except KeyboardInterrupt: 153 print('\nReceived Ctrl-C, exiting early') 154