Commit 460ee964 by David Baumgold

Make release table by PR, instead of by commit

parent 4a45719a
...@@ -2,17 +2,22 @@ ...@@ -2,17 +2,22 @@
""" """
a release-master multitool a release-master multitool
""" """
from __future__ import print_function, unicode_literals
import sys
from path import path from path import path
from git import Repo from git import Repo, Commit
from git.refs.symbolic import SymbolicReference
import argparse import argparse
from datetime import date, timedelta from datetime import date, timedelta
from dateutil.parser import parse as parse_datestring from dateutil.parser import parse as parse_datestring
import re import re
from collections import OrderedDict from collections import OrderedDict, defaultdict
import textwrap import textwrap
import requests
IGNORED_EMAILS = set(("vagrant@precise32.(none)",)) IGNORED_EMAILS = set(("vagrant@precise32.(none)",))
JIRA_RE = re.compile(r"\b[A-Z]{2,}-\d+\b") JIRA_RE = re.compile(r"\b[A-Z]{2,}-\d+\b")
PR_BRANCH_RE = re.compile(r"remotes/origin/pr/(\d+)")
PROJECT_ROOT = path(__file__).abspath().dirname() PROJECT_ROOT = path(__file__).abspath().dirname()
repo = Repo(PROJECT_ROOT) repo = Repo(PROJECT_ROOT)
git = repo.git git = repo.git
...@@ -36,9 +41,22 @@ def make_parser(): ...@@ -36,9 +41,22 @@ def make_parser():
parser.add_argument( parser.add_argument(
'--table', '-t', action="store_true", default=False, '--table', '-t', action="store_true", default=False,
help="only print table") help="only print table")
parser.add_argument(
'--commit-table', action="store_true", default=False,
help="Display table by commit, instead of by PR")
return parser return parser
def ensure_pr_fetch():
# it would be nice to use the git-python API to do this, but it doesn't seem
# to support configurations with more than one value per key. :(
origin_fetches = git.config("remote.origin.fetch", get_all=True).splitlines()
pr_fetch = '+refs/pull/*/head:refs/remotes/origin/pr/*'
if pr_fetch not in origin_fetches:
git.config("remote.origin.fetch", pr_fetch, add=True)
git.fetch()
def default_release_date(): def default_release_date():
""" """
Returns a date object corresponding to the expected date of the next release: Returns a date object corresponding to the expected date of the next release:
...@@ -93,24 +111,158 @@ def commits_by_email(commit_range, include_merge=False): ...@@ -93,24 +111,158 @@ def commits_by_email(commit_range, include_merge=False):
return data return data
def generate_table(commit_range, include_merge=False): class NotFoundError(Exception): pass
def get_pr_for_commit(commit, branch="master"):
"""
http://stackoverflow.com/questions/8475448/find-merge-commit-which-include-a-specific-commit
"""
remote_branch = git.describe(commit, all=True, contains=True)
match = PR_BRANCH_RE.search(remote_branch)
if match:
pr_num = int(match.group(1))
return pr_num
# if `git describe` didn't work, we need to use `git branch` -- it's slower
remote_branches = git.branch(commit, all=True, contains=True).splitlines()
for remote_branch in remote_branches:
remote_branch = remote_branch.strip()
match = PR_BRANCH_RE.search(remote_branch)
if match:
pr_num = int(match.group(1))
# we have a pull request -- but is it the right one?
ref = SymbolicReference(repo, "refs/{}".format(remote_branch))
merge_base = git.merge_base(ref, branch)
rev = "{base}^..{branch}".format(base=merge_base, branch=remote_branch)
pr_commits = list(Commit.iter_items(repo, rev))
if commit in pr_commits:
# found it!
return pr_num
err = NotFoundError(
"Can't find pull request for commit {commit} against branch {branch}".format(
commit=commit, branch=branch,
)
)
err.commit = commit
raise err
def get_merge_commit(commit, branch="master"):
"""
Given a commit that was merged into the given branch, return the merge commit
for that event.
http://stackoverflow.com/questions/8475448/find-merge-commit-which-include-a-specific-commit
"""
commit_range = "{}..{}".format(commit, branch)
ancestry_paths = git.rev_list(commit_range, ancestry_path=True).splitlines()
first_parents = git.rev_list(commit_range, first_parent=True).splitlines()
both = set(ancestry_paths) & set(first_parents)
for commit_hash in reversed(ancestry_paths):
if commit_hash in both:
return repo.commit(commit_hash)
raise ValueError("No merge commit for {commit} in {branch}!".format(
commit=commit, branch=branch,
))
def get_prs_for_commit_range(commit_range):
"""
Returns a set of pull requests (integers) that contain all the commits
in the given commit range.
"""
pull_requests = set()
for commit in Commit.iter_items(repo, commit_range):
# ignore merge commits
if len(commit.parents) > 1:
continue
pull_requests.add(get_pr_for_commit(commit))
return pull_requests
def prs_by_email(commit_range):
"""
Returns an ordered dictionary of {email: pr_list}
Email is the email address of the person who merged the pull request
The dictionary is alphabetically ordered by email address
The pull request list is ordered by merge date
"""
unordered_data = defaultdict(set)
for pr_num in get_prs_for_commit_range(commit_range):
ref = "refs/remotes/origin/pr/{num}".format(num=pr_num)
branch = SymbolicReference(repo, ref)
merge = get_merge_commit(branch.commit)
unordered_data[merge.author.email].add((pr_num, merge))
ordered_data = OrderedDict()
for email in sorted(unordered_data.keys()):
ordered = sorted(unordered_data[email], key=lambda pair: pair[1].authored_date)
ordered_data[email] = [num for num, merge in ordered]
return ordered_data
def generate_table_by_commit(commit_range, include_merge=False):
""" """
Return a string corresponding to a commit table to embed in Confluence Return a string corresponding to a commit table to embed in Confluence
""" """
header = u"||Author||Summary||Commit||JIRA||Verified?||" header = "||Author||Summary||Commit||JIRA||Verified?||"
commit_link = "[commit|https://github.com/edx/edx-platform/commit/{sha}]" commit_link = "[commit|https://github.com/edx/edx-platform/commit/{sha}]"
rows = [header] rows = [header]
cbe = commits_by_email(commit_range, include_merge) cbe = commits_by_email(commit_range, include_merge)
for email, commits in cbe.items(): for email, commits in cbe.items():
for i, commit in enumerate(commits): for i, commit in enumerate(commits):
rows.append(u"| {author} | {summary} | {commit} | {jira} | {verified} |".format( rows.append("| {author} | {summary} | {commit} | {jira} | {verified} |".format(
author=email if i == 0 else "", author=email if i == 0 else "",
summary=commit.summary.replace("|", "\|"), summary=commit.summary.replace("|", "\|"),
commit=commit_link.format(sha=commit.hexsha), commit=commit_link.format(sha=commit.hexsha),
jira=", ".join(parse_ticket_references(commit.message)), jira=", ".join(parse_ticket_references(commit.message)),
verified="", verified="",
)) ))
return u"\n".join(rows) return "\n".join(rows)
def get_pr_info(num):
"""
Returns the info from the Github API
"""
url = "https://api.github.com/repos/edx/edx-platform/pulls/{num}".format(num=num)
response = requests.get(url)
result = response.json()
if not response.ok:
raise requests.exceptions.RequestException(result["message"])
return result
def generate_table_by_pr(commit_range):
"""
Return a string corresponding to a commit table to embed in Confluence
"""
header = "|| Merged By || Title || PR || JIRA || Verified? ||"
pr_link = "[#{num}|https://github.com/edx/edx-platform/pull/{num}]"
rows = [header]
prbe = prs_by_email(commit_range)
for email, pull_requests in prbe.items():
for i, pull_request in enumerate(pull_requests):
try:
pr_info = get_pr_info(pull_request)
title = pr_info["title"] or ""
body = pr_info["body"] or ""
except requests.exceptions.RequestException as e:
print(
"Warning: could not fetch data for #{num}: {message}".format(
num=pull_request, message=e.message
),
file=sys.stderr,
)
title = "?"
body = "?"
rows.append("| {merged_by} | {title} | {pull_request} | {jira} | {verified} |".format(
merged_by=email if i == 0 else "",
title=title.replace("|", "\|"),
pull_request=pr_link.format(num=pull_request),
jira=", ".join(parse_ticket_references(body)),
verified="",
))
return "\n".join(rows)
def generate_email(commit_range, release_date=None): def generate_email(commit_range, release_date=None):
...@@ -149,7 +301,10 @@ def main(): ...@@ -149,7 +301,10 @@ def main():
commit_range = "{0}..{1}".format(args.previous, args.current) commit_range = "{0}..{1}".format(args.previous, args.current)
if args.table: if args.table:
print(generate_table(commit_range, include_merge=args.merge)) if args.commit_table:
print(generate_table_by_commit(commit_range, include_merge=args.merge))
else:
print(generate_table_by_pr(commit_range))
return return
print("EMAIL:") print("EMAIL:")
...@@ -161,7 +316,10 @@ def main(): ...@@ -161,7 +316,10 @@ def main():
"in your release wiki page" "in your release wiki page"
) )
print("\n") print("\n")
print(generate_table(commit_range, include_merge=args.merge).encode('UTF-8')) if args.commit_table:
print(generate_table_by_commit(commit_range, include_merge=args.merge))
else:
print(generate_table_by_pr(commit_range))
if __name__ == "__main__": if __name__ == "__main__":
main() main()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment