diff options
Diffstat (limited to 'org.eclipse.jgit.test/tst')
63 files changed, 8108 insertions, 878 deletions
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java index ed3907e9b2..aafda0171c 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java @@ -303,6 +303,21 @@ public class AddCommandTest extends RepositoryTestCase { } @Test + public void testAttributesConflictingMatch() throws Exception { + writeTrashFile(".gitattributes", "foo/** crlf=input\n*.jar binary"); + writeTrashFile("foo/bar.jar", "\r\n"); + // We end up with attributes [binary -diff -merge -text crlf=input]. + // crlf should have no effect when -text is present. + try (Git git = new Git(db)) { + git.add().addFilepattern(".").call(); + assertEquals( + "[.gitattributes, mode:100644, content:foo/** crlf=input\n*.jar binary]" + + "[foo/bar.jar, mode:100644, content:\r\n]", + indexState(CONTENT)); + } + } + + @Test public void testCleanFilterEnvironment() throws IOException, GitAPIException { writeTrashFile(".gitattributes", "*.txt filter=tstFilter"); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CheckoutCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CheckoutCommandTest.java index 3c196724a9..1201d9f391 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CheckoutCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CheckoutCommandTest.java @@ -85,7 +85,6 @@ import org.eclipse.jgit.lib.Sets; import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.storage.file.FileBasedConfig; -import org.eclipse.jgit.transport.RefSpec; import org.eclipse.jgit.transport.RemoteConfig; import org.eclipse.jgit.transport.URIish; import org.eclipse.jgit.util.FileUtils; @@ -431,8 +430,8 @@ public class CheckoutCommandTest extends RepositoryTestCase { config.save(); // fetch from first repository - RefSpec spec = new RefSpec("+refs/heads/*:refs/remotes/origin/*"); - git2.fetch().setRemote("origin").setRefSpecs(spec).call(); + git2.fetch().setRemote("origin") + .setRefSpecs("+refs/heads/*:refs/remotes/origin/*").call(); return db2; } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CloneCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CloneCommandTest.java index ae0b8dd3c2..e687a6ca7f 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CloneCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CloneCommandTest.java @@ -76,6 +76,7 @@ import org.eclipse.jgit.submodule.SubmoduleStatusType; import org.eclipse.jgit.submodule.SubmoduleWalk; import org.eclipse.jgit.transport.RefSpec; import org.eclipse.jgit.transport.RemoteConfig; +import org.eclipse.jgit.transport.URIish; import org.eclipse.jgit.util.SystemReader; import org.junit.Test; @@ -145,16 +146,36 @@ public class CloneCommandTest extends RepositoryTestCase { File directory = createTempDirectory("testCloneRepository"); CloneCommand command = Git.cloneRepository(); command.setDirectory(directory); - command.setGitDir(new File(directory, ".git")); + command.setGitDir(new File(directory, Constants.DOT_GIT)); command.setURI(fileUri()); Git git2 = command.call(); addRepoToClose(git2.getRepository()); assertEquals(directory, git2.getRepository().getWorkTree()); - assertEquals(new File(directory, ".git"), git2.getRepository() + assertEquals(new File(directory, Constants.DOT_GIT), git2.getRepository() .getDirectory()); } @Test + public void testCloneRepositoryDefaultDirectory() + throws URISyntaxException, JGitInternalException { + CloneCommand command = Git.cloneRepository().setURI(fileUri()); + + command.verifyDirectories(new URIish(fileUri())); + File directory = command.getDirectory(); + assertEquals(git.getRepository().getWorkTree().getName(), directory.getName()); + } + + @Test + public void testCloneBareRepositoryDefaultDirectory() + throws URISyntaxException, JGitInternalException { + CloneCommand command = Git.cloneRepository().setURI(fileUri()).setBare(true); + + command.verifyDirectories(new URIish(fileUri())); + File directory = command.getDirectory(); + assertEquals(git.getRepository().getWorkTree().getName() + Constants.DOT_GIT_EXT, directory.getName()); + } + + @Test public void testCloneRepositoryExplicitGitDirNonStd() throws IOException, JGitInternalException, GitAPIException { File directory = createTempDirectory("testCloneRepository"); @@ -168,8 +189,8 @@ public class CloneCommandTest extends RepositoryTestCase { assertEquals(directory, git2.getRepository().getWorkTree()); assertEquals(gDir, git2.getRepository() .getDirectory()); - assertTrue(new File(directory, ".git").isFile()); - assertFalse(new File(gDir, ".git").exists()); + assertTrue(new File(directory, Constants.DOT_GIT).isFile()); + assertFalse(new File(gDir, Constants.DOT_GIT).exists()); } @Test diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java index 37fee402e9..a0834e7e85 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java @@ -557,6 +557,11 @@ public class CommitCommandTest extends RepositoryTestCase { } catch (EmtpyCommitException e) { // expect this exception } + + // Allow empty commits also when setOnly was set + git.commit().setAuthor("New Author", "newauthor@example.org") + .setMessage("again no change").setOnly("file1") + .setAllowEmpty(true).call(); } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DescribeCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DescribeCommandTest.java index 1e5d3bc30e..6a667830e8 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DescribeCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DescribeCommandTest.java @@ -54,6 +54,7 @@ import java.util.Collection; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.RefNotFoundException; +import org.eclipse.jgit.errors.InvalidPatternException; import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.lib.ObjectId; import org.junit.Test; @@ -92,26 +93,65 @@ public class DescribeCommandTest extends RepositoryTestCase { ObjectId c1 = modify("aaa"); ObjectId c2 = modify("bbb"); - tag("t1"); + tag("alice-t1"); ObjectId c3 = modify("ccc"); - tag("t2"); + tag("bob-t2"); ObjectId c4 = modify("ddd"); assertNull(describe(c1)); assertNull(describe(c1, true)); - assertEquals("t1", describe(c2)); - assertEquals("t2", describe(c3)); - assertEquals("t2-0-g44579eb", describe(c3, true)); + assertNull(describe(c1, "a*", "b*", "c*")); + + assertEquals("alice-t1", describe(c2)); + assertEquals("alice-t1", describe(c2, "alice*")); + assertNull(describe(c2, "bob*")); + assertNull(describe(c2, "?ob*")); + assertEquals("alice-t1", describe(c2, "a*", "b*", "c*")); + + assertEquals("bob-t2", describe(c3)); + assertEquals("bob-t2-0-g44579eb", describe(c3, true)); + assertEquals("alice-t1-1-g44579eb", describe(c3, "alice*")); + assertEquals("alice-t1-1-g44579eb", describe(c3, "a??c?-t*")); + assertEquals("bob-t2", describe(c3, "bob*")); + assertEquals("bob-t2", describe(c3, "?ob*")); + assertEquals("bob-t2", describe(c3, "a*", "b*", "c*")); assertNameStartsWith(c4, "3e563c5"); // the value verified with git-describe(1) - assertEquals("t2-1-g3e563c5", describe(c4)); - assertEquals("t2-1-g3e563c5", describe(c4, true)); + assertEquals("bob-t2-1-g3e563c5", describe(c4)); + assertEquals("bob-t2-1-g3e563c5", describe(c4, true)); + assertEquals("alice-t1-2-g3e563c5", describe(c4, "alice*")); + assertEquals("bob-t2-1-g3e563c5", describe(c4, "bob*")); + assertEquals("bob-t2-1-g3e563c5", describe(c4, "a*", "b*", "c*")); // test default target - assertEquals("t2-1-g3e563c5", git.describe().call()); + assertEquals("bob-t2-1-g3e563c5", git.describe().call()); + } + + @Test + public void testDescribeMultiMatch() throws Exception { + ObjectId c1 = modify("aaa"); + tag("v1.0.0"); + tag("v1.1.1"); + ObjectId c2 = modify("bbb"); + + // Ensure that if we're interested in any tags, we get the first match as per Git behaviour + assertEquals("v1.0.0", describe(c1)); + assertEquals("v1.0.0-1-g3747db3", describe(c2)); + + // Ensure that if we're only interested in one of multiple tags, we get the right match + assertEquals("v1.0.0", describe(c1, "v1.0*")); + assertEquals("v1.1.1", describe(c1, "v1.1*")); + assertEquals("v1.0.0-1-g3747db3", describe(c2, "v1.0*")); + assertEquals("v1.1.1-1-g3747db3", describe(c2, "v1.1*")); + + // Ensure that ordering of match precedence is preserved as per Git behaviour + assertEquals("v1.0.0", describe(c1, "v1.0*", "v1.1*")); + assertEquals("v1.1.1", describe(c1, "v1.1*", "v1.0*")); + assertEquals("v1.0.0-1-g3747db3", describe(c2, "v1.0*", "v1.1*")); + assertEquals("v1.1.1-1-g3747db3", describe(c2, "v1.1*", "v1.0*")); } /** @@ -271,6 +311,10 @@ public class DescribeCommandTest extends RepositoryTestCase { return describe(c1, false); } + private String describe(ObjectId c1, String... patterns) throws GitAPIException, IOException, InvalidPatternException { + return git.describe().setTarget(c1).setMatch(patterns).call(); + } + private static void assertNameStartsWith(ObjectId c4, String prefix) { assertTrue(c4.name(), c4.name().startsWith(prefix)); } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/FetchCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/FetchCommandTest.java index a36f6c551a..530fb1b2fe 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/FetchCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/FetchCommandTest.java @@ -45,7 +45,9 @@ package org.eclipse.jgit.api; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import java.util.ArrayList; import java.util.Collection; +import java.util.List; import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.lib.Constants; @@ -93,9 +95,8 @@ public class FetchCommandTest extends RepositoryTestCase { RevCommit commit = remoteGit.commit().setMessage("initial commit").call(); Ref tagRef = remoteGit.tag().setName("tag").call(); - RefSpec spec = new RefSpec("refs/heads/master:refs/heads/x"); - git.fetch().setRemote("test").setRefSpecs(spec) - .call(); + git.fetch().setRemote("test") + .setRefSpecs("refs/heads/master:refs/heads/x").call(); assertEquals(commit.getId(), db.resolve(commit.getId().getName() + "^{commit}")); @@ -104,12 +105,97 @@ public class FetchCommandTest extends RepositoryTestCase { } @Test + public void fetchAddsBranches() throws Exception { + final String branch1 = "b1"; + final String branch2 = "b2"; + final String remoteBranch1 = "test/" + branch1; + final String remoteBranch2 = "test/" + branch2; + remoteGit.commit().setMessage("commit").call(); + Ref branchRef1 = remoteGit.branchCreate().setName(branch1).call(); + remoteGit.commit().setMessage("commit").call(); + Ref branchRef2 = remoteGit.branchCreate().setName(branch2).call(); + + String spec = "refs/heads/*:refs/remotes/test/*"; + git.fetch().setRemote("test").setRefSpecs(spec).call(); + assertEquals(branchRef1.getObjectId(), db.resolve(remoteBranch1)); + assertEquals(branchRef2.getObjectId(), db.resolve(remoteBranch2)); + } + + @Test + public void fetchDoesntDeleteBranches() throws Exception { + final String branch1 = "b1"; + final String branch2 = "b2"; + final String remoteBranch1 = "test/" + branch1; + final String remoteBranch2 = "test/" + branch2; + remoteGit.commit().setMessage("commit").call(); + Ref branchRef1 = remoteGit.branchCreate().setName(branch1).call(); + remoteGit.commit().setMessage("commit").call(); + Ref branchRef2 = remoteGit.branchCreate().setName(branch2).call(); + + String spec = "refs/heads/*:refs/remotes/test/*"; + git.fetch().setRemote("test").setRefSpecs(spec).call(); + assertEquals(branchRef1.getObjectId(), db.resolve(remoteBranch1)); + assertEquals(branchRef2.getObjectId(), db.resolve(remoteBranch2)); + + remoteGit.branchDelete().setBranchNames(branch1).call(); + git.fetch().setRemote("test").setRefSpecs(spec).call(); + assertEquals(branchRef1.getObjectId(), db.resolve(remoteBranch1)); + assertEquals(branchRef2.getObjectId(), db.resolve(remoteBranch2)); + } + + @Test + public void fetchUpdatesBranches() throws Exception { + final String branch1 = "b1"; + final String branch2 = "b2"; + final String remoteBranch1 = "test/" + branch1; + final String remoteBranch2 = "test/" + branch2; + remoteGit.commit().setMessage("commit").call(); + Ref branchRef1 = remoteGit.branchCreate().setName(branch1).call(); + remoteGit.commit().setMessage("commit").call(); + Ref branchRef2 = remoteGit.branchCreate().setName(branch2).call(); + + String spec = "refs/heads/*:refs/remotes/test/*"; + git.fetch().setRemote("test").setRefSpecs(spec).call(); + assertEquals(branchRef1.getObjectId(), db.resolve(remoteBranch1)); + assertEquals(branchRef2.getObjectId(), db.resolve(remoteBranch2)); + + remoteGit.commit().setMessage("commit").call(); + branchRef2 = remoteGit.branchCreate().setName(branch2).setForce(true).call(); + git.fetch().setRemote("test").setRefSpecs(spec).call(); + assertEquals(branchRef1.getObjectId(), db.resolve(remoteBranch1)); + assertEquals(branchRef2.getObjectId(), db.resolve(remoteBranch2)); + } + + @Test + public void fetchPrunesBranches() throws Exception { + final String branch1 = "b1"; + final String branch2 = "b2"; + final String remoteBranch1 = "test/" + branch1; + final String remoteBranch2 = "test/" + branch2; + remoteGit.commit().setMessage("commit").call(); + Ref branchRef1 = remoteGit.branchCreate().setName(branch1).call(); + remoteGit.commit().setMessage("commit").call(); + Ref branchRef2 = remoteGit.branchCreate().setName(branch2).call(); + + String spec = "refs/heads/*:refs/remotes/test/*"; + git.fetch().setRemote("test").setRefSpecs(spec).call(); + assertEquals(branchRef1.getObjectId(), db.resolve(remoteBranch1)); + assertEquals(branchRef2.getObjectId(), db.resolve(remoteBranch2)); + + remoteGit.branchDelete().setBranchNames(branch1).call(); + git.fetch().setRemote("test").setRefSpecs(spec) + .setRemoveDeletedRefs(true).call(); + assertNull(db.resolve(remoteBranch1)); + assertEquals(branchRef2.getObjectId(), db.resolve(remoteBranch2)); + } + + @Test public void fetchShouldAutoFollowTag() throws Exception { remoteGit.commit().setMessage("commit").call(); Ref tagRef = remoteGit.tag().setName("foo").call(); - RefSpec spec = new RefSpec("refs/heads/*:refs/remotes/origin/*"); - git.fetch().setRemote("test").setRefSpecs(spec) + git.fetch().setRemote("test") + .setRefSpecs("refs/heads/*:refs/remotes/origin/*") .setTagOpt(TagOpt.AUTO_FOLLOW).call(); assertEquals(tagRef.getObjectId(), db.resolve("foo")); @@ -120,8 +206,8 @@ public class FetchCommandTest extends RepositoryTestCase { remoteGit.commit().setMessage("commit").call(); Ref tagRef = remoteGit.tag().setName("foo").call(); remoteGit.commit().setMessage("commit2").call(); - RefSpec spec = new RefSpec("refs/heads/*:refs/remotes/origin/*"); - git.fetch().setRemote("test").setRefSpecs(spec) + git.fetch().setRemote("test") + .setRefSpecs("refs/heads/*:refs/remotes/origin/*") .setTagOpt(TagOpt.AUTO_FOLLOW).call(); assertEquals(tagRef.getObjectId(), db.resolve("foo")); } @@ -132,9 +218,8 @@ public class FetchCommandTest extends RepositoryTestCase { remoteGit.checkout().setName("other").setCreateBranch(true).call(); remoteGit.commit().setMessage("commit2").call(); remoteGit.tag().setName("foo").call(); - RefSpec spec = new RefSpec( - "refs/heads/master:refs/remotes/origin/master"); - git.fetch().setRemote("test").setRefSpecs(spec) + git.fetch().setRemote("test") + .setRefSpecs("refs/heads/master:refs/remotes/origin/master") .setTagOpt(TagOpt.AUTO_FOLLOW).call(); assertNull(db.resolve("foo")); } @@ -146,7 +231,7 @@ public class FetchCommandTest extends RepositoryTestCase { Ref tagRef = remoteGit.tag().setName(tagName).call(); ObjectId originalId = tagRef.getObjectId(); - RefSpec spec = new RefSpec("refs/heads/*:refs/remotes/origin/*"); + String spec = "refs/heads/*:refs/remotes/origin/*"; git.fetch().setRemote("test").setRefSpecs(spec) .setTagOpt(TagOpt.AUTO_FOLLOW).call(); assertEquals(originalId, db.resolve(tagName)); @@ -172,7 +257,7 @@ public class FetchCommandTest extends RepositoryTestCase { remoteGit.commit().setMessage("commit").call(); Ref tagRef1 = remoteGit.tag().setName(tagName).call(); - RefSpec spec = new RefSpec("refs/heads/*:refs/remotes/origin/*"); + String spec = "refs/heads/*:refs/remotes/origin/*"; git.fetch().setRemote("test").setRefSpecs(spec) .setTagOpt(TagOpt.AUTO_FOLLOW).call(); assertEquals(tagRef1.getObjectId(), db.resolve(tagName)); @@ -188,4 +273,75 @@ public class FetchCommandTest extends RepositoryTestCase { assertEquals(RefUpdate.Result.FORCED, update.getResult()); assertEquals(tagRef2.getObjectId(), db.resolve(tagName)); } + + @Test + public void fetchAddRefsWithDuplicateRefspec() throws Exception { + final String branchName = "branch"; + final String remoteBranchName = "test/" + branchName; + remoteGit.commit().setMessage("commit").call(); + Ref branchRef = remoteGit.branchCreate().setName(branchName).call(); + + final String spec1 = "+refs/heads/*:refs/remotes/test/*"; + final String spec2 = "refs/heads/*:refs/remotes/test/*"; + final StoredConfig config = db.getConfig(); + RemoteConfig remoteConfig = new RemoteConfig(config, "test"); + remoteConfig.addFetchRefSpec(new RefSpec(spec1)); + remoteConfig.addFetchRefSpec(new RefSpec(spec2)); + remoteConfig.update(config); + + git.fetch().setRemote("test").setRefSpecs(spec1).call(); + assertEquals(branchRef.getObjectId(), db.resolve(remoteBranchName)); + } + + @Test + public void fetchPruneRefsWithDuplicateRefspec() + throws Exception { + final String branchName = "branch"; + final String remoteBranchName = "test/" + branchName; + remoteGit.commit().setMessage("commit").call(); + Ref branchRef = remoteGit.branchCreate().setName(branchName).call(); + + final String spec1 = "+refs/heads/*:refs/remotes/test/*"; + final String spec2 = "refs/heads/*:refs/remotes/test/*"; + final StoredConfig config = db.getConfig(); + RemoteConfig remoteConfig = new RemoteConfig(config, "test"); + remoteConfig.addFetchRefSpec(new RefSpec(spec1)); + remoteConfig.addFetchRefSpec(new RefSpec(spec2)); + remoteConfig.update(config); + + git.fetch().setRemote("test").setRefSpecs(spec1).call(); + assertEquals(branchRef.getObjectId(), db.resolve(remoteBranchName)); + + remoteGit.branchDelete().setBranchNames(branchName).call(); + git.fetch().setRemote("test").setRefSpecs(spec1) + .setRemoveDeletedRefs(true).call(); + assertNull(db.resolve(remoteBranchName)); + } + + @Test + public void fetchUpdateRefsWithDuplicateRefspec() throws Exception { + final String tagName = "foo"; + remoteGit.commit().setMessage("commit").call(); + Ref tagRef1 = remoteGit.tag().setName(tagName).call(); + List<RefSpec> refSpecs = new ArrayList<>(); + refSpecs.add(new RefSpec("+refs/heads/*:refs/remotes/origin/*")); + refSpecs.add(new RefSpec("+refs/tags/*:refs/tags/*")); + // Updating tags via the RefSpecs and setting TagOpt.FETCH_TAGS (or + // AUTO_FOLLOW) will result internally in *two* updates for the same + // ref. + git.fetch().setRemote("test").setRefSpecs(refSpecs) + .setTagOpt(TagOpt.AUTO_FOLLOW).call(); + assertEquals(tagRef1.getObjectId(), db.resolve(tagName)); + + remoteGit.commit().setMessage("commit 2").call(); + Ref tagRef2 = remoteGit.tag().setName(tagName).setForceUpdate(true) + .call(); + FetchResult result = git.fetch().setRemote("test").setRefSpecs(refSpecs) + .setTagOpt(TagOpt.FETCH_TAGS).call(); + assertEquals(2, result.getTrackingRefUpdates().size()); + TrackingRefUpdate update = result + .getTrackingRefUpdate(Constants.R_TAGS + tagName); + assertEquals(RefUpdate.Result.FORCED, update.getResult()); + assertEquals(tagRef2.getObjectId(), db.resolve(tagName)); + } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/LogCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/LogCommandTest.java index 38178bfd0e..bd0efad016 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/LogCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/LogCommandTest.java @@ -289,4 +289,4 @@ public class LogCommandTest extends RepositoryTestCase { .setMessage("merge s0 with m1").call(); } -}
\ No newline at end of file +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java index 823516b99b..a341284850 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java @@ -620,4 +620,4 @@ public class PullCommandTest extends RepositoryTestCase { fis.close(); } } -}
\ No newline at end of file +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PushCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PushCommandTest.java index 8c613ec488..e0c1499030 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PushCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PushCommandTest.java @@ -83,6 +83,11 @@ public class PushCommandTest extends RepositoryTestCase { // create other repository Repository db2 = createWorkRepository(); + final StoredConfig config2 = db2.getConfig(); + + // this tests that this config can be parsed properly + config2.setString("fsck", "", "missingEmail", "ignore"); + config2.save(); // setup the first repository final StoredConfig config = db.getConfig(); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashApplyCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashApplyCommandTest.java index f2e4d5b3b3..ad3ab7fbdf 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashApplyCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashApplyCommandTest.java @@ -55,12 +55,15 @@ import org.eclipse.jgit.api.errors.InvalidRefNameException; import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.api.errors.NoHeadException; import org.eclipse.jgit.api.errors.StashApplyFailureException; +import org.eclipse.jgit.events.ChangeRecorder; +import org.eclipse.jgit.events.ListenerHandle; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.util.FileUtils; +import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -77,15 +80,31 @@ public class StashApplyCommandTest extends RepositoryTestCase { private File committedFile; + private ChangeRecorder recorder; + + private ListenerHandle handle; + @Override @Before public void setUp() throws Exception { super.setUp(); git = Git.wrap(db); + recorder = new ChangeRecorder(); + handle = db.getListenerList().addWorkingTreeModifiedListener(recorder); committedFile = writeTrashFile(PATH, "content"); git.add().addFilepattern(PATH).call(); head = git.commit().setMessage("add file").call(); assertNotNull(head); + recorder.assertNoEvent(); + } + + @Override + @After + public void tearDown() throws Exception { + if (handle != null) { + handle.remove(); + } + super.tearDown(); } @Test @@ -95,10 +114,12 @@ public class StashApplyCommandTest extends RepositoryTestCase { RevCommit stashed = git.stashCreate().call(); assertNotNull(stashed); assertEquals("content", read(committedFile)); + recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY); ObjectId unstashed = git.stashApply().call(); assertEquals(stashed, unstashed); assertFalse(committedFile.exists()); + recorder.assertEvent(ChangeRecorder.EMPTY, new String[] { PATH }); Status status = git.status().call(); assertTrue(status.getAdded().isEmpty()); @@ -121,11 +142,13 @@ public class StashApplyCommandTest extends RepositoryTestCase { RevCommit stashed = git.stashCreate().call(); assertNotNull(stashed); assertFalse(addedFile.exists()); + recorder.assertEvent(ChangeRecorder.EMPTY, new String[] { addedPath }); ObjectId unstashed = git.stashApply().call(); assertEquals(stashed, unstashed); assertTrue(addedFile.exists()); assertEquals("content2", read(addedFile)); + recorder.assertEvent(new String[] { addedPath }, ChangeRecorder.EMPTY); Status status = git.status().call(); assertTrue(status.getChanged().isEmpty()); @@ -142,14 +165,17 @@ public class StashApplyCommandTest extends RepositoryTestCase { @Test public void indexDelete() throws Exception { git.rm().addFilepattern("file.txt").call(); + recorder.assertEvent(ChangeRecorder.EMPTY, new String[] { "file.txt" }); RevCommit stashed = git.stashCreate().call(); assertNotNull(stashed); assertEquals("content", read(committedFile)); + recorder.assertEvent(new String[] { "file.txt" }, ChangeRecorder.EMPTY); ObjectId unstashed = git.stashApply().call(); assertEquals(stashed, unstashed); assertFalse(committedFile.exists()); + recorder.assertEvent(ChangeRecorder.EMPTY, new String[] { "file.txt" }); Status status = git.status().call(); assertTrue(status.getAdded().isEmpty()); @@ -170,10 +196,12 @@ public class StashApplyCommandTest extends RepositoryTestCase { RevCommit stashed = git.stashCreate().call(); assertNotNull(stashed); assertEquals("content", read(committedFile)); + recorder.assertEvent(new String[] { "file.txt" }, ChangeRecorder.EMPTY); ObjectId unstashed = git.stashApply().call(); assertEquals(stashed, unstashed); assertEquals("content2", read(committedFile)); + recorder.assertEvent(new String[] { "file.txt" }, ChangeRecorder.EMPTY); Status status = git.status().call(); assertTrue(status.getAdded().isEmpty()); @@ -193,16 +221,21 @@ public class StashApplyCommandTest extends RepositoryTestCase { File subfolderFile = writeTrashFile(path, "content"); git.add().addFilepattern(path).call(); head = git.commit().setMessage("add file").call(); + recorder.assertNoEvent(); writeTrashFile(path, "content2"); RevCommit stashed = git.stashCreate().call(); assertNotNull(stashed); assertEquals("content", read(subfolderFile)); + recorder.assertEvent(new String[] { "d1/d2/f.txt" }, + ChangeRecorder.EMPTY); ObjectId unstashed = git.stashApply().call(); assertEquals(stashed, unstashed); assertEquals("content2", read(subfolderFile)); + recorder.assertEvent(new String[] { "d1/d2/f.txt", "d1/d2", "d1" }, + ChangeRecorder.EMPTY); Status status = git.status().call(); assertTrue(status.getAdded().isEmpty()); @@ -225,10 +258,12 @@ public class StashApplyCommandTest extends RepositoryTestCase { RevCommit stashed = git.stashCreate().call(); assertNotNull(stashed); assertEquals("content", read(committedFile)); + recorder.assertEvent(new String[] { "file.txt" }, ChangeRecorder.EMPTY); ObjectId unstashed = git.stashApply().call(); assertEquals(stashed, unstashed); assertEquals("content3", read(committedFile)); + recorder.assertEvent(new String[] { "file.txt" }, ChangeRecorder.EMPTY); Status status = git.status().call(); assertTrue(status.getAdded().isEmpty()); @@ -252,10 +287,12 @@ public class StashApplyCommandTest extends RepositoryTestCase { RevCommit stashed = git.stashCreate().call(); assertNotNull(stashed); assertEquals("content", read(committedFile)); + recorder.assertEvent(new String[] { "file.txt" }, ChangeRecorder.EMPTY); ObjectId unstashed = git.stashApply().call(); assertEquals(stashed, unstashed); assertEquals("content2", read(committedFile)); + recorder.assertEvent(new String[] { "file.txt" }, ChangeRecorder.EMPTY); Status status = git.status().call(); assertTrue(status.getAdded().isEmpty()); @@ -281,10 +318,12 @@ public class StashApplyCommandTest extends RepositoryTestCase { RevCommit stashed = git.stashCreate().call(); assertNotNull(stashed); assertFalse(added.exists()); + recorder.assertNoEvent(); ObjectId unstashed = git.stashApply().call(); assertEquals(stashed, unstashed); assertEquals("content2", read(added)); + recorder.assertEvent(new String[] { path }, ChangeRecorder.EMPTY); Status status = git.status().call(); assertTrue(status.getChanged().isEmpty()); @@ -308,10 +347,12 @@ public class StashApplyCommandTest extends RepositoryTestCase { RevCommit stashed = git.stashCreate().call(); assertNotNull(stashed); assertEquals("content", read(committedFile)); + recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY); ObjectId unstashed = git.stashApply().call(); assertEquals(stashed, unstashed); assertFalse(committedFile.exists()); + recorder.assertEvent(ChangeRecorder.EMPTY, new String[] { PATH }); Status status = git.status().call(); assertTrue(status.getAdded().isEmpty()); @@ -337,9 +378,13 @@ public class StashApplyCommandTest extends RepositoryTestCase { assertNotNull(stashed); assertTrue(committedFile.exists()); assertFalse(addedFile.exists()); + recorder.assertEvent(new String[] { PATH }, + new String[] { "file2.txt" }); ObjectId unstashed = git.stashApply().call(); assertEquals(stashed, unstashed); + recorder.assertEvent(new String[] { "file2.txt" }, + new String[] { PATH }); Status status = git.status().call(); assertTrue(status.getChanged().isEmpty()); @@ -362,6 +407,7 @@ public class StashApplyCommandTest extends RepositoryTestCase { assertNotNull(stashed); assertEquals("content", read(committedFile)); assertTrue(git.status().call().isClean()); + recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY); writeTrashFile(PATH, "content3"); @@ -372,6 +418,7 @@ public class StashApplyCommandTest extends RepositoryTestCase { // expected } assertEquals("content3", read(PATH)); + recorder.assertNoEvent(); } @Test @@ -391,10 +438,12 @@ public class StashApplyCommandTest extends RepositoryTestCase { assertEquals("content\nhead change\nmore content\n", read(committedFile)); assertTrue(git.status().call().isClean()); + recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY); writeTrashFile(PATH, "content\nmore content\ncommitted change\n"); git.add().addFilepattern(PATH).call(); git.commit().setMessage("committed change").call(); + recorder.assertNoEvent(); try { git.stashApply().call(); @@ -402,6 +451,7 @@ public class StashApplyCommandTest extends RepositoryTestCase { } catch (StashApplyFailureException e) { // expected } + recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY); Status status = new StatusCommand(db).call(); assertEquals(1, status.getConflicting().size()); assertEquals( @@ -426,12 +476,15 @@ public class StashApplyCommandTest extends RepositoryTestCase { writeTrashFile(PATH, "master content"); git.add().addFilepattern(PATH).call(); git.commit().setMessage("even content").call(); + recorder.assertNoEvent(); git.checkout().setName(otherBranch).call(); + recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY); writeTrashFile(PATH, "otherBranch content"); git.add().addFilepattern(PATH).call(); git.commit().setMessage("even more content").call(); + recorder.assertNoEvent(); writeTrashFile(path2, "content\nstashed change\nmore content\n"); @@ -442,12 +495,15 @@ public class StashApplyCommandTest extends RepositoryTestCase { assertEquals("otherBranch content", read(committedFile)); assertTrue(git.status().call().isClean()); + recorder.assertEvent(new String[] { path2 }, ChangeRecorder.EMPTY); git.checkout().setName("master").call(); + recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY); git.stashApply().call(); assertEquals("content\nstashed change\nmore content\n", read(file2)); assertEquals("master content", read(committedFile)); + recorder.assertEvent(new String[] { path2 }, ChangeRecorder.EMPTY); } @Test @@ -467,12 +523,15 @@ public class StashApplyCommandTest extends RepositoryTestCase { writeTrashFile(PATH, "master content"); git.add().addFilepattern(PATH).call(); git.commit().setMessage("even content").call(); + recorder.assertNoEvent(); git.checkout().setName(otherBranch).call(); + recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY); writeTrashFile(PATH, "otherBranch content"); git.add().addFilepattern(PATH).call(); git.commit().setMessage("even more content").call(); + recorder.assertNoEvent(); writeTrashFile(path2, "content\nstashed change in index\nmore content\n"); @@ -485,8 +544,10 @@ public class StashApplyCommandTest extends RepositoryTestCase { assertEquals("content\nmore content\n", read(file2)); assertEquals("otherBranch content", read(committedFile)); assertTrue(git.status().call().isClean()); + recorder.assertEvent(new String[] { path2 }, ChangeRecorder.EMPTY); git.checkout().setName("master").call(); + recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY); git.stashApply().call(); assertEquals("content\nstashed change\nmore content\n", read(file2)); assertEquals( @@ -494,6 +555,7 @@ public class StashApplyCommandTest extends RepositoryTestCase { + "[file2.txt, mode:100644, content:content\nstashed change in index\nmore content\n]", indexState(CONTENT)); assertEquals("master content", read(committedFile)); + recorder.assertEvent(new String[] { path2 }, ChangeRecorder.EMPTY); } @Test @@ -501,6 +563,7 @@ public class StashApplyCommandTest extends RepositoryTestCase { writeTrashFile(PATH, "content\nmore content\n"); git.add().addFilepattern(PATH).call(); git.commit().setMessage("more content").call(); + recorder.assertNoEvent(); writeTrashFile(PATH, "content\nstashed change\nmore content\n"); @@ -508,15 +571,18 @@ public class StashApplyCommandTest extends RepositoryTestCase { assertNotNull(stashed); assertEquals("content\nmore content\n", read(committedFile)); assertTrue(git.status().call().isClean()); + recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY); writeTrashFile(PATH, "content\nmore content\ncommitted change\n"); git.add().addFilepattern(PATH).call(); git.commit().setMessage("committed change").call(); + recorder.assertNoEvent(); git.stashApply().call(); assertEquals( "content\nstashed change\nmore content\ncommitted change\n", read(committedFile)); + recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY); } @Test @@ -527,6 +593,7 @@ public class StashApplyCommandTest extends RepositoryTestCase { assertNotNull(stashed); assertEquals("content", read(committedFile)); assertTrue(git.status().call().isClean()); + recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY); writeTrashFile(PATH, "content3"); git.add().addFilepattern(PATH).call(); @@ -538,6 +605,7 @@ public class StashApplyCommandTest extends RepositoryTestCase { } catch (StashApplyFailureException e) { // expected } + recorder.assertNoEvent(); assertEquals("content2", read(PATH)); } @@ -549,6 +617,7 @@ public class StashApplyCommandTest extends RepositoryTestCase { assertNotNull(stashed); assertEquals("content", read(committedFile)); assertTrue(git.status().call().isClean()); + recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY); String path2 = "file2.txt"; writeTrashFile(path2, "content3"); @@ -557,6 +626,7 @@ public class StashApplyCommandTest extends RepositoryTestCase { ObjectId unstashed = git.stashApply().call(); assertEquals(stashed, unstashed); + recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY); Status status = git.status().call(); assertTrue(status.getAdded().isEmpty()); @@ -583,12 +653,15 @@ public class StashApplyCommandTest extends RepositoryTestCase { RevCommit stashed = git.stashCreate().call(); assertNotNull(stashed); assertTrue(git.status().call().isClean()); + recorder.assertEvent(ChangeRecorder.EMPTY, + new String[] { subdir, path }); git.branchCreate().setName(otherBranch).call(); git.checkout().setName(otherBranch).call(); ObjectId unstashed = git.stashApply().call(); assertEquals(stashed, unstashed); + recorder.assertEvent(new String[] { path }, ChangeRecorder.EMPTY); Status status = git.status().call(); assertTrue(status.getChanged().isEmpty()); @@ -643,12 +716,15 @@ public class StashApplyCommandTest extends RepositoryTestCase { git.commit().setMessage("x").call(); file.delete(); git.rm().addFilepattern("file").call(); + recorder.assertNoEvent(); git.stashCreate().call(); + recorder.assertEvent(new String[] { "file" }, ChangeRecorder.EMPTY); file.delete(); git.stashApply().setStashRef("stash@{0}").call(); assertFalse(file.exists()); + recorder.assertEvent(ChangeRecorder.EMPTY, new String[] { "file" }); } @Test @@ -660,9 +736,11 @@ public class StashApplyCommandTest extends RepositoryTestCase { git.add().addFilepattern(PATH).call(); git.stashCreate().call(); assertTrue(untrackedFile.exists()); + recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY); git.stashApply().setStashRef("stash@{0}").call(); assertTrue(untrackedFile.exists()); + recorder.assertEvent(new String[] { PATH }, ChangeRecorder.EMPTY); Status status = git.status().call(); assertEquals(1, status.getUntracked().size()); @@ -684,11 +762,14 @@ public class StashApplyCommandTest extends RepositoryTestCase { .call(); assertNotNull(stashedCommit); assertFalse(untrackedFile.exists()); + recorder.assertEvent(ChangeRecorder.EMPTY, new String[] { path }); + deleteTrashFile("a/b"); // checkout should create parent dirs git.stashApply().setStashRef("stash@{0}").call(); assertTrue(untrackedFile.exists()); assertEquals("content", read(path)); + recorder.assertEvent(new String[] { path }, ChangeRecorder.EMPTY); Status status = git.status().call(); assertEquals(1, status.getUntracked().size()); @@ -706,6 +787,7 @@ public class StashApplyCommandTest extends RepositoryTestCase { String path = "untracked.txt"; writeTrashFile(path, "untracked"); git.stashCreate().setIncludeUntracked(true).call(); + recorder.assertEvent(ChangeRecorder.EMPTY, new String[] { path }); writeTrashFile(path, "committed"); head = git.commit().setMessage("add file").call(); @@ -719,6 +801,7 @@ public class StashApplyCommandTest extends RepositoryTestCase { assertEquals(e.getMessage(), JGitText.get().stashApplyConflict); } assertEquals("committed", read(path)); + recorder.assertNoEvent(); } @Test @@ -727,6 +810,7 @@ public class StashApplyCommandTest extends RepositoryTestCase { String path = "untracked.txt"; writeTrashFile(path, "untracked"); git.stashCreate().setIncludeUntracked(true).call(); + recorder.assertEvent(ChangeRecorder.EMPTY, new String[] { path }); writeTrashFile(path, "working-directory"); try { @@ -736,6 +820,7 @@ public class StashApplyCommandTest extends RepositoryTestCase { assertEquals(e.getMessage(), JGitText.get().stashApplyConflict); } assertEquals("working-directory", read(path)); + recorder.assertNoEvent(); } @Test @@ -747,11 +832,13 @@ public class StashApplyCommandTest extends RepositoryTestCase { assertTrue(PATH + " should exist", check(PATH)); assertEquals(PATH + " should have been reset", "content", read(PATH)); assertFalse(path + " should not exist", check(path)); + recorder.assertEvent(new String[] { PATH }, new String[] { path }); git.stashApply().setStashRef("stash@{0}").call(); assertTrue(PATH + " should exist", check(PATH)); assertEquals(PATH + " should have new content", "changed", read(PATH)); assertTrue(path + " should exist", check(path)); assertEquals(path + " should have new content", "untracked", read(path)); + recorder.assertEvent(new String[] { PATH, path }, ChangeRecorder.EMPTY); } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashListCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashListCommandTest.java index a7e0ab9f51..d658a53942 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashListCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashListCommandTest.java @@ -51,6 +51,7 @@ import java.util.Iterator; import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.RefUpdate.Result; import org.eclipse.jgit.revwalk.RevCommit; @@ -94,9 +95,7 @@ public class StashListCommandTest extends RepositoryTestCase { git.add().addFilepattern("file.txt").call(); RevCommit commit = git.commit().setMessage("create file").call(); - RefUpdate update = db.updateRef(Constants.R_STASH); - update.setNewObjectId(commit); - assertEquals(Result.NEW, update.update()); + assertEquals(Result.NEW, newStashUpdate(commit).update()); StashListCommand command = git.stashList(); Collection<RevCommit> stashed = command.call(); @@ -117,13 +116,8 @@ public class StashListCommandTest extends RepositoryTestCase { git.add().addFilepattern("file.txt").call(); RevCommit commit2 = git.commit().setMessage("edit file").call(); - RefUpdate create = db.updateRef(Constants.R_STASH); - create.setNewObjectId(commit1); - assertEquals(Result.NEW, create.update()); - - RefUpdate update = db.updateRef(Constants.R_STASH); - update.setNewObjectId(commit2); - assertEquals(Result.FAST_FORWARD, update.update()); + assertEquals(Result.NEW, newStashUpdate(commit1).update()); + assertEquals(Result.FAST_FORWARD, newStashUpdate(commit2).update()); StashListCommand command = git.stashList(); Collection<RevCommit> stashed = command.call(); @@ -133,4 +127,11 @@ public class StashListCommandTest extends RepositoryTestCase { assertEquals(commit2, iter.next()); assertEquals(commit1, iter.next()); } + + private RefUpdate newStashUpdate(ObjectId newId) throws Exception { + RefUpdate ru = db.updateRef(Constants.R_STASH); + ru.setNewObjectId(newId); + ru.setForceRefLog(true); + return ru; + } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesHandlerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesHandlerTest.java index ca456b3c8a..5868482c88 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesHandlerTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesHandlerTest.java @@ -1,4 +1,7 @@ /* + * Copyright (C) 2015, 2017 Ivan Motsch <ivan.motsch@bsiag.com> + * and other copyright owners as documented in the project's IP log. + * * This program and the accompanying materials are made available * under the terms of the Eclipse Distribution License v1.0 which * accompanies this distribution, is reproduced below, and is @@ -254,6 +257,282 @@ public class AttributesHandlerTest extends RepositoryTestCase { endWalk(); } + @Test + public void testRelativePaths() throws Exception { + setupRepo("sub/ global", "sub/** init", + "sub/** top_sub\n*.txt top", + "sub/** subsub\nsub/ subsub2\n*.txt foo"); + // The last sub/** is in sub/.gitattributes. It must not + // apply to any of the files here. It would match for a + // further subdirectory sub/sub. The sub/ rules must match + // only for directories. + walk = beginWalk(); + assertIteration(F, ".gitattributes"); + assertIteration(D, "sub", attrs("global")); + assertIteration(F, "sub/.gitattributes", attrs("init top_sub")); + assertIteration(F, "sub/a.txt", attrs("init foo top top_sub")); + endWalk(); + // All right, let's see that they *do* apply in sub/sub: + writeTrashFile("sub/sub/b.txt", "b"); + walk = beginWalk(); + assertIteration(F, ".gitattributes"); + assertIteration(D, "sub", attrs("global")); + assertIteration(F, "sub/.gitattributes", attrs("init top_sub")); + assertIteration(F, "sub/a.txt", attrs("init foo top top_sub")); + assertIteration(D, "sub/sub", attrs("init subsub2 top_sub global")); + assertIteration(F, "sub/sub/b.txt", + attrs("init foo subsub top top_sub")); + endWalk(); + } + + @Test + public void testNestedMatchNot() throws Exception { + setupRepo(null, null, "*.xml xml\n*.jar jar", null); + writeTrashFile("foo.xml/bar.jar", "b"); + writeTrashFile("foo.xml/bar.xml", "bx"); + writeTrashFile("sub/b.jar", "bj"); + writeTrashFile("sub/b.xml", "bx"); + // On foo.xml/bar.jar we must not have 'xml' + walk = beginWalk(); + assertIteration(F, ".gitattributes"); + assertIteration(D, "foo.xml", attrs("xml")); + assertIteration(F, "foo.xml/bar.jar", attrs("jar")); + assertIteration(F, "foo.xml/bar.xml", attrs("xml")); + assertIteration(D, "sub"); + assertIteration(F, "sub/a.txt"); + assertIteration(F, "sub/b.jar", attrs("jar")); + assertIteration(F, "sub/b.xml", attrs("xml")); + endWalk(); + } + + @Test + public void testNestedMatch() throws Exception { + // See also CGitAttributeTest.testNestedMatch() + setupRepo(null, null, "foo/ xml\nsub/foo/ sub\n*.jar jar", null); + writeTrashFile("foo/bar.jar", "b"); + writeTrashFile("foo/bar.xml", "bx"); + writeTrashFile("sub/b.jar", "bj"); + writeTrashFile("sub/b.xml", "bx"); + writeTrashFile("sub/foo/b.jar", "bf"); + walk = beginWalk(); + assertIteration(F, ".gitattributes"); + assertIteration(D, "foo", attrs("xml")); + assertIteration(F, "foo/bar.jar", attrs("jar")); + assertIteration(F, "foo/bar.xml"); + assertIteration(D, "sub"); + assertIteration(F, "sub/a.txt"); + assertIteration(F, "sub/b.jar", attrs("jar")); + assertIteration(F, "sub/b.xml"); + assertIteration(D, "sub/foo", attrs("sub xml")); + assertIteration(F, "sub/foo/b.jar", attrs("jar")); + endWalk(); + } + + @Test + public void testNestedMatchRecursive() throws Exception { + setupRepo(null, null, "foo/** xml\n*.jar jar", null); + writeTrashFile("foo/bar.jar", "b"); + writeTrashFile("foo/bar.xml", "bx"); + writeTrashFile("sub/b.jar", "bj"); + writeTrashFile("sub/b.xml", "bx"); + writeTrashFile("sub/foo/b.jar", "bf"); + // On foo.xml/bar.jar we must not have 'xml' + walk = beginWalk(); + assertIteration(F, ".gitattributes"); + assertIteration(D, "foo"); + assertIteration(F, "foo/bar.jar", attrs("jar xml")); + assertIteration(F, "foo/bar.xml", attrs("xml")); + assertIteration(D, "sub"); + assertIteration(F, "sub/a.txt"); + assertIteration(F, "sub/b.jar", attrs("jar")); + assertIteration(F, "sub/b.xml"); + assertIteration(D, "sub/foo"); + assertIteration(F, "sub/foo/b.jar", attrs("jar")); + endWalk(); + } + + @Test + public void testStarMatchOnSlashNot() throws Exception { + setupRepo(null, null, "s*xt bar", null); + writeTrashFile("sub/a.txt", "1"); + writeTrashFile("foo/sext", "2"); + writeTrashFile("foo/s.txt", "3"); + walk = beginWalk(); + assertIteration(F, ".gitattributes"); + assertIteration(D, "foo"); + assertIteration(F, "foo/s.txt", attrs("bar")); + assertIteration(F, "foo/sext", attrs("bar")); + assertIteration(D, "sub"); + assertIteration(F, "sub/a.txt"); + endWalk(); + } + + @Test + public void testPrefixMatchNot() throws Exception { + setupRepo(null, null, "sub/new bar", null); + writeTrashFile("sub/new/foo.txt", "1"); + walk = beginWalk(); + assertIteration(F, ".gitattributes"); + assertIteration(D, "sub"); + assertIteration(F, "sub/a.txt"); + assertIteration(D, "sub/new", attrs("bar")); + assertIteration(F, "sub/new/foo.txt"); + endWalk(); + } + + @Test + public void testComplexPathMatch() throws Exception { + setupRepo(null, null, "s[t-v]b/n[de]w bar", null); + writeTrashFile("sub/new/foo.txt", "1"); + writeTrashFile("sub/ndw", "2"); + walk = beginWalk(); + assertIteration(F, ".gitattributes"); + assertIteration(D, "sub"); + assertIteration(F, "sub/a.txt"); + assertIteration(F, "sub/ndw", attrs("bar")); + assertIteration(D, "sub/new", attrs("bar")); + assertIteration(F, "sub/new/foo.txt"); + endWalk(); + } + + @Test + public void testStarPathMatch() throws Exception { + setupRepo(null, null, "sub/new/* bar", null); + writeTrashFile("sub/new/foo.txt", "1"); + writeTrashFile("sub/new/lower/foo.txt", "2"); + walk = beginWalk(); + assertIteration(F, ".gitattributes"); + assertIteration(D, "sub"); + assertIteration(F, "sub/a.txt"); + assertIteration(D, "sub/new"); + assertIteration(F, "sub/new/foo.txt", attrs("bar")); + assertIteration(D, "sub/new/lower", attrs("bar")); + assertIteration(F, "sub/new/lower/foo.txt"); + endWalk(); + } + + @Test + public void testDirectoryMatchSubSimple() throws Exception { + setupRepo(null, null, "sub/new/ bar", null); + writeTrashFile("sub/new/foo.txt", "1"); + writeTrashFile("foo/sub/new/foo.txt", "2"); + writeTrashFile("sub/sub/new/foo.txt", "3"); + walk = beginWalk(); + assertIteration(F, ".gitattributes"); + assertIteration(D, "foo"); + assertIteration(D, "foo/sub"); + assertIteration(D, "foo/sub/new"); + assertIteration(F, "foo/sub/new/foo.txt"); + assertIteration(D, "sub"); + assertIteration(F, "sub/a.txt"); + assertIteration(D, "sub/new", attrs("bar")); + assertIteration(F, "sub/new/foo.txt"); + assertIteration(D, "sub/sub"); + assertIteration(D, "sub/sub/new"); + assertIteration(F, "sub/sub/new/foo.txt"); + endWalk(); + } + + @Test + public void testDirectoryMatchSubRecursive() throws Exception { + setupRepo(null, null, "**/sub/new/ bar", null); + writeTrashFile("sub/new/foo.txt", "1"); + writeTrashFile("foo/sub/new/foo.txt", "2"); + walk = beginWalk(); + assertIteration(F, ".gitattributes"); + assertIteration(D, "foo"); + assertIteration(D, "foo/sub"); + assertIteration(D, "foo/sub/new", attrs("bar")); + assertIteration(F, "foo/sub/new/foo.txt"); + assertIteration(D, "sub"); + assertIteration(F, "sub/a.txt"); + assertIteration(D, "sub/new", attrs("bar")); + assertIteration(F, "sub/new/foo.txt"); + endWalk(); + } + + @Test + public void testDirectoryMatchSubRecursiveBacktrack() throws Exception { + setupRepo(null, null, "**/sub/new/ bar", null); + writeTrashFile("sub/new/foo.txt", "1"); + writeTrashFile("foo/sub/new/foo.txt", "2"); + writeTrashFile("sub/sub/new/foo.txt", "3"); + walk = beginWalk(); + assertIteration(F, ".gitattributes"); + assertIteration(D, "foo"); + assertIteration(D, "foo/sub"); + assertIteration(D, "foo/sub/new", attrs("bar")); + assertIteration(F, "foo/sub/new/foo.txt"); + assertIteration(D, "sub"); + assertIteration(F, "sub/a.txt"); + assertIteration(D, "sub/new", attrs("bar")); + assertIteration(F, "sub/new/foo.txt"); + assertIteration(D, "sub/sub"); + assertIteration(D, "sub/sub/new", attrs("bar")); + assertIteration(F, "sub/sub/new/foo.txt"); + endWalk(); + } + + @Test + public void testDirectoryMatchSubRecursiveBacktrack2() throws Exception { + setupRepo(null, null, "**/**/sub/new/ bar", null); + writeTrashFile("sub/new/foo.txt", "1"); + writeTrashFile("foo/sub/new/foo.txt", "2"); + writeTrashFile("sub/sub/new/foo.txt", "3"); + walk = beginWalk(); + assertIteration(F, ".gitattributes"); + assertIteration(D, "foo"); + assertIteration(D, "foo/sub"); + assertIteration(D, "foo/sub/new", attrs("bar")); + assertIteration(F, "foo/sub/new/foo.txt"); + assertIteration(D, "sub"); + assertIteration(F, "sub/a.txt"); + assertIteration(D, "sub/new", attrs("bar")); + assertIteration(F, "sub/new/foo.txt"); + assertIteration(D, "sub/sub"); + assertIteration(D, "sub/sub/new", attrs("bar")); + assertIteration(F, "sub/sub/new/foo.txt"); + endWalk(); + } + + @Test + public void testDirectoryMatchSubComplex() throws Exception { + setupRepo(null, null, "s[uv]b/n*/ bar", null); + writeTrashFile("sub/new/foo.txt", "1"); + writeTrashFile("foo/sub/new/foo.txt", "2"); + walk = beginWalk(); + assertIteration(F, ".gitattributes"); + assertIteration(D, "foo"); + assertIteration(D, "foo/sub"); + assertIteration(D, "foo/sub/new"); + assertIteration(F, "foo/sub/new/foo.txt"); + assertIteration(D, "sub"); + assertIteration(F, "sub/a.txt"); + assertIteration(D, "sub/new", attrs("bar")); + assertIteration(F, "sub/new/foo.txt"); + endWalk(); + } + + @Test + public void testDirectoryMatch() throws Exception { + setupRepo(null, null, "new/ bar", null); + writeTrashFile("sub/new/foo.txt", "1"); + writeTrashFile("foo/sub/new/foo.txt", "2"); + writeTrashFile("foo/new", "3"); + walk = beginWalk(); + assertIteration(F, ".gitattributes"); + assertIteration(D, "foo"); + assertIteration(F, "foo/new"); + assertIteration(D, "foo/sub"); + assertIteration(D, "foo/sub/new", attrs("bar")); + assertIteration(F, "foo/sub/new/foo.txt"); + assertIteration(D, "sub"); + assertIteration(F, "sub/a.txt"); + assertIteration(D, "sub/new", attrs("bar")); + assertIteration(F, "sub/new/foo.txt"); + endWalk(); + } + private static Collection<Attribute> attrs(String s) { return new AttributesRule("*", s).getAttributes(); } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesMatcherTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesMatcherTest.java index e8dd952324..72cc1d1814 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesMatcherTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesMatcherTest.java @@ -109,16 +109,16 @@ public class AttributesMatcherTest { pattern = "/src/ne?"; assertMatched(pattern, "/src/new/"); assertMatched(pattern, "/src/new"); - assertMatched(pattern, "/src/new/a.c"); - assertMatched(pattern, "/src/new/a/a.c"); + assertNotMatched(pattern, "/src/new/a.c"); + assertNotMatched(pattern, "/src/new/a/a.c"); assertNotMatched(pattern, "/src/new.c"); //Test name-only fnmatcher matches pattern = "ne?"; assertMatched(pattern, "/src/new/"); assertMatched(pattern, "/src/new"); - assertMatched(pattern, "/src/new/a.c"); - assertMatched(pattern, "/src/new/a/a.c"); + assertNotMatched(pattern, "/src/new/a.c"); + assertNotMatched(pattern, "/src/new/a/a.c"); assertMatched(pattern, "/neb"); assertNotMatched(pattern, "/src/new.c"); } @@ -169,16 +169,16 @@ public class AttributesMatcherTest { pattern = "/src/ne?"; assertMatched(pattern, "src/new/"); assertMatched(pattern, "src/new"); - assertMatched(pattern, "src/new/a.c"); - assertMatched(pattern, "src/new/a/a.c"); + assertNotMatched(pattern, "src/new/a.c"); + assertNotMatched(pattern, "src/new/a/a.c"); assertNotMatched(pattern, "src/new.c"); //Test name-only fnmatcher matches pattern = "ne?"; assertMatched(pattern, "src/new/"); assertMatched(pattern, "src/new"); - assertMatched(pattern, "src/new/a.c"); - assertMatched(pattern, "src/new/a/a.c"); + assertNotMatched(pattern, "src/new/a.c"); + assertNotMatched(pattern, "src/new/a/a.c"); assertMatched(pattern, "neb"); assertNotMatched(pattern, "src/new.c"); } @@ -197,35 +197,50 @@ public class AttributesMatcherTest { pattern = "/src/new"; assertMatched(pattern, "/src/new/"); assertMatched(pattern, "/src/new"); - assertMatched(pattern, "/src/new/a.c"); - assertMatched(pattern, "/src/new/a/a.c"); + assertNotMatched(pattern, "/src/new/a.c"); + assertNotMatched(pattern, "/src/new/a/a.c"); assertNotMatched(pattern, "/src/new.c"); //Test child directory is matched, slash after name pattern = "/src/new/"; assertMatched(pattern, "/src/new/"); - assertMatched(pattern, "/src/new/a.c"); - assertMatched(pattern, "/src/new/a/a.c"); + assertNotMatched(pattern, "/src/new/a.c"); + assertNotMatched(pattern, "/src/new/a/a.c"); assertNotMatched(pattern, "/src/new"); assertNotMatched(pattern, "/src/new.c"); //Test directory is matched by name only pattern = "b1"; - assertMatched(pattern, "/src/new/a/b1/a.c"); + assertNotMatched(pattern, "/src/new/a/b1/a.c"); assertNotMatched(pattern, "/src/new/a/b2/file.c"); assertNotMatched(pattern, "/src/new/a/bb1/file.c"); assertNotMatched(pattern, "/src/new/a/file.c"); + assertNotMatched(pattern, "/src/new/a/bb1"); + assertMatched(pattern, "/src/new/a/b1"); } @Test public void testTrailingSlash() { String pattern = "/src/"; assertMatched(pattern, "/src/"); - assertMatched(pattern, "/src/new"); - assertMatched(pattern, "/src/new/a.c"); - assertMatched(pattern, "/src/a.c"); + assertNotMatched(pattern, "/src/new"); + assertNotMatched(pattern, "/src/new/a.c"); + assertNotMatched(pattern, "/src/a.c"); assertNotMatched(pattern, "/src"); assertNotMatched(pattern, "/srcA/"); + + pattern = "src/"; + assertMatched(pattern, "src/"); + assertMatched(pattern, "/src/"); + assertNotMatched(pattern, "src"); + assertNotMatched(pattern, "/src/new"); + assertNotMatched(pattern, "/src/new/a.c"); + assertNotMatched(pattern, "/src/a.c"); + assertNotMatched(pattern, "foo/src/a.c"); + assertNotMatched(pattern, "foo/src/bar/a.c"); + assertNotMatched(pattern, "foo/src/bar/src"); + assertMatched(pattern, "foo/src/"); + assertMatched(pattern, "foo/src/bar/src/"); } @Test @@ -239,51 +254,58 @@ public class AttributesMatcherTest { assertMatched(pattern, "/src/test.stp"); assertNotMatched(pattern, "/test.stp1"); assertNotMatched(pattern, "/test.astp"); + assertNotMatched(pattern, "test.stp/foo.bar"); + assertMatched(pattern, "test.stp"); + assertMatched(pattern, "test.stp/"); + assertMatched(pattern, "test.stp/test.stp"); //Test matches for name-only, applies to file name or folder name pattern = "src"; assertMatched(pattern, "/src"); assertMatched(pattern, "/src/"); - assertMatched(pattern, "/src/a.c"); - assertMatched(pattern, "/src/new/a.c"); - assertMatched(pattern, "/new/src/a.c"); + assertNotMatched(pattern, "/src/a.c"); + assertNotMatched(pattern, "/src/new/a.c"); + assertNotMatched(pattern, "/new/src/a.c"); assertMatched(pattern, "/file/src"); //Test matches for name-only, applies only to folder names pattern = "src/"; - assertMatched(pattern, "/src/"); - assertMatched(pattern, "/src/a.c"); - assertMatched(pattern, "/src/new/a.c"); - assertMatched(pattern, "/new/src/a.c"); + assertNotMatched(pattern, "/src/a.c"); + assertNotMatched(pattern, "/src/new/a.c"); + assertNotMatched(pattern, "/new/src/a.c"); assertNotMatched(pattern, "/src"); assertNotMatched(pattern, "/file/src"); + assertMatched(pattern, "/file/src/"); //Test matches for name-only, applies to file name or folder name //With a small wildcard pattern = "?rc"; - assertMatched(pattern, "/src/a.c"); - assertMatched(pattern, "/src/new/a.c"); - assertMatched(pattern, "/new/src/a.c"); + assertNotMatched(pattern, "/src/a.c"); + assertNotMatched(pattern, "/src/new/a.c"); + assertNotMatched(pattern, "/new/src/a.c"); + assertMatched(pattern, "/new/src/"); assertMatched(pattern, "/file/src"); assertMatched(pattern, "/src/"); //Test matches for name-only, applies to file name or folder name //With a small wildcard pattern = "?r[a-c]"; - assertMatched(pattern, "/src/a.c"); - assertMatched(pattern, "/src/new/a.c"); - assertMatched(pattern, "/new/src/a.c"); + assertNotMatched(pattern, "/src/a.c"); + assertNotMatched(pattern, "/src/new/a.c"); + assertNotMatched(pattern, "/new/src/a.c"); assertMatched(pattern, "/file/src"); assertMatched(pattern, "/src/"); - assertMatched(pattern, "/srb/a.c"); - assertMatched(pattern, "/grb/new/a.c"); - assertMatched(pattern, "/new/crb/a.c"); + assertNotMatched(pattern, "/srb/a.c"); + assertNotMatched(pattern, "/grb/new/a.c"); + assertNotMatched(pattern, "/new/crb/a.c"); assertMatched(pattern, "/file/3rb"); assertMatched(pattern, "/xrb/"); - assertMatched(pattern, "/3ra/a.c"); - assertMatched(pattern, "/5ra/new/a.c"); - assertMatched(pattern, "/new/1ra/a.c"); + assertNotMatched(pattern, "/3ra/a.c"); + assertNotMatched(pattern, "/5ra/new/a.c"); + assertNotMatched(pattern, "/new/1ra/a.c"); + assertNotMatched(pattern, "/new/1ra/a.c/"); assertMatched(pattern, "/file/dra"); + assertMatched(pattern, "/file/dra/"); assertMatched(pattern, "/era/"); assertNotMatched(pattern, "/crg"); assertNotMatched(pattern, "/cr3"); @@ -360,6 +382,39 @@ public class AttributesMatcherTest { assertEquals(r.getAttributes().get(2).toString(), "attribute3=value"); } + @Test + public void testBracketsInGroup() { + //combinations of brackets in brackets, escaped and not + + String[] patterns = new String[]{"[[\\]]", "[\\[\\]]"}; + for (String pattern : patterns) { + assertNotMatched(pattern, ""); + assertNotMatched(pattern, "[]"); + assertNotMatched(pattern, "]["); + assertNotMatched(pattern, "[\\[]"); + assertNotMatched(pattern, "[[]"); + assertNotMatched(pattern, "[[]]"); + assertNotMatched(pattern, "[\\[\\]]"); + + assertMatched(pattern, "["); + assertMatched(pattern, "]"); + } + + patterns = new String[]{"[[]]", "[\\[]]"}; + for (String pattern : patterns) { + assertNotMatched(pattern, ""); + assertMatched(pattern, "[]"); + assertNotMatched(pattern, "]["); + assertNotMatched(pattern, "[\\[]"); + assertNotMatched(pattern, "[[]"); + assertNotMatched(pattern, "[[]]"); + assertNotMatched(pattern, "[\\[\\]]"); + + assertNotMatched(pattern, "["); + assertNotMatched(pattern, "]"); + } + } + /** * Check for a match. If target ends with "/", match will assume that the * target is meant to be a directory. diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeTest.java index ec2370e67f..f0d3c3690f 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeTest.java @@ -166,6 +166,25 @@ public class AttributesNodeTest { assertAttribute("file.type3", node, asSet(A_UNSET_ATTR, B_SET_ATTR)); } + @Test + public void testDoubleAsteriskAtEnd() throws IOException { + String attributeFileContent = "dir/** \tA -B\tC=value"; + + is = new ByteArrayInputStream(attributeFileContent.getBytes()); + AttributesNode node = new AttributesNode(); + node.parse(is); + assertAttribute("dir", node, + asSet(new Attribute[]{})); + assertAttribute("dir/", node, + asSet(new Attribute[]{})); + assertAttribute("dir/file.type1", node, + asSet(A_SET_ATTR, B_UNSET_ATTR, C_VALUE_ATTR)); + assertAttribute("dir/sub/", node, + asSet(A_SET_ATTR, B_UNSET_ATTR, C_VALUE_ATTR)); + assertAttribute("dir/sub/file.type1", node, + asSet(A_SET_ATTR, B_UNSET_ATTR, C_VALUE_ATTR)); + } + private void assertAttribute(String path, AttributesNode node, Attributes attrs) throws IOException { Attributes attributes = new Attributes(); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/CGitAttributesTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/CGitAttributesTest.java new file mode 100644 index 0000000000..34838138e3 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/CGitAttributesTest.java @@ -0,0 +1,389 @@ +/* + * Copyright (C) 2017 Thomas Wolf <thomas.wolf@paranor.ch> + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.attributes; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.StoredConfig; +import org.eclipse.jgit.treewalk.FileTreeIterator; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.treewalk.filter.NotIgnoredFilter; +import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.FS.ExecutionResult; +import org.eclipse.jgit.util.RawParseUtils; +import org.eclipse.jgit.util.TemporaryBuffer; +import org.junit.Before; +import org.junit.Test; + +/** + * Tests that verify that the attributes of files in a repository are the same + * in JGit and in C-git. + */ +public class CGitAttributesTest extends RepositoryTestCase { + + @Before + public void initRepo() throws IOException { + // Because we run C-git, we must ensure that global or user exclude + // files cannot influence the tests. So we set core.excludesFile to an + // empty file inside the repository. + StoredConfig config = db.getConfig(); + File fakeUserGitignore = writeTrashFile(".fake_user_gitignore", ""); + config.setString("core", null, "excludesFile", + fakeUserGitignore.getAbsolutePath()); + // Disable case-insensitivity -- JGit doesn't handle that yet. + config.setBoolean("core", null, "ignoreCase", false); + // And try to switch off the global attributes file, too. + config.setString("core", null, "attributesFile", + fakeUserGitignore.getAbsolutePath()); + config.save(); + } + + private void createFiles(String... paths) throws IOException { + for (String path : paths) { + writeTrashFile(path, "x"); + } + } + + private String toString(TemporaryBuffer b) throws IOException { + return RawParseUtils.decode(b.toByteArray()); + } + + private Attribute fromString(String key, String value) { + if ("set".equals(value)) { + return new Attribute(key, Attribute.State.SET); + } + if ("unset".equals(value)) { + return new Attribute(key, Attribute.State.UNSET); + } + if ("unspecified".equals(value)) { + return new Attribute(key, Attribute.State.UNSPECIFIED); + } + return new Attribute(key, value); + } + + private LinkedHashMap<String, Attributes> cgitAttributes( + Set<String> allFiles) throws Exception { + FS fs = db.getFS(); + StringBuilder input = new StringBuilder(); + for (String filename : allFiles) { + input.append(filename).append('\n'); + } + ProcessBuilder builder = fs.runInShell("git", + new String[] { "check-attr", "--stdin", "--all" }); + builder.directory(db.getWorkTree()); + builder.environment().put("HOME", fs.userHome().getAbsolutePath()); + ExecutionResult result = fs.execute(builder, new ByteArrayInputStream( + input.toString().getBytes(Constants.CHARSET))); + String errorOut = toString(result.getStderr()); + assertEquals("External git failed", "exit 0\n", + "exit " + result.getRc() + '\n' + errorOut); + LinkedHashMap<String, Attributes> map = new LinkedHashMap<>(); + try (BufferedReader r = new BufferedReader(new InputStreamReader( + new BufferedInputStream(result.getStdout().openInputStream()), + Constants.CHARSET))) { + r.lines().forEach(line -> { + // Parse the line and add to result map + int start = 0; + int i = line.indexOf(':'); + String path = line.substring(0, i).trim(); + start = i + 1; + i = line.indexOf(':', start); + String key = line.substring(start, i).trim(); + String value = line.substring(i + 1).trim(); + Attribute attr = fromString(key, value); + Attributes attrs = map.get(path); + if (attrs == null) { + attrs = new Attributes(attr); + map.put(path, attrs); + } else { + attrs.put(attr); + } + }); + } + return map; + } + + private LinkedHashMap<String, Attributes> jgitAttributes() + throws IOException { + // Do a tree walk and return a list of all files and directories with + // their attributes + LinkedHashMap<String, Attributes> result = new LinkedHashMap<>(); + try (TreeWalk walk = new TreeWalk(db)) { + walk.addTree(new FileTreeIterator(db)); + walk.setFilter(new NotIgnoredFilter(0)); + while (walk.next()) { + String path = walk.getPathString(); + if (walk.isSubtree() && !path.endsWith("/")) { + // git check-attr expects directory paths to end with a + // slash + path += '/'; + } + Attributes attrs = walk.getAttributes(); + if (attrs != null && !attrs.isEmpty()) { + result.put(path, attrs); + } else { + result.put(path, null); + } + if (walk.isSubtree()) { + walk.enterSubtree(); + } + } + } + return result; + } + + private void assertSameAsCGit() throws Exception { + LinkedHashMap<String, Attributes> jgit = jgitAttributes(); + LinkedHashMap<String, Attributes> cgit = cgitAttributes(jgit.keySet()); + // remove all without attributes + Iterator<Map.Entry<String, Attributes>> iterator = jgit.entrySet() + .iterator(); + while (iterator.hasNext()) { + Map.Entry<String, Attributes> entry = iterator.next(); + if (entry.getValue() == null) { + iterator.remove(); + } + } + assertArrayEquals("JGit attributes differ from C git", + cgit.entrySet().toArray(), jgit.entrySet().toArray()); + } + + @Test + public void testBug508568() throws Exception { + createFiles("foo.xml/bar.jar", "sub/foo.xml/bar.jar"); + writeTrashFile(".gitattributes", "*.xml xml\n" + "*.jar jar\n"); + assertSameAsCGit(); + } + + @Test + public void testRelativePath() throws Exception { + createFiles("sub/foo.txt"); + writeTrashFile("sub/.gitattributes", "sub/** sub\n" + "*.txt txt\n"); + assertSameAsCGit(); + } + + @Test + public void testRelativePaths() throws Exception { + createFiles("sub/foo.txt", "sub/sub/bar", "foo/sub/a.txt", + "foo/sub/bar/a.tmp"); + writeTrashFile(".gitattributes", "sub/** sub\n" + "*.txt txt\n"); + assertSameAsCGit(); + } + + @Test + public void testNestedMatchNot() throws Exception { + createFiles("foo.xml/bar.jar", "foo.xml/bar.xml", "sub/b.jar", + "sub/b.xml"); + writeTrashFile("sub/.gitattributes", "*.xml xml\n" + "*.jar jar\n"); + assertSameAsCGit(); + } + + @Test + public void testNestedMatch() throws Exception { + // This is an interesting test. At the time of this writing, the + // gitignore documentation says: "In other words, foo/ will match a + // directory foo AND PATHS UNDERNEATH IT, but will not match a regular + // file or a symbolic link foo". (Emphasis added.) And gitattributes is + // supposed to follow the same rules. But the documentation appears to + // lie: C-git will *not* apply the attribute "xml" to *any* files in + // any subfolder "foo" here. It will only apply the "jar" attribute + // to the three *.jar files. + // + // The point is probably that ignores are handled top-down, and once a + // directory "foo" is matched (here: on paths "foo" and "sub/foo" by + // pattern "foo/"), the directory is excluded and the gitignore + // documentation also says: "It is not possible to re-include a file if + // a parent directory of that file is excluded." So once the pattern + // "foo/" has matched, it appears as if everything beneath would also be + // matched. + // + // But not so for gitattributes! The foo/ rule only matches the + // directory itself, but not anything beneath. + createFiles("foo/bar.jar", "foo/bar.xml", "sub/b.jar", "sub/b.xml", + "sub/foo/b.jar"); + writeTrashFile(".gitattributes", + "foo/ xml\n" + "sub/foo/ sub\n" + "*.jar jar\n"); + assertSameAsCGit(); + } + + @Test + public void testNestedMatchWithWildcard() throws Exception { + // See above. + createFiles("foo/bar.jar", "foo/bar.xml", "sub/b.jar", "sub/b.xml", + "sub/foo/b.jar"); + writeTrashFile(".gitattributes", + "**/foo/ xml\n" + "*/foo/ sub\n" + "*.jar jar\n"); + assertSameAsCGit(); + } + + @Test + public void testNestedMatchRecursive() throws Exception { + createFiles("foo/bar.jar", "foo/bar.xml", "sub/b.jar", "sub/b.xml", + "sub/foo/b.jar"); + writeTrashFile(".gitattributes", "foo/** xml\n" + "*.jar jar\n"); + assertSameAsCGit(); + } + + @Test + public void testStarMatchOnSlashNot() throws Exception { + createFiles("sub/a.txt", "foo/sext", "foo/s.txt"); + writeTrashFile(".gitattributes", "s*xt bar"); + assertSameAsCGit(); + } + + @Test + public void testPrefixMatchNot() throws Exception { + createFiles("src/new/foo.txt"); + writeTrashFile(".gitattributes", "src/new bar\n"); + assertSameAsCGit(); + } + + @Test + public void testComplexPathMatchNot() throws Exception { + createFiles("src/new/foo.txt", "src/ndw"); + writeTrashFile(".gitattributes", "s[p-s]c/n[de]w bar\n"); + assertSameAsCGit(); + } + + @Test + public void testStarPathMatchNot() throws Exception { + createFiles("src/new/foo.txt", "src/ndw"); + writeTrashFile(".gitattributes", "src/* bar\n"); + assertSameAsCGit(); + } + + @Test + public void testDirectoryMatchSubSimple() throws Exception { + createFiles("src/new/foo.txt", "foo/src/new/foo.txt", "sub/src/new"); + writeTrashFile(".gitattributes", "src/new/ bar\n"); + assertSameAsCGit(); + } + + @Test + public void testDirectoryMatchSubRecursive() throws Exception { + createFiles("src/new/foo.txt", "foo/src/new/foo.txt", "sub/src/new"); + writeTrashFile(".gitattributes", "**/src/new/ bar\n"); + assertSameAsCGit(); + } + + @Test + public void testDirectoryMatchSubRecursiveBacktrack() throws Exception { + createFiles("src/new/foo.txt", "src/src/new/foo.txt"); + writeTrashFile(".gitattributes", "**/src/new/ bar\n"); + assertSameAsCGit(); + } + + @Test + public void testDirectoryMatchSubRecursiveBacktrack2() throws Exception { + createFiles("src/new/foo.txt", "src/src/new/foo.txt"); + writeTrashFile(".gitattributes", "**/**/src/new/ bar\n"); + assertSameAsCGit(); + } + + @Test + public void testDirectoryMatchSubRecursiveBacktrack3() throws Exception { + createFiles("src/new/src/new/foo.txt", + "foo/src/new/bar/src/new/foo.txt"); + writeTrashFile(".gitattributes", "**/src/new/ bar\n"); + assertSameAsCGit(); + } + + @Test + public void testDirectoryMatchSubRecursiveBacktrack4() throws Exception { + createFiles("src/src/src/new/foo.txt", + "foo/src/src/bar/src/new/foo.txt"); + writeTrashFile(".gitattributes", "**/src/ bar\n"); + assertSameAsCGit(); + } + + @Test + public void testDirectoryMatchSubRecursiveBacktrack5() throws Exception { + createFiles("x/a/a/b/foo.txt", "x/y/z/b/a/b/foo.txt", + "x/y/a/a/a/a/b/foo.txt", "x/y/a/a/a/a/b/a/b/foo.txt"); + writeTrashFile(".gitattributes", "**/*/a/b bar\n"); + assertSameAsCGit(); + } + + @Test + public void testDirectoryMatchSubRecursiveBacktrack6() throws Exception { + createFiles("x/a/a/b/foo.txt", "x/y/a/b/a/b/foo.txt"); + writeTrashFile(".gitattributes", "**/*/**/a/b bar\n"); + assertSameAsCGit(); + } + + @Test + public void testDirectoryMatchSubComplex() throws Exception { + createFiles("src/new/foo.txt", "foo/src/new/foo.txt", "sub/src/new"); + writeTrashFile(".gitattributes", "s[rs]c/n*/ bar\n"); + assertSameAsCGit(); + } + + @Test + public void testDirectoryMatch() throws Exception { + createFiles("src/new/foo.txt", "foo/src/new/foo.txt", "sub/src/new"); + writeTrashFile(".gitattributes", "new/ bar\n"); + assertSameAsCGit(); + } + + @Test + public void testBracketsInGroup() throws Exception { + createFiles("[", "]", "[]", "][", "[[]", "[]]", "[[]]"); + writeTrashFile(".gitattributes", "[[]] bar1\n" + "[\\[]] bar2\n" + + "[[\\]] bar3\n" + "[\\[\\]] bar4\n"); + assertSameAsCGit(); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/merge/MergeGitAttributeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/merge/MergeGitAttributeTest.java new file mode 100644 index 0000000000..a4f3d18d1f --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/merge/MergeGitAttributeTest.java @@ -0,0 +1,585 @@ +/* + * Copyright (C) 2017, Obeo (mathieu.cartaud@obeo.fr) + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.attributes.merge; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.util.function.Consumer; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.MergeResult; +import org.eclipse.jgit.api.MergeResult.MergeStatus; +import org.eclipse.jgit.api.errors.CheckoutConflictException; +import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.InvalidMergeHeadsException; +import org.eclipse.jgit.api.errors.NoFilepatternException; +import org.eclipse.jgit.api.errors.NoHeadException; +import org.eclipse.jgit.api.errors.NoMessageException; +import org.eclipse.jgit.api.errors.WrongRepositoryStateException; +import org.eclipse.jgit.attributes.Attribute; +import org.eclipse.jgit.attributes.Attributes; +import org.eclipse.jgit.errors.NoWorkTreeException; +import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.treewalk.FileTreeIterator; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.treewalk.filter.PathFilter; +import org.junit.Ignore; +import org.junit.Test; + +public class MergeGitAttributeTest extends RepositoryTestCase { + + private static final String REFS_HEADS_RIGHT = "refs/heads/right"; + + private static final String REFS_HEADS_MASTER = "refs/heads/master"; + + private static final String REFS_HEADS_LEFT = "refs/heads/left"; + + private static final String DISABLE_CHECK_BRANCH = "refs/heads/disabled_checked"; + + private static final String ENABLE_CHECKED_BRANCH = "refs/heads/enabled_checked"; + + private static final String ENABLED_CHECKED_GIF = "enabled_checked.gif"; + + public Git createRepositoryBinaryConflict(Consumer<Git> initialCommit, + Consumer<Git> leftCommit, Consumer<Git> rightCommit) + throws NoFilepatternException, GitAPIException, NoWorkTreeException, + IOException { + // Set up a git whith conflict commits on images + Git git = new Git(db); + + // First commit + initialCommit.accept(git); + git.add().addFilepattern(".").call(); + RevCommit firstCommit = git.commit().setAll(true) + .setMessage("initial commit adding git attribute file").call(); + + // Create branch and add an icon Checked_Boxe (enabled_checked) + createBranch(firstCommit, REFS_HEADS_LEFT); + checkoutBranch(REFS_HEADS_LEFT); + leftCommit.accept(git); + git.add().addFilepattern(".").call(); + git.commit().setMessage("Left").call(); + + // Create a second branch from master Unchecked_Boxe + checkoutBranch(REFS_HEADS_MASTER); + createBranch(firstCommit, REFS_HEADS_RIGHT); + checkoutBranch(REFS_HEADS_RIGHT); + rightCommit.accept(git); + git.add().addFilepattern(".").call(); + git.commit().setMessage("Right").call(); + + checkoutBranch(REFS_HEADS_LEFT); + return git; + + } + + @Test + public void mergeTextualFile_NoAttr() throws NoWorkTreeException, + NoFilepatternException, GitAPIException, IOException { + try (Git git = createRepositoryBinaryConflict(g -> { + try { + writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "D\n"); + } catch (IOException e) { + e.printStackTrace(); + } + }, g -> { + try { + writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "F\n"); + } catch (IOException e) { + e.printStackTrace(); + } + }, g -> { + try { + writeTrashFile("main.cat", "A\n" + "E\n" + "C\n" + "D\n"); + } catch (IOException e) { + e.printStackTrace(); + } + })) { + checkoutBranch(REFS_HEADS_LEFT); + // Merge refs/heads/enabled_checked -> refs/heads/disabled_checked + + MergeResult mergeResult = git.merge() + .include(git.getRepository().resolve(REFS_HEADS_RIGHT)) + .call(); + assertEquals(MergeStatus.MERGED, mergeResult.getMergeStatus()); + + assertNull(mergeResult.getConflicts()); + + // Check that the image was not modified (not conflict marker added) + String result = read( + writeTrashFile("res.cat", "A\n" + "E\n" + "C\n" + "F\n")); + assertEquals(result, read(git.getRepository().getWorkTree().toPath() + .resolve("main.cat").toFile())); + } + } + + @Test + public void mergeTextualFile_UnsetMerge_Conflict() + throws NoWorkTreeException, NoFilepatternException, GitAPIException, + IOException { + try (Git git = createRepositoryBinaryConflict(g -> { + try { + writeTrashFile(".gitattributes", "*.cat -merge"); + writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "D\n"); + } catch (IOException e) { + e.printStackTrace(); + } + }, g -> { + try { + writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "F\n"); + } catch (IOException e) { + e.printStackTrace(); + } + }, g -> { + try { + writeTrashFile("main.cat", "A\n" + "E\n" + "C\n" + "D\n"); + } catch (IOException e) { + e.printStackTrace(); + } + })) { + // Check that the merge attribute is unset + assertAddMergeAttributeUnset(REFS_HEADS_LEFT, "main.cat"); + assertAddMergeAttributeUnset(REFS_HEADS_RIGHT, "main.cat"); + + checkoutBranch(REFS_HEADS_LEFT); + // Merge refs/heads/enabled_checked -> refs/heads/disabled_checked + + String catContent = read(git.getRepository().getWorkTree().toPath() + .resolve("main.cat").toFile()); + + MergeResult mergeResult = git.merge() + .include(git.getRepository().resolve(REFS_HEADS_RIGHT)) + .call(); + assertEquals(MergeStatus.CONFLICTING, mergeResult.getMergeStatus()); + + // Check that the image was not modified (not conflict marker added) + assertEquals(catContent, read(git.getRepository().getWorkTree() + .toPath().resolve("main.cat").toFile())); + } + } + + @Test + public void mergeTextualFile_UnsetMerge_NoConflict() + throws NoWorkTreeException, NoFilepatternException, GitAPIException, + IOException { + try (Git git = createRepositoryBinaryConflict(g -> { + try { + writeTrashFile(".gitattributes", "*.txt -merge"); + writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "D\n"); + } catch (IOException e) { + e.printStackTrace(); + } + }, g -> { + try { + writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "F\n"); + } catch (IOException e) { + e.printStackTrace(); + } + }, g -> { + try { + writeTrashFile("main.cat", "A\n" + "E\n" + "C\n" + "D\n"); + } catch (IOException e) { + e.printStackTrace(); + } + })) { + // Check that the merge attribute is unset + assertAddMergeAttributeUndefined(REFS_HEADS_LEFT, "main.cat"); + assertAddMergeAttributeUndefined(REFS_HEADS_RIGHT, "main.cat"); + + checkoutBranch(REFS_HEADS_LEFT); + // Merge refs/heads/enabled_checked -> refs/heads/disabled_checked + + MergeResult mergeResult = git.merge() + .include(git.getRepository().resolve(REFS_HEADS_RIGHT)) + .call(); + assertEquals(MergeStatus.MERGED, mergeResult.getMergeStatus()); + + // Check that the image was not modified (not conflict marker added) + String result = read( + writeTrashFile("res.cat", "A\n" + "E\n" + "C\n" + "F\n")); + assertEquals(result, read(git.getRepository().getWorkTree() + .toPath().resolve("main.cat").toFile())); + } + } + + @Test + public void mergeTextualFile_SetBinaryMerge_Conflict() + throws NoWorkTreeException, NoFilepatternException, GitAPIException, + IOException { + try (Git git = createRepositoryBinaryConflict(g -> { + try { + writeTrashFile(".gitattributes", "*.cat merge=binary"); + writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "D\n"); + } catch (IOException e) { + e.printStackTrace(); + } + }, g -> { + try { + writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "F\n"); + } catch (IOException e) { + e.printStackTrace(); + } + }, g -> { + try { + writeTrashFile("main.cat", "A\n" + "E\n" + "C\n" + "D\n"); + } catch (IOException e) { + e.printStackTrace(); + } + })) { + // Check that the merge attribute is set to binary + assertAddMergeAttributeCustom(REFS_HEADS_LEFT, "main.cat", + "binary"); + assertAddMergeAttributeCustom(REFS_HEADS_RIGHT, "main.cat", + "binary"); + + checkoutBranch(REFS_HEADS_LEFT); + // Merge refs/heads/enabled_checked -> refs/heads/disabled_checked + + String catContent = read(git.getRepository().getWorkTree().toPath() + .resolve("main.cat").toFile()); + + MergeResult mergeResult = git.merge() + .include(git.getRepository().resolve(REFS_HEADS_RIGHT)) + .call(); + assertEquals(MergeStatus.CONFLICTING, mergeResult.getMergeStatus()); + + // Check that the image was not modified (not conflict marker added) + assertEquals(catContent, read(git.getRepository().getWorkTree() + .toPath().resolve("main.cat").toFile())); + } + } + + /* + * This test is commented because JGit add conflict markers in binary files. + * cf. https://www.eclipse.org/forums/index.php/t/1086511/ + */ + @Test + @Ignore + public void mergeBinaryFile_NoAttr_Conflict() throws IllegalStateException, + IOException, NoHeadException, ConcurrentRefUpdateException, + CheckoutConflictException, InvalidMergeHeadsException, + WrongRepositoryStateException, NoMessageException, GitAPIException { + + RevCommit disableCheckedCommit; + FileInputStream mergeResultFile = null; + // Set up a git with conflict commits on images + try (Git git = new Git(db)) { + // First commit + write(new File(db.getWorkTree(), ".gitattributes"), ""); + git.add().addFilepattern(".gitattributes").call(); + RevCommit firstCommit = git.commit() + .setMessage("initial commit adding git attribute file") + .call(); + + // Create branch and add an icon Checked_Boxe (enabled_checked) + createBranch(firstCommit, ENABLE_CHECKED_BRANCH); + checkoutBranch(ENABLE_CHECKED_BRANCH); + copy(ENABLED_CHECKED_GIF, ENABLED_CHECKED_GIF, ""); + git.add().addFilepattern(ENABLED_CHECKED_GIF).call(); + git.commit().setMessage("enabled_checked commit").call(); + + // Create a second branch from master Unchecked_Boxe + checkoutBranch(REFS_HEADS_MASTER); + createBranch(firstCommit, DISABLE_CHECK_BRANCH); + checkoutBranch(DISABLE_CHECK_BRANCH); + copy("disabled_checked.gif", ENABLED_CHECKED_GIF, ""); + git.add().addFilepattern(ENABLED_CHECKED_GIF).call(); + disableCheckedCommit = git.commit() + .setMessage("disabled_checked commit").call(); + + // Check that the merge attribute is unset + assertAddMergeAttributeUndefined(ENABLE_CHECKED_BRANCH, + ENABLED_CHECKED_GIF); + assertAddMergeAttributeUndefined(DISABLE_CHECK_BRANCH, + ENABLED_CHECKED_GIF); + + checkoutBranch(ENABLE_CHECKED_BRANCH); + // Merge refs/heads/enabled_checked -> refs/heads/disabled_checked + MergeResult mergeResult = git.merge().include(disableCheckedCommit) + .call(); + assertEquals(MergeStatus.CONFLICTING, mergeResult.getMergeStatus()); + + // Check that the image was not modified (no conflict marker added) + mergeResultFile = new FileInputStream( + db.getWorkTree().toPath().resolve(ENABLED_CHECKED_GIF) + .toFile()); + assertTrue(contentEquals( + getClass().getResourceAsStream(ENABLED_CHECKED_GIF), + mergeResultFile)); + } finally { + if (mergeResultFile != null) { + mergeResultFile.close(); + } + } + } + + @Test + public void mergeBinaryFile_UnsetMerge_Conflict() + throws IllegalStateException, + IOException, NoHeadException, ConcurrentRefUpdateException, + CheckoutConflictException, InvalidMergeHeadsException, + WrongRepositoryStateException, NoMessageException, GitAPIException { + + RevCommit disableCheckedCommit; + FileInputStream mergeResultFile = null; + // Set up a git whith conflict commits on images + try (Git git = new Git(db)) { + // First commit + write(new File(db.getWorkTree(), ".gitattributes"), "*.gif -merge"); + git.add().addFilepattern(".gitattributes").call(); + RevCommit firstCommit = git.commit() + .setMessage("initial commit adding git attribute file") + .call(); + + // Create branch and add an icon Checked_Boxe (enabled_checked) + createBranch(firstCommit, ENABLE_CHECKED_BRANCH); + checkoutBranch(ENABLE_CHECKED_BRANCH); + copy(ENABLED_CHECKED_GIF, ENABLED_CHECKED_GIF, ""); + git.add().addFilepattern(ENABLED_CHECKED_GIF).call(); + git.commit().setMessage("enabled_checked commit").call(); + + // Create a second branch from master Unchecked_Boxe + checkoutBranch(REFS_HEADS_MASTER); + createBranch(firstCommit, DISABLE_CHECK_BRANCH); + checkoutBranch(DISABLE_CHECK_BRANCH); + copy("disabled_checked.gif", ENABLED_CHECKED_GIF, ""); + git.add().addFilepattern(ENABLED_CHECKED_GIF).call(); + disableCheckedCommit = git.commit() + .setMessage("disabled_checked commit").call(); + + // Check that the merge attribute is unset + assertAddMergeAttributeUnset(ENABLE_CHECKED_BRANCH, + ENABLED_CHECKED_GIF); + assertAddMergeAttributeUnset(DISABLE_CHECK_BRANCH, + ENABLED_CHECKED_GIF); + + checkoutBranch(ENABLE_CHECKED_BRANCH); + // Merge refs/heads/enabled_checked -> refs/heads/disabled_checked + MergeResult mergeResult = git.merge().include(disableCheckedCommit) + .call(); + assertEquals(MergeStatus.CONFLICTING, mergeResult.getMergeStatus()); + + // Check that the image was not modified (not conflict marker added) + mergeResultFile = new FileInputStream(db.getWorkTree().toPath() + .resolve(ENABLED_CHECKED_GIF).toFile()); + assertTrue(contentEquals( + getClass().getResourceAsStream(ENABLED_CHECKED_GIF), + mergeResultFile)); + } finally { + if (mergeResultFile != null) { + mergeResultFile.close(); + } + } + } + + @Test + public void mergeBinaryFile_SetMerge_Conflict() + throws IllegalStateException, IOException, NoHeadException, + ConcurrentRefUpdateException, CheckoutConflictException, + InvalidMergeHeadsException, WrongRepositoryStateException, + NoMessageException, GitAPIException { + + RevCommit disableCheckedCommit; + FileInputStream mergeResultFile = null; + // Set up a git whith conflict commits on images + try (Git git = new Git(db)) { + // First commit + write(new File(db.getWorkTree(), ".gitattributes"), "*.gif merge"); + git.add().addFilepattern(".gitattributes").call(); + RevCommit firstCommit = git.commit() + .setMessage("initial commit adding git attribute file") + .call(); + + // Create branch and add an icon Checked_Boxe (enabled_checked) + createBranch(firstCommit, ENABLE_CHECKED_BRANCH); + checkoutBranch(ENABLE_CHECKED_BRANCH); + copy(ENABLED_CHECKED_GIF, ENABLED_CHECKED_GIF, ""); + git.add().addFilepattern(ENABLED_CHECKED_GIF).call(); + git.commit().setMessage("enabled_checked commit").call(); + + // Create a second branch from master Unchecked_Boxe + checkoutBranch(REFS_HEADS_MASTER); + createBranch(firstCommit, DISABLE_CHECK_BRANCH); + checkoutBranch(DISABLE_CHECK_BRANCH); + copy("disabled_checked.gif", ENABLED_CHECKED_GIF, ""); + git.add().addFilepattern(ENABLED_CHECKED_GIF).call(); + disableCheckedCommit = git.commit() + .setMessage("disabled_checked commit").call(); + + // Check that the merge attribute is set + assertAddMergeAttributeSet(ENABLE_CHECKED_BRANCH, + ENABLED_CHECKED_GIF); + assertAddMergeAttributeSet(DISABLE_CHECK_BRANCH, + ENABLED_CHECKED_GIF); + + checkoutBranch(ENABLE_CHECKED_BRANCH); + // Merge refs/heads/enabled_checked -> refs/heads/disabled_checked + MergeResult mergeResult = git.merge().include(disableCheckedCommit) + .call(); + assertEquals(MergeStatus.CONFLICTING, mergeResult.getMergeStatus()); + + // Check that the image was not modified (not conflict marker added) + mergeResultFile = new FileInputStream(db.getWorkTree().toPath() + .resolve(ENABLED_CHECKED_GIF).toFile()); + assertFalse(contentEquals( + getClass().getResourceAsStream(ENABLED_CHECKED_GIF), + mergeResultFile)); + } finally { + if (mergeResultFile != null) { + mergeResultFile.close(); + } + } + } + + /* + * Copied from org.apache.commons.io.IOUtils + */ + private boolean contentEquals(InputStream input1, InputStream input2) + throws IOException { + if (input1 == input2) { + return true; + } + if (!(input1 instanceof BufferedInputStream)) { + input1 = new BufferedInputStream(input1); + } + if (!(input2 instanceof BufferedInputStream)) { + input2 = new BufferedInputStream(input2); + } + + int ch = input1.read(); + while (-1 != ch) { + final int ch2 = input2.read(); + if (ch != ch2) { + return false; + } + ch = input1.read(); + } + + final int ch2 = input2.read(); + return ch2 == -1; + } + + private void assertAddMergeAttributeUnset(String branch, String fileName) + throws IllegalStateException, IOException { + checkoutBranch(branch); + + try (TreeWalk treeWaklEnableChecked = new TreeWalk(db)) { + treeWaklEnableChecked.addTree(new FileTreeIterator(db)); + treeWaklEnableChecked.setFilter(PathFilter.create(fileName)); + + assertTrue(treeWaklEnableChecked.next()); + Attributes attributes = treeWaklEnableChecked.getAttributes(); + Attribute mergeAttribute = attributes.get("merge"); + assertNotNull(mergeAttribute); + assertEquals(Attribute.State.UNSET, mergeAttribute.getState()); + } + } + + private void assertAddMergeAttributeSet(String branch, String fileName) + throws IllegalStateException, IOException { + checkoutBranch(branch); + + try (TreeWalk treeWaklEnableChecked = new TreeWalk(db)) { + treeWaklEnableChecked.addTree(new FileTreeIterator(db)); + treeWaklEnableChecked.setFilter(PathFilter.create(fileName)); + + assertTrue(treeWaklEnableChecked.next()); + Attributes attributes = treeWaklEnableChecked.getAttributes(); + Attribute mergeAttribute = attributes.get("merge"); + assertNotNull(mergeAttribute); + assertEquals(Attribute.State.SET, mergeAttribute.getState()); + } + } + + private void assertAddMergeAttributeUndefined(String branch, + String fileName) throws IllegalStateException, IOException { + checkoutBranch(branch); + + try (TreeWalk treeWaklEnableChecked = new TreeWalk(db)) { + treeWaklEnableChecked.addTree(new FileTreeIterator(db)); + treeWaklEnableChecked.setFilter(PathFilter.create(fileName)); + + assertTrue(treeWaklEnableChecked.next()); + Attributes attributes = treeWaklEnableChecked.getAttributes(); + Attribute mergeAttribute = attributes.get("merge"); + assertNull(mergeAttribute); + } + } + + private void assertAddMergeAttributeCustom(String branch, String fileName, + String value) throws IllegalStateException, IOException { + checkoutBranch(branch); + + try (TreeWalk treeWaklEnableChecked = new TreeWalk(db)) { + treeWaklEnableChecked.addTree(new FileTreeIterator(db)); + treeWaklEnableChecked.setFilter(PathFilter.create(fileName)); + + assertTrue(treeWaklEnableChecked.next()); + Attributes attributes = treeWaklEnableChecked.getAttributes(); + Attribute mergeAttribute = attributes.get("merge"); + assertNotNull(mergeAttribute); + assertEquals(Attribute.State.CUSTOM, mergeAttribute.getState()); + assertEquals(value, mergeAttribute.getValue()); + } + } + + private void copy(String resourcePath, String resourceNewName, + String pathInRepo) throws IOException { + InputStream input = getClass().getResourceAsStream(resourcePath); + Files.copy(input, db.getWorkTree().toPath().resolve(pathInRepo) + .resolve(resourceNewName)); + } + +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandSymlinkTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandSymlinkTest.java index 12f4dcc0c7..341cc4f215 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandSymlinkTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandSymlinkTest.java @@ -47,6 +47,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import java.io.File; + import org.eclipse.jgit.api.Git; import org.eclipse.jgit.junit.JGitTestUtil; import org.eclipse.jgit.junit.RepositoryTestCase; diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/CGitIgnoreTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/CGitIgnoreTest.java new file mode 100644 index 0000000000..ee8191ffc5 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/CGitIgnoreTest.java @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2017 Thomas Wolf <thomas.wolf@paranor.ch> + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.ignore; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.StoredConfig; +import org.eclipse.jgit.treewalk.FileTreeIterator; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.treewalk.WorkingTreeIterator; +import org.eclipse.jgit.treewalk.filter.NotIgnoredFilter; +import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.FS.ExecutionResult; +import org.eclipse.jgit.util.RawParseUtils; +import org.eclipse.jgit.util.TemporaryBuffer; +import org.junit.Before; +import org.junit.Test; + +/** + * Tests that verify that the set of ignore files in a repository is the same in + * JGit and in C-git. + */ +public class CGitIgnoreTest extends RepositoryTestCase { + + @Before + public void initRepo() throws IOException { + // These tests focus on .gitignore files inside the repository. Because + // we run C-git, we must ensure that global or user exclude files cannot + // influence the tests. So we set core.excludesFile to an empty file + // inside the repository. + File fakeUserGitignore = writeTrashFile(".fake_user_gitignore", ""); + StoredConfig config = db.getConfig(); + config.setString("core", null, "excludesFile", + fakeUserGitignore.getAbsolutePath()); + // Disable case-insensitivity -- JGit doesn't handle that yet. + config.setBoolean("core", null, "ignoreCase", false); + config.save(); + } + + private void createFiles(String... paths) throws IOException { + for (String path : paths) { + writeTrashFile(path, "x"); + } + } + + private String toString(TemporaryBuffer b) throws IOException { + return RawParseUtils.decode(b.toByteArray()); + } + + private String[] cgitIgnored() throws Exception { + FS fs = db.getFS(); + ProcessBuilder builder = fs.runInShell("git", new String[] { "ls-files", + "--ignored", "--exclude-standard", "-o" }); + builder.directory(db.getWorkTree()); + builder.environment().put("HOME", fs.userHome().getAbsolutePath()); + ExecutionResult result = fs.execute(builder, + new ByteArrayInputStream(new byte[0])); + String errorOut = toString(result.getStderr()); + assertEquals("External git failed", "exit 0\n", + "exit " + result.getRc() + '\n' + errorOut); + try (BufferedReader r = new BufferedReader(new InputStreamReader( + new BufferedInputStream(result.getStdout().openInputStream()), + Constants.CHARSET))) { + return r.lines().toArray(String[]::new); + } + } + + private LinkedHashSet<String> jgitIgnored() throws IOException { + // Do a tree walk that does descend into ignored directories and return + // a list of all ignored files + LinkedHashSet<String> result = new LinkedHashSet<>(); + try (TreeWalk walk = new TreeWalk(db)) { + walk.addTree(new FileTreeIterator(db)); + walk.setRecursive(true); + while (walk.next()) { + if (walk.getTree(WorkingTreeIterator.class).isEntryIgnored()) { + result.add(walk.getPathString()); + } + } + } + return result; + } + + private void assertNoIgnoredVisited(Set<String> ignored) throws Exception { + // Do a recursive tree walk with a NotIgnoredFilter and verify that none + // of the files visited is in the ignored set + try (TreeWalk walk = new TreeWalk(db)) { + walk.addTree(new FileTreeIterator(db)); + walk.setFilter(new NotIgnoredFilter(0)); + walk.setRecursive(true); + while (walk.next()) { + String path = walk.getPathString(); + assertFalse("File " + path + " is ignored, should not appear", + ignored.contains(path)); + } + } + } + + private void assertSameAsCGit(String... notIgnored) throws Exception { + LinkedHashSet<String> ignored = jgitIgnored(); + String[] cgit = cgitIgnored(); + assertArrayEquals(cgit, ignored.toArray()); + for (String notExcluded : notIgnored) { + assertFalse("File " + notExcluded + " should not be ignored", + ignored.contains(notExcluded)); + } + assertNoIgnoredVisited(ignored); + } + + @Test + public void testSimpleIgnored() throws Exception { + createFiles("a.txt", "a.tmp", "src/sub/a.txt", "src/a.tmp", + "src/a.txt/b.tmp", "ignored/a.tmp", "ignored/not_ignored/a.tmp", + "ignored/other/a.tmp"); + writeTrashFile(".gitignore", + "*.txt\n" + "/ignored/*\n" + "!/ignored/not_ignored"); + assertSameAsCGit("ignored/not_ignored/a.tmp"); + } + + @Test + public void testDirOnlyMatch() throws Exception { + createFiles("a.txt", "src/foo/a.txt", "src/a.txt", "foo/a.txt"); + writeTrashFile(".gitignore", "foo/"); + assertSameAsCGit(); + } + + @Test + public void testDirOnlyMatchDeep() throws Exception { + createFiles("a.txt", "src/foo/a.txt", "src/a.txt", "foo/a.txt"); + writeTrashFile(".gitignore", "**/foo/"); + assertSameAsCGit(); + } + + @Test + public void testStarMatchOnSlashNot() throws Exception { + createFiles("sub/a.txt", "foo/sext", "foo/s.txt"); + writeTrashFile(".gitignore", "s*xt"); + assertSameAsCGit("sub/a.txt"); + } + + @Test + public void testPrefixMatch() throws Exception { + createFiles("src/new/foo.txt"); + writeTrashFile(".gitignore", "src/new"); + assertSameAsCGit(); + } + + @Test + public void testDirectoryMatchSubRecursive() throws Exception { + createFiles("src/new/foo.txt", "foo/src/new/foo.txt", "sub/src/new"); + writeTrashFile(".gitignore", "**/src/new/"); + assertSameAsCGit(); + } + + @Test + public void testDirectoryMatchSubRecursiveBacktrack() throws Exception { + createFiles("src/new/foo.txt", "src/src/new/foo.txt"); + writeTrashFile(".gitignore", "**/src/new/"); + assertSameAsCGit(); + } + + @Test + public void testDirectoryMatchSubRecursiveBacktrack2() throws Exception { + createFiles("src/new/foo.txt", "src/src/new/foo.txt"); + writeTrashFile(".gitignore", "**/**/src/new/"); + assertSameAsCGit(); + } + + @Test + public void testDirectoryMatchSubRecursiveBacktrack3() throws Exception { + createFiles("x/a/a/b/foo.txt"); + writeTrashFile(".gitignore", "**/*/a/b/"); + assertSameAsCGit(); + } + + @Test + public void testDirectoryMatchSubRecursiveBacktrack4() throws Exception { + createFiles("x/a/a/b/foo.txt", "x/y/z/b/a/b/foo.txt", + "x/y/a/a/a/a/b/foo.txt", "x/y/a/a/a/a/b/a/b/foo.txt"); + writeTrashFile(".gitignore", "**/*/a/b bar\n"); + assertSameAsCGit(); + } + + @Test + public void testDirectoryMatchSubRecursiveBacktrack5() throws Exception { + createFiles("x/a/a/b/foo.txt", "x/y/a/b/a/b/foo.txt"); + writeTrashFile(".gitignore", "**/*/**/a/b bar\n"); + assertSameAsCGit(); + } + + @Test + public void testUnescapedBracketsInGroup() throws Exception { + createFiles("[", "]", "[]", "][", "[[]", "[]]", "[[]]"); + writeTrashFile(".gitignore", "[[]]\n"); + assertSameAsCGit(); + } + + @Test + public void testEscapedFirstBracketInGroup() throws Exception { + createFiles("[", "]", "[]", "][", "[[]", "[]]", "[[]]"); + writeTrashFile(".gitignore", "[\\[]]\n"); + assertSameAsCGit(); + } + + @Test + public void testEscapedSecondBracketInGroup() throws Exception { + createFiles("[", "]", "[]", "][", "[[]", "[]]", "[[]]"); + writeTrashFile(".gitignore", "[[\\]]\n"); + assertSameAsCGit(); + } + + @Test + public void testEscapedBothBracketsInGroup() throws Exception { + createFiles("[", "]", "[]", "][", "[[]", "[]]", "[[]]"); + writeTrashFile(".gitignore", "[\\[\\]]\n"); + assertSameAsCGit(); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/FastIgnoreRuleTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/FastIgnoreRuleTest.java index 1863b80321..bcc8f7e47f 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/FastIgnoreRuleTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/FastIgnoreRuleTest.java @@ -391,7 +391,6 @@ public class FastIgnoreRuleTest { assertMatched("/**/a/b", "c/d/a/b"); assertMatched("/**/**/a/b", "c/d/a/b"); - assertMatched("a/b/**", "a/b"); assertMatched("a/b/**", "a/b/c"); assertMatched("a/b/**", "a/b/c/d/"); assertMatched("a/b/**/**", "a/b/c/d"); @@ -415,6 +414,12 @@ public class FastIgnoreRuleTest { @Test public void testWildmatchDoNotMatch() { + assertNotMatched("a/**", "a/"); + assertNotMatched("a/b/**", "a/b/"); + assertNotMatched("a/**", "a"); + assertNotMatched("a/b/**", "a/b"); + assertNotMatched("a/b/**/", "a/b"); + assertNotMatched("a/b/**/**", "a/b"); assertNotMatched("**/a/b", "a/c/b"); assertNotMatched("!/**/*.zip", "c/a/b.zip"); assertNotMatched("!**/*.zip", "c/a/b.zip"); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/internal/StringsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/internal/StringsTest.java new file mode 100644 index 0000000000..468989fe13 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/internal/StringsTest.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2017, Thomas Wolf <thomas.wolf@paranor.ch> + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.ignore.internal; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class StringsTest { + + private void testString(String string, int n, int m) { + assertEquals(string, n, Strings.count(string, '/', false)); + assertEquals(string, m, Strings.count(string, '/', true)); + } + + @Test + public void testCount() { + testString("", 0, 0); + testString("/", 1, 0); + testString("//", 2, 0); + testString("///", 3, 1); + testString("////", 4, 2); + testString("foo", 0, 0); + testString("/foo", 1, 0); + testString("foo/", 1, 0); + testString("/foo/", 2, 0); + testString("foo/bar", 1, 1); + testString("/foo/bar/", 3, 1); + testString("/foo/bar//", 4, 2); + testString("/foo//bar/", 4, 2); + testString(" /foo/ ", 2, 2); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/indexdiff/IndexDiffWithSymlinkTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/indexdiff/IndexDiffWithSymlinkTest.java index 4f3b601d3c..4228c9dbec 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/indexdiff/IndexDiffWithSymlinkTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/indexdiff/IndexDiffWithSymlinkTest.java @@ -142,7 +142,11 @@ public class IndexDiffWithSymlinkTest extends LocalDiskRepositoryTestCase { String[] cmd = { "/bin/sh", "./" + name + ".sh" }; int exitCode; String stdErr; - Process process = Runtime.getRuntime().exec(cmd, null, testDir); + ProcessBuilder builder = new ProcessBuilder(cmd); + builder.environment().put("HOME", + FS.DETECTED.userHome().getAbsolutePath()); + builder.directory(testDir); + Process process = builder.start(); try (InputStream stdOutStream = process.getInputStream(); InputStream stdErrStream = process.getErrorStream(); OutputStream stdInStream = process.getOutputStream()) { diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DeltaBaseCacheTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DeltaBaseCacheTest.java index 5bef9fa29e..32d711f1f8 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DeltaBaseCacheTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DeltaBaseCacheTest.java @@ -57,13 +57,14 @@ import org.junit.Test; public class DeltaBaseCacheTest { private static final int SZ = 512; - private DfsPackKey key; + private DfsStreamKey key; private DeltaBaseCache cache; private TestRng rng; @Before public void setUp() { - key = new DfsPackKey(); + DfsRepositoryDescription repo = new DfsRepositoryDescription("test"); + key = DfsStreamKey.of(repo, "test.key"); cache = new DeltaBaseCache(SZ); rng = new TestRng(getClass().getSimpleName()); } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTest.java new file mode 100644 index 0000000000..2e3ee4526f --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTest.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2017, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.internal.storage.dfs; + +import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.eclipse.jgit.junit.TestRng; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.ObjectReader; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; + +public class DfsBlockCacheTest { + @Rule + public TestName testName = new TestName(); + private TestRng rng; + private DfsBlockCache cache; + + @Before + public void setUp() { + rng = new TestRng(testName.getMethodName()); + resetCache(); + } + + @SuppressWarnings("resource") + @Test + public void streamKeyReusesBlocks() throws Exception { + DfsRepositoryDescription repo = new DfsRepositoryDescription("test"); + InMemoryRepository r1 = new InMemoryRepository(repo); + byte[] content = rng.nextBytes(424242); + ObjectId id; + try (ObjectInserter ins = r1.newObjectInserter()) { + id = ins.insert(OBJ_BLOB, content); + ins.flush(); + } + + long oldSize = cache.getCurrentSize(); + assertTrue(oldSize > 2000); + assertEquals(0, cache.getHitCount()); + + List<DfsPackDescription> packs = r1.getObjectDatabase().listPacks(); + InMemoryRepository r2 = new InMemoryRepository(repo); + r2.getObjectDatabase().commitPack(packs, Collections.emptyList()); + try (ObjectReader rdr = r2.newObjectReader()) { + byte[] actual = rdr.open(id, OBJ_BLOB).getBytes(); + assertTrue(Arrays.equals(content, actual)); + } + assertEquals(0, cache.getMissCount()); + assertEquals(oldSize, cache.getCurrentSize()); + } + + @SuppressWarnings("resource") + @Test + public void weirdBlockSize() throws Exception { + DfsRepositoryDescription repo = new DfsRepositoryDescription("test"); + InMemoryRepository r1 = new InMemoryRepository(repo); + + byte[] content1 = rng.nextBytes(4); + byte[] content2 = rng.nextBytes(424242); + ObjectId id1; + ObjectId id2; + try (ObjectInserter ins = r1.newObjectInserter()) { + id1 = ins.insert(OBJ_BLOB, content1); + id2 = ins.insert(OBJ_BLOB, content2); + ins.flush(); + } + + resetCache(); + List<DfsPackDescription> packs = r1.getObjectDatabase().listPacks(); + + InMemoryRepository r2 = new InMemoryRepository(repo); + r2.getObjectDatabase().setReadableChannelBlockSizeForTest(500); + r2.getObjectDatabase().commitPack(packs, Collections.emptyList()); + try (ObjectReader rdr = r2.newObjectReader()) { + byte[] actual = rdr.open(id1, OBJ_BLOB).getBytes(); + assertTrue(Arrays.equals(content1, actual)); + } + + InMemoryRepository r3 = new InMemoryRepository(repo); + r3.getObjectDatabase().setReadableChannelBlockSizeForTest(500); + r3.getObjectDatabase().commitPack(packs, Collections.emptyList()); + try (ObjectReader rdr = r3.newObjectReader()) { + byte[] actual = rdr.open(id2, OBJ_BLOB).getBytes(); + assertTrue(Arrays.equals(content2, actual)); + } + } + + private void resetCache() { + DfsBlockCache.reconfigure(new DfsBlockCacheConfig() + .setBlockSize(512) + .setBlockLimit(1 << 20)); + cache = DfsBlockCache.getInstance(); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsFsckTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsFsckTest.java new file mode 100644 index 0000000000..804d744ae2 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsFsckTest.java @@ -0,0 +1,269 @@ +/* + * Copyright (C) 2017, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.internal.storage.dfs; + +import static org.eclipse.jgit.junit.JGitTestUtil.concat; +import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH; +import static org.eclipse.jgit.lib.Constants.encodeASCII; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.IOException; + +import org.eclipse.jgit.internal.fsck.FsckError; +import org.eclipse.jgit.internal.fsck.FsckError.CorruptObject; +import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectChecker.ErrorType; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.revwalk.RevCommit; +import org.junit.Before; +import org.junit.Test; + +public class DfsFsckTest { + private TestRepository<InMemoryRepository> git; + + private InMemoryRepository repo; + + private ObjectInserter ins; + + @Before + public void setUp() throws IOException { + DfsRepositoryDescription desc = new DfsRepositoryDescription("test"); + git = new TestRepository<>(new InMemoryRepository(desc)); + repo = git.getRepository(); + ins = repo.newObjectInserter(); + } + + @Test + public void testHealthyRepo() throws Exception { + RevCommit commit0 = git.commit().message("0").create(); + RevCommit commit1 = git.commit().message("1").parent(commit0).create(); + git.update("master", commit1); + + DfsFsck fsck = new DfsFsck(repo); + FsckError errors = fsck.check(null); + + assertEquals(errors.getCorruptObjects().size(), 0); + assertEquals(errors.getMissingObjects().size(), 0); + assertEquals(errors.getCorruptIndices().size(), 0); + } + + @Test + public void testCommitWithCorruptAuthor() throws Exception { + StringBuilder b = new StringBuilder(); + b.append("tree be9bfa841874ccc9f2ef7c48d0c76226f89b7189\n"); + b.append("author b <b@c> <b@c> 0 +0000\n"); + b.append("committer <> 0 +0000\n"); + byte[] data = encodeASCII(b.toString()); + ObjectId id = ins.insert(Constants.OBJ_COMMIT, data); + ins.flush(); + + DfsFsck fsck = new DfsFsck(repo); + FsckError errors = fsck.check(null); + + assertEquals(errors.getCorruptObjects().size(), 1); + CorruptObject o = errors.getCorruptObjects().iterator().next(); + assertTrue(o.getId().equals(id)); + assertEquals(o.getErrorType(), ErrorType.BAD_DATE); + } + + @Test + public void testCommitWithoutTree() throws Exception { + StringBuilder b = new StringBuilder(); + b.append("parent "); + b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); + b.append('\n'); + byte[] data = encodeASCII(b.toString()); + ObjectId id = ins.insert(Constants.OBJ_COMMIT, data); + ins.flush(); + + DfsFsck fsck = new DfsFsck(repo); + FsckError errors = fsck.check(null); + + assertEquals(errors.getCorruptObjects().size(), 1); + CorruptObject o = errors.getCorruptObjects().iterator().next(); + assertTrue(o.getId().equals(id)); + assertEquals(o.getErrorType(), ErrorType.MISSING_TREE); + } + + @Test + public void testTagWithoutObject() throws Exception { + StringBuilder b = new StringBuilder(); + b.append("type commit\n"); + b.append("tag test-tag\n"); + b.append("tagger A. U. Thor <author@localhost> 1 +0000\n"); + byte[] data = encodeASCII(b.toString()); + ObjectId id = ins.insert(Constants.OBJ_TAG, data); + ins.flush(); + + DfsFsck fsck = new DfsFsck(repo); + FsckError errors = fsck.check(null); + + assertEquals(errors.getCorruptObjects().size(), 1); + CorruptObject o = errors.getCorruptObjects().iterator().next(); + assertTrue(o.getId().equals(id)); + assertEquals(o.getErrorType(), ErrorType.MISSING_OBJECT); + } + + @Test + public void testTreeWithNullSha() throws Exception { + byte[] data = concat(encodeASCII("100644 A"), new byte[] { '\0' }, + new byte[OBJECT_ID_LENGTH]); + ObjectId id = ins.insert(Constants.OBJ_TREE, data); + ins.flush(); + + DfsFsck fsck = new DfsFsck(repo); + FsckError errors = fsck.check(null); + + assertEquals(errors.getCorruptObjects().size(), 1); + CorruptObject o = errors.getCorruptObjects().iterator().next(); + assertTrue(o.getId().equals(id)); + assertEquals(o.getErrorType(), ErrorType.NULL_SHA1); + } + + @Test + public void testMultipleInvalidObjects() throws Exception { + StringBuilder b = new StringBuilder(); + b.append("tree "); + b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189"); + b.append('\n'); + b.append("parent "); + b.append("\n"); + byte[] data = encodeASCII(b.toString()); + ObjectId id1 = ins.insert(Constants.OBJ_COMMIT, data); + + b = new StringBuilder(); + b.append("100644"); + data = encodeASCII(b.toString()); + ObjectId id2 = ins.insert(Constants.OBJ_TREE, data); + + ins.flush(); + + DfsFsck fsck = new DfsFsck(repo); + FsckError errors = fsck.check(null); + + assertEquals(errors.getCorruptObjects().size(), 2); + for (CorruptObject o : errors.getCorruptObjects()) { + if (o.getId().equals(id1)) { + assertEquals(o.getErrorType(), ErrorType.BAD_PARENT_SHA1); + } else if (o.getId().equals(id2)) { + assertNull(o.getErrorType()); + } else { + fail(); + } + } + } + + @Test + public void testValidConnectivity() throws Exception { + ObjectId blobId = ins + .insert(Constants.OBJ_BLOB, Constants.encode("foo")); + + byte[] blobIdBytes = new byte[OBJECT_ID_LENGTH]; + blobId.copyRawTo(blobIdBytes, 0); + byte[] data = concat(encodeASCII("100644 regular-file\0"), blobIdBytes); + ObjectId treeId = ins.insert(Constants.OBJ_TREE, data); + ins.flush(); + + RevCommit commit = git.commit().message("0").setTopLevelTree(treeId) + .create(); + + git.update("master", commit); + + DfsFsck fsck = new DfsFsck(repo); + FsckError errors = fsck.check(null); + assertEquals(errors.getMissingObjects().size(), 0); + } + + @Test + public void testMissingObject() throws Exception { + ObjectId blobId = ObjectId + .fromString("19102815663d23f8b75a47e7a01965dcdc96468c"); + byte[] blobIdBytes = new byte[OBJECT_ID_LENGTH]; + blobId.copyRawTo(blobIdBytes, 0); + byte[] data = concat(encodeASCII("100644 regular-file\0"), blobIdBytes); + ObjectId treeId = ins.insert(Constants.OBJ_TREE, data); + ins.flush(); + + RevCommit commit = git.commit().message("0").setTopLevelTree(treeId) + .create(); + + git.update("master", commit); + + DfsFsck fsck = new DfsFsck(repo); + FsckError errors = fsck.check(null); + assertEquals(errors.getMissingObjects().size(), 1); + assertEquals(errors.getMissingObjects().iterator().next(), blobId); + } + + @Test + public void testNonCommitHead() throws Exception { + RevCommit commit0 = git.commit().message("0").create(); + StringBuilder b = new StringBuilder(); + b.append("object "); + b.append(commit0.getName()); + b.append('\n'); + b.append("type commit\n"); + b.append("tag test-tag\n"); + b.append("tagger A. U. Thor <author@localhost> 1 +0000\n"); + + byte[] data = encodeASCII(b.toString()); + ObjectId tagId = ins.insert(Constants.OBJ_TAG, data); + ins.flush(); + + git.update("master", tagId); + + DfsFsck fsck = new DfsFsck(repo); + FsckError errors = fsck.check(null); + assertEquals(errors.getCorruptObjects().size(), 0); + assertEquals(errors.getNonCommitHeads().size(), 1); + assertEquals(errors.getNonCommitHeads().iterator().next(), + "refs/heads/master"); + } + +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollectorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollectorTest.java index 17c1835bd0..55a5f726de 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollectorTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollectorTest.java @@ -5,6 +5,7 @@ import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.GC import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.INSERT; import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.UNREACHABLE_GARBAGE; import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK; +import static org.eclipse.jgit.internal.storage.pack.PackExt.REFTABLE; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -13,24 +14,35 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.concurrent.TimeUnit; import org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource; +import org.eclipse.jgit.internal.storage.reftable.RefCursor; +import org.eclipse.jgit.internal.storage.reftable.ReftableConfig; +import org.eclipse.jgit.internal.storage.reftable.ReftableReader; +import org.eclipse.jgit.internal.storage.reftable.ReftableWriter; import org.eclipse.jgit.junit.MockSystemReader; import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.BatchRefUpdate; +import org.eclipse.jgit.lib.NullProgressMonitor; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdRef; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevBlob; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.storage.pack.PackConfig; +import org.eclipse.jgit.transport.ReceiveCommand; import org.eclipse.jgit.util.SystemReader; import org.junit.After; import org.junit.Before; import org.junit.Test; +/** Tests for pack creation and garbage expiration. */ public class DfsGarbageCollectorTest { private TestRepository<InMemoryRepository> git; private InMemoryRepository repo; @@ -632,6 +644,205 @@ public class DfsGarbageCollectorTest { } } + @Test + public void testSinglePackForAllRefs() throws Exception { + RevCommit commit0 = commit().message("0").create(); + git.update("head", commit0); + RevCommit commit1 = commit().message("1").parent(commit0).create(); + git.update("refs/notes/note1", commit1); + + DfsGarbageCollector gc = new DfsGarbageCollector(repo); + gc.setGarbageTtl(0, TimeUnit.MILLISECONDS); + gc.getPackConfig().setSinglePack(true); + run(gc); + assertEquals(1, odb.getPacks().length); + + gc = new DfsGarbageCollector(repo); + gc.setGarbageTtl(0, TimeUnit.MILLISECONDS); + gc.getPackConfig().setSinglePack(false); + run(gc); + assertEquals(2, odb.getPacks().length); + } + + @SuppressWarnings("boxing") + @Test + public void producesNewReftable() throws Exception { + String master = "refs/heads/master"; + RevCommit commit0 = commit().message("0").create(); + RevCommit commit1 = commit().message("1").parent(commit0).create(); + + BatchRefUpdate bru = git.getRepository().getRefDatabase() + .newBatchUpdate(); + bru.addCommand(new ReceiveCommand(ObjectId.zeroId(), commit1, master)); + for (int i = 1; i <= 5100; i++) { + bru.addCommand(new ReceiveCommand(ObjectId.zeroId(), commit0, + String.format("refs/pulls/%04d", i))); + } + try (RevWalk rw = new RevWalk(git.getRepository())) { + bru.execute(rw, NullProgressMonitor.INSTANCE); + } + + DfsGarbageCollector gc = new DfsGarbageCollector(repo); + gc.setReftableConfig(new ReftableConfig()); + run(gc); + + // Single GC pack present with all objects. + assertEquals(1, odb.getPacks().length); + DfsPackFile pack = odb.getPacks()[0]; + DfsPackDescription desc = pack.getPackDescription(); + assertEquals(GC, desc.getPackSource()); + assertTrue("commit0 in pack", isObjectInPack(commit0, pack)); + assertTrue("commit1 in pack", isObjectInPack(commit1, pack)); + + // Sibling REFTABLE is also present. + assertTrue(desc.hasFileExt(REFTABLE)); + ReftableWriter.Stats stats = desc.getReftableStats(); + assertNotNull(stats); + assertTrue(stats.totalBytes() > 0); + assertEquals(5101, stats.refCount()); + assertEquals(1, stats.minUpdateIndex()); + assertEquals(1, stats.maxUpdateIndex()); + + DfsReftable table = new DfsReftable(DfsBlockCache.getInstance(), desc); + try (DfsReader ctx = odb.newReader(); + ReftableReader rr = table.open(ctx); + RefCursor rc = rr.seekRef("refs/pulls/5100")) { + assertTrue(rc.next()); + assertEquals(commit0, rc.getRef().getObjectId()); + assertFalse(rc.next()); + } + } + + @Test + public void leavesNonGcReftablesIfNotConfigured() throws Exception { + String master = "refs/heads/master"; + RevCommit commit0 = commit().message("0").create(); + RevCommit commit1 = commit().message("1").parent(commit0).create(); + git.update(master, commit1); + + DfsPackDescription t1 = odb.newPack(INSERT); + try (DfsOutputStream out = odb.writeFile(t1, REFTABLE)) { + out.write("ignored".getBytes(StandardCharsets.UTF_8)); + t1.addFileExt(REFTABLE); + } + odb.commitPack(Collections.singleton(t1), null); + + DfsGarbageCollector gc = new DfsGarbageCollector(repo); + gc.setReftableConfig(null); + run(gc); + + // Single GC pack present with all objects. + assertEquals(1, odb.getPacks().length); + DfsPackFile pack = odb.getPacks()[0]; + DfsPackDescription desc = pack.getPackDescription(); + assertEquals(GC, desc.getPackSource()); + assertTrue("commit0 in pack", isObjectInPack(commit0, pack)); + assertTrue("commit1 in pack", isObjectInPack(commit1, pack)); + + // Only INSERT REFTABLE above is present. + DfsReftable[] tables = odb.getReftables(); + assertEquals(1, tables.length); + assertEquals(t1, tables[0].getPackDescription()); + } + + @Test + public void prunesNonGcReftables() throws Exception { + String master = "refs/heads/master"; + RevCommit commit0 = commit().message("0").create(); + RevCommit commit1 = commit().message("1").parent(commit0).create(); + git.update(master, commit1); + + DfsPackDescription t1 = odb.newPack(INSERT); + try (DfsOutputStream out = odb.writeFile(t1, REFTABLE)) { + out.write("ignored".getBytes(StandardCharsets.UTF_8)); + t1.addFileExt(REFTABLE); + } + odb.commitPack(Collections.singleton(t1), null); + odb.clearCache(); + + DfsGarbageCollector gc = new DfsGarbageCollector(repo); + gc.setReftableConfig(new ReftableConfig()); + run(gc); + + // Single GC pack present with all objects. + assertEquals(1, odb.getPacks().length); + DfsPackFile pack = odb.getPacks()[0]; + DfsPackDescription desc = pack.getPackDescription(); + assertEquals(GC, desc.getPackSource()); + assertTrue("commit0 in pack", isObjectInPack(commit0, pack)); + assertTrue("commit1 in pack", isObjectInPack(commit1, pack)); + + // Only sibling GC REFTABLE is present. + DfsReftable[] tables = odb.getReftables(); + assertEquals(1, tables.length); + assertEquals(desc, tables[0].getPackDescription()); + assertTrue(desc.hasFileExt(REFTABLE)); + } + + @Test + public void compactsReftables() throws Exception { + String master = "refs/heads/master"; + RevCommit commit0 = commit().message("0").create(); + RevCommit commit1 = commit().message("1").parent(commit0).create(); + git.update(master, commit1); + + DfsGarbageCollector gc = new DfsGarbageCollector(repo); + gc.setReftableConfig(new ReftableConfig()); + run(gc); + + DfsPackDescription t1 = odb.newPack(INSERT); + Ref next = new ObjectIdRef.PeeledNonTag(Ref.Storage.LOOSE, + "refs/heads/next", commit0.copy()); + try (DfsOutputStream out = odb.writeFile(t1, REFTABLE)) { + ReftableWriter w = new ReftableWriter(); + w.setMinUpdateIndex(42); + w.setMaxUpdateIndex(42); + w.begin(out); + w.sortAndWriteRefs(Collections.singleton(next)); + w.finish(); + t1.addFileExt(REFTABLE); + t1.setReftableStats(w.getStats()); + } + odb.commitPack(Collections.singleton(t1), null); + + gc = new DfsGarbageCollector(repo); + gc.setReftableConfig(new ReftableConfig()); + run(gc); + + // Single GC pack present with all objects. + assertEquals(1, odb.getPacks().length); + DfsPackFile pack = odb.getPacks()[0]; + DfsPackDescription desc = pack.getPackDescription(); + assertEquals(GC, desc.getPackSource()); + assertTrue("commit0 in pack", isObjectInPack(commit0, pack)); + assertTrue("commit1 in pack", isObjectInPack(commit1, pack)); + + // Only sibling GC REFTABLE is present. + DfsReftable[] tables = odb.getReftables(); + assertEquals(1, tables.length); + assertEquals(desc, tables[0].getPackDescription()); + assertTrue(desc.hasFileExt(REFTABLE)); + + // GC reftable contains the compaction. + DfsReftable table = new DfsReftable(DfsBlockCache.getInstance(), desc); + try (DfsReader ctx = odb.newReader(); + ReftableReader rr = table.open(ctx); + RefCursor rc = rr.allRefs()) { + assertEquals(1, rr.minUpdateIndex()); + assertEquals(42, rr.maxUpdateIndex()); + + assertTrue(rc.next()); + assertEquals(master, rc.getRef().getName()); + assertEquals(commit1, rc.getRef().getObjectId()); + + assertTrue(rc.next()); + assertEquals(next.getName(), rc.getRef().getName()); + assertEquals(commit0, rc.getRef().getObjectId()); + + assertFalse(rc.next()); + } + } + private TestRepository<InMemoryRepository>.CommitBuilder commit() { return git.commit(); } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BatchRefUpdateTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BatchRefUpdateTest.java new file mode 100644 index 0000000000..3c4b8cf4bc --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BatchRefUpdateTest.java @@ -0,0 +1,1054 @@ +/* + * Copyright (C) 2017 Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.internal.storage.file; + +import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.eclipse.jgit.internal.storage.file.BatchRefUpdateTest.Result.LOCK_FAILURE; +import static org.eclipse.jgit.internal.storage.file.BatchRefUpdateTest.Result.OK; +import static org.eclipse.jgit.internal.storage.file.BatchRefUpdateTest.Result.REJECTED_MISSING_OBJECT; +import static org.eclipse.jgit.internal.storage.file.BatchRefUpdateTest.Result.REJECTED_NONFASTFORWARD; +import static org.eclipse.jgit.internal.storage.file.BatchRefUpdateTest.Result.TRANSACTION_ABORTED; +import static org.eclipse.jgit.lib.ObjectId.zeroId; +import static org.eclipse.jgit.transport.ReceiveCommand.Type.CREATE; +import static org.eclipse.jgit.transport.ReceiveCommand.Type.DELETE; +import static org.eclipse.jgit.transport.ReceiveCommand.Type.UPDATE; +import static org.eclipse.jgit.transport.ReceiveCommand.Type.UPDATE_NONFASTFORWARD; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Predicate; + +import org.eclipse.jgit.events.ListenerHandle; +import org.eclipse.jgit.events.RefsChangedListener; +import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase; +import org.eclipse.jgit.junit.StrictWorkMonitor; +import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.BatchRefUpdate; +import org.eclipse.jgit.lib.CheckoutEntry; +import org.eclipse.jgit.lib.ConfigConstants; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.NullProgressMonitor; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefDatabase; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.lib.ReflogEntry; +import org.eclipse.jgit.lib.ReflogReader; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.StoredConfig; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.ReceiveCommand; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +@SuppressWarnings("boxing") +@RunWith(Parameterized.class) +public class BatchRefUpdateTest extends LocalDiskRepositoryTestCase { + @Parameter + public boolean atomic; + + @Parameters(name = "atomic={0}") + public static Collection<Object[]> data() { + return Arrays.asList(new Object[][]{ {Boolean.FALSE}, {Boolean.TRUE} }); + } + + private Repository diskRepo; + private TestRepository<Repository> repo; + private RefDirectory refdir; + private RevCommit A; + private RevCommit B; + + /** + * When asserting the number of RefsChangedEvents you must account for one + * additional event due to the initial ref setup via a number of calls to + * {@link #writeLooseRef(String, AnyObjectId)} (will be fired in execute() + * when it is detected that the on-disk loose refs have changed), or for one + * additional event per {@link #writeRef(String, AnyObjectId)}. + */ + private int refsChangedEvents; + + private ListenerHandle handle; + + private RefsChangedListener refsChangedListener = event -> { + refsChangedEvents++; + }; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + + diskRepo = createBareRepository(); + setLogAllRefUpdates(true); + + refdir = (RefDirectory) diskRepo.getRefDatabase(); + refdir.setRetrySleepMs(Arrays.asList(0, 0)); + + repo = new TestRepository<>(diskRepo); + A = repo.commit().create(); + B = repo.commit(repo.getRevWalk().parseCommit(A)); + refsChangedEvents = 0; + handle = diskRepo.getListenerList() + .addRefsChangedListener(refsChangedListener); + } + + @After + public void removeListener() { + handle.remove(); + refsChangedEvents = 0; + } + + @Test + public void simpleNoForce() throws IOException { + writeLooseRef("refs/heads/master", A); + writeLooseRef("refs/heads/masters", B); + + List<ReceiveCommand> cmds = Arrays.asList( + new ReceiveCommand(A, B, "refs/heads/master", UPDATE), + new ReceiveCommand(B, A, "refs/heads/masters", UPDATE_NONFASTFORWARD)); + execute(newBatchUpdate(cmds)); + + if (atomic) { + assertResults(cmds, TRANSACTION_ABORTED, REJECTED_NONFASTFORWARD); + assertRefs( + "refs/heads/master", A, + "refs/heads/masters", B); + assertEquals(1, refsChangedEvents); + } else { + assertResults(cmds, OK, REJECTED_NONFASTFORWARD); + assertRefs( + "refs/heads/master", B, + "refs/heads/masters", B); + assertEquals(2, refsChangedEvents); + } + } + + @Test + public void simpleForce() throws IOException { + writeLooseRef("refs/heads/master", A); + writeLooseRef("refs/heads/masters", B); + + List<ReceiveCommand> cmds = Arrays.asList( + new ReceiveCommand(A, B, "refs/heads/master", UPDATE), + new ReceiveCommand(B, A, "refs/heads/masters", UPDATE_NONFASTFORWARD)); + execute(newBatchUpdate(cmds).setAllowNonFastForwards(true)); + + assertResults(cmds, OK, OK); + assertRefs( + "refs/heads/master", B, + "refs/heads/masters", A); + assertEquals(atomic ? 2 : 3, refsChangedEvents); + } + + @Test + public void nonFastForwardDoesNotDoExpensiveMergeCheck() throws IOException { + writeLooseRef("refs/heads/master", B); + + List<ReceiveCommand> cmds = Arrays.asList( + new ReceiveCommand(B, A, "refs/heads/master", UPDATE_NONFASTFORWARD)); + try (RevWalk rw = new RevWalk(diskRepo) { + @Override + public boolean isMergedInto(RevCommit base, RevCommit tip) { + throw new AssertionError("isMergedInto() should not be called"); + } + }) { + newBatchUpdate(cmds) + .setAllowNonFastForwards(true) + .execute(rw, new StrictWorkMonitor()); + } + + assertResults(cmds, OK); + assertRefs("refs/heads/master", A); + assertEquals(2, refsChangedEvents); + } + + @Test + public void fileDirectoryConflict() throws IOException { + writeLooseRef("refs/heads/master", A); + writeLooseRef("refs/heads/masters", B); + + List<ReceiveCommand> cmds = Arrays.asList( + new ReceiveCommand(A, B, "refs/heads/master", UPDATE), + new ReceiveCommand(zeroId(), A, "refs/heads/master/x", CREATE), + new ReceiveCommand(zeroId(), A, "refs/heads", CREATE)); + execute(newBatchUpdate(cmds).setAllowNonFastForwards(true), false); + + if (atomic) { + // Atomic update sees that master and master/x are conflicting, then marks + // the first one in the list as LOCK_FAILURE and aborts the rest. + assertResults(cmds, + LOCK_FAILURE, TRANSACTION_ABORTED, TRANSACTION_ABORTED); + assertRefs( + "refs/heads/master", A, + "refs/heads/masters", B); + assertEquals(1, refsChangedEvents); + } else { + // Non-atomic updates are applied in order: master succeeds, then master/x + // fails due to conflict. + assertResults(cmds, OK, LOCK_FAILURE, LOCK_FAILURE); + assertRefs( + "refs/heads/master", B, + "refs/heads/masters", B); + assertEquals(2, refsChangedEvents); + } + } + + @Test + public void conflictThanksToDelete() throws IOException { + writeLooseRef("refs/heads/master", A); + writeLooseRef("refs/heads/masters", B); + + List<ReceiveCommand> cmds = Arrays.asList( + new ReceiveCommand(A, B, "refs/heads/master", UPDATE), + new ReceiveCommand(zeroId(), A, "refs/heads/masters/x", CREATE), + new ReceiveCommand(B, zeroId(), "refs/heads/masters", DELETE)); + execute(newBatchUpdate(cmds).setAllowNonFastForwards(true)); + + assertResults(cmds, OK, OK, OK); + assertRefs( + "refs/heads/master", B, + "refs/heads/masters/x", A); + if (atomic) { + assertEquals(2, refsChangedEvents); + } else { + // The non-atomic case actually produces 5 events, but that's an + // implementation detail. We expect at least 4 events, one for the + // initial read due to writeLooseRef(), and then one for each + // successful ref update. + assertTrue(refsChangedEvents >= 4); + } + } + + @Test + public void updateToMissingObject() throws IOException { + writeLooseRef("refs/heads/master", A); + + ObjectId bad = + ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"); + List<ReceiveCommand> cmds = Arrays.asList( + new ReceiveCommand(A, bad, "refs/heads/master", UPDATE), + new ReceiveCommand(zeroId(), B, "refs/heads/foo2", CREATE)); + execute(newBatchUpdate(cmds).setAllowNonFastForwards(true), false); + + if (atomic) { + assertResults(cmds, REJECTED_MISSING_OBJECT, TRANSACTION_ABORTED); + assertRefs("refs/heads/master", A); + assertEquals(1, refsChangedEvents); + } else { + assertResults(cmds, REJECTED_MISSING_OBJECT, OK); + assertRefs( + "refs/heads/master", A, + "refs/heads/foo2", B); + assertEquals(2, refsChangedEvents); + } + } + + @Test + public void addMissingObject() throws IOException { + writeLooseRef("refs/heads/master", A); + + ObjectId bad = + ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"); + List<ReceiveCommand> cmds = Arrays.asList( + new ReceiveCommand(A, B, "refs/heads/master", UPDATE), + new ReceiveCommand(zeroId(), bad, "refs/heads/foo2", CREATE)); + execute(newBatchUpdate(cmds).setAllowNonFastForwards(true), false); + + if (atomic) { + assertResults(cmds, TRANSACTION_ABORTED, REJECTED_MISSING_OBJECT); + assertRefs("refs/heads/master", A); + assertEquals(1, refsChangedEvents); + } else { + assertResults(cmds, OK, REJECTED_MISSING_OBJECT); + assertRefs("refs/heads/master", B); + assertEquals(2, refsChangedEvents); + } + } + + @Test + public void oneNonExistentRef() throws IOException { + List<ReceiveCommand> cmds = Arrays.asList( + new ReceiveCommand(A, B, "refs/heads/foo1", UPDATE), + new ReceiveCommand(zeroId(), B, "refs/heads/foo2", CREATE)); + execute(newBatchUpdate(cmds).setAllowNonFastForwards(true)); + + if (atomic) { + assertResults(cmds, LOCK_FAILURE, TRANSACTION_ABORTED); + assertRefs(); + assertEquals(0, refsChangedEvents); + } else { + assertResults(cmds, LOCK_FAILURE, OK); + assertRefs("refs/heads/foo2", B); + assertEquals(1, refsChangedEvents); + } + } + + @Test + public void oneRefWrongOldValue() throws IOException { + writeLooseRef("refs/heads/master", A); + + List<ReceiveCommand> cmds = Arrays.asList( + new ReceiveCommand(B, B, "refs/heads/master", UPDATE), + new ReceiveCommand(zeroId(), B, "refs/heads/foo2", CREATE)); + execute(newBatchUpdate(cmds).setAllowNonFastForwards(true)); + + if (atomic) { + assertResults(cmds, LOCK_FAILURE, TRANSACTION_ABORTED); + assertRefs("refs/heads/master", A); + assertEquals(1, refsChangedEvents); + } else { + assertResults(cmds, LOCK_FAILURE, OK); + assertRefs( + "refs/heads/master", A, + "refs/heads/foo2", B); + assertEquals(2, refsChangedEvents); + } + } + + @Test + public void nonExistentRef() throws IOException { + writeLooseRef("refs/heads/master", A); + + List<ReceiveCommand> cmds = Arrays.asList( + new ReceiveCommand(A, B, "refs/heads/master", UPDATE), + new ReceiveCommand(A, zeroId(), "refs/heads/foo2", DELETE)); + execute(newBatchUpdate(cmds).setAllowNonFastForwards(true)); + + if (atomic) { + assertResults(cmds, TRANSACTION_ABORTED, LOCK_FAILURE); + assertRefs("refs/heads/master", A); + assertEquals(1, refsChangedEvents); + } else { + assertResults(cmds, OK, LOCK_FAILURE); + assertRefs("refs/heads/master", B); + assertEquals(2, refsChangedEvents); + } + } + + @Test + public void noRefLog() throws IOException { + writeRef("refs/heads/master", A); + + Map<String, ReflogEntry> oldLogs = + getLastReflogs("refs/heads/master", "refs/heads/branch"); + assertEquals(Collections.singleton("refs/heads/master"), oldLogs.keySet()); + + List<ReceiveCommand> cmds = Arrays.asList( + new ReceiveCommand(A, B, "refs/heads/master", UPDATE), + new ReceiveCommand(zeroId(), B, "refs/heads/branch", CREATE)); + execute(newBatchUpdate(cmds).setAllowNonFastForwards(true)); + + assertResults(cmds, OK, OK); + assertRefs( + "refs/heads/master", B, + "refs/heads/branch", B); + assertEquals(atomic ? 2 : 3, refsChangedEvents); + assertReflogUnchanged(oldLogs, "refs/heads/master"); + assertReflogUnchanged(oldLogs, "refs/heads/branch"); + } + + @Test + public void reflogDefaultIdent() throws IOException { + writeRef("refs/heads/master", A); + writeRef("refs/heads/branch2", A); + + Map<String, ReflogEntry> oldLogs = getLastReflogs( + "refs/heads/master", "refs/heads/branch1", "refs/heads/branch2"); + List<ReceiveCommand> cmds = Arrays.asList( + new ReceiveCommand(A, B, "refs/heads/master", UPDATE), + new ReceiveCommand(zeroId(), B, "refs/heads/branch1", CREATE)); + execute( + newBatchUpdate(cmds) + .setAllowNonFastForwards(true) + .setRefLogMessage("a reflog", false)); + + assertResults(cmds, OK, OK); + assertRefs( + "refs/heads/master", B, + "refs/heads/branch1", B, + "refs/heads/branch2", A); + assertEquals(atomic ? 3 : 4, refsChangedEvents); + assertReflogEquals( + reflog(A, B, new PersonIdent(diskRepo), "a reflog"), + getLastReflog("refs/heads/master")); + assertReflogEquals( + reflog(zeroId(), B, new PersonIdent(diskRepo), "a reflog"), + getLastReflog("refs/heads/branch1")); + assertReflogUnchanged(oldLogs, "refs/heads/branch2"); + } + + @Test + public void reflogAppendStatusNoMessage() throws IOException { + writeRef("refs/heads/master", A); + writeRef("refs/heads/branch1", B); + + List<ReceiveCommand> cmds = Arrays.asList( + new ReceiveCommand(A, B, "refs/heads/master", UPDATE), + new ReceiveCommand(B, A, "refs/heads/branch1", UPDATE_NONFASTFORWARD), + new ReceiveCommand(zeroId(), A, "refs/heads/branch2", CREATE)); + execute( + newBatchUpdate(cmds) + .setAllowNonFastForwards(true) + .setRefLogMessage(null, true)); + + assertResults(cmds, OK, OK, OK); + assertRefs( + "refs/heads/master", B, + "refs/heads/branch1", A, + "refs/heads/branch2", A); + assertEquals(atomic ? 3 : 5, refsChangedEvents); + assertReflogEquals( + // Always forced; setAllowNonFastForwards(true) bypasses the check. + reflog(A, B, new PersonIdent(diskRepo), "forced-update"), + getLastReflog("refs/heads/master")); + assertReflogEquals( + reflog(B, A, new PersonIdent(diskRepo), "forced-update"), + getLastReflog("refs/heads/branch1")); + assertReflogEquals( + reflog(zeroId(), A, new PersonIdent(diskRepo), "created"), + getLastReflog("refs/heads/branch2")); + } + + @Test + public void reflogAppendStatusFastForward() throws IOException { + writeRef("refs/heads/master", A); + + List<ReceiveCommand> cmds = Arrays.asList( + new ReceiveCommand(A, B, "refs/heads/master", UPDATE)); + execute(newBatchUpdate(cmds).setRefLogMessage(null, true)); + + assertResults(cmds, OK); + assertRefs("refs/heads/master", B); + assertEquals(2, refsChangedEvents); + assertReflogEquals( + reflog(A, B, new PersonIdent(diskRepo), "fast-forward"), + getLastReflog("refs/heads/master")); + } + + @Test + public void reflogAppendStatusWithMessage() throws IOException { + writeRef("refs/heads/master", A); + + List<ReceiveCommand> cmds = Arrays.asList( + new ReceiveCommand(A, B, "refs/heads/master", UPDATE), + new ReceiveCommand(zeroId(), A, "refs/heads/branch", CREATE)); + execute(newBatchUpdate(cmds).setRefLogMessage("a reflog", true)); + + assertResults(cmds, OK, OK); + assertRefs( + "refs/heads/master", B, + "refs/heads/branch", A); + assertEquals(atomic ? 2 : 3, refsChangedEvents); + assertReflogEquals( + reflog(A, B, new PersonIdent(diskRepo), "a reflog: fast-forward"), + getLastReflog("refs/heads/master")); + assertReflogEquals( + reflog(zeroId(), A, new PersonIdent(diskRepo), "a reflog: created"), + getLastReflog("refs/heads/branch")); + } + + @Test + public void reflogCustomIdent() throws IOException { + writeRef("refs/heads/master", A); + + List<ReceiveCommand> cmds = Arrays.asList( + new ReceiveCommand(A, B, "refs/heads/master", UPDATE), + new ReceiveCommand(zeroId(), B, "refs/heads/branch", CREATE)); + PersonIdent ident = new PersonIdent("A Reflog User", "reflog@example.com"); + execute( + newBatchUpdate(cmds) + .setRefLogMessage("a reflog", false) + .setRefLogIdent(ident)); + + assertResults(cmds, OK, OK); + assertEquals(atomic ? 2 : 3, refsChangedEvents); + assertRefs( + "refs/heads/master", B, + "refs/heads/branch", B); + assertReflogEquals( + reflog(A, B, ident, "a reflog"), + getLastReflog("refs/heads/master"), + true); + assertReflogEquals( + reflog(zeroId(), B, ident, "a reflog"), + getLastReflog("refs/heads/branch"), + true); + } + + @Test + public void reflogDelete() throws IOException { + writeRef("refs/heads/master", A); + writeRef("refs/heads/branch", A); + assertEquals( + 2, getLastReflogs("refs/heads/master", "refs/heads/branch").size()); + + List<ReceiveCommand> cmds = Arrays.asList( + new ReceiveCommand(A, zeroId(), "refs/heads/master", DELETE), + new ReceiveCommand(A, B, "refs/heads/branch", UPDATE)); + execute(newBatchUpdate(cmds).setRefLogMessage("a reflog", false)); + + assertResults(cmds, OK, OK); + assertRefs("refs/heads/branch", B); + assertEquals(atomic ? 3 : 4, refsChangedEvents); + assertNull(getLastReflog("refs/heads/master")); + assertReflogEquals( + reflog(A, B, new PersonIdent(diskRepo), "a reflog"), + getLastReflog("refs/heads/branch")); + } + + @Test + public void reflogFileDirectoryConflict() throws IOException { + writeRef("refs/heads/master", A); + + List<ReceiveCommand> cmds = Arrays.asList( + new ReceiveCommand(A, zeroId(), "refs/heads/master", DELETE), + new ReceiveCommand(zeroId(), A, "refs/heads/master/x", CREATE)); + execute(newBatchUpdate(cmds).setRefLogMessage("a reflog", false)); + + assertResults(cmds, OK, OK); + assertRefs("refs/heads/master/x", A); + assertEquals(atomic ? 2 : 3, refsChangedEvents); + assertNull(getLastReflog("refs/heads/master")); + assertReflogEquals( + reflog(zeroId(), A, new PersonIdent(diskRepo), "a reflog"), + getLastReflog("refs/heads/master/x")); + } + + @Test + public void reflogOnLockFailure() throws IOException { + writeRef("refs/heads/master", A); + + Map<String, ReflogEntry> oldLogs = + getLastReflogs("refs/heads/master", "refs/heads/branch"); + + List<ReceiveCommand> cmds = Arrays.asList( + new ReceiveCommand(A, B, "refs/heads/master", UPDATE), + new ReceiveCommand(A, B, "refs/heads/branch", UPDATE)); + execute(newBatchUpdate(cmds).setRefLogMessage("a reflog", false)); + + if (atomic) { + assertResults(cmds, TRANSACTION_ABORTED, LOCK_FAILURE); + assertEquals(1, refsChangedEvents); + assertReflogUnchanged(oldLogs, "refs/heads/master"); + assertReflogUnchanged(oldLogs, "refs/heads/branch"); + } else { + assertResults(cmds, OK, LOCK_FAILURE); + assertEquals(2, refsChangedEvents); + assertReflogEquals( + reflog(A, B, new PersonIdent(diskRepo), "a reflog"), + getLastReflog("refs/heads/master")); + assertReflogUnchanged(oldLogs, "refs/heads/branch"); + } + } + + @Test + public void overrideRefLogMessage() throws Exception { + writeRef("refs/heads/master", A); + + List<ReceiveCommand> cmds = Arrays.asList( + new ReceiveCommand(A, B, "refs/heads/master", UPDATE), + new ReceiveCommand(zeroId(), B, "refs/heads/branch", CREATE)); + cmds.get(0).setRefLogMessage("custom log", false); + PersonIdent ident = new PersonIdent(diskRepo); + execute( + newBatchUpdate(cmds) + .setRefLogIdent(ident) + .setRefLogMessage("a reflog", true)); + + assertResults(cmds, OK, OK); + assertEquals(atomic ? 2 : 3, refsChangedEvents); + assertReflogEquals( + reflog(A, B, ident, "custom log"), + getLastReflog("refs/heads/master"), + true); + assertReflogEquals( + reflog(zeroId(), B, ident, "a reflog: created"), + getLastReflog("refs/heads/branch"), + true); + } + + @Test + public void overrideDisableRefLog() throws Exception { + writeRef("refs/heads/master", A); + + Map<String, ReflogEntry> oldLogs = + getLastReflogs("refs/heads/master", "refs/heads/branch"); + + List<ReceiveCommand> cmds = Arrays.asList( + new ReceiveCommand(A, B, "refs/heads/master", UPDATE), + new ReceiveCommand(zeroId(), B, "refs/heads/branch", CREATE)); + cmds.get(0).disableRefLog(); + execute(newBatchUpdate(cmds).setRefLogMessage("a reflog", true)); + + assertResults(cmds, OK, OK); + assertEquals(atomic ? 2 : 3, refsChangedEvents); + assertReflogUnchanged(oldLogs, "refs/heads/master"); + assertReflogEquals( + reflog(zeroId(), B, new PersonIdent(diskRepo), "a reflog: created"), + getLastReflog("refs/heads/branch")); + } + + @Test + public void refLogNotWrittenWithoutConfigOption() throws Exception { + setLogAllRefUpdates(false); + writeRef("refs/heads/master", A); + + Map<String, ReflogEntry> oldLogs = + getLastReflogs("refs/heads/master", "refs/heads/branch"); + assertTrue(oldLogs.isEmpty()); + + List<ReceiveCommand> cmds = Arrays.asList( + new ReceiveCommand(A, B, "refs/heads/master", UPDATE), + new ReceiveCommand(zeroId(), B, "refs/heads/branch", CREATE)); + execute(newBatchUpdate(cmds).setRefLogMessage("a reflog", false)); + + assertResults(cmds, OK, OK); + assertReflogUnchanged(oldLogs, "refs/heads/master"); + assertReflogUnchanged(oldLogs, "refs/heads/branch"); + } + + @Test + public void forceRefLogInUpdate() throws Exception { + setLogAllRefUpdates(false); + writeRef("refs/heads/master", A); + assertTrue( + getLastReflogs("refs/heads/master", "refs/heads/branch").isEmpty()); + + List<ReceiveCommand> cmds = Arrays.asList( + new ReceiveCommand(A, B, "refs/heads/master", UPDATE), + new ReceiveCommand(zeroId(), B, "refs/heads/branch", CREATE)); + execute( + newBatchUpdate(cmds) + .setRefLogMessage("a reflog", false) + .setForceRefLog(true)); + + assertResults(cmds, OK, OK); + assertReflogEquals( + reflog(A, B, new PersonIdent(diskRepo), "a reflog"), + getLastReflog("refs/heads/master")); + assertReflogEquals( + reflog(zeroId(), B, new PersonIdent(diskRepo), "a reflog"), + getLastReflog("refs/heads/branch")); + } + + @Test + public void forceRefLogInCommand() throws Exception { + setLogAllRefUpdates(false); + writeRef("refs/heads/master", A); + + Map<String, ReflogEntry> oldLogs = + getLastReflogs("refs/heads/master", "refs/heads/branch"); + assertTrue(oldLogs.isEmpty()); + + List<ReceiveCommand> cmds = Arrays.asList( + new ReceiveCommand(A, B, "refs/heads/master", UPDATE), + new ReceiveCommand(zeroId(), B, "refs/heads/branch", CREATE)); + cmds.get(1).setForceRefLog(true); + execute(newBatchUpdate(cmds).setRefLogMessage("a reflog", false)); + + assertResults(cmds, OK, OK); + assertReflogUnchanged(oldLogs, "refs/heads/master"); + assertReflogEquals( + reflog(zeroId(), B, new PersonIdent(diskRepo), "a reflog"), + getLastReflog("refs/heads/branch")); + } + + @Test + public void packedRefsLockFailure() throws Exception { + writeLooseRef("refs/heads/master", A); + + List<ReceiveCommand> cmds = Arrays.asList( + new ReceiveCommand(A, B, "refs/heads/master", UPDATE), + new ReceiveCommand(zeroId(), B, "refs/heads/branch", CREATE)); + + LockFile myLock = refdir.lockPackedRefs(); + try { + execute(newBatchUpdate(cmds).setAllowNonFastForwards(true)); + + assertFalse(getLockFile("refs/heads/master").exists()); + assertFalse(getLockFile("refs/heads/branch").exists()); + + if (atomic) { + assertResults(cmds, LOCK_FAILURE, TRANSACTION_ABORTED); + assertRefs("refs/heads/master", A); + assertEquals(1, refsChangedEvents); + } else { + // Only operates on loose refs, doesn't care that packed-refs is locked. + assertResults(cmds, OK, OK); + assertRefs( + "refs/heads/master", B, + "refs/heads/branch", B); + assertEquals(3, refsChangedEvents); + } + } finally { + myLock.unlock(); + } + } + + @Test + public void oneRefLockFailure() throws Exception { + writeLooseRef("refs/heads/master", A); + + List<ReceiveCommand> cmds = Arrays.asList( + new ReceiveCommand(zeroId(), B, "refs/heads/branch", CREATE), + new ReceiveCommand(A, B, "refs/heads/master", UPDATE)); + + LockFile myLock = new LockFile(refdir.fileFor("refs/heads/master")); + assertTrue(myLock.lock()); + try { + execute(newBatchUpdate(cmds).setAllowNonFastForwards(true)); + + assertFalse(LockFile.getLockFile(refdir.packedRefsFile).exists()); + assertFalse(getLockFile("refs/heads/branch").exists()); + + if (atomic) { + assertResults(cmds, TRANSACTION_ABORTED, LOCK_FAILURE); + assertRefs("refs/heads/master", A); + assertEquals(1, refsChangedEvents); + } else { + assertResults(cmds, OK, LOCK_FAILURE); + assertRefs( + "refs/heads/branch", B, + "refs/heads/master", A); + assertEquals(2, refsChangedEvents); + } + } finally { + myLock.unlock(); + } + } + + @Test + public void singleRefUpdateDoesNotRequirePackedRefsLock() throws Exception { + writeLooseRef("refs/heads/master", A); + + List<ReceiveCommand> cmds = Arrays.asList( + new ReceiveCommand(A, B, "refs/heads/master", UPDATE)); + + LockFile myLock = refdir.lockPackedRefs(); + try { + execute(newBatchUpdate(cmds).setAllowNonFastForwards(true)); + + assertFalse(getLockFile("refs/heads/master").exists()); + assertResults(cmds, OK); + assertEquals(2, refsChangedEvents); + assertRefs("refs/heads/master", B); + } finally { + myLock.unlock(); + } + } + + @Test + public void atomicUpdateRespectsInProcessLock() throws Exception { + assumeTrue(atomic); + + writeLooseRef("refs/heads/master", A); + + List<ReceiveCommand> cmds = Arrays.asList( + new ReceiveCommand(A, B, "refs/heads/master", UPDATE), + new ReceiveCommand(zeroId(), B, "refs/heads/branch", CREATE)); + + Thread t = new Thread(() -> { + try { + execute(newBatchUpdate(cmds).setAllowNonFastForwards(true)); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + + ReentrantLock l = refdir.inProcessPackedRefsLock; + l.lock(); + try { + t.start(); + long timeoutSecs = 10; + long startNanos = System.nanoTime(); + + // Hold onto the lock until we observe the worker thread has attempted to + // acquire it. + while (l.getQueueLength() == 0) { + long elapsedNanos = System.nanoTime() - startNanos; + assertTrue( + "timed out waiting for work thread to attempt to acquire lock", + NANOSECONDS.toSeconds(elapsedNanos) < timeoutSecs); + Thread.sleep(3); + } + + // Once we unlock, the worker thread should finish the update promptly. + l.unlock(); + t.join(SECONDS.toMillis(timeoutSecs)); + assertFalse(t.isAlive()); + } finally { + if (l.isHeldByCurrentThread()) { + l.unlock(); + } + } + + assertResults(cmds, OK, OK); + assertEquals(2, refsChangedEvents); + assertRefs( + "refs/heads/master", B, + "refs/heads/branch", B); + } + + private void setLogAllRefUpdates(boolean enable) throws Exception { + StoredConfig cfg = diskRepo.getConfig(); + cfg.load(); + cfg.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null, + ConfigConstants.CONFIG_KEY_LOGALLREFUPDATES, enable); + cfg.save(); + } + + private void writeLooseRef(String name, AnyObjectId id) throws IOException { + write(new File(diskRepo.getDirectory(), name), id.name() + "\n"); + } + + private void writeRef(String name, AnyObjectId id) throws IOException { + RefUpdate u = diskRepo.updateRef(name); + u.setRefLogMessage(getClass().getSimpleName(), false); + u.setForceUpdate(true); + u.setNewObjectId(id); + RefUpdate.Result r = u.update(); + switch (r) { + case NEW: + case FORCED: + return; + default: + throw new IOException("Got " + r + " while updating " + name); + } + } + + private BatchRefUpdate newBatchUpdate(List<ReceiveCommand> cmds) { + BatchRefUpdate u = refdir.newBatchUpdate(); + if (atomic) { + assertTrue(u.isAtomic()); + } else { + u.setAtomic(false); + } + u.addCommand(cmds); + return u; + } + + private void execute(BatchRefUpdate u) throws IOException { + execute(u, false); + } + + private void execute(BatchRefUpdate u, boolean strictWork) throws IOException { + try (RevWalk rw = new RevWalk(diskRepo)) { + u.execute(rw, + strictWork ? new StrictWorkMonitor() : NullProgressMonitor.INSTANCE); + } + } + + private void assertRefs(Object... args) throws IOException { + if (args.length % 2 != 0) { + throw new IllegalArgumentException( + "expected even number of args: " + Arrays.toString(args)); + } + + Map<String, AnyObjectId> expected = new LinkedHashMap<>(); + for (int i = 0; i < args.length; i += 2) { + expected.put((String) args[i], (AnyObjectId) args[i + 1]); + } + + Map<String, Ref> refs = refdir.getRefs(RefDatabase.ALL); + Ref actualHead = refs.remove(Constants.HEAD); + if (actualHead != null) { + String actualLeafName = actualHead.getLeaf().getName(); + assertEquals( + "expected HEAD to point to refs/heads/master, got: " + actualLeafName, + "refs/heads/master", actualLeafName); + AnyObjectId expectedMaster = expected.get("refs/heads/master"); + assertNotNull("expected master ref since HEAD exists", expectedMaster); + assertEquals(expectedMaster, actualHead.getObjectId()); + } + + Map<String, AnyObjectId> actual = new LinkedHashMap<>(); + refs.forEach((n, r) -> actual.put(n, r.getObjectId())); + + assertEquals(expected.keySet(), actual.keySet()); + actual.forEach((n, a) -> assertEquals(n, expected.get(n), a)); + } + + enum Result { + OK(ReceiveCommand.Result.OK), + LOCK_FAILURE(ReceiveCommand.Result.LOCK_FAILURE), + REJECTED_NONFASTFORWARD(ReceiveCommand.Result.REJECTED_NONFASTFORWARD), + REJECTED_MISSING_OBJECT(ReceiveCommand.Result.REJECTED_MISSING_OBJECT), + TRANSACTION_ABORTED(ReceiveCommand::isTransactionAborted); + + final Predicate<? super ReceiveCommand> p; + + private Result(Predicate<? super ReceiveCommand> p) { + this.p = p; + } + + private Result(ReceiveCommand.Result result) { + this(c -> c.getResult() == result); + } + } + + private void assertResults( + List<ReceiveCommand> cmds, Result... expected) { + if (expected.length != cmds.size()) { + throw new IllegalArgumentException( + "expected " + cmds.size() + " result args"); + } + for (int i = 0; i < cmds.size(); i++) { + ReceiveCommand c = cmds.get(i); + Result r = expected[i]; + assertTrue( + String.format( + "result of command (%d) should be %s: %s %s%s", + Integer.valueOf(i), r, c, + c.getResult(), + c.getMessage() != null ? " (" + c.getMessage() + ")" : ""), + r.p.test(c)); + } + } + + private Map<String, ReflogEntry> getLastReflogs(String... names) + throws IOException { + Map<String, ReflogEntry> result = new LinkedHashMap<>(); + for (String name : names) { + ReflogEntry e = getLastReflog(name); + if (e != null) { + result.put(name, e); + } + } + return result; + } + + private ReflogEntry getLastReflog(String name) throws IOException { + ReflogReader r = diskRepo.getReflogReader(name); + if (r == null) { + return null; + } + return r.getLastEntry(); + } + + private File getLockFile(String refName) { + return LockFile.getLockFile(refdir.fileFor(refName)); + } + + private void assertReflogUnchanged( + Map<String, ReflogEntry> old, String name) throws IOException { + assertReflogEquals(old.get(name), getLastReflog(name), true); + } + + private static void assertReflogEquals( + ReflogEntry expected, ReflogEntry actual) { + assertReflogEquals(expected, actual, false); + } + + private static void assertReflogEquals( + ReflogEntry expected, ReflogEntry actual, boolean strictTime) { + if (expected == null) { + assertNull(actual); + return; + } + assertNotNull(actual); + assertEquals(expected.getOldId(), actual.getOldId()); + assertEquals(expected.getNewId(), actual.getNewId()); + if (strictTime) { + assertEquals(expected.getWho(), actual.getWho()); + } else { + assertEquals(expected.getWho().getName(), actual.getWho().getName()); + assertEquals( + expected.getWho().getEmailAddress(), + actual.getWho().getEmailAddress()); + } + assertEquals(expected.getComment(), actual.getComment()); + } + + private static ReflogEntry reflog(ObjectId oldId, ObjectId newId, + PersonIdent who, String comment) { + return new ReflogEntry() { + @Override + public ObjectId getOldId() { + return oldId; + } + + @Override + public ObjectId getNewId() { + return newId; + } + + @Override + public PersonIdent getWho() { + return who; + } + + @Override + public String getComment() { + return comment; + } + + @Override + public CheckoutEntry parseCheckout() { + throw new UnsupportedOperationException(); + } + }; + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPackRefsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPackRefsTest.java index 11a2a22edb..c43bdbd298 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPackRefsTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPackRefsTest.java @@ -43,12 +43,13 @@ package org.eclipse.jgit.internal.storage.file; -import static java.lang.Integer.valueOf; +import static org.hamcrest.Matchers.lessThanOrEqualTo; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThat; import java.io.File; import java.io.IOException; @@ -74,6 +75,7 @@ import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.storage.file.FileBasedConfig; import org.junit.Test; +@SuppressWarnings("boxing") public class GcPackRefsTest extends GcTestCase { @Test public void looseRefPacked() throws Exception { @@ -100,27 +102,23 @@ public class GcPackRefsTest extends GcTestCase { RevBlob a = tr.blob("a"); tr.lightweightTag("t", a); - final CyclicBarrier syncPoint = new CyclicBarrier(2); + CyclicBarrier syncPoint = new CyclicBarrier(2); - Callable<Integer> packRefs = new Callable<Integer>() { - - /** @return 0 for success, 1 in case of error when writing pack */ - @Override - public Integer call() throws Exception { - syncPoint.await(); - try { - gc.packRefs(); - return valueOf(0); - } catch (IOException e) { - return valueOf(1); - } + // Returns 0 for success, 1 in case of error when writing pack. + Callable<Integer> packRefs = () -> { + syncPoint.await(); + try { + gc.packRefs(); + return 0; + } catch (IOException e) { + return 1; } }; ExecutorService pool = Executors.newFixedThreadPool(2); try { Future<Integer> p1 = pool.submit(packRefs); Future<Integer> p2 = pool.submit(packRefs); - assertEquals(1, p1.get().intValue() + p2.get().intValue()); + assertThat(p1.get() + p2.get(), lessThanOrEqualTo(1)); } finally { pool.shutdown(); pool.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcTemporaryFilesTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcTemporaryFilesTest.java new file mode 100644 index 0000000000..59d544e63f --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcTemporaryFilesTest.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2017 Ericsson + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.internal.storage.file; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.nio.file.Paths; +import java.time.Instant; + +import org.junit.Before; +import org.junit.Test; + +public class GcTemporaryFilesTest extends GcTestCase { + private static final String TEMP_IDX = "gc_1234567890.idx_tmp"; + + private static final String TEMP_PACK = "gc_1234567890.pack_tmp"; + + private File packDir; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + packDir = Paths.get(repo.getObjectsDirectory().getAbsolutePath(), + "pack").toFile(); //$NON-NLS-1$ + } + + @Test + public void oldTempPacksAndIdxAreDeleted() throws Exception { + File tempIndex = new File(packDir, TEMP_IDX); + File tempPack = new File(packDir, TEMP_PACK); + if (!packDir.exists() || !packDir.isDirectory()) { + assertTrue(packDir.mkdirs()); + } + assertTrue(tempPack.createNewFile()); + assertTrue(tempIndex.createNewFile()); + assertTrue(tempIndex.exists()); + assertTrue(tempPack.exists()); + long _24HoursBefore = Instant.now().toEpochMilli() + - 24 * 60 * 62 * 1000; + tempIndex.setLastModified(_24HoursBefore); + tempPack.setLastModified(_24HoursBefore); + gc.gc(); + assertFalse(tempIndex.exists()); + assertFalse(tempPack.exists()); + } + + @Test + public void recentTempPacksAndIdxAreNotDeleted() throws Exception { + File tempIndex = new File(packDir, TEMP_IDX); + File tempPack = new File(packDir, TEMP_PACK); + if (!packDir.exists() || !packDir.isDirectory()) { + assertTrue(packDir.mkdirs()); + } + assertTrue(tempPack.createNewFile()); + assertTrue(tempIndex.createNewFile()); + assertTrue(tempIndex.exists()); + assertTrue(tempPack.exists()); + gc.gc(); + assertTrue(tempIndex.exists()); + assertTrue(tempPack.exists()); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackInserterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackInserterTest.java new file mode 100644 index 0000000000..8596f74f81 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackInserterTest.java @@ -0,0 +1,585 @@ +/* + * Copyright (C) 2017, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.internal.storage.file; + +import static java.util.Comparator.comparing; +import static java.util.stream.Collectors.toList; + +import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; +import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.lessThan; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Random; +import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.dircache.DirCacheBuilder; +import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectLoader; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.ObjectStream; +import org.eclipse.jgit.storage.file.WindowCacheConfig; +import org.eclipse.jgit.treewalk.CanonicalTreeParser; +import org.eclipse.jgit.util.IO; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +@SuppressWarnings("boxing") +public class PackInserterTest extends RepositoryTestCase { + private WindowCacheConfig origWindowCacheConfig; + + @Before + public void setWindowCacheConfig() { + origWindowCacheConfig = new WindowCacheConfig(); + origWindowCacheConfig.install(); + } + + @After + public void resetWindowCacheConfig() { + origWindowCacheConfig.install(); + } + + @Before + public void emptyAtSetUp() throws Exception { + assertEquals(0, listPacks().size()); + assertNoObjects(); + } + + @Test + public void noFlush() throws Exception { + try (PackInserter ins = newInserter()) { + ins.insert(OBJ_BLOB, Constants.encode("foo contents")); + // No flush. + } + assertNoObjects(); + } + + @Test + public void flushEmptyPack() throws Exception { + try (PackInserter ins = newInserter()) { + ins.flush(); + } + assertNoObjects(); + } + + @Test + public void singlePack() throws Exception { + ObjectId blobId; + byte[] blob = Constants.encode("foo contents"); + ObjectId treeId; + ObjectId commitId; + byte[] commit; + try (PackInserter ins = newInserter()) { + blobId = ins.insert(OBJ_BLOB, blob); + + DirCache dc = DirCache.newInCore(); + DirCacheBuilder b = dc.builder(); + DirCacheEntry dce = new DirCacheEntry("foo"); + dce.setFileMode(FileMode.REGULAR_FILE); + dce.setObjectId(blobId); + b.add(dce); + b.finish(); + treeId = dc.writeTree(ins); + + CommitBuilder cb = new CommitBuilder(); + cb.setTreeId(treeId); + cb.setAuthor(author); + cb.setCommitter(committer); + cb.setMessage("Commit message"); + commit = cb.toByteArray(); + commitId = ins.insert(cb); + ins.flush(); + } + + assertPacksOnly(); + List<PackFile> packs = listPacks(); + assertEquals(1, packs.size()); + assertEquals(3, packs.get(0).getObjectCount()); + + try (ObjectReader reader = db.newObjectReader()) { + assertBlob(reader, blobId, blob); + + CanonicalTreeParser treeParser = + new CanonicalTreeParser(null, reader, treeId); + assertEquals("foo", treeParser.getEntryPathString()); + assertEquals(blobId, treeParser.getEntryObjectId()); + + ObjectLoader commitLoader = reader.open(commitId); + assertEquals(OBJ_COMMIT, commitLoader.getType()); + assertArrayEquals(commit, commitLoader.getBytes()); + } + } + + @Test + public void multiplePacks() throws Exception { + ObjectId blobId1; + ObjectId blobId2; + byte[] blob1 = Constants.encode("blob1"); + byte[] blob2 = Constants.encode("blob2"); + + try (PackInserter ins = newInserter()) { + blobId1 = ins.insert(OBJ_BLOB, blob1); + ins.flush(); + blobId2 = ins.insert(OBJ_BLOB, blob2); + ins.flush(); + } + + assertPacksOnly(); + List<PackFile> packs = listPacks(); + assertEquals(2, packs.size()); + assertEquals(1, packs.get(0).getObjectCount()); + assertEquals(1, packs.get(1).getObjectCount()); + + try (ObjectReader reader = db.newObjectReader()) { + assertBlob(reader, blobId1, blob1); + assertBlob(reader, blobId2, blob2); + } + } + + @Test + public void largeBlob() throws Exception { + ObjectId blobId; + byte[] blob = newLargeBlob(); + try (PackInserter ins = newInserter()) { + assertThat(blob.length, greaterThan(ins.getBufferSize())); + blobId = + ins.insert(OBJ_BLOB, blob.length, new ByteArrayInputStream(blob)); + ins.flush(); + } + + assertPacksOnly(); + Collection<PackFile> packs = listPacks(); + assertEquals(1, packs.size()); + PackFile p = packs.iterator().next(); + assertEquals(1, p.getObjectCount()); + + try (ObjectReader reader = db.newObjectReader()) { + assertBlob(reader, blobId, blob); + } + } + + @Test + public void overwriteExistingPack() throws Exception { + ObjectId blobId; + byte[] blob = Constants.encode("foo contents"); + + try (PackInserter ins = newInserter()) { + blobId = ins.insert(OBJ_BLOB, blob); + ins.flush(); + } + + assertPacksOnly(); + List<PackFile> packs = listPacks(); + assertEquals(1, packs.size()); + PackFile pack = packs.get(0); + assertEquals(1, pack.getObjectCount()); + + String inode = getInode(pack.getPackFile()); + + try (PackInserter ins = newInserter()) { + ins.checkExisting(false); + assertEquals(blobId, ins.insert(OBJ_BLOB, blob)); + ins.flush(); + } + + assertPacksOnly(); + packs = listPacks(); + assertEquals(1, packs.size()); + pack = packs.get(0); + assertEquals(1, pack.getObjectCount()); + + if (inode != null) { + // Old file was overwritten with new file, although objects were + // equivalent. + assertNotEquals(inode, getInode(pack.getPackFile())); + } + } + + @Test + public void checkExisting() throws Exception { + ObjectId blobId; + byte[] blob = Constants.encode("foo contents"); + + try (PackInserter ins = newInserter()) { + blobId = ins.insert(OBJ_BLOB, blob); + ins.insert(OBJ_BLOB, Constants.encode("another blob")); + ins.flush(); + } + + assertPacksOnly(); + assertEquals(1, listPacks().size()); + + try (PackInserter ins = newInserter()) { + assertEquals(blobId, ins.insert(OBJ_BLOB, blob)); + ins.flush(); + } + + assertPacksOnly(); + assertEquals(1, listPacks().size()); + + try (PackInserter ins = newInserter()) { + ins.checkExisting(false); + assertEquals(blobId, ins.insert(OBJ_BLOB, blob)); + ins.flush(); + } + + assertPacksOnly(); + assertEquals(2, listPacks().size()); + + try (ObjectReader reader = db.newObjectReader()) { + assertBlob(reader, blobId, blob); + } + } + + @Test + public void insertSmallInputStreamRespectsCheckExisting() throws Exception { + ObjectId blobId; + byte[] blob = Constants.encode("foo contents"); + try (PackInserter ins = newInserter()) { + assertThat(blob.length, lessThan(ins.getBufferSize())); + blobId = ins.insert(OBJ_BLOB, blob); + ins.insert(OBJ_BLOB, Constants.encode("another blob")); + ins.flush(); + } + + assertPacksOnly(); + assertEquals(1, listPacks().size()); + + try (PackInserter ins = newInserter()) { + assertEquals(blobId, + ins.insert(OBJ_BLOB, blob.length, new ByteArrayInputStream(blob))); + ins.flush(); + } + + assertPacksOnly(); + assertEquals(1, listPacks().size()); + } + + @Test + public void insertLargeInputStreamBypassesCheckExisting() throws Exception { + ObjectId blobId; + byte[] blob = newLargeBlob(); + + try (PackInserter ins = newInserter()) { + assertThat(blob.length, greaterThan(ins.getBufferSize())); + blobId = ins.insert(OBJ_BLOB, blob); + ins.insert(OBJ_BLOB, Constants.encode("another blob")); + ins.flush(); + } + + assertPacksOnly(); + assertEquals(1, listPacks().size()); + + try (PackInserter ins = newInserter()) { + assertEquals(blobId, + ins.insert(OBJ_BLOB, blob.length, new ByteArrayInputStream(blob))); + ins.flush(); + } + + assertPacksOnly(); + assertEquals(2, listPacks().size()); + } + + @Test + public void readBackSmallFiles() throws Exception { + ObjectId blobId1; + ObjectId blobId2; + ObjectId blobId3; + byte[] blob1 = Constants.encode("blob1"); + byte[] blob2 = Constants.encode("blob2"); + byte[] blob3 = Constants.encode("blob3"); + try (PackInserter ins = newInserter()) { + assertThat(blob1.length, lessThan(ins.getBufferSize())); + blobId1 = ins.insert(OBJ_BLOB, blob1); + + try (ObjectReader reader = ins.newReader()) { + assertBlob(reader, blobId1, blob1); + } + + // Read-back should not mess up the file pointer. + blobId2 = ins.insert(OBJ_BLOB, blob2); + ins.flush(); + + blobId3 = ins.insert(OBJ_BLOB, blob3); + } + + assertPacksOnly(); + List<PackFile> packs = listPacks(); + assertEquals(1, packs.size()); + assertEquals(2, packs.get(0).getObjectCount()); + + try (ObjectReader reader = db.newObjectReader()) { + assertBlob(reader, blobId1, blob1); + assertBlob(reader, blobId2, blob2); + + try { + reader.open(blobId3); + fail("Expected MissingObjectException"); + } catch (MissingObjectException expected) { + // Expected. + } + } + } + + @Test + public void readBackLargeFile() throws Exception { + ObjectId blobId; + byte[] blob = newLargeBlob(); + + WindowCacheConfig wcc = new WindowCacheConfig(); + wcc.setStreamFileThreshold(1024); + wcc.install(); + try (ObjectReader reader = db.newObjectReader()) { + assertThat(blob.length, greaterThan(reader.getStreamFileThreshold())); + } + + try (PackInserter ins = newInserter()) { + blobId = ins.insert(OBJ_BLOB, blob); + + try (ObjectReader reader = ins.newReader()) { + // Double-check threshold is propagated. + assertThat(blob.length, greaterThan(reader.getStreamFileThreshold())); + assertBlob(reader, blobId, blob); + } + } + + assertPacksOnly(); + // Pack was streamed out to disk and read back from the temp file, but + // ultimately rolled back and deleted. + assertEquals(0, listPacks().size()); + + try (ObjectReader reader = db.newObjectReader()) { + try { + reader.open(blobId); + fail("Expected MissingObjectException"); + } catch (MissingObjectException expected) { + // Expected. + } + } + } + + @Test + public void readBackFallsBackToRepo() throws Exception { + ObjectId blobId; + byte[] blob = Constants.encode("foo contents"); + try (PackInserter ins = newInserter()) { + assertThat(blob.length, lessThan(ins.getBufferSize())); + blobId = ins.insert(OBJ_BLOB, blob); + ins.flush(); + } + + try (PackInserter ins = newInserter(); + ObjectReader reader = ins.newReader()) { + assertBlob(reader, blobId, blob); + } + } + + @Test + public void readBackSmallObjectBeforeLargeObject() throws Exception { + WindowCacheConfig wcc = new WindowCacheConfig(); + wcc.setStreamFileThreshold(1024); + wcc.install(); + + ObjectId blobId1; + ObjectId blobId2; + ObjectId largeId; + byte[] blob1 = Constants.encode("blob1"); + byte[] blob2 = Constants.encode("blob2"); + byte[] largeBlob = newLargeBlob(); + try (PackInserter ins = newInserter()) { + assertThat(blob1.length, lessThan(ins.getBufferSize())); + assertThat(largeBlob.length, greaterThan(ins.getBufferSize())); + + blobId1 = ins.insert(OBJ_BLOB, blob1); + largeId = ins.insert(OBJ_BLOB, largeBlob); + + try (ObjectReader reader = ins.newReader()) { + // A previous bug did not reset the file pointer to EOF after reading + // back. We need to seek to something further back than a full buffer, + // since the read-back code eagerly reads a full buffer's worth of data + // from the file to pass to the inflater. If we seeked back just a small + // amount, this step would consume the rest of the file, so the file + // pointer would coincidentally end up back at EOF, hiding the bug. + assertBlob(reader, blobId1, blob1); + } + + blobId2 = ins.insert(OBJ_BLOB, blob2); + + try (ObjectReader reader = ins.newReader()) { + assertBlob(reader, blobId1, blob1); + assertBlob(reader, blobId2, blob2); + assertBlob(reader, largeId, largeBlob); + } + + ins.flush(); + } + + try (ObjectReader reader = db.newObjectReader()) { + assertBlob(reader, blobId1, blob1); + assertBlob(reader, blobId2, blob2); + assertBlob(reader, largeId, largeBlob); + } + } + + private List<PackFile> listPacks() throws Exception { + List<PackFile> fromOpenDb = listPacks(db); + List<PackFile> reopened; + try (FileRepository db2 = new FileRepository(db.getDirectory())) { + reopened = listPacks(db2); + } + assertEquals(fromOpenDb.size(), reopened.size()); + for (int i = 0 ; i < fromOpenDb.size(); i++) { + PackFile a = fromOpenDb.get(i); + PackFile b = reopened.get(i); + assertEquals(a.getPackName(), b.getPackName()); + assertEquals( + a.getPackFile().getAbsolutePath(), b.getPackFile().getAbsolutePath()); + assertEquals(a.getObjectCount(), b.getObjectCount()); + a.getObjectCount(); + } + return fromOpenDb; + } + + private static List<PackFile> listPacks(FileRepository db) throws Exception { + return db.getObjectDatabase().getPacks().stream() + .sorted(comparing(PackFile::getPackName)).collect(toList()); + } + + private PackInserter newInserter() { + return db.getObjectDatabase().newPackInserter(); + } + + private static byte[] newLargeBlob() { + byte[] blob = new byte[10240]; + new Random(0).nextBytes(blob); + return blob; + } + + private static String getInode(File f) throws Exception { + BasicFileAttributes attrs = Files.readAttributes( + f.toPath(), BasicFileAttributes.class); + Object k = attrs.fileKey(); + if (k == null) { + return null; + } + Pattern p = Pattern.compile("^\\(dev=[^,]*,ino=(\\d+)\\)$"); + Matcher m = p.matcher(k.toString()); + return m.matches() ? m.group(1) : null; + } + + private static void assertBlob(ObjectReader reader, ObjectId id, + byte[] expected) throws Exception { + ObjectLoader loader = reader.open(id); + assertEquals(OBJ_BLOB, loader.getType()); + assertEquals(expected.length, loader.getSize()); + try (ObjectStream s = loader.openStream()) { + int n = (int) s.getSize(); + byte[] actual = new byte[n]; + assertEquals(n, IO.readFully(s, actual, 0)); + assertArrayEquals(expected, actual); + } + } + + private void assertPacksOnly() throws Exception { + new BadFileCollector(f -> !f.endsWith(".pack") && !f.endsWith(".idx")) + .assertNoBadFiles(db.getObjectDatabase().getDirectory()); + } + + private void assertNoObjects() throws Exception { + new BadFileCollector(f -> true) + .assertNoBadFiles(db.getObjectDatabase().getDirectory()); + } + + private static class BadFileCollector extends SimpleFileVisitor<Path> { + private final Predicate<String> badName; + private List<String> bad; + + BadFileCollector(Predicate<String> badName) { + this.badName = badName; + } + + void assertNoBadFiles(File f) throws IOException { + bad = new ArrayList<>(); + Files.walkFileTree(f.toPath(), this); + if (!bad.isEmpty()) { + fail("unexpected files in object directory: " + bad); + } + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + String name = file.getFileName().toString(); + if (!attrs.isDirectory() && badName.test(name)) { + bad.add(name); + } + return FileVisitResult.CONTINUE; + } + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java index 53db123d34..fefccf314f 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java @@ -61,32 +61,27 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; -import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import org.eclipse.jgit.errors.LockFailedException; import org.eclipse.jgit.events.ListenerHandle; import org.eclipse.jgit.events.RefsChangedEvent; import org.eclipse.jgit.events.RefsChangedListener; import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase; import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.lib.AnyObjectId; -import org.eclipse.jgit.lib.BatchRefUpdate; -import org.eclipse.jgit.lib.NullProgressMonitor; -import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Ref.Storage; import org.eclipse.jgit.lib.RefDatabase; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevTag; -import org.eclipse.jgit.revwalk.RevWalk; -import org.eclipse.jgit.transport.ReceiveCommand; -import org.eclipse.jgit.transport.ReceiveCommand.Type; import org.junit.Before; import org.junit.Test; +@SuppressWarnings("boxing") public class RefDirectoryTest extends LocalDiskRepositoryTestCase { private Repository diskRepo; @@ -1293,125 +1288,20 @@ public class RefDirectoryTest extends LocalDiskRepositoryTestCase { } @Test - public void testBatchRefUpdateSimpleNoForce() throws IOException { + public void testPackedRefsLockFailure() throws Exception { writeLooseRef("refs/heads/master", A); - writeLooseRef("refs/heads/masters", B); - List<ReceiveCommand> commands = Arrays.asList( - newCommand(A, B, "refs/heads/master", - ReceiveCommand.Type.UPDATE), - newCommand(B, A, "refs/heads/masters", - ReceiveCommand.Type.UPDATE_NONFASTFORWARD)); - BatchRefUpdate batchUpdate = refdir.newBatchUpdate(); - batchUpdate.addCommand(commands); - batchUpdate.execute(new RevWalk(diskRepo), new StrictWorkMonitor()); - Map<String, Ref> refs = refdir.getRefs(RefDatabase.ALL); - assertEquals(ReceiveCommand.Result.OK, commands.get(0).getResult()); - assertEquals(ReceiveCommand.Result.REJECTED_NONFASTFORWARD, commands - .get(1).getResult()); - assertEquals("[HEAD, refs/heads/master, refs/heads/masters]", refs - .keySet().toString()); - assertEquals(B.getId(), refs.get("refs/heads/master").getObjectId()); - assertEquals(B.getId(), refs.get("refs/heads/masters").getObjectId()); - } - - @Test - public void testBatchRefUpdateSimpleForce() throws IOException { - writeLooseRef("refs/heads/master", A); - writeLooseRef("refs/heads/masters", B); - List<ReceiveCommand> commands = Arrays.asList( - newCommand(A, B, "refs/heads/master", - ReceiveCommand.Type.UPDATE), - newCommand(B, A, "refs/heads/masters", - ReceiveCommand.Type.UPDATE_NONFASTFORWARD)); - BatchRefUpdate batchUpdate = refdir.newBatchUpdate(); - batchUpdate.setAllowNonFastForwards(true); - batchUpdate.addCommand(commands); - batchUpdate.execute(new RevWalk(diskRepo), new StrictWorkMonitor()); - Map<String, Ref> refs = refdir.getRefs(RefDatabase.ALL); - assertEquals(ReceiveCommand.Result.OK, commands.get(0).getResult()); - assertEquals(ReceiveCommand.Result.OK, commands.get(1).getResult()); - assertEquals("[HEAD, refs/heads/master, refs/heads/masters]", refs - .keySet().toString()); - assertEquals(B.getId(), refs.get("refs/heads/master").getObjectId()); - assertEquals(A.getId(), refs.get("refs/heads/masters").getObjectId()); - } - - @Test - public void testBatchRefUpdateNonFastForwardDoesNotDoExpensiveMergeCheck() - throws IOException { - writeLooseRef("refs/heads/master", B); - List<ReceiveCommand> commands = Arrays.asList( - newCommand(B, A, "refs/heads/master", - ReceiveCommand.Type.UPDATE_NONFASTFORWARD)); - BatchRefUpdate batchUpdate = refdir.newBatchUpdate(); - batchUpdate.setAllowNonFastForwards(true); - batchUpdate.addCommand(commands); - batchUpdate.execute(new RevWalk(diskRepo) { - @Override - public boolean isMergedInto(RevCommit base, RevCommit tip) { - throw new AssertionError("isMergedInto() should not be called"); - } - }, new StrictWorkMonitor()); - Map<String, Ref> refs = refdir.getRefs(RefDatabase.ALL); - assertEquals(ReceiveCommand.Result.OK, commands.get(0).getResult()); - assertEquals(A.getId(), refs.get("refs/heads/master").getObjectId()); - } - - @Test - public void testBatchRefUpdateConflict() throws IOException { - writeLooseRef("refs/heads/master", A); - writeLooseRef("refs/heads/masters", B); - List<ReceiveCommand> commands = Arrays.asList( - newCommand(A, B, "refs/heads/master", - ReceiveCommand.Type.UPDATE), - newCommand(null, A, "refs/heads/master/x", - ReceiveCommand.Type.CREATE), - newCommand(null, A, "refs/heads", ReceiveCommand.Type.CREATE)); - BatchRefUpdate batchUpdate = refdir.newBatchUpdate(); - batchUpdate.setAllowNonFastForwards(true); - batchUpdate.addCommand(commands); - batchUpdate - .execute(new RevWalk(diskRepo), NullProgressMonitor.INSTANCE); - Map<String, Ref> refs = refdir.getRefs(RefDatabase.ALL); - assertEquals(ReceiveCommand.Result.OK, commands.get(0).getResult()); - assertEquals(ReceiveCommand.Result.LOCK_FAILURE, commands.get(1) - .getResult()); - assertEquals(ReceiveCommand.Result.LOCK_FAILURE, commands.get(2) - .getResult()); - assertEquals("[HEAD, refs/heads/master, refs/heads/masters]", refs - .keySet().toString()); - assertEquals(B.getId(), refs.get("refs/heads/master").getObjectId()); - assertEquals(B.getId(), refs.get("refs/heads/masters").getObjectId()); - } - - @Test - public void testBatchRefUpdateConflictThanksToDelete() throws IOException { - writeLooseRef("refs/heads/master", A); - writeLooseRef("refs/heads/masters", B); - List<ReceiveCommand> commands = Arrays.asList( - newCommand(A, B, "refs/heads/master", - ReceiveCommand.Type.UPDATE), - newCommand(null, A, "refs/heads/masters/x", - ReceiveCommand.Type.CREATE), - newCommand(B, null, "refs/heads/masters", - ReceiveCommand.Type.DELETE)); - BatchRefUpdate batchUpdate = refdir.newBatchUpdate(); - batchUpdate.setAllowNonFastForwards(true); - batchUpdate.addCommand(commands); - batchUpdate.execute(new RevWalk(diskRepo), new StrictWorkMonitor()); - Map<String, Ref> refs = refdir.getRefs(RefDatabase.ALL); - assertEquals(ReceiveCommand.Result.OK, commands.get(0).getResult()); - assertEquals(ReceiveCommand.Result.OK, commands.get(1).getResult()); - assertEquals(ReceiveCommand.Result.OK, commands.get(2).getResult()); - assertEquals("[HEAD, refs/heads/master, refs/heads/masters/x]", refs - .keySet().toString()); - assertEquals(A.getId(), refs.get("refs/heads/masters/x").getObjectId()); - } - - private static ReceiveCommand newCommand(RevCommit a, RevCommit b, - String string, Type update) { - return new ReceiveCommand(a != null ? a.getId() : null, - b != null ? b.getId() : null, string, update); + refdir.setRetrySleepMs(Arrays.asList(0, 0)); + LockFile myLock = refdir.lockPackedRefs(); + try { + refdir.pack(Arrays.asList("refs/heads/master")); + fail("expected LockFailedException"); + } catch (LockFailedException e) { + assertEquals(refdir.packedRefsFile.getPath(), e.getFile().getPath()); + } finally { + myLock.unlock(); + } + Ref ref = refdir.getRef("refs/heads/master"); + assertEquals(Storage.LOOSE, ref.getStorage()); } private void writeLooseRef(String name, AnyObjectId id) throws IOException { @@ -1439,34 +1329,4 @@ public class RefDirectoryTest extends LocalDiskRepositoryTestCase { File path = new File(diskRepo.getDirectory(), name); assertTrue("deleted " + name, path.delete()); } - - private static final class StrictWorkMonitor implements ProgressMonitor { - private int lastWork, totalWork; - - @Override - public void start(int totalTasks) { - // empty - } - - @Override - public void beginTask(String title, int total) { - this.totalWork = total; - lastWork = 0; - } - - @Override - public void update(int completed) { - lastWork += completed; - } - - @Override - public void endTask() { - assertEquals("Units of work recorded", totalWork, lastWork); - } - - @Override - public boolean isCancelled() { - return false; - } - } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefUpdateTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefUpdateTest.java index 0f26b0fa61..d8d45a85b1 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefUpdateTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefUpdateTest.java @@ -45,6 +45,7 @@ package org.eclipse.jgit.internal.storage.file; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.eclipse.jgit.junit.Assert.assertEquals; import static org.eclipse.jgit.lib.Constants.LOCK_SUFFIX; import static org.junit.Assert.assertEquals; @@ -65,6 +66,7 @@ import java.util.Map.Entry; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefRename; @@ -241,14 +243,73 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase { @Test public void testDeleteHeadInBareRepo() throws IOException { Repository bareRepo = createBareRepository(); + String master = "refs/heads/master"; + Ref head = bareRepo.exactRef(Constants.HEAD); + assertNotNull(head); + assertTrue(head.isSymbolic()); + assertEquals(master, head.getLeaf().getName()); + assertNull(head.getObjectId()); + assertNull(bareRepo.exactRef(master)); + + ObjectId blobId; + try (ObjectInserter ins = bareRepo.newObjectInserter()) { + blobId = ins.insert(Constants.OBJ_BLOB, "contents".getBytes(UTF_8)); + ins.flush(); + } + + // Create master via HEAD, so we delete it. RefUpdate ref = bareRepo.updateRef(Constants.HEAD); - ref.setNewObjectId(ObjectId - .fromString("0123456789012345678901234567890123456789")); - // Create the HEAD ref so we can delete it. + ref.setNewObjectId(blobId); assertEquals(Result.NEW, ref.update()); + + head = bareRepo.exactRef(Constants.HEAD); + assertTrue(head.isSymbolic()); + assertEquals(master, head.getLeaf().getName()); + assertEquals(blobId, head.getLeaf().getObjectId()); + assertEquals(blobId, bareRepo.exactRef(master).getObjectId()); + + // Unlike in a non-bare repo, deleting the HEAD is allowed, and leaves HEAD + // back in a dangling state. ref = bareRepo.updateRef(Constants.HEAD); - delete(bareRepo, ref, Result.NO_CHANGE, true, true); + ref.setExpectedOldObjectId(blobId); + ref.setForceUpdate(true); + delete(bareRepo, ref, Result.FORCED, true, true); + + head = bareRepo.exactRef(Constants.HEAD); + assertNotNull(head); + assertTrue(head.isSymbolic()); + assertEquals(master, head.getLeaf().getName()); + assertNull(head.getObjectId()); + assertNull(bareRepo.exactRef(master)); + } + + @Test + public void testDeleteSymref() throws IOException { + RefUpdate dst = updateRef("refs/heads/abc"); + assertEquals(Result.NEW, dst.update()); + ObjectId id = dst.getNewObjectId(); + + RefUpdate u = db.updateRef("refs/symref"); + assertEquals(Result.NEW, u.link(dst.getName())); + + Ref ref = db.exactRef(u.getName()); + assertNotNull(ref); + assertTrue(ref.isSymbolic()); + assertEquals(dst.getName(), ref.getLeaf().getName()); + assertEquals(id, ref.getLeaf().getObjectId()); + + u = db.updateRef(u.getName()); + u.setDetachingSymbolicRef(); + u.setForceUpdate(true); + assertEquals(Result.FORCED, u.delete()); + + assertNull(db.exactRef(u.getName())); + ref = db.exactRef(dst.getName()); + assertNotNull(ref); + assertFalse(ref.isSymbolic()); + assertEquals(id, ref.getObjectId()); } + /** * Delete a loose ref and make sure the directory in refs is deleted too, * and the reflog dir too @@ -899,12 +960,66 @@ public class RefUpdateTest extends SampleDataRepositoryTestCase { "HEAD").getReverseEntries().get(0).getComment()); } + @Test + public void testCreateMissingObject() throws IOException { + String name = "refs/heads/abc"; + ObjectId bad = + ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"); + RefUpdate ru = db.updateRef(name); + ru.setNewObjectId(bad); + Result update = ru.update(); + assertEquals(Result.REJECTED_MISSING_OBJECT, update); + + Ref ref = db.exactRef(name); + assertNull(ref); + } + + @Test + public void testUpdateMissingObject() throws IOException { + String name = "refs/heads/abc"; + RefUpdate ru = updateRef(name); + Result update = ru.update(); + assertEquals(Result.NEW, update); + ObjectId oldId = ru.getNewObjectId(); + + ObjectId bad = + ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"); + ru = db.updateRef(name); + ru.setNewObjectId(bad); + update = ru.update(); + assertEquals(Result.REJECTED_MISSING_OBJECT, update); + + Ref ref = db.exactRef(name); + assertNotNull(ref); + assertEquals(oldId, ref.getObjectId()); + } + + @Test + public void testForceUpdateMissingObject() throws IOException { + String name = "refs/heads/abc"; + RefUpdate ru = updateRef(name); + Result update = ru.update(); + assertEquals(Result.NEW, update); + ObjectId oldId = ru.getNewObjectId(); + + ObjectId bad = + ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"); + ru = db.updateRef(name); + ru.setNewObjectId(bad); + update = ru.forceUpdate(); + assertEquals(Result.REJECTED_MISSING_OBJECT, update); + + Ref ref = db.exactRef(name); + assertNotNull(ref); + assertEquals(oldId, ref.getObjectId()); + } + private static void writeReflog(Repository db, ObjectId newId, String msg, String refName) throws IOException { RefDirectory refs = (RefDirectory) db.getRefDatabase(); RefDirectoryUpdate update = refs.newUpdate(refName, true); update.setNewObjectId(newId); - refs.log(update, msg, true); + refs.log(false, update, msg, true); } private static class SubclassedId extends ObjectId { diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ReflogWriterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ReflogWriterTest.java index 89b969e3f1..7f40bae556 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ReflogWriterTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/ReflogWriterTest.java @@ -61,7 +61,8 @@ public class ReflogWriterTest extends SampleDataRepositoryTestCase { @Test public void shouldFilterLineFeedFromMessage() throws Exception { - ReflogWriter writer = new ReflogWriter(db); + ReflogWriter writer = + new ReflogWriter((RefDirectory) db.getRefDatabase()); PersonIdent ident = new PersonIdent("John Doe", "john@doe.com", 1243028200000L, 120); ObjectId oldId = ObjectId diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java index ae1e531b83..9d23d8334c 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java @@ -661,33 +661,39 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase { @Test public void test028_LockPackedRef() throws IOException { + ObjectId id1; + ObjectId id2; + try (ObjectInserter ins = db.newObjectInserter()) { + id1 = ins.insert( + Constants.OBJ_BLOB, "contents1".getBytes(Constants.CHARSET)); + id2 = ins.insert( + Constants.OBJ_BLOB, "contents2".getBytes(Constants.CHARSET)); + ins.flush(); + } + writeTrashFile(".git/packed-refs", - "7f822839a2fe9760f386cbbbcb3f92c5fe81def7 refs/heads/foobar"); + id1.name() + " refs/heads/foobar"); writeTrashFile(".git/HEAD", "ref: refs/heads/foobar\n"); BUG_WorkAroundRacyGitIssues("packed-refs"); BUG_WorkAroundRacyGitIssues("HEAD"); ObjectId resolve = db.resolve("HEAD"); - assertEquals("7f822839a2fe9760f386cbbbcb3f92c5fe81def7", resolve.name()); + assertEquals(id1, resolve); RefUpdate lockRef = db.updateRef("HEAD"); - ObjectId newId = ObjectId - .fromString("07f822839a2fe9760f386cbbbcb3f92c5fe81def"); - lockRef.setNewObjectId(newId); + lockRef.setNewObjectId(id2); assertEquals(RefUpdate.Result.FORCED, lockRef.forceUpdate()); assertTrue(new File(db.getDirectory(), "refs/heads/foobar").exists()); - assertEquals(newId, db.resolve("refs/heads/foobar")); + assertEquals(id2, db.resolve("refs/heads/foobar")); // Again. The ref already exists RefUpdate lockRef2 = db.updateRef("HEAD"); - ObjectId newId2 = ObjectId - .fromString("7f822839a2fe9760f386cbbbcb3f92c5fe81def7"); - lockRef2.setNewObjectId(newId2); + lockRef2.setNewObjectId(id1); assertEquals(RefUpdate.Result.FORCED, lockRef2.forceUpdate()); assertTrue(new File(db.getDirectory(), "refs/heads/foobar").exists()); - assertEquals(newId2, db.resolve("refs/heads/foobar")); + assertEquals(id1, db.resolve("refs/heads/foobar")); } @Test diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftable/MergedReftableTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftable/MergedReftableTest.java new file mode 100644 index 0000000000..adba048e65 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftable/MergedReftableTest.java @@ -0,0 +1,392 @@ +/* + * Copyright (C) 2017, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.internal.storage.reftable; + +import static org.eclipse.jgit.lib.Constants.HEAD; +import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH; +import static org.eclipse.jgit.lib.Constants.R_HEADS; +import static org.eclipse.jgit.lib.Ref.Storage.NEW; +import static org.eclipse.jgit.lib.Ref.Storage.PACKED; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jgit.internal.storage.io.BlockSource; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdRef; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefComparator; +import org.junit.Test; + +public class MergedReftableTest { + @Test + public void noTables() throws IOException { + MergedReftable mr = merge(new byte[0][]); + try (RefCursor rc = mr.allRefs()) { + assertFalse(rc.next()); + } + try (RefCursor rc = mr.seekRef(HEAD)) { + assertFalse(rc.next()); + } + try (RefCursor rc = mr.seekRef(R_HEADS)) { + assertFalse(rc.next()); + } + } + + @Test + public void oneEmptyTable() throws IOException { + MergedReftable mr = merge(write()); + try (RefCursor rc = mr.allRefs()) { + assertFalse(rc.next()); + } + try (RefCursor rc = mr.seekRef(HEAD)) { + assertFalse(rc.next()); + } + try (RefCursor rc = mr.seekRef(R_HEADS)) { + assertFalse(rc.next()); + } + } + + @Test + public void twoEmptyTables() throws IOException { + MergedReftable mr = merge(write(), write()); + try (RefCursor rc = mr.allRefs()) { + assertFalse(rc.next()); + } + try (RefCursor rc = mr.seekRef(HEAD)) { + assertFalse(rc.next()); + } + try (RefCursor rc = mr.seekRef(R_HEADS)) { + assertFalse(rc.next()); + } + } + + @SuppressWarnings("boxing") + @Test + public void oneTableScan() throws IOException { + List<Ref> refs = new ArrayList<>(); + for (int i = 1; i <= 567; i++) { + refs.add(ref(String.format("refs/heads/%03d", i), i)); + } + + MergedReftable mr = merge(write(refs)); + try (RefCursor rc = mr.allRefs()) { + for (Ref exp : refs) { + assertTrue("has " + exp.getName(), rc.next()); + Ref act = rc.getRef(); + assertEquals(exp.getName(), act.getName()); + assertEquals(exp.getObjectId(), act.getObjectId()); + } + assertFalse(rc.next()); + } + } + + @Test + public void deleteIsHidden() throws IOException { + List<Ref> delta1 = Arrays.asList( + ref("refs/heads/apple", 1), + ref("refs/heads/master", 2)); + List<Ref> delta2 = Arrays.asList(delete("refs/heads/apple")); + + MergedReftable mr = merge(write(delta1), write(delta2)); + try (RefCursor rc = mr.allRefs()) { + assertTrue(rc.next()); + assertEquals("refs/heads/master", rc.getRef().getName()); + assertEquals(id(2), rc.getRef().getObjectId()); + assertFalse(rc.next()); + } + } + + @Test + public void twoTableSeek() throws IOException { + List<Ref> delta1 = Arrays.asList( + ref("refs/heads/apple", 1), + ref("refs/heads/master", 2)); + List<Ref> delta2 = Arrays.asList(ref("refs/heads/banana", 3)); + + MergedReftable mr = merge(write(delta1), write(delta2)); + try (RefCursor rc = mr.seekRef("refs/heads/master")) { + assertTrue(rc.next()); + assertEquals("refs/heads/master", rc.getRef().getName()); + assertEquals(id(2), rc.getRef().getObjectId()); + assertFalse(rc.next()); + } + } + + @Test + public void twoTableById() throws IOException { + List<Ref> delta1 = Arrays.asList( + ref("refs/heads/apple", 1), + ref("refs/heads/master", 2)); + List<Ref> delta2 = Arrays.asList(ref("refs/heads/banana", 3)); + + MergedReftable mr = merge(write(delta1), write(delta2)); + try (RefCursor rc = mr.byObjectId(id(2))) { + assertTrue(rc.next()); + assertEquals("refs/heads/master", rc.getRef().getName()); + assertEquals(id(2), rc.getRef().getObjectId()); + assertFalse(rc.next()); + } + } + + @SuppressWarnings("boxing") + @Test + public void fourTableScan() throws IOException { + List<Ref> base = new ArrayList<>(); + for (int i = 1; i <= 567; i++) { + base.add(ref(String.format("refs/heads/%03d", i), i)); + } + + List<Ref> delta1 = Arrays.asList( + ref("refs/heads/next", 4), + ref(String.format("refs/heads/%03d", 55), 4096)); + List<Ref> delta2 = Arrays.asList( + delete("refs/heads/next"), + ref(String.format("refs/heads/%03d", 55), 8192)); + List<Ref> delta3 = Arrays.asList( + ref("refs/heads/master", 4242), + ref(String.format("refs/heads/%03d", 42), 5120), + ref(String.format("refs/heads/%03d", 98), 6120)); + + List<Ref> expected = merge(base, delta1, delta2, delta3); + MergedReftable mr = merge( + write(base), + write(delta1), + write(delta2), + write(delta3)); + try (RefCursor rc = mr.allRefs()) { + for (Ref exp : expected) { + assertTrue("has " + exp.getName(), rc.next()); + Ref act = rc.getRef(); + assertEquals(exp.getName(), act.getName()); + assertEquals(exp.getObjectId(), act.getObjectId()); + } + assertFalse(rc.next()); + } + } + + @Test + public void scanIncludeDeletes() throws IOException { + List<Ref> delta1 = Arrays.asList(ref("refs/heads/next", 4)); + List<Ref> delta2 = Arrays.asList(delete("refs/heads/next")); + List<Ref> delta3 = Arrays.asList(ref("refs/heads/master", 8)); + + MergedReftable mr = merge(write(delta1), write(delta2), write(delta3)); + mr.setIncludeDeletes(true); + try (RefCursor rc = mr.allRefs()) { + assertTrue(rc.next()); + Ref r = rc.getRef(); + assertEquals("refs/heads/master", r.getName()); + assertEquals(id(8), r.getObjectId()); + + assertTrue(rc.next()); + r = rc.getRef(); + assertEquals("refs/heads/next", r.getName()); + assertEquals(NEW, r.getStorage()); + assertNull(r.getObjectId()); + + assertFalse(rc.next()); + } + } + + @SuppressWarnings("boxing") + @Test + public void oneTableSeek() throws IOException { + List<Ref> refs = new ArrayList<>(); + for (int i = 1; i <= 567; i++) { + refs.add(ref(String.format("refs/heads/%03d", i), i)); + } + + MergedReftable mr = merge(write(refs)); + for (Ref exp : refs) { + try (RefCursor rc = mr.seekRef(exp.getName())) { + assertTrue("has " + exp.getName(), rc.next()); + Ref act = rc.getRef(); + assertEquals(exp.getName(), act.getName()); + assertEquals(exp.getObjectId(), act.getObjectId()); + assertFalse(rc.next()); + } + } + } + + @Test + public void missedUpdate() throws IOException { + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + ReftableWriter writer = new ReftableWriter() + .setMinUpdateIndex(1) + .setMaxUpdateIndex(3) + .begin(buf); + writer.writeRef(ref("refs/heads/a", 1), 1); + writer.writeRef(ref("refs/heads/c", 3), 3); + writer.finish(); + byte[] base = buf.toByteArray(); + + byte[] delta = write(Arrays.asList( + ref("refs/heads/b", 2), + ref("refs/heads/c", 4)), + 2); + MergedReftable mr = merge(base, delta); + try (RefCursor rc = mr.allRefs()) { + assertTrue(rc.next()); + assertEquals("refs/heads/a", rc.getRef().getName()); + assertEquals(id(1), rc.getRef().getObjectId()); + assertEquals(1, rc.getUpdateIndex()); + + assertTrue(rc.next()); + assertEquals("refs/heads/b", rc.getRef().getName()); + assertEquals(id(2), rc.getRef().getObjectId()); + assertEquals(2, rc.getUpdateIndex()); + + assertTrue(rc.next()); + assertEquals("refs/heads/c", rc.getRef().getName()); + assertEquals(id(3), rc.getRef().getObjectId()); + assertEquals(3, rc.getUpdateIndex()); + } + } + + @Test + public void compaction() throws IOException { + List<Ref> delta1 = Arrays.asList( + ref("refs/heads/next", 4), + ref("refs/heads/master", 1)); + List<Ref> delta2 = Arrays.asList(delete("refs/heads/next")); + List<Ref> delta3 = Arrays.asList(ref("refs/heads/master", 8)); + + ReftableCompactor compactor = new ReftableCompactor(); + compactor.addAll(Arrays.asList( + read(write(delta1)), + read(write(delta2)), + read(write(delta3)))); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + compactor.compact(out); + byte[] table = out.toByteArray(); + + ReftableReader reader = read(table); + try (RefCursor rc = reader.allRefs()) { + assertTrue(rc.next()); + Ref r = rc.getRef(); + assertEquals("refs/heads/master", r.getName()); + assertEquals(id(8), r.getObjectId()); + assertFalse(rc.next()); + } + } + + private static MergedReftable merge(byte[]... table) { + List<Reftable> stack = new ArrayList<>(table.length); + for (byte[] b : table) { + stack.add(read(b)); + } + return new MergedReftable(stack); + } + + private static ReftableReader read(byte[] table) { + return new ReftableReader(BlockSource.from(table)); + } + + private static Ref ref(String name, int id) { + return new ObjectIdRef.PeeledNonTag(PACKED, name, id(id)); + } + + private static Ref delete(String name) { + return new ObjectIdRef.Unpeeled(NEW, name, null); + } + + private static ObjectId id(int i) { + byte[] buf = new byte[OBJECT_ID_LENGTH]; + buf[0] = (byte) (i & 0xff); + buf[1] = (byte) ((i >>> 8) & 0xff); + buf[2] = (byte) ((i >>> 16) & 0xff); + buf[3] = (byte) (i >>> 24); + return ObjectId.fromRaw(buf); + } + + private byte[] write(Ref... refs) throws IOException { + return write(Arrays.asList(refs)); + } + + private byte[] write(Collection<Ref> refs) throws IOException { + return write(refs, 1); + } + + private byte[] write(Collection<Ref> refs, long updateIndex) + throws IOException { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + new ReftableWriter() + .setMinUpdateIndex(updateIndex) + .setMaxUpdateIndex(updateIndex) + .begin(buffer) + .sortAndWriteRefs(refs) + .finish(); + return buffer.toByteArray(); + } + + @SafeVarargs + private static List<Ref> merge(List<Ref>... tables) { + Map<String, Ref> expect = new HashMap<>(); + for (List<Ref> t : tables) { + for (Ref r : t) { + if (r.getStorage() == NEW && r.getObjectId() == null) { + expect.remove(r.getName()); + } else { + expect.put(r.getName(), r); + } + } + } + + List<Ref> expected = new ArrayList<>(expect.values()); + Collections.sort(expected, RefComparator.INSTANCE); + return expected; + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftable/ReftableTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftable/ReftableTest.java new file mode 100644 index 0000000000..3ea3061e38 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftable/ReftableTest.java @@ -0,0 +1,726 @@ +/* + * Copyright (C) 2017, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.internal.storage.reftable; + +import static org.eclipse.jgit.lib.Constants.HEAD; +import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH; +import static org.eclipse.jgit.lib.Constants.R_HEADS; +import static org.eclipse.jgit.lib.Ref.Storage.NEW; +import static org.eclipse.jgit.lib.Ref.Storage.PACKED; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.internal.storage.io.BlockSource; +import org.eclipse.jgit.internal.storage.reftable.ReftableWriter.Stats; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdRef; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.ReflogEntry; +import org.eclipse.jgit.lib.SymbolicRef; +import org.junit.Test; + +public class ReftableTest { + private static final String MASTER = "refs/heads/master"; + private static final String NEXT = "refs/heads/next"; + private static final String V1_0 = "refs/tags/v1.0"; + + private Stats stats; + + @Test + public void emptyTable() throws IOException { + byte[] table = write(); + assertEquals(92 /* header, footer */, table.length); + assertEquals('R', table[0]); + assertEquals('E', table[1]); + assertEquals('F', table[2]); + assertEquals('T', table[3]); + assertEquals(0x01, table[4]); + assertTrue(ReftableConstants.isFileHeaderMagic(table, 0, 8)); + assertTrue(ReftableConstants.isFileHeaderMagic(table, 24, 92)); + + Reftable t = read(table); + try (RefCursor rc = t.allRefs()) { + assertFalse(rc.next()); + } + try (RefCursor rc = t.seekRef(HEAD)) { + assertFalse(rc.next()); + } + try (RefCursor rc = t.seekRef(R_HEADS)) { + assertFalse(rc.next()); + } + try (LogCursor rc = t.allLogs()) { + assertFalse(rc.next()); + } + } + + @Test + public void emptyVirtualTableFromRefs() throws IOException { + Reftable t = Reftable.from(Collections.emptyList()); + try (RefCursor rc = t.allRefs()) { + assertFalse(rc.next()); + } + try (RefCursor rc = t.seekRef(HEAD)) { + assertFalse(rc.next()); + } + try (LogCursor rc = t.allLogs()) { + assertFalse(rc.next()); + } + } + + @Test + public void estimateCurrentBytesOneRef() throws IOException { + Ref exp = ref(MASTER, 1); + int expBytes = 24 + 4 + 5 + 4 + MASTER.length() + 20 + 68; + + byte[] table; + ReftableConfig cfg = new ReftableConfig(); + cfg.setIndexObjects(false); + ReftableWriter writer = new ReftableWriter().setConfig(cfg); + try (ByteArrayOutputStream buf = new ByteArrayOutputStream()) { + writer.begin(buf); + assertEquals(92, writer.estimateTotalBytes()); + writer.writeRef(exp); + assertEquals(expBytes, writer.estimateTotalBytes()); + writer.finish(); + table = buf.toByteArray(); + } + assertEquals(expBytes, table.length); + } + + @SuppressWarnings("boxing") + @Test + public void estimateCurrentBytesWithIndex() throws IOException { + List<Ref> refs = new ArrayList<>(); + for (int i = 1; i <= 5670; i++) { + refs.add(ref(String.format("refs/heads/%04d", i), i)); + } + + ReftableConfig cfg = new ReftableConfig(); + cfg.setIndexObjects(false); + cfg.setMaxIndexLevels(1); + + int expBytes = 147860; + byte[] table; + ReftableWriter writer = new ReftableWriter().setConfig(cfg); + try (ByteArrayOutputStream buf = new ByteArrayOutputStream()) { + writer.begin(buf); + writer.sortAndWriteRefs(refs); + assertEquals(expBytes, writer.estimateTotalBytes()); + writer.finish(); + stats = writer.getStats(); + table = buf.toByteArray(); + } + assertEquals(1, stats.refIndexLevels()); + assertEquals(expBytes, table.length); + } + + @Test + public void oneIdRef() throws IOException { + Ref exp = ref(MASTER, 1); + byte[] table = write(exp); + assertEquals(24 + 4 + 5 + 4 + MASTER.length() + 20 + 68, table.length); + + ReftableReader t = read(table); + try (RefCursor rc = t.allRefs()) { + assertTrue(rc.next()); + Ref act = rc.getRef(); + assertNotNull(act); + assertEquals(PACKED, act.getStorage()); + assertTrue(act.isPeeled()); + assertFalse(act.isSymbolic()); + assertEquals(exp.getName(), act.getName()); + assertEquals(exp.getObjectId(), act.getObjectId()); + assertNull(act.getPeeledObjectId()); + assertFalse(rc.wasDeleted()); + assertFalse(rc.next()); + } + try (RefCursor rc = t.seekRef(MASTER)) { + assertTrue(rc.next()); + Ref act = rc.getRef(); + assertNotNull(act); + assertEquals(exp.getName(), act.getName()); + assertFalse(rc.next()); + } + } + + @Test + public void oneTagRef() throws IOException { + Ref exp = tag(V1_0, 1, 2); + byte[] table = write(exp); + assertEquals(24 + 4 + 5 + 3 + V1_0.length() + 40 + 68, table.length); + + ReftableReader t = read(table); + try (RefCursor rc = t.allRefs()) { + assertTrue(rc.next()); + Ref act = rc.getRef(); + assertNotNull(act); + assertEquals(PACKED, act.getStorage()); + assertTrue(act.isPeeled()); + assertFalse(act.isSymbolic()); + assertEquals(exp.getName(), act.getName()); + assertEquals(exp.getObjectId(), act.getObjectId()); + assertEquals(exp.getPeeledObjectId(), act.getPeeledObjectId()); + } + } + + @Test + public void oneSymbolicRef() throws IOException { + Ref exp = sym(HEAD, MASTER); + byte[] table = write(exp); + assertEquals( + 24 + 4 + 5 + 2 + HEAD.length() + 2 + MASTER.length() + 68, + table.length); + + ReftableReader t = read(table); + try (RefCursor rc = t.allRefs()) { + assertTrue(rc.next()); + Ref act = rc.getRef(); + assertNotNull(act); + assertTrue(act.isSymbolic()); + assertEquals(exp.getName(), act.getName()); + assertNotNull(act.getLeaf()); + assertEquals(MASTER, act.getTarget().getName()); + assertNull(act.getObjectId()); + } + } + + @Test + public void resolveSymbolicRef() throws IOException { + Reftable t = read(write( + sym(HEAD, "refs/heads/tmp"), + sym("refs/heads/tmp", MASTER), + ref(MASTER, 1))); + + Ref head = t.exactRef(HEAD); + assertNull(head.getObjectId()); + assertEquals("refs/heads/tmp", head.getTarget().getName()); + + head = t.resolve(head); + assertNotNull(head); + assertEquals(id(1), head.getObjectId()); + + Ref master = t.exactRef(MASTER); + assertNotNull(master); + assertSame(master, t.resolve(master)); + } + + @Test + public void failDeepChainOfSymbolicRef() throws IOException { + Reftable t = read(write( + sym(HEAD, "refs/heads/1"), + sym("refs/heads/1", "refs/heads/2"), + sym("refs/heads/2", "refs/heads/3"), + sym("refs/heads/3", "refs/heads/4"), + sym("refs/heads/4", "refs/heads/5"), + sym("refs/heads/5", MASTER), + ref(MASTER, 1))); + + Ref head = t.exactRef(HEAD); + assertNull(head.getObjectId()); + assertNull(t.resolve(head)); + } + + @Test + public void oneDeletedRef() throws IOException { + String name = "refs/heads/gone"; + Ref exp = newRef(name); + byte[] table = write(exp); + assertEquals(24 + 4 + 5 + 3 + name.length() + 68, table.length); + + ReftableReader t = read(table); + try (RefCursor rc = t.allRefs()) { + assertFalse(rc.next()); + } + + t.setIncludeDeletes(true); + try (RefCursor rc = t.allRefs()) { + assertTrue(rc.next()); + Ref act = rc.getRef(); + assertNotNull(act); + assertFalse(act.isSymbolic()); + assertEquals(name, act.getName()); + assertEquals(NEW, act.getStorage()); + assertNull(act.getObjectId()); + assertTrue(rc.wasDeleted()); + } + } + + @Test + public void seekNotFound() throws IOException { + Ref exp = ref(MASTER, 1); + ReftableReader t = read(write(exp)); + try (RefCursor rc = t.seekRef("refs/heads/a")) { + assertFalse(rc.next()); + } + try (RefCursor rc = t.seekRef("refs/heads/n")) { + assertFalse(rc.next()); + } + } + + @Test + public void namespaceNotFound() throws IOException { + Ref exp = ref(MASTER, 1); + ReftableReader t = read(write(exp)); + try (RefCursor rc = t.seekRef("refs/changes/")) { + assertFalse(rc.next()); + } + try (RefCursor rc = t.seekRef("refs/tags/")) { + assertFalse(rc.next()); + } + } + + @Test + public void namespaceHeads() throws IOException { + Ref master = ref(MASTER, 1); + Ref next = ref(NEXT, 2); + Ref v1 = tag(V1_0, 3, 4); + + ReftableReader t = read(write(master, next, v1)); + try (RefCursor rc = t.seekRef("refs/tags/")) { + assertTrue(rc.next()); + assertEquals(V1_0, rc.getRef().getName()); + assertFalse(rc.next()); + } + try (RefCursor rc = t.seekRef("refs/heads/")) { + assertTrue(rc.next()); + assertEquals(MASTER, rc.getRef().getName()); + + assertTrue(rc.next()); + assertEquals(NEXT, rc.getRef().getName()); + + assertFalse(rc.next()); + } + } + + @SuppressWarnings("boxing") + @Test + public void indexScan() throws IOException { + List<Ref> refs = new ArrayList<>(); + for (int i = 1; i <= 5670; i++) { + refs.add(ref(String.format("refs/heads/%04d", i), i)); + } + + byte[] table = write(refs); + assertTrue(stats.refIndexLevels() > 0); + assertTrue(stats.refIndexSize() > 0); + assertScan(refs, read(table)); + } + + @SuppressWarnings("boxing") + @Test + public void indexSeek() throws IOException { + List<Ref> refs = new ArrayList<>(); + for (int i = 1; i <= 5670; i++) { + refs.add(ref(String.format("refs/heads/%04d", i), i)); + } + + byte[] table = write(refs); + assertTrue(stats.refIndexLevels() > 0); + assertTrue(stats.refIndexSize() > 0); + assertSeek(refs, read(table)); + } + + @SuppressWarnings("boxing") + @Test + public void noIndexScan() throws IOException { + List<Ref> refs = new ArrayList<>(); + for (int i = 1; i <= 567; i++) { + refs.add(ref(String.format("refs/heads/%03d", i), i)); + } + + byte[] table = write(refs); + assertEquals(0, stats.refIndexLevels()); + assertEquals(0, stats.refIndexSize()); + assertEquals(table.length, stats.totalBytes()); + assertScan(refs, read(table)); + } + + @SuppressWarnings("boxing") + @Test + public void noIndexSeek() throws IOException { + List<Ref> refs = new ArrayList<>(); + for (int i = 1; i <= 567; i++) { + refs.add(ref(String.format("refs/heads/%03d", i), i)); + } + + byte[] table = write(refs); + assertEquals(0, stats.refIndexLevels()); + assertSeek(refs, read(table)); + } + + @Test + public void withReflog() throws IOException { + Ref master = ref(MASTER, 1); + Ref next = ref(NEXT, 2); + PersonIdent who = new PersonIdent("Log", "Ger", 1500079709, -8 * 60); + String msg = "test"; + + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + ReftableWriter writer = new ReftableWriter() + .setMinUpdateIndex(1) + .setMaxUpdateIndex(1) + .begin(buffer); + + writer.writeRef(master); + writer.writeRef(next); + + writer.writeLog(MASTER, 1, who, ObjectId.zeroId(), id(1), msg); + writer.writeLog(NEXT, 1, who, ObjectId.zeroId(), id(2), msg); + + writer.finish(); + byte[] table = buffer.toByteArray(); + assertEquals(247, table.length); + + ReftableReader t = read(table); + try (RefCursor rc = t.allRefs()) { + assertTrue(rc.next()); + assertEquals(MASTER, rc.getRef().getName()); + assertEquals(id(1), rc.getRef().getObjectId()); + assertEquals(1, rc.getUpdateIndex()); + + assertTrue(rc.next()); + assertEquals(NEXT, rc.getRef().getName()); + assertEquals(id(2), rc.getRef().getObjectId()); + assertFalse(rc.next()); + } + try (LogCursor lc = t.allLogs()) { + assertTrue(lc.next()); + assertEquals(MASTER, lc.getRefName()); + assertEquals(1, lc.getUpdateIndex()); + assertEquals(ObjectId.zeroId(), lc.getReflogEntry().getOldId()); + assertEquals(id(1), lc.getReflogEntry().getNewId()); + assertEquals(who, lc.getReflogEntry().getWho()); + assertEquals(msg, lc.getReflogEntry().getComment()); + + assertTrue(lc.next()); + assertEquals(NEXT, lc.getRefName()); + assertEquals(1, lc.getUpdateIndex()); + assertEquals(ObjectId.zeroId(), lc.getReflogEntry().getOldId()); + assertEquals(id(2), lc.getReflogEntry().getNewId()); + assertEquals(who, lc.getReflogEntry().getWho()); + assertEquals(msg, lc.getReflogEntry().getComment()); + + assertFalse(lc.next()); + } + } + + @Test + public void onlyReflog() throws IOException { + PersonIdent who = new PersonIdent("Log", "Ger", 1500079709, -8 * 60); + String msg = "test"; + + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + ReftableWriter writer = new ReftableWriter() + .setMinUpdateIndex(1) + .setMaxUpdateIndex(1) + .begin(buffer); + writer.writeLog(MASTER, 1, who, ObjectId.zeroId(), id(1), msg); + writer.writeLog(NEXT, 1, who, ObjectId.zeroId(), id(2), msg); + writer.finish(); + byte[] table = buffer.toByteArray(); + stats = writer.getStats(); + assertEquals(170, table.length); + assertEquals(0, stats.refCount()); + assertEquals(0, stats.refBytes()); + assertEquals(0, stats.refIndexLevels()); + + ReftableReader t = read(table); + try (RefCursor rc = t.allRefs()) { + assertFalse(rc.next()); + } + try (RefCursor rc = t.seekRef("refs/heads/")) { + assertFalse(rc.next()); + } + try (LogCursor lc = t.allLogs()) { + assertTrue(lc.next()); + assertEquals(MASTER, lc.getRefName()); + assertEquals(1, lc.getUpdateIndex()); + assertEquals(ObjectId.zeroId(), lc.getReflogEntry().getOldId()); + assertEquals(id(1), lc.getReflogEntry().getNewId()); + assertEquals(who, lc.getReflogEntry().getWho()); + assertEquals(msg, lc.getReflogEntry().getComment()); + + assertTrue(lc.next()); + assertEquals(NEXT, lc.getRefName()); + assertEquals(1, lc.getUpdateIndex()); + assertEquals(ObjectId.zeroId(), lc.getReflogEntry().getOldId()); + assertEquals(id(2), lc.getReflogEntry().getNewId()); + assertEquals(who, lc.getReflogEntry().getWho()); + assertEquals(msg, lc.getReflogEntry().getComment()); + + assertFalse(lc.next()); + } + } + + @SuppressWarnings("boxing") + @Test + public void logScan() throws IOException { + ReftableConfig cfg = new ReftableConfig(); + cfg.setRefBlockSize(256); + cfg.setLogBlockSize(2048); + + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + ReftableWriter writer = new ReftableWriter(cfg); + writer.setMinUpdateIndex(1).setMaxUpdateIndex(1).begin(buffer); + + List<Ref> refs = new ArrayList<>(); + for (int i = 1; i <= 5670; i++) { + Ref ref = ref(String.format("refs/heads/%03d", i), i); + refs.add(ref); + writer.writeRef(ref); + } + + PersonIdent who = new PersonIdent("Log", "Ger", 1500079709, -8 * 60); + for (Ref ref : refs) { + writer.writeLog(ref.getName(), 1, who, + ObjectId.zeroId(), ref.getObjectId(), + "create " + ref.getName()); + } + writer.finish(); + stats = writer.getStats(); + assertTrue(stats.logBytes() > 4096); + byte[] table = buffer.toByteArray(); + + ReftableReader t = read(table); + try (LogCursor lc = t.allLogs()) { + for (Ref exp : refs) { + assertTrue("has " + exp.getName(), lc.next()); + assertEquals(exp.getName(), lc.getRefName()); + ReflogEntry entry = lc.getReflogEntry(); + assertNotNull(entry); + assertEquals(who, entry.getWho()); + assertEquals(ObjectId.zeroId(), entry.getOldId()); + assertEquals(exp.getObjectId(), entry.getNewId()); + assertEquals("create " + exp.getName(), entry.getComment()); + } + assertFalse(lc.next()); + } + } + + @SuppressWarnings("boxing") + @Test + public void byObjectIdOneRefNoIndex() throws IOException { + List<Ref> refs = new ArrayList<>(); + for (int i = 1; i <= 200; i++) { + refs.add(ref(String.format("refs/heads/%02d", i), i)); + } + refs.add(ref("refs/heads/master", 100)); + + ReftableReader t = read(write(refs)); + assertEquals(0, stats.objIndexSize()); + + try (RefCursor rc = t.byObjectId(id(42))) { + assertTrue("has 42", rc.next()); + assertEquals("refs/heads/42", rc.getRef().getName()); + assertEquals(id(42), rc.getRef().getObjectId()); + assertFalse(rc.next()); + } + try (RefCursor rc = t.byObjectId(id(100))) { + assertTrue("has 100", rc.next()); + assertEquals("refs/heads/100", rc.getRef().getName()); + assertEquals(id(100), rc.getRef().getObjectId()); + + assertTrue("has master", rc.next()); + assertEquals("refs/heads/master", rc.getRef().getName()); + assertEquals(id(100), rc.getRef().getObjectId()); + + assertFalse(rc.next()); + } + } + + @SuppressWarnings("boxing") + @Test + public void byObjectIdOneRefWithIndex() throws IOException { + List<Ref> refs = new ArrayList<>(); + for (int i = 1; i <= 5200; i++) { + refs.add(ref(String.format("refs/heads/%02d", i), i)); + } + refs.add(ref("refs/heads/master", 100)); + + ReftableReader t = read(write(refs)); + assertTrue(stats.objIndexSize() > 0); + + try (RefCursor rc = t.byObjectId(id(42))) { + assertTrue("has 42", rc.next()); + assertEquals("refs/heads/42", rc.getRef().getName()); + assertEquals(id(42), rc.getRef().getObjectId()); + assertFalse(rc.next()); + } + try (RefCursor rc = t.byObjectId(id(100))) { + assertTrue("has 100", rc.next()); + assertEquals("refs/heads/100", rc.getRef().getName()); + assertEquals(id(100), rc.getRef().getObjectId()); + + assertTrue("has master", rc.next()); + assertEquals("refs/heads/master", rc.getRef().getName()); + assertEquals(id(100), rc.getRef().getObjectId()); + + assertFalse(rc.next()); + } + } + + @Test + public void unpeeledDoesNotWrite() { + try { + write(new ObjectIdRef.Unpeeled(PACKED, MASTER, id(1))); + fail("expected IOException"); + } catch (IOException e) { + assertEquals(JGitText.get().peeledRefIsRequired, e.getMessage()); + } + } + + @Test + public void nameTooLongDoesNotWrite() throws IOException { + try { + ReftableConfig cfg = new ReftableConfig(); + cfg.setRefBlockSize(64); + + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + ReftableWriter writer = new ReftableWriter(cfg).begin(buffer); + writer.writeRef(ref("refs/heads/i-am-not-a-teapot", 1)); + writer.finish(); + fail("expected BlockSizeTooSmallException"); + } catch (BlockSizeTooSmallException e) { + assertEquals(85, e.getMinimumBlockSize()); + } + } + + @Test + public void badCrc32() throws IOException { + byte[] table = write(); + table[table.length - 1] = 0x42; + + try { + read(table).seekRef(HEAD); + fail("expected IOException"); + } catch (IOException e) { + assertEquals(JGitText.get().invalidReftableCRC, e.getMessage()); + } + } + + + private static void assertScan(List<Ref> refs, Reftable t) + throws IOException { + try (RefCursor rc = t.allRefs()) { + for (Ref exp : refs) { + assertTrue("has " + exp.getName(), rc.next()); + Ref act = rc.getRef(); + assertEquals(exp.getName(), act.getName()); + assertEquals(exp.getObjectId(), act.getObjectId()); + } + assertFalse(rc.next()); + } + } + + private static void assertSeek(List<Ref> refs, Reftable t) + throws IOException { + for (Ref exp : refs) { + try (RefCursor rc = t.seekRef(exp.getName())) { + assertTrue("has " + exp.getName(), rc.next()); + Ref act = rc.getRef(); + assertEquals(exp.getName(), act.getName()); + assertEquals(exp.getObjectId(), act.getObjectId()); + assertFalse(rc.next()); + } + } + } + + private static Ref ref(String name, int id) { + return new ObjectIdRef.PeeledNonTag(PACKED, name, id(id)); + } + + private static Ref tag(String name, int id1, int id2) { + return new ObjectIdRef.PeeledTag(PACKED, name, id(id1), id(id2)); + } + + private static Ref sym(String name, String target) { + return new SymbolicRef(name, newRef(target)); + } + + private static Ref newRef(String name) { + return new ObjectIdRef.Unpeeled(NEW, name, null); + } + + private static ObjectId id(int i) { + byte[] buf = new byte[OBJECT_ID_LENGTH]; + buf[0] = (byte) (i & 0xff); + buf[1] = (byte) ((i >>> 8) & 0xff); + buf[2] = (byte) ((i >>> 16) & 0xff); + buf[3] = (byte) (i >>> 24); + return ObjectId.fromRaw(buf); + } + + private static ReftableReader read(byte[] table) { + return new ReftableReader(BlockSource.from(table)); + } + + private byte[] write(Ref... refs) throws IOException { + return write(Arrays.asList(refs)); + } + + private byte[] write(Collection<Ref> refs) throws IOException { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + stats = new ReftableWriter() + .begin(buffer) + .sortAndWriteRefs(refs) + .finish() + .getStats(); + return buffer.toByteArray(); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/LocalDiskRefTreeDatabaseTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/LocalDiskRefTreeDatabaseTest.java index 67a7819900..d5a07e02fa 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/LocalDiskRefTreeDatabaseTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/LocalDiskRefTreeDatabaseTest.java @@ -83,7 +83,7 @@ public class LocalDiskRefTreeDatabaseTest extends LocalDiskRepositoryTestCase { FileRepository init = createWorkRepository(); FileBasedConfig cfg = init.getConfig(); cfg.setInt("core", null, "repositoryformatversion", 1); - cfg.setString("extensions", null, "refsStorage", "reftree"); + cfg.setString("extensions", null, "refStorage", "reftree"); cfg.save(); repo = (FileRepository) new FileRepositoryBuilder() diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/AbbreviatedObjectIdTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/AbbreviatedObjectIdTest.java index 6529d9ed57..30a9626b1f 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/AbbreviatedObjectIdTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/AbbreviatedObjectIdTest.java @@ -86,7 +86,6 @@ public class AbbreviatedObjectIdTest { final ObjectId f = i.toObjectId(); assertNotNull(f); assertEquals(ObjectId.fromString(s), f); - assertEquals(f.hashCode(), i.hashCode()); } @Test @@ -101,7 +100,6 @@ public class AbbreviatedObjectIdTest { final ObjectId f = i.toObjectId(); assertNotNull(f); assertEquals(ObjectId.fromString(s), f); - assertEquals(f.hashCode(), i.hashCode()); } @Test @@ -215,7 +213,7 @@ public class AbbreviatedObjectIdTest { } @Test - public void testEquals_Short() { + public void testEquals_Short8() { final String s = "7b6e8067"; final AbbreviatedObjectId a = AbbreviatedObjectId.fromString(s); final AbbreviatedObjectId b = AbbreviatedObjectId.fromString(s); @@ -226,6 +224,18 @@ public class AbbreviatedObjectIdTest { } @Test + public void testEquals_Short4() { + final String s = "7b6e"; + final AbbreviatedObjectId a = AbbreviatedObjectId.fromString(s); + final AbbreviatedObjectId b = AbbreviatedObjectId.fromString(s); + assertNotSame(a, b); + assertTrue(a.hashCode() != 0); + assertTrue(a.hashCode() == b.hashCode()); + assertEquals(b, a); + assertEquals(a, b); + } + + @Test public void testEquals_Full() { final String s = "7b6e8067ec96acef9a4184b43210d583b6d2f99a"; final AbbreviatedObjectId a = AbbreviatedObjectId.fromString(s); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java index e9505f67d0..a12831a149 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java @@ -80,6 +80,7 @@ import org.eclipse.jgit.storage.file.FileBasedConfig; import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.SystemReader; import org.junit.After; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -766,6 +767,7 @@ public class ConfigTest { } @Test + @Ignore public void testIncludeInvalidName() throws ConfigInvalidException { expectedEx.expect(ConfigInvalidException.class); expectedEx.expectMessage(JGitText.get().invalidLineInConfigFile); @@ -773,6 +775,7 @@ public class ConfigTest { } @Test + @Ignore public void testIncludeNoValue() throws ConfigInvalidException { expectedEx.expect(ConfigInvalidException.class); expectedEx.expectMessage(JGitText.get().invalidLineInConfigFile); @@ -780,6 +783,7 @@ public class ConfigTest { } @Test + @Ignore public void testIncludeEmptyValue() throws ConfigInvalidException { expectedEx.expect(ConfigInvalidException.class); expectedEx.expectMessage(JGitText.get().invalidLineInConfigFile); @@ -816,6 +820,7 @@ public class ConfigTest { } @Test + @Ignore public void testIncludeTooManyRecursions() throws IOException { File config = tmp.newFile("config"); String include = "[include]\npath=" + config.toPath() + "\n"; @@ -832,27 +837,14 @@ public class ConfigTest { } @Test - public void testInclude() throws IOException, ConfigInvalidException { + public void testIncludeIsNoop() throws IOException, ConfigInvalidException { File config = tmp.newFile("config"); - File more = tmp.newFile("config.more"); - File other = tmp.newFile("config.other"); String fooBar = "[foo]\nbar=true\n"; - String includeMore = "[include]\npath=" + more.toPath() + "\n"; - String includeOther = "path=" + other.toPath() + "\n"; - String fooPlus = fooBar + includeMore + includeOther; - Files.write(config.toPath(), fooPlus.getBytes()); - - String fooMore = "[foo]\nmore=bar\n"; - Files.write(more.toPath(), fooMore.getBytes()); - - String otherMore = "[other]\nmore=bar\n"; - Files.write(other.toPath(), otherMore.getBytes()); + Files.write(config.toPath(), fooBar.getBytes()); Config parsed = parse("[include]\npath=" + config.toPath() + "\n"); - assertTrue(parsed.getBoolean("foo", "bar", false)); - assertEquals("bar", parsed.getString("foo", null, "more")); - assertEquals("bar", parsed.getString("other", null, "more")); + assertFalse(parsed.getBoolean("foo", "bar", false)); } private static void assertReadLong(long exp) throws ConfigInvalidException { diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutTest.java index f8c2d4536d..05573b9468 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutTest.java @@ -72,6 +72,8 @@ import org.eclipse.jgit.dircache.DirCacheEntry; import org.eclipse.jgit.errors.CheckoutConflictException; import org.eclipse.jgit.errors.CorruptObjectException; import org.eclipse.jgit.errors.NoWorkTreeException; +import org.eclipse.jgit.events.ChangeRecorder; +import org.eclipse.jgit.events.ListenerHandle; import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.junit.TestRepository.BranchBuilder; @@ -141,14 +143,19 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { @Test public void testResetHard() throws IOException, NoFilepatternException, GitAPIException { + ChangeRecorder recorder = new ChangeRecorder(); + ListenerHandle handle = null; try (Git git = new Git(db)) { + handle = db.getListenerList() + .addWorkingTreeModifiedListener(recorder); writeTrashFile("f", "f()"); writeTrashFile("D/g", "g()"); git.add().addFilepattern(".").call(); git.commit().setMessage("inital").call(); assertIndex(mkmap("f", "f()", "D/g", "g()")); - + recorder.assertNoEvent(); git.branchCreate().setName("topic").call(); + recorder.assertNoEvent(); writeTrashFile("f", "f()\nmaster"); writeTrashFile("D/g", "g()\ng2()"); @@ -156,9 +163,12 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { git.add().addFilepattern(".").call(); RevCommit master = git.commit().setMessage("master-1").call(); assertIndex(mkmap("f", "f()\nmaster", "D/g", "g()\ng2()", "E/h", "h()")); + recorder.assertNoEvent(); checkoutBranch("refs/heads/topic"); assertIndex(mkmap("f", "f()", "D/g", "g()")); + recorder.assertEvent(new String[] { "f", "D/g" }, + new String[] { "E/h" }); writeTrashFile("f", "f()\nside"); assertTrue(new File(db.getWorkTree(), "D/g").delete()); @@ -167,26 +177,41 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { git.add().addFilepattern(".").setUpdate(true).call(); RevCommit topic = git.commit().setMessage("topic-1").call(); assertIndex(mkmap("f", "f()\nside", "G/i", "i()")); + recorder.assertNoEvent(); writeTrashFile("untracked", "untracked"); resetHard(master); assertIndex(mkmap("f", "f()\nmaster", "D/g", "g()\ng2()", "E/h", "h()")); + recorder.assertEvent(new String[] { "f", "D/g", "E/h" }, + new String[] { "G", "G/i" }); + resetHard(topic); assertIndex(mkmap("f", "f()\nside", "G/i", "i()")); assertWorkDir(mkmap("f", "f()\nside", "G/i", "i()", "untracked", "untracked")); + recorder.assertEvent(new String[] { "f", "G/i" }, + new String[] { "D", "D/g", "E", "E/h" }); assertEquals(MergeStatus.CONFLICTING, git.merge().include(master) .call().getMergeStatus()); assertEquals( "[D/g, mode:100644, stage:1][D/g, mode:100644, stage:3][E/h, mode:100644][G/i, mode:100644][f, mode:100644, stage:1][f, mode:100644, stage:2][f, mode:100644, stage:3]", indexState(0)); + recorder.assertEvent(new String[] { "f", "D/g", "E/h" }, + ChangeRecorder.EMPTY); resetHard(master); assertIndex(mkmap("f", "f()\nmaster", "D/g", "g()\ng2()", "E/h", "h()")); assertWorkDir(mkmap("f", "f()\nmaster", "D/g", "g()\ng2()", "E/h", "h()", "untracked", "untracked")); + recorder.assertEvent(new String[] { "f", "D/g" }, + new String[] { "G", "G/i" }); + + } finally { + if (handle != null) { + handle.remove(); + } } } @@ -202,13 +227,18 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { @Test public void testResetHardFromIndexEntryWithoutFileToTreeWithoutFile() throws Exception { + ChangeRecorder recorder = new ChangeRecorder(); + ListenerHandle handle = null; try (Git git = new Git(db)) { + handle = db.getListenerList() + .addWorkingTreeModifiedListener(recorder); writeTrashFile("x", "x"); git.add().addFilepattern("x").call(); RevCommit id1 = git.commit().setMessage("c1").call(); writeTrashFile("f/g", "f/g"); git.rm().addFilepattern("x").call(); + recorder.assertEvent(ChangeRecorder.EMPTY, new String[] { "x" }); git.add().addFilepattern("f/g").call(); git.commit().setMessage("c2").call(); deleteTrashFile("f/g"); @@ -217,6 +247,11 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { // The actual test git.reset().setMode(ResetType.HARD).setRef(id1.getName()).call(); assertIndex(mkmap("x", "x")); + recorder.assertEvent(new String[] { "x" }, ChangeRecorder.EMPTY); + } finally { + if (handle != null) { + handle.remove(); + } } } @@ -227,13 +262,22 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { */ @Test public void testInitialCheckout() throws Exception { + ChangeRecorder recorder = new ChangeRecorder(); + ListenerHandle handle = null; try (Git git = new Git(db)) { + handle = db.getListenerList() + .addWorkingTreeModifiedListener(recorder); TestRepository<Repository> db_t = new TestRepository<>(db); BranchBuilder master = db_t.branch("master"); master.commit().add("f", "1").message("m0").create(); assertFalse(new File(db.getWorkTree(), "f").exists()); git.checkout().setName("master").call(); assertTrue(new File(db.getWorkTree(), "f").exists()); + recorder.assertEvent(new String[] { "f" }, ChangeRecorder.EMPTY); + } finally { + if (handle != null) { + handle.remove(); + } } } @@ -930,120 +974,154 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { public void testCheckoutChangeLinkToEmptyDir() throws Exception { Assume.assumeTrue(FS.DETECTED.supportsSymlinks()); String fname = "was_file"; - Git git = Git.wrap(db); - - // Add a file - writeTrashFile(fname, "a"); - git.add().addFilepattern(fname).call(); - - // Add a link to file - String linkName = "link"; - File link = writeLink(linkName, fname).toFile(); - git.add().addFilepattern(linkName).call(); - git.commit().setMessage("Added file and link").call(); - - assertWorkDir(mkmap(linkName, "a", fname, "a")); - - // replace link with empty directory - FileUtils.delete(link); - FileUtils.mkdir(link); - assertTrue("Link must be a directory now", link.isDirectory()); - - // modify file - writeTrashFile(fname, "b"); - assertWorkDir(mkmap(fname, "b", linkName, "/")); - - // revert both paths to HEAD state - git.checkout().setStartPoint(Constants.HEAD) - .addPath(fname).addPath(linkName).call(); - - assertWorkDir(mkmap(fname, "a", linkName, "a")); - - Status st = git.status().call(); - assertTrue(st.isClean()); + ChangeRecorder recorder = new ChangeRecorder(); + ListenerHandle handle = null; + try (Git git = new Git(db)) { + handle = db.getListenerList() + .addWorkingTreeModifiedListener(recorder); + // Add a file + writeTrashFile(fname, "a"); + git.add().addFilepattern(fname).call(); + + // Add a link to file + String linkName = "link"; + File link = writeLink(linkName, fname).toFile(); + git.add().addFilepattern(linkName).call(); + git.commit().setMessage("Added file and link").call(); + + assertWorkDir(mkmap(linkName, "a", fname, "a")); + + // replace link with empty directory + FileUtils.delete(link); + FileUtils.mkdir(link); + assertTrue("Link must be a directory now", link.isDirectory()); + + // modify file + writeTrashFile(fname, "b"); + assertWorkDir(mkmap(fname, "b", linkName, "/")); + recorder.assertNoEvent(); + + // revert both paths to HEAD state + git.checkout().setStartPoint(Constants.HEAD).addPath(fname) + .addPath(linkName).call(); + + assertWorkDir(mkmap(fname, "a", linkName, "a")); + recorder.assertEvent(new String[] { fname, linkName }, + ChangeRecorder.EMPTY); + + Status st = git.status().call(); + assertTrue(st.isClean()); + } finally { + if (handle != null) { + handle.remove(); + } + } } @Test public void testCheckoutChangeLinkToEmptyDirs() throws Exception { Assume.assumeTrue(FS.DETECTED.supportsSymlinks()); String fname = "was_file"; - Git git = Git.wrap(db); - - // Add a file - writeTrashFile(fname, "a"); - git.add().addFilepattern(fname).call(); - - // Add a link to file - String linkName = "link"; - File link = writeLink(linkName, fname).toFile(); - git.add().addFilepattern(linkName).call(); - git.commit().setMessage("Added file and link").call(); - - assertWorkDir(mkmap(linkName, "a", fname, "a")); - - // replace link with directory containing only directories, no files - FileUtils.delete(link); - FileUtils.mkdirs(new File(link, "dummyDir")); - assertTrue("Link must be a directory now", link.isDirectory()); - - assertFalse("Must not delete non empty directory", link.delete()); - - // modify file - writeTrashFile(fname, "b"); - assertWorkDir(mkmap(fname, "b", linkName + "/dummyDir", "/")); - - // revert both paths to HEAD state - git.checkout().setStartPoint(Constants.HEAD) - .addPath(fname).addPath(linkName).call(); - - assertWorkDir(mkmap(fname, "a", linkName, "a")); - - Status st = git.status().call(); - assertTrue(st.isClean()); + ChangeRecorder recorder = new ChangeRecorder(); + ListenerHandle handle = null; + try (Git git = new Git(db)) { + handle = db.getListenerList() + .addWorkingTreeModifiedListener(recorder); + // Add a file + writeTrashFile(fname, "a"); + git.add().addFilepattern(fname).call(); + + // Add a link to file + String linkName = "link"; + File link = writeLink(linkName, fname).toFile(); + git.add().addFilepattern(linkName).call(); + git.commit().setMessage("Added file and link").call(); + + assertWorkDir(mkmap(linkName, "a", fname, "a")); + + // replace link with directory containing only directories, no files + FileUtils.delete(link); + FileUtils.mkdirs(new File(link, "dummyDir")); + assertTrue("Link must be a directory now", link.isDirectory()); + + assertFalse("Must not delete non empty directory", link.delete()); + + // modify file + writeTrashFile(fname, "b"); + assertWorkDir(mkmap(fname, "b", linkName + "/dummyDir", "/")); + recorder.assertNoEvent(); + + // revert both paths to HEAD state + git.checkout().setStartPoint(Constants.HEAD).addPath(fname) + .addPath(linkName).call(); + + assertWorkDir(mkmap(fname, "a", linkName, "a")); + recorder.assertEvent(new String[] { fname, linkName }, + ChangeRecorder.EMPTY); + + Status st = git.status().call(); + assertTrue(st.isClean()); + } finally { + if (handle != null) { + handle.remove(); + } + } } @Test public void testCheckoutChangeLinkToNonEmptyDirs() throws Exception { Assume.assumeTrue(FS.DETECTED.supportsSymlinks()); String fname = "file"; - Git git = Git.wrap(db); - - // Add a file - writeTrashFile(fname, "a"); - git.add().addFilepattern(fname).call(); + ChangeRecorder recorder = new ChangeRecorder(); + ListenerHandle handle = null; + try (Git git = new Git(db)) { + handle = db.getListenerList() + .addWorkingTreeModifiedListener(recorder); + // Add a file + writeTrashFile(fname, "a"); + git.add().addFilepattern(fname).call(); - // Add a link to file - String linkName = "link"; - File link = writeLink(linkName, fname).toFile(); - git.add().addFilepattern(linkName).call(); - git.commit().setMessage("Added file and link").call(); + // Add a link to file + String linkName = "link"; + File link = writeLink(linkName, fname).toFile(); + git.add().addFilepattern(linkName).call(); + git.commit().setMessage("Added file and link").call(); - assertWorkDir(mkmap(linkName, "a", fname, "a")); + assertWorkDir(mkmap(linkName, "a", fname, "a")); - // replace link with directory containing only directories, no files - FileUtils.delete(link); + // replace link with directory containing only directories, no files + FileUtils.delete(link); - // create but do not add a file in the new directory to the index - writeTrashFile(linkName + "/dir1", "file1", "c"); + // create but do not add a file in the new directory to the index + writeTrashFile(linkName + "/dir1", "file1", "c"); - // create but do not add a file in the new directory to the index - writeTrashFile(linkName + "/dir2", "file2", "d"); + // create but do not add a file in the new directory to the index + writeTrashFile(linkName + "/dir2", "file2", "d"); - assertTrue("File must be a directory now", link.isDirectory()); - assertFalse("Must not delete non empty directory", link.delete()); + assertTrue("File must be a directory now", link.isDirectory()); + assertFalse("Must not delete non empty directory", link.delete()); - // 2 extra files are created - assertWorkDir(mkmap(fname, "a", linkName + "/dir1/file1", "c", - linkName + "/dir2/file2", "d")); + // 2 extra files are created + assertWorkDir(mkmap(fname, "a", linkName + "/dir1/file1", "c", + linkName + "/dir2/file2", "d")); + recorder.assertNoEvent(); - // revert path to HEAD state - git.checkout().setStartPoint(Constants.HEAD).addPath(linkName).call(); + // revert path to HEAD state + git.checkout().setStartPoint(Constants.HEAD).addPath(linkName) + .call(); - // expect only the one added to the index - assertWorkDir(mkmap(linkName, "a", fname, "a")); + // expect only the one added to the index + assertWorkDir(mkmap(linkName, "a", fname, "a")); + recorder.assertEvent(new String[] { linkName }, + ChangeRecorder.EMPTY); - Status st = git.status().call(); - assertTrue(st.isClean()); + Status st = git.status().call(); + assertTrue(st.isClean()); + } finally { + if (handle != null) { + handle.remove(); + } + } } @Test @@ -1051,174 +1129,222 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { throws Exception { Assume.assumeTrue(FS.DETECTED.supportsSymlinks()); String fname = "file"; - Git git = Git.wrap(db); - - // Add a file - writeTrashFile(fname, "a"); - git.add().addFilepattern(fname).call(); + ChangeRecorder recorder = new ChangeRecorder(); + ListenerHandle handle = null; + try (Git git = new Git(db)) { + handle = db.getListenerList() + .addWorkingTreeModifiedListener(recorder); + // Add a file + writeTrashFile(fname, "a"); + git.add().addFilepattern(fname).call(); - // Add a link to file - String linkName = "link"; - File link = writeLink(linkName, fname).toFile(); - git.add().addFilepattern(linkName).call(); - git.commit().setMessage("Added file and link").call(); + // Add a link to file + String linkName = "link"; + File link = writeLink(linkName, fname).toFile(); + git.add().addFilepattern(linkName).call(); + git.commit().setMessage("Added file and link").call(); - assertWorkDir(mkmap(linkName, "a", fname, "a")); + assertWorkDir(mkmap(linkName, "a", fname, "a")); - // replace link with directory containing only directories, no files - FileUtils.delete(link); + // replace link with directory containing only directories, no files + FileUtils.delete(link); - // create and add a file in the new directory to the index - writeTrashFile(linkName + "/dir1", "file1", "c"); - git.add().addFilepattern(linkName + "/dir1/file1").call(); + // create and add a file in the new directory to the index + writeTrashFile(linkName + "/dir1", "file1", "c"); + git.add().addFilepattern(linkName + "/dir1/file1").call(); - // create but do not add a file in the new directory to the index - writeTrashFile(linkName + "/dir2", "file2", "d"); + // create but do not add a file in the new directory to the index + writeTrashFile(linkName + "/dir2", "file2", "d"); - assertTrue("File must be a directory now", link.isDirectory()); - assertFalse("Must not delete non empty directory", link.delete()); + assertTrue("File must be a directory now", link.isDirectory()); + assertFalse("Must not delete non empty directory", link.delete()); - // 2 extra files are created - assertWorkDir(mkmap(fname, "a", linkName + "/dir1/file1", "c", - linkName + "/dir2/file2", "d")); + // 2 extra files are created + assertWorkDir(mkmap(fname, "a", linkName + "/dir1/file1", "c", + linkName + "/dir2/file2", "d")); + recorder.assertNoEvent(); - // revert path to HEAD state - git.checkout().setStartPoint(Constants.HEAD).addPath(linkName).call(); + // revert path to HEAD state + git.checkout().setStartPoint(Constants.HEAD).addPath(linkName) + .call(); - // original file and link - assertWorkDir(mkmap(linkName, "a", fname, "a")); + // original file and link + assertWorkDir(mkmap(linkName, "a", fname, "a")); + recorder.assertEvent(new String[] { linkName }, + ChangeRecorder.EMPTY); - Status st = git.status().call(); - assertTrue(st.isClean()); + Status st = git.status().call(); + assertTrue(st.isClean()); + } finally { + if (handle != null) { + handle.remove(); + } + } } @Test public void testCheckoutChangeFileToEmptyDir() throws Exception { String fname = "was_file"; - Git git = Git.wrap(db); - - // Add a file - File file = writeTrashFile(fname, "a"); - git.add().addFilepattern(fname).call(); - git.commit().setMessage("Added file").call(); - - // replace file with empty directory - FileUtils.delete(file); - FileUtils.mkdir(file); - assertTrue("File must be a directory now", file.isDirectory()); - - assertWorkDir(mkmap(fname, "/")); - - // revert path to HEAD state - git.checkout().setStartPoint(Constants.HEAD).addPath(fname).call(); - - assertWorkDir(mkmap(fname, "a")); - - Status st = git.status().call(); - assertTrue(st.isClean()); + ChangeRecorder recorder = new ChangeRecorder(); + ListenerHandle handle = null; + try (Git git = new Git(db)) { + handle = db.getListenerList() + .addWorkingTreeModifiedListener(recorder); + // Add a file + File file = writeTrashFile(fname, "a"); + git.add().addFilepattern(fname).call(); + git.commit().setMessage("Added file").call(); + + // replace file with empty directory + FileUtils.delete(file); + FileUtils.mkdir(file); + assertTrue("File must be a directory now", file.isDirectory()); + assertWorkDir(mkmap(fname, "/")); + recorder.assertNoEvent(); + + // revert path to HEAD state + git.checkout().setStartPoint(Constants.HEAD).addPath(fname).call(); + assertWorkDir(mkmap(fname, "a")); + recorder.assertEvent(new String[] { fname }, ChangeRecorder.EMPTY); + + Status st = git.status().call(); + assertTrue(st.isClean()); + } finally { + if (handle != null) { + handle.remove(); + } + } } @Test public void testCheckoutChangeFileToEmptyDirs() throws Exception { String fname = "was_file"; - Git git = Git.wrap(db); - - // Add a file - File file = writeTrashFile(fname, "a"); - git.add().addFilepattern(fname).call(); - git.commit().setMessage("Added file").call(); - - // replace file with directory containing only directories, no files - FileUtils.delete(file); - FileUtils.mkdirs(new File(file, "dummyDir")); - assertTrue("File must be a directory now", file.isDirectory()); - assertFalse("Must not delete non empty directory", file.delete()); - - assertWorkDir(mkmap(fname + "/dummyDir", "/")); - - // revert path to HEAD state - git.checkout().setStartPoint(Constants.HEAD).addPath(fname).call(); - - assertWorkDir(mkmap(fname, "a")); - - Status st = git.status().call(); - assertTrue(st.isClean()); + ChangeRecorder recorder = new ChangeRecorder(); + ListenerHandle handle = null; + try (Git git = new Git(db)) { + handle = db.getListenerList() + .addWorkingTreeModifiedListener(recorder); + // Add a file + File file = writeTrashFile(fname, "a"); + git.add().addFilepattern(fname).call(); + git.commit().setMessage("Added file").call(); + + // replace file with directory containing only directories, no files + FileUtils.delete(file); + FileUtils.mkdirs(new File(file, "dummyDir")); + assertTrue("File must be a directory now", file.isDirectory()); + assertFalse("Must not delete non empty directory", file.delete()); + + assertWorkDir(mkmap(fname + "/dummyDir", "/")); + recorder.assertNoEvent(); + + // revert path to HEAD state + git.checkout().setStartPoint(Constants.HEAD).addPath(fname).call(); + assertWorkDir(mkmap(fname, "a")); + recorder.assertEvent(new String[] { fname }, ChangeRecorder.EMPTY); + + Status st = git.status().call(); + assertTrue(st.isClean()); + } finally { + if (handle != null) { + handle.remove(); + } + } } @Test public void testCheckoutChangeFileToNonEmptyDirs() throws Exception { String fname = "was_file"; - Git git = Git.wrap(db); - - // Add a file - File file = writeTrashFile(fname, "a"); - git.add().addFilepattern(fname).call(); - git.commit().setMessage("Added file").call(); + ChangeRecorder recorder = new ChangeRecorder(); + ListenerHandle handle = null; + try (Git git = new Git(db)) { + handle = db.getListenerList() + .addWorkingTreeModifiedListener(recorder); + // Add a file + File file = writeTrashFile(fname, "a"); + git.add().addFilepattern(fname).call(); + git.commit().setMessage("Added file").call(); - assertWorkDir(mkmap(fname, "a")); + assertWorkDir(mkmap(fname, "a")); - // replace file with directory containing only directories, no files - FileUtils.delete(file); + // replace file with directory containing only directories, no files + FileUtils.delete(file); - // create but do not add a file in the new directory to the index - writeTrashFile(fname + "/dir1", "file1", "c"); + // create but do not add a file in the new directory to the index + writeTrashFile(fname + "/dir1", "file1", "c"); - // create but do not add a file in the new directory to the index - writeTrashFile(fname + "/dir2", "file2", "d"); + // create but do not add a file in the new directory to the index + writeTrashFile(fname + "/dir2", "file2", "d"); - assertTrue("File must be a directory now", file.isDirectory()); - assertFalse("Must not delete non empty directory", file.delete()); + assertTrue("File must be a directory now", file.isDirectory()); + assertFalse("Must not delete non empty directory", file.delete()); - // 2 extra files are created - assertWorkDir( - mkmap(fname + "/dir1/file1", "c", fname + "/dir2/file2", "d")); + // 2 extra files are created + assertWorkDir(mkmap(fname + "/dir1/file1", "c", + fname + "/dir2/file2", "d")); + recorder.assertNoEvent(); - // revert path to HEAD state - git.checkout().setStartPoint(Constants.HEAD).addPath(fname).call(); + // revert path to HEAD state + git.checkout().setStartPoint(Constants.HEAD).addPath(fname).call(); - // expect only the one added to the index - assertWorkDir(mkmap(fname, "a")); + // expect only the one added to the index + assertWorkDir(mkmap(fname, "a")); + recorder.assertEvent(new String[] { fname }, ChangeRecorder.EMPTY); - Status st = git.status().call(); - assertTrue(st.isClean()); + Status st = git.status().call(); + assertTrue(st.isClean()); + } finally { + if (handle != null) { + handle.remove(); + } + } } @Test public void testCheckoutChangeFileToNonEmptyDirsAndNewIndexEntry() throws Exception { String fname = "was_file"; - Git git = Git.wrap(db); - - // Add a file - File file = writeTrashFile(fname, "a"); - git.add().addFilepattern(fname).call(); - git.commit().setMessage("Added file").call(); - - assertWorkDir(mkmap(fname, "a")); - - // replace file with directory containing only directories, no files - FileUtils.delete(file); - - // create and add a file in the new directory to the index - writeTrashFile(fname + "/dir", "file1", "c"); - git.add().addFilepattern(fname + "/dir/file1").call(); - - // create but do not add a file in the new directory to the index - writeTrashFile(fname + "/dir", "file2", "d"); - - assertTrue("File must be a directory now", file.isDirectory()); - assertFalse("Must not delete non empty directory", file.delete()); - - // 2 extra files are created - assertWorkDir( - mkmap(fname + "/dir/file1", "c", fname + "/dir/file2", "d")); - - // revert path to HEAD state - git.checkout().setStartPoint(Constants.HEAD).addPath(fname).call(); - assertWorkDir(mkmap(fname, "a")); - - Status st = git.status().call(); - assertTrue(st.isClean()); + ChangeRecorder recorder = new ChangeRecorder(); + ListenerHandle handle = null; + try (Git git = new Git(db)) { + handle = db.getListenerList() + .addWorkingTreeModifiedListener(recorder); + // Add a file + File file = writeTrashFile(fname, "a"); + git.add().addFilepattern(fname).call(); + git.commit().setMessage("Added file").call(); + + assertWorkDir(mkmap(fname, "a")); + + // replace file with directory containing only directories, no files + FileUtils.delete(file); + + // create and add a file in the new directory to the index + writeTrashFile(fname + "/dir", "file1", "c"); + git.add().addFilepattern(fname + "/dir/file1").call(); + + // create but do not add a file in the new directory to the index + writeTrashFile(fname + "/dir", "file2", "d"); + + assertTrue("File must be a directory now", file.isDirectory()); + assertFalse("Must not delete non empty directory", file.delete()); + + // 2 extra files are created + assertWorkDir(mkmap(fname + "/dir/file1", "c", fname + "/dir/file2", + "d")); + recorder.assertNoEvent(); + + // revert path to HEAD state + git.checkout().setStartPoint(Constants.HEAD).addPath(fname).call(); + assertWorkDir(mkmap(fname, "a")); + recorder.assertEvent(new String[] { fname }, ChangeRecorder.EMPTY); + Status st = git.status().call(); + assertTrue(st.isClean()); + } finally { + if (handle != null) { + handle.remove(); + } + } } @Test @@ -1293,76 +1419,100 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { public void testOverwriteUntrackedIgnoredFile() throws IOException, GitAPIException { String fname="file.txt"; - Git git = Git.wrap(db); - - // Add a file - writeTrashFile(fname, "a"); - git.add().addFilepattern(fname).call(); - git.commit().setMessage("create file").call(); - - // Create branch - git.branchCreate().setName("side").call(); - - // Modify file - writeTrashFile(fname, "b"); - git.add().addFilepattern(fname).call(); - git.commit().setMessage("modify file").call(); - - // Switch branches - git.checkout().setName("side").call(); - git.rm().addFilepattern(fname).call(); - writeTrashFile(".gitignore", fname); - git.add().addFilepattern(".gitignore").call(); - git.commit().setMessage("delete and ignore file").call(); - - writeTrashFile(fname, "Something different"); - git.checkout().setName("master").call(); - assertWorkDir(mkmap(fname, "b")); - assertTrue(git.status().call().isClean()); + ChangeRecorder recorder = new ChangeRecorder(); + ListenerHandle handle = null; + try (Git git = new Git(db)) { + handle = db.getListenerList() + .addWorkingTreeModifiedListener(recorder); + // Add a file + writeTrashFile(fname, "a"); + git.add().addFilepattern(fname).call(); + git.commit().setMessage("create file").call(); + + // Create branch + git.branchCreate().setName("side").call(); + + // Modify file + writeTrashFile(fname, "b"); + git.add().addFilepattern(fname).call(); + git.commit().setMessage("modify file").call(); + recorder.assertNoEvent(); + + // Switch branches + git.checkout().setName("side").call(); + recorder.assertEvent(new String[] { fname }, ChangeRecorder.EMPTY); + git.rm().addFilepattern(fname).call(); + recorder.assertEvent(ChangeRecorder.EMPTY, new String[] { fname }); + writeTrashFile(".gitignore", fname); + git.add().addFilepattern(".gitignore").call(); + git.commit().setMessage("delete and ignore file").call(); + + writeTrashFile(fname, "Something different"); + recorder.assertNoEvent(); + git.checkout().setName("master").call(); + assertWorkDir(mkmap(fname, "b")); + recorder.assertEvent(new String[] { fname }, + new String[] { ".gitignore" }); + assertTrue(git.status().call().isClean()); + } finally { + if (handle != null) { + handle.remove(); + } + } } @Test public void testOverwriteUntrackedFileModeChange() throws IOException, GitAPIException { String fname = "file.txt"; - Git git = Git.wrap(db); - - // Add a file - File file = writeTrashFile(fname, "a"); - git.add().addFilepattern(fname).call(); - git.commit().setMessage("create file").call(); - assertWorkDir(mkmap(fname, "a")); - - // Create branch - git.branchCreate().setName("side").call(); + ChangeRecorder recorder = new ChangeRecorder(); + ListenerHandle handle = null; + try (Git git = new Git(db)) { + handle = db.getListenerList() + .addWorkingTreeModifiedListener(recorder); + // Add a file + File file = writeTrashFile(fname, "a"); + git.add().addFilepattern(fname).call(); + git.commit().setMessage("create file").call(); + assertWorkDir(mkmap(fname, "a")); - // Switch branches - git.checkout().setName("side").call(); + // Create branch + git.branchCreate().setName("side").call(); - // replace file with directory containing files - FileUtils.delete(file); + // Switch branches + git.checkout().setName("side").call(); + recorder.assertNoEvent(); - // create and add a file in the new directory to the index - writeTrashFile(fname + "/dir1", "file1", "c"); - git.add().addFilepattern(fname + "/dir1/file1").call(); + // replace file with directory containing files + FileUtils.delete(file); - // create but do not add a file in the new directory to the index - writeTrashFile(fname + "/dir2", "file2", "d"); + // create and add a file in the new directory to the index + writeTrashFile(fname + "/dir1", "file1", "c"); + git.add().addFilepattern(fname + "/dir1/file1").call(); - assertTrue("File must be a directory now", file.isDirectory()); - assertFalse("Must not delete non empty directory", file.delete()); + // create but do not add a file in the new directory to the index + writeTrashFile(fname + "/dir2", "file2", "d"); - // 2 extra files are created - assertWorkDir( - mkmap(fname + "/dir1/file1", "c", fname + "/dir2/file2", "d")); + assertTrue("File must be a directory now", file.isDirectory()); + assertFalse("Must not delete non empty directory", file.delete()); - try { - git.checkout().setName("master").call(); - fail("did not throw exception"); - } catch (Exception e) { - // 2 extra files are still there + // 2 extra files are created assertWorkDir(mkmap(fname + "/dir1/file1", "c", fname + "/dir2/file2", "d")); + + try { + git.checkout().setName("master").call(); + fail("did not throw exception"); + } catch (Exception e) { + // 2 extra files are still there + assertWorkDir(mkmap(fname + "/dir1/file1", "c", + fname + "/dir2/file2", "d")); + } + recorder.assertNoEvent(); + } finally { + if (handle != null) { + handle.remove(); + } } } @@ -1371,50 +1521,60 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { throws Exception { Assume.assumeTrue(FS.DETECTED.supportsSymlinks()); String fname = "file.txt"; - Git git = Git.wrap(db); - - // Add a file - writeTrashFile(fname, "a"); - git.add().addFilepattern(fname).call(); - - // Add a link to file - String linkName = "link"; - File link = writeLink(linkName, fname).toFile(); - git.add().addFilepattern(linkName).call(); - git.commit().setMessage("Added file and link").call(); + ChangeRecorder recorder = new ChangeRecorder(); + ListenerHandle handle = null; + try (Git git = new Git(db)) { + handle = db.getListenerList() + .addWorkingTreeModifiedListener(recorder); + // Add a file + writeTrashFile(fname, "a"); + git.add().addFilepattern(fname).call(); - assertWorkDir(mkmap(linkName, "a", fname, "a")); + // Add a link to file + String linkName = "link"; + File link = writeLink(linkName, fname).toFile(); + git.add().addFilepattern(linkName).call(); + git.commit().setMessage("Added file and link").call(); - // Create branch - git.branchCreate().setName("side").call(); + assertWorkDir(mkmap(linkName, "a", fname, "a")); - // Switch branches - git.checkout().setName("side").call(); + // Create branch + git.branchCreate().setName("side").call(); - // replace link with directory containing files - FileUtils.delete(link); + // Switch branches + git.checkout().setName("side").call(); + recorder.assertNoEvent(); - // create and add a file in the new directory to the index - writeTrashFile(linkName + "/dir1", "file1", "c"); - git.add().addFilepattern(linkName + "/dir1/file1").call(); + // replace link with directory containing files + FileUtils.delete(link); - // create but do not add a file in the new directory to the index - writeTrashFile(linkName + "/dir2", "file2", "d"); + // create and add a file in the new directory to the index + writeTrashFile(linkName + "/dir1", "file1", "c"); + git.add().addFilepattern(linkName + "/dir1/file1").call(); - assertTrue("Link must be a directory now", link.isDirectory()); - assertFalse("Must not delete non empty directory", link.delete()); + // create but do not add a file in the new directory to the index + writeTrashFile(linkName + "/dir2", "file2", "d"); - // 2 extra files are created - assertWorkDir(mkmap(fname, "a", linkName + "/dir1/file1", "c", - linkName + "/dir2/file2", "d")); + assertTrue("Link must be a directory now", link.isDirectory()); + assertFalse("Must not delete non empty directory", link.delete()); - try { - git.checkout().setName("master").call(); - fail("did not throw exception"); - } catch (Exception e) { - // 2 extra files are still there + // 2 extra files are created assertWorkDir(mkmap(fname, "a", linkName + "/dir1/file1", "c", linkName + "/dir2/file2", "d")); + + try { + git.checkout().setName("master").call(); + fail("did not throw exception"); + } catch (Exception e) { + // 2 extra files are still there + assertWorkDir(mkmap(fname, "a", linkName + "/dir1/file1", "c", + linkName + "/dir2/file2", "d")); + } + recorder.assertNoEvent(); + } finally { + if (handle != null) { + handle.remove(); + } } } @@ -1423,36 +1583,47 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { if (!FS.DETECTED.supportsExecute()) return; - Git git = Git.wrap(db); - - // Add non-executable file - File file = writeTrashFile("file.txt", "a"); - git.add().addFilepattern("file.txt").call(); - git.commit().setMessage("commit1").call(); - assertFalse(db.getFS().canExecute(file)); - - // Create branch - git.branchCreate().setName("b1").call(); - - // Make file executable - db.getFS().setExecute(file, true); - git.add().addFilepattern("file.txt").call(); - git.commit().setMessage("commit2").call(); - - // Verify executable and working directory is clean - Status status = git.status().call(); - assertTrue(status.getModified().isEmpty()); - assertTrue(status.getChanged().isEmpty()); - assertTrue(db.getFS().canExecute(file)); - - // Switch branches - git.checkout().setName("b1").call(); - - // Verify not executable and working directory is clean - status = git.status().call(); - assertTrue(status.getModified().isEmpty()); - assertTrue(status.getChanged().isEmpty()); - assertFalse(db.getFS().canExecute(file)); + ChangeRecorder recorder = new ChangeRecorder(); + ListenerHandle handle = null; + try (Git git = new Git(db)) { + handle = db.getListenerList() + .addWorkingTreeModifiedListener(recorder); + // Add non-executable file + File file = writeTrashFile("file.txt", "a"); + git.add().addFilepattern("file.txt").call(); + git.commit().setMessage("commit1").call(); + assertFalse(db.getFS().canExecute(file)); + + // Create branch + git.branchCreate().setName("b1").call(); + + // Make file executable + db.getFS().setExecute(file, true); + git.add().addFilepattern("file.txt").call(); + git.commit().setMessage("commit2").call(); + recorder.assertNoEvent(); + + // Verify executable and working directory is clean + Status status = git.status().call(); + assertTrue(status.getModified().isEmpty()); + assertTrue(status.getChanged().isEmpty()); + assertTrue(db.getFS().canExecute(file)); + + // Switch branches + git.checkout().setName("b1").call(); + + // Verify not executable and working directory is clean + status = git.status().call(); + assertTrue(status.getModified().isEmpty()); + assertTrue(status.getChanged().isEmpty()); + assertFalse(db.getFS().canExecute(file)); + recorder.assertEvent(new String[] { "file.txt" }, + ChangeRecorder.EMPTY); + } finally { + if (handle != null) { + handle.remove(); + } + } } @Test @@ -1460,41 +1631,50 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { if (!FS.DETECTED.supportsExecute()) return; - Git git = Git.wrap(db); - - // Add non-executable file - File file = writeTrashFile("file.txt", "a"); - git.add().addFilepattern("file.txt").call(); - git.commit().setMessage("commit1").call(); - assertFalse(db.getFS().canExecute(file)); - - // Create branch - git.branchCreate().setName("b1").call(); - - // Make file executable - db.getFS().setExecute(file, true); - git.add().addFilepattern("file.txt").call(); - git.commit().setMessage("commit2").call(); - - // Verify executable and working directory is clean - Status status = git.status().call(); - assertTrue(status.getModified().isEmpty()); - assertTrue(status.getChanged().isEmpty()); - assertTrue(db.getFS().canExecute(file)); - - writeTrashFile("file.txt", "b"); - - // Switch branches - CheckoutCommand checkout = git.checkout().setName("b1"); - try { - checkout.call(); - fail("Checkout exception not thrown"); - } catch (org.eclipse.jgit.api.errors.CheckoutConflictException e) { - CheckoutResult result = checkout.getResult(); - assertNotNull(result); - assertNotNull(result.getConflictList()); - assertEquals(1, result.getConflictList().size()); - assertTrue(result.getConflictList().contains("file.txt")); + ChangeRecorder recorder = new ChangeRecorder(); + ListenerHandle handle = null; + try (Git git = new Git(db)) { + handle = db.getListenerList() + .addWorkingTreeModifiedListener(recorder); + // Add non-executable file + File file = writeTrashFile("file.txt", "a"); + git.add().addFilepattern("file.txt").call(); + git.commit().setMessage("commit1").call(); + assertFalse(db.getFS().canExecute(file)); + + // Create branch + git.branchCreate().setName("b1").call(); + + // Make file executable + db.getFS().setExecute(file, true); + git.add().addFilepattern("file.txt").call(); + git.commit().setMessage("commit2").call(); + + // Verify executable and working directory is clean + Status status = git.status().call(); + assertTrue(status.getModified().isEmpty()); + assertTrue(status.getChanged().isEmpty()); + assertTrue(db.getFS().canExecute(file)); + + writeTrashFile("file.txt", "b"); + + // Switch branches + CheckoutCommand checkout = git.checkout().setName("b1"); + try { + checkout.call(); + fail("Checkout exception not thrown"); + } catch (org.eclipse.jgit.api.errors.CheckoutConflictException e) { + CheckoutResult result = checkout.getResult(); + assertNotNull(result); + assertNotNull(result.getConflictList()); + assertEquals(1, result.getConflictList().size()); + assertTrue(result.getConflictList().contains("file.txt")); + } + recorder.assertNoEvent(); + } finally { + if (handle != null) { + handle.remove(); + } } } @@ -1504,40 +1684,52 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { if (!FS.DETECTED.supportsExecute()) return; - Git git = Git.wrap(db); - - // Add non-executable file - File file = writeTrashFile("file.txt", "a"); - git.add().addFilepattern("file.txt").call(); - git.commit().setMessage("commit1").call(); - assertFalse(db.getFS().canExecute(file)); - - // Create branch - git.branchCreate().setName("b1").call(); - - // Create second commit and don't touch file - writeTrashFile("file2.txt", ""); - git.add().addFilepattern("file2.txt").call(); - git.commit().setMessage("commit2").call(); - - // stage a mode change - writeTrashFile("file.txt", "a"); - db.getFS().setExecute(file, true); - git.add().addFilepattern("file.txt").call(); - - // dirty the file - writeTrashFile("file.txt", "b"); - - assertEquals( - "[file.txt, mode:100755, content:a][file2.txt, mode:100644, content:]", - indexState(CONTENT)); - assertWorkDir(mkmap("file.txt", "b", "file2.txt", "")); + ChangeRecorder recorder = new ChangeRecorder(); + ListenerHandle handle = null; + try (Git git = new Git(db)) { + handle = db.getListenerList() + .addWorkingTreeModifiedListener(recorder); + // Add non-executable file + File file = writeTrashFile("file.txt", "a"); + git.add().addFilepattern("file.txt").call(); + git.commit().setMessage("commit1").call(); + assertFalse(db.getFS().canExecute(file)); + + // Create branch + git.branchCreate().setName("b1").call(); + + // Create second commit and don't touch file + writeTrashFile("file2.txt", ""); + git.add().addFilepattern("file2.txt").call(); + git.commit().setMessage("commit2").call(); + + // stage a mode change + writeTrashFile("file.txt", "a"); + db.getFS().setExecute(file, true); + git.add().addFilepattern("file.txt").call(); + + // dirty the file + writeTrashFile("file.txt", "b"); - // Switch branches and check that the dirty file survived in worktree - // and index - git.checkout().setName("b1").call(); - assertEquals("[file.txt, mode:100755, content:a]", indexState(CONTENT)); - assertWorkDir(mkmap("file.txt", "b")); + assertEquals( + "[file.txt, mode:100755, content:a][file2.txt, mode:100644, content:]", + indexState(CONTENT)); + assertWorkDir(mkmap("file.txt", "b", "file2.txt", "")); + recorder.assertNoEvent(); + + // Switch branches and check that the dirty file survived in + // worktree and index + git.checkout().setName("b1").call(); + assertEquals("[file.txt, mode:100755, content:a]", + indexState(CONTENT)); + assertWorkDir(mkmap("file.txt", "b")); + recorder.assertEvent(ChangeRecorder.EMPTY, + new String[] { "file2.txt" }); + } finally { + if (handle != null) { + handle.remove(); + } + } } @Test @@ -1546,40 +1738,53 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { if (!FS.DETECTED.supportsExecute()) return; - Git git = Git.wrap(db); - - // Add non-executable file - File file = writeTrashFile("file.txt", "a"); - git.add().addFilepattern("file.txt").call(); - git.commit().setMessage("commit1").call(); - assertFalse(db.getFS().canExecute(file)); - - // Create branch - git.branchCreate().setName("b1").call(); - - // Create second commit with executable file - file = writeTrashFile("file.txt", "b"); - db.getFS().setExecute(file, true); - git.add().addFilepattern("file.txt").call(); - git.commit().setMessage("commit2").call(); - - // stage the same content as in the branch we want to switch to - writeTrashFile("file.txt", "a"); - db.getFS().setExecute(file, false); - git.add().addFilepattern("file.txt").call(); - - // dirty the file - writeTrashFile("file.txt", "c"); - db.getFS().setExecute(file, true); - - assertEquals("[file.txt, mode:100644, content:a]", indexState(CONTENT)); - assertWorkDir(mkmap("file.txt", "c")); - - // Switch branches and check that the dirty file survived in worktree - // and index - git.checkout().setName("b1").call(); - assertEquals("[file.txt, mode:100644, content:a]", indexState(CONTENT)); - assertWorkDir(mkmap("file.txt", "c")); + ChangeRecorder recorder = new ChangeRecorder(); + ListenerHandle handle = null; + try (Git git = new Git(db)) { + handle = db.getListenerList() + .addWorkingTreeModifiedListener(recorder); + // Add non-executable file + File file = writeTrashFile("file.txt", "a"); + git.add().addFilepattern("file.txt").call(); + git.commit().setMessage("commit1").call(); + assertFalse(db.getFS().canExecute(file)); + + // Create branch + git.branchCreate().setName("b1").call(); + + // Create second commit with executable file + file = writeTrashFile("file.txt", "b"); + db.getFS().setExecute(file, true); + git.add().addFilepattern("file.txt").call(); + git.commit().setMessage("commit2").call(); + + // stage the same content as in the branch we want to switch to + writeTrashFile("file.txt", "a"); + db.getFS().setExecute(file, false); + git.add().addFilepattern("file.txt").call(); + + // dirty the file + writeTrashFile("file.txt", "c"); + db.getFS().setExecute(file, true); + + assertEquals("[file.txt, mode:100644, content:a]", + indexState(CONTENT)); + assertWorkDir(mkmap("file.txt", "c")); + recorder.assertNoEvent(); + + // Switch branches and check that the dirty file survived in + // worktree + // and index + git.checkout().setName("b1").call(); + assertEquals("[file.txt, mode:100644, content:a]", + indexState(CONTENT)); + assertWorkDir(mkmap("file.txt", "c")); + recorder.assertNoEvent(); + } finally { + if (handle != null) { + handle.remove(); + } + } } @Test @@ -1587,31 +1792,44 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { if (!FS.DETECTED.supportsExecute()) return; - Git git = Git.wrap(db); - - // Add first file - File file1 = writeTrashFile("file1.txt", "a"); - git.add().addFilepattern("file1.txt").call(); - git.commit().setMessage("commit1").call(); - assertFalse(db.getFS().canExecute(file1)); - - // Add second file - File file2 = writeTrashFile("file2.txt", "b"); - git.add().addFilepattern("file2.txt").call(); - git.commit().setMessage("commit2").call(); - assertFalse(db.getFS().canExecute(file2)); - - // Create branch from first commit - assertNotNull(git.checkout().setCreateBranch(true).setName("b1") - .setStartPoint(Constants.HEAD + "~1").call()); - - // Change content and file mode in working directory and index - file1 = writeTrashFile("file1.txt", "c"); - db.getFS().setExecute(file1, true); - git.add().addFilepattern("file1.txt").call(); - - // Switch back to 'master' - assertNotNull(git.checkout().setName(Constants.MASTER).call()); + ChangeRecorder recorder = new ChangeRecorder(); + ListenerHandle handle = null; + try (Git git = new Git(db)) { + handle = db.getListenerList() + .addWorkingTreeModifiedListener(recorder); + // Add first file + File file1 = writeTrashFile("file1.txt", "a"); + git.add().addFilepattern("file1.txt").call(); + git.commit().setMessage("commit1").call(); + assertFalse(db.getFS().canExecute(file1)); + + // Add second file + File file2 = writeTrashFile("file2.txt", "b"); + git.add().addFilepattern("file2.txt").call(); + git.commit().setMessage("commit2").call(); + assertFalse(db.getFS().canExecute(file2)); + recorder.assertNoEvent(); + + // Create branch from first commit + assertNotNull(git.checkout().setCreateBranch(true).setName("b1") + .setStartPoint(Constants.HEAD + "~1").call()); + recorder.assertEvent(ChangeRecorder.EMPTY, + new String[] { "file2.txt" }); + + // Change content and file mode in working directory and index + file1 = writeTrashFile("file1.txt", "c"); + db.getFS().setExecute(file1, true); + git.add().addFilepattern("file1.txt").call(); + + // Switch back to 'master' + assertNotNull(git.checkout().setName(Constants.MASTER).call()); + recorder.assertEvent(new String[] { "file2.txt" }, + ChangeRecorder.EMPTY); + } finally { + if (handle != null) { + handle.remove(); + } + } } @Test(expected = CheckoutConflictException.class) diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexDiffSubmoduleTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexDiffSubmoduleTest.java index 0111b9411d..d89aabe75f 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexDiffSubmoduleTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexDiffSubmoduleTest.java @@ -43,11 +43,13 @@ package org.eclipse.jgit.lib; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import java.io.File; import java.io.IOException; +import java.util.Set; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; @@ -118,6 +120,31 @@ public class IndexDiffSubmoduleTest extends RepositoryTestCase { assertTrue(indexDiff.diff()); } + private void assertDiff(IndexDiff indexDiff, IgnoreSubmoduleMode mode, + IgnoreSubmoduleMode... expectedEmptyModes) throws IOException { + boolean diffResult = indexDiff.diff(); + Set<String> submodulePaths = indexDiff + .getPathsWithIndexMode(FileMode.GITLINK); + boolean emptyExpected = false; + for (IgnoreSubmoduleMode empty : expectedEmptyModes) { + if (mode.equals(empty)) { + emptyExpected = true; + break; + } + } + if (emptyExpected) { + assertFalse("diff should be false with mode=" + mode, + diffResult); + assertEquals("should have no paths with FileMode.GITLINK", 0, + submodulePaths.size()); + } else { + assertTrue("diff should be true with mode=" + mode, + diffResult); + assertTrue("submodule path should have FileMode.GITLINK", + submodulePaths.contains("modules/submodule")); + } + } + @Theory public void testDirtySubmoduleWorktree(IgnoreSubmoduleMode mode) throws IOException { @@ -125,13 +152,8 @@ public class IndexDiffSubmoduleTest extends RepositoryTestCase { IndexDiff indexDiff = new IndexDiff(db, Constants.HEAD, new FileTreeIterator(db)); indexDiff.setIgnoreSubmoduleMode(mode); - if (mode.equals(IgnoreSubmoduleMode.ALL) - || mode.equals(IgnoreSubmoduleMode.DIRTY)) - assertFalse("diff should be false with mode=" + mode, - indexDiff.diff()); - else - assertTrue("diff should be true with mode=" + mode, - indexDiff.diff()); + assertDiff(indexDiff, mode, IgnoreSubmoduleMode.ALL, + IgnoreSubmoduleMode.DIRTY); } @Theory @@ -145,12 +167,7 @@ public class IndexDiffSubmoduleTest extends RepositoryTestCase { IndexDiff indexDiff = new IndexDiff(db, Constants.HEAD, new FileTreeIterator(db)); indexDiff.setIgnoreSubmoduleMode(mode); - if (mode.equals(IgnoreSubmoduleMode.ALL)) - assertFalse("diff should be false with mode=" + mode, - indexDiff.diff()); - else - assertTrue("diff should be true with mode=" + mode, - indexDiff.diff()); + assertDiff(indexDiff, mode, IgnoreSubmoduleMode.ALL); } @Theory @@ -163,13 +180,8 @@ public class IndexDiffSubmoduleTest extends RepositoryTestCase { IndexDiff indexDiff = new IndexDiff(db, Constants.HEAD, new FileTreeIterator(db)); indexDiff.setIgnoreSubmoduleMode(mode); - if (mode.equals(IgnoreSubmoduleMode.ALL) - || mode.equals(IgnoreSubmoduleMode.DIRTY)) - assertFalse("diff should be false with mode=" + mode, - indexDiff.diff()); - else - assertTrue("diff should be true with mode=" + mode, - indexDiff.diff()); + assertDiff(indexDiff, mode, IgnoreSubmoduleMode.ALL, + IgnoreSubmoduleMode.DIRTY); } @Theory @@ -183,13 +195,8 @@ public class IndexDiffSubmoduleTest extends RepositoryTestCase { IndexDiff indexDiff = new IndexDiff(db, Constants.HEAD, new FileTreeIterator(db)); indexDiff.setIgnoreSubmoduleMode(mode); - if (mode.equals(IgnoreSubmoduleMode.ALL) - || mode.equals(IgnoreSubmoduleMode.DIRTY)) - assertFalse("diff should be false with mode=" + mode, - indexDiff.diff()); - else - assertTrue("diff should be true with mode=" + mode, - indexDiff.diff()); + assertDiff(indexDiff, mode, IgnoreSubmoduleMode.ALL, + IgnoreSubmoduleMode.DIRTY); } @Theory @@ -200,13 +207,7 @@ public class IndexDiffSubmoduleTest extends RepositoryTestCase { IndexDiff indexDiff = new IndexDiff(db, Constants.HEAD, new FileTreeIterator(db)); indexDiff.setIgnoreSubmoduleMode(mode); - if (mode.equals(IgnoreSubmoduleMode.ALL) - || mode.equals(IgnoreSubmoduleMode.DIRTY) - || mode.equals(IgnoreSubmoduleMode.UNTRACKED)) - assertFalse("diff should be false with mode=" + mode, - indexDiff.diff()); - else - assertTrue("diff should be true with mode=" + mode, - indexDiff.diff()); + assertDiff(indexDiff, mode, IgnoreSubmoduleMode.ALL, + IgnoreSubmoduleMode.DIRTY, IgnoreSubmoduleMode.UNTRACKED); } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectCheckerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectCheckerTest.java index 43160fb115..7475d69f6c 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectCheckerTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectCheckerTest.java @@ -45,6 +45,7 @@ package org.eclipse.jgit.lib; import static java.lang.Integer.valueOf; +import static org.eclipse.jgit.junit.JGitTestUtil.concat; import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH; import static org.eclipse.jgit.lib.Constants.OBJ_BAD; import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; @@ -62,6 +63,7 @@ import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.HAS_DOTGIT; import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.NULL_SHA1; import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.TREE_NOT_SORTED; import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.ZERO_PADDED_FILEMODE; +import static org.eclipse.jgit.util.RawParseUtils.decode; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; import static org.junit.Assert.fail; @@ -72,11 +74,52 @@ import java.text.MessageFormat; import org.eclipse.jgit.errors.CorruptObjectException; import org.eclipse.jgit.internal.JGitText; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; public class ObjectCheckerTest { + private static final ObjectChecker SECRET_KEY_CHECKER = new ObjectChecker() { + @Override + public void checkBlob(byte[] raw) throws CorruptObjectException { + String in = decode(raw); + if (in.contains("secret_key")) { + throw new CorruptObjectException("don't add a secret key"); + } + } + }; + + private static final ObjectChecker SECRET_KEY_BLOB_CHECKER = new ObjectChecker() { + @Override + public BlobObjectChecker newBlobObjectChecker() { + return new BlobObjectChecker() { + private boolean containSecretKey; + + @Override + public void update(byte[] in, int offset, int len) { + String str = decode(in, offset, offset + len); + if (str.contains("secret_key")) { + containSecretKey = true; + } + } + + @Override + public void endBlob(AnyObjectId id) + throws CorruptObjectException { + if (containSecretKey) { + throw new CorruptObjectException( + "don't add a secret key"); + } + } + }; + } + }; + private ObjectChecker checker; + @Rule + public final ExpectedException thrown = ExpectedException.none(); + @Before public void setUp() throws Exception { checker = new ObjectChecker(); @@ -101,6 +144,32 @@ public class ObjectCheckerTest { } @Test + public void testCheckBlobNotCorrupt() throws CorruptObjectException { + SECRET_KEY_CHECKER.check(OBJ_BLOB, encodeASCII("key = \"public_key\"")); + } + + @Test + public void testCheckBlobCorrupt() throws CorruptObjectException { + thrown.expect(CorruptObjectException.class); + SECRET_KEY_CHECKER.check(OBJ_BLOB, encodeASCII("key = \"secret_key\"")); + } + + @Test + public void testCheckBlobWithBlobObjectCheckerNotCorrupt() + throws CorruptObjectException { + SECRET_KEY_BLOB_CHECKER.check(OBJ_BLOB, + encodeASCII("key = \"public_key\"")); + } + + @Test + public void testCheckBlobWithBlobObjectCheckerCorrupt() + throws CorruptObjectException { + thrown.expect(CorruptObjectException.class); + SECRET_KEY_BLOB_CHECKER.check(OBJ_BLOB, + encodeASCII("key = \"secret_key\"")); + } + + @Test public void testValidCommitNoParent() throws CorruptObjectException { StringBuilder b = new StringBuilder(); @@ -1054,20 +1123,7 @@ public class ObjectCheckerTest { checker.checkTree(data); } - private static byte[] concat(byte[]... b) { - int n = 0; - for (byte[] a : b) { - n += a.length; - } - byte[] data = new byte[n]; - n = 0; - for (byte[] a : b) { - System.arraycopy(a, 0, data, n, a.length); - n += a.length; - } - return data; - } @Test public void testInvalidTreeNameIsMacHFSGitCorruptUTF8AtEnd() diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ReflogResolveTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ReflogResolveTest.java index 7db9f60fd9..15f28afa6a 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ReflogResolveTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ReflogResolveTest.java @@ -179,4 +179,4 @@ public class ReflogResolveTest extends RepositoryTestCase { } } } -}
\ No newline at end of file +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/SubmoduleConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/SubmoduleConfigTest.java new file mode 100644 index 0000000000..fb8dec51d3 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/SubmoduleConfigTest.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2017, David Pursehouse <david.pursehouse@gmail.com> + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.lib; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.eclipse.jgit.lib.SubmoduleConfig.FetchRecurseSubmodulesMode; +import org.junit.Test; + +public class SubmoduleConfigTest { + @Test + public void fetchRecurseMatch() throws Exception { + assertTrue(FetchRecurseSubmodulesMode.YES.matchConfigValue("yes")); + assertTrue(FetchRecurseSubmodulesMode.YES.matchConfigValue("YES")); + assertTrue(FetchRecurseSubmodulesMode.YES.matchConfigValue("true")); + assertTrue(FetchRecurseSubmodulesMode.YES.matchConfigValue("TRUE")); + + assertTrue(FetchRecurseSubmodulesMode.ON_DEMAND + .matchConfigValue("on-demand")); + assertTrue(FetchRecurseSubmodulesMode.ON_DEMAND + .matchConfigValue("ON-DEMAND")); + assertTrue(FetchRecurseSubmodulesMode.ON_DEMAND + .matchConfigValue("on_demand")); + assertTrue(FetchRecurseSubmodulesMode.ON_DEMAND + .matchConfigValue("ON_DEMAND")); + + assertTrue(FetchRecurseSubmodulesMode.NO.matchConfigValue("no")); + assertTrue(FetchRecurseSubmodulesMode.NO.matchConfigValue("NO")); + assertTrue(FetchRecurseSubmodulesMode.NO.matchConfigValue("false")); + assertTrue(FetchRecurseSubmodulesMode.NO.matchConfigValue("FALSE")); + } + + @Test + public void fetchRecurseNoMatch() throws Exception { + assertFalse(FetchRecurseSubmodulesMode.YES.matchConfigValue("Y")); + assertFalse(FetchRecurseSubmodulesMode.NO.matchConfigValue("N")); + assertFalse(FetchRecurseSubmodulesMode.ON_DEMAND + .matchConfigValue("ONDEMAND")); + assertFalse(FetchRecurseSubmodulesMode.YES.matchConfigValue("")); + assertFalse(FetchRecurseSubmodulesMode.YES.matchConfigValue(null)); + } + + @Test + public void fetchRecurseToConfigValue() { + assertEquals("on-demand", + FetchRecurseSubmodulesMode.ON_DEMAND.toConfigValue()); + assertEquals("true", FetchRecurseSubmodulesMode.YES.toConfigValue()); + assertEquals("false", FetchRecurseSubmodulesMode.NO.toConfigValue()); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkMergeBaseTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkMergeBaseTest.java index 2451c50f6f..077645e650 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkMergeBaseTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkMergeBaseTest.java @@ -171,4 +171,4 @@ public class RevWalkMergeBaseTest extends RevWalkTestCase { assertNull(rw.next()); } -}
\ No newline at end of file +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/SkipRevFilterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/SkipRevFilterTest.java index 353a487732..cf02aa84c6 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/SkipRevFilterTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/SkipRevFilterTest.java @@ -81,4 +81,4 @@ public class SkipRevFilterTest extends RevWalkTestCase { public void testSkipRevFilterNegative() throws Exception { SkipRevFilter.create(-1); } -}
\ No newline at end of file +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleAddTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleAddTest.java index 5c46659c0a..f42dd02814 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleAddTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleAddTest.java @@ -269,4 +269,4 @@ public class SubmoduleAddTest extends RepositoryTestCase { ConfigConstants.CONFIG_KEY_URL)); } } -}
\ No newline at end of file +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleStatusTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleStatusTest.java index 61df9d93f1..5832518f81 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleStatusTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleStatusTest.java @@ -59,11 +59,11 @@ import org.eclipse.jgit.dircache.DirCacheEditor; import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit; import org.eclipse.jgit.dircache.DirCacheEntry; import org.eclipse.jgit.junit.RepositoryTestCase; +import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.storage.file.FileBasedConfig; @@ -256,11 +256,16 @@ public class SubmoduleStatusTest extends RepositoryTestCase { } @Test - public void repositoryWithInitializedSubmodule() throws IOException, - GitAPIException { - final ObjectId id = ObjectId - .fromString("abcd1234abcd1234abcd1234abcd1234abcd1234"); - final String path = "sub"; + public void repositoryWithInitializedSubmodule() throws Exception { + String path = "sub"; + Repository subRepo = Git.init().setBare(false) + .setDirectory(new File(db.getWorkTree(), path)).call() + .getRepository(); + assertNotNull(subRepo); + + TestRepository<?> subTr = new TestRepository<>(subRepo); + ObjectId id = subTr.branch(Constants.HEAD).commit().create().copy(); + DirCache cache = db.lockDirCache(); DirCacheEditor editor = cache.editor(); editor.add(new PathEdit(path) { @@ -287,15 +292,6 @@ public class SubmoduleStatusTest extends RepositoryTestCase { ConfigConstants.CONFIG_KEY_URL, url); modulesConfig.save(); - Repository subRepo = Git.init().setBare(false) - .setDirectory(new File(db.getWorkTree(), path)).call() - .getRepository(); - assertNotNull(subRepo); - - RefUpdate update = subRepo.updateRef(Constants.HEAD, true); - update.setNewObjectId(id); - update.forceUpdate(); - SubmoduleStatusCommand command = new SubmoduleStatusCommand(db); Map<String, SubmoduleStatus> statuses = command.call(); assertNotNull(statuses); @@ -312,11 +308,16 @@ public class SubmoduleStatusTest extends RepositoryTestCase { } @Test - public void repositoryWithDifferentRevCheckedOutSubmodule() - throws IOException, GitAPIException { - final ObjectId id = ObjectId - .fromString("abcd1234abcd1234abcd1234abcd1234abcd1234"); - final String path = "sub"; + public void repositoryWithDifferentRevCheckedOutSubmodule() throws Exception { + String path = "sub"; + Repository subRepo = Git.init().setBare(false) + .setDirectory(new File(db.getWorkTree(), path)).call() + .getRepository(); + assertNotNull(subRepo); + + TestRepository<?> subTr = new TestRepository<>(subRepo); + ObjectId id = subTr.branch(Constants.HEAD).commit().create().copy(); + DirCache cache = db.lockDirCache(); DirCacheEditor editor = cache.editor(); editor.add(new PathEdit(path) { @@ -343,15 +344,7 @@ public class SubmoduleStatusTest extends RepositoryTestCase { ConfigConstants.CONFIG_KEY_URL, url); modulesConfig.save(); - Repository subRepo = Git.init().setBare(false) - .setDirectory(new File(db.getWorkTree(), path)).call() - .getRepository(); - assertNotNull(subRepo); - - RefUpdate update = subRepo.updateRef(Constants.HEAD, true); - update.setNewObjectId(ObjectId - .fromString("aaaa0000aaaa0000aaaa0000aaaa0000aaaa0000")); - update.forceUpdate(); + ObjectId newId = subTr.branch(Constants.HEAD).commit().create().copy(); SubmoduleStatusCommand command = new SubmoduleStatusCommand(db); Map<String, SubmoduleStatus> statuses = command.call(); @@ -365,7 +358,7 @@ public class SubmoduleStatusTest extends RepositoryTestCase { assertNotNull(status); assertEquals(path, status.getPath()); assertEquals(id, status.getIndexId()); - assertEquals(update.getNewObjectId(), status.getHeadId()); + assertEquals(newId, status.getHeadId()); assertEquals(SubmoduleStatusType.REV_CHECKED_OUT, status.getType()); } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleWalkTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleWalkTest.java index 8998a85462..fed22c0262 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleWalkTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleWalkTest.java @@ -444,4 +444,44 @@ public class SubmoduleWalkTest extends RepositoryTestCase { assertNull(gen.getRepository()); assertFalse(gen.next()); } + + @Test + public void testTreeIteratorWithGitmodulesNameNotPath() throws Exception { + final ObjectId subId = ObjectId + .fromString("abcd1234abcd1234abcd1234abcd1234abcd1234"); + final String path = "sub"; + final String arbitraryName = "x"; + + final Config gitmodules = new Config(); + gitmodules.setString(CONFIG_SUBMODULE_SECTION, arbitraryName, + CONFIG_KEY_PATH, "sub"); + gitmodules.setString(CONFIG_SUBMODULE_SECTION, arbitraryName, + CONFIG_KEY_URL, "git://example.com/sub"); + + RevCommit commit = testDb.getRevWalk() + .parseCommit(testDb.commit().noParents() + .add(DOT_GIT_MODULES, gitmodules.toText()) + .edit(new PathEdit(path) { + + @Override + public void apply(DirCacheEntry ent) { + ent.setFileMode(FileMode.GITLINK); + ent.setObjectId(subId); + } + }).create()); + + final CanonicalTreeParser p = new CanonicalTreeParser(); + p.reset(testDb.getRevWalk().getObjectReader(), commit.getTree()); + SubmoduleWalk gen = SubmoduleWalk.forPath(db, p, "sub"); + assertEquals(path, gen.getPath()); + assertEquals(subId, gen.getObjectId()); + assertEquals(new File(db.getWorkTree(), path), gen.getDirectory()); + assertNull(gen.getConfigUpdate()); + assertNull(gen.getConfigUrl()); + assertEquals("sub", gen.getModulesPath()); + assertNull(gen.getModulesUpdate()); + assertEquals("git://example.com/sub", gen.getModulesUrl()); + assertNull(gen.getRepository()); + assertFalse(gen.next()); + } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/DaemonTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/DaemonTest.java new file mode 100644 index 0000000000..a5e5441405 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/DaemonTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2017 Thomas Wolf <thomas.wolf@paranor.ch> + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.transport; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.net.InetSocketAddress; + +import org.junit.Test; + +/** + * Daemon tests. + */ +public class DaemonTest { + + @Test + public void testDaemonStop() throws Exception { + Daemon d = new Daemon(); + d.start(); + InetSocketAddress address = d.getAddress(); + assertTrue("Port should be allocated", address.getPort() > 0); + assertTrue("Daemon should be running", d.isRunning()); + Thread.sleep(1000); // Give it time to enter accept() + d.stopAndWait(); + // Try to start a new Daemon again on the same port + d = new Daemon(address); + d.start(); + InetSocketAddress newAddress = d.getAddress(); + assertEquals("New daemon should run on the same port", address, + newAddress); + assertTrue("Daemon should be running", d.isRunning()); + Thread.sleep(1000); + d.stopAndWait(); + } + + @Test + public void testDaemonRestart() throws Exception { + Daemon d = new Daemon(); + d.start(); + InetSocketAddress address = d.getAddress(); + assertTrue("Port should be allocated", address.getPort() > 0); + assertTrue("Daemon should be running", d.isRunning()); + Thread.sleep(1000); + d.stopAndWait(); + // Re-start the same daemon + d.start(); + InetSocketAddress newAddress = d.getAddress(); + assertEquals("Daemon should again run on the same port", address, + newAddress); + assertTrue("Daemon should be running", d.isRunning()); + Thread.sleep(1000); + d.stopAndWait(); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/HttpConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/HttpConfigTest.java new file mode 100644 index 0000000000..c6b016a4cc --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/HttpConfigTest.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2017, Thomas Wolf <thomas.wolf@paranor.ch> + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.transport; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.eclipse.jgit.lib.Config; +import org.junit.Before; +import org.junit.Test; + +/** + * Tests for correctly resolving URIs when reading http.* values from a + * {@link Config}. + */ +public class HttpConfigTest { + + private static final String DEFAULT = "[http]\n" + "\tpostBuffer = 1\n" + + "\tsslVerify= true\n" + "\tfollowRedirects = true\n" + + "\tmaxRedirects = 5\n\n"; + + private Config config; + + @Before + public void setUp() { + config = new Config(); + } + + @Test + public void testDefault() throws Exception { + HttpConfig http = new HttpConfig(config, + new URIish("http://example.com/path/repo.git")); + assertEquals(1024 * 1024, http.getPostBuffer()); + assertTrue(http.isSslVerify()); + assertEquals(HttpConfig.HttpRedirectMode.INITIAL, + http.getFollowRedirects()); + } + + @Test + public void testMatchSuccess() throws Exception { + config.fromText(DEFAULT + "[http \"http://example.com\"]\n" + + "\tpostBuffer = 1024\n"); + HttpConfig http = new HttpConfig(config, + new URIish("http://example.com/path/repo.git")); + assertEquals(1024, http.getPostBuffer()); + http = new HttpConfig(config, + new URIish("https://example.com/path/repo.git")); + assertEquals(1, http.getPostBuffer()); + http = new HttpConfig(config, + new URIish("http://example.org/path/repo.git")); + assertEquals(1, http.getPostBuffer()); + http = new HttpConfig(config, + new URIish("http://example.com:80/path/repo.git")); + assertEquals(1024, http.getPostBuffer()); + http = new HttpConfig(config, + new URIish("http://example.com:8080/path/repo.git")); + assertEquals(1, http.getPostBuffer()); + } + + @Test + public void testMatchWithOnlySchemeInConfig() throws Exception { + config.fromText( + DEFAULT + "[http \"http://\"]\n" + "\tpostBuffer = 1024\n"); + HttpConfig http = new HttpConfig(config, + new URIish("http://example.com/path/repo.git")); + assertEquals(1, http.getPostBuffer()); + } + + @Test + public void testMatchWithPrefixUriInConfig() throws Exception { + config.fromText(DEFAULT + "[http \"http://example\"]\n" + + "\tpostBuffer = 1024\n"); + HttpConfig http = new HttpConfig(config, + new URIish("http://example.com/path/repo.git")); + assertEquals(1, http.getPostBuffer()); + } + + @Test + public void testMatchCaseSensitivity() throws Exception { + config.fromText(DEFAULT + "[http \"http://exAMPle.com\"]\n" + + "\tpostBuffer = 1024\n"); + HttpConfig http = new HttpConfig(config, + new URIish("http://example.com/path/repo.git")); + assertEquals(1024, http.getPostBuffer()); + } + + @Test + public void testMatchWithInvalidUriInConfig() throws Exception { + config.fromText( + DEFAULT + "[http \"///\"]\n" + "\tpostBuffer = 1024\n"); + HttpConfig http = new HttpConfig(config, + new URIish("http://example.com/path/repo.git")); + assertEquals(1, http.getPostBuffer()); + } + + @Test + public void testMatchWithInvalidAndValidUriInConfig() throws Exception { + config.fromText(DEFAULT + "[http \"///\"]\n" + "\tpostBuffer = 1024\n" + + "[http \"http://example.com\"]\n" + "\tpostBuffer = 2048\n"); + HttpConfig http = new HttpConfig(config, + new URIish("http://example.com/path/repo.git")); + assertEquals(2048, http.getPostBuffer()); + } + + @Test + public void testMatchWithHostEndingInSlash() throws Exception { + config.fromText(DEFAULT + "[http \"http://example.com/\"]\n" + + "\tpostBuffer = 1024\n"); + HttpConfig http = new HttpConfig(config, + new URIish("http://example.com/path/repo.git")); + assertEquals(1024, http.getPostBuffer()); + } + + @Test + public void testMatchWithUser() throws Exception { + config.fromText(DEFAULT + "[http \"http://example.com/path\"]\n" + + "\tpostBuffer = 1024\n" + + "[http \"http://example.com/path/repo\"]\n" + + "\tpostBuffer = 2048\n" + + "[http \"http://user@example.com/path\"]\n" + + "\tpostBuffer = 4096\n"); + HttpConfig http = new HttpConfig(config, + new URIish("http://example.com/path/repo.git")); + assertEquals(1024, http.getPostBuffer()); + http = new HttpConfig(config, + new URIish("http://user@example.com/path/repo.git")); + assertEquals(4096, http.getPostBuffer()); + http = new HttpConfig(config, + new URIish("http://user@example.com/path/repo/foo.git")); + assertEquals(2048, http.getPostBuffer()); + http = new HttpConfig(config, + new URIish("http://user@example.com/path/foo.git")); + assertEquals(4096, http.getPostBuffer()); + http = new HttpConfig(config, + new URIish("http://example.com/path/foo.git")); + assertEquals(1024, http.getPostBuffer()); + http = new HttpConfig(config, + new URIish("http://User@example.com/path/repo/foo.git")); + assertEquals(2048, http.getPostBuffer()); + http = new HttpConfig(config, + new URIish("http://User@example.com/path/foo.git")); + assertEquals(1024, http.getPostBuffer()); + } + + @Test + public void testMatchLonger() throws Exception { + config.fromText(DEFAULT + "[http \"http://example.com/path\"]\n" + + "\tpostBuffer = 1024\n" + + "[http \"http://example.com/path/repo\"]\n" + + "\tpostBuffer = 2048\n"); + HttpConfig http = new HttpConfig(config, + new URIish("http://example.com/path/repo.git")); + assertEquals(1024, http.getPostBuffer()); + http = new HttpConfig(config, + new URIish("http://example.com/foo/repo.git")); + assertEquals(1, http.getPostBuffer()); + http = new HttpConfig(config, + new URIish("https://example.com/path/repo.git")); + assertEquals(1, http.getPostBuffer()); + http = new HttpConfig(config, + new URIish("http://example.com/path/repo/.git")); + assertEquals(2048, http.getPostBuffer()); + http = new HttpConfig(config, new URIish("http://example.com/path")); + assertEquals(1024, http.getPostBuffer()); + http = new HttpConfig(config, + new URIish("http://user@example.com/path")); + assertEquals(1024, http.getPostBuffer()); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/HttpConfigUriPathTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/HttpConfigUriPathTest.java new file mode 100644 index 0000000000..94de2f211a --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/HttpConfigUriPathTest.java @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2017, Thomas Wolf <thomas.wolf@paranor.ch> + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.transport; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import org.junit.Test; + +/** + * Basic URI path prefix match tests for {@link HttpConfig}. + */ +public class HttpConfigUriPathTest { + + @Test + public void testNormalizationEmptyPaths() { + assertEquals("/", HttpConfig.normalize("")); + assertEquals("/", HttpConfig.normalize("/")); + } + + @Test + public void testNormalization() { + assertEquals("/f", HttpConfig.normalize("f")); + assertEquals("/f", HttpConfig.normalize("/f")); + assertEquals("/f/", HttpConfig.normalize("/f/")); + assertEquals("/foo", HttpConfig.normalize("foo")); + assertEquals("/foo", HttpConfig.normalize("/foo")); + assertEquals("/foo/", HttpConfig.normalize("/foo/")); + assertEquals("/foo/bar", HttpConfig.normalize("foo/bar")); + assertEquals("/foo/bar", HttpConfig.normalize("/foo/bar")); + assertEquals("/foo/bar/", HttpConfig.normalize("/foo/bar/")); + } + + @Test + public void testNormalizationWithDot() { + assertEquals("/", HttpConfig.normalize(".")); + assertEquals("/", HttpConfig.normalize("/.")); + assertEquals("/", HttpConfig.normalize("/./")); + assertEquals("/foo", HttpConfig.normalize("foo/.")); + assertEquals("/foo/bar", HttpConfig.normalize("/foo/./bar")); + assertEquals("/foo/bar", HttpConfig.normalize("/foo/bar/.")); + assertEquals("/foo/bar/", HttpConfig.normalize("/foo/bar/./")); + assertEquals("/foo/bar", HttpConfig.normalize("/foo/./././bar")); + assertEquals("/foo/bar/", HttpConfig.normalize("/foo/./././bar/")); + assertEquals("/foo/bar", HttpConfig.normalize("/foo/bar/././.")); + assertEquals("/foo/bar/", HttpConfig.normalize("/foo/bar/./././")); + assertEquals("/foo/bar/.baz/bam", + HttpConfig.normalize("/foo/bar/.baz/bam")); + assertEquals("/foo/bar/.baz/bam/", + HttpConfig.normalize("/foo/bar/.baz/bam/")); + } + + @Test + public void testNormalizationWithDotDot() { + assertEquals("/", HttpConfig.normalize("foo/..")); + assertEquals("/", HttpConfig.normalize("/foo/..")); + assertEquals("/", HttpConfig.normalize("/foo/../bar/..")); + assertEquals("/", HttpConfig.normalize("/foo/.././bar/..")); + assertEquals("/bar", HttpConfig.normalize("foo/../bar")); + assertEquals("/bar", HttpConfig.normalize("/foo/../bar")); + assertEquals("/bar", HttpConfig.normalize("/foo/./.././bar")); + assertEquals("/bar/", HttpConfig.normalize("/foo/../bar/")); + assertEquals("/bar/", HttpConfig.normalize("/foo/./.././bar/")); + assertEquals("/foo/bar", HttpConfig.normalize("/foo/bar/baz/..")); + assertEquals("/foo/bar/", HttpConfig.normalize("/foo/bar/baz/../")); + assertEquals("/foo", HttpConfig.normalize("/foo/bar/baz/../..")); + assertEquals("/foo", HttpConfig.normalize("/foo/bar/baz/../..")); + assertEquals("/foo", HttpConfig.normalize("/foo/bar/baz/.././..")); + assertEquals("/foo", HttpConfig.normalize("/foo/bar/baz/../././..")); + assertEquals("/foo/baz", HttpConfig.normalize("/foo/bar/../baz")); + assertEquals("/foo/baz/", HttpConfig.normalize("/foo/bar/../baz/")); + assertEquals("/foo/baz", HttpConfig.normalize("/foo/bar/../baz/.")); + assertEquals("/foo/baz/", HttpConfig.normalize("/foo/bar/../baz/./")); + assertEquals("/foo", HttpConfig.normalize("/foo/bar/../baz/..")); + assertEquals("/foo/", HttpConfig.normalize("/foo/bar/../baz/../")); + assertEquals("/baz", HttpConfig.normalize("/foo/bar/../../baz")); + assertEquals("/baz/", HttpConfig.normalize("/foo/bar/../../baz/")); + assertEquals("/foo/.b/bar", HttpConfig.normalize("/foo/.b/bar")); + assertEquals("/.f/foo/.b/bar/", HttpConfig.normalize(".f/foo/.b/bar/")); + assertEquals("/foo/bar/..baz/bam", + HttpConfig.normalize("/foo/bar/..baz/bam")); + assertEquals("/foo/bar/..baz/bam/", + HttpConfig.normalize("/foo/bar/..baz/bam/")); + assertEquals("/foo/bar/.../baz/bam", + HttpConfig.normalize("/foo/bar/.../baz/bam")); + assertEquals("/foo/bar/.../baz/bam/", + HttpConfig.normalize("/foo/bar/.../baz/bam/")); + } + + @Test + public void testNormalizationWithDoubleSlash() { + assertEquals("/", HttpConfig.normalize("//")); + assertEquals("/foo/", HttpConfig.normalize("///foo//")); + assertEquals("/foo", HttpConfig.normalize("///foo//.")); + assertEquals("/foo/", HttpConfig.normalize("///foo//.////")); + assertEquals("/foo/bar", HttpConfig.normalize("/foo//bar")); + assertEquals("/foo/bar", HttpConfig.normalize("/foo//bar//.")); + assertEquals("/foo/bar/", HttpConfig.normalize("/foo//bar//./")); + } + + @Test + public void testNormalizationWithDotDotFailing() { + assertNull(HttpConfig.normalize("..")); + assertNull(HttpConfig.normalize("/..")); + assertNull(HttpConfig.normalize("/../")); + assertNull(HttpConfig.normalize("/../foo")); + assertNull(HttpConfig.normalize("./../foo")); + assertNull(HttpConfig.normalize("/./../foo")); + assertNull(HttpConfig.normalize("/foo/./.././..")); + assertNull(HttpConfig.normalize("/foo/../bar/../..")); + assertNull(HttpConfig.normalize("/foo/../bar/../../baz")); + } + + @Test + public void testSegmentCompare() { + // 2nd parameter is the match, will be normalized + assertSuccess("/foo", ""); + assertSuccess("/foo", "/"); + assertSuccess("/foo", "//"); + assertSuccess("/foo", "foo"); + assertSuccess("/foo", "/foo"); + assertSuccess("/foo/", "foo"); + assertSuccess("/foo/", "/foo"); + assertSuccess("/foo/", "foo/"); + assertSuccess("/foo/", "/foo/"); + assertSuccess("/foo/bar", "foo"); + assertSuccess("/foo/bar", "foo/"); + assertSuccess("/foo/bar", "foo/bar"); + assertSuccess("/foo/bar/", "foo/bar"); + assertSuccess("/foo/bar/", "foo/bar/"); + assertSuccess("/foo/bar", "/foo/bar"); + assertSuccess("/foo/bar/", "/foo/bar"); + assertSuccess("/foo/bar/", "/foo/bar/"); + assertSuccess("/foo/bar", "/foo/bar/.."); + assertSuccess("/foo/bar/", "/foo/bar/.."); + assertSuccess("/foo/bar/", "/foo/bar/../"); + assertSuccess("/foo/bar", "/foo/./bar"); + assertSuccess("/foo/bar/", "/foo/./bar/"); + assertSuccess("/some/repo/.git", "/some/repo"); + assertSuccess("/some/repo/bare.git", "/some/repo"); + assertSuccess("/some/repo/.git", "/some/repo/.git"); + assertSuccess("/some/repo/bare.git", "/some/repo/bare.git"); + } + + @Test + public void testSegmentCompareFailing() { + // 2nd parameter is the match, will be normalized + assertEquals(-1, HttpConfig.segmentCompare("/foo", "foo/")); + assertEquals(-1, HttpConfig.segmentCompare("/foo", "/foo/")); + assertEquals(-1, HttpConfig.segmentCompare("/foobar", "foo")); + assertEquals(-1, HttpConfig.segmentCompare("/foobar", "/foo")); + assertEquals(-1, + HttpConfig.segmentCompare("/foo/barbar/baz", "foo/bar")); + assertEquals(-1, HttpConfig.segmentCompare("/foo/barbar", "/foo/bar")); + assertEquals(-1, + HttpConfig.segmentCompare("/some/repo.git", "/some/repo")); + assertEquals(-1, + HttpConfig.segmentCompare("/some/repo.git", "/some/repo.g")); + assertEquals(-1, HttpConfig.segmentCompare("/some/repo/bare.git", + "/some/repo/bar")); + assertSuccess("/some/repo/bare.git", "/some/repo"); + // Just to make sure we don't use the PathMatchers... + assertEquals(-1, HttpConfig.segmentCompare("/foo/barbar/baz", "**")); + assertEquals(-1, + HttpConfig.segmentCompare("/foo/barbar/baz", "**/foo")); + assertEquals(-1, + HttpConfig.segmentCompare("/foo/barbar/baz", "/*/barbar/**")); + assertEquals(-1, HttpConfig.segmentCompare("/foo", "/*")); + assertEquals(-1, HttpConfig.segmentCompare("/foo", "/???")); + assertEquals(-1, HttpConfig.segmentCompare("/foo/bar/baz", "bar")); + // Failing to normalize + assertEquals(-1, + HttpConfig.segmentCompare("/foo/bar/baz", "bar/../..")); + } + + private void assertSuccess(String uri, String match) { + String normalized = HttpConfig.normalize(match); + assertEquals(normalized.length(), + HttpConfig.segmentCompare(uri, match)); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/JschConfigSessionFactoryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/JschConfigSessionFactoryTest.java new file mode 100644 index 0000000000..1e65a20d7f --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/JschConfigSessionFactoryTest.java @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.transport; + +import static org.junit.Assert.assertEquals; + +import java.io.File; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jgit.util.FS; +import org.junit.After; +import org.junit.Test; + +import com.jcraft.jsch.Session; + +/** + * Tests for correctly interpreting ssh config values when Jsch sessions are + * used. + */ +public class JschConfigSessionFactoryTest { + + File tmpConfigFile; + + OpenSshConfig tmpConfig; + + DefaultSshSessionFactory factory = new DefaultSshSessionFactory(); + + @After + public void removeTmpConfig() { + if (tmpConfigFile == null) { + return; + } + if (tmpConfigFile.exists() && !tmpConfigFile.delete()) { + tmpConfigFile.deleteOnExit(); + } + tmpConfigFile = null; + } + + @Test + public void testNoConfigEntry() throws Exception { + tmpConfigFile = File.createTempFile("jsch", "test"); + tmpConfig = new OpenSshConfig(tmpConfigFile.getParentFile(), + tmpConfigFile); + factory.setConfig(tmpConfig); + Session session = createSession("ssh://egit/egit/egit"); + assertEquals("egit", session.getHost()); + // No user in URI, none in ssh config: default is OS user name + assertEquals(System.getProperty("user.name"), session.getUserName()); + assertEquals(22, session.getPort()); + } + + @Test + public void testAlias() throws Exception { + tmpConfigFile = createConfig("Host egit", "Hostname git.eclipse.org", + "User foo", "Port 29418"); + tmpConfig = new OpenSshConfig(tmpConfigFile.getParentFile(), + tmpConfigFile); + factory.setConfig(tmpConfig); + Session session = createSession("ssh://egit/egit/egit"); + assertEquals("git.eclipse.org", session.getHost()); + assertEquals("foo", session.getUserName()); + assertEquals(29418, session.getPort()); + } + + @Test + public void testAliasWithUser() throws Exception { + tmpConfigFile = createConfig("Host egit", "Hostname git.eclipse.org", + "User foo", "Port 29418"); + tmpConfig = new OpenSshConfig(tmpConfigFile.getParentFile(), + tmpConfigFile); + factory.setConfig(tmpConfig); + Session session = createSession("ssh://bar@egit/egit/egit"); + assertEquals("git.eclipse.org", session.getHost()); + assertEquals("bar", session.getUserName()); + assertEquals(29418, session.getPort()); + } + + @Test + public void testAliasWithPort() throws Exception { + tmpConfigFile = createConfig("Host egit", "Hostname git.eclipse.org", + "User foo", "Port 29418"); + tmpConfig = new OpenSshConfig(tmpConfigFile.getParentFile(), + tmpConfigFile); + factory.setConfig(tmpConfig); + Session session = createSession("ssh://bar@egit:22/egit/egit"); + assertEquals("git.eclipse.org", session.getHost()); + assertEquals("bar", session.getUserName()); + assertEquals(22, session.getPort()); + } + + @Test + public void testAliasIdentical() throws Exception { + tmpConfigFile = createConfig("Host git.eclipse.org", + "Hostname git.eclipse.org", "User foo", "Port 29418"); + tmpConfig = new OpenSshConfig(tmpConfigFile.getParentFile(), + tmpConfigFile); + factory.setConfig(tmpConfig); + Session session = createSession("ssh://git.eclipse.org/egit/egit"); + assertEquals("git.eclipse.org", session.getHost()); + assertEquals("foo", session.getUserName()); + assertEquals(29418, session.getPort()); + } + + @Test + public void testAliasIdenticalWithUser() throws Exception { + tmpConfigFile = createConfig("Host git.eclipse.org", + "Hostname git.eclipse.org", "User foo", "Port 29418"); + tmpConfig = new OpenSshConfig(tmpConfigFile.getParentFile(), + tmpConfigFile); + factory.setConfig(tmpConfig); + Session session = createSession("ssh://bar@git.eclipse.org/egit/egit"); + assertEquals("git.eclipse.org", session.getHost()); + assertEquals("bar", session.getUserName()); + assertEquals(29418, session.getPort()); + } + + @Test + public void testAliasIdenticalWithPort() throws Exception { + tmpConfigFile = createConfig("Host git.eclipse.org", + "Hostname git.eclipse.org", "User foo", "Port 29418"); + tmpConfig = new OpenSshConfig(tmpConfigFile.getParentFile(), + tmpConfigFile); + factory.setConfig(tmpConfig); + Session session = createSession( + "ssh://bar@git.eclipse.org:300/egit/egit"); + assertEquals("git.eclipse.org", session.getHost()); + assertEquals("bar", session.getUserName()); + assertEquals(300, session.getPort()); + } + + @Test + public void testConnectTimout() throws Exception { + tmpConfigFile = createConfig("Host git.eclipse.org", + "Hostname git.eclipse.org", "User foo", "Port 29418", + "ConnectTimeout 10"); + tmpConfig = new OpenSshConfig(tmpConfigFile.getParentFile(), + tmpConfigFile); + factory.setConfig(tmpConfig); + Session session = createSession("ssh://git.eclipse.org/something"); + assertEquals("git.eclipse.org", session.getHost()); + assertEquals("foo", session.getUserName()); + assertEquals(29418, session.getPort()); + assertEquals(TimeUnit.SECONDS.toMillis(10), session.getTimeout()); + } + + @Test + public void testAliasCaseDifferenceUpcase() throws Exception { + tmpConfigFile = createConfig("Host Bitbucket.org", + "Hostname bitbucket.org", "User foo", "Port 29418", + "ConnectTimeout 10", // + "Host bitbucket.org", "Hostname bitbucket.org", "User bar", + "Port 22", "ConnectTimeout 5"); + tmpConfig = new OpenSshConfig(tmpConfigFile.getParentFile(), + tmpConfigFile); + factory.setConfig(tmpConfig); + Session session = createSession("ssh://Bitbucket.org/something"); + assertEquals("bitbucket.org", session.getHost()); + assertEquals("foo", session.getUserName()); + assertEquals(29418, session.getPort()); + assertEquals(TimeUnit.SECONDS.toMillis(10), session.getTimeout()); + } + + @Test + public void testAliasCaseDifferenceLowcase() throws Exception { + tmpConfigFile = createConfig("Host Bitbucket.org", + "Hostname bitbucket.org", "User foo", "Port 29418", + "ConnectTimeout 10", // + "Host bitbucket.org", "Hostname bitbucket.org", "User bar", + "Port 22", "ConnectTimeout 5"); + tmpConfig = new OpenSshConfig(tmpConfigFile.getParentFile(), + tmpConfigFile); + factory.setConfig(tmpConfig); + Session session = createSession("ssh://bitbucket.org/something"); + assertEquals("bitbucket.org", session.getHost()); + assertEquals("bar", session.getUserName()); + assertEquals(22, session.getPort()); + assertEquals(TimeUnit.SECONDS.toMillis(5), session.getTimeout()); + } + + @Test + public void testAliasCaseDifferenceUpcaseInverted() throws Exception { + tmpConfigFile = createConfig("Host bitbucket.org", + "Hostname bitbucket.org", "User bar", "Port 22", + "ConnectTimeout 5", // + "Host Bitbucket.org", "Hostname bitbucket.org", "User foo", + "Port 29418", "ConnectTimeout 10"); + tmpConfig = new OpenSshConfig(tmpConfigFile.getParentFile(), + tmpConfigFile); + factory.setConfig(tmpConfig); + Session session = createSession("ssh://Bitbucket.org/something"); + assertEquals("bitbucket.org", session.getHost()); + assertEquals("foo", session.getUserName()); + assertEquals(29418, session.getPort()); + assertEquals(TimeUnit.SECONDS.toMillis(10), session.getTimeout()); + } + + @Test + public void testAliasCaseDifferenceLowcaseInverted() throws Exception { + tmpConfigFile = createConfig("Host bitbucket.org", + "Hostname bitbucket.org", "User bar", "Port 22", + "ConnectTimeout 5", // + "Host Bitbucket.org", "Hostname bitbucket.org", "User foo", + "Port 29418", "ConnectTimeout 10"); + tmpConfig = new OpenSshConfig(tmpConfigFile.getParentFile(), + tmpConfigFile); + factory.setConfig(tmpConfig); + Session session = createSession("ssh://bitbucket.org/something"); + assertEquals("bitbucket.org", session.getHost()); + assertEquals("bar", session.getUserName()); + assertEquals(22, session.getPort()); + assertEquals(TimeUnit.SECONDS.toMillis(5), session.getTimeout()); + } + + private File createConfig(String... lines) throws Exception { + File f = File.createTempFile("jsch", "test"); + Files.write(f.toPath(), Arrays.asList(lines)); + return f; + } + + private Session createSession(String uriText) throws Exception { + // For this test to make sense, these few lines must correspond to the + // code in JschConfigSessionFactory.getSession(). Because of + // side-effects we cannot encapsulate that there properly and so we have + // to duplicate this bit here. We also can't test getSession() itself + // since it would try to actually connect to a server. + URIish uri = new URIish(uriText); + String host = uri.getHost(); + String user = uri.getUser(); + String password = uri.getPass(); + int port = uri.getPort(); + OpenSshConfig.Host hostConfig = tmpConfig.lookup(host); + if (port <= 0) { + port = hostConfig.getPort(); + } + if (user == null) { + user = hostConfig.getUser(); + } + return factory.createSession(null, FS.DETECTED, user, password, host, + port, hostConfig); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/OpenSshConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/OpenSshConfigTest.java index fc520ab17f..d604751fef 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/OpenSshConfigTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/OpenSshConfigTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008, 2014 Google Inc. + * Copyright (C) 2008, 2017 Google Inc. * and other copyright owners as documented in the project's IP log. * * This program and the accompanying materials are made available @@ -43,10 +43,13 @@ package org.eclipse.jgit.transport; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import java.io.File; @@ -58,9 +61,12 @@ import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.transport.OpenSshConfig.Host; import org.eclipse.jgit.util.FileUtils; +import org.eclipse.jgit.util.SystemReader; import org.junit.Before; import org.junit.Test; +import com.jcraft.jsch.ConfigRepository; + public class OpenSshConfigTest extends RepositoryTestCase { private File home; @@ -79,15 +85,18 @@ public class OpenSshConfigTest extends RepositoryTestCase { configFile = new File(new File(home, ".ssh"), Constants.CONFIG); FileUtils.mkdir(configFile.getParentFile()); - System.setProperty("user.name", "jex_junit"); + mockSystemReader.setProperty(Constants.OS_USER_NAME_KEY, "jex_junit"); osc = new OpenSshConfig(home, configFile); } private void config(final String data) throws IOException { - final OutputStreamWriter fw = new OutputStreamWriter( - new FileOutputStream(configFile), "UTF-8"); - fw.write(data); - fw.close(); + long lastMtime = configFile.lastModified(); + do { + try (final OutputStreamWriter fw = new OutputStreamWriter( + new FileOutputStream(configFile), "UTF-8")) { + fw.write(data); + } + } while (lastMtime == configFile.lastModified()); } @Test @@ -155,13 +164,18 @@ public class OpenSshConfigTest extends RepositoryTestCase { @Test public void testAlias_DoesNotMatch() throws Exception { - config("Host orcz\n" + "\tHostName repo.or.cz\n"); + config("Host orcz\n" + "Port 29418\n" + "\tHostName repo.or.cz\n"); final Host h = osc.lookup("repo.or.cz"); assertNotNull(h); assertEquals("repo.or.cz", h.getHostName()); assertEquals("jex_junit", h.getUser()); assertEquals(22, h.getPort()); assertNull(h.getIdentityFile()); + final Host h2 = osc.lookup("orcz"); + assertEquals("repo.or.cz", h.getHostName()); + assertEquals("jex_junit", h.getUser()); + assertEquals(29418, h2.getPort()); + assertNull(h.getIdentityFile()); } @Test @@ -282,4 +296,198 @@ public class OpenSshConfigTest extends RepositoryTestCase { assertNotNull(h); assertEquals(1, h.getConnectionAttempts()); } + + @Test + public void testDefaultBlock() throws Exception { + config("ConnectionAttempts 5\n\nHost orcz\nConnectionAttempts 3\n"); + final Host h = osc.lookup("orcz"); + assertNotNull(h); + assertEquals(5, h.getConnectionAttempts()); + } + + @Test + public void testHostCaseInsensitive() throws Exception { + config("hOsT orcz\nConnectionAttempts 3\n"); + final Host h = osc.lookup("orcz"); + assertNotNull(h); + assertEquals(3, h.getConnectionAttempts()); + } + + @Test + public void testListValueSingle() throws Exception { + config("Host orcz\nUserKnownHostsFile /foo/bar\n"); + final ConfigRepository.Config c = osc.getConfig("orcz"); + assertNotNull(c); + assertEquals("/foo/bar", c.getValue("UserKnownHostsFile")); + } + + @Test + public void testListValueMultiple() throws Exception { + // Tilde expansion occurs within the parser + config("Host orcz\nUserKnownHostsFile \"~/foo/ba z\" /foo/bar \n"); + final ConfigRepository.Config c = osc.getConfig("orcz"); + assertNotNull(c); + assertArrayEquals(new Object[] { new File(home, "foo/ba z").getPath(), + "/foo/bar" }, + c.getValues("UserKnownHostsFile")); + } + + @Test + public void testRepeatedLookups() throws Exception { + config("Host orcz\n" + "\tConnectionAttempts 5\n"); + final Host h1 = osc.lookup("orcz"); + final Host h2 = osc.lookup("orcz"); + assertNotNull(h1); + assertSame(h1, h2); + assertEquals(5, h1.getConnectionAttempts()); + assertEquals(h1.getConnectionAttempts(), h2.getConnectionAttempts()); + final ConfigRepository.Config c = osc.getConfig("orcz"); + assertNotNull(c); + assertSame(c, h1.getConfig()); + assertSame(c, h2.getConfig()); + } + + @Test + public void testRepeatedLookupsWithModification() throws Exception { + config("Host orcz\n" + "\tConnectionAttempts -1\n"); + final Host h1 = osc.lookup("orcz"); + assertNotNull(h1); + assertEquals(1, h1.getConnectionAttempts()); + config("Host orcz\n" + "\tConnectionAttempts 5\n"); + final Host h2 = osc.lookup("orcz"); + assertNotNull(h2); + assertNotSame(h1, h2); + assertEquals(5, h2.getConnectionAttempts()); + assertEquals(1, h1.getConnectionAttempts()); + assertNotSame(h1.getConfig(), h2.getConfig()); + } + + @Test + public void testIdentityFile() throws Exception { + config("Host orcz\nIdentityFile \"~/foo/ba z\"\nIdentityFile /foo/bar"); + final Host h = osc.lookup("orcz"); + assertNotNull(h); + File f = h.getIdentityFile(); + assertNotNull(f); + // Host does tilde replacement + assertEquals(new File(home, "foo/ba z"), f); + final ConfigRepository.Config c = h.getConfig(); + // Config does tilde replacement, too + assertArrayEquals(new Object[] { new File(home, "foo/ba z").getPath(), + "/foo/bar" }, + c.getValues("IdentityFile")); + } + + @Test + public void testMultiIdentityFile() throws Exception { + config("IdentityFile \"~/foo/ba z\"\nHost orcz\nIdentityFile /foo/bar\nHOST *\nIdentityFile /foo/baz"); + final Host h = osc.lookup("orcz"); + assertNotNull(h); + File f = h.getIdentityFile(); + assertNotNull(f); + // Host does tilde replacement + assertEquals(new File(home, "foo/ba z"), f); + final ConfigRepository.Config c = h.getConfig(); + // Config does tilde replacement, too + assertArrayEquals(new Object[] { new File(home, "foo/ba z").getPath(), + "/foo/bar", "/foo/baz" }, + c.getValues("IdentityFile")); + } + + @Test + public void testNegatedPattern() throws Exception { + config("Host repo.or.cz\nIdentityFile ~/foo/bar\nHOST !*.or.cz\nIdentityFile /foo/baz"); + final Host h = osc.lookup("repo.or.cz"); + assertNotNull(h); + assertEquals(new File(home, "foo/bar"), h.getIdentityFile()); + assertArrayEquals(new Object[] { new File(home, "foo/bar").getPath() }, + h.getConfig().getValues("IdentityFile")); + } + + @Test + public void testPattern() throws Exception { + config("Host repo.or.cz\nIdentityFile ~/foo/bar\nHOST *.or.cz\nIdentityFile /foo/baz"); + final Host h = osc.lookup("repo.or.cz"); + assertNotNull(h); + assertEquals(new File(home, "foo/bar"), h.getIdentityFile()); + assertArrayEquals(new Object[] { new File(home, "foo/bar").getPath(), + "/foo/baz" }, + h.getConfig().getValues("IdentityFile")); + } + + @Test + public void testMultiHost() throws Exception { + config("Host orcz *.or.cz\nIdentityFile ~/foo/bar\nHOST *.or.cz\nIdentityFile /foo/baz"); + final Host h1 = osc.lookup("repo.or.cz"); + assertNotNull(h1); + assertEquals(new File(home, "foo/bar"), h1.getIdentityFile()); + assertArrayEquals(new Object[] { new File(home, "foo/bar").getPath(), + "/foo/baz" }, + h1.getConfig().getValues("IdentityFile")); + final Host h2 = osc.lookup("orcz"); + assertNotNull(h2); + assertEquals(new File(home, "foo/bar"), h2.getIdentityFile()); + assertArrayEquals(new Object[] { new File(home, "foo/bar").getPath() }, + h2.getConfig().getValues("IdentityFile")); + } + + @Test + public void testEqualsSign() throws Exception { + config("Host=orcz\n\tConnectionAttempts = 5\n\tUser=\t foobar\t\n"); + final Host h = osc.lookup("orcz"); + assertNotNull(h); + assertEquals(5, h.getConnectionAttempts()); + assertEquals("foobar", h.getUser()); + } + + @Test + public void testMissingArgument() throws Exception { + config("Host=orcz\n\tSendEnv\nIdentityFile\t\nForwardX11\n\tUser=\t foobar\t\n"); + final Host h = osc.lookup("orcz"); + assertNotNull(h); + assertEquals("foobar", h.getUser()); + assertArrayEquals(new String[0], h.getConfig().getValues("SendEnv")); + assertNull(h.getIdentityFile()); + assertNull(h.getConfig().getValue("ForwardX11")); + } + + @Test + public void testHomeDirUserReplacement() throws Exception { + config("Host=orcz\n\tIdentityFile %d/.ssh/%u_id_dsa"); + final Host h = osc.lookup("orcz"); + assertNotNull(h); + assertEquals(new File(new File(home, ".ssh"), "jex_junit_id_dsa"), + h.getIdentityFile()); + } + + @Test + public void testHostnameReplacement() throws Exception { + config("Host=orcz\nHost *.*\n\tHostname %h\nHost *\n\tHostname %h.example.org"); + final Host h = osc.lookup("orcz"); + assertNotNull(h); + assertEquals("orcz.example.org", h.getHostName()); + } + + @Test + public void testRemoteUserReplacement() throws Exception { + config("Host=orcz\n\tUser foo\n" + "Host *.*\n\tHostname %h\n" + + "Host *\n\tHostname %h.ex%%20ample.org\n\tIdentityFile ~/.ssh/%h_%r_id_dsa"); + final Host h = osc.lookup("orcz"); + assertNotNull(h); + assertEquals( + new File(new File(home, ".ssh"), + "orcz.ex%20ample.org_foo_id_dsa"), + h.getIdentityFile()); + } + + @Test + public void testLocalhostFQDNReplacement() throws Exception { + String localhost = SystemReader.getInstance().getHostname(); + config("Host=orcz\n\tIdentityFile ~/.ssh/%l_id_dsa"); + final Host h = osc.lookup("orcz"); + assertNotNull(h); + assertEquals( + new File(new File(home, ".ssh"), localhost + "_id_dsa"), + h.getIdentityFile()); + } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushConfigTest.java new file mode 100644 index 0000000000..9610fbd6f1 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushConfigTest.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2017, David Pursehouse <david.pursehouse@gmail.com> + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.transport; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.eclipse.jgit.transport.PushConfig.PushRecurseSubmodulesMode; +import org.junit.Test; + +public class PushConfigTest { + @Test + public void pushRecurseSubmoduleMatch() throws Exception { + assertTrue(PushRecurseSubmodulesMode.CHECK.matchConfigValue("check")); + assertTrue(PushRecurseSubmodulesMode.CHECK.matchConfigValue("CHECK")); + + assertTrue(PushRecurseSubmodulesMode.ON_DEMAND + .matchConfigValue("on-demand")); + assertTrue(PushRecurseSubmodulesMode.ON_DEMAND + .matchConfigValue("ON-DEMAND")); + assertTrue(PushRecurseSubmodulesMode.ON_DEMAND + .matchConfigValue("on_demand")); + assertTrue(PushRecurseSubmodulesMode.ON_DEMAND + .matchConfigValue("ON_DEMAND")); + + assertTrue(PushRecurseSubmodulesMode.NO.matchConfigValue("no")); + assertTrue(PushRecurseSubmodulesMode.NO.matchConfigValue("NO")); + assertTrue(PushRecurseSubmodulesMode.NO.matchConfigValue("false")); + assertTrue(PushRecurseSubmodulesMode.NO.matchConfigValue("FALSE")); + } + + @Test + public void pushRecurseSubmoduleNoMatch() throws Exception { + assertFalse(PushRecurseSubmodulesMode.NO.matchConfigValue("N")); + assertFalse(PushRecurseSubmodulesMode.ON_DEMAND + .matchConfigValue("ONDEMAND")); + } + + @Test + public void pushRecurseSubmoduleToConfigValue() { + assertEquals("on-demand", + PushRecurseSubmodulesMode.ON_DEMAND.toConfigValue()); + assertEquals("check", PushRecurseSubmodulesMode.CHECK.toConfigValue()); + assertEquals("false", PushRecurseSubmodulesMode.NO.toConfigValue()); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ReceivePackAdvertiseRefsHookTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ReceivePackAdvertiseRefsHookTest.java index 3411122888..8ef87cb3c1 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ReceivePackAdvertiseRefsHookTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ReceivePackAdvertiseRefsHookTest.java @@ -58,6 +58,8 @@ import java.security.MessageDigest; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; import java.util.zip.Deflater; import org.eclipse.jgit.errors.MissingObjectException; @@ -159,6 +161,45 @@ public class ReceivePackAdvertiseRefsHookTest extends LocalDiskRepositoryTestCas } @Test + public void resetsHaves() throws Exception { + AtomicReference<Set<ObjectId>> haves = new AtomicReference<>(); + try (TransportLocal t = new TransportLocal(src, uriOf(dst), + dst.getDirectory()) { + @Override + ReceivePack createReceivePack(Repository db) { + dst.incrementOpen(); + + ReceivePack rp = super.createReceivePack(dst); + rp.setAdvertiseRefsHook(new AdvertiseRefsHook() { + @Override + public void advertiseRefs(BaseReceivePack rp2) + throws ServiceMayNotContinueException { + rp.setAdvertisedRefs(rp.getRepository().getAllRefs(), + null); + new HidePrivateHook().advertiseRefs(rp); + haves.set(rp.getAdvertisedObjects()); + } + + @Override + public void advertiseRefs(UploadPack uploadPack) + throws ServiceMayNotContinueException { + throw new UnsupportedOperationException(); + } + }); + return rp; + } + }) { + try (PushConnection c = t.openPush()) { + // Just has to open/close for advertisement. + } + } + + assertEquals(1, haves.get().size()); + assertTrue(haves.get().contains(B)); + assertFalse(haves.get().contains(P)); + } + + @Test public void testSuccess() throws Exception { // Manually force a delta of an object so we reuse it later. // diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RemoteConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RemoteConfigTest.java index 0cada5c7ee..a0cf0d2dbc 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RemoteConfigTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RemoteConfigTest.java @@ -51,6 +51,7 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import java.util.Arrays; +import java.util.Collections; import java.util.List; import org.eclipse.jgit.errors.ConfigInvalidException; @@ -498,19 +499,30 @@ public class RemoteConfigTest { } @Test - public void singlePushInsteadOf() throws Exception { + public void pushInsteadOfNotAppliedToPushUri() throws Exception { config.setString("remote", "origin", "pushurl", "short:project.git"); config.setString("url", "https://server/repos/", "pushInsteadOf", "short:"); RemoteConfig rc = new RemoteConfig(config, "origin"); assertFalse(rc.getPushURIs().isEmpty()); - assertEquals("https://server/repos/project.git", rc.getPushURIs() - .get(0).toASCIIString()); + assertEquals("short:project.git", + rc.getPushURIs().get(0).toASCIIString()); + } + + @Test + public void pushInsteadOfAppliedToUri() throws Exception { + config.setString("remote", "origin", "url", "short:project.git"); + config.setString("url", "https://server/repos/", "pushInsteadOf", + "short:"); + RemoteConfig rc = new RemoteConfig(config, "origin"); + assertFalse(rc.getPushURIs().isEmpty()); + assertEquals("https://server/repos/project.git", + rc.getPushURIs().get(0).toASCIIString()); } @Test public void multiplePushInsteadOf() throws Exception { - config.setString("remote", "origin", "pushurl", "prefixproject.git"); + config.setString("remote", "origin", "url", "prefixproject.git"); config.setStringList("url", "https://server/repos/", "pushInsteadOf", Arrays.asList("pre", "prefix", "pref", "perf")); RemoteConfig rc = new RemoteConfig(config, "origin"); @@ -518,4 +530,17 @@ public class RemoteConfigTest { assertEquals("https://server/repos/project.git", rc.getPushURIs() .get(0).toASCIIString()); } + + @Test + public void pushInsteadOfNoPushUrl() throws Exception { + config.setString("remote", "origin", "url", + "http://git.eclipse.org/gitroot/jgit/jgit"); + config.setStringList("url", "ssh://someone@git.eclipse.org:29418/", + "pushInsteadOf", + Collections.singletonList("http://git.eclipse.org/gitroot/")); + RemoteConfig rc = new RemoteConfig(config, "origin"); + assertFalse(rc.getPushURIs().isEmpty()); + assertEquals("ssh://someone@git.eclipse.org:29418/jgit/jgit", + rc.getPushURIs().get(0).toASCIIString()); + } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackTest.java new file mode 100644 index 0000000000..27c7674e9c --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackTest.java @@ -0,0 +1,90 @@ +package org.eclipse.jgit.transport; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Collections; +import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription; +import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; +import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.NullProgressMonitor; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.transport.UploadPack.RequestPolicy; +import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; +import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException; +import org.eclipse.jgit.transport.resolver.UploadPackFactory; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * Tests for server upload-pack utilities. + */ +public class UploadPackTest { + private URIish uri; + + private TestProtocol<Object> testProtocol; + + private Object ctx = new Object(); + + private InMemoryRepository server; + + private InMemoryRepository client; + + private RevCommit commit0; + + private RevCommit commit1; + + private RevCommit tip; + + @Before + public void setUp() throws Exception { + server = newRepo("server"); + client = newRepo("client"); + + TestRepository<InMemoryRepository> remote = + new TestRepository<>(server); + commit0 = remote.commit().message("0").create(); + commit1 = remote.commit().message("1").parent(commit0).create(); + tip = remote.commit().message("2").parent(commit1).create(); + remote.update("master", tip); + } + + @After + public void tearDown() { + Transport.unregister(testProtocol); + } + + private static InMemoryRepository newRepo(String name) { + return new InMemoryRepository(new DfsRepositoryDescription(name)); + } + + @Test + public void testFetchParentOfShallowCommit() throws Exception { + testProtocol = new TestProtocol<>( + new UploadPackFactory<Object>() { + @Override + public UploadPack create(Object req, Repository db) + throws ServiceNotEnabledException, + ServiceNotAuthorizedException { + UploadPack up = new UploadPack(db); + up.setRequestPolicy(RequestPolicy.REACHABLE_COMMIT); + // assume client has a shallow commit + up.getRevWalk().assumeShallow( + Collections.singleton(commit1.getId())); + return up; + } + }, null); + uri = testProtocol.register(ctx, server); + + assertFalse(client.hasObject(commit0.toObjectId())); + + // Fetch of the parent of the shallow commit + try (Transport tn = testProtocol.open(uri, client, "server")) { + tn.fetch(NullProgressMonitor.INSTANCE, + Collections.singletonList(new RefSpec(commit0.name()))); + assertTrue(client.hasObject(commit0.toObjectId())); + } + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/PathFilterLogicTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/PathFilterLogicTest.java index 7c819c5eea..0394f68162 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/PathFilterLogicTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/PathFilterLogicTest.java @@ -42,6 +42,14 @@ */ package org.eclipse.jgit.treewalk.filter; +import static org.junit.Assert.assertEquals; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheBuilder; import org.eclipse.jgit.dircache.DirCacheEntry; @@ -53,14 +61,6 @@ import org.eclipse.jgit.treewalk.TreeWalk; import org.junit.Before; import org.junit.Test; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import static org.junit.Assert.assertEquals; - public class PathFilterLogicTest extends RepositoryTestCase { private ObjectId treeId; diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/IntListTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/IntListTest.java index c6eca9d5e7..d6ea8c604c 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/IntListTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/IntListTest.java @@ -44,6 +44,7 @@ package org.eclipse.jgit.util; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -186,6 +187,16 @@ public class IntListTest { } @Test + public void testContains() { + IntList i = new IntList(); + i.add(1); + i.add(4); + assertTrue(i.contains(1)); + assertTrue(i.contains(4)); + assertFalse(i.contains(2)); + } + + @Test public void testToString() { final IntList i = new IntList(); i.add(1); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/LongMapTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/LongMapTest.java index 1a86aaff33..054c61e2be 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/LongMapTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/LongMapTest.java @@ -41,7 +41,7 @@ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package org.eclipse.jgit.transport; +package org.eclipse.jgit.util; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/NBTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/NBTest.java index 7e11a61035..d2d44ffdc8 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/NBTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/NBTest.java @@ -90,6 +90,24 @@ public class NBTest { } @Test + public void testDecodeUInt24() { + assertEquals(0, NB.decodeUInt24(b(0, 0, 0), 0)); + assertEquals(0, NB.decodeUInt24(padb(3, 0, 0, 0), 3)); + + assertEquals(3, NB.decodeUInt24(b(0, 0, 3), 0)); + assertEquals(3, NB.decodeUInt24(padb(3, 0, 0, 3), 3)); + + assertEquals(0xcede03, NB.decodeUInt24(b(0xce, 0xde, 3), 0)); + assertEquals(0xbade03, NB.decodeUInt24(padb(3, 0xba, 0xde, 3), 3)); + + assertEquals(0x03bade, NB.decodeUInt24(b(3, 0xba, 0xde), 0)); + assertEquals(0x03bade, NB.decodeUInt24(padb(3, 3, 0xba, 0xde), 3)); + + assertEquals(0xffffff, NB.decodeUInt24(b(0xff, 0xff, 0xff), 0)); + assertEquals(0xffffff, NB.decodeUInt24(padb(3, 0xff, 0xff, 0xff), 3)); + } + + @Test public void testDecodeInt32() { assertEquals(0, NB.decodeInt32(b(0, 0, 0, 0), 0)); assertEquals(0, NB.decodeInt32(padb(3, 0, 0, 0, 0), 3)); @@ -198,6 +216,39 @@ public class NBTest { } @Test + public void testEncodeInt24() { + byte[] out = new byte[16]; + + prepareOutput(out); + NB.encodeInt24(out, 0, 0); + assertOutput(b(0, 0, 0), out, 0); + + prepareOutput(out); + NB.encodeInt24(out, 3, 0); + assertOutput(b(0, 0, 0), out, 3); + + prepareOutput(out); + NB.encodeInt24(out, 0, 3); + assertOutput(b(0, 0, 3), out, 0); + + prepareOutput(out); + NB.encodeInt24(out, 3, 3); + assertOutput(b(0, 0, 3), out, 3); + + prepareOutput(out); + NB.encodeInt24(out, 0, 0xc0deac); + assertOutput(b(0xc0, 0xde, 0xac), out, 0); + + prepareOutput(out); + NB.encodeInt24(out, 3, 0xbadeac); + assertOutput(b(0xba, 0xde, 0xac), out, 3); + + prepareOutput(out); + NB.encodeInt24(out, 3, -1); + assertOutput(b(0xff, 0xff, 0xff), out, 3); + } + + @Test public void testEncodeInt32() { final byte[] out = new byte[16]; @@ -315,10 +366,24 @@ public class NBTest { return r; } + private static byte[] b(int a, int b, int c) { + return new byte[] { (byte) a, (byte) b, (byte) c }; + } + private static byte[] b(final int a, final int b, final int c, final int d) { return new byte[] { (byte) a, (byte) b, (byte) c, (byte) d }; } + private static byte[] padb(int len, int a, int b, int c) { + final byte[] r = new byte[len + 4]; + for (int i = 0; i < len; i++) + r[i] = (byte) 0xaf; + r[len] = (byte) a; + r[len + 1] = (byte) b; + r[len + 2] = (byte) c; + return r; + } + private static byte[] padb(final int len, final int a, final int b, final int c, final int d) { final byte[] r = new byte[len + 4]; diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RawParseUtils_LineMapTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RawParseUtils_LineMapTest.java index 593971478d..6efdce6d77 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RawParseUtils_LineMapTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RawParseUtils_LineMapTest.java @@ -43,7 +43,7 @@ package org.eclipse.jgit.util; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertNotNull; import java.io.UnsupportedEncodingException; @@ -55,52 +55,51 @@ public class RawParseUtils_LineMapTest { public void testEmpty() { final IntList map = RawParseUtils.lineMap(new byte[] {}, 0, 0); assertNotNull(map); - assertEquals(2, map.size()); - assertEquals(Integer.MIN_VALUE, map.get(0)); - assertEquals(0, map.get(1)); + assertArrayEquals(new int[]{Integer.MIN_VALUE, 0}, asInts(map)); } @Test public void testOneBlankLine() { final IntList map = RawParseUtils.lineMap(new byte[] { '\n' }, 0, 1); - assertEquals(3, map.size()); - assertEquals(Integer.MIN_VALUE, map.get(0)); - assertEquals(0, map.get(1)); - assertEquals(1, map.get(2)); + assertArrayEquals(new int[]{Integer.MIN_VALUE, 0, 1}, asInts(map)); } @Test public void testTwoLineFooBar() throws UnsupportedEncodingException { final byte[] buf = "foo\nbar\n".getBytes("ISO-8859-1"); final IntList map = RawParseUtils.lineMap(buf, 0, buf.length); - assertEquals(4, map.size()); - assertEquals(Integer.MIN_VALUE, map.get(0)); - assertEquals(0, map.get(1)); - assertEquals(4, map.get(2)); - assertEquals(buf.length, map.get(3)); + assertArrayEquals(new int[]{Integer.MIN_VALUE, 0, 4, buf.length}, asInts(map)); } @Test public void testTwoLineNoLF() throws UnsupportedEncodingException { final byte[] buf = "foo\nbar".getBytes("ISO-8859-1"); final IntList map = RawParseUtils.lineMap(buf, 0, buf.length); - assertEquals(4, map.size()); - assertEquals(Integer.MIN_VALUE, map.get(0)); - assertEquals(0, map.get(1)); - assertEquals(4, map.get(2)); - assertEquals(buf.length, map.get(3)); + assertArrayEquals(new int[]{Integer.MIN_VALUE, 0, 4, buf.length}, asInts(map)); + } + + @Test + public void testBinary() throws UnsupportedEncodingException { + final byte[] buf = "xxxfoo\nb\0ar".getBytes("ISO-8859-1"); + final IntList map = RawParseUtils.lineMap(buf, 3, buf.length); + assertArrayEquals(new int[]{Integer.MIN_VALUE, 3, buf.length}, asInts(map)); } @Test public void testFourLineBlanks() throws UnsupportedEncodingException { final byte[] buf = "foo\n\n\nbar\n".getBytes("ISO-8859-1"); final IntList map = RawParseUtils.lineMap(buf, 0, buf.length); - assertEquals(6, map.size()); - assertEquals(Integer.MIN_VALUE, map.get(0)); - assertEquals(0, map.get(1)); - assertEquals(4, map.get(2)); - assertEquals(5, map.get(3)); - assertEquals(6, map.get(4)); - assertEquals(buf.length, map.get(5)); + + assertArrayEquals(new int[]{ + Integer.MIN_VALUE, 0, 4, 5, 6, buf.length + }, asInts(map)); + } + + private int[] asInts(IntList l) { + int[] result = new int[l.size()]; + for (int i = 0; i < l.size(); i++) { + result[i] = l.get(i); + } + return result; } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RelativeDateFormatterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RelativeDateFormatterTest.java index 76687d15a5..fb76ec4e59 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RelativeDateFormatterTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/RelativeDateFormatterTest.java @@ -128,7 +128,14 @@ public class RelativeDateFormatterTest { assertFormat(380, DAY_IN_MILLIS, "1 year, 1 month ago"); assertFormat(410, DAY_IN_MILLIS, "1 year, 2 months ago"); assertFormat(2, YEAR_IN_MILLIS, "2 years ago"); - assertFormat(1824, DAY_IN_MILLIS, "4 years, 12 months ago"); + } + + @Test + public void testFullYearMissingSomeDays() { + // avoid "x year(s), 12 months", as humans would always round this up to + // "x+1 years" + assertFormat(5 * 365 + 1, DAY_IN_MILLIS, "5 years ago"); + assertFormat(2 * 365 - 10, DAY_IN_MILLIS, "2 years ago"); } @Test |