| #!/bin/sh |
| # |
| # Rewrite revision history |
| # Copyright (c) Petr Baudis, 2006 |
| # Minimal changes to "port" it to core-git (c) Johannes Schindelin, 2007 |
| # |
| # Lets you rewrite the revision history of the current branch, creating |
| # a new branch. You can specify a number of filters to modify the commits, |
| # files and trees. |
| |
| set -e |
| |
| USAGE="git-filter-branch [-d TEMPDIR] [FILTERS] DESTBRANCH [REV-RANGE]" |
| . git-sh-setup |
| |
| map() |
| { |
| # if it was not rewritten, take the original |
| test -r "$workdir/../map/$1" || echo "$1" |
| cat "$workdir/../map/$1" |
| } |
| |
| # When piped a commit, output a script to set the ident of either |
| # "author" or "committer |
| |
| set_ident () { |
| lid="$(echo "$1" | tr "A-Z" "a-z")" |
| uid="$(echo "$1" | tr "a-z" "A-Z")" |
| pick_id_script=' |
| /^'$lid' /{ |
| s/'\''/'\''\\'\'\''/g |
| h |
| s/^'$lid' \([^<]*\) <[^>]*> .*$/\1/ |
| s/'\''/'\''\'\'\''/g |
| s/.*/export GIT_'$uid'_NAME='\''&'\''/p |
| |
| g |
| s/^'$lid' [^<]* <\([^>]*\)> .*$/\1/ |
| s/'\''/'\''\'\'\''/g |
| s/.*/export GIT_'$uid'_EMAIL='\''&'\''/p |
| |
| g |
| s/^'$lid' [^<]* <[^>]*> \(.*\)$/\1/ |
| s/'\''/'\''\'\'\''/g |
| s/.*/export GIT_'$uid'_DATE='\''&'\''/p |
| |
| q |
| } |
| ' |
| |
| LANG=C LC_ALL=C sed -ne "$pick_id_script" |
| # Ensure non-empty id name. |
| echo "[ -n \"\$GIT_${uid}_NAME\" ] || export GIT_${uid}_NAME=\"\${GIT_${uid}_EMAIL%%@*}\"" |
| } |
| |
| tempdir=.git-rewrite |
| filter_env= |
| filter_tree= |
| filter_index= |
| filter_parent= |
| filter_msg=cat |
| filter_commit='git commit-tree "$@"' |
| filter_tag_name= |
| filter_subdir= |
| while case "$#" in 0) usage;; esac |
| do |
| case "$1" in |
| --) |
| shift |
| break |
| ;; |
| -*) |
| ;; |
| *) |
| break; |
| esac |
| |
| # all switches take one argument |
| ARG="$1" |
| case "$#" in 1) usage ;; esac |
| shift |
| OPTARG="$1" |
| shift |
| |
| case "$ARG" in |
| -d) |
| tempdir="$OPTARG" |
| ;; |
| --env-filter) |
| filter_env="$OPTARG" |
| ;; |
| --tree-filter) |
| filter_tree="$OPTARG" |
| ;; |
| --index-filter) |
| filter_index="$OPTARG" |
| ;; |
| --parent-filter) |
| filter_parent="$OPTARG" |
| ;; |
| --msg-filter) |
| filter_msg="$OPTARG" |
| ;; |
| --commit-filter) |
| filter_commit="$OPTARG" |
| ;; |
| --tag-name-filter) |
| filter_tag_name="$OPTARG" |
| ;; |
| --subdirectory-filter) |
| filter_subdir="$OPTARG" |
| ;; |
| *) |
| usage |
| ;; |
| esac |
| done |
| |
| dstbranch="$1" |
| shift |
| test -n "$dstbranch" || die "missing branch name" |
| git show-ref "refs/heads/$dstbranch" 2> /dev/null && |
| die "branch $dstbranch already exists" |
| |
| test ! -e "$tempdir" || die "$tempdir already exists, please remove it" |
| mkdir -p "$tempdir/t" |
| cd "$tempdir/t" |
| workdir="$(pwd)" |
| |
| case "$GIT_DIR" in |
| /*) |
| ;; |
| *) |
| GIT_DIR="$(pwd)/../../$GIT_DIR" |
| ;; |
| esac |
| export GIT_DIR GIT_WORK_TREE=. |
| |
| export GIT_INDEX_FILE="$(pwd)/../index" |
| git read-tree # seed the index file |
| |
| ret=0 |
| |
| |
| mkdir ../map # map old->new commit ids for rewriting parents |
| |
| case "$filter_subdir" in |
| "") |
| git rev-list --reverse --topo-order --default HEAD \ |
| --parents "$@" |
| ;; |
| *) |
| git rev-list --reverse --topo-order --default HEAD \ |
| --parents --full-history "$@" -- "$filter_subdir" |
| esac > ../revs |
| commits=$(cat ../revs | wc -l | tr -d " ") |
| |
| test $commits -eq 0 && die "Found nothing to rewrite" |
| |
| i=0 |
| while read commit parents; do |
| i=$(($i+1)) |
| printf "$commit ($i/$commits) " |
| |
| case "$filter_subdir" in |
| "") |
| git read-tree -i -m $commit |
| ;; |
| *) |
| git read-tree -i -m $commit:"$filter_subdir" |
| esac |
| |
| export GIT_COMMIT=$commit |
| git cat-file commit "$commit" >../commit |
| |
| eval "$(set_ident AUTHOR <../commit)" |
| eval "$(set_ident COMMITTER <../commit)" |
| eval "$filter_env" < /dev/null |
| |
| if [ "$filter_tree" ]; then |
| git checkout-index -f -u -a |
| # files that $commit removed are now still in the working tree; |
| # remove them, else they would be added again |
| git ls-files -z --others | xargs -0 rm -f |
| eval "$filter_tree" < /dev/null |
| git diff-index -r $commit | cut -f 2- | tr '\n' '\0' | \ |
| xargs -0 git update-index --add --replace --remove |
| git ls-files -z --others | \ |
| xargs -0 git update-index --add --replace --remove |
| fi |
| |
| eval "$filter_index" < /dev/null |
| |
| parentstr= |
| for parent in $parents; do |
| for reparent in $(map "$parent"); do |
| parentstr="$parentstr -p $reparent" |
| done |
| done |
| if [ "$filter_parent" ]; then |
| parentstr="$(echo "$parentstr" | eval "$filter_parent")" |
| fi |
| |
| sed -e '1,/^$/d' <../commit | \ |
| eval "$filter_msg" | \ |
| sh -c "$filter_commit" "git commit-tree" $(git write-tree) $parentstr | \ |
| tee ../map/$commit |
| done <../revs |
| |
| src_head=$(tail -n 1 ../revs | sed -e 's/ .*//') |
| target_head=$(head -n 1 ../map/$src_head) |
| case "$target_head" in |
| '') |
| echo Nothing rewritten |
| ;; |
| *) |
| git update-ref refs/heads/"$dstbranch" $target_head |
| if [ $(cat ../map/$src_head | wc -l) -gt 1 ]; then |
| echo "WARNING: Your commit filter caused the head commit to expand to several rewritten commits. Only the first such commit was recorded as the current $dstbranch head but you will need to resolve the situation now (probably by manually merging the other commits). These are all the commits:" >&2 |
| sed 's/^/ /' ../map/$src_head >&2 |
| ret=1 |
| fi |
| ;; |
| esac |
| |
| if [ "$filter_tag_name" ]; then |
| git for-each-ref --format='%(objectname) %(objecttype) %(refname)' refs/tags | |
| while read sha1 type ref; do |
| ref="${ref#refs/tags/}" |
| # XXX: Rewrite tagged trees as well? |
| if [ "$type" != "commit" -a "$type" != "tag" ]; then |
| continue; |
| fi |
| |
| if [ "$type" = "tag" ]; then |
| # Dereference to a commit |
| sha1t="$sha1" |
| sha1="$(git rev-parse "$sha1"^{commit} 2>/dev/null)" || continue |
| fi |
| |
| [ -f "../map/$sha1" ] || continue |
| new_sha1="$(cat "../map/$sha1")" |
| export GIT_COMMIT="$sha1" |
| new_ref="$(echo "$ref" | eval "$filter_tag_name")" |
| |
| echo "$ref -> $new_ref ($sha1 -> $new_sha1)" |
| |
| if [ "$type" = "tag" ]; then |
| # Warn that we are not rewriting the tag object itself. |
| warn "unreferencing tag object $sha1t" |
| fi |
| |
| git update-ref "refs/tags/$new_ref" "$new_sha1" |
| done |
| fi |
| |
| cd ../.. |
| rm -rf "$tempdir" |
| echo "Rewritten history saved to the $dstbranch branch" |
| |
| exit $ret |