|
| 1 | +#!/bin/zsh |
| 2 | + |
| 3 | +# Note: This script is a modified version of the mkreleaselog script used by |
| 4 | +# the go-ipfs team. |
| 5 | +# |
| 6 | +# Usage: ./mkreleaselog v0.25.0 v0.26.0 > /tmp/release.log |
| 7 | + |
| 8 | +set -euo pipefail |
| 9 | +export GO111MODULE=on |
| 10 | +export GOPATH="$(go env GOPATH)" |
| 11 | + |
| 12 | +alias jq="jq --unbuffered" |
| 13 | + |
| 14 | +REPO_SUFFIXES_TO_STRIP=( |
| 15 | + "/v2" |
| 16 | + "/v3" |
| 17 | + "/v4" |
| 18 | + "/v5" |
| 19 | + "/v6" |
| 20 | +) |
| 21 | + |
| 22 | +AUTHORS=( |
| 23 | + # orgs |
| 24 | + filecoin-project |
| 25 | + |
| 26 | + # Authors of personal repos used by filecoin-ffi that should be mentioned in the |
| 27 | + # release notes. |
| 28 | + xlab |
| 29 | +) |
| 30 | + |
| 31 | +[[ -n "${REPO_FILTER+x}" ]] || REPO_FILTER="github.com/(${$(printf "|%s" "${AUTHORS[@]}"):1})" |
| 32 | + |
| 33 | +[[ -n "${IGNORED_FILES+x}" ]] || IGNORED_FILES='^\(\.gx\|package\.json\|\.travis\.yml\|go.mod\|go\.sum|\.github|\.circleci\)$' |
| 34 | + |
| 35 | +NL=$'\n' |
| 36 | + |
| 37 | +msg() { |
| 38 | + echo "$*" >&2 |
| 39 | +} |
| 40 | + |
| 41 | +statlog() { |
| 42 | + rpath="$GOPATH/src/$1" |
| 43 | + for s in $REPO_SUFFIXES_TO_STRIP; do |
| 44 | + rpath=${rpath%$s} |
| 45 | + done |
| 46 | + |
| 47 | + start="${2:-}" |
| 48 | + end="${3:-HEAD}" |
| 49 | + |
| 50 | + git -C "$rpath" log --shortstat --no-merges --pretty="tformat:%H%n%aN%n%aE" "$start..$end" | while |
| 51 | + read hash |
| 52 | + read name |
| 53 | + read email |
| 54 | + read _ # empty line |
| 55 | + read changes |
| 56 | + do |
| 57 | + changed=0 |
| 58 | + insertions=0 |
| 59 | + deletions=0 |
| 60 | + while read count event; do |
| 61 | + if [[ "$event" =~ ^file ]]; then |
| 62 | + changed=$count |
| 63 | + elif [[ "$event" =~ ^insertion ]]; then |
| 64 | + insertions=$count |
| 65 | + elif [[ "$event" =~ ^deletion ]]; then |
| 66 | + deletions=$count |
| 67 | + else |
| 68 | + echo "unknown event $event" >&2 |
| 69 | + exit 1 |
| 70 | + fi |
| 71 | + done<<<"${changes//,/$NL}" |
| 72 | + |
| 73 | + jq -n \ |
| 74 | + --arg "hash" "$hash" \ |
| 75 | + --arg "name" "$name" \ |
| 76 | + --arg "email" "$email" \ |
| 77 | + --argjson "changed" "$changed" \ |
| 78 | + --argjson "insertions" "$insertions" \ |
| 79 | + --argjson "deletions" "$deletions" \ |
| 80 | + '{Commit: $hash, Author: $name, Email: $email, Files: $changed, Insertions: $insertions, Deletions: $deletions}' |
| 81 | + done |
| 82 | +} |
| 83 | + |
| 84 | +# Returns a stream of deps changed between $1 and $2. |
| 85 | +dep_changes() { |
| 86 | + { |
| 87 | + <"$1" |
| 88 | + <"$2" |
| 89 | + } | jq -s 'JOIN(INDEX(.[0][]; .Path); .[1][]; .Path; {Path: .[0].Path, Old: (.[1] | del(.Path)), New: (.[0] | del(.Path))}) | select(.New.Version != .Old.Version)' |
| 90 | +} |
| 91 | + |
| 92 | +# resolve_commits resolves a git ref for each version. |
| 93 | +resolve_commits() { |
| 94 | + jq '. + {Ref: (.Version|capture("^((?<ref1>.*)\\+incompatible|v.*-(0\\.)?[0-9]{14}-(?<ref2>[a-f0-9]{12})|(?<ref3>v.*))$") | .ref1 // .ref2 // .ref3)}' |
| 95 | +} |
| 96 | + |
| 97 | +pr_link() { |
| 98 | + local repo="$1" |
| 99 | + local prnum="$2" |
| 100 | + local ghname="${repo##github.com/}" |
| 101 | + printf -- "[%s#%s](https://%s/pull/%s)" "$ghname" "$prnum" "$repo" "$prnum" |
| 102 | +} |
| 103 | + |
| 104 | +# Generate a release log for a range of commits in a single repo. |
| 105 | +release_log() { |
| 106 | + setopt local_options BASH_REMATCH |
| 107 | + |
| 108 | + local repo="$1" |
| 109 | + local start="$2" |
| 110 | + local end="${3:-HEAD}" |
| 111 | + local dir="$GOPATH/src/$repo" |
| 112 | + |
| 113 | + local commit pr |
| 114 | + git -C "$dir" log \ |
| 115 | + --format='tformat:%H %s' \ |
| 116 | + --first-parent \ |
| 117 | + "$start..$end" | |
| 118 | + while read commit subject; do |
| 119 | + # Skip gx-only PRs. |
| 120 | + git -C "$dir" diff-tree --no-commit-id --name-only "$commit^" "$commit" | |
| 121 | + grep -v "${IGNORED_FILES}" >/dev/null || continue |
| 122 | + |
| 123 | + if [[ "$subject" =~ '^Merge pull request #([0-9]+) from' ]]; then |
| 124 | + local prnum="${BASH_REMATCH[2]}" |
| 125 | + local desc="$(git -C "$dir" show --summary --format='tformat:%b' "$commit" | head -1)" |
| 126 | + printf -- "- %s (%s)\n" "$desc" "$(pr_link "$repo" "$prnum")" |
| 127 | + elif [[ "$subject" =~ '\(#([0-9]+)\)$' ]]; then |
| 128 | + local prnum="${BASH_REMATCH[2]}" |
| 129 | + printf -- "- %s (%s)\n" "$subject" "$(pr_link "$repo" "$prnum")" |
| 130 | + else |
| 131 | + printf -- "- %s\n" "$subject" |
| 132 | + fi |
| 133 | + done |
| 134 | +} |
| 135 | + |
| 136 | +indent() { |
| 137 | + sed -e 's/^/ /' |
| 138 | +} |
| 139 | + |
| 140 | +mod_deps() { |
| 141 | + go list -json -m all | jq 'select(.Version != null)' |
| 142 | +} |
| 143 | + |
| 144 | +ensure() { |
| 145 | + local repo="$1" |
| 146 | + for s in $REPO_SUFFIXES_TO_STRIP; do |
| 147 | + repo=${repo%$s} |
| 148 | + done |
| 149 | + |
| 150 | + local commit="$2" |
| 151 | + |
| 152 | + local rpath="$GOPATH/src/$repo" |
| 153 | + if [[ ! -d "$rpath" ]]; then |
| 154 | + msg "Cloning $repo..." |
| 155 | + git clone "http://$repo" "$rpath" >&2 |
| 156 | + fi |
| 157 | + |
| 158 | + if ! git -C "$rpath" rev-parse --verify "$commit" >/dev/null; then |
| 159 | + msg "Fetching $repo..." |
| 160 | + git -C "$rpath" fetch --all >&2 |
| 161 | + fi |
| 162 | + |
| 163 | + git -C "$rpath" rev-parse --verify "$commit" >/dev/null || return 1 |
| 164 | +} |
| 165 | + |
| 166 | +statsummary() { |
| 167 | + jq -s 'group_by(.Author)[] | {Author: .[0].Author, Commits: (. | length), Insertions: (map(.Insertions) | add), Deletions: (map(.Deletions) | add), Files: (map(.Files) | add)}' | |
| 168 | + jq '. + {Lines: (.Deletions + .Insertions)}' |
| 169 | +} |
| 170 | + |
| 171 | +recursive_release_log() { |
| 172 | + local start="${1:-$(git tag -l | sort -V | grep -v -- '-rc' | grep 'v'| tail -n1)}" |
| 173 | + local end="${2:-$(git rev-parse HEAD)}" |
| 174 | + local repo_root="$(git rev-parse --show-toplevel)" |
| 175 | + local package="$(cd "$repo_root" && go list)" |
| 176 | + |
| 177 | + if ! [[ "${GOPATH}/${package}" != "${repo_root}" ]]; then |
| 178 | + echo "This script requires the target package and all dependencies to live in a GOPATH." |
| 179 | + return 1 |
| 180 | + fi |
| 181 | + |
| 182 | + ( |
| 183 | + local result=0 |
| 184 | + local workspace="$(mktemp -d)" |
| 185 | + trap "$(printf 'rm -rf "%q"' "$workspace")" INT TERM EXIT |
| 186 | + cd "$workspace" |
| 187 | + |
| 188 | + echo "Computing old deps..." >&2 |
| 189 | + git -C "$repo_root" show "$start:go.mod" >go.mod |
| 190 | + mod_deps | resolve_commits | jq -s > old_deps.json |
| 191 | + |
| 192 | + echo "Computing new deps..." >&2 |
| 193 | + git -C "$repo_root" show "$end:go.mod" >go.mod |
| 194 | + mod_deps | resolve_commits | jq -s > new_deps.json |
| 195 | + |
| 196 | + rm -f go.mod go.sum |
| 197 | + |
| 198 | + printf -- "Generating Changelog for %s %s..%s\n" "$package" "$start" "$end" >&2 |
| 199 | + |
| 200 | + printf -- "- %s:\n" "$package" |
| 201 | + release_log "$package" "$start" "$end" | indent |
| 202 | + |
| 203 | + statlog "$package" "$start" "$end" > statlog.json |
| 204 | + |
| 205 | + dep_changes old_deps.json new_deps.json | |
| 206 | + jq --arg filter "$REPO_FILTER" 'select(.Path | match($filter))' | |
| 207 | + # Compute changelogs |
| 208 | + jq -r '"\(.Path) \(.New.Version) \(.New.Ref) \(.Old.Version) \(.Old.Ref // "")"' | |
| 209 | + while read repo new new_ref old old_ref; do |
| 210 | + for s in $REPO_SUFFIXES_TO_STRIP; do |
| 211 | + repo=${repo%$s} |
| 212 | + done |
| 213 | + |
| 214 | + if ! ensure "$repo" "$new_ref"; then |
| 215 | + result=1 |
| 216 | + local changelog="failed to fetch repo" |
| 217 | + else |
| 218 | + statlog "$repo" "$old_ref" "$new_ref" >> statlog.json |
| 219 | + local changelog="$(release_log "$repo" "$old_ref" "$new_ref")" |
| 220 | + fi |
| 221 | + if [[ -n "$changelog" ]]; then |
| 222 | + printf -- "- %s (%s -> %s):\n" "$repo" "$old" "$new" |
| 223 | + echo "$changelog" | indent |
| 224 | + fi |
| 225 | + done |
| 226 | + |
| 227 | + echo |
| 228 | + echo "Contributors" |
| 229 | + echo |
| 230 | + |
| 231 | + echo "| Contributor | Commits | Lines ± | Files Changed |" |
| 232 | + echo "|-------------|---------|---------|---------------|" |
| 233 | + statsummary <statlog.json | |
| 234 | + jq -s 'sort_by(.Lines) | reverse | .[]' | |
| 235 | + jq -r '"| \(.Author) | \(.Commits) | +\(.Insertions)/-\(.Deletions) | \(.Files) |"' |
| 236 | + return "$status" |
| 237 | + ) |
| 238 | +} |
| 239 | + |
| 240 | +recursive_release_log "$@" |
0 commit comments