release.py 5.51 KB
Newer Older
David Baumgold committed
1 2 3 4 5 6 7 8 9 10 11 12 13
#!/usr/bin/env python
"""
a release-master multitool
"""
from path import path
from git import Repo
import argparse
from datetime import date, timedelta
from dateutil.parser import parse as parse_datestring
import re
from collections import OrderedDict
import textwrap

14
IGNORED_EMAILS = set(("vagrant@precise32.(none)",))
David Baumgold committed
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
JIRA_RE = re.compile(r"\b[A-Z]{2,}-\d+\b")
PROJECT_ROOT = path(__file__).abspath().dirname()
repo = Repo(PROJECT_ROOT)
git = repo.git


def make_parser():
    parser = argparse.ArgumentParser(description="release master multitool")
    parser.add_argument(
        '--previous', '--prev', '-p', metavar="GITREV", default="origin/release",
        help="previous release [origin/release]")
    parser.add_argument(
        '--current', '--curr', '-c', metavar="GITREV", default="HEAD",
        help="current release candidate [HEAD]")
    parser.add_argument(
        '--date', '-d',
        help="expected release date: defaults to "
        "next Tuesday [{}]".format(default_release_date()))
    parser.add_argument(
        '--merge', '-m', action="store_true", default=False,
        help="include merge commits")
    parser.add_argument(
        '--table', '-t', action="store_true", default=False,
        help="only print table")
    return parser


def default_release_date():
    """
    Returns a date object corresponding to the expected date of the next release:
    normally, this Tuesday.
    """
    today = date.today()
    TUESDAY = 2
    days_until_tuesday = (TUESDAY - today.isoweekday()) % 7
    return today + timedelta(days=days_until_tuesday)


def parse_ticket_references(text):
    """
    Given a commit message, return a list of all JIRA ticket references in that
    message. If there are no ticket references, return an empty list.
    """
    return JIRA_RE.findall(text)


61
def emails(commit_range):
David Baumgold committed
62 63 64 65 66 67 68
    """
    Returns a set of all email addresses responsible for the commits between
    the two commit references.
    """
    # %ae prints the authored_by email for the commit
    # %n prints a newline
    # %ce prints the committed_by email for the commit
69 70
    emails = set(git.log(commit_range, format='%ae%n%ce').splitlines())
    return emails - IGNORED_EMAILS
David Baumgold committed
71 72


73
def commits_by_email(commit_range, include_merge=False):
David Baumgold committed
74 75 76 77 78 79 80 81 82 83
    """
    Return a ordered dictionary of {email: commit_list}
    The dictionary is alphabetically ordered by email address
    The commit list is ordered by commit author date
    """
    kwargs = {}
    if not include_merge:
        kwargs["no-merges"] = True

    data = OrderedDict()
84
    for email in sorted(emails(commit_range)):
David Baumgold committed
85 86 87 88 89 90 91 92 93 94 95
        authored_commits = set(repo.iter_commits(
            commit_range, author=email, **kwargs
        ))
        committed_commits = set(repo.iter_commits(
            commit_range, committer=email, **kwargs
        ))
        commits = authored_commits | committed_commits
        data[email] = sorted(commits, key=lambda c: c.authored_date)
    return data


96
def generate_table(commit_range, include_merge=False):
David Baumgold committed
97 98 99
    """
    Return a string corresponding to a commit table to embed in Confluence
    """
e0d committed
100
    header = u"||Author||Summary||Commit||JIRA||Verified?||"
David Baumgold committed
101 102
    commit_link = "[commit|https://github.com/edx/edx-platform/commit/{sha}]"
    rows = [header]
103
    cbe = commits_by_email(commit_range, include_merge)
David Baumgold committed
104 105
    for email, commits in cbe.items():
        for i, commit in enumerate(commits):
e0d committed
106
            rows.append(u"| {author} | {summary} | {commit} | {jira} | {verified} |".format(
David Baumgold committed
107
                author=email if i == 0 else "",
e0d committed
108
                summary=commit.summary.replace("|", "\|"),
David Baumgold committed
109 110 111 112
                commit=commit_link.format(sha=commit.hexsha),
                jira=", ".join(parse_ticket_references(commit.message)),
                verified="",
            ))
e0d committed
113
    return u"\n".join(rows)
David Baumgold committed
114 115


116
def generate_email(commit_range, release_date=None):
David Baumgold committed
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131
    """
    Returns a string roughly approximating an email.
    """
    if release_date is None:
        release_date = default_release_date()

    email = """
        To: {emails}

        You've made changes that are about to be released. All of the commits
        that you either authored or committed are listed below. Please verify them on
        stage.edx.org and stage-edge.edx.org.

        Please record your notes on https://edx-wiki.atlassian.net/wiki/display/ENG/Release+Page%3A+{date}
        and add any bugs found to the Release Candidate Bugs section.
132 133 134 135

        If you are a non-affiliated open-source contributor to edx-platform,
        the edX employee who merged in your pull request will manually verify
        your change(s), and you may disregard this message.
David Baumgold committed
136
    """.format(
137
        emails=", ".join(sorted(emails(commit_range))),
David Baumgold committed
138 139 140 141 142 143 144 145 146 147 148
        date=release_date.isoformat(),
    )
    return textwrap.dedent(email).strip()


def main():
    parser = make_parser()
    args = parser.parse_args()
    if isinstance(args.date, basestring):
        # user passed in a custom date, so we need to parse it
        args.date = parse_datestring(args.date).date()
149
    commit_range = "{0}..{1}".format(args.previous, args.current)
David Baumgold committed
150 151

    if args.table:
152
        print(generate_table(commit_range, include_merge=args.merge))
David Baumgold committed
153 154 155
        return

    print("EMAIL:")
156
    print(generate_email(commit_range, release_date=args.date).encode('UTF-8'))
David Baumgold committed
157 158 159 160 161 162 163
    print("\n")
    print("Wiki Table:")
    print(
        "Type Ctrl+Shift+D on Confluence to embed the following table "
        "in your release wiki page"
    )
    print("\n")
164
    print(generate_table(commit_range, include_merge=args.merge).encode('UTF-8'))
David Baumgold committed
165 166 167

if __name__ == "__main__":
    main()