Instantly share code, notes, and snippets.
Last active
August 29, 2015 14:04
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save larryv/b251a63672ecba1afb3f to your computer and use it in GitHub Desktop.
This file contains hidden or 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
#!/usr/bin/env zsh | |
emulate -R zsh # Reset (most) options to defaults. | |
setopt EXTENDED_GLOB | |
##### APPETIZERS (or: preliminary setup) ##### | |
# | |
# Adapted from git's "git-sh-setup". | |
function die { die_with_status 1 "$@"; } | |
function die_with_status { | |
local _status=$1 | |
shift | |
printf >&2 '%s\n' "$*" | |
exit $_status | |
} | |
function require_clean_work_tree { | |
git rev-parse --verify HEAD > /dev/null || exit 1 | |
git update-index -q --ignore-submodules --refresh | |
git diff-files --quiet --ignore-submodules || | |
local UNSTAGED=yes | |
git diff-index --cached --quiet --ignore-submodules HEAD -- || | |
local UNCOMMITTED=yes | |
case $UNSTAGED,$UNCOMMITTED in | |
yes,) | |
printf >&2 "Cannot $1: You have unstaged changes.\n" ;; | |
,yes) | |
printf >&2 "Cannot $1: Your index contains uncommitted changes.\n" ;; | |
yes,yes) | |
printf >&2 "Cannot $1: You have unstaged changes. " | |
printf >&2 "Additionally, your index contains uncommitted changes.\n" | |
;; | |
esac | |
if [[ -n $UNSTAGED || -n $UNCOMMITTED ]] | |
then | |
[[ -n $2 ]] && printf >&2 "%s\n" $2 | |
exit 1 | |
fi | |
} | |
function parse_ident_from_commit { | |
# TODO: Worry about encodings and junk. | |
# TODO: Worry about single quotes in the input. | |
typeset -A fields | |
fields=("$@") | |
(( ${#fields} == 0 )) && return | |
while read field data && [[ -n $field ]] | |
do | |
[[ -n ${upper::=$fields[$field]} ]] || continue | |
name=${data% <*} | |
email=${${data##* <}%> *} | |
date=${data##*> } | |
#printf >&2 'name %s email %s date %s\n' $name $email $date | |
printf "GIT_%s_NAME='%s'\n" $upper $name | |
printf "GIT_%s_EMAIL='%s'\n" $upper $email | |
printf "GIT_%s_DATE='%s'\n" $upper $date | |
done | |
} | |
# Normalize environment a bit. | |
unset cdpath # Make sure "cd" works normally. | |
unset IFS # Make sure field splitting works normally. | |
# Make sure we're in a git repository. | |
GIT_DIR="$(git rev-parse --git-dir)" || exit | |
[[ -n $GIT_DIR ]] && GIT_DIR="$(cd -q $GIT_DIR && pwd)" || | |
die 'Unable to determine absolute path of git directory.' | |
: ${GIT_OBJECT_DIRECTORY=$GIT_DIR/objects} | |
##### MAIN COURSE (or: main logic) ##### | |
# | |
# Adapted from git's "git-filter-branch". | |
function warn { | |
printf >&2 "%s\n" "$*" | |
} | |
function finish_ident { | |
# Use default value if id name is missing. | |
printf ": \${GIT_$1_NAME:=\${GIT_$1_EMAIL%%%%@*}}\n" | |
printf "export GIT_$1_NAME GIT_$1_EMAIL GIT_$1_DATE\n" | |
} | |
function set_ident { | |
parse_ident_from_commit author AUTHOR committer COMMITTER | |
finish_ident AUTHOR | |
finish_ident COMMITTER | |
} | |
# Make sure the repo is clean. | |
if [[ "$(git rev-parse --is-bare-repository)" == false ]] | |
then | |
require_clean_work_tree 'rewrite branches' | |
fi | |
# Parameters. | |
tempdir=.git-rewrite | |
filter_tag_name=cat | |
orig_namespace=refs/original/ | |
force= | |
remap_to_ancestor= | |
# Custom author/committer-rewriting logic. | |
typeset -A authors | |
while read author eq name_email | |
do | |
name=${name_email% <*} | |
email=${${name_email##* <}:0:-1} | |
authors[$author]="$name $email" | |
done | |
filter_env=' | |
auth=$authors[$GIT_AUTHOR_NAME] | |
if [[ -n $auth ]] | |
then | |
name=${auth% *} | |
email=${auth##* } | |
export GIT_AUTHOR_NAME=$name GIT_AUTHOR_EMAIL=$email \ | |
GIT_COMMITTER_NAME=$name GIT_COMMITTER_EMAIL=$email | |
fi | |
' | |
# TODO: Support more command-line arguments. For now, edit parameters | |
# directly. | |
while : | |
do | |
case $1 in | |
--force|-f) | |
shift | |
force=t | |
continue | |
;; | |
*) | |
break | |
;; | |
esac | |
done | |
# Check for leftovers from aborted git-filter-branch operation. | |
case $force in | |
t) | |
rm -fR $tempdir | |
;; | |
'') | |
[[ -d $tempdir ]] && die "$tempdir already exists, please remove it." | |
;; | |
esac | |
# Set up working directory. | |
orig_dir="$(pwd)" | |
mkdir -p $tempdir/t && | |
tempdir="$(cd $tempdir && pwd)" && | |
cd $tempdir/t && | |
workdir="$(pwd)" || | |
die '' | |
trap 'cd $orig_dir && rm -fR $tempdir' 0 | |
# Save the environment. | |
ORIG_GIT_DIR=$GIT_DIR | |
ORIG_GIT_WORK_TREE=$GIT_WORK_TREE | |
ORIG_GIT_INDEX_FILE=$GIT_INDEX_FILE | |
GIT_WORK_TREE=. | |
export GIT_DIR GIT_WORK_TREE | |
# Make sure orig_namespace is empty. | |
git for-each-ref | | |
while read sha1 type name | |
do | |
case $force,$name in | |
,$orig_namespace*) | |
die "Cannot create a new backup. | |
A previous backup already exists in $orig_namespace | |
Force overwriting the backup with -f" | |
;; | |
t,$orig_namespace*) | |
git update-ref -d $name $sha1 | |
;; | |
esac | |
done || exit | |
# Update refs if their heads were rewritten. | |
git rev-parse --no-flags --revs-only --symbolic-full-name \ | |
--default HEAD "$@" | sed '/^^/d' > $tempdir/heads | |
[[ -s $tempdir/heads ]] || die 'Which ref do you want to rewrite?' | |
export GIT_INDEX_FILE="$(pwd)/../index" | |
# Map old->new commit IDs for rewriting parents. | |
typeset -A map | |
nonrevs="$(git rev-parse --no-revs "$@")" || exit | |
[[ -n $nonrevs ]] && remap_to_ancestor=t | |
# Stash rev arguments and save non-rev arguments as positional parameters. | |
parse="$(git rev-parse --revs-only "$@")" | |
eval set -- "$(git rev-parse --sq --no-revs "$@")" | |
# Enumerate the commits to be rewritten. | |
git rev-list --reverse --topo-order --default HEAD \ | |
--parents --simplify-merges --stdin "$@" <<< $parse > ../revs || | |
die 'Could not get the commits.' | |
commits="$(wc -l < ../revs | tr -d ' ')" | |
(( commits == 0 )) && die 'Found nothing to rewrite.' | |
# SHOW TIME | |
(( git_filter_branch__commit_count = 0 )) | |
while read commit parents | |
do | |
(( git_filter_branch__commit_count += 1 )) | |
printf "\rRewrite $commit ($git_filter_branch__commit_count/$commits)" | |
GIT_ALLOW_NULL_SHA1=1 git read-tree -i -m $commit || | |
die 'Could not initialize the index.' | |
export GIT_COMMIT=$commit | |
commit_data="$(git cat-file commit $commit)" || | |
die 'Cannot read commit $commit.' | |
eval "$(set_ident <<< $commit_data)" || | |
die "Setting author/committer failed for commit $commit" | |
eval "$filter_env" < /dev/null || | |
die "env filter failed: $filter_env" | |
parentstr=() | |
for parent in $parents; do for reparent in ${map[$parent]:-$parent} | |
do | |
[[ -z $parentstr[(re)$reparent] ]] && | |
parentstr=($parentstr -p $reparent) | |
done; done | |
message=${commit_data#* | |
} | |
map[$commit]="$(git commit-tree "$(git write-tree)" "$parentstr[@]" | |
<<< $message)" || | |
die 'Could not write rewritten commit.' | |
done < ../revs | |
# TODO: Support path filtering. | |
printf "\n" | |
while read ref | |
do | |
[[ -f $orig_namespace$ref ]] && continue | |
sha1="$(git rev-parse "$ref^0")" | |
rewritten=${map[$sha1]:-$sha1} | |
[[ $sha1 == $rewritten ]] && | |
warn "WARNING: Ref '$ref' is unchanged." && continue | |
case $rewritten in | |
'') | |
printf "Ref '$ref' was deleted.\n" | |
git update-ref -m 'rewrite-authors: delete' -d $ref $sha1 || | |
die "Could not delete $ref." | |
;; | |
[0-9a-f](#c40)) | |
printf "Ref '$ref' was rewritten.\n" | |
if ! git update-ref -m 'rewrite-authors: rewrite' \ | |
$ref $rewritten $sha1 2> /dev/null | |
then | |
if [[ "$(git cat-file -t $ref)" == tag ]] | |
then | |
[[ -z $filter_tag_name ]] && | |
warn "WARNING: You said to rewrite tagged commits, but not the corresponding tag." && | |
warn "WARNING: Perhaps use '--tag-name-filter cat' to rewrite the tag." | |
else | |
die "Could not rewrite $ref." | |
fi | |
fi | |
;; | |
*) | |
warn "WARNING: '$ref' was rewritten into multiple commits:" | |
warn $rewritten | |
warn "WARNING: Ref '$ref' points to the first one now." | |
rewritten=${rewritten%% | |
*} | |
git update-ref -m 'rewrite-authors: rewrite to first' \ | |
$ref $rewritten $sha1 || | |
die "Could not rewrite $ref." | |
;; | |
esac | |
git update-ref -m 'rewrite-authors: backup' $orig_namespace$ref $sha1 || | |
exit | |
done < $tempdir/heads | |
# TODO: Support tag rewriting. | |
##### DESSERT (or: cleanup) ##### | |
# | |
# Adapted from git's "git-filter-branch". | |
cd $orig_dir | |
rm -fR $tempdir | |
trap - 0 | |
unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE | |
[[ -n $ORIG_GIT_DIR ]] && export GIT_DIR=$ORIG_GIT_DIR | |
[[ -n $ORIG_GIT_WORK_TREE ]] && export GIT_WORK_TREE=$ORIG_GIT_WORK_TREE | |
[[ -n $ORIG_GIT_INDEX_FILE ]] && export GIT_INDEX_FILE=$ORIG_GIT_INDEX_FILE | |
if [[ "$(git rev-parse --is-bare-repository)" == false ]] | |
then | |
git read-tree -u -m HEAD || exit | |
fi | |
exit 0 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment