git-filter-branch.shon commit Merge branch 'maint' (e2b1acc)
   1#!/bin/sh
   2#
   3# Rewrite revision history
   4# Copyright (c) Petr Baudis, 2006
   5# Minimal changes to "port" it to core-git (c) Johannes Schindelin, 2007
   6#
   7# Lets you rewrite the revision history of the current branch, creating
   8# a new branch. You can specify a number of filters to modify the commits,
   9# files and trees.
  10
  11set -e
  12
  13USAGE="git-filter-branch [-d TEMPDIR] [FILTERS] DESTBRANCH [REV-RANGE]"
  14. git-sh-setup
  15
  16map()
  17{
  18        # if it was not rewritten, take the original
  19        test -r "$workdir/../map/$1" || echo "$1"
  20        cat "$workdir/../map/$1"
  21}
  22
  23# When piped a commit, output a script to set the ident of either
  24# "author" or "committer
  25
  26set_ident () {
  27        lid="$(echo "$1" | tr "A-Z" "a-z")"
  28        uid="$(echo "$1" | tr "a-z" "A-Z")"
  29        pick_id_script='
  30                /^'$lid' /{
  31                        s/'\''/'\''\\'\'\''/g
  32                        h
  33                        s/^'$lid' \([^<]*\) <[^>]*> .*$/\1/
  34                        s/'\''/'\''\'\'\''/g
  35                        s/.*/export GIT_'$uid'_NAME='\''&'\''/p
  36
  37                        g
  38                        s/^'$lid' [^<]* <\([^>]*\)> .*$/\1/
  39                        s/'\''/'\''\'\'\''/g
  40                        s/.*/export GIT_'$uid'_EMAIL='\''&'\''/p
  41
  42                        g
  43                        s/^'$lid' [^<]* <[^>]*> \(.*\)$/\1/
  44                        s/'\''/'\''\'\'\''/g
  45                        s/.*/export GIT_'$uid'_DATE='\''&'\''/p
  46
  47                        q
  48                }
  49        '
  50
  51        LANG=C LC_ALL=C sed -ne "$pick_id_script"
  52        # Ensure non-empty id name.
  53        echo "[ -n \"\$GIT_${uid}_NAME\" ] || export GIT_${uid}_NAME=\"\${GIT_${uid}_EMAIL%%@*}\""
  54}
  55
  56tempdir=.git-rewrite
  57filter_env=
  58filter_tree=
  59filter_index=
  60filter_parent=
  61filter_msg=cat
  62filter_commit='git commit-tree "$@"'
  63filter_tag_name=
  64filter_subdir=
  65while case "$#" in 0) usage;; esac
  66do
  67        case "$1" in
  68        --)
  69                shift
  70                break
  71                ;;
  72        -*)
  73                ;;
  74        *)
  75                break;
  76        esac
  77
  78        # all switches take one argument
  79        ARG="$1"
  80        case "$#" in 1) usage ;; esac
  81        shift
  82        OPTARG="$1"
  83        shift
  84
  85        case "$ARG" in
  86        -d)
  87                tempdir="$OPTARG"
  88                ;;
  89        --env-filter)
  90                filter_env="$OPTARG"
  91                ;;
  92        --tree-filter)
  93                filter_tree="$OPTARG"
  94                ;;
  95        --index-filter)
  96                filter_index="$OPTARG"
  97                ;;
  98        --parent-filter)
  99                filter_parent="$OPTARG"
 100                ;;
 101        --msg-filter)
 102                filter_msg="$OPTARG"
 103                ;;
 104        --commit-filter)
 105                filter_commit="$OPTARG"
 106                ;;
 107        --tag-name-filter)
 108                filter_tag_name="$OPTARG"
 109                ;;
 110        --subdirectory-filter)
 111                filter_subdir="$OPTARG"
 112                ;;
 113        *)
 114                usage
 115                ;;
 116        esac
 117done
 118
 119dstbranch="$1"
 120shift
 121test -n "$dstbranch" || die "missing branch name"
 122git show-ref "refs/heads/$dstbranch" 2> /dev/null &&
 123        die "branch $dstbranch already exists"
 124
 125test ! -e "$tempdir" || die "$tempdir already exists, please remove it"
 126mkdir -p "$tempdir/t"
 127cd "$tempdir/t"
 128workdir="$(pwd)"
 129
 130case "$GIT_DIR" in
 131/*)
 132        ;;
 133*)
 134        GIT_DIR="$(pwd)/../../$GIT_DIR"
 135        ;;
 136esac
 137export GIT_DIR GIT_WORK_TREE=.
 138
 139export GIT_INDEX_FILE="$(pwd)/../index"
 140git read-tree # seed the index file
 141
 142ret=0
 143
 144
 145mkdir ../map # map old->new commit ids for rewriting parents
 146
 147case "$filter_subdir" in
 148"")
 149        git rev-list --reverse --topo-order --default HEAD \
 150                --parents "$@"
 151        ;;
 152*)
 153        git rev-list --reverse --topo-order --default HEAD \
 154                --parents --full-history "$@" -- "$filter_subdir"
 155esac > ../revs
 156commits=$(cat ../revs | wc -l | tr -d " ")
 157
 158test $commits -eq 0 && die "Found nothing to rewrite"
 159
 160i=0
 161while read commit parents; do
 162        i=$(($i+1))
 163        printf "$commit ($i/$commits) "
 164
 165        case "$filter_subdir" in
 166        "")
 167                git read-tree -i -m $commit
 168                ;;
 169        *)
 170                git read-tree -i -m $commit:"$filter_subdir"
 171        esac
 172
 173        export GIT_COMMIT=$commit
 174        git cat-file commit "$commit" >../commit
 175
 176        eval "$(set_ident AUTHOR <../commit)"
 177        eval "$(set_ident COMMITTER <../commit)"
 178        eval "$filter_env" < /dev/null
 179
 180        if [ "$filter_tree" ]; then
 181                git checkout-index -f -u -a
 182                # files that $commit removed are now still in the working tree;
 183                # remove them, else they would be added again
 184                git ls-files -z --others | xargs -0 rm -f
 185                eval "$filter_tree" < /dev/null
 186                git diff-index -r $commit | cut -f 2- | tr '\n' '\0' | \
 187                        xargs -0 git update-index --add --replace --remove
 188                git ls-files -z --others | \
 189                        xargs -0 git update-index --add --replace --remove
 190        fi
 191
 192        eval "$filter_index" < /dev/null
 193
 194        parentstr=
 195        for parent in $parents; do
 196                for reparent in $(map "$parent"); do
 197                        parentstr="$parentstr -p $reparent"
 198                done
 199        done
 200        if [ "$filter_parent" ]; then
 201                parentstr="$(echo "$parentstr" | eval "$filter_parent")"
 202        fi
 203
 204        sed -e '1,/^$/d' <../commit | \
 205                eval "$filter_msg" | \
 206                sh -c "$filter_commit" "git commit-tree" $(git write-tree) $parentstr | \
 207                tee ../map/$commit
 208done <../revs
 209
 210src_head=$(tail -n 1 ../revs | sed -e 's/ .*//')
 211target_head=$(head -n 1 ../map/$src_head)
 212case "$target_head" in
 213'')
 214        echo Nothing rewritten
 215        ;;
 216*)
 217        git update-ref refs/heads/"$dstbranch" $target_head
 218        if [ $(cat ../map/$src_head | wc -l) -gt 1 ]; then
 219                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
 220                sed 's/^/       /' ../map/$src_head >&2
 221                ret=1
 222        fi
 223        ;;
 224esac
 225
 226if [ "$filter_tag_name" ]; then
 227        git for-each-ref --format='%(objectname) %(objecttype) %(refname)' refs/tags |
 228        while read sha1 type ref; do
 229                ref="${ref#refs/tags/}"
 230                # XXX: Rewrite tagged trees as well?
 231                if [ "$type" != "commit" -a "$type" != "tag" ]; then
 232                        continue;
 233                fi
 234
 235                if [ "$type" = "tag" ]; then
 236                        # Dereference to a commit
 237                        sha1t="$sha1"
 238                        sha1="$(git rev-parse "$sha1"^{commit} 2>/dev/null)" || continue
 239                fi
 240
 241                [ -f "../map/$sha1" ] || continue
 242                new_sha1="$(cat "../map/$sha1")"
 243                export GIT_COMMIT="$sha1"
 244                new_ref="$(echo "$ref" | eval "$filter_tag_name")"
 245
 246                echo "$ref -> $new_ref ($sha1 -> $new_sha1)"
 247
 248                if [ "$type" = "tag" ]; then
 249                        # Warn that we are not rewriting the tag object itself.
 250                        warn "unreferencing tag object $sha1t"
 251                fi
 252
 253                git update-ref "refs/tags/$new_ref" "$new_sha1"
 254        done
 255fi
 256
 257cd ../..
 258rm -rf "$tempdir"
 259echo "Rewritten history saved to the $dstbranch branch"
 260
 261exit $ret