Git fork
at reftables-rust 665 lines 16 kB view raw
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 292if test -n "$state_branch" 293then 294 state_commit=$(git rev-parse --no-flags --revs-only "$state_branch") 295 if test -n "$state_commit" 296 then 297 echo "Populating map from $state_branch ($state_commit)" 1>&2 298 299 git show "$state_commit:filter.map" >"$tempdir"/filter-map || 300 die "Unable to load state from $state_branch:filter.map" 301 while read line 302 do 303 case "$line" in 304 *:*) 305 echo "${line%:*}" >../map/"${line#*:}";; 306 *) 307 die "Unable to load state from $state_branch:filter.map";; 308 esac 309 done <"$tempdir"/filter-map 310 else 311 echo "Branch $state_branch does not exist. Will create" 1>&2 312 fi 313fi 314 315# we need "--" only if there are no path arguments in $@ 316nonrevs=$(git rev-parse --no-revs "$@") || exit 317if test -z "$nonrevs" 318then 319 dashdash=-- 320else 321 dashdash= 322 remap_to_ancestor=t 323fi 324 325git rev-parse --revs-only "$@" >../parse 326 327case "$filter_subdir" in 328"") 329 eval set -- "$(git rev-parse --sq --no-revs "$@")" 330 ;; 331*) 332 eval set -- "$(git rev-parse --sq --no-revs "$@" $dashdash \ 333 "$filter_subdir")" 334 ;; 335esac 336 337git rev-list --reverse --topo-order --default HEAD \ 338 --parents --simplify-merges --stdin "$@" <../parse >../revs || 339 die "Could not get the commits" 340commits=$(wc -l <../revs | tr -d " ") 341 342test $commits -eq 0 && die_with_status 2 "Found nothing to rewrite" 343 344# Rewrite the commits 345report_progress () 346{ 347 if test -n "$progress" && 348 test $git_filter_branch__commit_count -gt $next_sample_at 349 then 350 count=$git_filter_branch__commit_count 351 352 now=$(date +%s) 353 elapsed=$(($now - $start_timestamp)) 354 remaining=$(( ($commits - $count) * $elapsed / $count )) 355 if test $elapsed -gt 0 356 then 357 next_sample_at=$(( ($elapsed + 1) * $count / $elapsed )) 358 else 359 next_sample_at=$(($next_sample_at + 1)) 360 fi 361 progress=" ($elapsed seconds passed, remaining $remaining predicted)" 362 fi 363 printf "\rRewrite $commit ($count/$commits)$progress " 364} 365 366git_filter_branch__commit_count=0 367 368progress= start_timestamp= 369if date '+%s' 2>/dev/null | grep -q '^[0-9][0-9]*$' 370then 371 next_sample_at=0 372 progress="dummy to ensure this is not empty" 373 start_timestamp=$(date '+%s') 374fi 375 376if test -n "$filter_index" || 377 test -n "$filter_tree" || 378 test -n "$filter_subdir" 379then 380 need_index=t 381else 382 need_index= 383fi 384 385eval "$filter_setup" < /dev/null || 386 die "filter setup failed: $filter_setup" 387 388while read commit parents; do 389 git_filter_branch__commit_count=$(($git_filter_branch__commit_count+1)) 390 391 report_progress 392 test -f "$workdir"/../map/$commit && continue 393 394 case "$filter_subdir" in 395 "") 396 if test -n "$need_index" 397 then 398 GIT_ALLOW_NULL_SHA1=1 git read-tree -i -m $commit 399 fi 400 ;; 401 *) 402 # The commit may not have the subdirectory at all 403 err=$(GIT_ALLOW_NULL_SHA1=1 \ 404 git read-tree -i -m $commit:"$filter_subdir" 2>&1) || { 405 if ! git rev-parse -q --verify $commit:"$filter_subdir" 406 then 407 rm -f "$GIT_INDEX_FILE" 408 else 409 echo >&2 "$err" 410 false 411 fi 412 } 413 esac || die "Could not initialize the index" 414 415 GIT_COMMIT=$commit 416 export GIT_COMMIT 417 git cat-file commit "$commit" >../commit || 418 die "Cannot read commit $commit" 419 420 eval "$(set_ident <../commit)" || 421 die "setting author/committer failed for commit $commit" 422 eval "$filter_env" < /dev/null || 423 die "env filter failed: $filter_env" 424 425 if [ "$filter_tree" ]; then 426 git checkout-index -f -u -a || 427 die "Could not checkout the index" 428 # files that $commit removed are now still in the working tree; 429 # remove them, else they would be added again 430 git clean -d -q -f -x 431 eval "$filter_tree" < /dev/null || 432 die "tree filter failed: $filter_tree" 433 434 ( 435 git diff-index -r --name-only --ignore-submodules $commit -- && 436 git ls-files --others 437 ) > "$tempdir"/tree-state || exit 438 git update-index --add --replace --remove --stdin \ 439 < "$tempdir"/tree-state || exit 440 fi 441 442 eval "$filter_index" < /dev/null || 443 die "index filter failed: $filter_index" 444 445 parentstr= 446 for parent in $parents; do 447 for reparent in $(map "$parent"); do 448 case "$parentstr " in 449 *" -p $reparent "*) 450 ;; 451 *) 452 parentstr="$parentstr -p $reparent" 453 ;; 454 esac 455 done 456 done 457 if [ "$filter_parent" ]; then 458 parentstr="$(echo "$parentstr" | eval "$filter_parent")" || 459 die "parent filter failed: $filter_parent" 460 fi 461 462 { 463 while IFS='' read -r header_line && test -n "$header_line" 464 do 465 # skip header lines... 466 :; 467 done 468 # and output the actual commit message 469 cat 470 } <../commit | 471 eval "$filter_msg" > ../message || 472 die "msg filter failed: $filter_msg" 473 474 if test -n "$need_index" 475 then 476 tree=$(git write-tree) 477 else 478 tree=$(git rev-parse "$commit^{tree}") 479 fi 480 workdir=$workdir @SHELL_PATH@ -c "$filter_commit" "git commit-tree" \ 481 "$tree" $parentstr < ../message > ../map/$commit || 482 die "could not write rewritten commit" 483done <../revs 484 485# If we are filtering for paths, as in the case of a subdirectory 486# filter, it is possible that a specified head is not in the set of 487# rewritten commits, because it was pruned by the revision walker. 488# Ancestor remapping fixes this by mapping these heads to the unique 489# nearest ancestor that survived the pruning. 490 491if test "$remap_to_ancestor" = t 492then 493 while read ref 494 do 495 sha1=$(git rev-parse "$ref"^0) 496 test -f "$workdir"/../map/$sha1 && continue 497 ancestor=$(git rev-list --simplify-merges -1 "$ref" "$@") 498 test "$ancestor" && echo $(map $ancestor) >"$workdir"/../map/$sha1 499 done < "$tempdir"/heads 500fi 501 502# Finally update the refs 503 504echo 505while read ref 506do 507 # avoid rewriting a ref twice 508 test -f "$orig_namespace$ref" && continue 509 510 sha1=$(git rev-parse "$ref"^0) 511 rewritten=$(map $sha1) 512 513 test $sha1 = "$rewritten" && 514 warn "WARNING: Ref '$ref' is unchanged" && 515 continue 516 517 case "$rewritten" in 518 '') 519 echo "Ref '$ref' was deleted" 520 git update-ref -m "filter-branch: delete" -d "$ref" $sha1 || 521 die "Could not delete $ref" 522 ;; 523 *) 524 echo "Ref '$ref' was rewritten" 525 if ! git update-ref -m "filter-branch: rewrite" \ 526 "$ref" $rewritten $sha1 2>/dev/null; then 527 if test $(git cat-file -t "$ref") = tag; then 528 if test -z "$filter_tag_name"; then 529 warn "WARNING: You said to rewrite tagged commits, but not the corresponding tag." 530 warn "WARNING: Perhaps use '--tag-name-filter cat' to rewrite the tag." 531 fi 532 else 533 die "Could not rewrite $ref" 534 fi 535 fi 536 ;; 537 esac 538 git update-ref -m "filter-branch: backup" "$orig_namespace$ref" $sha1 || 539 exit 540done < "$tempdir"/heads 541 542# TODO: This should possibly go, with the semantics that all positive given 543# refs are updated, and their original heads stored in refs/original/ 544# Filter tags 545 546if [ "$filter_tag_name" ]; then 547 git for-each-ref --format='%(objectname) %(objecttype) %(refname)' refs/tags | 548 while read sha1 type ref; do 549 ref="${ref#refs/tags/}" 550 # XXX: Rewrite tagged trees as well? 551 if [ "$type" != "commit" -a "$type" != "tag" ]; then 552 continue; 553 fi 554 555 if [ "$type" = "tag" ]; then 556 # Dereference to a commit 557 sha1t="$sha1" 558 sha1="$(git rev-parse -q "$sha1"^{commit})" || continue 559 fi 560 561 [ -f "../map/$sha1" ] || continue 562 new_sha1="$(cat "../map/$sha1")" 563 GIT_COMMIT="$sha1" 564 export GIT_COMMIT 565 new_ref="$(echo "$ref" | eval "$filter_tag_name")" || 566 die "tag name filter failed: $filter_tag_name" 567 568 echo "$ref -> $new_ref ($sha1 -> $new_sha1)" 569 570 if [ "$type" = "tag" ]; then 571 new_sha1=$( ( printf 'object %s\ntype commit\ntag %s\n' \ 572 "$new_sha1" "$new_ref" 573 git cat-file tag "$ref" | 574 sed -n \ 575 -e '1,/^$/{ 576 /^object /d 577 /^type /d 578 /^tag /d 579 }' \ 580 -e '/^-----BEGIN PGP SIGNATURE-----/q' \ 581 -e 'p' ) | 582 git hash-object -t tag -w --stdin) || 583 die "Could not create new tag object for $ref" 584 if git cat-file tag "$ref" | \ 585 grep '^-----BEGIN PGP SIGNATURE-----' >/dev/null 2>&1 586 then 587 warn "gpg signature stripped from tag object $sha1t" 588 fi 589 fi 590 591 git update-ref "refs/tags/$new_ref" "$new_sha1" || 592 die "Could not write tag $new_ref" 593 done 594fi 595 596unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE 597unset GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_AUTHOR_DATE 598unset GIT_COMMITTER_NAME GIT_COMMITTER_EMAIL GIT_COMMITTER_DATE 599test -z "$ORIG_GIT_DIR" || { 600 GIT_DIR="$ORIG_GIT_DIR" && export GIT_DIR 601} 602test -z "$ORIG_GIT_WORK_TREE" || { 603 GIT_WORK_TREE="$ORIG_GIT_WORK_TREE" && 604 export GIT_WORK_TREE 605} 606test -z "$ORIG_GIT_INDEX_FILE" || { 607 GIT_INDEX_FILE="$ORIG_GIT_INDEX_FILE" && 608 export GIT_INDEX_FILE 609} 610test -z "$ORIG_GIT_AUTHOR_NAME" || { 611 GIT_AUTHOR_NAME="$ORIG_GIT_AUTHOR_NAME" && 612 export GIT_AUTHOR_NAME 613} 614test -z "$ORIG_GIT_AUTHOR_EMAIL" || { 615 GIT_AUTHOR_EMAIL="$ORIG_GIT_AUTHOR_EMAIL" && 616 export GIT_AUTHOR_EMAIL 617} 618test -z "$ORIG_GIT_AUTHOR_DATE" || { 619 GIT_AUTHOR_DATE="$ORIG_GIT_AUTHOR_DATE" && 620 export GIT_AUTHOR_DATE 621} 622test -z "$ORIG_GIT_COMMITTER_NAME" || { 623 GIT_COMMITTER_NAME="$ORIG_GIT_COMMITTER_NAME" && 624 export GIT_COMMITTER_NAME 625} 626test -z "$ORIG_GIT_COMMITTER_EMAIL" || { 627 GIT_COMMITTER_EMAIL="$ORIG_GIT_COMMITTER_EMAIL" && 628 export GIT_COMMITTER_EMAIL 629} 630test -z "$ORIG_GIT_COMMITTER_DATE" || { 631 GIT_COMMITTER_DATE="$ORIG_GIT_COMMITTER_DATE" && 632 export GIT_COMMITTER_DATE 633} 634 635if test -n "$state_branch" 636then 637 echo "Saving rewrite state to $state_branch" 1>&2 638 state_blob=$( 639 for file in ../map/* 640 do 641 from_commit=$(basename "$file") 642 to_commit=$(cat "$file") 643 echo "$from_commit:$to_commit" 644 done | git hash-object -w --stdin || die "Unable to save state" 645 ) 646 state_tree=$(printf '100644 blob %s\tfilter.map\n' "$state_blob" | git mktree) 647 if test -n "$state_commit" 648 then 649 state_commit=$(echo "Sync" | git commit-tree "$state_tree" -p "$state_commit") 650 else 651 state_commit=$(echo "Sync" | git commit-tree "$state_tree" ) 652 fi 653 git update-ref "$state_branch" "$state_commit" 654fi 655 656cd "$orig_dir" 657rm -rf "$tempdir" 658 659trap - 0 660 661if [ "$(is_bare_repository)" = false ]; then 662 git read-tree -u -m HEAD || exit 663fi 664 665exit 0