Skip to content

Instantly share code, notes, and snippets.

@micoli
Created December 31, 2021 10:50
Show Gist options
  • Select an option

  • Save micoli/f6c04e7b8bce1ec23a6ca5f6461bb840 to your computer and use it in GitHub Desktop.

Select an option

Save micoli/f6c04e7b8bce1ec23a6ca5f6461bb840 to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
# git-fixup (https://github.com/keis/git-fixup)
SUBDIRECTORY_OK=yes
. "$(git --exec-path)/git-sh-setup"
# Define a sed program that turns `git diff` output into a stream of filenames
# and sections within those files.
grok_diff='/^--- .*/p ;
s/^@@ -\([0-9]*\),\([0-9]*\).*/\1 \2/p'
# Produce suggestion of commits by finding the sections of files with changes
# staged (U1 to diff is used to give some context for when adding items to
# lists etc) and looking up the previous commits touching those sections.
function fixup_candidates_lines () {
git diff --cached -U1 --no-prefix | sed -n "$grok_diff" | (
file=''
while read offs len; do
if test "$offs" == '---'; then
file="$len"
else
if test "$len" != '0'; then
if test "$file" != '/dev/null'; then
git blame -sl -L "$offs,+$len" $rev_range -- "$file"
fi
fi
fi
done
) | grep -v "^^" | cut -d' ' -f 1 | sed 's/^/L /g'
}
# Produce suggestion of commits by taking the latest commit to each file with
# staged changes
function fixup_candidates_files () {
git diff --cached --name-only | (
while read file; do
git rev-list $rev_range -- $file \
| grep -v -f <(git rev-list -E --grep='^(fixup|squash)' $rev_range -- $file) \
| head -n1
done
) | sed 's/^/F /g'
}
# Pretty print details of a commit
function print_sha () {
local sha=$1
local type=$2
git --no-pager log --date=format:'%Y-%m-%d %H:%M:%S' --format="%H %ad [$type] %s <%ae>" -n 1 "$sha"
}
# Call git commit
function call_commit() {
local target=$1
git commit --$op=$target
}
# Print list of fixup/squash candidates
function print_candidates() {
(
fixup_candidates_lines
fixup_candidates_files
) | sort -uk2 | while read type sha; do
if test "$sha" != ""; then
print_sha "$sha" "$type"
fi
done | sort --key 2 --reverse
}
function fallback_menu() {
(
readarray -t options
IFS=$'\n'
PS3="Which commit should I $op? "
select line in "${options[@]}"; do
if test -z "$line"; then
declare -a 'args=('"$REPLY"')'
case ${args[0]} in
quit|q)
echo "Alright, no action taken." >&2
break
;;
show|s)
idx=$((${args[1]} - 1))
if test $idx -ge 0; then
git show ${options[$idx]%% *} >&2
fi
;;
help|h)
local fmt="%s\n %s\n"
printf $fmt "<n>" "$op the <n>-th commit from the list" >&2
printf $fmt "s[how] <n>" "show the <n>-th commit from the list" >&2
printf $fmt "q[uit]" "abort operation" >&2
printf $fmt "h[elp]" "show this help message" >&2
;;
esac
else
echo $line
break
fi
done < /dev/tty
)
}
op="fixup"
target=
create_commit=$(git config --default=false --type bool fixup.commit)
while test $# -gt 0; do
case "$1" in
-s|--squash)
op="squash"
shift
;;
-c|--commit)
create_commit=true
shift
;;
--no-commit)
create_commit=false
shift
;;
-*)
echo "Invalid option: $1" >&2
exit 1
;;
*)
if ! test -z "$target"; then
echo "Pass only one ref, please." >&2
exit 1
fi
target="$1"
shift
;;
esac
done
if ! test -z "$target"; then
call_commit $target
exit
fi
if git diff --cached --quiet; then
echo 'No staged changes. Use git add -p to add them.' >&2
exit 1
fi
cd_to_toplevel
upstream=`git rev-parse @{upstream} 2>/dev/null`
head=`git rev-parse HEAD 2>/dev/null`
if test -z "$upstream" -o "$upstream" = "$head"; then
rev_range="HEAD"
else
rev_range="@{upstream}..HEAD"
fi
if test "$create_commit" == "true"; then
GITFIXUPMENU=${GITFIXUPMENU:-$(git config --default="" fixup.menu)}
if test -n "$GITFIXUPMENU"; then
menu="eval command $GITFIXUPMENU"
else
menu=fallback_menu
fi
target=$(print_candidates | $menu)
! test -z "$target" && call_commit ${target%% *}
else
print_candidates
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment