|
| 1 | +#!/usr/bin/env bash |
| 2 | +# Syncs this fork with upstream cel-go. |
| 3 | +# Run it locally, or from Actions -> "Sync with upstream" -> Run workflow. |
| 4 | +# |
| 5 | +# sync-upstream.sh # full sync |
| 6 | +# sync-upstream.sh --continue # after resolving conflicts by hand |
| 7 | +# sync-upstream.sh rename [--reverse] # just the rename |
| 8 | +# |
| 9 | +# This fork differs from upstream in exactly one way: every |
| 10 | +# `github.com/google/cel-go` import is rewritten to `github.com/authzed/cel-go`. |
| 11 | +# Merging upstream directly means both sides edited the same import lines, so it |
| 12 | +# conflicts on nearly every file upstream touched -- ~77 conflicts, none real. |
| 13 | +# |
| 14 | +# So the rename is applied last, and never merged: |
| 15 | +# |
| 16 | +# 1. un-rename, making our tree match the upstream commit we last synced from |
| 17 | +# 2. merge upstream, which now sees no changes on our side |
| 18 | +# 3. re-rename |
| 19 | +# 4. build and test |
| 20 | +# |
| 21 | +# Commits, never pushes. The workflow hands the commits to |
| 22 | +# peter-evans/create-pull-request. |
| 23 | +# |
| 24 | +# Merge the resulting PR with a merge commit, NOT a squash. Step 2's merge |
| 25 | +# commit is what moves `git merge-base master upstream/master` forward, and that |
| 26 | +# is what keeps the next sync clean. |
| 27 | +# |
| 28 | +# Env: |
| 29 | +# PREFER_UPSTREAM=0 stop on conflicts instead of taking upstream's version |
| 30 | +# NO_TEST=1 skip `go test ./...` |
| 31 | +# UPSTREAM_REMOTE default: upstream |
| 32 | +# UPSTREAM_URL default: https://github.com/cel-expr/cel-go.git |
| 33 | +# UPSTREAM_BRANCH default: master |
| 34 | +# BRANCH branch to commit on; defaults to a new |
| 35 | +# sync-upstream-<sha> locally, current branch on CI |
| 36 | +set -euo pipefail |
| 37 | + |
| 38 | +cd "$(git rev-parse --show-toplevel)" |
| 39 | + |
| 40 | +UPSTREAM_PATH="github.com/google/cel-go" |
| 41 | +FORK_PATH="github.com/authzed/cel-go" |
| 42 | + |
| 43 | +UPSTREAM_REMOTE="${UPSTREAM_REMOTE:-upstream}" |
| 44 | +UPSTREAM_URL="${UPSTREAM_URL:-https://github.com/cel-expr/cel-go.git}" |
| 45 | +UPSTREAM_BRANCH="${UPSTREAM_BRANCH:-master}" |
| 46 | +PREFER_UPSTREAM="${PREFER_UPSTREAM:-1}" |
| 47 | + |
| 48 | +# Skipped when verifying: these do not build against unmodified upstream |
| 49 | +# either, so a failure there says nothing about the sync. |
| 50 | +SKIP_MODULES="repl codelab tools" |
| 51 | + |
| 52 | +log() { printf '\n\033[1m==> %s\033[0m\n' "$*"; } |
| 53 | +warn() { printf '\033[33m%s\033[0m\n' "$*"; } |
| 54 | +die() { printf '\n\033[31merror: %s\033[0m\n' "$*" >&2; exit 1; } |
| 55 | + |
| 56 | +# The entire fork delta, as one substitution. |
| 57 | +rename() { |
| 58 | + local from="$UPSTREAM_PATH" to="$FORK_PATH" files count |
| 59 | + if [[ "${1:-}" == "--reverse" ]]; then |
| 60 | + from="$FORK_PATH" |
| 61 | + to="$UPSTREAM_PATH" |
| 62 | + fi |
| 63 | + |
| 64 | + # -I skips binary files, and git grep only looks at tracked ones. |
| 65 | + files="$(git grep -I --name-only --fixed-strings -e "$from" -- . || true)" |
| 66 | + if [[ -z "$files" ]]; then |
| 67 | + echo "rename: no occurrences of ${from}; nothing to do" |
| 68 | + return 0 |
| 69 | + fi |
| 70 | + |
| 71 | + count="$(printf '%s\n' "$files" | wc -l | tr -d ' ')" |
| 72 | + printf '%s\n' "$files" | tr '\n' '\0' \ |
| 73 | + | xargs -0 perl -pi -e "s{\\Q${from}\\E}{${to}}g" |
| 74 | + echo "rename: ${from} -> ${to} in ${count} file(s)" |
| 75 | +} |
| 76 | + |
| 77 | +# Take upstream's version, and honor upstream's deletions. Correct for a fork |
| 78 | +# whose only intentional change is the rename, which goes back on afterwards. |
| 79 | +auto_resolve() { |
| 80 | + local unmerged path |
| 81 | + unmerged="$(git diff --name-only --diff-filter=U)" |
| 82 | + [[ -z "$unmerged" ]] && return 0 |
| 83 | + |
| 84 | + warn "Auto-resolving in upstream's favor (PREFER_UPSTREAM=1):" |
| 85 | + while IFS= read -r path; do |
| 86 | + [[ -z "$path" ]] && continue |
| 87 | + if git rev-parse -q --verify ":3:$path" >/dev/null 2>&1; then |
| 88 | + git checkout --theirs -- "$path" |
| 89 | + git add -- "$path" |
| 90 | + echo " took upstream: $path" |
| 91 | + else |
| 92 | + git rm -q --force -- "$path" >/dev/null |
| 93 | + echo " upstream deleted: $path" |
| 94 | + fi |
| 95 | + done <<< "$unmerged" |
| 96 | +} |
| 97 | + |
| 98 | +# Builds every Go module in the repo. The set is discovered, not listed, |
| 99 | +# because it changes as upstream adds and drops modules. |
| 100 | +verify() { |
| 101 | + local m |
| 102 | + for m in . $(git ls-files '*/go.mod' | sed 's|/go.mod$||' | sort -u); do |
| 103 | + case " $SKIP_MODULES " in |
| 104 | + *" $m "*) echo " skipped $m" ; continue ;; |
| 105 | + esac |
| 106 | + ( cd "$m" && go build ./... ) || die "go build failed in module '$m'" |
| 107 | + echo " go build OK $m" |
| 108 | + done |
| 109 | + if [[ "${NO_TEST:-0}" == "1" ]]; then |
| 110 | + echo " go test skipped (NO_TEST=1)" |
| 111 | + else |
| 112 | + go test ./... >/dev/null || die "go test failed in the root module" |
| 113 | + echo " go test OK ." |
| 114 | + fi |
| 115 | +} |
| 116 | + |
| 117 | +finish() { |
| 118 | + log "Step 3/4: re-applying the rename" |
| 119 | + rename |
| 120 | + if git diff --quiet && git diff --cached --quiet; then |
| 121 | + echo "Nothing to re-rename." |
| 122 | + else |
| 123 | + git add -A |
| 124 | + git commit -qm "Re-apply authzed rename" |
| 125 | + fi |
| 126 | + |
| 127 | + log "Step 4/4: verifying" |
| 128 | + verify |
| 129 | + |
| 130 | + # The fork should be upstream plus the rename and nothing else. |
| 131 | + local extra |
| 132 | + extra="$(git diff "$UPSTREAM_REMOTE/$UPSTREAM_BRANCH" -- . ':(exclude).github' \ |
| 133 | + | grep -E '^[+-]' | grep -vE '^(\+\+\+|---)' | grep -cv 'cel-go' || true)" |
| 134 | + echo " non-rename lines vs upstream (excluding .github/): ${extra}" |
| 135 | + |
| 136 | + log "Sync complete on '$(git rev-parse --abbrev-ref HEAD)'. Nothing was pushed." |
| 137 | +} |
| 138 | + |
| 139 | +case "${1:-}" in |
| 140 | +rename) |
| 141 | + shift |
| 142 | + rename "$@" |
| 143 | + exit 0 |
| 144 | + ;; |
| 145 | +--continue) |
| 146 | + if git rev-parse -q --verify MERGE_HEAD >/dev/null; then |
| 147 | + if git diff --name-only --diff-filter=U | grep -q .; then |
| 148 | + die "still unresolved: $(git diff --name-only --diff-filter=U | tr '\n' ' ')" |
| 149 | + fi |
| 150 | + git commit -qm "Merge ${UPSTREAM_REMOTE}/${UPSTREAM_BRANCH} into fork" |
| 151 | + fi |
| 152 | + finish |
| 153 | + exit 0 |
| 154 | + ;; |
| 155 | +"") ;; |
| 156 | +*) die "unknown argument '${1}'" ;; |
| 157 | +esac |
| 158 | + |
| 159 | +if git rev-parse -q --verify MERGE_HEAD >/dev/null; then |
| 160 | + die "a merge is already in progress; run 'git merge --abort' or use --continue" |
| 161 | +fi |
| 162 | +if ! git diff --quiet || ! git diff --cached --quiet; then |
| 163 | + die "working tree is dirty; commit or stash first" |
| 164 | +fi |
| 165 | + |
| 166 | +log "Step 1/4: fetching ${UPSTREAM_REMOTE}/${UPSTREAM_BRANCH}" |
| 167 | +git remote get-url "$UPSTREAM_REMOTE" >/dev/null 2>&1 \ |
| 168 | + || git remote add "$UPSTREAM_REMOTE" "$UPSTREAM_URL" |
| 169 | +git fetch --quiet "$UPSTREAM_REMOTE" "$UPSTREAM_BRANCH" |
| 170 | +UPSTREAM_REF="$UPSTREAM_REMOTE/$UPSTREAM_BRANCH" |
| 171 | +UPSTREAM_SHA="$(git rev-parse --short "$UPSTREAM_REF")" |
| 172 | +echo "upstream is at ${UPSTREAM_SHA}" |
| 173 | + |
| 174 | +if git merge-base --is-ancestor "$UPSTREAM_REF" HEAD; then |
| 175 | + log "Already up to date with ${UPSTREAM_REF}. Nothing to do." |
| 176 | + exit 0 |
| 177 | +fi |
| 178 | + |
| 179 | +# On CI, commit onto the branch that is already checked out. |
| 180 | +# create-pull-request only pushes commits as-is when they sit on the base |
| 181 | +# branch; otherwise it cherry-picks them, and cherry-pick cannot replay the |
| 182 | +# merge commit that step 2 creates. |
| 183 | +if [[ -n "${BRANCH:-}" ]]; then |
| 184 | + git checkout -q -B "$BRANCH" |
| 185 | +elif [[ -z "${GITHUB_ACTIONS:-}" ]]; then |
| 186 | + git checkout -q -B "sync-upstream-${UPSTREAM_SHA}" |
| 187 | +fi |
| 188 | +echo "working on '$(git rev-parse --abbrev-ref HEAD)'" |
| 189 | + |
| 190 | +BASE="$(git merge-base HEAD "$UPSTREAM_REF")" |
| 191 | + |
| 192 | +log "Step 2/4: reverting the rename, then merging" |
| 193 | +rename --reverse |
| 194 | +git add -A |
| 195 | +if git diff --cached --quiet; then |
| 196 | + echo "Nothing to un-rename." |
| 197 | +else |
| 198 | + git commit -qm "Revert authzed rename for upstream sync" |
| 199 | +fi |
| 200 | + |
| 201 | +# If un-renaming did not make our tree identical to the merge base, the fork |
| 202 | +# carries changes of its own, and those files are the only ones that can |
| 203 | +# conflict below. |
| 204 | +DRIFT="$(git diff --name-only "$BASE" HEAD)" |
| 205 | +if [[ -n "$DRIFT" ]]; then |
| 206 | + warn "Fork differs from upstream beyond the rename in $(printf '%s\n' "$DRIFT" | wc -l | tr -d ' ') file(s):" |
| 207 | + printf '%s\n' "$DRIFT" | sed 's/^/ /' |
| 208 | + echo " -> only these files can conflict." |
| 209 | +else |
| 210 | + echo "drift check: clean. Fork is pure upstream + rename; the merge cannot conflict." |
| 211 | +fi |
| 212 | + |
| 213 | +if ! git merge --no-edit "$UPSTREAM_REF"; then |
| 214 | + if [[ "$PREFER_UPSTREAM" == "1" ]]; then |
| 215 | + auto_resolve |
| 216 | + git commit -qm "Merge ${UPSTREAM_REF} into fork" |
| 217 | + else |
| 218 | + warn "Conflicts (real fork divergence, not rename noise):" |
| 219 | + git diff --name-only --diff-filter=U | sed 's/^/ /' |
| 220 | + cat <<'EOF' |
| 221 | +
|
| 222 | +Resolve them, `git add` the files, then run: |
| 223 | + .github/workflows/sync-upstream.sh --continue |
| 224 | +EOF |
| 225 | + exit 1 |
| 226 | + fi |
| 227 | +fi |
| 228 | + |
| 229 | +finish |
0 commit comments