#!/bin/sh # # Rewrite revision history # Copyright (c) Petr Baudis, 2006 # Minimal changes to "port" it to core-git (c) Johannes Schindelin, 2007 # # Lets you rewrite the revision history of the current branch, creating # a new branch. You can specify a number of filters to modify the commits, # files and trees. # The following functions will also be available in the commit filter: setglobal functions = $[cat << ''' warn () { echo "$*" >&2 } map() { # if it was not rewritten, take the original if test -r "$workdir/../map/$1" then cat "$workdir/../map/$1" else echo "$1" 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 die() { echo >&2 echo "$*" >&2 exit 1 } ''' ] eval $functions proc finish_ident { # Ensure non-empty id name. echo "case \"\$GIT_$1_NAME\" in \"\") GIT_$1_NAME=\"\${GIT_$1_EMAIL%%@*}\" && export GIT_$1_NAME;; esac" # And make sure everything is exported. echo "export GIT_$1_NAME" echo "export GIT_$1_EMAIL" echo "export GIT_$1_DATE" } proc set_ident { parse_ident_from_commit author AUTHOR committer COMMITTER finish_ident AUTHOR finish_ident COMMITTER } setglobal USAGE = '"[--env-filter ] [--tree-filter ] [--index-filter ] [--parent-filter ] [--msg-filter ] [--commit-filter ] [--tag-name-filter ] [--subdirectory-filter ] [--original ] [-d ] [-f | --force] [...]'" setglobal OPTIONS_SPEC = '' source git-sh-setup if test $[is_bare_repository] = false { require_clean_work_tree 'rewrite branches' } setglobal tempdir = '.git-rewrite' setglobal filter_env = '' setglobal filter_tree = '' setglobal filter_index = '' setglobal filter_parent = '' setglobal filter_msg = 'cat' setglobal filter_commit = '' setglobal filter_tag_name = '' setglobal filter_subdir = '' setglobal orig_namespace = 'refs/original/' setglobal force = '' setglobal prune_empty = '' setglobal remap_to_ancestor = '' while : { match $1 { with -- shift break with --force|-f shift setglobal force = 't' continue with --remap-to-ancestor # deprecated ($remap_to_ancestor is set now automatically) shift setglobal remap_to_ancestor = 't' continue with --prune-empty shift setglobal prune_empty = 't' continue with -* with * break; } # all switches take one argument setglobal ARG = $1 match "$Argc" { with 1 usage } shift setglobal OPTARG = $1 shift match $ARG { with -d setglobal tempdir = $OPTARG with --env-filter setglobal filter_env = $OPTARG with --tree-filter setglobal filter_tree = $OPTARG with --index-filter setglobal filter_index = $OPTARG with --parent-filter setglobal filter_parent = $OPTARG with --msg-filter setglobal filter_msg = $OPTARG with --commit-filter setglobal filter_commit = ""$functions; $OPTARG"" with --tag-name-filter setglobal filter_tag_name = $OPTARG with --subdirectory-filter setglobal filter_subdir = $OPTARG setglobal remap_to_ancestor = 't' with --original setglobal orig_namespace = "$[expr "$OPTARG/" : '\(.*[^/]\)/*$]/" with * usage } } match "$prune_empty,$filter_commit" { with , setglobal filter_commit = ''git commit-tree "$@"'' with t, setglobal filter_commit = ""$functions;"' git_commit_non_empty_tree "$@""' with ,* with * die "Cannot set --prune-empty and --commit-filter at the same time" } match $force { with t rm -rf $tempdir with '' test -d $tempdir && die "$tempdir already exists, please remove it" } setglobal orig_dir = $[pwd] mkdir -p "$tempdir/t" && setglobal tempdir = $[cd $tempdir; pwd] && cd "$tempdir/t" && setglobal workdir = $[pwd] || die "" # Remove tempdir on exit trap 'cd "$orig_dir"; rm -rf "$tempdir"' 0 setglobal ORIG_GIT_DIR = $GIT_DIR setglobal ORIG_GIT_WORK_TREE = $GIT_WORK_TREE setglobal ORIG_GIT_INDEX_FILE = $GIT_INDEX_FILE setglobal 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 { match "$force,$name" { with ,$orig_namespace* die "Cannot create a new backup. A previous backup already exists in $orig_namespace Force overwriting the backup with -f" with t,$orig_namespace* git update-ref -d $name $sha1 } } < "$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 @Argv > "$tempdir"/raw-heads || exit sed -e '/^^/d' "$tempdir"/raw-heads >"$tempdir"/heads test -s "$tempdir"/heads || die "Which ref do you want to rewrite?" setglobal 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 $@ setglobal nonrevs = $[git rev-parse --no-revs @Argv] || exit if test -z $nonrevs { setglobal dashdash = '--' } else { setglobal dashdash = '' setglobal remap_to_ancestor = 't' } git rev-parse --revs-only @Argv >../parse match $filter_subdir { with "" eval set -- $[git rev-parse --sq --no-revs @Argv] with * eval set -- $[git rev-parse --sq --no-revs @Argv $dashdash \ $filter_subdir] } git rev-list --reverse --topo-order --default HEAD \ --parents --simplify-merges --stdin @Argv <../parse >../revs || die "Could not get the commits" setglobal commits = $[wc -l <../revs | tr -d " ] test $commits -eq 0 && die "Found nothing to rewrite" # Rewrite the commits proc report_progress { if test -n $progress && test $git_filter_branch__commit_count -gt $next_sample_at { setglobal count = $git_filter_branch__commit_count setglobal now = $[date +%s] setglobal elapsed = $shExpr('$now - $start_timestamp') setglobal remaining = $shExpr(' ($commits - $count) * $elapsed / $count ') if test $elapsed -gt 0 { setglobal next_sample_at = $shExpr(' ($elapsed + 1) * $count / $elapsed ') } else { setglobal next_sample_at = $shExpr('$next_sample_at + 1') } setglobal progress = "" ($elapsed seconds passed, remaining $remaining predicted)"" } printf "\rRewrite $commit ($count/$commits)$progress " } setglobal git_filter_branch__commit_count = '0' setglobal progress = '', start_timestamp = '' if date '+%s' !2 >/dev/null | grep -q '^[0-9][0-9]*$' { setglobal next_sample_at = '0' setglobal progress = '"dummy to ensure this is not empty'" setglobal start_timestamp = $[date '+%s] } if test -n $filter_index || test -n $filter_tree || test -n $filter_subdir { setglobal need_index = 't' } else { setglobal need_index = '' } while read commit parents { setglobal git_filter_branch__commit_count = $shExpr('$git_filter_branch__commit_count+1') report_progress match $filter_subdir { with "" if test -n $need_index { env GIT_ALLOW_NULL_SHA1=1 git read-tree -i -m $commit } with * # The commit may not have the subdirectory at all setglobal err = $[env GIT_ALLOW_NULL_SHA1=1 \ git read-tree -i -m $commit:"$filter_subdir" !2 > !1] || do { if ! git rev-parse -q --verify $commit:"$filter_subdir" { rm -f $GIT_INDEX_FILE } else { echo >&2 $err> !2 "$err" false } } } || die "Could not initialize the index" setglobal GIT_COMMIT = $commit export GIT_COMMIT git cat-file commit $commit >../commit || die "Cannot read commit $commit" eval $[set_ident <../commit] || die "setting author/committer failed for commit $commit" eval $filter_env < /dev/null || die "env filter failed: $filter_env" if test $filter_tree { git checkout-index -f -u -a || 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 clean -d -q -f -x eval $filter_tree < /dev/null || die "tree filter failed: $filter_tree" shell { 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 } eval $filter_index < /dev/null || die "index filter failed: $filter_index" setglobal parentstr = '' for parent in [$parents] { for reparent in [$[map $parent]] { match "$parentstr " { with *" -p $reparent "* with * setglobal parentstr = ""$parentstr -p $reparent"" } } } if test $filter_parent { setglobal parentstr = $[echo $parentstr | eval $filter_parent] || die "parent filter failed: $filter_parent" } do { while IFS='' read -r header_line && test -n "$header_line" { # skip header lines... :; } # and output the actual commit message cat } <../commit | eval $filter_msg > ../message || die "msg filter failed: $filter_msg" if test -n $need_index { setglobal tree = $[git write-tree] } else { setglobal tree = $[git rev-parse "$commit^{tree}] } env workdir=$workdir @SHELL_PATH@ -c $filter_commit "git commit-tree" \ $tree $parentstr < ../message > ../map/$commit || die "could not write rewritten commit" } <../revs # 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 { while read ref { setglobal sha1 = $[git rev-parse "$ref"^0] test -f "$workdir"/../map/$sha1 && continue setglobal ancestor = $[git rev-list --simplify-merges -1 $ref @Argv] test $ancestor && echo $[map $ancestor] >> "$workdir"/../map/$sha1 } < "$tempdir"/heads } # Finally update the refs setglobal _x40 = ''[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]'' setglobal _x40 = ""$_x40$_x40$_x40$_x40$_x40$_x40$_x40$_x40"" echo while read ref { # avoid rewriting a ref twice test -f "$orig_namespace$ref" && continue setglobal sha1 = $[git rev-parse "$ref"^0] setglobal rewritten = $[map $sha1] test $sha1 = $rewritten && warn "WARNING: Ref '$ref' is unchanged" && continue match $rewritten { with '' echo "Ref '$ref' was deleted" git update-ref -m "filter-branch: delete" -d $ref $sha1 || die "Could not delete $ref" with $_x40 echo "Ref '$ref' was rewritten" if ! git update-ref -m "filter-branch: rewrite" \ $ref $rewritten $sha1 !2 >/dev/null { if test $[git cat-file -t $ref] = tag { if test -z $filter_tag_name { 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." } } else { die "Could not rewrite $ref" } } with * # 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." setglobal rewritten = $[echo $rewritten | head -n 1] git update-ref -m "filter-branch: rewrite to first" \ $ref $rewritten $sha1 || die "Could not rewrite $ref" } git update-ref -m "filter-branch: backup" "$orig_namespace$ref" $sha1 || exit } < "$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 test $filter_tag_name { git for-each-ref --format='%(objectname) %(objecttype) %(refname)' refs/tags | while read sha1 type ref { setglobal ref = $(ref#refs/tags/) # XXX: Rewrite tagged trees as well? if test $type != "commit" -a $type != "tag" { continue; } if test $type = "tag" { # Dereference to a commit setglobal sha1t = $sha1 setglobal sha1 = $[git rev-parse -q "$sha1"^{commit}] || continue } test -f "../map/$sha1" || continue setglobal new_sha1 = $[cat "../map/$sha1] setglobal GIT_COMMIT = $sha1 export GIT_COMMIT setglobal new_ref = $[echo $ref | eval $filter_tag_name] || die "tag name filter failed: $filter_tag_name" echo "$ref -> $new_ref ($sha1 -> $new_sha1)" if test $type = "tag" { setglobal new_sha1 = $[ shell { 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 { warn "gpg signature stripped from tag object $sha1t" } } git update-ref "refs/tags/$new_ref" $new_sha1 || die "Could not write tag $new_ref" } } cd $orig_dir rm -rf $tempdir trap - 0 unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE test -z $ORIG_GIT_DIR || do { setglobal GIT_DIR = $ORIG_GIT_DIR && export GIT_DIR } test -z $ORIG_GIT_WORK_TREE || do { setglobal GIT_WORK_TREE = $ORIG_GIT_WORK_TREE && export GIT_WORK_TREE } test -z $ORIG_GIT_INDEX_FILE || do { setglobal GIT_INDEX_FILE = $ORIG_GIT_INDEX_FILE && export GIT_INDEX_FILE } if test $[is_bare_repository] = false { git read-tree -u -m HEAD || exit } exit 0