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