Last active
January 3, 2022 22:42
-
-
Save nicois/e7f90dce7031993afd4677dfb7e84284 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
set -euo pipefail | |
# Use this script if the branch you intend to merge into | |
# has changed underneath you. | |
# OMIT_PRECOMMIT=Y will suppress the second commit | |
# $1 is optional, and is an alternative commit to rebase off, | |
# instead of the default origin/develop (or origin/main or | |
# origin/master if origin/develop doesn't exist) | |
if [[ $(git describe --always --dirty) == *dirty ]] ; then | |
echo "Your branch is dirty. Commit your changes first." | |
exit 1 | |
fi | |
# Make sure we are in the git project root | |
cd "$(git rev-parse --show-toplevel 2>/dev/null)" | |
# Prefix commands with $CHRONIC if it's there, to make the output | |
# less noisy unless there are failures | |
CHRONIC=$(which chronic) || : # it's OK if this doesn't exist | |
CURRENT_BRANCH="$(git branch --show-current)" | |
TRACKING_BRANCH=$(git rev-parse --abbrev-ref "${CURRENT_BRANCH}@{u}") | |
ORIGIN="${TRACKING_BRANCH///*}" | |
if [[ -n "${1:-}" ]] ; then | |
REBASE_OFF="$1" | |
else | |
# Select the branch will intend to rebase off. Usually origin/develop, if it exists. | |
# If you want to rebase off a different commit, simply pass it in as an argument | |
# on the commandline. e.g. rebase origin/release-1.23 | |
for candidate in develop main master ; do | |
if git rev-parse --verify "${ORIGIN}/${candidate}" >/dev/null 2>/dev/null ; then | |
REBASE_OFF="$candidate" | |
break | |
fi | |
done | |
# A sanity-check that the ref is defined | |
[[ -n ${REBASE_OFF:-} ]] | |
fi | |
# Ensure the branch starts with XXX-### (e.g. SRE-123) | |
# This is used to keep track of where this branch's commits | |
# diverge from origin/develop | |
if [[ "$CURRENT_BRANCH" =~ ^[[:alpha:]]+\-[[:digit:]]+ ]] ; then | |
BRANCH_PREFIX="$BASH_REMATCH" | |
else | |
echo "Your branch needs to start with WWW-NNN: " | |
echo "where W is a letter and N is a digit." | |
exit 1 | |
fi | |
ORIGINAL_DIRECTORY="$(pwd -P)" | |
# Uncomment this if you will always be online when running this, and | |
# want the script to verify you've got the latest commit on your current branch. | |
# As long as you use the recommended --force-with-lease push variant, this is not | |
# required. | |
# $CHRONIC git fetch "${ORIGIN}" "${CURRENT_BRANCH}" # sync with the git server, if necessary | |
# if ! $CHRONIC git pull --ff-only ; then | |
# echo "Your branch is not up to date. Aborting." | |
# exit 1 | |
# fi | |
PRE_COMMIT_STRING=": pre-commit linting" | |
# This is the commit which has the commit message we will want to use | |
MESSAGE_COMMIT=$(git rev-parse "HEAD^{/^${BRANCH_PREFIX}}" 2>/dev/null) # work out what the commit was before we diverged | |
MESSAGE="$(git show -s --format=%B ${MESSAGE_COMMIT})" | |
# If there is a pre-commit preceding the other stuff, prefer this as the initial commit of the branch | |
FIRST_COMMIT=$(git rev-parse "HEAD^{/^${BRANCH_PREFIX}${PRE_COMMIT_STRING}}" 2>/dev/null) || FIRST_COMMIT="${MESSAGE_COMMIT}" | |
# Make sure there are no prior commits which match this same expression | |
if git rev-parse "${FIRST_COMMIT}^^{/^${BRANCH_PREFIX}}" >/dev/null 2>/dev/null ; then | |
echo "There are commits prior to ${FIRST_COMMIT} which also start with ${BRANCH_PREFIX} so I don't know which one is to be treated as the first. Aborting." | |
exit 1 | |
fi | |
# This is simply the commit before the first commit | |
PRIOR_COMMIT="$(git rev-parse ${FIRST_COMMIT}^)" | |
CURRENT_COMMIT=$(git rev-parse HEAD) # remember the current commit | |
NEW_BASE_COMMIT=$(git rev-parse origin/${REBASE_OFF}) | |
if [[ "$FIRST_COMMIT" == "$CURRENT_COMMIT" && "$PRIOR_COMMIT" == "$NEW_BASE_COMMIT" ]] ; then | |
echo 'This is already up to date; nothing to squash or rebase!' | |
exit 0 | |
fi | |
echo "Rebasing (${CURRENT_COMMIT:0:10}) on ${CURRENT_BRANCH} off ${REBASE_OFF}" | |
# echo "Commit message: ${MESSAGE}" | |
function on_error() { | |
local STATUS_CODE=$? | |
cd "$ORIGINAL_DIRECTORY" | |
if [[ $STATUS_CODE -gt 0 ]] ; then | |
echo "Restoring to how things were, as something went wrong." | |
set +e | |
git rebase --abort 2>/dev/null # in case a rebase was in progress | |
git checkout "$CURRENT_BRANCH" 2>/dev/null | |
git reset --hard "$CURRENT_COMMIT" 2>/dev/null | |
if [[ -n $EXTRA_ERROR_MESSAGE ]] ; then | |
echo "${EXTRA_ERROR_MESSAGE}" | |
fi | |
fi | |
exit $STATUS_CODE | |
} | |
trap on_error SIGINT ERR | |
# Perform an interactive rebase to squash all commits, preserving the message | |
if [[ "$OSTYPE" == "darwin"* ]]; then | |
# mac sed needs an argument to -i, yuck | |
EDITOR="sed -i '' '2,/^$/s/^pick\b/s/'" $CHRONIC git rebase -i "${PRIOR_COMMIT}" | |
else | |
EDITOR="sed -i '2,/^$/s/^pick\b/s/'" $CHRONIC git rebase -i "${PRIOR_COMMIT}" | |
fi | |
$CHRONIC git commit --amend -m "${MESSAGE}" | |
SINGLE_COMMIT="$(git rev-parse HEAD)" | |
echo "Squashed the commits on this branch since ${PRIOR_COMMIT:0:10}, making a single commit ${SINGLE_COMMIT:0:10}" | |
$CHRONIC git reset --hard "${NEW_BASE_COMMIT}" | |
echo "Cherry picking our squashed commit onto ${REBASE_OFF}" | |
EXTRA_ERROR_MESSAGE="If you want to resolve this manually, run this: | |
git reset --hard ${NEW_BASE_COMMIT} | |
git cherry-pick ${SINGLE_COMMIT}" | |
$CHRONIC git cherry-pick "${SINGLE_COMMIT}" 2>/dev/null | |
NEW_COMMIT="$(git rev-parse HEAD)" | |
unset EXTRA_ERROR_MESSAGE | |
# If this repo doesn't use pre-commit, bail here | |
if [[ ! -e .pre-commit-config.yaml || -n ${OMIT_PRECOMMIT:-} ]] ; then | |
echo "Rebased successfully. Run the following to push:" | |
echo " git push --force-with-lease" | |
exit 0 | |
fi | |
# Now for bonus points, can we generate a two-commit version which first does the pre-commit autofixes | |
# then bas a second commit with the non-pre-commit autofixes? | |
# If we fail here, revert to using NEW_COMMIT. | |
function on_precommit_error() { | |
local STATUS_CODE=$? | |
cd "$ORIGINAL_DIRECTORY" | |
if [[ $STATUS_CODE -gt 0 ]] ; then | |
set +e | |
git rebase --abort 2>/dev/null # in case a rebase was in progress | |
$CHRONIC git checkout "$CURRENT_BRANCH" | |
$CHRONIC git reset --hard "$NEW_COMMIT" | |
fi | |
echo "Run the following to push:" | |
echo " git push --force-with-lease" | |
exit 0 | |
} | |
trap on_precommit_error SIGINT EXIT ERR | |
git reset --hard "${NEW_BASE_COMMIT}" | |
# We want to raise (and then trap) a nonzero exit code, as this means pre-commit did not do anything, so stop | |
if $CHRONIC pre-commit run --from-ref "${NEW_BASE_COMMIT}" --to-ref "${NEW_COMMIT}" 2>/dev/null ; then | |
echo "No files required pre-commit autofixes before your changes." | |
exit 1 | |
fi | |
echo Running pre-commit a second time to ensure the files are now stable | |
$CHRONIC git add -u :/ | |
$CHRONIC pre-commit run --from-ref "${NEW_BASE_COMMIT}" --to-ref "${NEW_COMMIT}" | |
$CHRONIC git add -u :/ | |
$CHRONIC git commit -m "${BRANCH_PREFIX}${PRE_COMMIT_STRING}" | |
PRECOMMIT_COMMIT="$(git rev-parse HEAD)" | |
$CHRONIC git reset --hard "${NEW_COMMIT}" | |
$CHRONIC git rebase -Xtheirs "${PRECOMMIT_COMMIT}" | |
echo "Successfully created two commits" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
prerequisites
e.g here you can see the EVE-7136 branch has a commit whose comment starts with EVE-7136, followed by a number of WIP commits:
what will the script do?
the main way it works is this: