for-every-commit
1 #!/usr/bin/env bash 2 # 3 # Usage: 4 # -common/for-every-commit [--] COMMAND ARGS... 5 # 6 # Runs COMMAND ARGS... for every commit since the baseline 7 # (oldest first). Fails as soon as any such command fails. 8 # 9 # Makes some assumptions: 10 # - Your main branch is called `main` 11 # - Your baseline repository is mentioned in your `Cargo.toml` 12 # - When running under CI, CI_PROJECT_URL is set (as by gitlab) 13 # - When not running under CI, your `origin` remote is sensible 14 # 15 # When run outside CI, will try to restore your branch to where you were. 16 # 17 # Requires TOML.pm, so you may need: 18 # - maint/apt-install libtoml-perl 19 # 20 # *WARNING* this script can be dangerous. It may `git clean` 21 # and `git reset` in order to try to check out differing versions. 22 23 set -e 24 set -o pipefail 25 26 BASE_BRANCHES='main' 27 28 # this include stanza is automatically maintained by update-shell-includes 29 common_dir=$(realpath "$0") 30 common_dir=$(dirname "$common_dir") 31 # shellcheck source=maint/common/bash-utils.sh 32 . "$common_dir"/bash-utils.sh 33 34 reject_options 35 36 repo=$( 37 perl -MTOML -we ' 38 use strict; 39 undef $/; 40 my $toml = from_toml(<STDIN>); 41 print $toml->{package}{repository} // die; 42 ' <Cargo.toml 43 ) 44 repo_host=$( 45 perl -we ' 46 $ARGV[0] =~ m{^https://[-.0-9a-z]+/} 47 or die "bad repository url"; 48 print "$&\n"; 49 ' "$repo" 50 ) 51 52 echo "Cargo.toml package.repository: $repo" 53 echo "Upstream repository instance: $repo_host" 54 55 # Ideally we would somehow find which repo and branch this was going 56 # to be an MR for. But, in fact, we can't even find out which 57 # repo this repo it was forked from, in a principled way, with env vars. 58 # This approach to finding the upstream will have to do for now. 59 case "$CI_PROJECT_URL" in 60 "$repo_host"*) 61 # `package.repository` in `Cargo.toml` is 62 # on same instance as this CI job. 63 # 64 # Use $BASE_BRANCHES on `package.repository` as the baseline. 65 git remote rm upstream.tmp >/dev/null 2>&1 ||: 66 git remote rename upstream upstream.tmp >/dev/null 2>&1 ||: 67 git remote add upstream "$repo" 68 ORIGIN=upstream 69 ;; 70 '') 71 # Not running in CI, use a guess at the baseline: 72 # $BASE_BRANCHES at whatever `origin` points to. 73 ORIGIN=origin 74 ;; 75 *) 76 # We are apparently running in a different instance to upstream. 77 # It's not clear what this testing would mean. 78 # Should it use our repo URL? That would be quite exciting. 79 echo >&2 "CI_PROJECT_URL $CI_PROJECT_URL not recognised!" 80 exit 4 81 ;; 82 esac 83 84 echo "Baseline repo remote $ORIGIN, at:" 85 git remote get-url "$ORIGIN" 86 87 tlref () { 88 echo "refs/remotes/$ORIGIN/$tbranch" 89 } 90 91 x () { 92 echo "+ $*" 93 "$@" 94 } 95 96 git_checkout_maybe_clean () { 97 x=$1; shift 98 if 99 $x git checkout -q "$1" 100 then : 101 else 102 echo "Checkout failed, trying with git clean and git reset" 103 x git clean -xdff 104 x git reset --hard 105 x git checkout -q "$1" 106 fi 107 } 108 109 old_branch=$(git symbolic-ref -q HEAD || test $? = 1) 110 111 restore_old_branch () { 112 case "$old_branch" in 113 refs/heads/*) 114 git_checkout_maybe_clean '' "${old_branch#refs/heads/}" || 115 echo '*** Failed to return to original branch ***' 116 ;; 117 *) 118 echo "*** Was not originally on a branch, HEAD changed! ***" 119 ;; 120 esac 121 } 122 123 trap 'restore_old_branch' 0 124 125 refspecs=() 126 for tbranch in $BASE_BRANCHES; do 127 refspecs+=(+"refs/heads/$tbranch:$(tlref)") 128 done 129 130 # If you try to unshallow a repo that's already complete, you get 131 # an error! Hence this. Not all versions of git-rev-parse 132 # understand this option; we err on the side of trying the unshallow. 133 if [ "x$(git rev-parse --is-shallow-repository)" != xfalse ]; then 134 # TODO really we want something like the converse of 135 # git fetch --shallow-exclude 136 # but it doesn't seem to exist. 137 git fetch --unshallow "$ORIGIN" "${refspecs[@]}" 138 fi 139 140 for tbranch in $BASE_BRANCHES; do 141 trevlist="$(tlref)..HEAD" 142 tcount=$(git rev-list --count "$trevlist") 143 printf 'HEAD is %3d commits ahead of %s\n' "$tcount" "$tbranch" 144 if [ "$count" ] && [ "$count" -le "$tcount" ]; then continue; fi 145 count=$tcount 146 branch=$tbranch 147 revlist=$trevlist 148 done 149 150 echo "Testing every commit not already on $branch" 151 152 commits=$(git rev-list --reverse "$revlist") 153 154 i=0 155 for commit in $commits; do 156 banner='##############################' 157 printf '|\n%s %d/%d %s\n|\n' "$banner" "$i" "$count" "$banner" 158 git log -n1 "$commit" | sed 's/^/| /' 159 echo '|' 160 161 git_checkout_maybe_clean x "$commit" 162 163 if x "$@"; then :; else 164 rc="$?" 165 echo " 166 | 167 ========================= Failed after $i/$count ========================= 168 | 169 Earliest broken commit is " 170 git --no-pager log -n1 --pretty=oneline "$commit" 171 exit "$rc" 172 fi 173 i=$(( i + 1 )) 174 done 175 176 echo "| 177 $banner ok $banner 178 |" 179 180 rc=0