View Javadoc
1   /*
2    * Copyright (C) 2011, 2019 GitHub 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  package org.eclipse.jgit.api;
11  
12  import static org.junit.Assert.assertEquals;
13  import static org.junit.Assert.assertNotNull;
14  import static org.junit.Assert.assertNull;
15  import static org.junit.Assert.assertTrue;
16  
17  import java.io.File;
18  
19  import org.eclipse.jgit.api.MergeCommand.FastForwardMode;
20  import org.eclipse.jgit.api.ResetCommand.ResetType;
21  import org.eclipse.jgit.blame.BlameResult;
22  import org.eclipse.jgit.diff.RawTextComparator;
23  import org.eclipse.jgit.junit.RepositoryTestCase;
24  import org.eclipse.jgit.lib.ConfigConstants;
25  import org.eclipse.jgit.lib.CoreConfig.AutoCRLF;
26  import org.eclipse.jgit.revwalk.RevCommit;
27  import org.eclipse.jgit.storage.file.FileBasedConfig;
28  import org.junit.Test;
29  
30  /**
31   * Unit tests of {@link BlameCommand}
32   */
33  public class BlameCommandTest extends RepositoryTestCase {
34  
35  	private static String join(String... lines) {
36  		StringBuilder joined = new StringBuilder();
37  		for (String line : lines)
38  			joined.append(line).append('\n');
39  		return joined.toString();
40  	}
41  
42  	@Test
43  	public void testSingleRevision() throws Exception {
44  		try (Git git = new Git(db)) {
45  			String[] content = new String[] { "first", "second", "third" };
46  
47  			writeTrashFile("file.txt", join(content));
48  			git.add().addFilepattern("file.txt").call();
49  			RevCommit commit = git.commit().setMessage("create file").call();
50  
51  			BlameCommand command = new BlameCommand(db);
52  			command.setFilePath("file.txt");
53  			BlameResult lines = command.call();
54  			assertNotNull(lines);
55  			assertEquals(3, lines.getResultContents().size());
56  
57  			for (int i = 0; i < 3; i++) {
58  				assertEquals(commit, lines.getSourceCommit(i));
59  				assertEquals(i, lines.getSourceLine(i));
60  			}
61  		}
62  	}
63  
64  	@Test
65  	public void testTwoRevisions() throws Exception {
66  		try (Git git = new Git(db)) {
67  			String[] content1 = new String[] { "first", "second" };
68  			writeTrashFile("file.txt", join(content1));
69  			git.add().addFilepattern("file.txt").call();
70  			RevCommit commit1 = git.commit().setMessage("create file").call();
71  
72  			String[] content2 = new String[] { "first", "second", "third" };
73  			writeTrashFile("file.txt", join(content2));
74  			git.add().addFilepattern("file.txt").call();
75  			RevCommit commit2 = git.commit().setMessage("create file").call();
76  
77  			BlameCommand command = new BlameCommand(db);
78  			command.setFilePath("file.txt");
79  			BlameResult lines = command.call();
80  			assertEquals(3, lines.getResultContents().size());
81  
82  			assertEquals(commit1, lines.getSourceCommit(0));
83  			assertEquals(0, lines.getSourceLine(0));
84  
85  			assertEquals(commit1, lines.getSourceCommit(1));
86  			assertEquals(1, lines.getSourceLine(1));
87  
88  			assertEquals(commit2, lines.getSourceCommit(2));
89  			assertEquals(2, lines.getSourceLine(2));
90  		}
91  	}
92  
93  	@Test
94  	public void testRename() throws Exception {
95  		testRename("file1.txt", "file2.txt");
96  	}
97  
98  	@Test
99  	public void testRenameInSubDir() throws Exception {
100 		testRename("subdir/file1.txt", "subdir/file2.txt");
101 	}
102 
103 	@Test
104 	public void testMoveToOtherDir() throws Exception {
105 		testRename("subdir/file1.txt", "otherdir/file1.txt");
106 	}
107 
108 	private void testRename(String sourcePath, String destPath)
109 			throws Exception {
110 		try (Git git = new Git(db)) {
111 			String[] content1 = new String[] { "a", "b", "c" };
112 			writeTrashFile(sourcePath, join(content1));
113 			git.add().addFilepattern(sourcePath).call();
114 			RevCommit commit1 = git.commit().setMessage("create file").call();
115 
116 			writeTrashFile(destPath, join(content1));
117 			git.add().addFilepattern(destPath).call();
118 			git.rm().addFilepattern(sourcePath).call();
119 			git.commit().setMessage("moving file").call();
120 
121 			String[] content2 = new String[] { "a", "b", "c2" };
122 			writeTrashFile(destPath, join(content2));
123 			git.add().addFilepattern(destPath).call();
124 			RevCommit commit3 = git.commit().setMessage("editing file").call();
125 
126 			BlameCommand command = new BlameCommand(db);
127 			command.setFollowFileRenames(true);
128 			command.setFilePath(destPath);
129 			BlameResult lines = command.call();
130 
131 			assertEquals(commit1, lines.getSourceCommit(0));
132 			assertEquals(0, lines.getSourceLine(0));
133 			assertEquals(sourcePath, lines.getSourcePath(0));
134 
135 			assertEquals(commit1, lines.getSourceCommit(1));
136 			assertEquals(1, lines.getSourceLine(1));
137 			assertEquals(sourcePath, lines.getSourcePath(1));
138 
139 			assertEquals(commit3, lines.getSourceCommit(2));
140 			assertEquals(2, lines.getSourceLine(2));
141 			assertEquals(destPath, lines.getSourcePath(2));
142 		}
143 	}
144 
145 	@Test
146 	public void testTwoRenames() throws Exception {
147 		try (Git git = new Git(db)) {
148 			// Commit 1: Add file.txt
149 			String[] content1 = new String[] { "a" };
150 			writeTrashFile("file.txt", join(content1));
151 			git.add().addFilepattern("file.txt").call();
152 			RevCommit commit1 = git.commit().setMessage("create file").call();
153 
154 			// Commit 2: Rename to file1.txt
155 			writeTrashFile("file1.txt", join(content1));
156 			git.add().addFilepattern("file1.txt").call();
157 			git.rm().addFilepattern("file.txt").call();
158 			git.commit().setMessage("moving file").call();
159 
160 			// Commit 3: Edit file1.txt
161 			String[] content2 = new String[] { "a", "b" };
162 			writeTrashFile("file1.txt", join(content2));
163 			git.add().addFilepattern("file1.txt").call();
164 			RevCommit commit3 = git.commit().setMessage("editing file").call();
165 
166 			// Commit 4: Rename to file2.txt
167 			writeTrashFile("file2.txt", join(content2));
168 			git.add().addFilepattern("file2.txt").call();
169 			git.rm().addFilepattern("file1.txt").call();
170 			git.commit().setMessage("moving file again").call();
171 
172 			BlameCommand command = new BlameCommand(db);
173 			command.setFollowFileRenames(true);
174 			command.setFilePath("file2.txt");
175 			BlameResult lines = command.call();
176 
177 			assertEquals(commit1, lines.getSourceCommit(0));
178 			assertEquals(0, lines.getSourceLine(0));
179 			assertEquals("file.txt", lines.getSourcePath(0));
180 
181 			assertEquals(commit3, lines.getSourceCommit(1));
182 			assertEquals(1, lines.getSourceLine(1));
183 			assertEquals("file1.txt", lines.getSourcePath(1));
184 		}
185 	}
186 
187 	@Test
188 	public void testDeleteTrailingLines() throws Exception {
189 		try (Git git = new Git(db)) {
190 			String[] content1 = new String[] { "a", "b", "c", "d" };
191 			String[] content2 = new String[] { "a", "b" };
192 
193 			writeTrashFile("file.txt", join(content2));
194 			git.add().addFilepattern("file.txt").call();
195 			RevCommit commit1 = git.commit().setMessage("create file").call();
196 
197 			writeTrashFile("file.txt", join(content1));
198 			git.add().addFilepattern("file.txt").call();
199 			git.commit().setMessage("edit file").call();
200 
201 			writeTrashFile("file.txt", join(content2));
202 			git.add().addFilepattern("file.txt").call();
203 			git.commit().setMessage("edit file").call();
204 
205 			BlameCommand command = new BlameCommand(db);
206 
207 			command.setFilePath("file.txt");
208 			BlameResult lines = command.call();
209 			assertEquals(content2.length, lines.getResultContents().size());
210 
211 			assertEquals(commit1, lines.getSourceCommit(0));
212 			assertEquals(commit1, lines.getSourceCommit(1));
213 
214 			assertEquals(0, lines.getSourceLine(0));
215 			assertEquals(1, lines.getSourceLine(1));
216 		}
217 	}
218 
219 	@Test
220 	public void testDeleteMiddleLines() throws Exception {
221 		try (Git git = new Git(db)) {
222 			String[] content1 = new String[] { "a", "b", "c", "d", "e" };
223 			String[] content2 = new String[] { "a", "c", "e" };
224 
225 			writeTrashFile("file.txt", join(content2));
226 			git.add().addFilepattern("file.txt").call();
227 			RevCommit commit1 = git.commit().setMessage("edit file").call();
228 
229 			writeTrashFile("file.txt", join(content1));
230 			git.add().addFilepattern("file.txt").call();
231 			git.commit().setMessage("edit file").call();
232 
233 			writeTrashFile("file.txt", join(content2));
234 			git.add().addFilepattern("file.txt").call();
235 			git.commit().setMessage("edit file").call();
236 
237 			BlameCommand command = new BlameCommand(db);
238 
239 			command.setFilePath("file.txt");
240 			BlameResult lines = command.call();
241 			assertEquals(content2.length, lines.getResultContents().size());
242 
243 			assertEquals(commit1, lines.getSourceCommit(0));
244 			assertEquals(0, lines.getSourceLine(0));
245 
246 			assertEquals(commit1, lines.getSourceCommit(1));
247 			assertEquals(1, lines.getSourceLine(1));
248 
249 			assertEquals(commit1, lines.getSourceCommit(2));
250 			assertEquals(2, lines.getSourceLine(2));
251 		}
252 	}
253 
254 	@Test
255 	public void testEditAllLines() throws Exception {
256 		try (Git git = new Git(db)) {
257 			String[] content1 = new String[] { "a", "1" };
258 			String[] content2 = new String[] { "b", "2" };
259 
260 			writeTrashFile("file.txt", join(content1));
261 			git.add().addFilepattern("file.txt").call();
262 			git.commit().setMessage("edit file").call();
263 
264 			writeTrashFile("file.txt", join(content2));
265 			git.add().addFilepattern("file.txt").call();
266 			RevCommit commit2 = git.commit().setMessage("create file").call();
267 
268 			BlameCommand command = new BlameCommand(db);
269 
270 			command.setFilePath("file.txt");
271 			BlameResult lines = command.call();
272 			assertEquals(content2.length, lines.getResultContents().size());
273 			assertEquals(commit2, lines.getSourceCommit(0));
274 			assertEquals(commit2, lines.getSourceCommit(1));
275 		}
276 	}
277 
278 	@Test
279 	public void testMiddleClearAllLines() throws Exception {
280 		try (Git git = new Git(db)) {
281 			String[] content1 = new String[] { "a", "b", "c" };
282 
283 			writeTrashFile("file.txt", join(content1));
284 			git.add().addFilepattern("file.txt").call();
285 			git.commit().setMessage("edit file").call();
286 
287 			writeTrashFile("file.txt", "");
288 			git.add().addFilepattern("file.txt").call();
289 			git.commit().setMessage("create file").call();
290 
291 			writeTrashFile("file.txt", join(content1));
292 			git.add().addFilepattern("file.txt").call();
293 			RevCommit commit3 = git.commit().setMessage("edit file").call();
294 
295 			BlameCommand command = new BlameCommand(db);
296 
297 			command.setFilePath("file.txt");
298 			BlameResult lines = command.call();
299 			assertEquals(content1.length, lines.getResultContents().size());
300 			assertEquals(commit3, lines.getSourceCommit(0));
301 			assertEquals(commit3, lines.getSourceCommit(1));
302 			assertEquals(commit3, lines.getSourceCommit(2));
303 		}
304 	}
305 
306 	@Test
307 	public void testCoreAutoCrlf1() throws Exception {
308 		testCoreAutoCrlf(AutoCRLF.INPUT, AutoCRLF.FALSE);
309 	}
310 
311 	@Test
312 	public void testCoreAutoCrlf2() throws Exception {
313 		testCoreAutoCrlf(AutoCRLF.FALSE, AutoCRLF.FALSE);
314 	}
315 
316 	@Test
317 	public void testCoreAutoCrlf3() throws Exception {
318 		testCoreAutoCrlf(AutoCRLF.INPUT, AutoCRLF.INPUT);
319 	}
320 
321 	@Test
322 	public void testCoreAutoCrlf4() throws Exception {
323 		testCoreAutoCrlf(AutoCRLF.FALSE, AutoCRLF.INPUT);
324 	}
325 
326 	@Test
327 	public void testCoreAutoCrlf5() throws Exception {
328 		testCoreAutoCrlf(AutoCRLF.INPUT, AutoCRLF.TRUE);
329 	}
330 
331 	private void testCoreAutoCrlf(AutoCRLF modeForCommitting,
332 			AutoCRLF modeForReset) throws Exception {
333 		try (Git git = new Git(db)) {
334 			FileBasedConfig config = db.getConfig();
335 			config.setEnum(ConfigConstants.CONFIG_CORE_SECTION, null,
336 					ConfigConstants.CONFIG_KEY_AUTOCRLF, modeForCommitting);
337 			config.save();
338 
339 			String joinedCrlf = "a\r\nb\r\nc\r\n";
340 			File trashFile = writeTrashFile("file.txt", joinedCrlf);
341 			git.add().addFilepattern("file.txt").call();
342 			RevCommit commit = git.commit().setMessage("create file").call();
343 
344 			// re-create file from the repo
345 			trashFile.delete();
346 			config.setEnum(ConfigConstants.CONFIG_CORE_SECTION, null,
347 					ConfigConstants.CONFIG_KEY_AUTOCRLF, modeForReset);
348 			config.save();
349 			git.reset().setMode(ResetType.HARD).call();
350 
351 			BlameCommand command = new BlameCommand(db);
352 			command.setFilePath("file.txt");
353 			BlameResult lines = command.call();
354 
355 			assertEquals(3, lines.getResultContents().size());
356 			assertEquals(commit, lines.getSourceCommit(0));
357 			assertEquals(commit, lines.getSourceCommit(1));
358 			assertEquals(commit, lines.getSourceCommit(2));
359 		}
360 	}
361 
362 	@Test
363 	public void testConflictingMerge1() throws Exception {
364 		try (Git git = new Git(db)) {
365 			RevCommit base = commitFile("file.txt", join("0", "1", "2", "3", "4"),
366 					"master");
367 
368 			git.checkout().setName("side").setCreateBranch(true)
369 					.setStartPoint(base).call();
370 			RevCommit side = commitFile("file.txt",
371 					join("0", "1 side", "2", "3 on side", "4"), "side");
372 
373 			commitFile("file.txt", join("0", "1", "2"), "master");
374 
375 			checkoutBranch("refs/heads/master");
376 			git.merge().include(side).call();
377 
378 			// The merge results in a conflict, which we resolve using mostly the
379 			// side branch contents. Especially the "4" survives.
380 			RevCommit merge = commitFile("file.txt",
381 					join("0", "1 side", "2", "3 resolved", "4"), "master");
382 
383 			BlameCommand command = new BlameCommand(db);
384 			command.setFilePath("file.txt");
385 			BlameResult lines = command.call();
386 
387 			assertEquals(5, lines.getResultContents().size());
388 			assertEquals(base, lines.getSourceCommit(0));
389 			assertEquals(side, lines.getSourceCommit(1));
390 			assertEquals(base, lines.getSourceCommit(2));
391 			assertEquals(merge, lines.getSourceCommit(3));
392 			assertEquals(base, lines.getSourceCommit(4));
393 		}
394 	}
395 
396 	// this test inverts the order of the master and side commit and is
397 	// otherwise identical to testConflictingMerge1
398 	@Test
399 	public void testConflictingMerge2() throws Exception {
400 		try (Git git = new Git(db)) {
401 			RevCommit base = commitFile("file.txt", join("0", "1", "2", "3", "4"),
402 					"master");
403 
404 			commitFile("file.txt", join("0", "1", "2"), "master");
405 
406 			git.checkout().setName("side").setCreateBranch(true)
407 					.setStartPoint(base).call();
408 			RevCommit side = commitFile("file.txt",
409 					join("0", "1 side", "2", "3 on side", "4"), "side");
410 
411 			checkoutBranch("refs/heads/master");
412 			git.merge().include(side).call();
413 
414 			// The merge results in a conflict, which we resolve using mostly the
415 			// side branch contents. Especially the "4" survives.
416 			RevCommit merge = commitFile("file.txt",
417 					join("0", "1 side", "2", "3 resolved", "4"), "master");
418 
419 			BlameCommand command = new BlameCommand(db);
420 			command.setFilePath("file.txt");
421 			BlameResult lines = command.call();
422 
423 			assertEquals(5, lines.getResultContents().size());
424 			assertEquals(base, lines.getSourceCommit(0));
425 			assertEquals(side, lines.getSourceCommit(1));
426 			assertEquals(base, lines.getSourceCommit(2));
427 			assertEquals(merge, lines.getSourceCommit(3));
428 			assertEquals(base, lines.getSourceCommit(4));
429 		}
430 	}
431 
432 	@Test
433 	public void testUnresolvedMergeConflict() throws Exception {
434 		try (Git git = new Git(db)) {
435 			RevCommit base = commitFile("file.txt", "Origin\n", "master");
436 
437 			RevCommit master = commitFile("file.txt",
438 					"Change on master branch\n", "master");
439 
440 			git.checkout().setName("side").setCreateBranch(true)
441 					.setStartPoint(base).call();
442 			RevCommit side = commitFile("file.txt",
443 					"Conflicting change on side\n", "side");
444 
445 			checkoutBranch("refs/heads/master");
446 			MergeResult result = git.merge().include(side).call();
447 
448 			// The merge results in a conflict, which we do not resolve
449 			assertTrue("Expected a conflict",
450 					result.getConflicts().containsKey("file.txt"));
451 
452 			BlameCommand command = new BlameCommand(db);
453 			command.setFilePath("file.txt");
454 			BlameResult lines = command.call();
455 
456 			assertEquals(5, lines.getResultContents().size());
457 			assertNull(lines.getSourceCommit(0));
458 			assertEquals(master, lines.getSourceCommit(1));
459 			assertNull(lines.getSourceCommit(2));
460 			assertEquals(side, lines.getSourceCommit(3));
461 			assertNull(lines.getSourceCommit(4));
462 		}
463 	}
464 
465 	@Test
466 	public void testWhitespaceMerge() throws Exception {
467 		try (Git git = new Git(db)) {
468 			RevCommit base = commitFile("file.txt", join("0", "1", "2"), "master");
469 			RevCommit side = commitFile("file.txt", join("0", "1", "   2 side  "),
470 					"side");
471 
472 			checkoutBranch("refs/heads/master");
473 			git.merge().setFastForward(FastForwardMode.NO_FF).include(side).call();
474 
475 			// change whitespace, so the merge content is not identical to side, but
476 			// is the same when ignoring whitespace
477 			writeTrashFile("file.txt", join("0", "1", "2 side"));
478 			RevCommit merge = git.commit().setAll(true).setMessage("merge")
479 					.setAmend(true)
480 					.call();
481 
482 			BlameCommand command = new BlameCommand(db);
483 			command.setFilePath("file.txt")
484 					.setTextComparator(RawTextComparator.WS_IGNORE_ALL)
485 					.setStartCommit(merge.getId());
486 			BlameResult lines = command.call();
487 
488 			assertEquals(3, lines.getResultContents().size());
489 			assertEquals(base, lines.getSourceCommit(0));
490 			assertEquals(base, lines.getSourceCommit(1));
491 			assertEquals(side, lines.getSourceCommit(2));
492 		}
493 	}
494 
495 	@Test
496 	public void testBlameWithNulByteInHistory() throws Exception {
497 		try (Git git = new Git(db)) {
498 			String[] content1 = { "First line", "Another line" };
499 			writeTrashFile("file.txt", join(content1));
500 			git.add().addFilepattern("file.txt").call();
501 			RevCommit c1 = git.commit().setMessage("create file").call();
502 
503 			String[] content2 = { "First line", "Second line with NUL >\000<",
504 					"Another line" };
505 			assertTrue("Content should contain a NUL byte",
506 					content2[1].indexOf(0) > 0);
507 			writeTrashFile("file.txt", join(content2));
508 			git.add().addFilepattern("file.txt").call();
509 			git.commit().setMessage("add line with NUL").call();
510 
511 			String[] content3 = { "First line", "Second line with NUL >\000<",
512 					"Third line" };
513 			writeTrashFile("file.txt", join(content3));
514 			git.add().addFilepattern("file.txt").call();
515 			RevCommit c3 = git.commit().setMessage("change third line").call();
516 
517 			String[] content4 = { "First line", "Second line with NUL >\\000<",
518 					"Third line" };
519 			assertTrue("Content should not contain a NUL byte",
520 					content4[1].indexOf(0) < 0);
521 			writeTrashFile("file.txt", join(content4));
522 			git.add().addFilepattern("file.txt").call();
523 			RevCommit c4 = git.commit().setMessage("fix NUL line").call();
524 
525 			BlameResult lines = git.blame().setFilePath("file.txt").call();
526 			assertEquals(3, lines.getResultContents().size());
527 			assertEquals(c1, lines.getSourceCommit(0));
528 			assertEquals(c4, lines.getSourceCommit(1));
529 			assertEquals(c3, lines.getSourceCommit(2));
530 		}
531 	}
532 
533 	@Test
534 	public void testBlameWithNulByteInTopRevision() throws Exception {
535 		try (Git git = new Git(db)) {
536 			String[] content1 = { "First line", "Another line" };
537 			writeTrashFile("file.txt", join(content1));
538 			git.add().addFilepattern("file.txt").call();
539 			RevCommit c1 = git.commit().setMessage("create file").call();
540 
541 			String[] content2 = { "First line", "Second line with NUL >\000<",
542 					"Another line" };
543 			assertTrue("Content should contain a NUL byte",
544 					content2[1].indexOf(0) > 0);
545 			writeTrashFile("file.txt", join(content2));
546 			git.add().addFilepattern("file.txt").call();
547 			RevCommit c2 = git.commit().setMessage("add line with NUL").call();
548 
549 			String[] content3 = { "First line", "Second line with NUL >\000<",
550 					"Third line" };
551 			writeTrashFile("file.txt", join(content3));
552 			git.add().addFilepattern("file.txt").call();
553 			RevCommit c3 = git.commit().setMessage("change third line").call();
554 
555 			BlameResult lines = git.blame().setFilePath("file.txt").call();
556 			assertEquals(3, lines.getResultContents().size());
557 			assertEquals(c1, lines.getSourceCommit(0));
558 			assertEquals(c2, lines.getSourceCommit(1));
559 			assertEquals(c3, lines.getSourceCommit(2));
560 		}
561 	}
562 
563 }