Skip to content

Instantly share code, notes, and snippets.

@codeinthehole
Created August 8, 2024 11:01
Show Gist options
  • Save codeinthehole/903ed1df376aad09322d3a8bc2fec3cf to your computer and use it in GitHub Desktop.
Save codeinthehole/903ed1df376aad09322d3a8bc2fec3cf to your computer and use it in GitHub Desktop.
Custom version of `git absorb` which autosquashes unstaged changes
#!/usr/bin/env bash
#
# Try and squash unstaged changes into existing branch commits.
#
# This command examines each unstaged file and attempts to create a fix-up
# commit to squash it into its natural parent in the current branch.
#
# - If it's able to do this for all modified files, the fix-up files are
# automatically squashed in.
#
# - If it's unable to do this, fix-up commits are created but not autosquashed
# and the problematic files have their git history printed to stdout.
set -euo pipefail
main() {
# Track how many files couldn't be fixed up.
local num_unfixed_files=0
local delta
changed_filepaths | while read -r filepath;
do
delta=$(analyse_filepath "$filepath")
num_unfixed_files=$((num_unfixed_files + delta))
done
if [[ $num_unfixed_files -eq 0 ]]
then
# All changes have a fix-up commit - let's autosquash!
printf "Autosquashing" >&2
# Specify a no-op editor as we don't want need to be interactive.
GIT_SEQUENCE_EDITOR=true git rebase -i --autosquash `git merge-base head origin/$(git defaultbranch)`
fi
}
# Emit a stream of changed filepaths.
changed_filepaths() {
git status -sb | awk '/^ M/ { print substr($0, 4); }' | sed 's/^"\|"$//g'
}
# Attempt to create a fix-up commit for the passed filepath. Prints 1 if a
# fix-up commit couldn't be created.
analyse_filepath() {
local filepath="$1"
local num_unfixed_files=0
printf "Looking for previous commits for %s - " "$filepath" >&2
# Count commits on this branch that touch the passed filepath.
commit_hashes=$(filepath_commit_hashes "$filepath")
num_commits=$(echo -e "$commit_hashes" | wc -l)
if [ "$num_commits" -eq 1 ]
then
# Only one commit touch the filepath. We assume the unstaged changes
# can be squashed in so we create a fix-up commit.
printf "creating fixup commit\n" >&2
git add "$filepath" >&2
git commit --no-verify --fixup="$commit_hashes" > /dev/null 2>&1
else
# More than one commit touched the filepath. We don't know which of the
# commits to squash the changes into so we show commits to help decide.
printf "found %s commits that touched file\n" "$num_commits" >&2
num_unfixed_files=1
echo "$commit_hashes" | while read -r sha;
do
git show --color --pretty=format:"%h - %s" "$sha" -- "$filepath" >&2
done
fi
echo "$num_unfixed_files"
}
# Emit the commit hashes from this branch that touched the passed filepath.
filepath_commit_hashes() {
git log --format="%H" origin/$(git defaultbranch).. -- "$1"
}
main
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment