Skip to content

Instantly share code, notes, and snippets.

@0cjs
Created September 14, 2017 13:58
Show Gist options
  • Save 0cjs/7c4ca55dadf3d55a1b4cdb147a6982bc to your computer and use it in GitHub Desktop.
Save 0cjs/7c4ca55dadf3d55a1b4cdb147a6982bc to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
set -o pipefail
. "$(git --exec-path)/git-sh-setup"
#
# Also see "$(git --exec-path)/git-filter-branch" for, e.g., a list of
# functions available in the commit filter.
# set_reflog_action
contains() {
local match="$1"; shift
local el
for el; do [[ $el == $match ]] && return 0; done
return 1
}
default_author() {
local a_tree=$(git cat-file -p @ | sed -n -e 's/tree //p')
local a_commit=$(git commit-tree -m '' $a_tree </dev/null)
git cat-file -p $a_commit \
| sed -n -e 's/ [0-9]* +[0-9]*$//' -e 's/author //p'
}
branches_with() {
[[ -n $1 || -z $2 ]] || die "INTERNAL ERROR: branches: bad args"
git branch --list --format='%(refname)' --contains "$1"
}
############################################################
# Main
reviewer="$(default_author)"
branch=
dry_run=false
force=false
while true; do case "$1" in
-b|--branch) shift; branch="$1"; shift;;
-f|--force) shift; force=true;;
-n|--dry-run) shift; dry_run=true;;
-r|--reviewer) shift; reviewer="$1"; shift;;
*) break;;
esac; done
# Canonicalize ref name (HEAD if not specified)
ref="$(git rev-parse --symbolic-full-name --short ${branch:-@} 2>/dev/null)"
[[ -z $ref ]] && die "Unknown ref: $branch"
# Create list of individual commits (in abbreviated hash format)
commits=()
for i in "$@"; do
if [[ $i =~ \.\. ]]; then
commits+=($(git log --format=%h "$i"))
else
commits+=($(git log --format=%h -1 "$i"))
fi
done
#for i in "${commits[@]}"; do echo commit: $i; done
# Ensure all commits are on the given branch
for i in "${commits[@]}"; do
# XXX For some reason, `git-branch` says that `refs/heads/foo` doesn't
# contain a commit when `foo` contains it. We hack by removing the
# `refs/heads` prefix, but there must be a better/more reliable way....
[[ -n $(git branch --contains "$i" "${ref#refs/heads/}") ]] \
|| die "Branch $ref doesn't contain commit $i"
done
# Here it would be nice to figure out the earliest commit so we didn't
# have to rewrite the entire branch back to the beginning. It would also
# reduce the amount of output generated by progress mesages, which can
# be considerable. (There's no way to disable the progress messages, and
# if they're longer than a terminal line there are many, many lines of
# output due to `\n` going back to beginning of line instead of message.)
echo "Marking these commits on $ref as"
echo " Reviewed-by: $reviewer"
for i in "${commits[@]}"; do
git log -n 1 --format='%h %ae %s' "$i" | sed -e 's/@[^ ]*/@/'
done
$dry_run && exit 1
$force || {
read -p 'OK? [y/N]' ok
[[ $ok =~ ^[Yy] ]] || die "Aborted"
}
echo
trailer_config="-c trailer.where=after \
-c trailer.ifmissing=add -c trailer.ifexists=addIfDifferent \
" # User may override trailer.separators (default `:`) if he likes.
# To avoid halting due to the existence of a `refs/original` entry
# from a previous `git filter-branch` we use `--force` to wipe it out
# (which wipes out its reflog, too). We compensate by ensuring a
# reflog is created for the branch we're filtering.
#
echo XXX rewriting "'$ref'"
commits="${commits[@]}" # convert to space separated due to quoting below
git -c core.logAllRefUpdates=always filter-branch --force \
--msg-filter "
for id in $commits; do
[[ \$GIT_COMMIT =~ ^\$id ]] && {
git $trailer_config interpret-trailers \
--trailer 'Reviewed-by: $reviewer'
exit
}
done
cat
" "$ref"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment