/ maint / rust-maint-common / for-every-commit
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