View Javadoc
1   /*
2    * Copyright (C) 2011-2012, 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.assertNotEquals;
14  import static org.junit.Assert.assertNotNull;
15  import static org.junit.Assert.assertNull;
16  import static org.junit.Assert.assertSame;
17  import static org.junit.Assert.assertTrue;
18  import static org.junit.Assert.fail;
19  import static org.junit.Assume.assumeTrue;
20  
21  import java.io.File;
22  import java.util.Date;
23  import java.util.List;
24  import java.util.TimeZone;
25  import java.util.concurrent.atomic.AtomicInteger;
26  
27  import org.eclipse.jgit.api.CherryPickResult.CherryPickStatus;
28  import org.eclipse.jgit.api.errors.CanceledException;
29  import org.eclipse.jgit.api.errors.EmptyCommitException;
30  import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
31  import org.eclipse.jgit.diff.DiffEntry;
32  import org.eclipse.jgit.dircache.DirCache;
33  import org.eclipse.jgit.dircache.DirCacheBuilder;
34  import org.eclipse.jgit.dircache.DirCacheEntry;
35  import org.eclipse.jgit.junit.RepositoryTestCase;
36  import org.eclipse.jgit.junit.time.TimeUtil;
37  import org.eclipse.jgit.lib.CommitBuilder;
38  import org.eclipse.jgit.lib.ConfigConstants;
39  import org.eclipse.jgit.lib.Constants;
40  import org.eclipse.jgit.lib.FileMode;
41  import org.eclipse.jgit.lib.GpgSigner;
42  import org.eclipse.jgit.lib.ObjectId;
43  import org.eclipse.jgit.lib.PersonIdent;
44  import org.eclipse.jgit.lib.RefUpdate;
45  import org.eclipse.jgit.lib.RefUpdate.Result;
46  import org.eclipse.jgit.lib.ReflogEntry;
47  import org.eclipse.jgit.lib.Repository;
48  import org.eclipse.jgit.lib.StoredConfig;
49  import org.eclipse.jgit.revwalk.RevCommit;
50  import org.eclipse.jgit.storage.file.FileBasedConfig;
51  import org.eclipse.jgit.submodule.SubmoduleWalk;
52  import org.eclipse.jgit.transport.CredentialsProvider;
53  import org.eclipse.jgit.treewalk.TreeWalk;
54  import org.eclipse.jgit.treewalk.filter.TreeFilter;
55  import org.eclipse.jgit.util.FS;
56  import org.junit.Ignore;
57  import org.junit.Test;
58  
59  /**
60   * Unit tests of {@link CommitCommand}.
61   */
62  public class CommitCommandTest extends RepositoryTestCase {
63  
64  	@Test
65  	public void testExecutableRetention() throws Exception {
66  		StoredConfig config = db.getConfig();
67  		config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
68  				ConfigConstants.CONFIG_KEY_FILEMODE, true);
69  		config.save();
70  
71  		FS executableFs = new FS() {
72  
73  			@Override
74  			public boolean supportsExecute() {
75  				return true;
76  			}
77  
78  			@Override
79  			public boolean setExecute(File f, boolean canExec) {
80  				return true;
81  			}
82  
83  			@Override
84  			public ProcessBuilder runInShell(String cmd, String[] args) {
85  				return null;
86  			}
87  
88  			@Override
89  			public boolean retryFailedLockFileCommit() {
90  				return false;
91  			}
92  
93  			@Override
94  			public FS newInstance() {
95  				return this;
96  			}
97  
98  			@Override
99  			protected File discoverGitExe() {
100 				return null;
101 			}
102 
103 			@Override
104 			public boolean canExecute(File f) {
105 				return true;
106 			}
107 
108 			@Override
109 			public boolean isCaseSensitive() {
110 				return true;
111 			}
112 		};
113 
114 		Git git = Git.open(db.getDirectory(), executableFs);
115 		String path = "a.txt";
116 		writeTrashFile(path, "content");
117 		git.add().addFilepattern(path).call();
118 		RevCommit commit1 = git.commit().setMessage("commit").call();
119 		try (TreeWalk walk = TreeWalk.forPath(db, path, commit1.getTree())) {
120 			assertNotNull(walk);
121 			assertEquals(FileMode.EXECUTABLE_FILE, walk.getFileMode(0));
122 		}
123 
124 		FS nonExecutableFs = new FS() {
125 
126 			@Override
127 			public boolean supportsExecute() {
128 				return false;
129 			}
130 
131 			@Override
132 			public boolean setExecute(File f, boolean canExec) {
133 				return false;
134 			}
135 
136 			@Override
137 			public ProcessBuilder runInShell(String cmd, String[] args) {
138 				return null;
139 			}
140 
141 			@Override
142 			public boolean retryFailedLockFileCommit() {
143 				return false;
144 			}
145 
146 			@Override
147 			public FS newInstance() {
148 				return this;
149 			}
150 
151 			@Override
152 			protected File discoverGitExe() {
153 				return null;
154 			}
155 
156 			@Override
157 			public boolean canExecute(File f) {
158 				return false;
159 			}
160 
161 			@Override
162 			public boolean isCaseSensitive() {
163 				return true;
164 			}
165 		};
166 
167 		config = db.getConfig();
168 		config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
169 				ConfigConstants.CONFIG_KEY_FILEMODE, false);
170 		config.save();
171 
172 		Git git2 = Git.open(db.getDirectory(), nonExecutableFs);
173 		writeTrashFile(path, "content2");
174 		RevCommit commit2 = git2.commit().setOnly(path).setMessage("commit2")
175 				.call();
176 		try (TreeWalk walk = TreeWalk.forPath(db, path, commit2.getTree())) {
177 			assertNotNull(walk);
178 			assertEquals(FileMode.EXECUTABLE_FILE, walk.getFileMode(0));
179 		}
180 	}
181 
182 	@Test
183 	public void commitNewSubmodule() throws Exception {
184 		try (Git git = new Git(db)) {
185 			writeTrashFile("file.txt", "content");
186 			git.add().addFilepattern("file.txt").call();
187 			RevCommit commit = git.commit().setMessage("create file").call();
188 
189 			SubmoduleAddCommand command = new SubmoduleAddCommand(db);
190 			String path = "sub";
191 			command.setPath(path);
192 			String uri = db.getDirectory().toURI().toString();
193 			command.setURI(uri);
194 			Repository repo = command.call();
195 			assertNotNull(repo);
196 			addRepoToClose(repo);
197 
198 			try (SubmoduleWalk generator = SubmoduleWalk.forIndex(db)) {
199 				assertTrue(generator.next());
200 				assertEquals(path, generator.getPath());
201 				assertEquals(commit, generator.getObjectId());
202 				assertEquals(uri, generator.getModulesUrl());
203 				assertEquals(path, generator.getModulesPath());
204 				assertEquals(uri, generator.getConfigUrl());
205 				try (Repository subModRepo = generator.getRepository()) {
206 					assertNotNull(subModRepo);
207 				}
208 			}
209 			assertEquals(commit, repo.resolve(Constants.HEAD));
210 
211 			RevCommit submoduleCommit = git.commit().setMessage("submodule add")
212 					.setOnly(path).call();
213 			assertNotNull(submoduleCommit);
214 			try (TreeWalk walk = new TreeWalk(db)) {
215 				walk.addTree(commit.getTree());
216 				walk.addTree(submoduleCommit.getTree());
217 				walk.setFilter(TreeFilter.ANY_DIFF);
218 				List<DiffEntry> diffs = DiffEntry.scan(walk);
219 				assertEquals(1, diffs.size());
220 				DiffEntry subDiff = diffs.get(0);
221 				assertEquals(FileMode.MISSING, subDiff.getOldMode());
222 				assertEquals(FileMode.GITLINK, subDiff.getNewMode());
223 				assertEquals(ObjectId.zeroId(), subDiff.getOldId().toObjectId());
224 				assertEquals(commit, subDiff.getNewId().toObjectId());
225 				assertEquals(path, subDiff.getNewPath());
226 			}
227 		}
228 	}
229 
230 	@Test
231 	public void commitSubmoduleUpdate() throws Exception {
232 		try (Git git = new Git(db)) {
233 			writeTrashFile("file.txt", "content");
234 			git.add().addFilepattern("file.txt").call();
235 			RevCommit commit = git.commit().setMessage("create file").call();
236 			writeTrashFile("file.txt", "content2");
237 			git.add().addFilepattern("file.txt").call();
238 			RevCommit commit2 = git.commit().setMessage("edit file").call();
239 
240 			SubmoduleAddCommand command = new SubmoduleAddCommand(db);
241 			String path = "sub";
242 			command.setPath(path);
243 			String uri = db.getDirectory().toURI().toString();
244 			command.setURI(uri);
245 			Repository repo = command.call();
246 			assertNotNull(repo);
247 			addRepoToClose(repo);
248 
249 			try (SubmoduleWalk generator = SubmoduleWalk.forIndex(db)) {
250 				assertTrue(generator.next());
251 				assertEquals(path, generator.getPath());
252 				assertEquals(commit2, generator.getObjectId());
253 				assertEquals(uri, generator.getModulesUrl());
254 				assertEquals(path, generator.getModulesPath());
255 				assertEquals(uri, generator.getConfigUrl());
256 				try (Repository subModRepo = generator.getRepository()) {
257 					assertNotNull(subModRepo);
258 				}
259 			}
260 			assertEquals(commit2, repo.resolve(Constants.HEAD));
261 
262 			RevCommit submoduleAddCommit = git.commit().setMessage("submodule add")
263 					.setOnly(path).call();
264 			assertNotNull(submoduleAddCommit);
265 
266 			RefUpdate update = repo.updateRef(Constants.HEAD);
267 			update.setNewObjectId(commit);
268 			assertEquals(Result.FORCED, update.forceUpdate());
269 
270 			RevCommit submoduleEditCommit = git.commit()
271 					.setMessage("submodule add").setOnly(path).call();
272 			assertNotNull(submoduleEditCommit);
273 			try (TreeWalk walk = new TreeWalk(db)) {
274 				walk.addTree(submoduleAddCommit.getTree());
275 				walk.addTree(submoduleEditCommit.getTree());
276 				walk.setFilter(TreeFilter.ANY_DIFF);
277 				List<DiffEntry> diffs = DiffEntry.scan(walk);
278 				assertEquals(1, diffs.size());
279 				DiffEntry subDiff = diffs.get(0);
280 				assertEquals(FileMode.GITLINK, subDiff.getOldMode());
281 				assertEquals(FileMode.GITLINK, subDiff.getNewMode());
282 				assertEquals(commit2, subDiff.getOldId().toObjectId());
283 				assertEquals(commit, subDiff.getNewId().toObjectId());
284 				assertEquals(path, subDiff.getNewPath());
285 				assertEquals(path, subDiff.getOldPath());
286 			}
287 		}
288 	}
289 
290 	@Ignore("very flaky when run with Hudson")
291 	@Test
292 	public void commitUpdatesSmudgedEntries() throws Exception {
293 		try (Git git = new Git(db)) {
294 			File file1 = writeTrashFile("file1.txt", "content1");
295 			TimeUtil.setLastModifiedWithOffset(file1.toPath(), -5000L);
296 			File file2 = writeTrashFile("file2.txt", "content2");
297 			TimeUtil.setLastModifiedWithOffset(file2.toPath(), -5000L);
298 			File file3 = writeTrashFile("file3.txt", "content3");
299 			TimeUtil.setLastModifiedWithOffset(file3.toPath(), -5000L);
300 
301 			assertNotNull(git.add().addFilepattern("file1.txt")
302 					.addFilepattern("file2.txt").addFilepattern("file3.txt").call());
303 			RevCommit commit = git.commit().setMessage("add files").call();
304 			assertNotNull(commit);
305 
306 			DirCache cache = DirCache.read(db.getIndexFile(), db.getFS());
307 			int file1Size = cache.getEntry("file1.txt").getLength();
308 			int file2Size = cache.getEntry("file2.txt").getLength();
309 			int file3Size = cache.getEntry("file3.txt").getLength();
310 			ObjectId file2Id = cache.getEntry("file2.txt").getObjectId();
311 			ObjectId file3Id = cache.getEntry("file3.txt").getObjectId();
312 			assertTrue(file1Size > 0);
313 			assertTrue(file2Size > 0);
314 			assertTrue(file3Size > 0);
315 
316 			// Smudge entries
317 			cache = DirCache.lock(db.getIndexFile(), db.getFS());
318 			cache.getEntry("file1.txt").setLength(0);
319 			cache.getEntry("file2.txt").setLength(0);
320 			cache.getEntry("file3.txt").setLength(0);
321 			cache.write();
322 			assertTrue(cache.commit());
323 
324 			// Verify entries smudged
325 			cache = DirCache.read(db.getIndexFile(), db.getFS());
326 			assertEquals(0, cache.getEntry("file1.txt").getLength());
327 			assertEquals(0, cache.getEntry("file2.txt").getLength());
328 			assertEquals(0, cache.getEntry("file3.txt").getLength());
329 
330 			TimeUtil.setLastModifiedWithOffset(db.getIndexFile().toPath(),
331 					-5000L);
332 
333 			write(file1, "content4");
334 
335 			TimeUtil.setLastModifiedWithOffset(file1.toPath(), 2500L);
336 			assertNotNull(git.commit().setMessage("edit file").setOnly("file1.txt")
337 					.call());
338 
339 			cache = db.readDirCache();
340 			assertEquals(file1Size, cache.getEntry("file1.txt").getLength());
341 			assertEquals(file2Size, cache.getEntry("file2.txt").getLength());
342 			assertEquals(file3Size, cache.getEntry("file3.txt").getLength());
343 			assertEquals(file2Id, cache.getEntry("file2.txt").getObjectId());
344 			assertEquals(file3Id, cache.getEntry("file3.txt").getObjectId());
345 		}
346 	}
347 
348 	@Ignore("very flaky when run with Hudson")
349 	@Test
350 	public void commitIgnoresSmudgedEntryWithDifferentId() throws Exception {
351 		try (Git git = new Git(db)) {
352 			File file1 = writeTrashFile("file1.txt", "content1");
353 			TimeUtil.setLastModifiedWithOffset(file1.toPath(), -5000L);
354 			File file2 = writeTrashFile("file2.txt", "content2");
355 			TimeUtil.setLastModifiedWithOffset(file2.toPath(), -5000L);
356 
357 			assertNotNull(git.add().addFilepattern("file1.txt")
358 					.addFilepattern("file2.txt").call());
359 			RevCommit commit = git.commit().setMessage("add files").call();
360 			assertNotNull(commit);
361 
362 			DirCache cache = DirCache.read(db.getIndexFile(), db.getFS());
363 			int file1Size = cache.getEntry("file1.txt").getLength();
364 			int file2Size = cache.getEntry("file2.txt").getLength();
365 			assertTrue(file1Size > 0);
366 			assertTrue(file2Size > 0);
367 
368 			writeTrashFile("file2.txt", "content3");
369 			assertNotNull(git.add().addFilepattern("file2.txt").call());
370 			writeTrashFile("file2.txt", "content4");
371 
372 			// Smudge entries
373 			cache = DirCache.lock(db.getIndexFile(), db.getFS());
374 			cache.getEntry("file1.txt").setLength(0);
375 			cache.getEntry("file2.txt").setLength(0);
376 			cache.write();
377 			assertTrue(cache.commit());
378 
379 			// Verify entries smudged
380 			cache = db.readDirCache();
381 			assertEquals(0, cache.getEntry("file1.txt").getLength());
382 			assertEquals(0, cache.getEntry("file2.txt").getLength());
383 
384 			TimeUtil.setLastModifiedWithOffset(db.getIndexFile().toPath(),
385 					-5000L);
386 
387 			write(file1, "content5");
388 			TimeUtil.setLastModifiedWithOffset(file1.toPath(), 1000L);
389 
390 			assertNotNull(git.commit().setMessage("edit file").setOnly("file1.txt")
391 					.call());
392 
393 			cache = db.readDirCache();
394 			assertEquals(file1Size, cache.getEntry("file1.txt").getLength());
395 			assertEquals(0, cache.getEntry("file2.txt").getLength());
396 		}
397 	}
398 
399 	@Test
400 	public void commitAfterSquashMerge() throws Exception {
401 		try (Git git = new Git(db)) {
402 			writeTrashFile("file1", "file1");
403 			git.add().addFilepattern("file1").call();
404 			RevCommit first = git.commit().setMessage("initial commit").call();
405 
406 			assertTrue(new File(db.getWorkTree(), "file1").exists());
407 			createBranch(first, "refs/heads/branch1");
408 			checkoutBranch("refs/heads/branch1");
409 
410 			writeTrashFile("file2", "file2");
411 			git.add().addFilepattern("file2").call();
412 			git.commit().setMessage("second commit").call();
413 			assertTrue(new File(db.getWorkTree(), "file2").exists());
414 
415 			checkoutBranch("refs/heads/master");
416 
417 			MergeResult result = git.merge()
418 					.include(db.exactRef("refs/heads/branch1"))
419 					.setSquash(true)
420 					.call();
421 
422 			assertTrue(new File(db.getWorkTree(), "file1").exists());
423 			assertTrue(new File(db.getWorkTree(), "file2").exists());
424 			assertEquals(MergeResult.MergeStatus.FAST_FORWARD_SQUASHED,
425 					result.getMergeStatus());
426 
427 			// comment not set, should be inferred from SQUASH_MSG
428 			RevCommit squashedCommit = git.commit().call();
429 
430 			assertEquals(1, squashedCommit.getParentCount());
431 			assertNull(db.readSquashCommitMsg());
432 			assertEquals("commit: Squashed commit of the following:", db
433 					.getReflogReader(Constants.HEAD).getLastEntry().getComment());
434 			assertEquals("commit: Squashed commit of the following:", db
435 					.getReflogReader(db.getBranch()).getLastEntry().getComment());
436 		}
437 	}
438 
439 	@Test
440 	public void testReflogs() throws Exception {
441 		try (Git git = new Git(db)) {
442 			writeTrashFile("f", "1");
443 			git.add().addFilepattern("f").call();
444 			git.commit().setMessage("c1").call();
445 			writeTrashFile("f", "2");
446 			git.commit().setMessage("c2").setAll(true).setReflogComment(null)
447 					.call();
448 			writeTrashFile("f", "3");
449 			git.commit().setMessage("c3").setAll(true)
450 					.setReflogComment("testRl").call();
451 
452 			db.getReflogReader(Constants.HEAD).getReverseEntries();
453 
454 			assertEquals("testRl;commit (initial): c1;", reflogComments(
455 					db.getReflogReader(Constants.HEAD).getReverseEntries()));
456 			assertEquals("testRl;commit (initial): c1;", reflogComments(
457 					db.getReflogReader(db.getBranch()).getReverseEntries()));
458 		}
459 	}
460 
461 	private static String reflogComments(List<ReflogEntry> entries) {
462 		StringBuilder b = new StringBuilder();
463 		for (ReflogEntry e : entries) {
464 			b.append(e.getComment()).append(";");
465 		}
466 		return b.toString();
467 	}
468 
469 	@Test(expected = WrongRepositoryStateException.class)
470 	public void commitAmendOnInitialShouldFail() throws Exception {
471 		try (Git git = new Git(db)) {
472 			git.commit().setAmend(true).setMessage("initial commit").call();
473 		}
474 	}
475 
476 	@Test
477 	public void commitAmendWithoutAuthorShouldSetOriginalAuthorAndAuthorTime()
478 			throws Exception {
479 		try (Git git = new Git(db)) {
480 			writeTrashFile("file1", "file1");
481 			git.add().addFilepattern("file1").call();
482 
483 			final String authorName = "First Author";
484 			final String authorEmail = "author@example.org";
485 			final Date authorDate = new Date(1349621117000L);
486 			PersonIdent firstAuthor = new PersonIdent(authorName, authorEmail,
487 					authorDate, TimeZone.getTimeZone("UTC"));
488 			git.commit().setMessage("initial commit").setAuthor(firstAuthor).call();
489 
490 			RevCommit amended = git.commit().setAmend(true)
491 					.setMessage("amend commit").call();
492 
493 			PersonIdent amendedAuthor = amended.getAuthorIdent();
494 			assertEquals(authorName, amendedAuthor.getName());
495 			assertEquals(authorEmail, amendedAuthor.getEmailAddress());
496 			assertEquals(authorDate.getTime(), amendedAuthor.getWhen().getTime());
497 		}
498 	}
499 
500 	@Test
501 	public void commitAmendWithAuthorShouldUseIt() throws Exception {
502 		try (Git git = new Git(db)) {
503 			writeTrashFile("file1", "file1");
504 			git.add().addFilepattern("file1").call();
505 			git.commit().setMessage("initial commit").call();
506 
507 			RevCommit amended = git.commit().setAmend(true)
508 					.setAuthor("New Author", "newauthor@example.org")
509 					.setMessage("amend commit").call();
510 
511 			PersonIdent amendedAuthor = amended.getAuthorIdent();
512 			assertEquals("New Author", amendedAuthor.getName());
513 			assertEquals("newauthor@example.org", amendedAuthor.getEmailAddress());
514 		}
515 	}
516 
517 	@Test
518 	public void commitEmptyCommits() throws Exception {
519 		try (Git git = new Git(db)) {
520 
521 			writeTrashFile("file1", "file1");
522 			git.add().addFilepattern("file1").call();
523 			RevCommit initial = git.commit().setMessage("initial commit")
524 					.call();
525 
526 			RevCommit emptyFollowUp = git.commit()
527 					.setAuthor("New Author", "newauthor@example.org")
528 					.setMessage("no change").call();
529 
530 			assertNotEquals(initial.getId(), emptyFollowUp.getId());
531 			assertEquals(initial.getTree().getId(),
532 					emptyFollowUp.getTree().getId());
533 
534 			try {
535 				git.commit().setAuthor("New Author", "newauthor@example.org")
536 						.setMessage("again no change").setAllowEmpty(false)
537 						.call();
538 				fail("Didn't get the expected EmptyCommitException");
539 			} catch (EmptyCommitException e) {
540 				// expect this exception
541 			}
542 
543 			// Allow empty commits also when setOnly was set
544 			git.commit().setAuthor("New Author", "newauthor@example.org")
545 					.setMessage("again no change").setOnly("file1")
546 					.setAllowEmpty(true).call();
547 		}
548 	}
549 
550 	@Test
551 	public void commitOnlyShouldCommitUnmergedPathAndNotAffectOthers()
552 			throws Exception {
553 		DirCache index = db.lockDirCache();
554 		DirCacheBuilder builder = index.builder();
555 		addUnmergedEntry("unmerged1", builder);
556 		addUnmergedEntry("unmerged2", builder);
557 		DirCacheEntry other = new DirCacheEntry("other");
558 		other.setFileMode(FileMode.REGULAR_FILE);
559 		builder.add(other);
560 		builder.commit();
561 
562 		writeTrashFile("unmerged1", "unmerged1 data");
563 		writeTrashFile("unmerged2", "unmerged2 data");
564 		writeTrashFile("other", "other data");
565 
566 		assertEquals("[other, mode:100644]"
567 				+ "[unmerged1, mode:100644, stage:1]"
568 				+ "[unmerged1, mode:100644, stage:2]"
569 				+ "[unmerged1, mode:100644, stage:3]"
570 				+ "[unmerged2, mode:100644, stage:1]"
571 				+ "[unmerged2, mode:100644, stage:2]"
572 				+ "[unmerged2, mode:100644, stage:3]",
573 				indexState(0));
574 
575 		try (Git git = new Git(db)) {
576 			RevCommit commit = git.commit().setOnly("unmerged1")
577 					.setMessage("Only one file").call();
578 
579 			assertEquals("[other, mode:100644]" + "[unmerged1, mode:100644]"
580 					+ "[unmerged2, mode:100644, stage:1]"
581 					+ "[unmerged2, mode:100644, stage:2]"
582 					+ "[unmerged2, mode:100644, stage:3]",
583 					indexState(0));
584 
585 			try (TreeWalk walk = TreeWalk.forPath(db, "unmerged1", commit.getTree())) {
586 				assertEquals(FileMode.REGULAR_FILE, walk.getFileMode(0));
587 			}
588 		}
589 	}
590 
591 	@Test
592 	public void commitOnlyShouldHandleIgnored() throws Exception {
593 		try (Git git = new Git(db)) {
594 			writeTrashFile("subdir/foo", "Hello World");
595 			writeTrashFile("subdir/bar", "Hello World");
596 			writeTrashFile(".gitignore", "bar");
597 			git.add().addFilepattern("subdir").call();
598 			git.commit().setOnly("subdir").setMessage("first commit").call();
599 			assertEquals("[subdir/foo, mode:100644, content:Hello World]",
600 					indexState(CONTENT));
601 		}
602 	}
603 
604 	private void nonNormalizedIndexTest(boolean executable) throws Exception {
605 		String mode = executable ? "100755" : "100644";
606 		try (Git git = new Git(db)) {
607 			// Commit a file with CR/LF into the index
608 			FileBasedConfig config = db.getConfig();
609 			config.setString("core", null, "autocrlf", "false");
610 			config.save();
611 			File testFile = writeTrashFile("file.txt", "line 1\r\nline 2\r\n");
612 			if (executable) {
613 				FS.DETECTED.setExecute(testFile, true);
614 			}
615 			git.add().addFilepattern("file.txt").call();
616 			git.commit().setMessage("Initial").call();
617 			assertEquals(
618 					"[file.txt, mode:" + mode
619 							+ ", content:line 1\r\nline 2\r\n]",
620 					indexState(CONTENT));
621 			config.setString("core", null, "autocrlf", "true");
622 			config.save();
623 			writeTrashFile("file.txt", "line 1\r\nline 1.5\r\nline 2\r\n");
624 			testFile = writeTrashFile("file2.txt", "new\r\nfile\r\n");
625 			if (executable) {
626 				FS.DETECTED.setExecute(testFile, true);
627 			}
628 			git.add().addFilepattern("file.txt").addFilepattern("file2.txt")
629 					.call();
630 			git.commit().setMessage("Second").call();
631 			assertEquals(
632 					"[file.txt, mode:" + mode
633 							+ ", content:line 1\r\nline 1.5\r\nline 2\r\n]"
634 							+ "[file2.txt, mode:" + mode
635 							+ ", content:new\nfile\n]",
636 					indexState(CONTENT));
637 			writeTrashFile("file2.txt", "new\r\nfile\r\ncontent\r\n");
638 			git.add().addFilepattern("file2.txt").call();
639 			git.commit().setMessage("Third").call();
640 			assertEquals(
641 					"[file.txt, mode:" + mode
642 							+ ", content:line 1\r\nline 1.5\r\nline 2\r\n]"
643 							+ "[file2.txt, mode:" + mode
644 							+ ", content:new\nfile\ncontent\n]",
645 					indexState(CONTENT));
646 		}
647 	}
648 
649 	@Test
650 	public void commitWithAutoCrlfAndNonNormalizedIndex() throws Exception {
651 		nonNormalizedIndexTest(false);
652 	}
653 
654 	@Test
655 	public void commitExecutableWithAutoCrlfAndNonNormalizedIndex()
656 			throws Exception {
657 		assumeTrue(FS.DETECTED.supportsExecute());
658 		nonNormalizedIndexTest(true);
659 	}
660 
661 	@Test
662 	public void testDeletionConflictWithAutoCrlf() throws Exception {
663 		try (Git git = new Git(db)) {
664 			// Commit a file with CR/LF into the index
665 			FileBasedConfig config = db.getConfig();
666 			config.setString("core", null, "autocrlf", "false");
667 			config.save();
668 			File file = writeTrashFile("file.txt", "foo\r\n");
669 			git.add().addFilepattern("file.txt").call();
670 			git.commit().setMessage("Initial").call();
671 			// Switch to side branch
672 			git.checkout().setCreateBranch(true).setName("side").call();
673 			assertTrue(file.delete());
674 			git.rm().addFilepattern("file.txt").call();
675 			git.commit().setMessage("Side").call();
676 			// Switch on autocrlf=true
677 			config.setString("core", null, "autocrlf", "true");
678 			config.save();
679 			// Switch back to master and commit a conflict
680 			git.checkout().setName("master").call();
681 			writeTrashFile("file.txt", "foob\r\n");
682 			git.add().addFilepattern("file.txt").call();
683 			assertEquals("[file.txt, mode:100644, content:foob\r\n]",
684 					indexState(CONTENT));
685 			writeTrashFile("g", "file2.txt", "anything");
686 			git.add().addFilepattern("g/file2.txt");
687 			RevCommit master = git.commit().setMessage("Second").call();
688 			// Switch to side branch again so that the deletion is "ours"
689 			git.checkout().setName("side").call();
690 			// Cherry pick master: produces a delete-modify conflict.
691 			CherryPickResult pick = git.cherryPick().include(master).call();
692 			assertEquals("Expected a cherry-pick conflict",
693 					CherryPickStatus.CONFLICTING, pick.getStatus());
694 			// XXX: g/file2.txt should actually be staged already, but isn't.
695 			git.add().addFilepattern("g/file2.txt").call();
696 			// Resolve the conflict by taking the master version
697 			writeTrashFile("file.txt", "foob\r\n");
698 			git.add().addFilepattern("file.txt").call();
699 			git.commit().setMessage("Cherry").call();
700 			// We expect this to be committed with a single LF since there is no
701 			// "ours" stage.
702 			assertEquals(
703 					"[file.txt, mode:100644, content:foob\n]"
704 							+ "[g/file2.txt, mode:100644, content:anything]",
705 					indexState(CONTENT));
706 		}
707 	}
708 
709 	private void testConflictWithAutoCrlf(String baseLf, String lf)
710 			throws Exception {
711 		try (Git git = new Git(db)) {
712 			// Commit a file with CR/LF into the index
713 			FileBasedConfig config = db.getConfig();
714 			config.setString("core", null, "autocrlf", "false");
715 			config.save();
716 			writeTrashFile("file.txt", "foo" + baseLf);
717 			git.add().addFilepattern("file.txt").call();
718 			git.commit().setMessage("Initial").call();
719 			// Switch to side branch
720 			git.checkout().setCreateBranch(true).setName("side").call();
721 			writeTrashFile("file.txt", "bar\r\n");
722 			git.add().addFilepattern("file.txt").call();
723 			RevCommit side = git.commit().setMessage("Side").call();
724 			// Switch back to master and commit a conflict with the given lf
725 			git.checkout().setName("master");
726 			writeTrashFile("file.txt", "foob" + lf);
727 			git.add().addFilepattern("file.txt").call();
728 			git.commit().setMessage("Second").call();
729 			// Switch on autocrlf=true
730 			config.setString("core", null, "autocrlf", "true");
731 			config.save();
732 			// Cherry pick side: conflict. Resolve with CR-LF and commit.
733 			CherryPickResult pick = git.cherryPick().include(side).call();
734 			assertEquals("Expected a cherry-pick conflict",
735 					CherryPickStatus.CONFLICTING, pick.getStatus());
736 			writeTrashFile("file.txt", "foobar\r\n");
737 			git.add().addFilepattern("file.txt").call();
738 			git.commit().setMessage("Second").call();
739 			assertEquals("[file.txt, mode:100644, content:foobar" + lf + "]",
740 					indexState(CONTENT));
741 		}
742 	}
743 
744 	@Test
745 	public void commitConflictWithAutoCrlfBaseCrLfOursLf() throws Exception {
746 		testConflictWithAutoCrlf("\r\n", "\n");
747 	}
748 
749 	@Test
750 	public void commitConflictWithAutoCrlfBaseLfOursLf() throws Exception {
751 		testConflictWithAutoCrlf("\n", "\n");
752 	}
753 
754 	@Test
755 	public void commitConflictWithAutoCrlfBasCrLfOursCrLf() throws Exception {
756 		testConflictWithAutoCrlf("\r\n", "\r\n");
757 	}
758 
759 	@Test
760 	public void commitConflictWithAutoCrlfBaseLfOursCrLf() throws Exception {
761 		testConflictWithAutoCrlf("\n", "\r\n");
762 	}
763 
764 	private static void addUnmergedEntry(String file, DirCacheBuilder builder) {
765 		DirCacheEntry stage1 = new DirCacheEntry(file, DirCacheEntry.STAGE_1);
766 		DirCacheEntry stage2 = new DirCacheEntry(file, DirCacheEntry.STAGE_2);
767 		DirCacheEntry stage3 = new DirCacheEntry(file, DirCacheEntry.STAGE_3);
768 		stage1.setFileMode(FileMode.REGULAR_FILE);
769 		stage2.setFileMode(FileMode.REGULAR_FILE);
770 		stage3.setFileMode(FileMode.REGULAR_FILE);
771 		builder.add(stage1);
772 		builder.add(stage2);
773 		builder.add(stage3);
774 	}
775 
776 	@Test
777 	public void callSignerWithProperSigningKey() throws Exception {
778 		try (Git git = new Git(db)) {
779 			writeTrashFile("file1", "file1");
780 			git.add().addFilepattern("file1").call();
781 
782 			String[] signingKey = new String[1];
783 			PersonIdent[] signingCommitters = new PersonIdent[1];
784 			AtomicInteger callCount = new AtomicInteger();
785 			GpgSigner.setDefault(new GpgSigner() {
786 				@Override
787 				public void sign(CommitBuilder commit, String gpgSigningKey,
788 						PersonIdent signingCommitter, CredentialsProvider credentialsProvider) {
789 					signingKey[0] = gpgSigningKey;
790 					signingCommitters[0] = signingCommitter;
791 					callCount.incrementAndGet();
792 				}
793 
794 				@Override
795 				public boolean canLocateSigningKey(String gpgSigningKey,
796 						PersonIdent signingCommitter,
797 						CredentialsProvider credentialsProvider)
798 						throws CanceledException {
799 					return false;
800 				}
801 			});
802 
803 			// first call should use config, which is expected to be null at
804 			// this time
805 			git.commit().setCommitter(committer).setSign(Boolean.TRUE)
806 					.setMessage("initial commit")
807 					.call();
808 			assertNull(signingKey[0]);
809 			assertEquals(1, callCount.get());
810 			assertSame(committer, signingCommitters[0]);
811 
812 			writeTrashFile("file2", "file2");
813 			git.add().addFilepattern("file2").call();
814 
815 			// second commit applies config value
816 			String expectedConfigSigningKey = "config-" + System.nanoTime();
817 			StoredConfig config = git.getRepository().getConfig();
818 			config.setString("user", null, "signingKey",
819 					expectedConfigSigningKey);
820 			config.save();
821 
822 			git.commit().setCommitter(committer).setSign(Boolean.TRUE)
823 					.setMessage("initial commit")
824 					.call();
825 			assertEquals(expectedConfigSigningKey, signingKey[0]);
826 			assertEquals(2, callCount.get());
827 			assertSame(committer, signingCommitters[0]);
828 
829 			writeTrashFile("file3", "file3");
830 			git.add().addFilepattern("file3").call();
831 
832 			// now use specific on api
833 			String expectedSigningKey = "my-" + System.nanoTime();
834 			git.commit().setCommitter(committer).setSign(Boolean.TRUE)
835 					.setSigningKey(expectedSigningKey)
836 					.setMessage("initial commit").call();
837 			assertEquals(expectedSigningKey, signingKey[0]);
838 			assertEquals(3, callCount.get());
839 			assertSame(committer, signingCommitters[0]);
840 		}
841 	}
842 
843 	@Test
844 	public void callSignerOnlyWhenSigning() throws Exception {
845 		try (Git git = new Git(db)) {
846 			writeTrashFile("file1", "file1");
847 			git.add().addFilepattern("file1").call();
848 
849 			AtomicInteger callCount = new AtomicInteger();
850 			GpgSigner.setDefault(new GpgSigner() {
851 				@Override
852 				public void sign(CommitBuilder commit, String gpgSigningKey,
853 						PersonIdent signingCommitter, CredentialsProvider credentialsProvider) {
854 					callCount.incrementAndGet();
855 				}
856 
857 				@Override
858 				public boolean canLocateSigningKey(String gpgSigningKey,
859 						PersonIdent signingCommitter,
860 						CredentialsProvider credentialsProvider)
861 						throws CanceledException {
862 					return false;
863 				}
864 			});
865 
866 			// first call should use config, which is expected to be null at
867 			// this time
868 			git.commit().setMessage("initial commit").call();
869 			assertEquals(0, callCount.get());
870 
871 			writeTrashFile("file2", "file2");
872 			git.add().addFilepattern("file2").call();
873 
874 			// now force signing
875 			git.commit().setSign(Boolean.TRUE).setMessage("commit").call();
876 			assertEquals(1, callCount.get());
877 
878 			writeTrashFile("file3", "file3");
879 			git.add().addFilepattern("file3").call();
880 
881 			// now rely on config
882 			StoredConfig config = git.getRepository().getConfig();
883 			config.setBoolean("commit", null, "gpgSign", true);
884 			config.save();
885 
886 			git.commit().setMessage("commit").call();
887 			assertEquals(2, callCount.get());
888 
889 			writeTrashFile("file4", "file4");
890 			git.add().addFilepattern("file4").call();
891 
892 			// now force "no-sign" (even though config is true)
893 			git.commit().setSign(Boolean.FALSE).setMessage("commit").call();
894 			assertEquals(2, callCount.get());
895 		}
896 	}
897 }