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