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