#!/usr/bin/env bash # # Usage: # -common/for-every-commit [--] COMMAND ARGS... # # Runs COMMAND ARGS... for every commit since the baseline # (oldest first). Fails as soon as any such command fails. # # Makes some assumptions: # - Your main branch is called `main` # - Your baseline repository is mentioned in your `Cargo.toml` # - When running under CI, CI_PROJECT_URL is set (as by gitlab) # - When not running under CI, your `origin` remote is sensible # # When run outside CI, will try to restore your branch to where you were. # # Requires TOML.pm, so you may need: # - maint/apt-install libtoml-perl # # *WARNING* this script can be dangerous. It may `git clean` # and `git reset` in order to try to check out differing versions. set -e set -o pipefail BASE_BRANCHES='main' # this include stanza is automatically maintained by update-shell-includes common_dir=$(realpath "$0") common_dir=$(dirname "$common_dir") # shellcheck source=maint/common/bash-utils.sh . "$common_dir"/bash-utils.sh reject_options repo=$( perl -MTOML -we ' use strict; undef $/; my $toml = from_toml(); print $toml->{package}{repository} // die; ' /dev/null 2>&1 ||: git remote rename upstream upstream.tmp >/dev/null 2>&1 ||: git remote add upstream "$repo" ORIGIN=upstream ;; '') # Not running in CI, use a guess at the baseline: # $BASE_BRANCHES at whatever `origin` points to. ORIGIN=origin ;; *) # We are apparently running in a different instance to upstream. # It's not clear what this testing would mean. # Should it use our repo URL? That would be quite exciting. echo >&2 "CI_PROJECT_URL $CI_PROJECT_URL not recognised!" exit 4 ;; esac echo "Baseline repo remote $ORIGIN, at:" git remote get-url "$ORIGIN" tlref () { echo "refs/remotes/$ORIGIN/$tbranch" } x () { echo "+ $*" "$@" } git_checkout_maybe_clean () { x=$1; shift if $x git checkout -q "$1" then : else echo "Checkout failed, trying with git clean and git reset" x git clean -xdff x git reset --hard x git checkout -q "$1" fi } old_branch=$(git symbolic-ref -q HEAD || test $? = 1) restore_old_branch () { case "$old_branch" in refs/heads/*) git_checkout_maybe_clean '' "${old_branch#refs/heads/}" || echo '*** Failed to return to original branch ***' ;; *) echo "*** Was not originally on a branch, HEAD changed! ***" ;; esac } trap 'restore_old_branch' 0 refspecs=() for tbranch in $BASE_BRANCHES; do refspecs+=(+"refs/heads/$tbranch:$(tlref)") done # If you try to unshallow a repo that's already complete, you get # an error! Hence this. Not all versions of git-rev-parse # understand this option; we err on the side of trying the unshallow. if [ "x$(git rev-parse --is-shallow-repository)" != xfalse ]; then # TODO really we want something like the converse of # git fetch --shallow-exclude # but it doesn't seem to exist. git fetch --unshallow "$ORIGIN" "${refspecs[@]}" fi subtrees=$( git log -P --grep '^git-subtree-split:' | sed -n 's/^ git-subtree-split: //p' | sed 's/$/../' ) echo "Excluding subtree inputs: $subtrees" for tbranch in $BASE_BRANCHES; do # shellcheck disable=SC2206 trevlist=($subtrees "$(tlref)..HEAD") tcount=$(git rev-list --count "${trevlist[@]}") printf 'HEAD is %3d commits ahead of %s\n' "$tcount" "$tbranch" if [ "$count" ] && [ "$count" -le "$tcount" ]; then continue; fi count=$tcount branch=$tbranch revlist=("${trevlist[@]}") done echo "Testing every commit not already on $branch" commits=$(git rev-list --reverse "${revlist[@]}") i=0 for commit in $commits; do banner='##############################' printf '|\n%s %d/%d %s\n|\n' "$banner" "$i" "$count" "$banner" git log -n1 "$commit" | sed 's/^/| /' echo '|' git_checkout_maybe_clean x "$commit" if x "$@"; then :; else rc="$?" echo " | ========================= Failed after $i/$count ========================= | Earliest broken commit is " git --no-pager log -n1 --pretty=oneline "$commit" exit "$rc" fi i=$(( i + 1 )) done echo "| $banner ok $banner |" rc=0