xref: /freebsd/sys/contrib/device-tree/scripts/git-filter-branch (revision 7d0873ebb83b19ba1e8a89e679470d885efe12e3)
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