git-filter-branch.shon commit git-diff: turn on recursion by default (82cb8af)
   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
  11USAGE="git-filter-branch [-d TEMPDIR] [FILTERS] DESTBRANCH [REV-RANGE]"
  12. git-sh-setup
  13
  14warn () {
  15        echo "$*" >&2
  16}
  17
  18map()
  19{
  20        # if it was not rewritten, take the original
  21        if test -r "$workdir/../map/$1"
  22        then
  23                cat "$workdir/../map/$1"
  24        else
  25                echo "$1"
  26        fi
  27}
  28
  29# override die(): this version puts in an extra line break, so that
  30# the progress is still visible
  31
  32die()
  33{
  34        echo >&2
  35        echo "$*" >&2
  36        exit 1
  37}
  38
  39# When piped a commit, output a script to set the ident of either
  40# "author" or "committer
  41
  42set_ident () {
  43        lid="$(echo "$1" | tr "A-Z" "a-z")"
  44        uid="$(echo "$1" | tr "a-z" "A-Z")"
  45        pick_id_script='
  46                /^'$lid' /{
  47                        s/'\''/'\''\\'\'\''/g
  48                        h
  49                        s/^'$lid' \([^<]*\) <[^>]*> .*$/\1/
  50                        s/'\''/'\''\'\'\''/g
  51                        s/.*/export GIT_'$uid'_NAME='\''&'\''/p
  52
  53                        g
  54                        s/^'$lid' [^<]* <\([^>]*\)> .*$/\1/
  55                        s/'\''/'\''\'\'\''/g
  56                        s/.*/export GIT_'$uid'_EMAIL='\''&'\''/p
  57
  58                        g
  59                        s/^'$lid' [^<]* <[^>]*> \(.*\)$/\1/
  60                        s/'\''/'\''\'\'\''/g
  61                        s/.*/export GIT_'$uid'_DATE='\''&'\''/p
  62
  63                        q
  64                }
  65        '
  66
  67        LANG=C LC_ALL=C sed -ne "$pick_id_script"
  68        # Ensure non-empty id name.
  69        echo "[ -n \"\$GIT_${uid}_NAME\" ] || export GIT_${uid}_NAME=\"\${GIT_${uid}_EMAIL%%@*}\""
  70}
  71
  72tempdir=.git-rewrite
  73filter_env=
  74filter_tree=
  75filter_index=
  76filter_parent=
  77filter_msg=cat
  78filter_commit='git commit-tree "$@"'
  79filter_tag_name=
  80filter_subdir=
  81orig_namespace=refs/original/
  82force=
  83while case "$#" in 0) usage;; esac
  84do
  85        case "$1" in
  86        --)
  87                shift
  88                break
  89                ;;
  90        --force|-f)
  91                shift
  92                force=t
  93                continue
  94                ;;
  95        -*)
  96                ;;
  97        *)
  98                break;
  99        esac
 100
 101        # all switches take one argument
 102        ARG="$1"
 103        case "$#" in 1) usage ;; esac
 104        shift
 105        OPTARG="$1"
 106        shift
 107
 108        case "$ARG" in
 109        -d)
 110                tempdir="$OPTARG"
 111                ;;
 112        --env-filter)
 113                filter_env="$OPTARG"
 114                ;;
 115        --tree-filter)
 116                filter_tree="$OPTARG"
 117                ;;
 118        --index-filter)
 119                filter_index="$OPTARG"
 120                ;;
 121        --parent-filter)
 122                filter_parent="$OPTARG"
 123                ;;
 124        --msg-filter)
 125                filter_msg="$OPTARG"
 126                ;;
 127        --commit-filter)
 128                filter_commit="$OPTARG"
 129                ;;
 130        --tag-name-filter)
 131                filter_tag_name="$OPTARG"
 132                ;;
 133        --subdirectory-filter)
 134                filter_subdir="$OPTARG"
 135                ;;
 136        --original)
 137                orig_namespace="$OPTARG"
 138                ;;
 139        *)
 140                usage
 141                ;;
 142        esac
 143done
 144
 145case "$force" in
 146t)
 147        rm -rf "$tempdir"
 148;;
 149'')
 150        test -d "$tempdir" &&
 151                die "$tempdir already exists, please remove it"
 152esac
 153mkdir -p "$tempdir/t" &&
 154tempdir="$(cd "$tempdir"; pwd)" &&
 155cd "$tempdir/t" &&
 156workdir="$(pwd)" ||
 157die ""
 158
 159# Make sure refs/original is empty
 160git for-each-ref > "$tempdir"/backup-refs
 161while read sha1 type name
 162do
 163        case "$force,$name" in
 164        ,$orig_namespace*)
 165                die "Namespace $orig_namespace not empty"
 166        ;;
 167        t,$orig_namespace*)
 168                git update-ref -d "$name" $sha1
 169        ;;
 170        esac
 171done < "$tempdir"/backup-refs
 172
 173case "$GIT_DIR" in
 174/*)
 175        ;;
 176*)
 177        GIT_DIR="$(pwd)/../../$GIT_DIR"
 178        ;;
 179esac
 180export GIT_DIR GIT_WORK_TREE=.
 181
 182# These refs should be updated if their heads were rewritten
 183
 184git rev-parse --revs-only --symbolic "$@" |
 185while read ref
 186do
 187        # normalize ref
 188        case "$ref" in
 189        HEAD)
 190                ref="$(git symbolic-ref "$ref")"
 191        ;;
 192        refs/*)
 193        ;;
 194        *)
 195                ref="$(git for-each-ref --format='%(refname)' |
 196                        grep /"$ref")"
 197        esac
 198
 199        git check-ref-format "$ref" && echo "$ref"
 200done > "$tempdir"/heads
 201
 202test -s "$tempdir"/heads ||
 203        die "Which ref do you want to rewrite?"
 204
 205export GIT_INDEX_FILE="$(pwd)/../index"
 206git read-tree || die "Could not seed the index"
 207
 208ret=0
 209
 210# map old->new commit ids for rewriting parents
 211mkdir ../map || die "Could not create map/ directory"
 212
 213case "$filter_subdir" in
 214"")
 215        git rev-list --reverse --topo-order --default HEAD \
 216                --parents "$@"
 217        ;;
 218*)
 219        git rev-list --reverse --topo-order --default HEAD \
 220                --parents --full-history "$@" -- "$filter_subdir"
 221esac > ../revs || die "Could not get the commits"
 222commits=$(wc -l <../revs | tr -d " ")
 223
 224test $commits -eq 0 && die "Found nothing to rewrite"
 225
 226# Rewrite the commits
 227
 228i=0
 229while read commit parents; do
 230        i=$(($i+1))
 231        printf "\rRewrite $commit ($i/$commits)"
 232
 233        case "$filter_subdir" in
 234        "")
 235                git read-tree -i -m $commit
 236                ;;
 237        *)
 238                git read-tree -i -m $commit:"$filter_subdir"
 239        esac || die "Could not initialize the index"
 240
 241        export GIT_COMMIT=$commit
 242        git cat-file commit "$commit" >../commit ||
 243                die "Cannot read commit $commit"
 244
 245        eval "$(set_ident AUTHOR <../commit)" ||
 246                die "setting author failed for commit $commit"
 247        eval "$(set_ident COMMITTER <../commit)" ||
 248                die "setting committer failed for commit $commit"
 249        eval "$filter_env" < /dev/null ||
 250                die "env filter failed: $filter_env"
 251
 252        if [ "$filter_tree" ]; then
 253                git checkout-index -f -u -a ||
 254                        die "Could not checkout the index"
 255                # files that $commit removed are now still in the working tree;
 256                # remove them, else they would be added again
 257                git ls-files -z --others | xargs -0 rm -f
 258                eval "$filter_tree" < /dev/null ||
 259                        die "tree filter failed: $filter_tree"
 260
 261                git diff-index -r $commit | cut -f 2- | tr '\n' '\0' | \
 262                        xargs -0 git update-index --add --replace --remove
 263                git ls-files -z --others | \
 264                        xargs -0 git update-index --add --replace --remove
 265        fi
 266
 267        eval "$filter_index" < /dev/null ||
 268                die "index filter failed: $filter_index"
 269
 270        parentstr=
 271        for parent in $parents; do
 272                for reparent in $(map "$parent"); do
 273                        parentstr="$parentstr -p $reparent"
 274                done
 275        done
 276        if [ "$filter_parent" ]; then
 277                parentstr="$(echo "$parentstr" | eval "$filter_parent")" ||
 278                                die "parent filter failed: $filter_parent"
 279        fi
 280
 281        sed -e '1,/^$/d' <../commit | \
 282                eval "$filter_msg" > ../message ||
 283                        die "msg filter failed: $filter_msg"
 284        sh -c "$filter_commit" "git commit-tree" \
 285                $(git write-tree) $parentstr < ../message > ../map/$commit
 286done <../revs
 287
 288# In case of a subdirectory filter, it is possible that a specified head
 289# is not in the set of rewritten commits, because it was pruned by the
 290# revision walker.  Fix it by mapping these heads to the next rewritten
 291# ancestor(s), i.e. the boundaries in the set of rewritten commits.
 292
 293# NEEDSWORK: we should sort the unmapped refs topologically first
 294while read ref
 295do
 296        sha1=$(git rev-parse "$ref"^0)
 297        test -f "$workdir"/../map/$sha1 && continue
 298        # Assign the boundarie(s) in the set of rewritten commits
 299        # as the replacement commit(s).
 300        # (This would look a bit nicer if --not --stdin worked.)
 301        for p in $( (cd "$workdir"/../map; ls | sed "s/^/^/") |
 302                git rev-list $ref --boundary --stdin |
 303                sed -n "s/^-//p")
 304        do
 305                map $p >> "$workdir"/../map/$sha1
 306        done
 307done < "$tempdir"/heads
 308
 309# Finally update the refs
 310
 311_x40='[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]'
 312_x40="$_x40$_x40$_x40$_x40$_x40$_x40$_x40$_x40"
 313count=0
 314echo
 315while read ref
 316do
 317        # avoid rewriting a ref twice
 318        test -f "$orig_namespace$ref" && continue
 319
 320        sha1=$(git rev-parse "$ref"^0)
 321        rewritten=$(map $sha1)
 322
 323        test $sha1 = "$rewritten" &&
 324                warn "WARNING: Ref '$ref' is unchanged" &&
 325                continue
 326
 327        case "$rewritten" in
 328        '')
 329                echo "Ref '$ref' was deleted"
 330                git update-ref -m "filter-branch: delete" -d "$ref" $sha1 ||
 331                        die "Could not delete $ref"
 332        ;;
 333        $_x40)
 334                echo "Ref '$ref' was rewritten"
 335                git update-ref -m "filter-branch: rewrite" \
 336                                "$ref" $rewritten $sha1 ||
 337                        die "Could not rewrite $ref"
 338        ;;
 339        *)
 340                # NEEDSWORK: possibly add -Werror, making this an error
 341                warn "WARNING: '$ref' was rewritten into multiple commits:"
 342                warn "$rewritten"
 343                warn "WARNING: Ref '$ref' points to the first one now."
 344                rewritten=$(echo "$rewritten" | head -n 1)
 345                git update-ref -m "filter-branch: rewrite to first" \
 346                                "$ref" $rewritten $sha1 ||
 347                        die "Could not rewrite $ref"
 348        ;;
 349        esac
 350        git update-ref -m "filter-branch: backup" "$orig_namespace$ref" $sha1
 351        count=$(($count+1))
 352done < "$tempdir"/heads
 353
 354# TODO: This should possibly go, with the semantics that all positive given
 355#       refs are updated, and their original heads stored in refs/original/
 356# Filter tags
 357
 358if [ "$filter_tag_name" ]; then
 359        git for-each-ref --format='%(objectname) %(objecttype) %(refname)' refs/tags |
 360        while read sha1 type ref; do
 361                ref="${ref#refs/tags/}"
 362                # XXX: Rewrite tagged trees as well?
 363                if [ "$type" != "commit" -a "$type" != "tag" ]; then
 364                        continue;
 365                fi
 366
 367                if [ "$type" = "tag" ]; then
 368                        # Dereference to a commit
 369                        sha1t="$sha1"
 370                        sha1="$(git rev-parse "$sha1"^{commit} 2>/dev/null)" || continue
 371                fi
 372
 373                [ -f "../map/$sha1" ] || continue
 374                new_sha1="$(cat "../map/$sha1")"
 375                export GIT_COMMIT="$sha1"
 376                new_ref="$(echo "$ref" | eval "$filter_tag_name")" ||
 377                        die "tag name filter failed: $filter_tag_name"
 378
 379                echo "$ref -> $new_ref ($sha1 -> $new_sha1)"
 380
 381                if [ "$type" = "tag" ]; then
 382                        # Warn that we are not rewriting the tag object itself.
 383                        warn "unreferencing tag object $sha1t"
 384                fi
 385
 386                git update-ref "refs/tags/$new_ref" "$new_sha1" ||
 387                        die "Could not write tag $new_ref"
 388        done
 389fi
 390
 391cd ../..
 392rm -rf "$tempdir"
 393echo
 394test $count -gt 0 && echo "These refs were rewritten:"
 395git show-ref | grep ^"$orig_namespace"
 396
 397exit $ret