1#!/bin/sh
2
3test_description='git filter-branch'
4GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
5export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
6
7. ./test-lib.sh
8. "$TEST_DIRECTORY/lib-gpg.sh"
9
10test_expect_success 'setup' '
11	test_commit A &&
12	GIT_COMMITTER_DATE="@0 +0000" GIT_AUTHOR_DATE="@0 +0000" &&
13	test_commit --notick B &&
14	git checkout -b branch B &&
15	test_commit D &&
16	mkdir dir &&
17	test_commit dir/D &&
18	test_commit E &&
19	git checkout main &&
20	test_commit C &&
21	git checkout branch &&
22	git merge C &&
23	git tag F &&
24	test_commit G &&
25	test_commit H
26'
27# * (HEAD, branch) H
28# * G
29# *   Merge commit 'C' into branch
30# |\
31# | * (main) C
32# * | E
33# * | dir/D
34# * | D
35# |/
36# * B
37# * A
38
39
40H=$(git rev-parse H)
41
42test_expect_success 'rewrite identically' '
43	git filter-branch branch
44'
45test_expect_success 'result is really identical' '
46	test $H = $(git rev-parse HEAD)
47'
48
49test_expect_success 'rewrite bare repository identically' '
50	(git config core.bare true && cd .git &&
51	 git filter-branch branch > filter-output 2>&1 &&
52	! fgrep fatal filter-output)
53'
54git config core.bare false
55test_expect_success 'result is really identical' '
56	test $H = $(git rev-parse HEAD)
57'
58
59TRASHDIR=$(pwd)
60test_expect_success 'correct GIT_DIR while using -d' '
61	mkdir drepo &&
62	( cd drepo &&
63	git init &&
64	test_commit drepo &&
65	git filter-branch -d "$TRASHDIR/dfoo" \
66		--index-filter "cp \"$TRASHDIR\"/dfoo/backup-refs \"$TRASHDIR\"" \
67	) &&
68	grep drepo "$TRASHDIR/backup-refs"
69'
70
71test_expect_success 'tree-filter works with -d' '
72	git init drepo-tree &&
73	(
74		cd drepo-tree &&
75		test_commit one &&
76		git filter-branch -d "$TRASHDIR/dfoo" \
77			--tree-filter "echo changed >one.t" &&
78		echo changed >expect &&
79		git cat-file blob HEAD:one.t >actual &&
80		test_cmp expect actual &&
81		test_cmp one.t actual
82	)
83'
84
85test_expect_success 'Fail if commit filter fails' '
86	test_must_fail git filter-branch -f --commit-filter "exit 1" HEAD
87'
88
89test_expect_success 'rewrite, renaming a specific file' '
90	git filter-branch -f --tree-filter "mv D.t doh || :" HEAD
91'
92
93test_expect_success 'test that the file was renamed' '
94	test D = "$(git show HEAD:doh --)" &&
95	! test -f D.t &&
96	test -f doh &&
97	test D = "$(cat doh)"
98'
99
100test_expect_success 'rewrite, renaming a specific directory' '
101	git filter-branch -f --tree-filter "mv dir diroh || :" HEAD
102'
103
104test_expect_success 'test that the directory was renamed' '
105	test dir/D = "$(git show HEAD:diroh/D.t --)" &&
106	! test -d dir &&
107	test -d diroh &&
108	! test -d diroh/dir &&
109	test -f diroh/D.t &&
110	test dir/D = "$(cat diroh/D.t)"
111'
112
113V=$(git rev-parse HEAD)
114
115test_expect_success 'populate --state-branch' '
116	git filter-branch --state-branch state -f --tree-filter "touch file || :" HEAD
117'
118
119W=$(git rev-parse HEAD)
120
121test_expect_success 'using --state-branch to skip already rewritten commits' '
122	test_when_finished git reset --hard $V &&
123	git reset --hard $V &&
124	git filter-branch --state-branch state -f --tree-filter "touch file || :" HEAD &&
125	test_cmp_rev $W HEAD
126'
127
128git tag oldD HEAD~4
129test_expect_success 'rewrite one branch, keeping a side branch' '
130	git branch modD oldD &&
131	git filter-branch -f --tree-filter "mv B.t boh || :" D..modD
132'
133
134test_expect_success 'common ancestor is still common (unchanged)' '
135	test "$(git merge-base modD D)" = "$(git rev-parse B)"
136'
137
138test_expect_success 'filter subdirectory only' '
139	mkdir subdir &&
140	touch subdir/new &&
141	git add subdir/new &&
142	test_tick &&
143	git commit -m "subdir" &&
144	echo H > A.t &&
145	test_tick &&
146	git commit -m "not subdir" A.t &&
147	echo A > subdir/new &&
148	test_tick &&
149	git commit -m "again subdir" subdir/new &&
150	git rm A.t &&
151	test_tick &&
152	git commit -m "again not subdir" &&
153	git branch sub &&
154	git branch sub-earlier HEAD~2 &&
155	git filter-branch -f --subdirectory-filter subdir \
156		refs/heads/sub refs/heads/sub-earlier
157'
158
159test_expect_success 'subdirectory filter result looks okay' '
160	test 2 = $(git rev-list sub | wc -l) &&
161	git show sub:new &&
162	test_must_fail git show sub:subdir &&
163	git show sub-earlier:new &&
164	test_must_fail git show sub-earlier:subdir
165'
166
167test_expect_success 'more setup' '
168	git checkout main &&
169	mkdir subdir &&
170	echo A > subdir/new &&
171	git add subdir/new &&
172	test_tick &&
173	git commit -m "subdir on main" subdir/new &&
174	git rm A.t &&
175	test_tick &&
176	git commit -m "again subdir on main" &&
177	git merge branch
178'
179
180test_expect_success 'use index-filter to move into a subdirectory' '
181	git branch directorymoved &&
182	git filter-branch -f --index-filter \
183		 "git ls-files -s | sed \"s-	-&newsubdir/-\" |
184	          GIT_INDEX_FILE=\$GIT_INDEX_FILE.new \
185			git update-index --index-info &&
186		  mv \"\$GIT_INDEX_FILE.new\" \"\$GIT_INDEX_FILE\"" directorymoved &&
187	git diff --exit-code HEAD directorymoved:newsubdir
188'
189
190test_expect_success 'stops when msg filter fails' '
191	old=$(git rev-parse HEAD) &&
192	test_must_fail git filter-branch -f --msg-filter false HEAD &&
193	test $old = $(git rev-parse HEAD) &&
194	rm -rf .git-rewrite
195'
196
197test_expect_success 'author information is preserved' '
198	: > i &&
199	git add i &&
200	test_tick &&
201	GIT_AUTHOR_NAME="B V Uips" git commit -m bvuips &&
202	git branch preserved-author &&
203	(sane_unset GIT_AUTHOR_NAME &&
204	 git filter-branch -f --msg-filter "cat; \
205			test \$GIT_COMMIT != $(git rev-parse main) || \
206			echo Hallo" \
207		preserved-author) &&
208	git rev-list --author="B V Uips" preserved-author >actual &&
209	test_line_count = 1 actual
210'
211
212test_expect_success "remove a certain author's commits" '
213	echo i > i &&
214	test_tick &&
215	git commit -m i i &&
216	git branch removed-author &&
217	git filter-branch -f --commit-filter "\
218		if [ \"\$GIT_AUTHOR_NAME\" = \"B V Uips\" ];\
219		then\
220			skip_commit \"\$@\";
221		else\
222			git commit-tree \"\$@\";\
223		fi" removed-author &&
224	cnt1=$(git rev-list main | wc -l) &&
225	cnt2=$(git rev-list removed-author | wc -l) &&
226	test $cnt1 -eq $(($cnt2 + 1)) &&
227	git rev-list --author="B V Uips" removed-author >actual &&
228	test_line_count = 0 actual
229'
230
231test_expect_success 'barf on invalid name' '
232	test_must_fail git filter-branch -f main xy-problem &&
233	test_must_fail git filter-branch -f HEAD^
234'
235
236test_expect_success '"map" works in commit filter' '
237	git filter-branch -f --commit-filter "\
238		parent=\$(git rev-parse \$GIT_COMMIT^) &&
239		mapped=\$(map \$parent) &&
240		actual=\$(echo \"\$@\" | sed \"s/^.*-p //\") &&
241		test \$mapped = \$actual &&
242		git commit-tree \"\$@\";" main~2..main &&
243	git rev-parse --verify main
244'
245
246test_expect_success 'Name needing quotes' '
247
248	git checkout -b rerere A &&
249	mkdir foo &&
250	name="れれれ" &&
251	>foo/$name &&
252	git add foo &&
253	git commit -m "Adding a file" &&
254	git filter-branch --tree-filter "rm -fr foo" &&
255	test_must_fail git ls-files --error-unmatch "foo/$name" &&
256	test $(git rev-parse --verify rerere) != $(git rev-parse --verify A)
257
258'
259
260test_expect_success 'Subdirectory filter with disappearing trees' '
261	git reset --hard &&
262	git checkout main &&
263
264	mkdir foo &&
265	touch foo/bar &&
266	git add foo &&
267	test_tick &&
268	git commit -m "Adding foo" &&
269
270	git rm -r foo &&
271	test_tick &&
272	git commit -m "Removing foo" &&
273
274	mkdir foo &&
275	touch foo/bar &&
276	git add foo &&
277	test_tick &&
278	git commit -m "Re-adding foo" &&
279
280	git filter-branch -f --subdirectory-filter foo &&
281	git rev-list main >actual &&
282	test_line_count = 3 actual
283'
284
285test_expect_success 'Tag name filtering retains tag message' '
286	git tag -m atag T &&
287	git cat-file tag T > expect &&
288	git filter-branch -f --tag-name-filter cat &&
289	git cat-file tag T > actual &&
290	test_cmp expect actual
291'
292
293faux_gpg_tag='object XXXXXX
294type commit
295tag S
296tagger T A Gger <tagger@example.com> 1206026339 -0500
297
298This is a faux gpg signed tag.
299-----BEGIN PGP SIGNATURE-----
300Version: FauxGPG v0.0.0 (FAUX/Linux)
301
302gdsfoewhxu/6l06f1kxyxhKdZkrcbaiOMtkJUA9ITAc1mlamh0ooasxkH1XwMbYQ
303acmwXaWET20H0GeAGP+7vow=
304=agpO
305-----END PGP SIGNATURE-----
306'
307test_expect_success 'Tag name filtering strips gpg signature' '
308	sha1=$(git rev-parse HEAD) &&
309	sha1t=$(echo "$faux_gpg_tag" | sed -e s/XXXXXX/$sha1/ | git mktag) &&
310	git update-ref "refs/tags/S" "$sha1t" &&
311	echo "$faux_gpg_tag" | sed -e s/XXXXXX/$sha1/ | head -n 6 > expect &&
312	git filter-branch -f --tag-name-filter cat &&
313	git cat-file tag S > actual &&
314	test_cmp expect actual
315'
316
317test_expect_success GPG 'Filtering retains message of gpg signed commit' '
318	mkdir gpg &&
319	touch gpg/foo &&
320	git add gpg &&
321	test_tick &&
322	git commit -S -m "Adding gpg" &&
323
324	git log -1 --format="%s" > expect &&
325	git filter-branch -f --msg-filter "cat" &&
326	git log -1 --format="%s" > actual &&
327	test_cmp expect actual
328'
329
330test_expect_success 'Tag name filtering allows slashes in tag names' '
331	git tag -m tag-with-slash X/1 &&
332	git cat-file tag X/1 | sed -e s,X/1,X/2, > expect &&
333	git filter-branch -f --tag-name-filter "echo X/2" &&
334	git cat-file tag X/2 > actual &&
335	test_cmp expect actual
336'
337test_expect_success 'setup --prune-empty comparisons' '
338	git checkout --orphan main-no-a &&
339	git rm -rf . &&
340	unset test_tick &&
341	test_tick &&
342	GIT_COMMITTER_DATE="@0 +0000" GIT_AUTHOR_DATE="@0 +0000" &&
343	test_commit --notick B B.t B Bx &&
344	git checkout -b branch-no-a Bx &&
345	test_commit D D.t D Dx &&
346	mkdir dir &&
347	test_commit dir/D dir/D.t dir/D dir/Dx &&
348	test_commit E E.t E Ex &&
349	git checkout main-no-a &&
350	test_commit C C.t C Cx &&
351	git checkout branch-no-a &&
352	git merge Cx -m "Merge tag '\''C'\'' into branch" &&
353	git tag Fx &&
354	test_commit G G.t G Gx &&
355	test_commit H H.t H Hx &&
356	git checkout branch
357'
358
359test_expect_success 'Prune empty commits' '
360	git rev-list HEAD > expect &&
361	test_commit to_remove &&
362	git filter-branch -f --index-filter "git update-index --remove to_remove.t" --prune-empty HEAD &&
363	git rev-list HEAD > actual &&
364	test_cmp expect actual
365'
366
367test_expect_success 'prune empty collapsed merges' '
368	test_config merge.ff false &&
369	git rev-list HEAD >expect &&
370	test_commit to_remove_2 &&
371	git reset --hard HEAD^ &&
372	test_merge non-ff to_remove_2 &&
373	git filter-branch -f --index-filter "git update-index --remove to_remove_2.t" --prune-empty HEAD &&
374	git rev-list HEAD >actual &&
375	test_cmp expect actual
376'
377
378test_expect_success 'prune empty works even without index/tree filters' '
379	git rev-list HEAD >expect &&
380	git commit --allow-empty -m empty &&
381	git filter-branch -f --prune-empty HEAD &&
382	git rev-list HEAD >actual &&
383	test_cmp expect actual
384'
385
386test_expect_success '--prune-empty is able to prune root commit' '
387	git rev-list branch-no-a >expect &&
388	git branch testing H &&
389	git filter-branch -f --prune-empty --index-filter "git update-index --remove A.t" testing &&
390	git rev-list testing >actual &&
391	git branch -D testing &&
392	test_cmp expect actual
393'
394
395test_expect_success '--prune-empty is able to prune entire branch' '
396	git branch prune-entire B &&
397	git filter-branch -f --prune-empty --index-filter "git update-index --remove A.t B.t" prune-entire &&
398	test_must_fail git rev-parse refs/heads/prune-entire &&
399	if test_have_prereq REFFILES
400	then
401		test_must_fail git reflog exists refs/heads/prune-entire
402	fi
403'
404
405test_expect_success '--remap-to-ancestor with filename filters' '
406	git checkout main &&
407	git reset --hard A &&
408	test_commit add-foo foo 1 &&
409	git branch moved-foo &&
410	test_commit add-bar bar a &&
411	git branch invariant &&
412	orig_invariant=$(git rev-parse invariant) &&
413	git branch moved-bar &&
414	test_commit change-foo foo 2 &&
415	git filter-branch -f --remap-to-ancestor \
416		moved-foo moved-bar A..main \
417		-- -- foo &&
418	test $(git rev-parse moved-foo) = $(git rev-parse moved-bar) &&
419	test $(git rev-parse moved-foo) = $(git rev-parse main^) &&
420	test $orig_invariant = $(git rev-parse invariant)
421'
422
423test_expect_success 'automatic remapping to ancestor with filename filters' '
424	git checkout main &&
425	git reset --hard A &&
426	test_commit add-foo2 foo 1 &&
427	git branch moved-foo2 &&
428	test_commit add-bar2 bar a &&
429	git branch invariant2 &&
430	orig_invariant=$(git rev-parse invariant2) &&
431	git branch moved-bar2 &&
432	test_commit change-foo2 foo 2 &&
433	git filter-branch -f \
434		moved-foo2 moved-bar2 A..main \
435		-- -- foo &&
436	test $(git rev-parse moved-foo2) = $(git rev-parse moved-bar2) &&
437	test $(git rev-parse moved-foo2) = $(git rev-parse main^) &&
438	test $orig_invariant = $(git rev-parse invariant2)
439'
440
441test_expect_success 'setup submodule' '
442	rm -fr ?* .git &&
443	git init &&
444	test_commit file &&
445	mkdir submod &&
446	submodurl="$PWD/submod" &&
447	( cd submod &&
448	  git init &&
449	  test_commit file-in-submod ) &&
450	git submodule add "$submodurl" &&
451	git commit -m "added submodule" &&
452	test_commit add-file &&
453	( cd submod && test_commit add-in-submodule ) &&
454	git add submod &&
455	git commit -m "changed submodule" &&
456	git branch original HEAD
457'
458
459orig_head=$(git show-ref --hash --head HEAD)
460
461test_expect_success 'rewrite submodule with another content' '
462	git filter-branch --tree-filter "test -d submod && {
463					 rm -rf submod &&
464					 git rm -rf --quiet submod &&
465					 mkdir submod &&
466					 : > submod/file
467					 } || :" HEAD &&
468	test $orig_head != $(git show-ref --hash --head HEAD)
469'
470
471test_expect_success 'replace submodule revision' '
472	invalid=$(test_oid numeric) &&
473	git reset --hard original &&
474	git filter-branch -f --tree-filter \
475	    "if git ls-files --error-unmatch -- submod > /dev/null 2>&1
476	     then git update-index --cacheinfo 160000 $invalid submod
477	     fi" HEAD &&
478	test $orig_head != $(git show-ref --hash --head HEAD)
479'
480
481test_expect_success 'filter commit message without trailing newline' '
482	git reset --hard original &&
483	commit=$(printf "no newline" | git commit-tree HEAD^{tree}) &&
484	git update-ref refs/heads/no-newline $commit &&
485	git filter-branch -f refs/heads/no-newline &&
486	echo $commit >expect &&
487	git rev-parse refs/heads/no-newline >actual &&
488	test_cmp expect actual
489'
490
491test_expect_success 'tree-filter deals with object name vs pathname ambiguity' '
492	test_when_finished "git reset --hard original" &&
493	ambiguous=$(git rev-list -1 HEAD) &&
494	git filter-branch --tree-filter "mv file.t $ambiguous" HEAD^.. &&
495	git show HEAD:$ambiguous
496'
497
498test_expect_success 'rewrite repository including refs that point at non-commit object' '
499	test_when_finished "git reset --hard original" &&
500	tree=$(git rev-parse HEAD^{tree}) &&
501	test_when_finished "git replace -d $tree" &&
502	echo A >new &&
503	git add new &&
504	new_tree=$(git write-tree) &&
505	git replace $tree $new_tree &&
506	git tag -a -m "tag to a tree" treetag $new_tree &&
507	git reset --hard HEAD &&
508	git filter-branch -f -- --all >filter-output 2>&1 &&
509	! fgrep fatal filter-output
510'
511
512test_expect_success 'filter-branch handles ref deletion' '
513	git switch --orphan empty-commit &&
514	git commit --allow-empty -m "empty commit" &&
515	git tag empty &&
516	git branch to-delete &&
517	git filter-branch -f --prune-empty to-delete >out 2>&1 &&
518	grep "to-delete.*was deleted" out &&
519	test_must_fail git rev-parse --verify to-delete
520'
521
522test_expect_success 'filter-branch handles ref rewrite' '
523	git checkout empty &&
524	test_commit to-drop &&
525	git branch rewrite &&
526	git filter-branch -f \
527		--index-filter "git rm --ignore-unmatch --cached to-drop.t" \
528		 rewrite >out 2>&1 &&
529	grep "rewrite.*was rewritten" out &&
530	! grep -i warning out &&
531	git diff-tree empty rewrite
532'
533
534test_expect_success 'filter-branch handles ancestor rewrite' '
535	test_commit to-exclude &&
536	git branch ancestor &&
537	git filter-branch -f ancestor -- :^to-exclude.t >out 2>&1 &&
538	grep "ancestor.*was rewritten" out &&
539	! grep -i warning out &&
540	git diff-tree HEAD^ ancestor
541'
542
543test_done
544