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 11# The following functions will also be available in the commit filter: 12 13functions=$(cat << \EOF 14EMPTY_TREE=$(git hash-object -t tree /dev/null) 15 16warn () { 17 echo "$*" >&2 18} 19 20map() 21{ 22 # if it was not rewritten, take the original 23 if test -r "$workdir/../map/$1" 24 then 25 cat "$workdir/../map/$1" 26 else 27 echo "$1" 28 fi 29} 30 31# if you run 'skip_commit "$@"' in a commit filter, it will print 32# the (mapped) parents, effectively skipping the commit. 33 34skip_commit() 35{ 36 shift; 37 while [ -n "$1" ]; 38 do 39 shift; 40 map "$1"; 41 shift; 42 done; 43} 44 45# if you run 'git_commit_non_empty_tree "$@"' in a commit filter, 46# it will skip commits that leave the tree untouched, commit the other. 47git_commit_non_empty_tree() 48{ 49 if test $# = 3 && test "$1" = $(git rev-parse "$3^{tree}"); then 50 map "$3" 51 elif test $# = 1 && test "$1" = $EMPTY_TREE; then 52 : 53 else 54 git commit-tree "$@" 55 fi 56} 57# override die(): this version puts in an extra line break, so that 58# the progress is still visible 59 60die() 61{ 62 echo >&2 63 echo "$*" >&2 64 exit 1 65} 66EOF 67) 68 69eval "$functions" 70 71finish_ident() { 72 # Ensure non-empty id name. 73 echo "case \"\$GIT_$1_NAME\" in \"\") GIT_$1_NAME=\"\${GIT_$1_EMAIL%%@*}\" && export GIT_$1_NAME;; esac" 74 # And make sure everything is exported. 75 echo "export GIT_$1_NAME" 76 echo "export GIT_$1_EMAIL" 77 echo "export GIT_$1_DATE" 78} 79 80set_ident () { 81 parse_ident_from_commit author AUTHOR committer COMMITTER 82 finish_ident AUTHOR 83 finish_ident COMMITTER 84} 85 86if test -z "$FILTER_BRANCH_SQUELCH_WARNING$GIT_TEST_DISALLOW_ABBREVIATED_OPTIONS" 87then 88 cat <<EOF 89WARNING: git-filter-branch has a glut of gotchas generating mangled history 90 rewrites. Hit Ctrl-C before proceeding to abort, then use an 91 alternative filtering tool such as 'git filter-repo' 92 (https://github.com/newren/git-filter-repo/) instead. See the 93 filter-branch manual page for more details; to squelch this warning, 94 set FILTER_BRANCH_SQUELCH_WARNING=1. 95EOF 96 sleep 10 97 printf "Proceeding with filter-branch...\n\n" 98fi 99 100USAGE="[--setup <command>] [--subdirectory-filter <directory>] [--env-filter <command>] 101 [--tree-filter <command>] [--index-filter <command>] 102 [--parent-filter <command>] [--msg-filter <command>] 103 [--commit-filter <command>] [--tag-name-filter <command>] 104 [--original <namespace>] 105 [-d <directory>] [-f | --force] [--state-branch <branch>] 106 [--] [<rev-list options>...]" 107 108OPTIONS_SPEC= 109. git-sh-setup 110 111if [ "$(is_bare_repository)" = false ]; then 112 require_clean_work_tree 'rewrite branches' 113fi 114 115tempdir=.git-rewrite 116filter_setup= 117filter_env= 118filter_tree= 119filter_index= 120filter_parent= 121filter_msg=cat 122filter_commit= 123filter_tag_name= 124filter_subdir= 125state_branch= 126orig_namespace=refs/original/ 127force= 128prune_empty= 129remap_to_ancestor= 130while : 131do 132 case "$1" in 133 --) 134 shift 135 break 136 ;; 137 --force|-f) 138 shift 139 force=t 140 continue 141 ;; 142 --remap-to-ancestor) 143 # deprecated ($remap_to_ancestor is set now automatically) 144 shift 145 remap_to_ancestor=t 146 continue 147 ;; 148 --prune-empty) 149 shift 150 prune_empty=t 151 continue 152 ;; 153 -*) 154 ;; 155 *) 156 break; 157 esac 158 159 # all switches take one argument 160 ARG="$1" 161 case "$#" in 1) usage ;; esac 162 shift 163 OPTARG="$1" 164 shift 165 166 case "$ARG" in 167 -d) 168 tempdir="$OPTARG" 169 ;; 170 --setup) 171 filter_setup="$OPTARG" 172 ;; 173 --subdirectory-filter) 174 filter_subdir="$OPTARG" 175 remap_to_ancestor=t 176 ;; 177 --env-filter) 178 filter_env="$OPTARG" 179 ;; 180 --tree-filter) 181 filter_tree="$OPTARG" 182 ;; 183 --index-filter) 184 filter_index="$OPTARG" 185 ;; 186 --parent-filter) 187 filter_parent="$OPTARG" 188 ;; 189 --msg-filter) 190 filter_msg="$OPTARG" 191 ;; 192 --commit-filter) 193 filter_commit="$functions; $OPTARG" 194 ;; 195 --tag-name-filter) 196 filter_tag_name="$OPTARG" 197 ;; 198 --original) 199 orig_namespace=$(expr "$OPTARG/" : '\(.*[^/]\)/*$')/ 200 ;; 201 --state-branch) 202 state_branch="$OPTARG" 203 ;; 204 *) 205 usage 206 ;; 207 esac 208done 209 210case "$prune_empty,$filter_commit" in 211,) 212 filter_commit='git commit-tree "$@"';; 213t,) 214 filter_commit="$functions;"' git_commit_non_empty_tree "$@"';; 215,*) 216 ;; 217*) 218 die "Cannot set --prune-empty and --commit-filter at the same time" 219esac 220 221case "$force" in 222t) 223 rm -rf "$tempdir" 224;; 225'') 226 test -d "$tempdir" && 227 die "$tempdir already exists, please remove it" 228esac 229orig_dir=$(pwd) 230mkdir -p "$tempdir/t" && 231tempdir="$(cd "$tempdir"; pwd)" && 232cd "$tempdir/t" && 233workdir="$(pwd)" || 234die "" 235 236# Remove tempdir on exit 237trap 'cd "$orig_dir"; rm -rf "$tempdir"' 0 238 239ORIG_GIT_DIR="$GIT_DIR" 240ORIG_GIT_WORK_TREE="$GIT_WORK_TREE" 241ORIG_GIT_INDEX_FILE="$GIT_INDEX_FILE" 242ORIG_GIT_AUTHOR_NAME="$GIT_AUTHOR_NAME" 243ORIG_GIT_AUTHOR_EMAIL="$GIT_AUTHOR_EMAIL" 244ORIG_GIT_AUTHOR_DATE="$GIT_AUTHOR_DATE" 245ORIG_GIT_COMMITTER_NAME="$GIT_COMMITTER_NAME" 246ORIG_GIT_COMMITTER_EMAIL="$GIT_COMMITTER_EMAIL" 247ORIG_GIT_COMMITTER_DATE="$GIT_COMMITTER_DATE" 248 249GIT_WORK_TREE=. 250export GIT_DIR GIT_WORK_TREE 251 252# Make sure refs/original is empty 253git for-each-ref > "$tempdir"/backup-refs || exit 254while read sha1 type name 255do 256 case "$force,$name" in 257 ,$orig_namespace*) 258 die "Cannot create a new backup. 259A previous backup already exists in $orig_namespace 260Force overwriting the backup with -f" 261 ;; 262 t,$orig_namespace*) 263 git update-ref -d "$name" $sha1 264 ;; 265 esac 266done < "$tempdir"/backup-refs 267 268# The refs should be updated if their heads were rewritten 269git rev-parse --no-flags --revs-only --symbolic-full-name \ 270 --default HEAD "$@" > "$tempdir"/raw-refs || exit 271while read ref 272do 273 case "$ref" in ^?*) continue ;; esac 274 275 if git rev-parse --verify "$ref"^0 >/dev/null 2>&1 276 then 277 echo "$ref" 278 else 279 warn "WARNING: not rewriting '$ref' (not a committish)" 280 fi 281done >"$tempdir"/heads <"$tempdir"/raw-refs 282 283test -s "$tempdir"/heads || 284 die "You must specify a ref to rewrite." 285 286GIT_INDEX_FILE="$(pwd)/../index" 287export GIT_INDEX_FILE 288 289# map old->new commit ids for rewriting parents 290mkdir ../map || die "Could not create map/ directory" 291 292state_prefixes="0 1 2 3 4 5 6 7 8 9 a b c d e f" 293 294if test -n "$state_branch" 295then 296 state_commit=$(git rev-parse --no-flags --revs-only "$state_branch") 297 if test -n "$state_commit" 298 then 299 echo "Populating map from $state_branch ($state_commit)" 1>&2 300 for prefix in $state_prefixes ; do 301 perl -e'open(MAP, "-|", "git show $ARGV[0]:$ARGV[1]") or die; 302 while (<MAP>) { 303 m/(.*):(.*)/ or die; 304 open F, ">../map/$1" or die; 305 print F "$2" or die; 306 close(F) or die; 307 } 308 close(MAP) or die;' "$state_commit" "filter_${prefix}.map" \ 309 || die "Unable to load state from $state_branch:filter_${prefix}.map" 310 done 311 else 312 echo "Branch $state_branch does not exist. Will create" 1>&2 313 fi 314fi 315 316# we need "--" only if there are no path arguments in $@ 317nonrevs=$(git rev-parse --no-revs "$@") || exit 318if test -z "$nonrevs" 319then 320 dashdash=-- 321else 322 dashdash= 323 remap_to_ancestor=t 324fi 325 326git rev-parse --revs-only "$@" >../parse 327 328case "$filter_subdir" in 329"") 330 eval set -- "$(git rev-parse --sq --no-revs "$@")" 331 ;; 332*) 333 eval set -- "$(git rev-parse --sq --no-revs "$@" $dashdash \ 334 "$filter_subdir")" 335 ;; 336esac 337 338git rev-list --reverse --topo-order --default HEAD \ 339 --parents --simplify-merges --stdin "$@" <../parse >../revs || 340 die "Could not get the commits" 341commits=$(wc -l <../revs | tr -d " ") 342 343test $commits -eq 0 && die_with_status 2 "Found nothing to rewrite" 344 345# Rewrite the commits 346report_progress () 347{ 348 if test -n "$progress" && 349 test $git_filter_branch__commit_count -gt $next_sample_at 350 then 351 count=$git_filter_branch__commit_count 352 353 now=$(date +%s) 354 elapsed=$(($now - $start_timestamp)) 355 remaining=$(( ($commits - $count) * $elapsed / $count )) 356 if test $elapsed -gt 0 357 then 358 next_sample_at=$(( ($elapsed + 1) * $count / $elapsed )) 359 else 360 next_sample_at=$(($next_sample_at + 1)) 361 fi 362 progress=" ($elapsed seconds passed, remaining $remaining predicted)" 363 fi 364 printf "\rRewrite $commit ($count/$commits)$progress " 365} 366 367git_filter_branch__commit_count=0 368 369progress= start_timestamp= 370if date '+%s' 2>/dev/null | grep -q '^[0-9][0-9]*$' 371then 372 next_sample_at=0 373 progress="dummy to ensure this is not empty" 374 start_timestamp=$(date '+%s') 375fi 376 377if test -n "$filter_index" || 378 test -n "$filter_tree" || 379 test -n "$filter_subdir" 380then 381 need_index=t 382else 383 need_index= 384fi 385 386eval "$filter_setup" < /dev/null || 387 die "filter setup failed: $filter_setup" 388 389while read commit parents; do 390 git_filter_branch__commit_count=$(($git_filter_branch__commit_count+1)) 391 392 report_progress 393 test -f "$workdir"/../map/$commit && continue 394 395 case "$filter_subdir" in 396 "") 397 if test -n "$need_index" 398 then 399 GIT_ALLOW_NULL_SHA1=1 git read-tree -i -m $commit 400 fi 401 ;; 402 *) 403 # The commit may not have the subdirectory at all 404 err=$(GIT_ALLOW_NULL_SHA1=1 \ 405 git read-tree -i -m $commit:"$filter_subdir" 2>&1) || { 406 if ! git rev-parse -q --verify $commit:"$filter_subdir" 407 then 408 rm -f "$GIT_INDEX_FILE" 409 else 410 echo >&2 "$err" 411 false 412 fi 413 } 414 esac || die "Could not initialize the index" 415 416 GIT_COMMIT=$commit 417 export GIT_COMMIT 418 git cat-file commit "$commit" >../commit || 419 die "Cannot read commit $commit" 420 421 eval "$(set_ident <../commit)" || 422 die "setting author/committer failed for commit $commit" 423 eval "$filter_env" < /dev/null || 424 die "env filter failed: $filter_env" 425 426 if [ "$filter_tree" ]; then 427 git checkout-index -f -u -a || 428 die "Could not checkout the index" 429 # files that $commit removed are now still in the working tree; 430 # remove them, else they would be added again 431 git clean -d -q -f -x 432 eval "$filter_tree" < /dev/null || 433 die "tree filter failed: $filter_tree" 434 435 ( 436 git diff-index -r --name-only --ignore-submodules $commit -- && 437 git ls-files --others 438 ) > "$tempdir"/tree-state || exit 439 git update-index --add --replace --remove --stdin \ 440 < "$tempdir"/tree-state || exit 441 fi 442 443 eval "$filter_index" < /dev/null || 444 die "index filter failed: $filter_index" 445 446 parentstr= 447 for parent in $parents; do 448 for reparent in $(map "$parent"); do 449 case "$parentstr " in 450 *" -p $reparent "*) 451 ;; 452 *) 453 parentstr="$parentstr -p $reparent" 454 ;; 455 esac 456 done 457 done 458 if [ "$filter_parent" ]; then 459 parentstr="$(echo "$parentstr" | eval "$filter_parent")" || 460 die "parent filter failed: $filter_parent" 461 fi 462 463 { 464 while IFS='' read -r header_line && test -n "$header_line" 465 do 466 # skip header lines... 467 :; 468 done 469 # and output the actual commit message 470 cat 471 } <../commit | 472 eval "$filter_msg" > ../message || 473 die "msg filter failed: $filter_msg" 474 475 if test -n "$need_index" 476 then 477 tree=$(git write-tree) 478 else 479 tree=$(git rev-parse "$commit^{tree}") 480 fi 481 workdir=$workdir /bin/sh -c "$filter_commit" "git commit-tree" \ 482 "$tree" $parentstr < ../message > ../map/$commit || 483 die "could not write rewritten commit" 484done <../revs 485 486# If we are filtering for paths, as in the case of a subdirectory 487# filter, it is possible that a specified head is not in the set of 488# rewritten commits, because it was pruned by the revision walker. 489# Ancestor remapping fixes this by mapping these heads to the unique 490# nearest ancestor that survived the pruning. 491 492if test "$remap_to_ancestor" = t 493then 494 while read ref 495 do 496 sha1=$(git rev-parse "$ref"^0) 497 test -f "$workdir"/../map/$sha1 && continue 498 ancestor=$(git rev-list --simplify-merges -1 "$ref" "$@") 499 test "$ancestor" && echo $(map $ancestor) >"$workdir"/../map/$sha1 500 done < "$tempdir"/heads 501fi 502 503# Finally update the refs 504 505echo 506while read ref 507do 508 # avoid rewriting a ref twice 509 test -f "$orig_namespace$ref" && continue 510 511 sha1=$(git rev-parse "$ref"^0) 512 rewritten=$(map $sha1) 513 514 test $sha1 = "$rewritten" && 515 warn "WARNING: Ref '$ref' is unchanged" && 516 continue 517 518 case "$rewritten" in 519 '') 520 echo "Ref '$ref' was deleted" 521 git update-ref -m "filter-branch: delete" -d "$ref" $sha1 || 522 die "Could not delete $ref" 523 ;; 524 *) 525 echo "Ref '$ref' was rewritten" 526 if ! git update-ref -m "filter-branch: rewrite" \ 527 "$ref" $rewritten $sha1 2>/dev/null; then 528 if test $(git cat-file -t "$ref") = tag; then 529 if test -z "$filter_tag_name"; then 530 warn "WARNING: You said to rewrite tagged commits, but not the corresponding tag." 531 warn "WARNING: Perhaps use '--tag-name-filter cat' to rewrite the tag." 532 fi 533 else 534 die "Could not rewrite $ref" 535 fi 536 fi 537 ;; 538 esac 539 git update-ref -m "filter-branch: backup" "$orig_namespace$ref" $sha1 || 540 exit 541done < "$tempdir"/heads 542 543# TODO: This should possibly go, with the semantics that all positive given 544# refs are updated, and their original heads stored in refs/original/ 545# Filter tags 546 547if [ "$filter_tag_name" ]; then 548 git for-each-ref --format='%(objectname) %(objecttype) %(refname)' refs/tags | 549 while read sha1 type ref; do 550 ref="${ref#refs/tags/}" 551 # XXX: Rewrite tagged trees as well? 552 if [ "$type" != "commit" -a "$type" != "tag" ]; then 553 continue; 554 fi 555 556 if [ "$type" = "tag" ]; then 557 # Dereference to a commit 558 sha1t="$sha1" 559 sha1="$(git rev-parse -q "$sha1"^{commit})" || continue 560 fi 561 562 [ -f "../map/$sha1" ] || continue 563 new_sha1="$(cat "../map/$sha1")" 564 GIT_COMMIT="$sha1" 565 export GIT_COMMIT 566 new_ref="$(echo "$ref" | eval "$filter_tag_name")" || 567 die "tag name filter failed: $filter_tag_name" 568 569 echo "$ref -> $new_ref ($sha1 -> $new_sha1)" 570 571 if [ "$type" = "tag" ]; then 572 new_sha1=$( ( printf 'object %s\ntype commit\ntag %s\n' \ 573 "$new_sha1" "$new_ref" 574 git cat-file tag "$ref" | 575 sed -n \ 576 -e '1,/^$/{ 577 /^object /d 578 /^type /d 579 /^tag /d 580 }' \ 581 -e '/^-----BEGIN PGP SIGNATURE-----/q' \ 582 -e 'p' ) | 583 git hash-object -t tag -w --stdin) || 584 die "Could not create new tag object for $ref" 585 if git cat-file tag "$ref" | \ 586 grep '^-----BEGIN PGP SIGNATURE-----' >/dev/null 2>&1 587 then 588 warn "gpg signature stripped from tag object $sha1t" 589 fi 590 fi 591 592 git update-ref "refs/tags/$new_ref" "$new_sha1" || 593 die "Could not write tag $new_ref" 594 done 595fi 596 597unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE 598unset GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_AUTHOR_DATE 599unset GIT_COMMITTER_NAME GIT_COMMITTER_EMAIL GIT_COMMITTER_DATE 600test -z "$ORIG_GIT_DIR" || { 601 GIT_DIR="$ORIG_GIT_DIR" && export GIT_DIR 602} 603test -z "$ORIG_GIT_WORK_TREE" || { 604 GIT_WORK_TREE="$ORIG_GIT_WORK_TREE" && 605 export GIT_WORK_TREE 606} 607test -z "$ORIG_GIT_INDEX_FILE" || { 608 GIT_INDEX_FILE="$ORIG_GIT_INDEX_FILE" && 609 export GIT_INDEX_FILE 610} 611test -z "$ORIG_GIT_AUTHOR_NAME" || { 612 GIT_AUTHOR_NAME="$ORIG_GIT_AUTHOR_NAME" && 613 export GIT_AUTHOR_NAME 614} 615test -z "$ORIG_GIT_AUTHOR_EMAIL" || { 616 GIT_AUTHOR_EMAIL="$ORIG_GIT_AUTHOR_EMAIL" && 617 export GIT_AUTHOR_EMAIL 618} 619test -z "$ORIG_GIT_AUTHOR_DATE" || { 620 GIT_AUTHOR_DATE="$ORIG_GIT_AUTHOR_DATE" && 621 export GIT_AUTHOR_DATE 622} 623test -z "$ORIG_GIT_COMMITTER_NAME" || { 624 GIT_COMMITTER_NAME="$ORIG_GIT_COMMITTER_NAME" && 625 export GIT_COMMITTER_NAME 626} 627test -z "$ORIG_GIT_COMMITTER_EMAIL" || { 628 GIT_COMMITTER_EMAIL="$ORIG_GIT_COMMITTER_EMAIL" && 629 export GIT_COMMITTER_EMAIL 630} 631test -z "$ORIG_GIT_COMMITTER_DATE" || { 632 GIT_COMMITTER_DATE="$ORIG_GIT_COMMITTER_DATE" && 633 export GIT_COMMITTER_DATE 634} 635 636if test -n "$state_branch" 637then 638 echo "Saving rewrite state to $state_branch" 1>&2 639 for prefix in $state_prefixes ; do 640 state_blob=$( 641 perl -e'opendir D, "../map" or die; 642 open H, "|-", "git hash-object -w --stdin" or die; 643 foreach (sort readdir(D)) { 644 next if m/^\.\.?$/; 645 next unless m/^$ARGV[0]/; 646 open F, "<../map/$_" or die; 647 chomp($f = <F>); 648 print H "$_:$f\n" or die; 649 } 650 close(H) or die;' "$prefix" || die "Unable to save state") 651 printf '100644 blob %s\tfilter_%s.map\n' "$state_blob" "$prefix" >> $tempdir/state-branch-tree 652 done 653 state_tree=$(cat $tempdir/state-branch-tree | git mktree) 654 655 if test -n "$state_commit" 656 then 657 state_commit=$(echo "Sync" | git commit-tree "$state_tree" -p "$state_commit") 658 else 659 state_commit=$(echo "Sync" | git commit-tree "$state_tree" ) 660 fi 661 git update-ref "$state_branch" "$state_commit" 662fi 663 664cd "$orig_dir" 665rm -rf "$tempdir" 666 667trap - 0 668 669if [ "$(is_bare_repository)" = false ]; then 670 git read-tree -u -m HEAD || exit 671fi 672 673exit 0 674