]> CyberLeo.Net >> Repos - FreeBSD/FreeBSD.git/blob - scripts/git-filter-branch
Bring in Ian Campbell's pruned dts repo.
[FreeBSD/FreeBSD.git] / scripts / git-filter-branch
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
13 export PATH=/usr/lib/git-core:$PATH
14
15 functions=$(cat << \EOF
16 warn () {
17         echo "$*" >&2
18 }
19
20 map()
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
34 skip_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.
47 git_commit_non_empty_tree()
48 {
49         if test $# = 3 && test "$1" = $(git rev-parse "$3^{tree}"); then
50                 map "$3"
51         else
52                 git commit-tree "$@"
53         fi
54 }
55 # override die(): this version puts in an extra line break, so that
56 # the progress is still visible
57
58 die()
59 {
60         echo >&2
61         echo "$*" >&2
62         exit 1
63 }
64 EOF
65 )
66
67 eval "$functions"
68
69 # When piped a commit, output a script to set the ident of either
70 # "author" or "committer
71
72 set_ident () {
73         lid="$(echo "$1" | tr "[A-Z]" "[a-z]")"
74         uid="$(echo "$1" | tr "[a-z]" "[A-Z]")"
75         pick_id_script='
76                 /^'$lid' /{
77                         s/'\''/'\''\\'\'\''/g
78                         h
79                         s/^'$lid' \([^<]*\) <[^>]*> .*$/\1/
80                         s/'\''/'\''\'\'\''/g
81                         s/.*/GIT_'$uid'_NAME='\''&'\''; export GIT_'$uid'_NAME/p
82
83                         g
84                         s/^'$lid' [^<]* <\([^>]*\)> .*$/\1/
85                         s/'\''/'\''\'\'\''/g
86                         s/.*/GIT_'$uid'_EMAIL='\''&'\''; export GIT_'$uid'_EMAIL/p
87
88                         g
89                         s/^'$lid' [^<]* <[^>]*> \(.*\)$/\1/
90                         s/'\''/'\''\'\'\''/g
91                         s/.*/GIT_'$uid'_DATE='\''&'\''; export GIT_'$uid'_DATE/p
92
93                         q
94                 }
95         '
96
97         LANG=C LC_ALL=C sed -ne "$pick_id_script"
98         # Ensure non-empty id name.
99         echo "case \"\$GIT_${uid}_NAME\" in \"\") GIT_${uid}_NAME=\"\${GIT_${uid}_EMAIL%%@*}\" && export GIT_${uid}_NAME;; esac"
100 }
101
102 USAGE="[--env-filter <command>] [--tree-filter <command>]
103         [--index-filter <command>] [--parent-filter <command>]
104         [--msg-filter <command>] [--commit-filter <command>]
105         [--tag-name-filter <command>] [--subdirectory-filter <directory>]
106         [--original <namespace>] [-d <directory>] [-f | --force]
107         [--state-branch <branch>]
108         [<rev-list options>...]"
109
110 OPTIONS_SPEC=
111 . git-sh-setup
112
113 if [ "$(is_bare_repository)" = false ]; then
114         require_clean_work_tree 'rewrite branches'
115 fi
116
117 tempdir=.git-rewrite
118 filter_env=
119 filter_tree=
120 filter_index=
121 filter_parent=
122 filter_msg=cat
123 filter_commit=
124 filter_tag_name=
125 filter_subdir=
126 state_branch=
127 orig_namespace=refs/original/
128 force=
129 prune_empty=
130 remap_to_ancestor=
131 while :
132 do
133         case "$1" in
134         --)
135                 shift
136                 break
137                 ;;
138         --force|-f)
139                 shift
140                 force=t
141                 continue
142                 ;;
143         --remap-to-ancestor)
144                 # deprecated ($remap_to_ancestor is set now automatically)
145                 shift
146                 remap_to_ancestor=t
147                 continue
148                 ;;
149         --prune-empty)
150                 shift
151                 prune_empty=t
152                 continue
153                 ;;
154         -*)
155                 ;;
156         *)
157                 break;
158         esac
159
160         # all switches take one argument
161         ARG="$1"
162         case "$#" in 1) usage ;; esac
163         shift
164         OPTARG="$1"
165         shift
166
167         case "$ARG" in
168         -d)
169                 tempdir="$OPTARG"
170                 ;;
171         --env-filter)
172                 filter_env="$OPTARG"
173                 ;;
174         --tree-filter)
175                 filter_tree="$OPTARG"
176                 ;;
177         --index-filter)
178                 filter_index="$OPTARG"
179                 ;;
180         --parent-filter)
181                 filter_parent="$OPTARG"
182                 ;;
183         --msg-filter)
184                 filter_msg="$OPTARG"
185                 ;;
186         --commit-filter)
187                 filter_commit="$functions; $OPTARG"
188                 ;;
189         --tag-name-filter)
190                 filter_tag_name="$OPTARG"
191                 ;;
192         --subdirectory-filter)
193                 filter_subdir="$OPTARG"
194                 remap_to_ancestor=t
195                 ;;
196         --original)
197                 orig_namespace=$(expr "$OPTARG/" : '\(.*[^/]\)/*$')/
198                 ;;
199         --state-branch)
200                 state_branch="$OPTARG"
201                 ;;
202         *)
203                 usage
204                 ;;
205         esac
206 done
207
208 case "$prune_empty,$filter_commit" in
209 ,)
210         filter_commit='git commit-tree "$@"';;
211 t,)
212         filter_commit="$functions;"' git_commit_non_empty_tree "$@"';;
213 ,*)
214         ;;
215 *)
216         die "Cannot set --prune-empty and --commit-filter at the same time"
217 esac
218
219 case "$force" in
220 t)
221         rm -rf "$tempdir"
222 ;;
223 '')
224         test -d "$tempdir" &&
225                 die "$tempdir already exists, please remove it"
226 esac
227 mkdir -p "$tempdir/t" &&
228 tempdir="$(cd "$tempdir"; pwd)" &&
229 cd "$tempdir/t" &&
230 workdir="$(pwd)" ||
231 die ""
232
233 # Remove tempdir on exit
234 trap 'cd ../..; rm -rf "$tempdir"' 0
235
236 ORIG_GIT_DIR="$GIT_DIR"
237 ORIG_GIT_WORK_TREE="$GIT_WORK_TREE"
238 ORIG_GIT_INDEX_FILE="$GIT_INDEX_FILE"
239 GIT_WORK_TREE=.
240 export GIT_DIR GIT_WORK_TREE
241
242 # Make sure refs/original is empty
243 git for-each-ref > "$tempdir"/backup-refs || exit
244 while read sha1 type name
245 do
246         case "$force,$name" in
247         ,$orig_namespace*)
248                 die "Cannot create a new backup.
249 A previous backup already exists in $orig_namespace
250 Force overwriting the backup with -f"
251         ;;
252         t,$orig_namespace*)
253                 git update-ref -d "$name" $sha1
254         ;;
255         esac
256 done < "$tempdir"/backup-refs
257
258 # The refs should be updated if their heads were rewritten
259 git rev-parse --no-flags --revs-only --symbolic-full-name \
260         --default HEAD "$@" > "$tempdir"/raw-heads || exit
261 sed -e '/^^/d' "$tempdir"/raw-heads >"$tempdir"/heads
262
263 test -s "$tempdir"/heads ||
264         die "Which ref do you want to rewrite?"
265
266 GIT_INDEX_FILE="$(pwd)/../index"
267 export GIT_INDEX_FILE
268
269 # map old->new commit ids for rewriting parents
270 mkdir ../map || die "Could not create map/ directory"
271
272 if [ -n "$state_branch" ] ; then
273         state_commit=`git show-ref -s "$state_branch"`
274         if [ -n "$state_commit" ] ; then
275                 echo "Populating map from $state_branch ($state_commit)" 1>&2
276                 git show "$state_commit":filter.map |
277                     perl -n -e 'm/(.*):(.*)/ or die;
278                                 open F, ">../map/$1" or die;
279                                 print F "$2" or die;
280                                 close(F) or die'
281         else
282                 echo "Branch $state_branch does not exist. Will create" 1>&2
283         fi
284 fi
285
286 # we need "--" only if there are no path arguments in $@
287 nonrevs=$(git rev-parse --no-revs "$@") || exit
288 if test -z "$nonrevs"
289 then
290         dashdash=--
291 else
292         dashdash=
293         remap_to_ancestor=t
294 fi
295
296 rev_args=$(git rev-parse --revs-only "$@")
297
298 case "$filter_subdir" in
299 "")
300         eval set -- "$(git rev-parse --sq --no-revs "$@")"
301         ;;
302 *)
303         eval set -- "$(git rev-parse --sq --no-revs "$@" $dashdash \
304                 "$filter_subdir")"
305         ;;
306 esac
307
308 git rev-list --reverse --topo-order --default HEAD \
309         --parents --simplify-merges $rev_args "$@" > ../revs ||
310         die "Could not get the commits"
311 commits=$(wc -l <../revs | tr -d " ")
312
313 test $commits -eq 0 && die "Found nothing to rewrite"
314
315 # Rewrite the commits
316
317 git_filter_branch__commit_count=0
318 while read commit parents; do
319         git_filter_branch__commit_count=$(($git_filter_branch__commit_count+1))
320         printf "\rRewrite $commit ($git_filter_branch__commit_count/$commits)"
321
322         case "$filter_subdir" in
323         "")
324                 git read-tree -i -m $commit
325                 ;;
326         *)
327                 # The commit may not have the subdirectory at all
328                 err=$(git read-tree -i -m $commit:"$filter_subdir" 2>&1) || {
329                         if ! git rev-parse -q --verify $commit:"$filter_subdir"
330                         then
331                                 rm -f "$GIT_INDEX_FILE"
332                         else
333                                 echo >&2 "$err"
334                                 false
335                         fi
336                 }
337         esac || die "Could not initialize the index"
338
339         GIT_COMMIT=$commit
340         export GIT_COMMIT
341         git cat-file commit "$commit" >../commit ||
342                 die "Cannot read commit $commit"
343
344         eval "$(set_ident AUTHOR <../commit)" ||
345                 die "setting author failed for commit $commit"
346         eval "$(set_ident COMMITTER <../commit)" ||
347                 die "setting committer failed for commit $commit"
348         eval "$filter_env" < /dev/null ||
349                 die "env filter failed: $filter_env"
350
351         if [ "$filter_tree" ]; then
352                 git checkout-index -f -u -a ||
353                         die "Could not checkout the index"
354                 # files that $commit removed are now still in the working tree;
355                 # remove them, else they would be added again
356                 git clean -d -q -f -x
357                 eval "$filter_tree" < /dev/null ||
358                         die "tree filter failed: $filter_tree"
359
360                 (
361                         git diff-index -r --name-only --ignore-submodules $commit &&
362                         git ls-files --others
363                 ) > "$tempdir"/tree-state || exit
364                 git update-index --add --replace --remove --stdin \
365                         < "$tempdir"/tree-state || exit
366         fi
367
368         eval "$filter_index" < /dev/null ||
369                 die "index filter failed: $filter_index"
370
371         parentstr=
372         for parent in $parents; do
373                 for reparent in $(map "$parent"); do
374                         parentstr="$parentstr -p $reparent"
375                 done
376         done
377         if [ "$filter_parent" ]; then
378                 parentstr="$(echo "$parentstr" | eval "$filter_parent")" ||
379                                 die "parent filter failed: $filter_parent"
380         fi
381
382         sed -e '1,/^$/d' <../commit | \
383                 eval "$filter_msg" > ../message ||
384                         die "msg filter failed: $filter_msg"
385         workdir=$workdir /bin/sh -c "$filter_commit" "git commit-tree" \
386                 $(git write-tree) $parentstr < ../message > ../map/$commit ||
387                         die "could not write rewritten commit"
388 done <../revs
389
390 # If we are filtering for paths, as in the case of a subdirectory
391 # filter, it is possible that a specified head is not in the set of
392 # rewritten commits, because it was pruned by the revision walker.
393 # Ancestor remapping fixes this by mapping these heads to the unique
394 # nearest ancestor that survived the pruning.
395
396 if test "$remap_to_ancestor" = t
397 then
398         while read ref
399         do
400                 sha1=$(git rev-parse "$ref"^0)
401                 test -f "$workdir"/../map/$sha1 && continue
402                 ancestor=$(git rev-list --simplify-merges -1 "$ref" "$@")
403                 test "$ancestor" && echo $(map $ancestor) >> "$workdir"/../map/$sha1
404         done < "$tempdir"/heads
405 fi
406
407 # Finally update the refs
408
409 _x40='[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]'
410 _x40="$_x40$_x40$_x40$_x40$_x40$_x40$_x40$_x40"
411 echo
412 while read ref
413 do
414         # avoid rewriting a ref twice
415         test -f "$orig_namespace$ref" && continue
416
417         sha1=$(git rev-parse "$ref"^0)
418         rewritten=$(map $sha1)
419
420         test $sha1 = "$rewritten" &&
421                 warn "WARNING: Ref '$ref' is unchanged" &&
422                 continue
423
424         case "$rewritten" in
425         '')
426                 echo "Ref '$ref' was deleted"
427                 git update-ref -m "filter-branch: delete" -d "$ref" $sha1 ||
428                         die "Could not delete $ref"
429         ;;
430         $_x40)
431                 echo "Ref '$ref' was rewritten"
432                 if ! git update-ref -m "filter-branch: rewrite" \
433                                         "$ref" $rewritten $sha1 2>/dev/null; then
434                         if test $(git cat-file -t "$ref") = tag; then
435                                 if test -z "$filter_tag_name"; then
436                                         warn "WARNING: You said to rewrite tagged commits, but not the corresponding tag."
437                                         warn "WARNING: Perhaps use '--tag-name-filter cat' to rewrite the tag."
438                                 fi
439                         else
440                                 die "Could not rewrite $ref"
441                         fi
442                 fi
443         ;;
444         *)
445                 # NEEDSWORK: possibly add -Werror, making this an error
446                 warn "WARNING: '$ref' was rewritten into multiple commits:"
447                 warn "$rewritten"
448                 warn "WARNING: Ref '$ref' points to the first one now."
449                 rewritten=$(echo "$rewritten" | head -n 1)
450                 git update-ref -m "filter-branch: rewrite to first" \
451                                 "$ref" $rewritten $sha1 ||
452                         die "Could not rewrite $ref"
453         ;;
454         esac
455         git update-ref -m "filter-branch: backup" "$orig_namespace$ref" $sha1 ||
456                  exit
457 done < "$tempdir"/heads
458
459 # TODO: This should possibly go, with the semantics that all positive given
460 #       refs are updated, and their original heads stored in refs/original/
461 # Filter tags
462
463 if [ "$filter_tag_name" ]; then
464         git for-each-ref --format='%(objectname) %(objecttype) %(refname)' refs/tags |
465         while read sha1 type ref; do
466                 ref="${ref#refs/tags/}"
467                 # XXX: Rewrite tagged trees as well?
468                 if [ "$type" != "commit" -a "$type" != "tag" ]; then
469                         continue;
470                 fi
471
472                 if [ "$type" = "tag" ]; then
473                         # Dereference to a commit
474                         sha1t="$sha1"
475                         sha1="$(git rev-parse -q "$sha1"^{commit})" || continue
476                 fi
477
478                 [ -f "../map/$sha1" ] || continue
479                 new_sha1="$(cat "../map/$sha1")"
480                 GIT_COMMIT="$sha1"
481                 export GIT_COMMIT
482                 new_ref="$(echo "$ref" | eval "$filter_tag_name")" ||
483                         die "tag name filter failed: $filter_tag_name"
484
485                 echo "$ref -> $new_ref ($sha1 -> $new_sha1)"
486
487                 if [ "$type" = "tag" ]; then
488                         new_sha1=$( ( printf 'object %s\ntype commit\ntag %s\n' \
489                                                 "$new_sha1" "$new_ref"
490                                 git cat-file tag "$ref" |
491                                 awk '/^tagger/ { tagged=1 }
492                                      /^$/      { if (!tagged && !done) { print "tagger Unknown <unknown@example.com> 0 +0000" } ; done=1 }
493                                      //        { print }' |
494                                 sed -n \
495                                     -e '1,/^$/{
496                                           /^object /d
497                                           /^type /d
498                                           /^tag /d
499                                         }' \
500                                     -e '/^-----BEGIN PGP SIGNATURE-----/q' \
501                                     -e 'p' ) |
502                                 git mktag) ||
503                                 die "Could not create new tag object for $ref"
504                         if git cat-file tag "$ref" | \
505                            sane_grep '^-----BEGIN PGP SIGNATURE-----' >/dev/null 2>&1
506                         then
507                                 warn "gpg signature stripped from tag object $sha1t"
508                         fi
509                 fi
510
511                 git update-ref "refs/tags/$new_ref" "$new_sha1" ||
512                         die "Could not write tag $new_ref"
513         done
514 fi
515
516 if [ -n "$state_branch" ] ; then
517         echo "Saving rewrite state to $state_branch" 1>&2
518         STATE_BLOB=$(ls ../map |
519             perl -n -e 'chomp();
520                         open F, "<../map/$_" or die;
521                         chomp($f = <F>); print "$_:$f\n";' |
522             git hash-object -w --stdin )
523         STATE_TREE=$(/bin/echo -e "100644 blob $STATE_BLOB\tfilter.map" | git mktree)
524         STATE_PARENT=$(git show-ref -s "$state_branch")
525         unset GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_AUTHOR_DATE
526         unset GIT_COMMITTER_NAME GIT_COMMITTER_EMAIL GIT_COMMITTER_DATE
527         if [ -n "$STATE_PARENT" ] ; then
528             STATE_COMMIT=$(/bin/echo "Sync" | git commit-tree "$STATE_TREE" -p "$STATE_PARENT")
529         else
530             STATE_COMMIT=$(/bin/echo "Sync" | git commit-tree "$STATE_TREE" )
531         fi
532         git update-ref "$state_branch" "$STATE_COMMIT"
533 fi
534
535 cd ../..
536 rm -rf "$tempdir"
537
538 trap - 0
539
540 unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE
541 test -z "$ORIG_GIT_DIR" || {
542         GIT_DIR="$ORIG_GIT_DIR" && export GIT_DIR
543 }
544 test -z "$ORIG_GIT_WORK_TREE" || {
545         GIT_WORK_TREE="$ORIG_GIT_WORK_TREE" &&
546         export GIT_WORK_TREE
547 }
548 test -z "$ORIG_GIT_INDEX_FILE" || {
549         GIT_INDEX_FILE="$ORIG_GIT_INDEX_FILE" &&
550         export GIT_INDEX_FILE
551 }
552
553 if [ "$(is_bare_repository)" = false ]; then
554         git read-tree -u -m HEAD || exit
555 fi
556
557 exit 0