1#!/bin/sh
2
3test_description='compare & swap push force/delete safety'
4
5GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main
6export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME
7
8. ./test-lib.sh
9
10setup_srcdst_basic () {
11	rm -fr src dst &&
12	git clone --no-local . src &&
13	git clone --no-local src dst &&
14	(
15		cd src && git checkout HEAD^0
16	)
17}
18
19# For tests with "--force-if-includes".
20setup_src_dup_dst () {
21	rm -fr src dup dst &&
22	git init --bare dst &&
23	git clone --no-local dst src &&
24	git clone --no-local dst dup
25	(
26		cd src &&
27		test_commit A &&
28		test_commit B &&
29		test_commit C &&
30		git push origin
31	) &&
32	(
33		cd dup &&
34		git fetch &&
35		git merge origin/main &&
36		git switch -c branch main~2 &&
37		test_commit D &&
38		test_commit E &&
39		git push origin --all
40	) &&
41	(
42		cd src &&
43		git switch main &&
44		git fetch --all &&
45		git branch branch --track origin/branch &&
46		git rebase origin/main
47	) &&
48	(
49		cd dup &&
50		git switch main &&
51		test_commit F &&
52		test_commit G &&
53		git switch branch &&
54		test_commit H &&
55		git push origin --all
56	)
57}
58
59test_expect_success setup '
60	# create template repository
61	test_commit A &&
62	test_commit B &&
63	test_commit C
64'
65
66test_expect_success 'push to update (protected)' '
67	setup_srcdst_basic &&
68	(
69		cd dst &&
70		test_commit D &&
71		test_must_fail git push --force-with-lease=main:main origin main 2>err &&
72		grep "stale info" err
73	) &&
74	git ls-remote . refs/heads/main >expect &&
75	git ls-remote src refs/heads/main >actual &&
76	test_cmp expect actual
77'
78
79test_expect_success 'push to update (protected, forced)' '
80	setup_srcdst_basic &&
81	(
82		cd dst &&
83		test_commit D &&
84		git push --force --force-with-lease=main:main origin main 2>err &&
85		grep "forced update" err
86	) &&
87	git ls-remote dst refs/heads/main >expect &&
88	git ls-remote src refs/heads/main >actual &&
89	test_cmp expect actual
90'
91
92test_expect_success 'push to update (protected, tracking)' '
93	setup_srcdst_basic &&
94	(
95		cd src &&
96		git checkout main &&
97		test_commit D &&
98		git checkout HEAD^0
99	) &&
100	git ls-remote src refs/heads/main >expect &&
101	(
102		cd dst &&
103		test_commit E &&
104		git ls-remote . refs/remotes/origin/main >expect &&
105		test_must_fail git push --force-with-lease=main origin main &&
106		git ls-remote . refs/remotes/origin/main >actual &&
107		test_cmp expect actual
108	) &&
109	git ls-remote src refs/heads/main >actual &&
110	test_cmp expect actual
111'
112
113test_expect_success 'push to update (protected, tracking, forced)' '
114	setup_srcdst_basic &&
115	(
116		cd src &&
117		git checkout main &&
118		test_commit D &&
119		git checkout HEAD^0
120	) &&
121	(
122		cd dst &&
123		test_commit E &&
124		git ls-remote . refs/remotes/origin/main >expect &&
125		git push --force --force-with-lease=main origin main
126	) &&
127	git ls-remote dst refs/heads/main >expect &&
128	git ls-remote src refs/heads/main >actual &&
129	test_cmp expect actual
130'
131
132test_expect_success 'push to update (allowed)' '
133	setup_srcdst_basic &&
134	(
135		cd dst &&
136		test_commit D &&
137		git push --force-with-lease=main:main^ origin main
138	) &&
139	git ls-remote dst refs/heads/main >expect &&
140	git ls-remote src refs/heads/main >actual &&
141	test_cmp expect actual
142'
143
144test_expect_success 'push to update (allowed, tracking)' '
145	setup_srcdst_basic &&
146	(
147		cd dst &&
148		test_commit D &&
149		git push --force-with-lease=main origin main 2>err &&
150		! grep "forced update" err
151	) &&
152	git ls-remote dst refs/heads/main >expect &&
153	git ls-remote src refs/heads/main >actual &&
154	test_cmp expect actual
155'
156
157test_expect_success 'push to update (allowed even though no-ff)' '
158	setup_srcdst_basic &&
159	(
160		cd dst &&
161		git reset --hard HEAD^ &&
162		test_commit D &&
163		git push --force-with-lease=main origin main 2>err &&
164		grep "forced update" err
165	) &&
166	git ls-remote dst refs/heads/main >expect &&
167	git ls-remote src refs/heads/main >actual &&
168	test_cmp expect actual
169'
170
171test_expect_success 'push to delete (protected)' '
172	setup_srcdst_basic &&
173	git ls-remote src refs/heads/main >expect &&
174	(
175		cd dst &&
176		test_must_fail git push --force-with-lease=main:main^ origin :main
177	) &&
178	git ls-remote src refs/heads/main >actual &&
179	test_cmp expect actual
180'
181
182test_expect_success 'push to delete (protected, forced)' '
183	setup_srcdst_basic &&
184	(
185		cd dst &&
186		git push --force --force-with-lease=main:main^ origin :main
187	) &&
188	git ls-remote src refs/heads/main >actual &&
189	test_must_be_empty actual
190'
191
192test_expect_success 'push to delete (allowed)' '
193	setup_srcdst_basic &&
194	(
195		cd dst &&
196		git push --force-with-lease=main origin :main 2>err &&
197		grep deleted err
198	) &&
199	git ls-remote src refs/heads/main >actual &&
200	test_must_be_empty actual
201'
202
203test_expect_success 'cover everything with default force-with-lease (protected)' '
204	setup_srcdst_basic &&
205	(
206		cd src &&
207		git branch nain main^
208	) &&
209	git ls-remote src refs/heads/\* >expect &&
210	(
211		cd dst &&
212		test_must_fail git push --force-with-lease origin main main:nain
213	) &&
214	git ls-remote src refs/heads/\* >actual &&
215	test_cmp expect actual
216'
217
218test_expect_success 'cover everything with default force-with-lease (allowed)' '
219	setup_srcdst_basic &&
220	(
221		cd src &&
222		git branch nain main^
223	) &&
224	(
225		cd dst &&
226		git fetch &&
227		git push --force-with-lease origin main main:nain
228	) &&
229	git ls-remote dst refs/heads/main |
230	sed -e "s/main/nain/" >expect &&
231	git ls-remote src refs/heads/nain >actual &&
232	test_cmp expect actual
233'
234
235test_expect_success 'new branch covered by force-with-lease' '
236	setup_srcdst_basic &&
237	(
238		cd dst &&
239		git branch branch main &&
240		git push --force-with-lease=branch origin branch
241	) &&
242	git ls-remote dst refs/heads/branch >expect &&
243	git ls-remote src refs/heads/branch >actual &&
244	test_cmp expect actual
245'
246
247test_expect_success 'new branch covered by force-with-lease (explicit)' '
248	setup_srcdst_basic &&
249	(
250		cd dst &&
251		git branch branch main &&
252		git push --force-with-lease=branch: origin branch
253	) &&
254	git ls-remote dst refs/heads/branch >expect &&
255	git ls-remote src refs/heads/branch >actual &&
256	test_cmp expect actual
257'
258
259test_expect_success 'new branch already exists' '
260	setup_srcdst_basic &&
261	(
262		cd src &&
263		git checkout -b branch main &&
264		test_commit F
265	) &&
266	(
267		cd dst &&
268		git branch branch main &&
269		test_must_fail git push --force-with-lease=branch: origin branch
270	)
271'
272
273test_expect_success 'background updates of REMOTE can be mitigated with a non-updated REMOTE-push' '
274	rm -rf src dst &&
275	git init --bare src.bare &&
276	test_when_finished "rm -rf src.bare" &&
277	git clone --no-local src.bare dst &&
278	test_when_finished "rm -rf dst" &&
279	(
280		cd dst &&
281		test_commit G &&
282		git remote add origin-push ../src.bare &&
283		git push origin-push main:main
284	) &&
285	git clone --no-local src.bare dst2 &&
286	test_when_finished "rm -rf dst2" &&
287	(
288		cd dst2 &&
289		test_commit H &&
290		git push
291	) &&
292	(
293		cd dst &&
294		test_commit I &&
295		git fetch origin &&
296		test_must_fail git push --force-with-lease origin-push &&
297		git fetch origin-push &&
298		git push --force-with-lease origin-push
299	)
300'
301
302test_expect_success 'background updates to remote can be mitigated with "--force-if-includes"' '
303	setup_src_dup_dst &&
304	test_when_finished "rm -fr dst src dup" &&
305	git ls-remote dst refs/heads/main >expect.main &&
306	git ls-remote dst refs/heads/branch >expect.branch &&
307	(
308		cd src &&
309		git switch branch &&
310		test_commit I &&
311		git switch main &&
312		test_commit J &&
313		git fetch --all &&
314		test_must_fail git push --force-with-lease --force-if-includes --all
315	) &&
316	git ls-remote dst refs/heads/main >actual.main &&
317	git ls-remote dst refs/heads/branch >actual.branch &&
318	test_cmp expect.main actual.main &&
319	test_cmp expect.branch actual.branch
320'
321
322test_expect_success 'background updates to remote can be mitigated with "push.useForceIfIncludes"' '
323	setup_src_dup_dst &&
324	test_when_finished "rm -fr dst src dup" &&
325	git ls-remote dst refs/heads/main >expect.main &&
326	(
327		cd src &&
328		git switch branch &&
329		test_commit I &&
330		git switch main &&
331		test_commit J &&
332		git fetch --all &&
333		git config --local push.useForceIfIncludes true &&
334		test_must_fail git push --force-with-lease=main origin main
335	) &&
336	git ls-remote dst refs/heads/main >actual.main &&
337	test_cmp expect.main actual.main
338'
339
340test_expect_success '"--force-if-includes" should be disabled for --force-with-lease="<refname>:<expect>"' '
341	setup_src_dup_dst &&
342	test_when_finished "rm -fr dst src dup" &&
343	git ls-remote dst refs/heads/main >expect.main &&
344	(
345		cd src &&
346		git switch branch &&
347		test_commit I &&
348		git switch main &&
349		test_commit J &&
350		remote_head="$(git rev-parse refs/remotes/origin/main)" &&
351		git fetch --all &&
352		test_must_fail git push --force-if-includes --force-with-lease="main:$remote_head" 2>err &&
353		grep "stale info" err
354	) &&
355	git ls-remote dst refs/heads/main >actual.main &&
356	test_cmp expect.main actual.main
357'
358
359test_expect_success '"--force-if-includes" should allow forced update after a rebase ("pull --rebase")' '
360	setup_src_dup_dst &&
361	test_when_finished "rm -fr dst src dup" &&
362	(
363		cd src &&
364		git switch branch &&
365		test_commit I &&
366		git switch main &&
367		test_commit J &&
368		git pull --rebase origin main &&
369		git push --force-if-includes --force-with-lease="main"
370	)
371'
372
373test_expect_success '"--force-if-includes" should allow forced update after a rebase ("pull --rebase", local rebase)' '
374	setup_src_dup_dst &&
375	test_when_finished "rm -fr dst src dup" &&
376	(
377		cd src &&
378		git switch branch &&
379		test_commit I &&
380		git switch main &&
381		test_commit J &&
382		git pull --rebase origin main &&
383		git rebase --onto HEAD~4 HEAD~1 &&
384		git push --force-if-includes --force-with-lease="main"
385	)
386'
387
388test_expect_success '"--force-if-includes" should allow deletes' '
389	setup_src_dup_dst &&
390	test_when_finished "rm -fr dst src dup" &&
391	(
392		cd src &&
393		git switch branch &&
394		git pull --rebase origin branch &&
395		git push --force-if-includes --force-with-lease="branch" origin :branch
396	)
397'
398
399test_done
400