-
-
Save gregjkal/3a41c09906daccf28637c1f38057e1d6 to your computer and use it in GitHub Desktop.
Rebuild GitHub contribution graph data via the gh CLI (works around the private-profile API blackout) for display on gregkaleka.com
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # | |
| # fetch-github-activity.sh | |
| # | |
| # Rebuilds GitHub contribution-graph data for the authenticated user and | |
| # writes it as JSON for Hugo to render (data/github_activity.json). | |
| # | |
| # Why this exists: my GitHub profile is set to private, which blanks the | |
| # contributionsCollection GraphQL API even for my own token. The underlying | |
| # activity is still visible to an authenticated token through repo-scoped | |
| # search, so this script reassembles the contribution calendar and activity | |
| # overview from primary sources: | |
| # | |
| # - commits: GET /search/commits (author:me, bucketed by author date; | |
| # commit search indexes default branches, same as the graph) | |
| # - pull requests: GET /search/issues (type:pr, bucketed by created date) | |
| # - issues: GET /search/issues (type:issue, bucketed by created date) | |
| # - reviews: GET /search/issues (reviewed-by:me) to find PRs, then | |
| # GraphQL to get each review's submittedAt date | |
| # | |
| # Counts are a close approximation of GitHub's own graph, not a byte-for-byte | |
| # copy (GitHub also counts things like repository creation, and buckets by the | |
| # viewer's timezone). | |
| # | |
| # Requirements: gh (authenticated, repo scope), jq. | |
| # Usage: ./scripts/fetch-github-activity.sh [output.json] | |
| set -euo pipefail | |
| OUT="${1:-data/github_activity.json}" | |
| USER=$(gh api user --jq .login) | |
| # BSD (macOS) and GNU date compatibility | |
| if date -v -1d +%F >/dev/null 2>&1; then | |
| days_ago() { date -v -"$1"d +%F; } | |
| else | |
| days_ago() { date -d "$1 days ago" +%F; } | |
| fi | |
| END=$(date +%F) | |
| TODAY_DOW=$(date +%w) # 0 = Sunday | |
| # Match GitHub's grid: 52 full Sun-Sat weeks plus the current partial week, | |
| # so the first column starts on a Sunday. | |
| START=$(days_ago $((364 + TODAY_DOW))) | |
| echo "Fetching contribution data for @$USER ($START .. $END)" >&2 | |
| tmp=$(mktemp -d) | |
| trap 'rm -rf "$tmp"' EXIT | |
| echo " commits..." >&2 | |
| gh api -X GET search/commits -f q="author:$USER author-date:$START..$END" \ | |
| -f per_page=100 -f sort=author-date --paginate \ | |
| --jq '.items[].commit.author.date[:10]' > "$tmp/commits.txt" | |
| echo " pull requests..." >&2 | |
| gh api -X GET search/issues -f q="author:$USER type:pr created:$START..$END" \ | |
| -f advanced_search=true -f per_page=100 --paginate \ | |
| --jq '.items[].created_at[:10]' > "$tmp/prs.txt" | |
| echo " issues..." >&2 | |
| gh api -X GET search/issues -f q="author:$USER type:issue created:$START..$END" \ | |
| -f advanced_search=true -f per_page=100 --paginate \ | |
| --jq '.items[].created_at[:10]' > "$tmp/issues.txt" | |
| echo " reviewed PRs..." >&2 | |
| # A review submission bumps a PR's updated date, so updated:>=START finds | |
| # every PR that could hold a review inside the window. | |
| gh api -X GET search/issues -f q="reviewed-by:$USER -author:$USER type:pr updated:>=$START" \ | |
| -f advanced_search=true -f per_page=100 --paginate \ | |
| --jq '.items[].node_id' > "$tmp/review_pr_ids.txt" | |
| echo " review dates ($(wc -l < "$tmp/review_pr_ids.txt" | tr -d ' ') PRs)..." >&2 | |
| : > "$tmp/reviews.txt" | |
| if [ -s "$tmp/review_pr_ids.txt" ]; then | |
| split -l 50 "$tmp/review_pr_ids.txt" "$tmp/id_batch_" | |
| for batch in "$tmp"/id_batch_*; do | |
| args=() | |
| while IFS= read -r id; do | |
| args+=(-f "ids[]=$id") | |
| done < "$batch" | |
| gh api graphql \ | |
| -f query='query($ids: [ID!]!, $login: String!) { | |
| nodes(ids: $ids) { | |
| ... on PullRequest { | |
| reviews(first: 100, author: $login) { nodes { submittedAt } } | |
| } | |
| } | |
| }' \ | |
| -f login="$USER" "${args[@]}" \ | |
| --jq '.data.nodes[].reviews.nodes[].submittedAt // empty | .[:10]' >> "$tmp/reviews.txt" | |
| done | |
| fi | |
| for f in commits prs issues reviews; do | |
| jq -R -s 'split("\n") | map(select(. != ""))' < "$tmp/$f.txt" > "$tmp/$f.json" | |
| done | |
| jq -n \ | |
| --arg user "$USER" \ | |
| --arg start "$START" \ | |
| --arg end "$END" \ | |
| --slurpfile commits "$tmp/commits.json" \ | |
| --slurpfile prs "$tmp/prs.json" \ | |
| --slurpfile issues "$tmp/issues.json" \ | |
| --slurpfile reviews "$tmp/reviews.json" \ | |
| ' | |
| def bucket: reduce .[] as $d ({}; .[$d] = (.[$d] // 0) + 1); | |
| ($start | strptime("%Y-%m-%d") | mktime) as $s | | |
| ($end | strptime("%Y-%m-%d") | mktime) as $e | | |
| ([range($s; $e + 86400; 86400) | strftime("%Y-%m-%d")]) as $dates | | |
| # Clamp every source to the window (reviews especially can fall outside it) | |
| def clip: map(select(. >= $start and . <= $end)); | |
| ($commits[0] | clip) as $c | | |
| ($prs[0] | clip) as $p | | |
| ($issues[0] | clip) as $i | | |
| ($reviews[0] | clip) as $r | | |
| ($c | bucket) as $cb | ($p | bucket) as $pb | | |
| ($i | bucket) as $ib | ($r | bucket) as $rb | | |
| ($dates | map({ | |
| date: ., | |
| count: (($cb[.] // 0) + ($pb[.] // 0) + ($ib[.] // 0) + ($rb[.] // 0)) | |
| })) as $days | | |
| # GitHub-style intensity levels: quartiles over the nonzero days | |
| ([$days[].count | select(. > 0)] | sort) as $nz | | |
| (if ($nz | length) == 0 then [1, 2, 3] | |
| else [ $nz[(($nz | length) * 0.25 | floor)], | |
| $nz[(($nz | length) * 0.50 | floor)], | |
| $nz[(($nz | length) * 0.75 | floor)] ] | |
| end) as $q | | |
| ($days | map(. + { | |
| level: (if .count == 0 then 0 | |
| elif .count <= $q[0] then 1 | |
| elif .count <= $q[1] then 2 | |
| elif .count <= $q[2] then 3 | |
| else 4 end) | |
| })) as $days | | |
| { | |
| username: $user, | |
| fetched_at: ($e | strftime("%Y-%m-%d")), | |
| range: { start: $start, end: $end }, | |
| total_contributions: ([$days[].count] | add), | |
| counts: { | |
| commits: ($c | length), | |
| pull_requests: ($p | length), | |
| issues: ($i | length), | |
| code_reviews: ($r | length) | |
| }, | |
| weeks: ([range(0; ($days | length); 7)] | map($days[. : . + 7])) | |
| } | |
| ' > "$OUT" | |
| echo "Wrote $OUT" >&2 | |
| jq -r '" \(.total_contributions) contributions | commits \(.counts.commits), PRs \(.counts.pull_requests), issues \(.counts.issues), reviews \(.counts.code_reviews)"' "$OUT" >&2 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment