git-filter-branch.shon commit Merge branch 'master' of git://git.kernel.org/pub/scm/gitk/gitk (941fd1c)
   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] [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
 173export GIT_DIR GIT_WORK_TREE=.
 174
 175# These refs should be updated if their heads were rewritten
 176
 177git rev-parse --revs-only --symbolic "$@" |
 178while read ref
 179do
 180        # normalize ref
 181        case "$ref" in
 182        HEAD)
 183                ref="$(git symbolic-ref "$ref")"
 184        ;;
 185        refs/*)
 186        ;;
 187        *)
 188                ref="$(git for-each-ref --format='%(refname)' |
 189                        grep /"$ref")"
 190        esac
 191
 192        git check-ref-format "$ref" && echo "$ref"
 193done > "$tempdir"/heads
 194
 195test -s "$tempdir"/heads ||
 196        die "Which ref do you want to rewrite?"
 197
 198export GIT_INDEX_FILE="$(pwd)/../index"
 199git read-tree || die "Could not seed the index"
 200
 201ret=0
 202
 203# map old->new commit ids for rewriting parents
 204mkdir ../map || die "Could not create map/ directory"
 205
 206case "$filter_subdir" in
 207"")
 208        git rev-list --reverse --topo-order --default HEAD \
 209                --parents "$@"
 210        ;;
 211*)
 212        git rev-list --reverse --topo-order --default HEAD \
 213                --parents --full-history "$@" -- "$filter_subdir"
 214esac > ../revs || die "Could not get the commits"
 215commits=$(wc -l <../revs | tr -d " ")
 216
 217test $commits -eq 0 && die "Found nothing to rewrite"
 218
 219# Rewrite the commits
 220
 221i=0
 222while read commit parents; do
 223        i=$(($i+1))
 224        printf "\rRewrite $commit ($i/$commits)"
 225
 226        case "$filter_subdir" in
 227        "")
 228                git read-tree -i -m $commit
 229                ;;
 230        *)
 231                git read-tree -i -m $commit:"$filter_subdir"
 232        esac || die "Could not initialize the index"
 233
 234        export GIT_COMMIT=$commit
 235        git cat-file commit "$commit" >../commit ||
 236                die "Cannot read commit $commit"
 237
 238        eval "$(set_ident AUTHOR <../commit)" ||
 239                die "setting author failed for commit $commit"
 240        eval "$(set_ident COMMITTER <../commit)" ||
 241                die "setting committer failed for commit $commit"
 242        eval "$filter_env" < /dev/null ||
 243                die "env filter failed: $filter_env"
 244
 245        if [ "$filter_tree" ]; then
 246                git checkout-index -f -u -a ||
 247                        die "Could not checkout the index"
 248                # files that $commit removed are now still in the working tree;
 249                # remove them, else they would be added again
 250                git ls-files -z --others | xargs -0 rm -f
 251                eval "$filter_tree" < /dev/null ||
 252                        die "tree filter failed: $filter_tree"
 253
 254                git diff-index -r $commit | cut -f 2- | tr '\n' '\0' | \
 255                        xargs -0 git update-index --add --replace --remove
 256                git ls-files -z --others | \
 257                        xargs -0 git update-index --add --replace --remove
 258        fi
 259
 260        eval "$filter_index" < /dev/null ||
 261                die "index filter failed: $filter_index"
 262
 263        parentstr=
 264        for parent in $parents; do
 265                for reparent in $(map "$parent"); do
 266                        parentstr="$parentstr -p $reparent"
 267                done
 268        done
 269        if [ "$filter_parent" ]; then
 270                parentstr="$(echo "$parentstr" | eval "$filter_parent")" ||
 271                                die "parent filter failed: $filter_parent"
 272        fi
 273
 274        sed -e '1,/^$/d' <../commit | \
 275                eval "$filter_msg" > ../message ||
 276                        die "msg filter failed: $filter_msg"
 277        sh -c "$filter_commit" "git commit-tree" \
 278                $(git write-tree) $parentstr < ../message > ../map/$commit
 279done <../revs
 280
 281# In case of a subdirectory filter, it is possible that a specified head
 282# is not in the set of rewritten commits, because it was pruned by the
 283# revision walker.  Fix it by mapping these heads to the next rewritten
 284# ancestor(s), i.e. the boundaries in the set of rewritten commits.
 285
 286# NEEDSWORK: we should sort the unmapped refs topologically first
 287while read ref
 288do
 289        sha1=$(git rev-parse "$ref"^0)
 290        test -f "$workdir"/../map/$sha1 && continue
 291        # Assign the boundarie(s) in the set of rewritten commits
 292        # as the replacement commit(s).
 293        # (This would look a bit nicer if --not --stdin worked.)
 294        for p in $( (cd "$workdir"/../map; ls | sed "s/^/^/") |
 295                git rev-list $ref --boundary --stdin |
 296                sed -n "s/^-//p")
 297        do
 298                map $p >> "$workdir"/../map/$sha1
 299        done
 300done < "$tempdir"/heads
 301
 302# Finally update the refs
 303
 304_x40='[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]'
 305_x40="$_x40$_x40$_x40$_x40$_x40$_x40$_x40$_x40"
 306count=0
 307echo
 308while read ref
 309do
 310        # avoid rewriting a ref twice
 311        test -f "$orig_namespace$ref" && continue
 312
 313        sha1=$(git rev-parse "$ref"^0)
 314        rewritten=$(map $sha1)
 315
 316        test $sha1 = "$rewritten" &&
 317                warn "WARNING: Ref '$ref' is unchanged" &&
 318                continue
 319
 320        case "$rewritten" in
 321        '')
 322                echo "Ref '$ref' was deleted"
 323                git update-ref -m "filter-branch: delete" -d "$ref" $sha1 ||
 324                        die "Could not delete $ref"
 325        ;;
 326        $_x40)
 327                echo "Ref '$ref' was rewritten"
 328                git update-ref -m "filter-branch: rewrite" \
 329                                "$ref" $rewritten $sha1 ||
 330                        die "Could not rewrite $ref"
 331        ;;
 332        *)
 333                # NEEDSWORK: possibly add -Werror, making this an error
 334                warn "WARNING: '$ref' was rewritten into multiple commits:"
 335                warn "$rewritten"
 336                warn "WARNING: Ref '$ref' points to the first one now."
 337                rewritten=$(echo "$rewritten" | head -n 1)
 338                git update-ref -m "filter-branch: rewrite to first" \
 339                                "$ref" $rewritten $sha1 ||
 340                        die "Could not rewrite $ref"
 341        ;;
 342        esac
 343        git update-ref -m "filter-branch: backup" "$orig_namespace$ref" $sha1
 344        count=$(($count+1))
 345done < "$tempdir"/heads
 346
 347# TODO: This should possibly go, with the semantics that all positive given
 348#       refs are updated, and their original heads stored in refs/original/
 349# Filter tags
 350
 351if [ "$filter_tag_name" ]; then
 352        git for-each-ref --format='%(objectname) %(objecttype) %(refname)' refs/tags |
 353        while read sha1 type ref; do
 354                ref="${ref#refs/tags/}"
 355                # XXX: Rewrite tagged trees as well?
 356                if [ "$type" != "commit" -a "$type" != "tag" ]; then
 357                        continue;
 358                fi
 359
 360                if [ "$type" = "tag" ]; then
 361                        # Dereference to a commit
 362                        sha1t="$sha1"
 363                        sha1="$(git rev-parse "$sha1"^{commit} 2>/dev/null)" || continue
 364                fi
 365
 366                [ -f "../map/$sha1" ] || continue
 367                new_sha1="$(cat "../map/$sha1")"
 368                export GIT_COMMIT="$sha1"
 369                new_ref="$(echo "$ref" | eval "$filter_tag_name")" ||
 370                        die "tag name filter failed: $filter_tag_name"
 371
 372                echo "$ref -> $new_ref ($sha1 -> $new_sha1)"
 373
 374                if [ "$type" = "tag" ]; then
 375                        # Warn that we are not rewriting the tag object itself.
 376                        warn "unreferencing tag object $sha1t"
 377                fi
 378
 379                git update-ref "refs/tags/$new_ref" "$new_sha1" ||
 380                        die "Could not write tag $new_ref"
 381        done
 382fi
 383
 384cd ../..
 385rm -rf "$tempdir"
 386echo
 387test $count -gt 0 && echo "These refs were rewritten:"
 388git show-ref | grep ^"$orig_namespace"
 389
 390exit $ret