Skip to content

Instantly share code, notes, and snippets.

@nicois
Last active January 3, 2022 22:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nicois/e7f90dce7031993afd4677dfb7e84284 to your computer and use it in GitHub Desktop.
Save nicois/e7f90dce7031993afd4677dfb7e84284 to your computer and use it in GitHub Desktop.
#!/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"
@nicois
Copy link
Author

nicois commented Jan 3, 2022

prerequisites

  • you are on a feature branch named something like EVE-123-fix-thing
  • your first commit’s comment is something like 'EVE-123: fix things”

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:

image

what will the script do?

the main way it works is this:

  • all the commits after the initial commit will be squashed (reducing the risk of superfluous conflicts)
  • the branch will be “hard reset” to origin/develop (or another branch, see below)
  • the squashed commit will be cherry-picked onto origin/develop

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment