From c8b34b9e9660b0799dee59747266a3a9fc7d1de4 Mon Sep 17 00:00:00 2001 From: Jared Morrow Date: Fri, 19 Jun 2015 09:44:33 -0600 Subject: Add pr2relnotes tool to generate release notes pr2relnotes takes merged PRs from the git log and generates formatted release notes including links to the PRs themselves --- .gitignore | 1 + pr2relnotes.py | 188 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100755 pr2relnotes.py diff --git a/.gitignore b/.gitignore index 8028d9c..c6be7ef 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ logs priv/templates/*.dtl.erl ebin .edts +env diff --git a/pr2relnotes.py b/pr2relnotes.py new file mode 100755 index 0000000..6e9a4f4 --- /dev/null +++ b/pr2relnotes.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python + +## Install info +## $ virtualenv env +## $ source env/bin/activate +## $ pip install PyGithub +## +## Examples: +## Find the differences from last tag to current +## $ pr2relnotes.py alpha-6 HEAD + +import argparse +import re +import os +import subprocess +from github import Github +from github import GithubException + + +def dprint(*args): + if VERBOSE: + print str(args) + +def get_args(): + """ + Get command line arguments + """ + parser = argparse.ArgumentParser(description="Find the PR's between two versions") + parser.add_argument("old", help = "old version to use") + parser.add_argument("new", help = "new version to use") + parser.add_argument("-v", "--verbose", help="Enable debug output", + default=False, + action="store_true") + parser.add_argument("-f", "--file", + help="Output file to store results (default: tagdiff.md)", + default="tagdiff.md") + return parser.parse_args() + +def search_prs(log): + """ + Search lines of text for PR numbers + """ + # Find all matches using regex iterator, using the PR # as the group match + resultlist = [str(m.group(1)) for m in re.finditer(r"erge pull request #(\d+)", log)] + return sorted(resultlist) + +def get_env(env): + return os.environ[env] + +def get_formatted_issue(repo, issue, title, url): + """ + Single place to adjust formatting output of PR data + """ + # Newline support writelines() call which doesn't add newlines + # on its own + return("* {}/{}: [{}]({})\n".format(repo, issue, title, url)) + +def gh_get_issue_output(org, repo, issuenum): + """ + Look up PR information using the GitHub api + """ + # Attempt to look up the PR, and don't take down the whole + # shebang if a API call fails + # This will fail often on forks who don't have the + # PRs numbers associated with the forked account + # Return empty string on error + try: + repoObj = gh.get_repo(org + "/" + repo) + issue = repoObj.get_issue(int(issuenum)) + title = issue.title + html_url = issue.html_url + except GithubException as e: + print "Github error({0}): {1}".format(e.status, e.data) + return "" + except: + print "Some github error" + return "" + + return(get_formatted_issue(repo, issuenum, title, html_url)) + + +def get_org(repourl): + """ + Simple function to parse the organization out of a GitHub URL + """ + dprint("Current repourl to search: " + repourl) + # GitHub URLs can be: + # http[s]://www.github.com/org/repo + # or git@github.com:/org/repo + pattern = re.compile(r"github.com[/:]+(\w+)/") + m = re.search(pattern, repourl) + # Fail fast if this is wrong so we can add a pattern to the search + if m: + return m.group(1) + else: + raise Exception("Incorrect regex pattern finding repo org") + +def get_name(repourl): + """ + Simple function to parse the repository name out of a GitHub URL + """ + dprint("Current repourl to search: " + repourl) + repo_pattern = re.compile(r"github.com[/:]\w+/(\w+)") + m = re.search(repo_pattern, repourl) + if m: + return m.group(1) + else: + raise Exception("Incorrect rexex pattern finding repo url") + +def get_repo_url_from_remote(): + """ + Function that gets the repository URL from the `git remote` listing + """ + git_remote_bytes = subprocess.check_output(["git", "remote", "-v"]) + # check_output returns the command results in raw byte format + remote_string = git_remote_bytes.decode('utf-8') + + pattern = re.compile(r"github.com[/:]\w+/\w+") + m = re.search(pattern, remote_string) + if m: + return m.group(0) + else: + raise Exception("Incorrect rexex pattern finding repo url") + +def process_log(gitlog, repo_url): + """ + Handles the processing of the gitlog and returns a list + of PRs already formatted for output + """ + pr_list = search_prs(gitlog) + repoorg = get_org(repo_url) + reponame = get_name(repo_url) + pr_buffer = [] + for issue in pr_list: + pr_buffer.append(gh_get_issue_output(repoorg, reponame, issue)) + + return pr_buffer + +def fetch_log(old_ver, new_ver): + """ + Function that processes the git log between the old and new versions + """ + dprint("Current working directory", os.getcwd()) + gitlogbytes = subprocess.check_output(["git", "log", + str(old_ver + ".." + new_ver)]) + return gitlogbytes.decode('utf-8') + + +def compare_versions(repo_url, old_ver, new_ver): + # Formatted list of all PRs for all repos + pr_out = [] + gitlog = fetch_log(old_ver, new_ver) + pr_out.extend(process_log(gitlog, repo_url)) + return pr_out + +def main(): + args = get_args() + + # Setup the GitHub object for later use + global gh + gh = Github(get_env("GHAUTH")) + + if gh == "": + raise Exception("Env var GHAUTH must be set to a valid GitHub API key") + + if args.verbose: + global VERBOSE + VERBOSE=True + + dprint("Inspecting difference in between: ", args.old, " and ", args.new) + + # Find the github URL of the repo we are operating on + repo_url = get_repo_url_from_remote() + + # Compare old and new versions + pr_list = compare_versions(repo_url, args.old, args.new) + + # Writeout PR listing + print "Writing output to file %s" % args.file + with open(args.file, 'w') as output: + output.writelines(pr_list) + + +if __name__ == "__main__": + VERBOSE=False + gh=None + topdir=os.getcwd() + main() -- cgit v1.1