View Javadoc
1   /*
2    * Copyright (C) 2022, Google Inc. 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  
11  package org.eclipse.jgit.patch;
12  
13  import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
14  import static org.junit.Assert.assertArrayEquals;
15  import static org.junit.Assert.assertEquals;
16  import static org.junit.Assert.assertFalse;
17  import static org.junit.Assert.assertNotNull;
18  import static org.junit.Assert.assertNull;
19  import static org.junit.Assert.assertTrue;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.io.OutputStream;
25  import java.nio.charset.StandardCharsets;
26  import java.nio.file.Files;
27  import org.eclipse.jgit.api.Git;
28  import org.eclipse.jgit.api.errors.PatchApplyException;
29  import org.eclipse.jgit.api.errors.PatchFormatException;
30  import org.eclipse.jgit.attributes.FilterCommand;
31  import org.eclipse.jgit.attributes.FilterCommandFactory;
32  import org.eclipse.jgit.attributes.FilterCommandRegistry;
33  import org.eclipse.jgit.junit.RepositoryTestCase;
34  import org.eclipse.jgit.junit.TestRepository;
35  import org.eclipse.jgit.lib.Config;
36  import org.eclipse.jgit.lib.ConfigConstants;
37  import org.eclipse.jgit.lib.ObjectId;
38  import org.eclipse.jgit.lib.ObjectInserter;
39  import org.eclipse.jgit.patch.PatchApplier.Result;
40  import org.eclipse.jgit.revwalk.RevCommit;
41  import org.eclipse.jgit.revwalk.RevTree;
42  import org.eclipse.jgit.revwalk.RevWalk;
43  import org.eclipse.jgit.treewalk.TreeWalk;
44  import org.eclipse.jgit.util.FS;
45  import org.eclipse.jgit.util.IO;
46  import org.junit.Test;
47  import org.junit.runner.RunWith;
48  import org.junit.runners.Suite;
49  
50  @RunWith(Suite.class)
51  @Suite.SuiteClasses({
52   		PatchApplierTest.WithWorktree. class, //
53  		PatchApplierTest.InCore.class, //
54  })
55  public class PatchApplierTest {
56  
57  	public abstract static class Base extends RepositoryTestCase {
58  
59  		protected String name;
60  
61  		/** data before patching. */
62  		protected byte[] preImage;
63  		/** expected data after patching. */
64  		protected byte[] postImage;
65  
66  		protected String expectedText;
67  		protected RevTree baseTip;
68  		public boolean inCore;
69  
70  		Base(boolean inCore) {
71  			this.inCore = inCore;
72  		}
73  
74  		protected void init(String aName, boolean preExists, boolean postExists)
75  				throws Exception {
76  			/* Patch and pre/postimage are read from data org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/diff/ */
77  			this.name = aName;
78  			if (postExists) {
79  				postImage = IO
80  						.readWholeStream(getTestResource(name + "_PostImage"), 0)
81  						.array();
82  				expectedText = new String(postImage, StandardCharsets.UTF_8);
83  			}
84  
85  			File f = new File(db.getWorkTree(), name);
86  			if (preExists) {
87  				preImage = IO
88  						.readWholeStream(getTestResource(name + "_PreImage"), 0)
89  						.array();
90  				try (Git git = new Git(db)) {
91  					Files.write(f.toPath(), preImage);
92  					git.add().addFilepattern(name).call();
93  				}
94  			}
95  			try (Git git = new Git(db)) {
96  				RevCommit base = git.commit().setMessage("PreImage").call();
97  				baseTip = base.getTree();
98  			}
99  		}
100 
101 		void init(final String aName) throws Exception {
102 			init(aName, true, true);
103 		}
104 
105 		protected Result applyPatch()
106 				throws PatchApplyException, PatchFormatException, IOException {
107 			InputStream patchStream = getTestResource(name + ".patch");
108 			if (inCore) {
109 				try (ObjectInserter oi = db.newObjectInserter()) {
110 					return new PatchApplier(db, baseTip, oi).applyPatch(patchStream);
111 				}
112 			}
113 			return new PatchApplier(db).applyPatch(patchStream);
114 		}
115 
116 		protected static InputStream getTestResource(String patchFile) {
117 			return PatchApplierTest.class.getClassLoader()
118 					.getResourceAsStream("org/eclipse/jgit/diff/" + patchFile);
119 		}
120 		void verifyChange(Result result, String aName) throws Exception {
121 			verifyChange(result, aName, true);
122 		}
123 
124 		protected void verifyContent(Result result, String path, boolean exists) throws Exception {
125 			if (inCore) {
126 				byte[] output = readBlob(result.getTreeId(), path);
127 				if (!exists)
128 					assertNull(output);
129 				else {
130 					assertNotNull(output);
131 					assertEquals(new String(output, StandardCharsets.UTF_8), expectedText);
132 				}
133 			} else {
134 				File f = new File(db.getWorkTree(), path);
135 				if (!exists)
136 					assertFalse(f.exists());
137 				else
138 					checkFile(f, expectedText);
139 			}
140 		}
141 
142 		void verifyChange(Result result, String aName, boolean exists) throws Exception {
143 			assertEquals(1, result.getPaths().size());
144 			verifyContent(result, aName, exists);
145 		}
146 
147 		protected byte[] readBlob(ObjectId treeish, String path) throws Exception {
148 			try (TestRepository<?> tr = new TestRepository<>(db);
149 					RevWalk rw = tr.getRevWalk()) {
150 				db.incrementOpen();
151 				RevTree tree = rw.parseTree(treeish);
152 				try (TreeWalk tw = TreeWalk.forPath(db,path,tree)){
153 					if (tw == null) {
154 						return null;
155 					}
156 					return tw.getObjectReader().open(tw.getObjectId(0), OBJ_BLOB).getBytes();
157 				}
158 			}
159 		}
160 
161 		protected void checkBinary(Result result, int numberOfFiles) throws Exception {
162 			assertEquals(numberOfFiles, result.getPaths().size());
163 			if (inCore) {
164 				assertArrayEquals(postImage, readBlob(result.getTreeId(), result.getPaths().get(0)));
165 			} else {
166 				File f = new File(db.getWorkTree(), name);
167 				assertArrayEquals(postImage, Files.readAllBytes(f.toPath()));
168 			}
169 		}
170 
171 		/* tests */
172 
173 		@Test
174 		public void testBinaryDelta() throws Exception {
175 			init("delta");
176 			checkBinary(applyPatch(), 1);
177 		}
178 
179 		@Test
180 		public void testBinaryLiteral() throws Exception {
181 			init("literal");
182 			checkBinary(applyPatch(), 1);
183 		}
184 
185 		@Test
186 		public void testBinaryLiteralAdd() throws Exception {
187 			init("literal_add", false, true);
188 			checkBinary(applyPatch(), 1);
189 		}
190 
191 		@Test
192 		public void testModifyM2() throws Exception {
193 			init("M2", true, true);
194 
195 			Result result = applyPatch();
196 
197 			if (!inCore && FS.DETECTED.supportsExecute()) {
198 				assertEquals(1, result.getPaths().size());
199 				File f = new File(db.getWorkTree(), result.getPaths().get(0));
200 				assertTrue(FS.DETECTED.canExecute(f));
201 			}
202 
203 			verifyChange(result, "M2");
204 		}
205 
206 		@Test
207 		public void testModifyM3() throws Exception {
208 			init("M3", true, true);
209 
210 			Result result = applyPatch();
211 
212 			verifyChange(result, "M3");
213 			if (!inCore && FS.DETECTED.supportsExecute()) {
214 				File f = new File(db.getWorkTree(), result.getPaths().get(0));
215 				assertFalse(FS.DETECTED.canExecute(f));
216 			}
217 		}
218 
219 		@Test
220 		public void testModifyX() throws Exception {
221 			init("X");
222 
223 			Result result = applyPatch();
224 			verifyChange(result, "X");
225 		}
226 
227 		@Test
228 		public void testModifyY() throws Exception {
229 			init("Y");
230 
231 			Result result = applyPatch();
232 
233 			verifyChange(result, "Y");
234 		}
235 
236 		@Test
237 		public void testModifyZ() throws Exception {
238 			init("Z");
239 
240 			Result result = applyPatch();
241 			verifyChange(result, "Z");
242 		}
243 
244 		@Test
245 		public void testNonASCII() throws Exception {
246 			init("NonASCII");
247 
248 			Result result = applyPatch();
249 			verifyChange(result, "NonASCII");
250 		}
251 
252 		@Test
253 		public void testNonASCII2() throws Exception {
254 			init("NonASCII2");
255 
256 			Result result = applyPatch();
257 			verifyChange(result, "NonASCII2");
258 		}
259 
260 		@Test
261 		public void testNonASCIIAdd() throws Exception {
262 			init("NonASCIIAdd");
263 
264 			Result result = applyPatch();
265 			verifyChange(result, "NonASCIIAdd");
266 		}
267 
268 		@Test
269 		public void testNonASCIIAdd2() throws Exception {
270 			init("NonASCIIAdd2", false, true);
271 
272 			Result result = applyPatch();
273 			verifyChange(result, "NonASCIIAdd2");
274 		}
275 
276 		@Test
277 		public void testNonASCIIDel() throws Exception {
278 			init("NonASCIIDel", true, false);
279 
280 			Result result = applyPatch();
281 			verifyChange(result, "NonASCIIDel", false);
282 			assertEquals("NonASCIIDel", result.getPaths().get(0));
283 		}
284 
285 		@Test
286 		public void testRenameNoHunks() throws Exception {
287 			init("RenameNoHunks", true, true);
288 
289 			Result result = applyPatch();
290 
291 			assertEquals(2, result.getPaths().size());
292 			assertTrue(result.getPaths().contains("RenameNoHunks"));
293 			assertTrue(result.getPaths().contains("nested/subdir/Renamed"));
294 
295 			verifyContent(result,"nested/subdir/Renamed", true);
296 		}
297 
298 		@Test
299 		public void testRenameWithHunks() throws Exception {
300 			init("RenameWithHunks", true, true);
301 
302 			Result result = applyPatch();
303 			assertEquals(2, result.getPaths().size());
304 			assertTrue(result.getPaths().contains("RenameWithHunks"));
305 			assertTrue(result.getPaths().contains("nested/subdir/Renamed"));
306 
307 			verifyContent(result,"nested/subdir/Renamed", true);
308 		}
309 
310 		@Test
311 		public void testCopyWithHunks() throws Exception {
312 			init("CopyWithHunks", true, true);
313 
314 			Result result = applyPatch();
315 			verifyChange(result, "CopyResult", true);
316 		}
317 
318 		@Test
319 		public void testShiftUp() throws Exception {
320 			init("ShiftUp");
321 
322 			Result result = applyPatch();
323 			verifyChange(result, "ShiftUp");
324 		}
325 
326 		@Test
327 		public void testShiftUp2() throws Exception {
328 			init("ShiftUp2");
329 
330 			Result result = applyPatch();
331 			verifyChange(result, "ShiftUp2");
332 		}
333 
334 		@Test
335 		public void testShiftDown() throws Exception {
336 			init("ShiftDown");
337 
338 			Result result = applyPatch();
339 			verifyChange(result, "ShiftDown");
340 		}
341 
342 		@Test
343 		public void testShiftDown2() throws Exception {
344 			init("ShiftDown2");
345 
346 			Result result = applyPatch();
347 			verifyChange(result, "ShiftDown2");
348 		}
349 	}
350 
351 	public static class InCore extends Base {
352 
353 		public InCore() {
354 			super(true);
355 		}
356 	}
357 
358 	public static class WithWorktree extends Base {
359 		public WithWorktree() { super(false); }
360 
361 		@Test
362 		public void testModifyNL1() throws Exception {
363 			init("NL1");
364 
365 			Result result = applyPatch();
366 			verifyChange(result, "NL1");
367 		}
368 
369 		@Test
370 		public void testCrLf() throws Exception {
371 			try {
372 				db.getConfig().setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
373 						ConfigConstants.CONFIG_KEY_AUTOCRLF, true);
374 				init("crlf", true, true);
375 
376 				Result result = applyPatch();
377 
378 				verifyChange(result, "crlf");
379 			} finally {
380 				db.getConfig().unset(ConfigConstants.CONFIG_CORE_SECTION, null,
381 						ConfigConstants.CONFIG_KEY_AUTOCRLF);
382 			}
383 		}
384 
385 		@Test
386 		public void testCrLfOff() throws Exception {
387 			try {
388 				db.getConfig().setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
389 						ConfigConstants.CONFIG_KEY_AUTOCRLF, false);
390 				init("crlf", true, true);
391 
392 				Result result = applyPatch();
393 
394 				verifyChange(result, "crlf");
395 			} finally {
396 				db.getConfig().unset(ConfigConstants.CONFIG_CORE_SECTION, null,
397 						ConfigConstants.CONFIG_KEY_AUTOCRLF);
398 			}
399 		}
400 
401 		@Test
402 		public void testCrLfEmptyCommitted() throws Exception {
403 			try {
404 				db.getConfig().setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
405 						ConfigConstants.CONFIG_KEY_AUTOCRLF, true);
406 				init("crlf3", true, true);
407 
408 				Result result = applyPatch();
409 
410 				verifyChange(result, "crlf3");
411 			} finally {
412 				db.getConfig().unset(ConfigConstants.CONFIG_CORE_SECTION, null,
413 						ConfigConstants.CONFIG_KEY_AUTOCRLF);
414 			}
415 		}
416 
417 		@Test
418 		public void testCrLfNewFile() throws Exception {
419 			try {
420 				db.getConfig().setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
421 						ConfigConstants.CONFIG_KEY_AUTOCRLF, true);
422 				init("crlf4", false, true);
423 
424 				Result result = applyPatch();
425 
426 				verifyChange(result, "crlf4");
427 			} finally {
428 				db.getConfig().unset(ConfigConstants.CONFIG_CORE_SECTION, null,
429 						ConfigConstants.CONFIG_KEY_AUTOCRLF);
430 			}
431 		}
432 
433 		@Test
434 		public void testPatchWithCrLf() throws Exception {
435 			try {
436 				db.getConfig().setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
437 						ConfigConstants.CONFIG_KEY_AUTOCRLF, false);
438 				init("crlf2", true, true);
439 
440 				Result result = applyPatch();
441 
442 				verifyChange(result, "crlf2");
443 			} finally {
444 				db.getConfig().unset(ConfigConstants.CONFIG_CORE_SECTION, null,
445 						ConfigConstants.CONFIG_KEY_AUTOCRLF);
446 			}
447 		}
448 
449 		@Test
450 		public void testPatchWithCrLf2() throws Exception {
451 			String aName = "crlf2";
452 			try (Git git = new Git(db)) {
453 				db.getConfig().setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
454 						ConfigConstants.CONFIG_KEY_AUTOCRLF, false);
455 				init(aName, true, true);
456 				db.getConfig().setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
457 						ConfigConstants.CONFIG_KEY_AUTOCRLF, true);
458 
459 				Result result = applyPatch();
460 
461 				verifyChange(result, aName);
462 			} finally {
463 				db.getConfig().unset(ConfigConstants.CONFIG_CORE_SECTION, null,
464 						ConfigConstants.CONFIG_KEY_AUTOCRLF);
465 			}
466 		}
467 
468 		// Clean/smudge filter for testFiltering. The smudgetest test resources were
469 		// created with C git using a clean filter sed -e "s/A/E/g" and the smudge
470 		// filter sed -e "s/E/A/g". To keep the test independent of the presence of
471 		// sed, implement this with a built-in filter.
472 		private static class ReplaceFilter extends FilterCommand {
473 
474 			private final char toReplace;
475 
476 			private final char replacement;
477 
478 			ReplaceFilter(InputStream in, OutputStream out, char toReplace,
479 					char replacement) {
480 				super(in, out);
481 				this.toReplace = toReplace;
482 				this.replacement = replacement;
483 			}
484 
485 			@Override
486 			public int run() throws IOException {
487 				int b = in.read();
488 				if (b < 0) {
489 					in.close();
490 					out.close();
491 					return -1;
492 				}
493 				if ((b & 0xFF) == toReplace) {
494 					b = replacement;
495 				}
496 				out.write(b);
497 				return 1;
498 			}
499 		}
500 
501 		@Test
502 		public void testFiltering() throws Exception {
503 			// Set up filter
504 			FilterCommandFactory clean = (repo, in, out) -> new ReplaceFilter(in, out, 'A', 'E');
505 			FilterCommandFactory smudge = (repo, in, out) -> new ReplaceFilter(in, out, 'E', 'A');
506 			FilterCommandRegistry.register("jgit://builtin/a2e/clean", clean);
507 			FilterCommandRegistry.register("jgit://builtin/a2e/smudge", smudge);
508 			Config config = db.getConfig();
509 			try (Git git = new Git(db)) {
510 				config.setString(ConfigConstants.CONFIG_FILTER_SECTION, "a2e",
511 						"clean", "jgit://builtin/a2e/clean");
512 				config.setString(ConfigConstants.CONFIG_FILTER_SECTION, "a2e",
513 						"smudge", "jgit://builtin/a2e/smudge");
514 				write(new File(db.getWorkTree(), ".gitattributes"),
515 						"smudgetest filter=a2e");
516 				git.add().addFilepattern(".gitattributes").call();
517 				git.commit().setMessage("Attributes").call();
518 				init("smudgetest", true, true);
519 
520 				Result result = applyPatch();
521 
522 				verifyChange(result, name);
523 			} finally {
524 				config.unset(ConfigConstants.CONFIG_FILTER_SECTION, "a2e",
525 						"clean");
526 				config.unset(ConfigConstants.CONFIG_FILTER_SECTION, "a2e",
527 						"smudge");
528 				// Tear down filter
529 				FilterCommandRegistry.unregister("jgit://builtin/a2e/clean");
530 				FilterCommandRegistry.unregister("jgit://builtin/a2e/smudge");
531 			}
532 		}
533 	}
534 }