|
| 1 | +#!/usr/bin/env bash |
| 2 | +# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors |
| 3 | +# SPDX-License-Identifier: AGPL-3.0-or-later |
| 4 | +# |
| 5 | +# Rewrite the gh-pages history to keep it from growing without bounds. |
| 6 | +# |
| 7 | +# Every deploy commits a full set of rebuilt PDF and ePub files, so the branch |
| 8 | +# grows by tens of MiB per deploy even when the docs barely change. The built |
| 9 | +# output has no value as history, so this script rebuilds the branch as: |
| 10 | +# |
| 11 | +# - one snapshot commit per release day (final vX.Y.Z tags only), holding the |
| 12 | +# tree of the last deploy made before the end of that day (UTC), |
| 13 | +# - every deploy made after the last release, unchanged, |
| 14 | +# - version folders below <frozen-below> (out of support, no more releases) |
| 15 | +# replaced by their current content in every commit, so they are stored once. |
| 16 | +# |
| 17 | +# The tip tree is never modified, so the published site is identical. The |
| 18 | +# rewrite is deterministic: running it again on its own output yields the same |
| 19 | +# commits, which makes it safe to run on every release tag. |
| 20 | +# |
| 21 | +# Usage: squash-gh-pages.sh <branch> <frozen-below> <output-ref> |
| 22 | +# |
| 23 | +# Prints changed=true|false and tip=<sha> for $GITHUB_OUTPUT, and a commit |
| 24 | +# count summary on stderr. |
| 25 | + |
| 26 | +set -euo pipefail |
| 27 | + |
| 28 | +# Group final release tags by UTC day. |
| 29 | +# stdin: "<tag> <unix timestamp>" lines |
| 30 | +# stdout: "<end of day timestamp> <tag>[,<tag>...]" lines, oldest day first |
| 31 | +release_days() { |
| 32 | + grep -E '^v[0-9]+\.[0-9]+\.[0-9]+ ' \ |
| 33 | + | awk '{ print int($2 / 86400) * 86400 + 86399, $1 }' \ |
| 34 | + | LC_ALL=C sort -k1,1n -k2,2 \ |
| 35 | + | awk '$1 == day { tags = tags "," $2; next } |
| 36 | + day { print day, tags } |
| 37 | + { day = $1; tags = $2 } |
| 38 | + END { if (day) print day, tags }' |
| 39 | +} |
| 40 | + |
| 41 | +# Pick the commits to keep. |
| 42 | +# $1: file of "<sha> <committer timestamp>" lines, oldest first |
| 43 | +# $2: file of release_days output |
| 44 | +# stdout: "<sha> <tags>" for a snapshot, "<sha> -" for an unreleased commit |
| 45 | +plan() { |
| 46 | + awk 'NR == FNR { sha[NR] = $1; ts[NR] = $2; n = NR; next } |
| 47 | + { |
| 48 | + last = 0 |
| 49 | + for (i = 1; i <= n; i++) if (ts[i] <= $1) last = i |
| 50 | + if (!last) next |
| 51 | + if (last in tags) tags[last] = tags[last] "," $2 |
| 52 | + else { order[++m] = last; tags[last] = $2 } |
| 53 | + } |
| 54 | + END { |
| 55 | + for (k = 1; k <= m; k++) print sha[order[k]], tags[order[k]] |
| 56 | + for (i = (m ? order[m] + 1 : 1); i <= n; i++) print sha[i], "-" |
| 57 | + }' "$1" "$2" |
| 58 | +} |
| 59 | + |
| 60 | +# ls-tree lines of server/<N> folders below $2 in tree-ish $1 (or the others with $3 = keep) |
| 61 | +version_entries() { |
| 62 | + git ls-tree "$1" 2>/dev/null | awk -F '\t' -v below="$2" -v keep="${3:-}" ' |
| 63 | + { frozen = $2 ~ /^[0-9]+$/ && $2 + 0 < below } |
| 64 | + frozen != (keep == "keep")' |
| 65 | +} |
| 66 | + |
| 67 | +# Tree of commit $1 with every frozen server/<N> folder replaced by its tip content. |
| 68 | +frozen_tree() { |
| 69 | + local server |
| 70 | + server=$( { version_entries "$1:server" "$FROZEN_BELOW" keep; printf '%s' "$FROZEN_ENTRIES"; } \ |
| 71 | + | awk NF | git mktree) |
| 72 | + { |
| 73 | + git ls-tree "$1^{tree}" | awk -F '\t' '$2 != "server"' |
| 74 | + if [ "$server" != "$(git mktree </dev/null)" ]; then |
| 75 | + printf '040000 tree %s\tserver\n' "$server" |
| 76 | + fi |
| 77 | + } | git mktree |
| 78 | +} |
| 79 | + |
| 80 | +# Commit tree $2 on parent $3 (may be empty) with the metadata of commit $1. |
| 81 | +# stdin: commit message |
| 82 | +commit_like() { |
| 83 | + local header author committer author_email committer_email |
| 84 | + header=$(git cat-file commit "$1" | sed '/^$/q') |
| 85 | + # "Name <email> <timestamp> <tz>" |
| 86 | + author=$(sed -n 's/^author //p' <<<"$header") |
| 87 | + committer=$(sed -n 's/^committer //p' <<<"$header") |
| 88 | + author_email=${author##*<} |
| 89 | + committer_email=${committer##*<} |
| 90 | + GIT_AUTHOR_NAME=${author% <*} GIT_AUTHOR_EMAIL=${author_email%%>*} GIT_AUTHOR_DATE=${author##*> } \ |
| 91 | + GIT_COMMITTER_NAME=${committer% <*} GIT_COMMITTER_EMAIL=${committer_email%%>*} GIT_COMMITTER_DATE=${committer##*> } \ |
| 92 | + git commit-tree "$2" ${3:+-p "$3"} |
| 93 | +} |
| 94 | + |
| 95 | +# Rebuild branch $1 with folders below $2 frozen, print the new tip (the branch is not moved). |
| 96 | +rewrite() { |
| 97 | + local branch=$1 tmp parent='' sha tags tree |
| 98 | + FROZEN_BELOW=$2 |
| 99 | + FROZEN_ENTRIES=$(version_entries "$branch:server" "$FROZEN_BELOW") |
| 100 | + tmp=$(mktemp -d) |
| 101 | + |
| 102 | + git log --first-parent --reverse --format='%H %ct' "$branch" >"$tmp/commits" |
| 103 | + git for-each-ref --format='%(refname:short) %(creatordate:unix)' refs/tags \ |
| 104 | + | { release_days || true; } >"$tmp/days" |
| 105 | + |
| 106 | + while read -r sha tags; do |
| 107 | + tree=$(frozen_tree "$sha") |
| 108 | + if [ "$tags" = - ]; then |
| 109 | + parent=$(git cat-file commit "$sha" | sed '1,/^$/d' | commit_like "$sha" "$tree" "$parent") |
| 110 | + else |
| 111 | + parent=$(printf 'chore: documentation snapshot for %s\n\nSquashed gh-pages history up to this release.\n' \ |
| 112 | + "${tags//,/, }" | commit_like "$sha" "$tree" "$parent") |
| 113 | + fi |
| 114 | + done < <(plan "$tmp/commits" "$tmp/days") |
| 115 | + |
| 116 | + rm -rf "$tmp" |
| 117 | + echo "$parent" |
| 118 | +} |
| 119 | + |
| 120 | +main() { |
| 121 | + if [ $# -ne 3 ]; then |
| 122 | + echo "Usage: $0 <branch> <frozen-below> <output-ref>" >&2 |
| 123 | + exit 1 |
| 124 | + fi |
| 125 | + local branch=$1 tip |
| 126 | + tip=$(rewrite "$branch" "$2") |
| 127 | + |
| 128 | + if [ "$(git rev-parse "$tip^{tree}")" != "$(git rev-parse "$branch^{tree}")" ]; then |
| 129 | + echo "Rewritten tip tree differs from the current branch, aborting" >&2 |
| 130 | + exit 1 |
| 131 | + fi |
| 132 | + git update-ref "$3" "$tip" |
| 133 | + |
| 134 | + echo "$branch: $(git rev-list --count --first-parent "$branch") commits -> $(git rev-list --count "$tip") commits" >&2 |
| 135 | + if [ "$tip" = "$(git rev-parse "$branch")" ]; then echo changed=false; else echo changed=true; fi |
| 136 | + echo "tip=$tip" |
| 137 | +} |
| 138 | + |
| 139 | +if [ "${BASH_SOURCE[0]}" = "$0" ]; then |
| 140 | + main "$@" |
| 141 | +fi |
0 commit comments