bin/mkreleaselog: add github handle resolution and deduplication
- convert from zsh to bash for portability and shellcheck support - resolve GitHub handles via multiple methods: - noreply email pattern (user@users.noreply.github.com) - merge commit messages (Merge pull request #N from user/branch) - gh CLI API for PR authors (squash merge commits) - gh CLI API for commit authors (fallback for non-PR commits) - deduplicate contributors by GitHub handle instead of author name - cache resolved mappings in ~/.cache/mkreleaselog/github-handles.json - output clickable GitHub profile links in contributor table
Marcin Rataj committed
Nov 27, 2025 at 02:06 UTC
e0e6cacc49d61a2f9f80bf3f51040c4fb9a74b4a
1 file changed
+459
-68
bin/mkreleaselog
+459
-68
@@ -1,10 +1,19 @@
1
-#!/bin/zsh
1
+#!/bin/bash
2
#
3
# Invocation: mkreleaselog [FIRST_REF [LAST_REF]]
4
+#
5
+# Generates release notes with contributor statistics, deduplicating by GitHub handle.
6
+# GitHub handles are resolved from:
7
+# 1. GitHub noreply emails (user@users.noreply.github.com)
8
+# 2. Merge commit messages (Merge pull request #N from user/branch)
9
+# 3. GitHub API via gh CLI (for squash merges)
10
+#
11
+# Results are cached in ~/.cache/mkreleaselog/github-handles.json
12
13
set -euo pipefail
14
export GO111MODULE=on
7
-export GOPATH="$(go env GOPATH)"
15
+GOPATH="$(go env GOPATH)"
16
+export GOPATH
17
18
# List of PCRE regular expressions to match "included" modules.
19
INCLUDE_MODULES=(
@@ -47,16 +56,349 @@ IGNORE_FILES=(
56
"*.gen.go"
57
)
58
59
+##########################################################################################
60
+# GitHub Handle Resolution Infrastructure
61
+##########################################################################################
62
+
63
+# Cache location following XDG spec
64
+GITHUB_CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/mkreleaselog"
65
+GITHUB_CACHE_FILE="$GITHUB_CACHE_DIR/github-handles.json"
66
+
67
+# Timeout for gh CLI commands (seconds)
68
+GH_TIMEOUT=10
69
+
70
+# Associative array for email -> github handle mapping (runtime cache)
71
+declare -A EMAIL_TO_GITHUB
72
+
73
+# Check if gh CLI is available and authenticated
74
+gh_available() {
75
+ command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1
76
+}
77
+
78
+# Load cached email -> github handle mappings from disk
79
+load_github_cache() {
80
+ EMAIL_TO_GITHUB=()
81
+
82
+ if [[ ! -f "$GITHUB_CACHE_FILE" ]]; then
83
+ return 0
84
+ fi
85
+
86
+ # Validate JSON before loading
87
+ if ! jq -e '.' "$GITHUB_CACHE_FILE" >/dev/null 2>&1; then
88
+ msg "Warning: corrupted cache file, ignoring"
89
+ return 0
90
+ fi
91
+
92
+ local email handle
93
+ while IFS=$'\t' read -r email handle; do
94
+ # Validate handle format (alphanumeric, hyphens, max 39 chars)
95
+ if [[ -n "$email" && -n "$handle" && "$handle" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,37}[a-zA-Z0-9])?$ ]]; then
96
+ EMAIL_TO_GITHUB["$email"]="$handle"
97
+ fi
98
+ done < <(jq -r 'to_entries[] | "\(.key)\t\(.value)"' "$GITHUB_CACHE_FILE" 2>/dev/null)
99
+
100
+ msg "Loaded ${#EMAIL_TO_GITHUB[@]} cached GitHub handle mappings"
101
+}
102
+
103
+# Save email -> github handle mappings to disk (atomic write)
104
+save_github_cache() {
105
+ if [[ ${#EMAIL_TO_GITHUB[@]} -eq 0 ]]; then
106
+ return 0
107
+ fi
108
+
109
+ mkdir -p "$GITHUB_CACHE_DIR"
110
+
111
+ local tmp_file
112
+ tmp_file="$(mktemp "$GITHUB_CACHE_DIR/cache.XXXXXX")" || return 1
113
+
114
+ # Build JSON from associative array
115
+ {
116
+ echo "{"
117
+ local first=true
118
+ local key
119
+ for key in "${!EMAIL_TO_GITHUB[@]}"; do
120
+ if [[ "$first" == "true" ]]; then
121
+ first=false
122
+ else
123
+ echo ","
124
+ fi
125
+ # Escape special characters in email for JSON
126
+ printf ' %s: %s' "$(jq -n --arg e "$key" '$e')" "$(jq -n --arg h "${EMAIL_TO_GITHUB[$key]}" '$h')"
127
+ done
128
+ echo
129
+ echo "}"
130
+ } > "$tmp_file"
131
+
132
+ # Validate before replacing
133
+ if jq -e '.' "$tmp_file" >/dev/null 2>&1; then
134
+ mv "$tmp_file" "$GITHUB_CACHE_FILE"
135
+ msg "Saved ${#EMAIL_TO_GITHUB[@]} GitHub handle mappings to cache"
136
+ else
137
+ rm -f "$tmp_file"
138
+ msg "Warning: failed to save cache (invalid JSON)"
139
+ fi
140
+}
141
+
142
+# Extract GitHub handle from email if it's a GitHub noreply address
143
+# Handles: user@users.noreply.github.com and 12345678+user@users.noreply.github.com
144
+extract_handle_from_noreply() {
145
+ local email="$1"
146
+
147
+ if [[ "$email" =~ ^([0-9]+\+)?([a-zA-Z0-9]([a-zA-Z0-9-]{0,37}[a-zA-Z0-9])?)@users\.noreply\.github\.com$ ]]; then
148
+ echo "${BASH_REMATCH[2]}"
149
+ return 0
150
+ fi
151
+ return 1
152
+}
153
+
154
+# Extract GitHub handle from merge commit subject
155
+# Handles: "Merge pull request #123 from username/branch"
156
+extract_handle_from_merge_commit() {
157
+ local subject="$1"
158
+
159
+ if [[ "$subject" =~ ^Merge\ pull\ request\ \#[0-9]+\ from\ ([a-zA-Z0-9]([a-zA-Z0-9-]{0,37}[a-zA-Z0-9])?)/.*$ ]]; then
160
+ echo "${BASH_REMATCH[1]}"
161
+ return 0
162
+ fi
163
+ return 1
164
+}
165
+
166
+# Extract PR number from commit subject
167
+# Handles: "Subject (#123)" and "Merge pull request #123 from"
168
+extract_pr_number() {
169
+ local subject="$1"
170
+
171
+ if [[ "$subject" =~ \(#([0-9]+)\)$ ]]; then
172
+ echo "${BASH_REMATCH[1]}"
173
+ return 0
174
+ elif [[ "$subject" =~ ^Merge\ pull\ request\ \#([0-9]+)\ from ]]; then
175
+ echo "${BASH_REMATCH[1]}"
176
+ return 0
177
+ fi
178
+ return 1
179
+}
180
+
181
+# Query GitHub API for PR author (with timeout and error handling)
182
+query_pr_author() {
183
+ local gh_repo="$1" # e.g., "ipfs/kubo"
184
+ local pr_num="$2"
185
+
186
+ if ! gh_available; then
187
+ return 1
188
+ fi
189
+
190
+ local handle
191
+ handle="$(timeout "$GH_TIMEOUT" gh pr view "$pr_num" --repo "$gh_repo" --json author -q '.author.login' 2>/dev/null)" || return 1
192
+
193
+ # Validate handle format
194
+ if [[ -n "$handle" && "$handle" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,37}[a-zA-Z0-9])?$ ]]; then
195
+ echo "$handle"
196
+ return 0
197
+ fi
198
+ return 1
199
+}
200
+
201
+# Query GitHub API for commit author (fallback when no PR available)
202
+query_commit_author() {
203
+ local gh_repo="$1" # e.g., "ipfs/kubo"
204
+ local commit_sha="$2"
205
+
206
+ if ! gh_available; then
207
+ return 1
208
+ fi
209
+
210
+ local handle
211
+ handle="$(timeout "$GH_TIMEOUT" gh api "/repos/$gh_repo/commits/$commit_sha" --jq '.author.login // empty' 2>/dev/null)" || return 1
212
+
213
+ # Validate handle format
214
+ if [[ -n "$handle" && "$handle" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,37}[a-zA-Z0-9])?$ ]]; then
215
+ echo "$handle"
216
+ return 0
217
+ fi
218
+ return 1
219
+}
220
+
221
+# Resolve email to GitHub handle using all available methods
222
+# Args: email, commit_hash (optional), repo_dir (optional), gh_repo (optional)
223
+resolve_github_handle() {
224
+ local email="$1"
225
+ local commit="${2:-}"
226
+ local repo_dir="${3:-}"
227
+ local gh_repo="${4:-}"
228
+
229
+ # Skip empty emails
230
+ [[ -z "$email" ]] && return 1
231
+
232
+ # Check runtime cache first
233
+ if [[ -n "${EMAIL_TO_GITHUB[$email]:-}" ]]; then
234
+ echo "${EMAIL_TO_GITHUB[$email]}"
235
+ return 0
236
+ fi
237
+
238
+ local handle=""
239
+
240
+ # Method 1: Extract from noreply email
241
+ if handle="$(extract_handle_from_noreply "$email")"; then
242
+ EMAIL_TO_GITHUB["$email"]="$handle"
243
+ echo "$handle"
244
+ return 0
245
+ fi
246
+
247
+ # Method 2: Look at commit message for merge commit pattern
248
+ if [[ -n "$commit" && -n "$repo_dir" ]]; then
249
+ local subject
250
+ subject="$(git -C "$repo_dir" log -1 --format='%s' "$commit" 2>/dev/null)" || true
251
+
252
+ if [[ -n "$subject" ]]; then
253
+ if handle="$(extract_handle_from_merge_commit "$subject")"; then
254
+ EMAIL_TO_GITHUB["$email"]="$handle"
255
+ echo "$handle"
256
+ return 0
257
+ fi
258
+
259
+ # Method 3: Query GitHub API for PR author
260
+ if [[ -n "$gh_repo" ]]; then
261
+ local pr_num
262
+ if pr_num="$(extract_pr_number "$subject")"; then
263
+ if handle="$(query_pr_author "$gh_repo" "$pr_num")"; then
264
+ EMAIL_TO_GITHUB["$email"]="$handle"
265
+ echo "$handle"
266
+ return 0
267
+ fi
268
+ fi
269
+ fi
270
+ fi
271
+ fi
272
+
273
+ return 1
274
+}
275
+
276
+# Build GitHub handle mappings for all commits in a range
277
+# This does a single pass to collect PR numbers, then batch queries them
278
+build_github_mappings() {
279
+ local module="$1"
280
+ local start="$2"
281
+ local end="${3:-HEAD}"
282
+ local repo
283
+ repo="$(strip_version "$module")"
284
+ local dir
285
+ local gh_repo=""
286
+
287
+ if [[ "$module" == "github.com/ipfs/kubo" ]]; then
288
+ dir="$ROOT_DIR"
289
+ else
290
+ dir="$GOPATH/src/$repo"
291
+ fi
292
+
293
+ # Extract gh_repo for API calls (e.g., "ipfs/kubo" from "github.com/ipfs/kubo")
294
+ if [[ "$repo" =~ ^github\.com/(.+)$ ]]; then
295
+ gh_repo="${BASH_REMATCH[1]}"
296
+ fi
297
+
298
+ msg "Building GitHub handle mappings for $module..."
299
+
300
+ # Collect all unique emails and their commit context
301
+ declare -A email_commits=()
302
+ local hash email subject
303
+
304
+ while IFS=$'\t' read -r hash email subject; do
305
+ [[ -z "$email" ]] && continue
306
+
307
+ # Skip if already resolved
308
+ [[ -n "${EMAIL_TO_GITHUB[$email]:-}" ]] && continue
309
+
310
+ # Try to resolve without API first
311
+ local handle=""
312
+
313
+ # Method 1: noreply email
314
+ if handle="$(extract_handle_from_noreply "$email")"; then
315
+ EMAIL_TO_GITHUB["$email"]="$handle"
316
+ continue
317
+ fi
318
+
319
+ # Method 2: merge commit message
320
+ if handle="$(extract_handle_from_merge_commit "$subject")"; then
321
+ EMAIL_TO_GITHUB["$email"]="$handle"
322
+ continue
323
+ fi
324
+
325
+ # Store for potential API lookup
326
+ if [[ -z "${email_commits[$email]:-}" ]]; then
327
+ email_commits["$email"]="$hash"
328
+ fi
329
+ done < <(git -C "$dir" log --format='tformat:%H%x09%aE%x09%s' --no-merges "$start..$end" 2>/dev/null)
330
+
331
+ # API batch lookup for remaining emails (if gh is available)
332
+ if gh_available && [[ -n "$gh_repo" && ${#email_commits[@]} -gt 0 ]]; then
333
+ msg "Querying GitHub API for ${#email_commits[@]} unknown contributors..."
334
+ local key
335
+ for key in "${!email_commits[@]}"; do
336
+ # Skip if already resolved
337
+ [[ -n "${EMAIL_TO_GITHUB[$key]:-}" ]] && continue
338
+
339
+ local commit_hash="${email_commits[$key]}"
340
+ local subj handle
341
+ subj="$(git -C "$dir" log -1 --format='%s' "$commit_hash" 2>/dev/null)" || true
342
+
343
+ # Try PR author lookup first (cheaper API call)
344
+ local pr_num
345
+ if pr_num="$(extract_pr_number "$subj")"; then
346
+ if handle="$(query_pr_author "$gh_repo" "$pr_num")"; then
347
+ EMAIL_TO_GITHUB["$key"]="$handle"
348
+ continue
349
+ fi
350
+ fi
351
+
352
+ # Fallback: commit author API (works for any commit)
353
+ if handle="$(query_commit_author "$gh_repo" "$commit_hash")"; then
354
+ EMAIL_TO_GITHUB["$key"]="$handle"
355
+ fi
356
+ done
357
+ fi
358
+}
359
+
360
+##########################################################################################
361
+# Original infrastructure with modifications
362
##########################################################################################
363
364
+build_include_regex() {
365
+ local result=""
366
+ local mod
367
+ for mod in "${INCLUDE_MODULES[@]}"; do
368
+ if [[ -n "$result" ]]; then
369
+ result="$result|$mod"
370
+ else
371
+ result="$mod"
372
+ fi
373
+ done
374
+ echo "($result)"
375
+}
376
+
377
+build_exclude_regex() {
378
+ local result=""
379
+ local mod
380
+ for mod in "${EXCLUDE_MODULES[@]}"; do
381
+ if [[ -n "$result" ]]; then
382
+ result="$result|$mod"
383
+ else
384
+ result="$mod"
385
+ fi
386
+ done
387
+ if [[ -n "$result" ]]; then
388
+ echo "($result)"
389
+ else
390
+ echo '$^' # match nothing
391
+ fi
392
+}
393
+
394
if [[ ${#INCLUDE_MODULES[@]} -gt 0 ]]; then
53
- INCLUDE_REGEX="(${$(printf "|%s" "${INCLUDE_MODULES[@]}"):1})"
395
+ INCLUDE_REGEX="$(build_include_regex)"
396
else
397
INCLUDE_REGEX="" # "match anything"
398
fi
399
400
if [[ ${#EXCLUDE_MODULES[@]} -gt 0 ]]; then
59
- EXCLUDE_REGEX="(${$(printf "|%s" "${EXCLUDE_MODULES[@]}"):1})"
401
+ EXCLUDE_REGEX="$(build_exclude_regex)"
402
else
403
EXCLUDE_REGEX='$^' # "match nothing"
404
fi
@@ -71,8 +413,6 @@ NL=$'\n'
413
414
ROOT_DIR="$(git rev-parse --show-toplevel)"
415
74
-alias jq="jq --unbuffered"
75
-
416
msg() {
417
echo "$*" >&2
418
}
@@ -80,11 +420,21 @@ msg() {
420
statlog() {
421
local module="$1"
422
local rpath
423
+ local gh_repo=""
424
+
425
if [[ "$module" == "github.com/ipfs/kubo" ]]; then
426
rpath="$ROOT_DIR"
427
else
428
rpath="$GOPATH/src/$(strip_version "$module")"
429
fi
430
+
431
+ # Extract gh_repo for API calls
432
+ local repo
433
+ repo="$(strip_version "$module")"
434
+ if [[ "$repo" =~ ^github\.com/(.+)$ ]]; then
435
+ gh_repo="${BASH_REMATCH[1]}"
436
+ fi
437
+
438
local start="${2:-}"
439
local end="${3:-HEAD}"
440
local mailmap_file="$rpath/.mailmap"
@@ -93,18 +443,21 @@ statlog() {
443
fi
444
445
local stack=()
96
- git -C "$rpath" -c mailmap.file="$mailmap_file" log --use-mailmap --shortstat --no-merges --pretty="tformat:%H%x09%aN%x09%aE" "$start..$end" -- . "${IGNORE_FILES_PATHSPEC[@]}" | while read -r line; do
446
+ local line
447
+ while read -r line; do
448
if [[ -n "$line" ]]; then
449
stack+=("$line")
450
continue
451
fi
452
453
+ local changes
454
read -r changes
455
104
- changed=0
105
- insertions=0
106
- deletions=0
107
- while read count event; do
456
+ local changed=0
457
+ local insertions=0
458
+ local deletions=0
459
+ local count event
460
+ while read -r count event; do
461
if [[ "$event" =~ ^file ]]; then
462
changed=$count
463
elif [[ "$event" =~ ^insertion ]]; then
@@ -117,27 +470,32 @@ statlog() {
470
fi
471
done<<<"${changes//,/$NL}"
472
473
+ local author
474
for author in "${stack[@]}"; do
475
+ local hash name email
476
IFS=$'\t' read -r hash name email <<<"$author"
477
+
478
+ # Resolve GitHub handle
479
+ local github_handle=""
480
+ github_handle="$(resolve_github_handle "$email" "$hash" "$rpath" "$gh_repo")" || true
481
+
482
jq -n \
483
--arg "hash" "$hash" \
484
--arg "name" "$name" \
485
--arg "email" "$email" \
486
+ --arg "github" "$github_handle" \
487
--argjson "changed" "$changed" \
488
--argjson "insertions" "$insertions" \
489
--argjson "deletions" "$deletions" \
129
- '{Commit: $hash, Author: $name, Email: $email, Files: $changed, Insertions: $insertions, Deletions: $deletions}'
490
+ '{Commit: $hash, Author: $name, Email: $email, GitHub: $github, Files: $changed, Insertions: $insertions, Deletions: $deletions}'
491
done
492
stack=()
132
- done
493
+ done < <(git -C "$rpath" -c mailmap.file="$mailmap_file" log --use-mailmap --shortstat --no-merges --pretty="tformat:%H%x09%aN%x09%aE" "$start..$end" -- . "${IGNORE_FILES_PATHSPEC[@]}")
494
}
495
496
# Returns a stream of deps changed between $1 and $2.
497
dep_changes() {
137
- {
138
- <"$1"
139
- <"$2"
140
- } | jq -s 'JOIN(INDEX(.[0][]; .Path); .[1][]; .Path; {Path: .[0].Path, Old: (.[1] | del(.Path)), New: (.[0] | del(.Path))}) | select(.New.Version != .Old.Version)'
498
+ cat "$1" "$2" | jq -s 'JOIN(INDEX(.[0][]; .Path); .[1][]; .Path; {Path: .[0].Path, Old: (.[1] | del(.Path)), New: (.[0] | del(.Path))}) | select(.New.Version != .Old.Version)'
499
}
500
501
# resolve_commits resolves a git ref for each version.
@@ -165,12 +523,11 @@ ignored_commit() {
523
524
# Generate a release log for a range of commits in a single repo.
525
release_log() {
168
- setopt local_options BASH_REMATCH
169
-
526
local module="$1"
527
local start="$2"
528
local end="${3:-HEAD}"
173
- local repo="$(strip_version "$1")"
529
+ local repo
530
+ repo="$(strip_version "$1")"
531
local dir
532
if [[ "$module" == "github.com/ipfs/kubo" ]]; then
533
dir="$ROOT_DIR"
@@ -178,28 +535,25 @@ release_log() {
535
dir="$GOPATH/src/$repo"
536
fi
537
181
- local commit pr
182
- git -C "$dir" log \
183
- --format='tformat:%H %s' \
184
- --first-parent \
185
- "$start..$end" |
186
- while read commit subject; do
187
- # Skip commits that only touch ignored files.
188
- if ignored_commit "$dir" "$commit"; then
189
- continue
190
- fi
538
+ local commit subject
539
+ while read -r commit subject; do
540
+ # Skip commits that only touch ignored files.
541
+ if ignored_commit "$dir" "$commit"; then
542
+ continue
543
+ fi
544
192
- if [[ "$subject" =~ '^Merge pull request #([0-9]+) from' ]]; then
193
- local prnum="${BASH_REMATCH[2]}"
194
- local desc="$(git -C "$dir" show --summary --format='tformat:%b' "$commit" | head -1)"
195
- printf -- "- %s (%s)\n" "$desc" "$(pr_link "$repo" "$prnum")"
196
- elif [[ "$subject" =~ '\(#([0-9]+)\)$' ]]; then
197
- local prnum="${BASH_REMATCH[2]}"
198
- printf -- "- %s (%s)\n" "$subject" "$(pr_link "$repo" "$prnum")"
199
- else
200
- printf -- "- %s\n" "$subject"
201
- fi
202
- done
545
+ if [[ "$subject" =~ ^Merge\ pull\ request\ \#([0-9]+)\ from ]]; then
546
+ local prnum="${BASH_REMATCH[1]}"
547
+ local desc
548
+ desc="$(git -C "$dir" show --summary --format='tformat:%b' "$commit" | head -1)"
549
+ printf -- "- %s (%s)\n" "$desc" "$(pr_link "$repo" "$prnum")"
550
+ elif [[ "$subject" =~ \(#([0-9]+)\)$ ]]; then
551
+ local prnum="${BASH_REMATCH[1]}"
552
+ printf -- "- %s (%s)\n" "$subject" "$(pr_link "$repo" "$prnum")"
553
+ else
554
+ printf -- "- %s\n" "$subject"
555
+ fi
556
+ done < <(git -C "$dir" log --format='tformat:%H %s' --first-parent "$start..$end")
557
}
558
559
indent() {
@@ -211,7 +565,8 @@ mod_deps() {
565
}
566
567
ensure() {
214
- local repo="$(strip_version "$1")"
568
+ local repo
569
+ repo="$(strip_version "$1")"
570
local commit="$2"
571
local rpath
572
if [[ "$1" == "github.com/ipfs/kubo" ]]; then
@@ -232,14 +587,27 @@ ensure() {
587
git -C "$rpath" rev-parse --verify "$commit" >/dev/null || return 1
588
}
589
590
+# Summarize stats, grouping by GitHub handle (with fallback to email for dedup)
591
statsummary() {
236
- jq -s 'group_by(.Author)[] | {Author: .[0].Author, Commits: (. | length), Insertions: (map(.Insertions) | add), Deletions: (map(.Deletions) | add), Files: (map(.Files) | add)}' |
237
- jq '. + {Lines: (.Deletions + .Insertions)}'
592
+ jq -s '
593
+ # Group by GitHub handle if available, otherwise by email
594
+ group_by(if .GitHub != "" then .GitHub else .Email end)[] |
595
+ {
596
+ # Use first non-empty GitHub handle, or fall back to Author name
597
+ Author: .[0].Author,
598
+ GitHub: (map(select(.GitHub != "")) | .[0].GitHub // ""),
599
+ Email: .[0].Email,
600
+ Commits: (. | length),
601
+ Insertions: (map(.Insertions) | add),
602
+ Deletions: (map(.Deletions) | add),
603
+ Files: (map(.Files) | add)
604
+ }
605
+ ' | jq '. + {Lines: (.Deletions + .Insertions)}'
606
}
607
608
strip_version() {
609
local repo="$1"
242
- if [[ "$repo" =~ '.*/v[0-9]+$' ]]; then
610
+ if [[ "$repo" =~ .*/v[0-9]+$ ]]; then
611
repo="$(dirname "$repo")"
612
fi
613
echo "$repo"
@@ -248,16 +616,24 @@ strip_version() {
616
recursive_release_log() {
617
local start="${1:-$(git tag -l | sort -V | grep -v -- '-rc' | grep 'v'| tail -n1)}"
618
local end="${2:-$(git rev-parse HEAD)}"
251
- local repo_root="$(git rev-parse --show-toplevel)"
252
- local module="$(go list -m)"
253
- local dir="$(go list -m -f '{{.Dir}}')"
619
+ local repo_root
620
+ repo_root="$(git rev-parse --show-toplevel)"
621
+ local module
622
+ module="$(go list -m)"
623
+ local dir
624
+ dir="$(go list -m -f '{{.Dir}}')"
625
+
626
+ # Load cached GitHub handle mappings
627
+ load_github_cache
628
629
# Kubo can be run from any directory, dependencies still use GOPATH
630
631
(
632
local result=0
259
- local workspace="$(mktemp -d)"
260
- trap "$(printf 'rm -rf "%q"' "$workspace")" INT TERM EXIT
633
+ local workspace
634
+ workspace="$(mktemp -d)"
635
+ # shellcheck disable=SC2064
636
+ trap "rm -rf '$workspace'" INT TERM EXIT
637
cd "$workspace"
638
639
echo "Computing old deps..." >&2
@@ -272,6 +648,9 @@ recursive_release_log() {
648
649
printf -- "Generating Changelog for %s %s..%s\n" "$module" "$start" "$end" >&2
650
651
+ # Pre-build GitHub mappings for main module
652
+ build_github_mappings "$module" "$start" "$end"
653
+
654
echo "### 📝 Changelog"
655
echo
656
echo "<details><summary>Full Changelog</summary>"
@@ -282,24 +661,26 @@ recursive_release_log() {
661
662
statlog "$module" "$start" "$end" > statlog.json
663
285
- dep_changes old_deps.json new_deps.json |
664
+ local dep_module new new_ref old old_ref
665
+ while read -r dep_module new new_ref old old_ref; do
666
+ if ! ensure "$dep_module" "$new_ref"; then
667
+ result=1
668
+ local changelog="failed to fetch repo"
669
+ else
670
+ # Pre-build GitHub mappings for dependency
671
+ build_github_mappings "$dep_module" "$old_ref" "$new_ref"
672
+ statlog "$dep_module" "$old_ref" "$new_ref" >> statlog.json
673
+ local changelog
674
+ changelog="$(release_log "$dep_module" "$old_ref" "$new_ref")"
675
+ fi
676
+ if [[ -n "$changelog" ]]; then
677
+ printf -- "- %s (%s -> %s):\n" "$dep_module" "$old" "$new"
678
+ echo "$changelog" | indent
679
+ fi
680
+ done < <(dep_changes old_deps.json new_deps.json |
681
jq --arg inc "$INCLUDE_REGEX" --arg exc "$EXCLUDE_REGEX" \
682
'select(.Path | test($inc)) | select(.Path | test($exc) | not)' |
288
- # Compute changelogs
289
- jq -r '"\(.Path) \(.New.Version) \(.New.Ref) \(.Old.Version) \(.Old.Ref // "")"' |
290
- while read module new new_ref old old_ref; do
291
- if ! ensure "$module" "$new_ref"; then
292
- result=1
293
- local changelog="failed to fetch repo"
294
- else
295
- statlog "$module" "$old_ref" "$new_ref" >> statlog.json
296
- local changelog="$(release_log "$module" "$old_ref" "$new_ref")"
297
- fi
298
- if [[ -n "$changelog" ]]; then
299
- printf -- "- %s (%s -> %s):\n" "$module" "$old" "$new"
300
- echo "$changelog" | indent
301
- fi
302
- done
683
+ jq -r '"\(.Path) \(.New.Version) \(.New.Ref) \(.Old.Version) \(.Old.Ref // "")"')
684
685
echo
686
echo "</details>"
@@ -311,8 +692,18 @@ recursive_release_log() {
692
echo "|-------------|---------|---------|---------------|"
693
statsummary <statlog.json |
694
jq -s 'sort_by(.Lines) | reverse | .[]' |
314
- jq -r '"| \(.Author) | \(.Commits) | +\(.Insertions)/-\(.Deletions) | \(.Files) |"'
315
- return "$status"
695
+ jq -r '
696
+ if .GitHub != "" then
697
+ "| [@\(.GitHub)](https://github.com/\(.GitHub)) | \(.Commits) | +\(.Insertions)/-\(.Deletions) | \(.Files) |"
698
+ else
699
+ "| \(.Author) | \(.Commits) | +\(.Insertions)/-\(.Deletions) | \(.Files) |"
700
+ end
701
+ '
702
+
703
+ # Save cache before exiting
704
+ save_github_cache
705
+
706
+ return "$result"
707
)
708
}
709