|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# -*- encoding:utf-8 -*- |
| 3 | +""" |
| 4 | +Script to generate contributor and pull request lists |
| 5 | +
|
| 6 | +This script generates contributor and pull request lists for release |
| 7 | +announcements using Github v3 protocol. Use requires an authentication token in |
| 8 | +order to have sufficient bandwidth, you can get one following the directions at |
| 9 | +`<https://help.github.com/articles/creating-an-access-token-for-command-line-use/>_ |
| 10 | +Don't add any scope, as the default is read access to public information. The |
| 11 | +token may be stored in an environment variable as you only get one chance to |
| 12 | +see it. |
| 13 | +
|
| 14 | +Usage:: |
| 15 | +
|
| 16 | + $ ./scripts/announce.py <token> <revision range> |
| 17 | +
|
| 18 | +The output is utf8 rst. |
| 19 | +
|
| 20 | +Custom extension from the Pandas library: https://github.com/pandas-dev/pandas/blob/1.1.x/doc/sphinxext/announce.py |
| 21 | +Copied 10 August 2020 and subsequently modified. |
| 22 | +Specifically, get_authors was adjusted to check for a .mailmap file and use the git through the command line in order to utilize it if present. Using a mailmap file is currently not possible in gitpython (from git import Repo), and the recommended solution is to bring in the mailmap file yourself and use it to modify the author list (i.e. replicate the functionality that already exists in git). This felt a bit out of time-scope for right now. Alternatively, the git-fame library (imported as gitfame) uses the mailmap file and compiles statistics, but the python wrapper for this command line tool was taking forever. So, I've reverted to using os.system to use git behind the scenes instead. |
| 23 | +
|
| 24 | +Dependencies |
| 25 | +------------ |
| 26 | +
|
| 27 | +- gitpython |
| 28 | +- pygithub |
| 29 | +
|
| 30 | +Some code was copied from scipy `tools/gh_lists.py` and `tools/authors.py`. |
| 31 | +
|
| 32 | +Examples |
| 33 | +-------- |
| 34 | +
|
| 35 | +From the bash command line with $GITHUB token. |
| 36 | +
|
| 37 | + $ ./scripts/announce.py $GITHUB v1.11.0..v1.11.1 > announce.rst |
| 38 | +
|
| 39 | +""" |
| 40 | +import codecs |
| 41 | +import os |
| 42 | +import re |
| 43 | +import textwrap |
| 44 | + |
| 45 | +from git import Repo |
| 46 | + |
| 47 | +UTF8Writer = codecs.getwriter("utf8") |
| 48 | +this_repo = Repo(os.path.join(os.path.dirname(__file__), "..", "..")) |
| 49 | + |
| 50 | +author_msg = """\ |
| 51 | +A total of %d people contributed to this release. People with a |
| 52 | +"+" by their names contributed for the first time. |
| 53 | +""" |
| 54 | + |
| 55 | +pull_request_msg = """\ |
| 56 | +A total of %d pull requests were merged for this release. |
| 57 | +""" |
| 58 | + |
| 59 | + |
| 60 | +def get_authors(revision_range): |
| 61 | + pat = "^.*\\t(.*)$" |
| 62 | + lst_release, cur_release = [r.strip() for r in revision_range.split("..")] |
| 63 | + |
| 64 | + if "|" in cur_release: |
| 65 | + # e.g. v1.0.1|HEAD |
| 66 | + maybe_tag, head = cur_release.split("|") |
| 67 | + assert head == "HEAD" |
| 68 | + if maybe_tag in this_repo.tags: |
| 69 | + cur_release = maybe_tag |
| 70 | + else: |
| 71 | + cur_release = head |
| 72 | + revision_range = f"{lst_release}..{cur_release}" |
| 73 | + |
| 74 | + # authors, in current release and previous to current release. |
| 75 | + # We need two passes over the log for cur and prev, one to get the |
| 76 | + # "Co-authored by" commits, which come from backports by the bot, |
| 77 | + # and one for regular commits. |
| 78 | + if ".mailmap" in os.listdir(this_repo.git.working_dir): |
| 79 | + |
| 80 | + xpr = re.compile(r"Co-authored-by: (?P<name>[^<]+) ") |
| 81 | + |
| 82 | + gitcur = list(os.popen("git shortlog -s " + revision_range).readlines()) |
| 83 | + cur = [] |
| 84 | + for n in gitcur: |
| 85 | + n = re.search(r".*?\t(.*)\n.*", n).group(1) |
| 86 | + cur.append(n) |
| 87 | + cur = set(cur) |
| 88 | + |
| 89 | + gitpre = list(os.popen("git shortlog -s " + lst_release).readlines()) |
| 90 | + pre = [] |
| 91 | + for n in gitpre: |
| 92 | + n = re.search(r".*?\t(.*)\n.*", n).group(1) |
| 93 | + pre.append(n) |
| 94 | + pre = set(pre) |
| 95 | + |
| 96 | + else: |
| 97 | + |
| 98 | + xpr = re.compile(r"Co-authored-by: (?P<name>[^<]+) ") |
| 99 | + cur = set( |
| 100 | + xpr.findall( |
| 101 | + this_repo.git.log("--grep=Co-authored", "--pretty=%b", revision_range) |
| 102 | + ) |
| 103 | + ) |
| 104 | + cur |= set(re.findall(pat, this_repo.git.shortlog("-se", revision_range), re.M)) |
| 105 | + |
| 106 | + pre = set( |
| 107 | + xpr.findall( |
| 108 | + this_repo.git.log("--grep=Co-authored", "--pretty=%b", lst_release) |
| 109 | + ) |
| 110 | + ) |
| 111 | + pre |= set(re.findall(pat, this_repo.git.shortlog("-se", lst_release), re.M)) |
| 112 | + |
| 113 | + # Homu is the author of auto merges, clean him out. |
| 114 | + # cur.discard("Homu") |
| 115 | + # pre.discard("Homu") |
| 116 | + |
| 117 | + # Append '+' to new authors. |
| 118 | + authors = [s + " +" for s in cur - pre] + [s for s in cur & pre] |
| 119 | + authors.sort() |
| 120 | + return authors |
| 121 | + |
| 122 | + |
| 123 | +def get_pull_requests(repo, revision_range): |
| 124 | + prnums = [] |
| 125 | + |
| 126 | + # From regular merges |
| 127 | + merges = this_repo.git.log("--oneline", "--merges", revision_range) |
| 128 | + issues = re.findall("Merge pull request \\#(\\d*)", merges) |
| 129 | + prnums.extend(int(s) for s in issues) |
| 130 | + |
| 131 | + # From Homu merges (Auto merges) |
| 132 | + issues = re.findall("Auto merge of \\#(\\d*)", merges) |
| 133 | + prnums.extend(int(s) for s in issues) |
| 134 | + |
| 135 | + # From fast forward squash-merges |
| 136 | + commits = this_repo.git.log( |
| 137 | + "--oneline", "--no-merges", "--first-parent", revision_range |
| 138 | + ) |
| 139 | + issues = re.findall("^.*\\(\\#(\\d+)\\)$", commits, re.M) |
| 140 | + prnums.extend(int(s) for s in issues) |
| 141 | + |
| 142 | + # get PR data from github repo |
| 143 | + prnums.sort() |
| 144 | + prs = [repo.get_pull(n) for n in prnums] |
| 145 | + return prs |
| 146 | + |
| 147 | + |
| 148 | +def build_components(revision_range, heading="Contributors"): |
| 149 | + lst_release, cur_release = [r.strip() for r in revision_range.split("..")] |
| 150 | + authors = get_authors(revision_range) |
| 151 | + |
| 152 | + return { |
| 153 | + "heading": heading, |
| 154 | + "author_message": author_msg % len(authors), |
| 155 | + "authors": authors, |
| 156 | + } |
| 157 | + |
| 158 | + |
| 159 | +def build_string(revision_range, heading="Contributors"): |
| 160 | + components = build_components(revision_range, heading=heading) |
| 161 | + components["uline"] = "=" * len(components["heading"]) |
| 162 | + components["authors"] = "* " + "\n* ".join(components["authors"]) |
| 163 | + |
| 164 | + # Don't change this to an fstring. It breaks the formatting. |
| 165 | + tpl = textwrap.dedent( |
| 166 | + """\ |
| 167 | + {heading} |
| 168 | + {uline} |
| 169 | +
|
| 170 | + {author_message} |
| 171 | + {authors}""" |
| 172 | + ).format(**components) |
| 173 | + return tpl |
| 174 | + |
| 175 | + |
| 176 | +def main(revision_range): |
| 177 | + # document authors |
| 178 | + text = build_string(revision_range) |
| 179 | + print(text) |
| 180 | + |
| 181 | + |
| 182 | +if __name__ == "__main__": |
| 183 | + from argparse import ArgumentParser |
| 184 | + |
| 185 | + parser = ArgumentParser(description="Generate author lists for release") |
| 186 | + parser.add_argument("revision_range", help="<revision>..<revision>") |
| 187 | + args = parser.parse_args() |
| 188 | + main(args.revision_range) |
| 189 | + |
| 190 | + |
| 191 | +""" |
| 192 | +Early attempts at implementing use of mailmap within this script: |
| 193 | +
|
| 194 | +cur = re.compile('|'.join(map(re.escape, ['<*>']))) |
| 195 | +# ------------ |
| 196 | +with open(os.path.join(os.path.dirname('[fill in here]'), "[repo_name]/.mailmap")) as f: |
| 197 | + l = [line.rstrip('\n') for line in f] |
| 198 | +
|
| 199 | +m={} |
| 200 | +for line in l: |
| 201 | + try: |
| 202 | + m.update(dict(line.split('> '))) |
| 203 | + except ValueError: |
| 204 | + m.update({line:''}) |
| 205 | +""" |
0 commit comments