#!/usr/bin/env python # The MIT License (MIT) # # Copyright (c) 2015 Jared Morrow (github.com/jaredmorrow) # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. ## 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()