View Javadoc
1   /*
2    * Copyright (C) 2022 Thomas Wolf <twolf@apache.org> and others
3    *
4    * This program and the accompanying materials are made available under the
5    * terms of the Eclipse Distribution License v. 1.0 which is available at
6    * https://www.eclipse.org/org/documents/edl-v10.php.
7    *
8    * SPDX-License-Identifier: BSD-3-Clause
9    */
10  package org.eclipse.jgit.merge;
11  
12  import static org.junit.Assert.assertEquals;
13  import static org.junit.Assert.assertFalse;
14  import static org.junit.Assert.assertTrue;
15  import static org.junit.Assume.assumeTrue;
16  
17  import java.io.File;
18  import java.io.IOException;
19  import java.nio.file.Files;
20  import java.nio.file.LinkOption;
21  
22  import org.eclipse.jgit.api.Git;
23  import org.eclipse.jgit.api.MergeResult;
24  import org.eclipse.jgit.api.MergeResult.MergeStatus;
25  import org.eclipse.jgit.api.ResetCommand.ResetType;
26  import org.eclipse.jgit.junit.RepositoryTestCase;
27  import org.eclipse.jgit.junit.TestRepository;
28  import org.eclipse.jgit.lib.ConfigConstants;
29  import org.eclipse.jgit.lib.Repository;
30  import org.eclipse.jgit.lib.StoredConfig;
31  import org.eclipse.jgit.revwalk.RevCommit;
32  import org.junit.Test;
33  import org.junit.runner.RunWith;
34  import org.junit.runners.Parameterized;
35  import org.junit.runners.Parameterized.Parameter;
36  import org.junit.runners.Parameterized.Parameters;
37  
38  /**
39   * Tests for merges involving symlinks.
40   */
41  @RunWith(Parameterized.class)
42  public class SymlinkMergeTest extends RepositoryTestCase {
43  
44  	@Parameters(name = "target={0}, core.symlinks={1}")
45  	public static Object[][] parameters() {
46  		return new Object[][] {
47  			{ Target.NONE, Boolean.TRUE },
48  			{ Target.FILE, Boolean.TRUE },
49  			{ Target.DIRECTORY, Boolean.TRUE },
50  			{ Target.NONE, Boolean.FALSE },
51  			{ Target.FILE, Boolean.FALSE },
52  			{ Target.DIRECTORY, Boolean.FALSE },
53  		};
54  	}
55  
56  	public enum Target {
57  		NONE, FILE, DIRECTORY
58  	}
59  
60  	@Parameter(0)
61  	public Target target;
62  
63  	@Parameter(1)
64  	public boolean useSymLinks;
65  
66  	private void setTargets() throws IOException {
67  		switch (target) {
68  		case DIRECTORY:
69  			assertTrue(new File(trash, "target").mkdir());
70  			assertTrue(new File(trash, "target1").mkdir());
71  			assertTrue(new File(trash, "target2").mkdir());
72  			break;
73  		case FILE:
74  			writeTrashFile("target", "t");
75  			writeTrashFile("target1", "t1");
76  			writeTrashFile("target2", "t2");
77  			break;
78  		default:
79  			break;
80  		}
81  	}
82  
83  	private void checkTargets() throws IOException {
84  		File t = new File(trash, "target");
85  		File t1 = new File(trash, "target1");
86  		File t2 = new File(trash, "target2");
87  		switch (target) {
88  		case DIRECTORY:
89  			assertTrue(t.isDirectory());
90  			assertTrue(t1.isDirectory());
91  			assertTrue(t2.isDirectory());
92  			break;
93  		case FILE:
94  			checkFile(t, "t");
95  			checkFile(t1, "t1");
96  			checkFile(t2, "t2");
97  			break;
98  		default:
99  			assertFalse(Files.exists(t.toPath(), LinkOption.NOFOLLOW_LINKS));
100 			assertFalse(Files.exists(t1.toPath(), LinkOption.NOFOLLOW_LINKS));
101 			assertFalse(Files.exists(t2.toPath(), LinkOption.NOFOLLOW_LINKS));
102 			break;
103 		}
104 	}
105 
106 	private void assertSymLink(File link, String content) throws Exception {
107 		if (useSymLinks) {
108 			assertTrue(Files.isSymbolicLink(link.toPath()));
109 			assertEquals(content, db.getFS().readSymLink(link));
110 		} else {
111 			assertFalse(Files.isSymbolicLink(link.toPath()));
112 			assertTrue(link.isFile());
113 			checkFile(link, content);
114 		}
115 	}
116 
117 	// Link/link conflict: C git records the conflict but leaves the link in the
118 	// working tree unchanged.
119 
120 	@Test
121 	public void mergeWithSymlinkConflict() throws Exception {
122 		assumeTrue(db.getFS().supportsSymlinks() || !useSymLinks);
123 		StoredConfig config = db.getConfig();
124 		config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
125 				ConfigConstants.CONFIG_KEY_SYMLINKS, useSymLinks);
126 		config.save();
127 		try (TestRepository<Repository> repo = new TestRepository<>(db)) {
128 			db.incrementOpen();
129 			// Create the links directly in the git repo, then use a hard reset
130 			// to get them into the workspace. This enables us to run these
131 			// tests also with core.symLinks = false.
132 			RevCommit base = repo
133 					.commit(repo.tree(repo.link("link", repo.blob("target"))));
134 			RevCommit side = repo.commit(
135 					repo.tree(repo.link("link", repo.blob("target1"))), base);
136 			RevCommit head = repo.commit(
137 					repo.tree(repo.link("link", repo.blob("target2"))), base);
138 			try (Git git = new Git(db)) {
139 				setTargets();
140 				git.reset().setMode(ResetType.HARD).setRef(head.name()).call();
141 				File link = new File(trash, "link");
142 				assertSymLink(link, "target2");
143 				MergeResult result = git.merge().include(side)
144 						.setMessage("merged").call();
145 				assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
146 				// Link should be unmodified
147 				assertSymLink(link, "target2");
148 				checkTargets();
149 				assertEquals("[link, mode:120000, stage:1, content:target]"
150 						+ "[link, mode:120000, stage:2, content:target2]"
151 						+ "[link, mode:120000, stage:3, content:target1]",
152 						indexState(CONTENT));
153 			}
154 		}
155 	}
156 
157 	// In file/link conflicts, C git never does a content merge. It records the
158 	// stages in the index, and always puts the file into the workspace.
159 
160 	@Test
161 	public void mergeWithFileSymlinkConflict() throws Exception {
162 		assumeTrue(db.getFS().supportsSymlinks() || !useSymLinks);
163 		StoredConfig config = db.getConfig();
164 		config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
165 				ConfigConstants.CONFIG_KEY_SYMLINKS, useSymLinks);
166 		config.save();
167 		try (TestRepository<Repository> repo = new TestRepository<>(db)) {
168 			db.incrementOpen();
169 			RevCommit base = repo.commit(repo.tree());
170 			RevCommit side = repo.commit(
171 					repo.tree(repo.link("link", repo.blob("target1"))), base);
172 			RevCommit head = repo.commit(
173 					repo.tree(repo.file("link", repo.blob("not a link"))),
174 					base);
175 			try (Git git = new Git(db)) {
176 				setTargets();
177 				git.reset().setMode(ResetType.HARD).setRef(head.name()).call();
178 				File link = new File(trash, "link");
179 				assertFalse(Files.isSymbolicLink(link.toPath()));
180 				checkFile(link, "not a link");
181 				MergeResult result = git.merge().include(side)
182 						.setMessage("merged").call();
183 				assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
184 				// File should be unmodified
185 				assertFalse(Files.isSymbolicLink(link.toPath()));
186 				checkFile(link, "not a link");
187 				checkTargets();
188 				assertEquals("[link, mode:100644, stage:2, content:not a link]"
189 						+ "[link, mode:120000, stage:3, content:target1]",
190 						indexState(CONTENT));
191 			}
192 		}
193 	}
194 
195 	@Test
196 	public void mergeWithSymlinkFileConflict() throws Exception {
197 		assumeTrue(db.getFS().supportsSymlinks() || !useSymLinks);
198 		StoredConfig config = db.getConfig();
199 		config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
200 				ConfigConstants.CONFIG_KEY_SYMLINKS, useSymLinks);
201 		config.save();
202 		try (TestRepository<Repository> repo = new TestRepository<>(db)) {
203 			db.incrementOpen();
204 			RevCommit base = repo.commit(repo.tree());
205 			RevCommit side = repo.commit(
206 					repo.tree(repo.file("link", repo.blob("not a link"))),
207 					base);
208 			RevCommit head = repo.commit(
209 					repo.tree(repo.link("link", repo.blob("target2"))), base);
210 			try (Git git = new Git(db)) {
211 				setTargets();
212 				git.reset().setMode(ResetType.HARD).setRef(head.name()).call();
213 				File link = new File(trash, "link");
214 				assertSymLink(link, "target2");
215 				MergeResult result = git.merge().include(side)
216 						.setMessage("merged").call();
217 				assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
218 				// Should now be a file!
219 				assertFalse(Files.isSymbolicLink(link.toPath()));
220 				checkFile(link, "not a link");
221 				checkTargets();
222 				assertEquals("[link, mode:120000, stage:2, content:target2]"
223 						+ "[link, mode:100644, stage:3, content:not a link]",
224 						indexState(CONTENT));
225 			}
226 		}
227 	}
228 
229 	// In Delete/modify conflicts with the non-deleted side a link, C git puts
230 	// the link into the working tree.
231 
232 	@Test
233 	public void mergeWithSymlinkDeleteModify() throws Exception {
234 		assumeTrue(db.getFS().supportsSymlinks() || !useSymLinks);
235 		StoredConfig config = db.getConfig();
236 		config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
237 				ConfigConstants.CONFIG_KEY_SYMLINKS, useSymLinks);
238 		config.save();
239 		try (TestRepository<Repository> repo = new TestRepository<>(db)) {
240 			db.incrementOpen();
241 			RevCommit base = repo
242 					.commit(repo.tree(repo.link("link", repo.blob("target"))));
243 			RevCommit side = repo.commit(
244 					repo.tree(repo.link("link", repo.blob("target1"))), base);
245 			RevCommit head = repo.commit(repo.tree(), base);
246 			try (Git git = new Git(db)) {
247 				setTargets();
248 				git.reset().setMode(ResetType.HARD).setRef(head.name()).call();
249 				File link = new File(trash, "link");
250 				assertFalse(
251 						Files.exists(link.toPath(), LinkOption.NOFOLLOW_LINKS));
252 				MergeResult result = git.merge().include(side)
253 						.setMessage("merged").call();
254 				assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
255 				// Link should have the content from side
256 				assertSymLink(link, "target1");
257 				checkTargets();
258 				assertEquals("[link, mode:120000, stage:1, content:target]"
259 						+ "[link, mode:120000, stage:3, content:target1]",
260 						indexState(CONTENT));
261 			}
262 		}
263 	}
264 
265 	@Test
266 	public void mergeWithSymlinkModifyDelete() throws Exception {
267 		assumeTrue(db.getFS().supportsSymlinks() || !useSymLinks);
268 		StoredConfig config = db.getConfig();
269 		config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
270 				ConfigConstants.CONFIG_KEY_SYMLINKS, useSymLinks);
271 		config.save();
272 		try (TestRepository<Repository> repo = new TestRepository<>(db)) {
273 			db.incrementOpen();
274 			RevCommit base = repo
275 					.commit(repo.tree(repo.link("link", repo.blob("target"))));
276 			RevCommit side = repo.commit(repo.tree(), base);
277 			RevCommit head = repo.commit(
278 					repo.tree(repo.link("link", repo.blob("target2"))), base);
279 			try (Git git = new Git(db)) {
280 				setTargets();
281 				git.reset().setMode(ResetType.HARD).setRef(head.name()).call();
282 				File link = new File(trash, "link");
283 				assertSymLink(link, "target2");
284 				MergeResult result = git.merge().include(side)
285 						.setMessage("merged").call();
286 				assertEquals(MergeStatus.CONFLICTING, result.getMergeStatus());
287 				// Link should be unmodified
288 				assertSymLink(link, "target2");
289 				checkTargets();
290 				assertEquals("[link, mode:120000, stage:1, content:target]"
291 						+ "[link, mode:120000, stage:2, content:target2]",
292 						indexState(CONTENT));
293 			}
294 		}
295 	}
296 }