Remove python 2.5'isms
[gitweb.git] / git-filter-branch.sh
index 0d000ed306caad7be70856c070a362f09439e48d..88fb0f070e5f32c62ae47f90f1f27ffeef836d8f 100755 (executable)
@@ -8,9 +8,9 @@
 # a new branch. You can specify a number of filters to modify the commits,
 # files and trees.
 
-USAGE="git-filter-branch [-d TEMPDIR] [FILTERS] DESTBRANCH [REV-RANGE]"
-. git-sh-setup
+# The following functions will also be available in the commit filter:
 
+functions=$(cat << \EOF
 warn () {
         echo "$*" >&2
 }
@@ -26,6 +26,30 @@ map()
        fi
 }
 
+# if you run 'skip_commit "$@"' in a commit filter, it will print
+# the (mapped) parents, effectively skipping the commit.
+
+skip_commit()
+{
+       shift;
+       while [ -n "$1" ];
+       do
+               shift;
+               map "$1";
+               shift;
+       done;
+}
+
+# if you run 'git_commit_non_empty_tree "$@"' in a commit filter,
+# it will skip commits that leave the tree untouched, commit the other.
+git_commit_non_empty_tree()
+{
+       if test $# = 3 && test "$1" = $(git rev-parse "$3^{tree}"); then
+               map "$3"
+       else
+               git commit-tree "$@"
+       fi
+}
 # override die(): this version puts in an extra line break, so that
 # the progress is still visible
 
@@ -35,30 +59,34 @@ die()
        echo "$*" >&2
        exit 1
 }
+EOF
+)
+
+eval "$functions"
 
 # 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")"
+       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
+                       s/.*/GIT_'$uid'_NAME='\''&'\''; export GIT_'$uid'_NAME/p
 
                        g
                        s/^'$lid' [^<]* <\([^>]*\)> .*$/\1/
                        s/'\''/'\''\'\'\''/g
-                       s/.*/export GIT_'$uid'_EMAIL='\''&'\''/p
+                       s/.*/GIT_'$uid'_EMAIL='\''&'\''; export GIT_'$uid'_EMAIL/p
 
                        g
                        s/^'$lid' [^<]* <[^>]*> \(.*\)$/\1/
                        s/'\''/'\''\'\'\''/g
-                       s/.*/export GIT_'$uid'_DATE='\''&'\''/p
+                       s/.*/GIT_'$uid'_DATE='\''&'\''; export GIT_'$uid'_DATE/p
 
                        q
                }
@@ -66,25 +94,60 @@ set_ident () {
 
        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%%@*}\""
+       echo "case \"\$GIT_${uid}_NAME\" in \"\") GIT_${uid}_NAME=\"\${GIT_${uid}_EMAIL%%@*}\" && export GIT_${uid}_NAME;; esac"
 }
 
+USAGE="[--env-filter <command>] [--tree-filter <command>]
+            [--index-filter <command>] [--parent-filter <command>]
+            [--msg-filter <command>] [--commit-filter <command>]
+            [--tag-name-filter <command>] [--subdirectory-filter <directory>]
+            [--original <namespace>] [-d <directory>] [-f | --force]
+            [<rev-list options>...]"
+
+OPTIONS_SPEC=
+. git-sh-setup
+
+if [ "$(is_bare_repository)" = false ]; then
+       git diff-files --ignore-submodules --quiet &&
+       git diff-index --cached --quiet HEAD -- ||
+       die "Cannot rewrite branch(es) with a dirty working directory."
+fi
+
 tempdir=.git-rewrite
 filter_env=
 filter_tree=
 filter_index=
 filter_parent=
 filter_msg=cat
-filter_commit='git commit-tree "$@"'
+filter_commit=
 filter_tag_name=
 filter_subdir=
-while case "$#" in 0) usage;; esac
+orig_namespace=refs/original/
+force=
+prune_empty=
+remap_to_ancestor=
+while :
 do
        case "$1" in
        --)
                shift
                break
                ;;
+       --force|-f)
+               shift
+               force=t
+               continue
+               ;;
+       --remap-to-ancestor)
+               shift
+               remap_to_ancestor=t
+               continue
+               ;;
+       --prune-empty)
+               shift
+               prune_empty=t
+               continue
+               ;;
        -*)
                ;;
        *)
@@ -118,13 +181,17 @@ do
                filter_msg="$OPTARG"
                ;;
        --commit-filter)
-               filter_commit="$OPTARG"
+               filter_commit="$functions; $OPTARG"
                ;;
        --tag-name-filter)
                filter_tag_name="$OPTARG"
                ;;
        --subdirectory-filter)
                filter_subdir="$OPTARG"
+               remap_to_ancestor=t
+               ;;
+       --original)
+               orig_namespace=$(expr "$OPTARG/" : '\(.*[^/]\)/*$')/
                ;;
        *)
                usage
@@ -132,62 +199,118 @@ do
        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"
+case "$prune_empty,$filter_commit" in
+,)
+       filter_commit='git commit-tree "$@"';;
+t,)
+       filter_commit="$functions;"' git_commit_non_empty_tree "$@"';;
+,*)
+       ;;
+*)
+       die "Cannot set --prune-empty and --commit-filter at the same time"
+esac
 
-test ! -e "$tempdir" || die "$tempdir already exists, please remove it"
+case "$force" in
+t)
+       rm -rf "$tempdir"
+;;
+'')
+       test -d "$tempdir" &&
+               die "$tempdir already exists, please remove it"
+esac
 mkdir -p "$tempdir/t" &&
+tempdir="$(cd "$tempdir"; pwd)" &&
 cd "$tempdir/t" &&
 workdir="$(pwd)" ||
 die ""
 
-case "$GIT_DIR" in
-/*)
+# Remove tempdir on exit
+trap 'cd ../..; rm -rf "$tempdir"' 0
+
+ORIG_GIT_DIR="$GIT_DIR"
+ORIG_GIT_WORK_TREE="$GIT_WORK_TREE"
+ORIG_GIT_INDEX_FILE="$GIT_INDEX_FILE"
+GIT_WORK_TREE=.
+export GIT_DIR GIT_WORK_TREE
+
+# Make sure refs/original is empty
+git for-each-ref > "$tempdir"/backup-refs || exit
+while read sha1 type name
+do
+       case "$force,$name" in
+       ,$orig_namespace*)
+               die "Cannot create a new backup.
+A previous backup already exists in $orig_namespace
+Force overwriting the backup with -f"
        ;;
-*)
-       GIT_DIR="$(pwd)/../../$GIT_DIR"
+       t,$orig_namespace*)
+               git update-ref -d "$name" $sha1
        ;;
-esac
-export GIT_DIR GIT_WORK_TREE=.
+       esac
+done < "$tempdir"/backup-refs
+
+# The refs should be updated if their heads were rewritten
+git rev-parse --no-flags --revs-only --symbolic-full-name \
+       --default HEAD "$@" > "$tempdir"/raw-heads || exit
+sed -e '/^^/d' "$tempdir"/raw-heads >"$tempdir"/heads
 
-export GIT_INDEX_FILE="$(pwd)/../index"
-git read-tree || die "Could not seed the index"
+test -s "$tempdir"/heads ||
+       die "Which ref do you want to rewrite?"
 
-ret=0
+GIT_INDEX_FILE="$(pwd)/../index"
+export GIT_INDEX_FILE
 
 # map old->new commit ids for rewriting parents
 mkdir ../map || die "Could not create map/ directory"
 
+# we need "--" only if there are no path arguments in $@
+nonrevs=$(git rev-parse --no-revs "$@") || exit
+test -z "$nonrevs" && dashdash=-- || dashdash=
+rev_args=$(git rev-parse --revs-only "$@")
+
 case "$filter_subdir" in
 "")
-       git rev-list --reverse --topo-order --default HEAD \
-               --parents "$@"
+       eval set -- "$(git rev-parse --sq --no-revs "$@")"
        ;;
 *)
-       git rev-list --reverse --topo-order --default HEAD \
-               --parents --full-history "$@" -- "$filter_subdir"
-esac > ../revs || die "Could not get the commits"
+       eval set -- "$(git rev-parse --sq --no-revs "$@" $dashdash \
+               "$filter_subdir")"
+       ;;
+esac
+
+git rev-list --reverse --topo-order --default HEAD \
+       --parents --simplify-merges $rev_args "$@" > ../revs ||
+       die "Could not get the commits"
 commits=$(wc -l <../revs | tr -d " ")
 
 test $commits -eq 0 && die "Found nothing to rewrite"
 
-i=0
+# Rewrite the commits
+
+git_filter_branch__commit_count=0
 while read commit parents; do
-       i=$(($i+1))
-       printf "\rRewrite $commit ($i/$commits)"
+       git_filter_branch__commit_count=$(($git_filter_branch__commit_count+1))
+       printf "\rRewrite $commit ($git_filter_branch__commit_count/$commits)"
 
        case "$filter_subdir" in
        "")
                git read-tree -i -m $commit
                ;;
        *)
-               git read-tree -i -m $commit:"$filter_subdir"
+               # The commit may not have the subdirectory at all
+               err=$(git read-tree -i -m $commit:"$filter_subdir" 2>&1) || {
+                       if ! git rev-parse -q --verify $commit:"$filter_subdir"
+                       then
+                               rm -f "$GIT_INDEX_FILE"
+                       else
+                               echo >&2 "$err"
+                               false
+                       fi
+               }
        esac || die "Could not initialize the index"
 
-       export GIT_COMMIT=$commit
+       GIT_COMMIT=$commit
+       export GIT_COMMIT
        git cat-file commit "$commit" >../commit ||
                die "Cannot read commit $commit"
 
@@ -203,14 +326,16 @@ while read commit parents; do
                        die "Could not checkout the index"
                # 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
+               git clean -d -q -f -x
                eval "$filter_tree" < /dev/null ||
                        die "tree filter failed: $filter_tree"
 
-               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
+               (
+                       git diff-index -r --name-only --ignore-submodules $commit &&
+                       git ls-files --others
+               ) > "$tempdir"/tree-state || exit
+               git update-index --add --replace --remove --stdin \
+                       < "$tempdir"/tree-state || exit
        fi
 
        eval "$filter_index" < /dev/null ||
@@ -230,26 +355,83 @@ while read commit parents; do
        sed -e '1,/^$/d' <../commit | \
                eval "$filter_msg" > ../message ||
                        die "msg filter failed: $filter_msg"
-       sh -c "$filter_commit" "git commit-tree" \
-               $(git write-tree) $parentstr < ../message > ../map/$commit
+       @SHELL_PATH@ -c "$filter_commit" "git commit-tree" \
+               $(git write-tree) $parentstr < ../message > ../map/$commit ||
+                       die "could not write rewritten 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
+# If we are filtering for paths, as in the case of a subdirectory
+# filter, it is possible that a specified head is not in the set of
+# rewritten commits, because it was pruned by the revision walker.
+# Ancestor remapping fixes this by mapping these heads to the unique
+# nearest ancestor that survived the pruning.
+
+if test "$remap_to_ancestor" = t
+then
+       while read ref
+       do
+               sha1=$(git rev-parse "$ref"^0)
+               test -f "$workdir"/../map/$sha1 && continue
+               ancestor=$(git rev-list --simplify-merges -1 "$ref" "$@")
+               test "$ancestor" && echo $(map $ancestor) >> "$workdir"/../map/$sha1
+       done < "$tempdir"/heads
+fi
+
+# Finally update the refs
+
+_x40='[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]'
+_x40="$_x40$_x40$_x40$_x40$_x40$_x40$_x40$_x40"
+echo
+while read ref
+do
+       # avoid rewriting a ref twice
+       test -f "$orig_namespace$ref" && continue
+
+       sha1=$(git rev-parse "$ref"^0)
+       rewritten=$(map $sha1)
+
+       test $sha1 = "$rewritten" &&
+               warn "WARNING: Ref '$ref' is unchanged" &&
+               continue
+
+       case "$rewritten" in
+       '')
+               echo "Ref '$ref' was deleted"
+               git update-ref -m "filter-branch: delete" -d "$ref" $sha1 ||
+                       die "Could not delete $ref"
        ;;
-*)
-       git update-ref refs/heads/"$dstbranch" $target_head ||
-               die "Could not update $dstbranch with $target_head"
-       if [ $(wc -l <../map/$src_head) -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
+       $_x40)
+               echo "Ref '$ref' was rewritten"
+               if ! git update-ref -m "filter-branch: rewrite" \
+                                       "$ref" $rewritten $sha1 2>/dev/null; then
+                       if test $(git cat-file -t "$ref") = tag; then
+                               if test -z "$filter_tag_name"; then
+                                       warn "WARNING: You said to rewrite tagged commits, but not the corresponding tag."
+                                       warn "WARNING: Perhaps use '--tag-name-filter cat' to rewrite the tag."
+                               fi
+                       else
+                               die "Could not rewrite $ref"
+                       fi
+               fi
        ;;
-esac
+       *)
+               # NEEDSWORK: possibly add -Werror, making this an error
+               warn "WARNING: '$ref' was rewritten into multiple commits:"
+               warn "$rewritten"
+               warn "WARNING: Ref '$ref' points to the first one now."
+               rewritten=$(echo "$rewritten" | head -n 1)
+               git update-ref -m "filter-branch: rewrite to first" \
+                               "$ref" $rewritten $sha1 ||
+                       die "Could not rewrite $ref"
+       ;;
+       esac
+       git update-ref -m "filter-branch: backup" "$orig_namespace$ref" $sha1 ||
+                exit
+done < "$tempdir"/heads
+
+# TODO: This should possibly go, with the semantics that all positive given
+#       refs are updated, and their original heads stored in refs/original/
+# Filter tags
 
 if [ "$filter_tag_name" ]; then
        git for-each-ref --format='%(objectname) %(objecttype) %(refname)' refs/tags |
@@ -263,20 +445,37 @@ if [ "$filter_tag_name" ]; then
                if [ "$type" = "tag" ]; then
                        # Dereference to a commit
                        sha1t="$sha1"
-                       sha1="$(git rev-parse "$sha1"^{commit} 2>/dev/null)" || continue
+                       sha1="$(git rev-parse -q "$sha1"^{commit})" || continue
                fi
 
                [ -f "../map/$sha1" ] || continue
                new_sha1="$(cat "../map/$sha1")"
-               export GIT_COMMIT="$sha1"
+               GIT_COMMIT="$sha1"
+               export GIT_COMMIT
                new_ref="$(echo "$ref" | eval "$filter_tag_name")" ||
                        die "tag name filter failed: $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"
+                       new_sha1=$( ( printf 'object %s\ntype commit\ntag %s\n' \
+                                               "$new_sha1" "$new_ref"
+                               git cat-file tag "$ref" |
+                               sed -n \
+                                   -e '1,/^$/{
+                                         /^object /d
+                                         /^type /d
+                                         /^tag /d
+                                       }' \
+                                   -e '/^-----BEGIN PGP SIGNATURE-----/q' \
+                                   -e 'p' ) |
+                               git mktag) ||
+                               die "Could not create new tag object for $ref"
+                       if git cat-file tag "$ref" | \
+                          sane_grep '^-----BEGIN PGP SIGNATURE-----' >/dev/null 2>&1
+                       then
+                               warn "gpg signature stripped from tag object $sha1t"
+                       fi
                fi
 
                git update-ref "refs/tags/$new_ref" "$new_sha1" ||
@@ -286,6 +485,24 @@ fi
 
 cd ../..
 rm -rf "$tempdir"
-printf "\nRewritten history saved to the $dstbranch branch\n"
 
-exit $ret
+trap - 0
+
+unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE
+test -z "$ORIG_GIT_DIR" || {
+       GIT_DIR="$ORIG_GIT_DIR" && export GIT_DIR
+}
+test -z "$ORIG_GIT_WORK_TREE" || {
+       GIT_WORK_TREE="$ORIG_GIT_WORK_TREE" &&
+       export GIT_WORK_TREE
+}
+test -z "$ORIG_GIT_INDEX_FILE" || {
+       GIT_INDEX_FILE="$ORIG_GIT_INDEX_FILE" &&
+       export GIT_INDEX_FILE
+}
+
+if [ "$(is_bare_repository)" = false ]; then
+       git read-tree -u -m HEAD || exit
+fi
+
+exit 0