diff options
Diffstat (limited to 'org.eclipse.jgit.test/tst')
17 files changed, 1639 insertions, 256 deletions
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ApplyCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ApplyCommandTest.java index 1dd329a9e7..714a54c90c 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ApplyCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ApplyCommandTest.java @@ -111,6 +111,16 @@ public class ApplyCommandTest extends RepositoryTestCase { } @Test + public void testAddA3() throws Exception { + ApplyResult result = init("A3", false, true); + assertEquals(1, result.getUpdatedFiles().size()); + assertEquals(new File(db.getWorkTree(), "A3"), + result.getUpdatedFiles().get(0)); + checkFile(new File(db.getWorkTree(), "A3"), + b.getString(0, b.size(), false)); + } + + @Test public void testAddA1Sub() throws Exception { ApplyResult result = init("A1_sub", false, false); assertEquals(1, result.getUpdatedFiles().size()); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java index dd7230bdbf..563b32dab8 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java @@ -194,6 +194,42 @@ public class ResetCommandTest extends RepositoryTestCase { } @Test + public void testHardResetWithConflicts_CreateFolder_UnstagedChanges() throws Exception { + setupRepository(); + + writeTrashFile("dir-or-file/c.txt", "content"); + git.add().addFilepattern("dir-or-file/c.txt").call(); + git.commit().setMessage("adding dir-or-file/c.txt").call(); + + FileUtils.delete(new File(db.getWorkTree(), "dir-or-file"), FileUtils.RECURSIVE); + writeTrashFile("dir-or-file", "content"); + + // bug 479266: cannot create folder "dir-or-file" + git.reset().setMode(ResetType.HARD).setRef(Constants.HEAD).call(); + assertTrue(new File(db.getWorkTree(), "dir-or-file/c.txt").exists()); + } + + @Test + public void testHardResetWithConflicts_DeleteFolder_UnstagedChanges() throws Exception { + setupRepository(); + ObjectId prevHead = db.resolve(Constants.HEAD); + + writeTrashFile("dir-or-file/c.txt", "content"); + git.add().addFilepattern("dir-or-file/c.txt").call(); + git.commit().setMessage("adding dir-or-file/c.txt").call(); + + writeTrashFile("dir-or-file-2/d.txt", "content"); + git.add().addFilepattern("dir-or-file-2/d.txt").call(); + FileUtils.delete(new File(db.getWorkTree(), "dir-or-file-2"), FileUtils.RECURSIVE); + writeTrashFile("dir-or-file-2", "content"); + + // bug 479266: cannot delete folder "dir-or-file" + git.reset().setMode(ResetType.HARD).setRef(prevHead.getName()).call(); + assertFalse(new File(db.getWorkTree(), "dir-or-file").exists()); + assertFalse(new File(db.getWorkTree(), "dir-or-file-2").exists()); + } + + @Test public void testResetToNonexistingHEAD() throws JGitInternalException, AmbiguousObjectException, IOException, GitAPIException { diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/SecurityManagerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/SecurityManagerTest.java new file mode 100644 index 0000000000..62d3530338 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/SecurityManagerTest.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2019 Nail Samatov <sanail@yandex.ru> + * 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.api; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.FilePermission; +import java.io.IOException; +import java.lang.reflect.ReflectPermission; +import java.nio.file.Files; +import java.security.Permission; +import java.security.SecurityPermission; +import java.util.ArrayList; +import java.util.List; +import java.util.PropertyPermission; +import java.util.logging.LoggingPermission; + +import javax.security.auth.AuthPermission; + +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.junit.JGitTestUtil; +import org.eclipse.jgit.junit.MockSystemReader; +import org.eclipse.jgit.junit.SeparateClassloaderTestRunner; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.util.FileUtils; +import org.eclipse.jgit.util.SystemReader; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * <p> + * Tests if jgit works if SecurityManager is enabled. + * </p> + * + * <p> + * Note: JGit's classes shouldn't be used before SecurityManager is configured. + * If you use some JGit's class before SecurityManager is replaced then part of + * the code can be invoked outside of our custom SecurityManager and this test + * becomes useless. + * </p> + * + * <p> + * For example the class {@link org.eclipse.jgit.util.FS} is used widely in jgit + * sources. It contains DETECTED static field. At the first usage of the class + * FS the field DETECTED is initialized and during initialization many system + * operations that SecurityManager can forbid are invoked. + * </p> + * + * <p> + * For this reason this test doesn't extend LocalDiskRepositoryTestCase (it uses + * JGit's classes in setUp() method) and other JGit's utility classes. It's done + * to affect SecurityManager as less as possible. + * </p> + * + * <p> + * We use SeparateClassloaderTestRunner to isolate FS.DETECTED field + * initialization between different tests run. + * </p> + */ +@RunWith(SeparateClassloaderTestRunner.class) +public class SecurityManagerTest { + private File root; + + private SecurityManager originalSecurityManager; + + private List<Permission> permissions = new ArrayList<>(); + + @Before + public void setUp() throws Exception { + // Create working directory + SystemReader.setInstance(new MockSystemReader()); + root = Files.createTempDirectory("jgit-security").toFile(); + + // Add system permissions + permissions.add(new RuntimePermission("*")); + permissions.add(new SecurityPermission("*")); + permissions.add(new AuthPermission("*")); + permissions.add(new ReflectPermission("*")); + permissions.add(new PropertyPermission("*", "read,write")); + permissions.add(new LoggingPermission("control", null)); + + permissions.add(new FilePermission( + System.getProperty("java.home") + "/-", "read")); + + String tempDir = System.getProperty("java.io.tmpdir"); + permissions.add(new FilePermission(tempDir, "read,write,delete")); + permissions + .add(new FilePermission(tempDir + "/-", "read,write,delete")); + + // Add permissions to dependent jar files. + String classPath = System.getProperty("java.class.path"); + if (classPath != null) { + for (String path : classPath.split(File.pathSeparator)) { + permissions.add(new FilePermission(path, "read")); + } + } + // Add permissions to jgit class files. + String jgitSourcesRoot = new File(System.getProperty("user.dir")) + .getParent(); + permissions.add(new FilePermission(jgitSourcesRoot + "/-", "read")); + + // Add permissions to working dir for jgit. Our git repositories will be + // initialized and cloned here. + permissions.add(new FilePermission(root.getPath() + "/-", + "read,write,delete,execute")); + + // Replace Security Manager + originalSecurityManager = System.getSecurityManager(); + System.setSecurityManager(new SecurityManager() { + + @Override + public void checkPermission(Permission requested) { + for (Permission permission : permissions) { + if (permission.implies(requested)) { + return; + } + } + + super.checkPermission(requested); + } + }); + } + + @After + public void tearDown() throws Exception { + System.setSecurityManager(originalSecurityManager); + + // Note: don't use this method before security manager is replaced in + // setUp() method. The method uses FS.DETECTED internally and can affect + // the test. + FileUtils.delete(root, FileUtils.RECURSIVE | FileUtils.RETRY); + } + + @Test + public void testInitAndClone() throws IOException, GitAPIException { + File remote = new File(root, "remote"); + File local = new File(root, "local"); + + try (Git git = Git.init().setDirectory(remote).call()) { + JGitTestUtil.write(new File(remote, "hello.txt"), "Hello world!"); + git.add().addFilepattern(".").call(); + git.commit().setMessage("Initial commit").call(); + } + + try (Git git = Git.cloneRepository().setURI(remote.toURI().toString()) + .setDirectory(local).call()) { + assertTrue(new File(local, ".git").exists()); + + JGitTestUtil.write(new File(local, "hi.txt"), "Hi!"); + git.add().addFilepattern(".").call(); + RevCommit commit1 = git.commit().setMessage("Commit on local repo") + .call(); + assertEquals("Commit on local repo", commit1.getFullMessage()); + assertNotNull(TreeWalk.forPath(git.getRepository(), "hello.txt", + commit1.getTree())); + } + + } + +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/AlternatesTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/AlternatesTest.java index b09db03ea7..2306e0bbb1 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/AlternatesTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/AlternatesTest.java @@ -42,6 +42,7 @@ */ package org.eclipse.jgit.internal.storage.file; +import static org.eclipse.jgit.lib.Constants.INFO_ALTERNATES; import static org.junit.Assert.assertTrue; import java.io.File; @@ -76,7 +77,7 @@ public class AlternatesTest extends SampleDataRepositoryTestCase { private void setAlternate(FileRepository from, FileRepository to) throws IOException { File alt = new File(from.getObjectDatabase().getDirectory(), - "info/alternates"); + INFO_ALTERNATES); alt.getParentFile().mkdirs(); File fromDir = from.getObjectDatabase().getDirectory(); File toDir = to.getObjectDatabase().getDirectory(); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java index 5d0a7e2a0b..1e2341b6a5 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java @@ -44,6 +44,7 @@ package org.eclipse.jgit.internal.storage.file; import static org.eclipse.jgit.internal.storage.pack.PackWriter.NONE; +import static org.eclipse.jgit.lib.Constants.INFO_ALTERNATES; import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -138,7 +139,7 @@ public class PackWriterTest extends SampleDataRepositoryTestCase { config = new PackConfig(db); dst = createBareRepository(); - File alt = new File(dst.getObjectDatabase().getDirectory(), "info/alternates"); + File alt = new File(dst.getObjectDatabase().getDirectory(), INFO_ALTERNATES); alt.getParentFile().mkdirs(); write(alt, db.getObjectDatabase().getDirectory().getAbsolutePath() + "\n"); } 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 e2887d9053..43f30d8ca8 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 @@ -98,7 +98,7 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase { public void test001_Initalize() { final File gitdir = new File(trash, Constants.DOT_GIT); final File hooks = new File(gitdir, "hooks"); - final File objects = new File(gitdir, "objects"); + final File objects = new File(gitdir, Constants.OBJECTS); final File objects_pack = new File(objects, "pack"); final File objects_info = new File(objects, "info"); final File refs = new File(gitdir, "refs"); @@ -150,7 +150,7 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase { assertEqualsPath(theDir, r.getDirectory()); assertEqualsPath(repo1Parent, r.getWorkTree()); assertEqualsPath(new File(theDir, "index"), r.getIndexFile()); - assertEqualsPath(new File(theDir, "objects"), r.getObjectDatabase() + assertEqualsPath(new File(theDir, Constants.OBJECTS), r.getObjectDatabase() .getDirectory()); } @@ -176,7 +176,7 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase { assertEqualsPath(theDir, r.getDirectory()); assertEqualsPath(repo1Parent.getParentFile(), r.getWorkTree()); assertEqualsPath(new File(theDir, "index"), r.getIndexFile()); - assertEqualsPath(new File(theDir, "objects"), r.getObjectDatabase() + assertEqualsPath(new File(theDir, Constants.OBJECTS), r.getObjectDatabase() .getDirectory()); } @@ -200,7 +200,7 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase { assertEqualsPath(theDir, r.getDirectory()); assertEqualsPath(repo1Parent, r.getWorkTree()); assertEqualsPath(new File(theDir, "index"), r.getIndexFile()); - assertEqualsPath(new File(theDir, "objects"), r.getObjectDatabase() + assertEqualsPath(new File(theDir, Constants.OBJECTS), r.getObjectDatabase() .getDirectory()); } @@ -229,7 +229,7 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase { assertEqualsPath(theDir, r.getDirectory()); assertEqualsPath(workdir, r.getWorkTree()); assertEqualsPath(new File(theDir, "index"), r.getIndexFile()); - assertEqualsPath(new File(theDir, "objects"), r.getObjectDatabase() + assertEqualsPath(new File(theDir, Constants.OBJECTS), r.getObjectDatabase() .getDirectory()); } @@ -258,7 +258,7 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase { assertEqualsPath(theDir, r.getDirectory()); assertEqualsPath(workdir, r.getWorkTree()); assertEqualsPath(new File(theDir, "index"), r.getIndexFile()); - assertEqualsPath(new File(theDir, "objects"), r.getObjectDatabase() + assertEqualsPath(new File(theDir, Constants.OBJECTS), r.getObjectDatabase() .getDirectory()); } @@ -314,7 +314,7 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase { } final File o = new File(new File(new File(newdb.getDirectory(), - "objects"), "4b"), "825dc642cb6eb9a060e54bf8d69288fbee4904"); + Constants.OBJECTS), "4b"), "825dc642cb6eb9a060e54bf8d69288fbee4904"); assertTrue("Exists " + o, o.isFile()); assertTrue("Read-only " + o, !o.canWrite()); } @@ -326,7 +326,7 @@ public class T0003_BasicTest extends SampleDataRepositoryTestCase { final ObjectId treeId = insertTree(new TreeFormatter()); assertEquals("4b825dc642cb6eb9a060e54bf8d69288fbee4904", treeId.name()); final File o = new File(new File( - new File(db.getDirectory(), "objects"), "4b"), + new File(db.getDirectory(), Constants.OBJECTS), "4b"), "825dc642cb6eb9a060e54bf8d69288fbee4904"); assertFalse("Exists " + o, o.isFile()); } 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 index a142166983..0e33fa6e76 100644 --- 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 @@ -46,13 +46,16 @@ 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.MoreAsserts.assertThrows; import static org.eclipse.jgit.lib.Ref.Storage.NEW; import static org.eclipse.jgit.lib.Ref.Storage.PACKED; +import static org.hamcrest.CoreMatchers.containsString; 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 static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -415,6 +418,54 @@ public class ReftableTest { } @Test + public void invalidRefWriteOrder() throws IOException { + Ref master = ref(MASTER, 1); + Ref next = ref(NEXT, 2); + ReftableWriter writer = new ReftableWriter() + .setMinUpdateIndex(1) + .setMaxUpdateIndex(1) + .begin(new ByteArrayOutputStream()); + + writer.writeRef(next); + IllegalArgumentException e = assertThrows( + IllegalArgumentException.class, + () -> writer.writeRef(master)); + assertThat(e.getMessage(), containsString("records must be increasing")); + } + + @Test + public void invalidReflogWriteOrderUpdateIndex() throws IOException { + ReftableWriter writer = new ReftableWriter() + .setMinUpdateIndex(1) + .setMaxUpdateIndex(2) + .begin(new ByteArrayOutputStream()); + PersonIdent who = new PersonIdent("Log", "Ger", 1500079709, -8 * 60); + String msg = "test"; + + writer.writeLog(MASTER, 1, who, ObjectId.zeroId(), id(1), msg); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> writer.writeLog( + MASTER, 2, who, ObjectId.zeroId(), id(2), msg)); + assertThat(e.getMessage(), containsString("records must be increasing")); + } + + @Test + public void invalidReflogWriteOrderName() throws IOException { + ReftableWriter writer = new ReftableWriter() + .setMinUpdateIndex(1) + .setMaxUpdateIndex(1) + .begin(new ByteArrayOutputStream()); + PersonIdent who = new PersonIdent("Log", "Ger", 1500079709, -8 * 60); + String msg = "test"; + + writer.writeLog(NEXT, 1, who, ObjectId.zeroId(), id(1), msg); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, + () -> writer.writeLog( + MASTER, 1, who, ObjectId.zeroId(), id(2), msg)); + assertThat(e.getMessage(), containsString("records must be increasing")); + } + + @Test public void withReflog() throws IOException { Ref master = ref(MASTER, 1); Ref next = ref(NEXT, 2); @@ -472,6 +523,74 @@ public class ReftableTest { } @Test + public void reflogSeek() throws IOException { + PersonIdent who = new PersonIdent("Log", "Ger", 1500079709, -8 * 60); + String msg = "test"; + String msgNext = "test next"; + + 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), msgNext); + + writer.finish(); + byte[] table = buffer.toByteArray(); + + ReftableReader t = read(table); + try (LogCursor c = t.seekLog(MASTER, Long.MAX_VALUE)) { + assertTrue(c.next()); + assertEquals(c.getReflogEntry().getComment(), msg); + } + try (LogCursor c = t.seekLog(MASTER, 0)) { + assertFalse(c.next()); + } + try (LogCursor c = t.seekLog(MASTER, 1)) { + assertTrue(c.next()); + assertEquals(c.getUpdateIndex(), 1); + assertEquals(c.getReflogEntry().getComment(), msg); + } + try (LogCursor c = t.seekLog(NEXT, Long.MAX_VALUE)) { + assertTrue(c.next()); + assertEquals(c.getReflogEntry().getComment(), msgNext); + } + try (LogCursor c = t.seekLog(NEXT, 0)) { + assertFalse(c.next()); + } + try (LogCursor c = t.seekLog(NEXT, 1)) { + assertTrue(c.next()); + assertEquals(c.getUpdateIndex(), 1); + assertEquals(c.getReflogEntry().getComment(), msgNext); + } + } + + @Test + public void reflogSeekPrefix() throws IOException { + PersonIdent who = new PersonIdent("Log", "Ger", 1500079709, -8 * 60); + + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + ReftableWriter writer = new ReftableWriter() + .setMinUpdateIndex(1) + .setMaxUpdateIndex(1) + .begin(buffer); + + writer.writeLog("branchname", 1, who, ObjectId.zeroId(), id(1), "branchname"); + + writer.finish(); + byte[] table = buffer.toByteArray(); + + ReftableReader t = read(table); + try (LogCursor c = t.seekLog("branch", Long.MAX_VALUE)) { + // We find a reflog block, but the iteration won't confuse branchname + // and branch. + assertFalse(c.next()); + } + } + + @Test public void onlyReflog() throws IOException { PersonIdent who = new PersonIdent("Log", "Ger", 1500079709, -8 * 60); String msg = "test"; @@ -505,6 +624,8 @@ public class ReftableTest { assertEquals(ObjectId.zeroId(), lc.getReflogEntry().getOldId()); assertEquals(id(1), lc.getReflogEntry().getNewId()); assertEquals(who, lc.getReflogEntry().getWho()); + // compare string too, to catch tz differences. + assertEquals(who.toExternalString(), lc.getReflogEntry().getWho().toExternalString()); assertEquals(msg, lc.getReflogEntry().getComment()); assertTrue(lc.next()); @@ -532,7 +653,7 @@ public class ReftableTest { List<Ref> refs = new ArrayList<>(); for (int i = 1; i <= 5670; i++) { - Ref ref = ref(String.format("refs/heads/%03d", i), i); + Ref ref = ref(String.format("refs/heads/%04d", i), i); refs.add(ref); writer.writeRef(ref); } 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 483051ceeb..8092c3134b 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 @@ -1977,6 +1977,29 @@ public class DirCacheCheckoutTest extends RepositoryTestCase { } } + @Test + public void testCheckoutWithEmptyIndexDoesntOverwrite() throws Exception { + try (Git git = new Git(db); + TestRepository<Repository> db_t = new TestRepository<>(db)) { + // prepare the commits + BranchBuilder master = db_t.branch("master"); + RevCommit mergeCommit = master.commit() + .add("p/x", "headContent") + .message("m0").create(); + master.commit().add("p/x", "headContent").message("m1").create(); + git.checkout().setName("master").call(); + + // empty index and write unsaved data in 'p' + git.rm().addFilepattern("p").call(); + writeTrashFile("p", "important data"); + + git.checkout().setName(mergeCommit.getName()).call(); + + assertEquals("", indexState(CONTENT)); + assertEquals("important data", read("p")); + } + } + private static class TestFileTreeIterator extends FileTreeIterator { // For assertions only diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RebaseTodoFileTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RebaseTodoFileTest.java index 5cfc75ca93..3d3c697107 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RebaseTodoFileTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RebaseTodoFileTest.java @@ -68,7 +68,7 @@ public class RebaseTodoFileTest extends RepositoryTestCase { @Test public void testReadTodoFile() throws Exception { String[] expected = { "reword " + ObjectId.zeroId().name() + " Foo", - "# A comment in the the todo list", + "# A comment in the todo list", "pick " + ObjectId.zeroId().name() + " Foo fie", "squash " + ObjectId.zeroId().name() + " F", "fixup " + ObjectId.zeroId().name(), @@ -93,7 +93,7 @@ public class RebaseTodoFileTest extends RepositoryTestCase { assertEquals("Expected COMMENT", RebaseTodoLine.Action.COMMENT, line.getAction()); assertEquals("Unexpected Message", - "# A comment in the the todo list", + "# A comment in the todo list", line.getComment()); break; case 2: diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/FirstParentRevWalkTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/FirstParentRevWalkTest.java new file mode 100644 index 0000000000..1fc7a55457 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/FirstParentRevWalkTest.java @@ -0,0 +1,428 @@ +/* + * Copyright (C) 2019, Google LLC. + * 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.revwalk; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.revwalk.filter.MessageRevFilter; +import org.eclipse.jgit.revwalk.filter.RevFilter; +import org.junit.Test; + +public class FirstParentRevWalkTest extends RevWalkTestCase { + @Test + public void testStringOfPearls() throws Exception { + RevCommit a = commit(); + RevCommit b = commit(a); + RevCommit c = commit(b); + + rw.reset(); + rw.setFirstParent(true); + markStart(c); + assertCommit(c, rw.next()); + assertCommit(b, rw.next()); + assertCommit(a, rw.next()); + assertNull(rw.next()); + } + + @Test + public void testSideBranch() throws Exception { + RevCommit a = commit(); + RevCommit b1 = commit(a); + RevCommit b2 = commit(a); + RevCommit c1 = commit(b1); + RevCommit c2 = commit(b2); + RevCommit d = commit(c1, c2); + + rw.reset(); + rw.setFirstParent(true); + markStart(d); + assertCommit(d, rw.next()); + assertCommit(c1, rw.next()); + assertCommit(b1, rw.next()); + assertCommit(a, rw.next()); + assertNull(rw.next()); + } + + @Test + public void testSecondParentAncestorOfFirstParent() throws Exception { + RevCommit a = commit(); + RevCommit b = commit(a); + RevCommit c = commit(b, a); + + rw.reset(); + rw.setFirstParent(true); + markStart(c); + assertCommit(c, rw.next()); + assertCommit(b, rw.next()); + assertCommit(a, rw.next()); + assertNull(rw.next()); + } + + @Test + public void testFirstParentMultipleOccurrences() throws Exception { + RevCommit a = commit(); + RevCommit b = commit(a); + RevCommit c = commit(b); + RevCommit d = commit(b); + + rw.reset(); + rw.setFirstParent(true); + markStart(c); + markStart(d); + assertCommit(d, rw.next()); + assertCommit(c, rw.next()); + assertCommit(b, rw.next()); + assertCommit(a, rw.next()); + assertNull(rw.next()); + } + + @Test + public void testReachableAlongFirstAndLaterParents() throws Exception { + RevCommit a = commit(); + RevCommit b1 = commit(a); + RevCommit b2 = commit(a); + RevCommit b3 = commit(a); + RevCommit c = commit(b1, b2); + RevCommit d = commit(b2, b3); + + rw.reset(); + rw.setFirstParent(true); + markStart(c); + markStart(d); + assertCommit(d, rw.next()); + assertCommit(c, rw.next()); + // b3 is only reachable from c's second parent. + // b2 is reachable from c's second parent but d's first parent. + assertCommit(b2, rw.next()); + assertCommit(b1, rw.next()); + assertCommit(a, rw.next()); + assertNull(rw.next()); + } + + @Test + public void testStartCommitReachableOnlyFromLaterParents() + throws Exception { + RevCommit a = commit(); + RevCommit b1 = commit(a); + RevCommit b2 = commit(a); + RevCommit c = commit(b1, b2); + + rw.reset(); + rw.setFirstParent(true); + markStart(c); + markStart(b2); + assertCommit(c, rw.next()); + // b2 is only reachable from second parent, but is itself a start + // commit. + assertCommit(b2, rw.next()); + assertCommit(b1, rw.next()); + assertCommit(a, rw.next()); + assertNull(rw.next()); + } + + @Test + public void testRevFilter() throws Exception { + RevCommit a = commit(); + RevCommit b1 = commitBuilder().parent(a).message("commit b1").create(); + RevCommit b2 = commitBuilder().parent(a).message("commit b2").create(); + RevCommit c = commit(b1, b2); + + rw.reset(); + rw.setFirstParent(true); + rw.setRevFilter(MessageRevFilter.create("commit b")); + rw.markStart(c); + assertCommit(b1, rw.next()); + assertNull(rw.next()); + } + + @Test + public void testTopoSort() throws Exception { + RevCommit a = commit(); + RevCommit b1 = commit(a); + RevCommit b2 = commit(a); + RevCommit c = commit(b1, b2); + + rw.reset(); + rw.sort(RevSort.TOPO); + rw.setFirstParent(true); + markStart(c); + assertCommit(c, rw.next()); + assertCommit(b1, rw.next()); + assertCommit(a, rw.next()); + assertNull(rw.next()); + } + + @Test + public void testCommitTimeSort() throws Exception { + RevCommit a = commit(); + RevCommit b1 = commit(a); + RevCommit b2 = commit(a); + RevCommit c = commit(b1, b2); + + rw.reset(); + rw.sort(RevSort.COMMIT_TIME_DESC); + rw.setFirstParent(true); + markStart(c); + assertCommit(c, rw.next()); + assertCommit(b1, rw.next()); + assertCommit(a, rw.next()); + assertNull(rw.next()); + } + + @Test + public void testReverseSort() throws Exception { + RevCommit a = commit(); + RevCommit b1 = commit(a); + RevCommit b2 = commit(a); + RevCommit c = commit(b1, b2); + + rw.reset(); + rw.sort(RevSort.REVERSE); + rw.setFirstParent(true); + markStart(c); + assertCommit(a, rw.next()); + assertCommit(b1, rw.next()); + assertCommit(c, rw.next()); + assertNull(rw.next()); + } + + @Test + public void testBoundarySort() throws Exception { + RevCommit a = commit(); + RevCommit b = commit(a); + RevCommit c1 = commit(b); + RevCommit c2 = commit(b); + RevCommit d = commit(c1, c2); + + rw.reset(); + rw.sort(RevSort.BOUNDARY); + rw.setFirstParent(true); + markStart(d); + markUninteresting(a); + assertCommit(d, rw.next()); + assertCommit(c1, rw.next()); + assertCommit(b, rw.next()); + assertCommit(a, rw.next()); + assertNull(rw.next()); + } + + @Test + public void testFirstParentOfFirstParentMarkedUninteresting() + throws Exception { + RevCommit a = commit(); + RevCommit b1 = commit(a); + RevCommit b2 = commit(a); + RevCommit c1 = commit(b1); + RevCommit c2 = commit(b2); + RevCommit d = commit(c1, c2); + + rw.reset(); + rw.setFirstParent(true); + markStart(d); + markUninteresting(b1); + assertCommit(d, rw.next()); + assertCommit(c1, rw.next()); + assertNull(rw.next()); + } + + @Test + public void testUnparsedFirstParentOfFirstParentMarkedUninteresting() + throws Exception { + ObjectId a = unparsedCommit(); + ObjectId b1 = unparsedCommit(a); + ObjectId b2 = unparsedCommit(a); + ObjectId c1 = unparsedCommit(b1); + ObjectId c2 = unparsedCommit(b2); + ObjectId d = unparsedCommit(c1, c2); + + rw.reset(); + rw.setFirstParent(true); + RevCommit parsedD = rw.parseCommit(d); + markStart(parsedD); + markUninteresting(rw.parseCommit(b1)); + assertCommit(parsedD, rw.next()); + assertCommit(rw.parseCommit(c1), rw.next()); + assertNull(rw.next()); + } + + @Test + public void testFirstParentMarkedUninteresting() throws Exception { + RevCommit a = commit(); + RevCommit b1 = commit(a); + RevCommit b2 = commit(a); + RevCommit c = commit(b1, b2); + + rw.reset(); + rw.setFirstParent(true); + markStart(c); + markUninteresting(b1); + assertCommit(c, rw.next()); + assertNull(rw.next()); + } + + @Test + public void testUnparsedFirstParentMarkedUninteresting() throws Exception { + ObjectId a = unparsedCommit(); + ObjectId b1 = unparsedCommit(a); + ObjectId b2 = unparsedCommit(a); + ObjectId c = unparsedCommit(b1, b2); + + rw.reset(); + rw.setFirstParent(true); + RevCommit parsedC = rw.parseCommit(c); + markStart(parsedC); + markUninteresting(rw.parseCommit(b1)); + assertCommit(parsedC, rw.next()); + assertNull(rw.next()); + } + + @Test + public void testUninterestingCommitWithTwoParents() throws Exception { + RevCommit a = commit(); + RevCommit b = commit(a); + RevCommit c1 = commit(b); + RevCommit c2 = commit(b); + RevCommit d = commit(c1); + RevCommit e = commit(c1, c2); + + RevCommit uA = commit(a, b); + RevCommit uB1 = commit(uA, c2); + RevCommit uB2 = commit(uA, d); + RevCommit uninteresting = commit(uB1, uB2); + + rw.reset(); + rw.setFirstParent(true); + markStart(e); + markUninteresting(uninteresting); + + assertCommit(e, rw.next()); + assertNull(rw.next()); + } + + /** + * This fails if we try to propagate flags before parsing commits. + * + * @throws Exception + */ + @Test + public void testUnparsedUninterestingCommitWithTwoParents() + throws Exception { + ObjectId a = unparsedCommit(); + ObjectId b = unparsedCommit(a); + ObjectId c1 = unparsedCommit(b); + ObjectId c2 = unparsedCommit(b); + ObjectId d = unparsedCommit(c1); + ObjectId e = unparsedCommit(c1, c2); + + ObjectId uA = unparsedCommit(a, b); + ObjectId uB1 = unparsedCommit(uA, c2); + ObjectId uB2 = unparsedCommit(uA, d); + ObjectId uninteresting = unparsedCommit(uB1, uB2); + + rw.reset(); + rw.setFirstParent(true); + RevCommit parsedE = rw.parseCommit(e); + markStart(parsedE); + markUninteresting(rw.parseCommit(uninteresting)); + + assertCommit(parsedE, rw.next()); + assertNull(rw.next()); + } + + @Test + public void testDepthWalk() throws Exception { + RevCommit a = commit(); + RevCommit b1 = commit(a); + RevCommit b2 = commit(a); + RevCommit c = commit(b1, b2); + + try (DepthWalk.RevWalk dw = new DepthWalk.RevWalk(db, 1)) { + dw.setFirstParent(true); + dw.markRoot(dw.parseCommit(c)); + dw.markStart(dw.parseCommit(c)); + assertEquals(c, dw.next()); + assertEquals(b1, dw.next()); + assertNull(dw.next()); + } + } + + @Test + public void testDoNotRewriteParents() throws Exception { + RevCommit a = commit(); + RevCommit b1 = commit(a); + RevCommit b2 = commit(a); + RevCommit c = commit(b1, b2); + + rw.reset(); + rw.setFirstParent(true); + rw.setRewriteParents(false); + markStart(c); + assertCommit(c, rw.next()); + assertCommit(b1, rw.next()); + assertCommit(a, rw.next()); + assertNull(rw.next()); + } + + @Test(expected = IllegalStateException.class) + public void testMarkStartBeforeSetFirstParent() throws Exception { + RevCommit a = commit(); + + rw.reset(); + markStart(a); + rw.setFirstParent(true); + } + + @Test(expected = IllegalStateException.class) + public void testMergeBaseWithFirstParentNotAllowed() throws Exception { + RevCommit a = commit(); + + rw.reset(); + rw.setFirstParent(true); + rw.setRevFilter(RevFilter.MERGE_BASE); + markStart(a); + assertNull(rw.next()); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkTestCase.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkTestCase.java index 544398219f..a0056ae217 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkTestCase.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkTestCase.java @@ -51,6 +51,7 @@ import org.eclipse.jgit.dircache.DirCacheEntry; import org.eclipse.jgit.junit.RepositoryTestCase; import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.junit.TestRepository.CommitBuilder; +import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Repository; /** Support for tests of the {@link RevWalk} class. */ @@ -96,6 +97,10 @@ public abstract class RevWalkTestCase extends RepositoryTestCase { return util.get(tree, path); } + protected ObjectId unparsedCommit(ObjectId... parents) throws Exception { + return util.unparsedCommit(parents); + } + protected RevCommit commit(RevCommit... parents) throws Exception { return util.commit(parents); } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PostUploadHookChainTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PostUploadHookChainTest.java new file mode 100644 index 0000000000..ea7e8ed672 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PostUploadHookChainTest.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2019, Google LLC. + * 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.util.Arrays; + +import org.eclipse.jgit.storage.pack.PackStatistics; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class PostUploadHookChainTest { + + @Test + public void testDefaultIfEmpty() { + PostUploadHook[] noHooks = {}; + PostUploadHook newChain = PostUploadHookChain + .newChain(Arrays.asList(noHooks)); + assertEquals(newChain, PostUploadHook.NULL); + } + + @Test + public void testFlattenChainIfOnlyOne() { + FakePostUploadHook hook1 = new FakePostUploadHook(); + PostUploadHook newChain = PostUploadHookChain + .newChain(Arrays.asList(PostUploadHook.NULL, hook1)); + assertEquals(newChain, hook1); + } + + @Test + public void testMultipleHooks() { + FakePostUploadHook hook1 = new FakePostUploadHook(); + FakePostUploadHook hook2 = new FakePostUploadHook(); + + PostUploadHook chained = PostUploadHookChain + .newChain(Arrays.asList(hook1, hook2)); + chained.onPostUpload(null); + + assertTrue(hook1.wasInvoked()); + assertTrue(hook2.wasInvoked()); + } + + private static final class FakePostUploadHook implements PostUploadHook { + boolean invoked; + + public boolean wasInvoked() { + return invoked; + } + + @Override + public void onPostUpload(PackStatistics stats) { + invoked = true; + } + } +}
\ No newline at end of file diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PreUploadHookChainTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PreUploadHookChainTest.java new file mode 100644 index 0000000000..2a36d8a599 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PreUploadHookChainTest.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2019, Google LLC. + * 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.util.Arrays; +import java.util.Collection; + +import org.eclipse.jgit.lib.ObjectId; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class PreUploadHookChainTest { + + @Test + public void testDefaultIfEmpty() { + PreUploadHook[] noHooks = {}; + PreUploadHook newChain = PreUploadHookChain + .newChain(Arrays.asList(noHooks)); + assertEquals(newChain, PreUploadHook.NULL); + } + + @Test + public void testFlattenChainIfOnlyOne() { + FakePreUploadHook hook1 = new FakePreUploadHook(); + PreUploadHook newChain = PreUploadHookChain + .newChain(Arrays.asList(PreUploadHook.NULL, hook1)); + assertEquals(newChain, hook1); + } + + @Test + public void testMultipleHooks() throws ServiceMayNotContinueException { + FakePreUploadHook hook1 = new FakePreUploadHook(); + FakePreUploadHook hook2 = new FakePreUploadHook(); + + PreUploadHook chained = PreUploadHookChain + .newChain(Arrays.asList(hook1, hook2)); + chained.onBeginNegotiateRound(null, null, 0); + + assertTrue(hook1.wasInvoked()); + assertTrue(hook2.wasInvoked()); + } + + private static final class FakePreUploadHook implements PreUploadHook { + boolean invoked; + + @Override + public void onBeginNegotiateRound(UploadPack up, + Collection<? extends ObjectId> wants, int cntOffered) + throws ServiceMayNotContinueException { + invoked = true; + } + + @Override + public void onEndNegotiateRound(UploadPack up, + Collection<? extends ObjectId> wants, int cntCommon, + int cntNotFound, boolean ready) + throws ServiceMayNotContinueException { + throw new UnsupportedOperationException(); + } + + @Override + public void onSendPack(UploadPack up, + Collection<? extends ObjectId> wants, + Collection<? extends ObjectId> haves) + throws ServiceMayNotContinueException { + throw new UnsupportedOperationException(); + } + + public boolean wasInvoked() { + return invoked; + } + } +}
\ No newline at end of file diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ProtocolV2HookChainTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ProtocolV2HookChainTest.java new file mode 100644 index 0000000000..8fb1ca8199 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ProtocolV2HookChainTest.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2019, Google LLC. + * 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.util.Arrays; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class ProtocolV2HookChainTest { + + @Test + public void testDefaultIfEmpty() { + ProtocolV2Hook[] noHooks = {}; + ProtocolV2Hook newChain = ProtocolV2HookChain + .newChain(Arrays.asList(noHooks)); + assertEquals(newChain, ProtocolV2Hook.DEFAULT); + } + + @Test + public void testFlattenChainIfOnlyOne() { + FakeProtocolV2Hook hook1 = new FakeProtocolV2Hook(); + ProtocolV2Hook newChain = ProtocolV2HookChain + .newChain(Arrays.asList(ProtocolV2Hook.DEFAULT, hook1)); + assertEquals(newChain, hook1); + } + + @Test + public void testMultipleHooks() throws ServiceMayNotContinueException { + FakeProtocolV2Hook hook1 = new FakeProtocolV2Hook(); + FakeProtocolV2Hook hook2 = new FakeProtocolV2Hook(); + + ProtocolV2Hook chained = ProtocolV2HookChain + .newChain(Arrays.asList(hook1, hook2)); + chained.onLsRefs(LsRefsV2Request.builder().build()); + + assertTrue(hook1.wasInvoked()); + assertTrue(hook2.wasInvoked()); + } + + private static final class FakeProtocolV2Hook implements ProtocolV2Hook { + boolean invoked; + + @Override + public void onLsRefs(LsRefsV2Request req) + throws ServiceMayNotContinueException { + invoked = true; + } + + @Override + public void onCapabilities(CapabilitiesV2Request req) + throws ServiceMayNotContinueException { + throw new UnsupportedOperationException(); + } + + @Override + public void onFetch(FetchV2Request req) + throws ServiceMayNotContinueException { + throw new UnsupportedOperationException(); + } + + public boolean wasInvoked() { + return invoked; + } + } +} 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 index 260130b2bd..528a63f9c0 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/UploadPackTest.java @@ -1,5 +1,7 @@ package org.eclipse.jgit.transport; +import static org.eclipse.jgit.lib.MoreAsserts.assertThrows; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.hasItems; import static org.hamcrest.Matchers.is; @@ -9,27 +11,32 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.StringWriter; 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 java.util.Objects; +import java.util.function.Consumer; import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheBuilder; import org.eclipse.jgit.dircache.DirCacheEntry; -import org.eclipse.jgit.errors.PackProtocolException; import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.internal.storage.dfs.DfsGarbageCollector; import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription; import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; +import org.eclipse.jgit.internal.storage.file.PackLock; +import org.eclipse.jgit.internal.storage.pack.CachedPack; +import org.eclipse.jgit.internal.storage.pack.CachedPackUriProvider; import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectId; @@ -47,25 +54,19 @@ import org.eclipse.jgit.revwalk.RevTree; import org.eclipse.jgit.storage.pack.PackStatistics; import org.eclipse.jgit.transport.UploadPack.RequestPolicy; import org.eclipse.jgit.util.io.NullOutputStream; -import org.hamcrest.Matchers; import org.junit.After; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; /** * Tests for server upload-pack utilities. */ public class UploadPackTest { - @Rule - public ExpectedException thrown = ExpectedException.none(); - private URIish uri; private TestProtocol<Object> testProtocol; - private Object ctx = new Object(); + private final Object ctx = new Object(); private InMemoryRepository server; @@ -144,11 +145,11 @@ public class UploadPackTest { assertFalse(client.getObjectDatabase().has(blob.toObjectId())); try (Transport tn = testProtocol.open(uri, client, "server")) { - thrown.expect(TransportException.class); - thrown.expectMessage(Matchers.containsString( - "want " + blob.name() + " not valid")); - tn.fetch(NullProgressMonitor.INSTANCE, - Collections.singletonList(new RefSpec(blob.name()))); + TransportException e = assertThrows(TransportException.class, + () -> tn.fetch(NullProgressMonitor.INSTANCE, Collections + .singletonList(new RefSpec(blob.name())))); + assertThat(e.getMessage(), + containsString("want " + blob.name() + " not valid")); } } @@ -183,11 +184,41 @@ public class UploadPackTest { assertFalse(client.getObjectDatabase().has(blob.toObjectId())); try (Transport tn = testProtocol.open(uri, client, "server")) { - thrown.expect(TransportException.class); - thrown.expectMessage(Matchers.containsString( + TransportException e = assertThrows(TransportException.class, + () -> tn.fetch(NullProgressMonitor.INSTANCE, Collections + .singletonList(new RefSpec(blob.name())))); + assertThat(e.getMessage(), + containsString( "want " + blob.name() + " not valid")); - tn.fetch(NullProgressMonitor.INSTANCE, - Collections.singletonList(new RefSpec(blob.name()))); + } + } + + @Test + public void testFetchReachableBlobWithoutBitmapButFilterAllowed() throws Exception { + InMemoryRepository server2 = newRepo("server2"); + try (TestRepository<InMemoryRepository> remote2 = new TestRepository<>( + server2)) { + RevBlob blob = remote2.blob("foo"); + RevCommit commit = remote2.commit(remote2.tree(remote2.file("foo", blob))); + remote2.update("master", commit); + + server2.getConfig().setBoolean("uploadpack", null, "allowfilter", + true); + + testProtocol = new TestProtocol<>((Object req, Repository db) -> { + UploadPack up = new UploadPack(db); + up.setRequestPolicy(RequestPolicy.REACHABLE_COMMIT); + return up; + }, null); + uri = testProtocol.register(ctx, server2); + + assertFalse(client.getObjectDatabase().has(blob.toObjectId())); + + try (Transport tn = testProtocol.open(uri, client, "server2")) { + tn.fetch(NullProgressMonitor.INSTANCE, + Collections.singletonList(new RefSpec(blob.name()))); + assertTrue(client.getObjectDatabase().has(blob.toObjectId())); + } } } @@ -384,12 +415,11 @@ public class UploadPackTest { try (Transport tn = testProtocol.open(uri, client, "server2")) { tn.setFilterSpec(FilterSpec.withBlobLimit(0)); - thrown.expect(TransportException.class); - thrown.expectMessage( - "filter requires server to advertise that capability"); - - tn.fetch(NullProgressMonitor.INSTANCE, - Collections.singletonList(new RefSpec(commit.name()))); + TransportException e = assertThrows(TransportException.class, + () -> tn.fetch(NullProgressMonitor.INSTANCE, Collections + .singletonList(new RefSpec(commit.name())))); + assertThat(e.getMessage(), containsString( + "filter requires server to advertise that capability")); } } } @@ -398,22 +428,18 @@ public class UploadPackTest { * Invokes UploadPack with protocol v2 and sends it the given lines, * and returns UploadPack's output stream. */ - private ByteArrayInputStream uploadPackV2Setup(RequestPolicy requestPolicy, - RefFilter refFilter, ProtocolV2Hook hook, String... inputLines) + private ByteArrayInputStream uploadPackV2Setup( + Consumer<UploadPack> postConstructionSetup, String... inputLines) throws Exception { ByteArrayInputStream send = linesAsInputStream(inputLines); server.getConfig().setString("protocol", null, "version", "2"); UploadPack up = new UploadPack(server); - if (requestPolicy != null) - up.setRequestPolicy(requestPolicy); - if (refFilter != null) - up.setRefFilter(refFilter); - up.setExtraParameters(Sets.of("version=2")); - if (hook != null) { - up.setProtocolV2Hook(hook); + if (postConstructionSetup != null) { + postConstructionSetup.accept(up); } + up.setExtraParameters(Sets.of("version=2")); ByteArrayOutputStream recv = new ByteArrayOutputStream(); up.upload(send, recv, null); @@ -427,6 +453,7 @@ public class UploadPackTest { try (ByteArrayOutputStream send = new ByteArrayOutputStream()) { PacketLineOut pckOut = new PacketLineOut(send); for (String line : inputLines) { + Objects.requireNonNull(line); if (PacketLineIn.isEnd(line)) { pckOut.end(); } else if (PacketLineIn.isDelimiter(line)) { @@ -444,11 +471,12 @@ public class UploadPackTest { * Returns UploadPack's output stream, not including the capability * advertisement by the server. */ - private ByteArrayInputStream uploadPackV2(RequestPolicy requestPolicy, - RefFilter refFilter, ProtocolV2Hook hook, String... inputLines) + private ByteArrayInputStream uploadPackV2( + Consumer<UploadPack> postConstructionSetup, + String... inputLines) throws Exception { ByteArrayInputStream recvStream = - uploadPackV2Setup(requestPolicy, refFilter, hook, inputLines); + uploadPackV2Setup(postConstructionSetup, inputLines); PacketLineIn pckIn = new PacketLineIn(recvStream); // drain capabilities @@ -459,7 +487,7 @@ public class UploadPackTest { } private ByteArrayInputStream uploadPackV2(String... inputLines) throws Exception { - return uploadPackV2(null, null, null, inputLines); + return uploadPackV2(null, inputLines); } private static class TestV2Hook implements ProtocolV2Hook { @@ -488,8 +516,9 @@ public class UploadPackTest { @Test public void testV2Capabilities() throws Exception { TestV2Hook hook = new TestV2Hook(); - ByteArrayInputStream recvStream = - uploadPackV2Setup(null, null, hook, PacketLineIn.end()); + ByteArrayInputStream recvStream = uploadPackV2Setup( + (UploadPack up) -> {up.setProtocolV2Hook(hook);}, + PacketLineIn.end()); PacketLineIn pckIn = new PacketLineIn(recvStream); assertThat(hook.capabilitiesRequest, notNullValue()); assertThat(pckIn.readString(), is("version 2")); @@ -506,54 +535,72 @@ public class UploadPackTest { assertTrue(PacketLineIn.isEnd(pckIn.readString())); } - @Test - public void testV2CapabilitiesAllowFilter() throws Exception { - server.getConfig().setBoolean("uploadpack", null, "allowfilter", true); + private void checkAdvertisedIfAllowed(String configSection, String configName, + String fetchCapability) throws Exception { + server.getConfig().setBoolean(configSection, null, configName, true); ByteArrayInputStream recvStream = - uploadPackV2Setup(null, null, null, PacketLineIn.end()); + uploadPackV2Setup(null, PacketLineIn.end()); PacketLineIn pckIn = new PacketLineIn(recvStream); assertThat(pckIn.readString(), is("version 2")); - assertThat( - Arrays.asList(pckIn.readString(), pckIn.readString(), - pckIn.readString()), - // TODO(jonathantanmy) This check overspecifies the - // order of the capabilities of "fetch". - hasItems("ls-refs", "fetch=filter shallow", "server-option")); - assertTrue(PacketLineIn.isEnd(pckIn.readString())); + + ArrayList<String> lines = new ArrayList<>(); + String line; + while (!PacketLineIn.isEnd((line = pckIn.readString()))) { + if (line.startsWith("fetch=")) { + assertThat( + Arrays.asList(line.substring(6).split(" ")), + containsInAnyOrder(fetchCapability, "shallow")); + lines.add("fetch"); + } else { + lines.add(line); + } + } + assertThat(lines, containsInAnyOrder("ls-refs", "fetch", "server-option")); } - @Test - public void testV2CapabilitiesRefInWant() throws Exception { - server.getConfig().setBoolean("uploadpack", null, "allowrefinwant", true); + private void checkUnadvertisedIfUnallowed(String fetchCapability) throws Exception { ByteArrayInputStream recvStream = - uploadPackV2Setup(null, null, null, PacketLineIn.end()); + uploadPackV2Setup(null, PacketLineIn.end()); PacketLineIn pckIn = new PacketLineIn(recvStream); assertThat(pckIn.readString(), is("version 2")); - assertThat( - Arrays.asList(pckIn.readString(), pckIn.readString(), - pckIn.readString()), - // TODO(jonathantanmy) This check overspecifies the - // order of the capabilities of "fetch". - hasItems("ls-refs", "fetch=ref-in-want shallow", - "server-option")); - assertTrue(PacketLineIn.isEnd(pckIn.readString())); + + ArrayList<String> lines = new ArrayList<>(); + String line; + while (!PacketLineIn.isEnd((line = pckIn.readString()))) { + if (line.startsWith("fetch=")) { + assertThat( + Arrays.asList(line.substring(6).split(" ")), + hasItems("shallow")); + lines.add("fetch"); + } else { + lines.add(line); + } + } + assertThat(lines, hasItems("ls-refs", "fetch", "server-option")); + } + + @Test + public void testV2CapabilitiesAllowFilter() throws Exception { + checkAdvertisedIfAllowed("uploadpack", "allowfilter", "filter"); + checkUnadvertisedIfUnallowed("filter"); + } + + @Test + public void testV2CapabilitiesRefInWant() throws Exception { + checkAdvertisedIfAllowed("uploadpack", "allowrefinwant", "ref-in-want"); } @Test public void testV2CapabilitiesRefInWantNotAdvertisedIfUnallowed() throws Exception { - server.getConfig().setBoolean("uploadpack", null, "allowrefinwant", false); - ByteArrayInputStream recvStream = - uploadPackV2Setup(null, null, null, PacketLineIn.end()); - PacketLineIn pckIn = new PacketLineIn(recvStream); + checkUnadvertisedIfUnallowed("ref-in-want"); + } - assertThat(pckIn.readString(), is("version 2")); - assertThat( - Arrays.asList(pckIn.readString(), pckIn.readString(), - pckIn.readString()), - hasItems("ls-refs", "fetch=shallow", "server-option")); - assertTrue(PacketLineIn.isEnd(pckIn.readString())); + @Test + public void testV2CapabilitiesAllowSidebandAll() throws Exception { + checkAdvertisedIfAllowed("uploadpack", "allowsidebandall", "sideband-all"); + checkUnadvertisedIfUnallowed("sideband-all"); } @Test @@ -561,7 +608,7 @@ public class UploadPackTest { server.getConfig().setBoolean("uploadpack", null, "allowrefinwant", true); server.getConfig().setBoolean("uploadpack", null, "advertiserefinwant", false); ByteArrayInputStream recvStream = - uploadPackV2Setup(null, null, null, PacketLineIn.end()); + uploadPackV2Setup(null, PacketLineIn.end()); PacketLineIn pckIn = new PacketLineIn(recvStream); assertThat(pckIn.readString(), is("version 2")); @@ -589,7 +636,8 @@ public class UploadPackTest { remote.update("refs/tags/tag", tag); TestV2Hook hook = new TestV2Hook(); - ByteArrayInputStream recvStream = uploadPackV2(null, null, hook, + ByteArrayInputStream recvStream = uploadPackV2( + (UploadPack up) -> {up.setProtocolV2Hook(hook);}, "command=ls-refs\n", PacketLineIn.end()); PacketLineIn pckIn = new PacketLineIn(recvStream); @@ -707,13 +755,13 @@ public class UploadPackTest { @Test public void testV2LsRefsUnrecognizedArgument() throws Exception { - thrown.expect(PackProtocolException.class); - thrown.expectMessage("unexpected invalid-argument"); - uploadPackV2( - "command=ls-refs\n", - PacketLineIn.delimiter(), - "invalid-argument\n", - PacketLineIn.end()); + UploadPackInternalServerErrorException e = assertThrows( + UploadPackInternalServerErrorException.class, + () -> uploadPackV2("command=ls-refs\n", + PacketLineIn.delimiter(), "invalid-argument\n", + PacketLineIn.end())); + assertThat(e.getCause().getMessage(), + containsString("unexpected invalid-argument")); } @Test @@ -724,7 +772,7 @@ public class UploadPackTest { PacketLineIn.end() }; TestV2Hook testHook = new TestV2Hook(); - uploadPackV2Setup(null, null, testHook, lines); + uploadPackV2Setup((UploadPack up) -> {up.setProtocolV2Hook(testHook);}, lines); LsRefsV2Request req = testHook.lsRefsRequest; assertEquals(2, req.getServerOptions().size()); @@ -761,118 +809,117 @@ public class UploadPackTest { // This works uploadPackV2( - RequestPolicy.ADVERTISED, - null, - null, + (UploadPack up) -> {up.setRequestPolicy(RequestPolicy.ADVERTISED);}, "command=fetch\n", PacketLineIn.delimiter(), "want " + advertized.name() + "\n", - PacketLineIn.end()); + PacketLineIn.end()); // This doesn't - thrown.expect(TransportException.class); - thrown.expectMessage(Matchers.containsString( - "want " + unadvertized.name() + " not valid")); - uploadPackV2( - RequestPolicy.ADVERTISED, - null, - null, - "command=fetch\n", - PacketLineIn.delimiter(), - "want " + unadvertized.name() + "\n", - PacketLineIn.end()); + UploadPackInternalServerErrorException e = assertThrows( + UploadPackInternalServerErrorException.class, + () -> uploadPackV2( + (UploadPack up) -> {up.setRequestPolicy(RequestPolicy.ADVERTISED);}, + "command=fetch\n", PacketLineIn.delimiter(), + "want " + unadvertized.name() + "\n", + PacketLineIn.end())); + assertThat(e.getCause().getMessage(), + containsString("want " + unadvertized.name() + " not valid")); } @Test public void testV2FetchRequestPolicyReachableCommit() throws Exception { RevCommit reachable = remote.commit().message("x").create(); - RevCommit advertized = remote.commit().message("x").parent(reachable).create(); + RevCommit advertized = remote.commit().message("x").parent(reachable) + .create(); RevCommit unreachable = remote.commit().message("y").create(); remote.update("branch1", advertized); // This works uploadPackV2( - RequestPolicy.REACHABLE_COMMIT, - null, - null, + (UploadPack up) -> {up.setRequestPolicy(RequestPolicy.REACHABLE_COMMIT);}, "command=fetch\n", PacketLineIn.delimiter(), "want " + reachable.name() + "\n", PacketLineIn.end()); // This doesn't - thrown.expect(TransportException.class); - thrown.expectMessage(Matchers.containsString( - "want " + unreachable.name() + " not valid")); - uploadPackV2( - RequestPolicy.REACHABLE_COMMIT, - null, - null, - "command=fetch\n", - PacketLineIn.delimiter(), - "want " + unreachable.name() + "\n", - PacketLineIn.end()); + UploadPackInternalServerErrorException e = assertThrows( + UploadPackInternalServerErrorException.class, + () -> uploadPackV2( + (UploadPack up) -> {up.setRequestPolicy(RequestPolicy.REACHABLE_COMMIT);}, + "command=fetch\n", PacketLineIn.delimiter(), + "want " + unreachable.name() + "\n", + PacketLineIn.end())); + assertThat(e.getCause().getMessage(), + containsString("want " + unreachable.name() + " not valid")); } @Test public void testV2FetchRequestPolicyTip() throws Exception { RevCommit parentOfTip = remote.commit().message("x").create(); - RevCommit tip = remote.commit().message("y").parent(parentOfTip).create(); + RevCommit tip = remote.commit().message("y").parent(parentOfTip) + .create(); remote.update("secret", tip); // This works uploadPackV2( - RequestPolicy.TIP, - new RejectAllRefFilter(), - null, + (UploadPack up) -> { + up.setRequestPolicy(RequestPolicy.TIP); + up.setRefFilter(new RejectAllRefFilter()); + }, "command=fetch\n", PacketLineIn.delimiter(), "want " + tip.name() + "\n", PacketLineIn.end()); // This doesn't - thrown.expect(TransportException.class); - thrown.expectMessage(Matchers.containsString( - "want " + parentOfTip.name() + " not valid")); - uploadPackV2( - RequestPolicy.TIP, - new RejectAllRefFilter(), - null, - "command=fetch\n", - PacketLineIn.delimiter(), - "want " + parentOfTip.name() + "\n", - PacketLineIn.end()); + UploadPackInternalServerErrorException e = assertThrows( + UploadPackInternalServerErrorException.class, + () -> uploadPackV2( + (UploadPack up) -> { + up.setRequestPolicy(RequestPolicy.TIP); + up.setRefFilter(new RejectAllRefFilter()); + }, + "command=fetch\n", PacketLineIn.delimiter(), + "want " + parentOfTip.name() + "\n", + PacketLineIn.end())); + assertThat(e.getCause().getMessage(), + containsString("want " + parentOfTip.name() + " not valid")); } @Test public void testV2FetchRequestPolicyReachableCommitTip() throws Exception { RevCommit parentOfTip = remote.commit().message("x").create(); - RevCommit tip = remote.commit().message("y").parent(parentOfTip).create(); + RevCommit tip = remote.commit().message("y").parent(parentOfTip) + .create(); RevCommit unreachable = remote.commit().message("y").create(); remote.update("secret", tip); // This works uploadPackV2( - RequestPolicy.REACHABLE_COMMIT_TIP, - new RejectAllRefFilter(), - null, - "command=fetch\n", - PacketLineIn.delimiter(), - "want " + parentOfTip.name() + "\n", + (UploadPack up) -> { + up.setRequestPolicy(RequestPolicy.REACHABLE_COMMIT_TIP); + up.setRefFilter(new RejectAllRefFilter()); + }, + "command=fetch\n", + PacketLineIn.delimiter(), "want " + parentOfTip.name() + "\n", PacketLineIn.end()); // This doesn't - thrown.expect(TransportException.class); - thrown.expectMessage(Matchers.containsString( - "want " + unreachable.name() + " not valid")); - uploadPackV2( - RequestPolicy.REACHABLE_COMMIT_TIP, - new RejectAllRefFilter(), - null, - "command=fetch\n", - PacketLineIn.delimiter(), - "want " + unreachable.name() + "\n", - PacketLineIn.end()); + UploadPackInternalServerErrorException e = assertThrows( + UploadPackInternalServerErrorException.class, + () -> uploadPackV2( + (UploadPack up) -> { + up.setRequestPolicy(RequestPolicy.REACHABLE_COMMIT_TIP); + up.setRefFilter(new RejectAllRefFilter()); + }, + "command=fetch\n", + PacketLineIn.delimiter(), + "want " + unreachable.name() + "\n", + PacketLineIn.end())); + assertThat(e.getCause().getMessage(), + containsString("want " + unreachable.name() + " not valid")); } @Test @@ -881,9 +928,7 @@ public class UploadPackTest { // Exercise to make sure that even unreachable commits can be fetched uploadPackV2( - RequestPolicy.ANY, - null, - null, + (UploadPack up) -> {up.setRequestPolicy(RequestPolicy.ANY);}, "command=fetch\n", PacketLineIn.delimiter(), "want " + unreachable.name() + "\n", @@ -980,29 +1025,29 @@ public class UploadPackTest { String commonInBlob = "abcdefghijklmnopqrstuvwxyz"; RevBlob parentBlob = remote.blob(commonInBlob + "a"); - RevCommit parent = remote.commit(remote.tree(remote.file("foo", parentBlob))); + RevCommit parent = remote + .commit(remote.tree(remote.file("foo", parentBlob))); RevBlob childBlob = remote.blob(commonInBlob + "b"); - RevCommit child = remote.commit(remote.tree(remote.file("foo", childBlob)), parent); + RevCommit child = remote + .commit(remote.tree(remote.file("foo", childBlob)), parent); remote.update("branch1", child); // Pretend that we have parent to get a thin pack based on it. - ByteArrayInputStream recvStream = uploadPackV2( - "command=fetch\n", - PacketLineIn.delimiter(), - "want " + child.toObjectId().getName() + "\n", - "have " + parent.toObjectId().getName() + "\n", - "thin-pack\n", - "done\n", - PacketLineIn.end()); + ByteArrayInputStream recvStream = uploadPackV2("command=fetch\n", + PacketLineIn.delimiter(), + "want " + child.toObjectId().getName() + "\n", + "have " + parent.toObjectId().getName() + "\n", "thin-pack\n", + "done\n", PacketLineIn.end()); PacketLineIn pckIn = new PacketLineIn(recvStream); assertThat(pckIn.readString(), is("packfile")); // Verify that we received a thin pack by trying to apply it // against the client repo, which does not have parent. - thrown.expect(IOException.class); - thrown.expectMessage("pack has unresolved deltas"); - parsePack(recvStream); + IOException e = assertThrows(IOException.class, + () -> parsePack(recvStream)); + assertThat(e.getMessage(), + containsString("pack has unresolved deltas")); } @Test @@ -1303,19 +1348,18 @@ public class UploadPackTest { PersonIdent person = new PersonIdent(remote.getRepository()); RevCommit tooOld = remote.commit() - .committer(new PersonIdent(person, 1500000000, 0)).create(); + .committer(new PersonIdent(person, 1500000000, 0)).create(); remote.update("branch1", tooOld); - thrown.expect(PackProtocolException.class); - thrown.expectMessage("No commits selected for shallow request"); - uploadPackV2( - "command=fetch\n", - PacketLineIn.delimiter(), - "deepen-since 1510000\n", - "want " + tooOld.toObjectId().getName() + "\n", - "done\n", - PacketLineIn.end()); + UploadPackInternalServerErrorException e = assertThrows( + UploadPackInternalServerErrorException.class, + () -> uploadPackV2("command=fetch\n", PacketLineIn.delimiter(), + "deepen-since 1510000\n", + "want " + tooOld.toObjectId().getName() + "\n", + "done\n", PacketLineIn.end())); + assertThat(e.getCause().getMessage(), + containsString("No commits selected for shallow request")); } @Test @@ -1374,7 +1418,8 @@ public class UploadPackTest { } @Test - public void testV2FetchDeepenNot_excludeDescendantOfWant() throws Exception { + public void testV2FetchDeepenNot_excludeDescendantOfWant() + throws Exception { RevCommit one = remote.commit().message("one").create(); RevCommit two = remote.commit().message("two").parent(one).create(); RevCommit three = remote.commit().message("three").parent(two).create(); @@ -1383,15 +1428,14 @@ public class UploadPackTest { remote.update("two", two); remote.update("four", four); - thrown.expect(PackProtocolException.class); - thrown.expectMessage("No commits selected for shallow request"); - uploadPackV2( - "command=fetch\n", - PacketLineIn.delimiter(), - "deepen-not four\n", - "want " + two.toObjectId().getName() + "\n", - "done\n", - PacketLineIn.end()); + UploadPackInternalServerErrorException e = assertThrows( + UploadPackInternalServerErrorException.class, + () -> uploadPackV2("command=fetch\n", PacketLineIn.delimiter(), + "deepen-not four\n", + "want " + two.toObjectId().getName() + "\n", "done\n", + PacketLineIn.end())); + assertThat(e.getCause().getMessage(), + containsString("No commits selected for shallow request")); } @Test @@ -1469,13 +1513,12 @@ public class UploadPackTest { @Test public void testV2FetchUnrecognizedArgument() throws Exception { - thrown.expect(PackProtocolException.class); - thrown.expectMessage("unexpected invalid-argument"); - uploadPackV2( - "command=fetch\n", - PacketLineIn.delimiter(), - "invalid-argument\n", - PacketLineIn.end()); + UploadPackInternalServerErrorException e = assertThrows( + UploadPackInternalServerErrorException.class, + () -> uploadPackV2("command=fetch\n", PacketLineIn.delimiter(), + "invalid-argument\n", PacketLineIn.end())); + assertThat(e.getCause().getMessage(), + containsString("unexpected invalid-argument")); } @Test @@ -1485,7 +1528,7 @@ public class UploadPackTest { PacketLineIn.end() }; TestV2Hook testHook = new TestV2Hook(); - uploadPackV2Setup(null, null, testHook, lines); + uploadPackV2Setup((UploadPack up) -> {up.setProtocolV2Hook(testHook);}, lines); FetchV2Request req = testHook.fetchRequest; assertNotNull(req); @@ -1569,8 +1612,9 @@ public class UploadPackTest { input.add("done\n"); input.add(PacketLineIn.end()); ByteArrayInputStream recvStream = - uploadPackV2(RequestPolicy.ANY, /*refFilter=*/null, - /*hook=*/null, input.toArray(new String[0])); + uploadPackV2( + (UploadPack up) -> {up.setRequestPolicy(RequestPolicy.ANY);}, + input.toArray(new String[0])); PacketLineIn pckIn = new PacketLineIn(recvStream); assertThat(pckIn.readString(), is("packfile")); parsePack(recvStream); @@ -1837,43 +1881,39 @@ public class UploadPackTest { .has(preparator.subtree3.toObjectId())); } - @Test - public void testV2FetchFilterWhenNotAllowed() throws Exception { + private void checkV2FetchWhenNotAllowed(String fetchLine, String expectedMessage) + throws Exception { RevCommit commit = remote.commit().message("0").create(); remote.update("master", commit); - server.getConfig().setBoolean("uploadpack", null, "allowfilter", false); + UploadPackInternalServerErrorException e = assertThrows( + UploadPackInternalServerErrorException.class, + () -> uploadPackV2("command=fetch\n", PacketLineIn.delimiter(), + "want " + commit.toObjectId().getName() + "\n", + fetchLine, "done\n", PacketLineIn.end())); + assertThat(e.getCause().getMessage(), + containsString(expectedMessage)); + } - thrown.expect(PackProtocolException.class); - thrown.expectMessage("unexpected filter blob:limit=5"); - uploadPackV2( - "command=fetch\n", - PacketLineIn.delimiter(), - "want " + commit.toObjectId().getName() + "\n", + @Test + public void testV2FetchFilterWhenNotAllowed() throws Exception { + checkV2FetchWhenNotAllowed( "filter blob:limit=5\n", - "done\n", - PacketLineIn.end()); + "unexpected filter blob:limit=5"); } @Test public void testV2FetchWantRefIfNotAllowed() throws Exception { - RevCommit one = remote.commit().message("1").create(); - remote.update("one", one); + checkV2FetchWhenNotAllowed( + "want-ref refs/heads/one\n", + "unexpected want-ref refs/heads/one"); + } - try { - uploadPackV2( - "command=fetch\n", - PacketLineIn.delimiter(), - "want-ref refs/heads/one\n", - "done\n", - PacketLineIn.end()); - } catch (PackProtocolException e) { - assertThat( - e.getMessage(), - containsString("unexpected want-ref refs/heads/one")); - return; - } - fail("expected PackProtocolException"); + @Test + public void testV2FetchSidebandAllIfNotAllowed() throws Exception { + checkV2FetchWhenNotAllowed( + "sideband-all\n", + "unexpected sideband-all"); } @Test @@ -1915,23 +1955,17 @@ public class UploadPackTest { RevCommit one = remote.commit().message("1").create(); remote.update("one", one); - server.getConfig().setBoolean("uploadpack", null, "allowrefinwant", true); + server.getConfig().setBoolean("uploadpack", null, "allowrefinwant", + true); - try { - uploadPackV2( - "command=fetch\n", - PacketLineIn.delimiter(), - "want-ref refs/heads/one\n", - "want-ref refs/heads/nonExistentRef\n", - "done\n", - PacketLineIn.end()); - } catch (PackProtocolException e) { - assertThat( - e.getMessage(), + UploadPackInternalServerErrorException e = assertThrows( + UploadPackInternalServerErrorException.class, + () -> uploadPackV2("command=fetch\n", PacketLineIn.delimiter(), + "want-ref refs/heads/one\n", + "want-ref refs/heads/nonExistentRef\n", "done\n", + PacketLineIn.end())); + assertThat(e.getCause().getMessage(), containsString("Invalid ref name: refs/heads/nonExistentRef")); - return; - } - fail("expected PackProtocolException"); } @Test @@ -2066,6 +2100,110 @@ public class UploadPackTest { } @Test + public void testV2FetchSidebandAllNoPackfile() throws Exception { + RevCommit fooParent = remote.commit().message("x").create(); + RevCommit fooChild = remote.commit().message("x").parent(fooParent).create(); + RevCommit barParent = remote.commit().message("y").create(); + RevCommit barChild = remote.commit().message("y").parent(barParent).create(); + remote.update("branch1", fooChild); + remote.update("branch2", barChild); + + server.getConfig().setBoolean("uploadpack", null, "allowsidebandall", true); + + ByteArrayInputStream recvStream = uploadPackV2( + "command=fetch\n", + PacketLineIn.DELIM, + "sideband-all\n", + "want " + fooChild.toObjectId().getName() + "\n", + "want " + barChild.toObjectId().getName() + "\n", + "have " + fooParent.toObjectId().getName() + "\n", + PacketLineIn.END); + PacketLineIn pckIn = new PacketLineIn(recvStream); + + assertThat(pckIn.readString(), is("\001acknowledgments")); + assertThat(pckIn.readString(), is("\001ACK " + fooParent.getName())); + assertTrue(PacketLineIn.isEnd(pckIn.readString())); + } + + @Test + public void testV2FetchSidebandAllPackfile() throws Exception { + RevCommit commit = remote.commit().message("x").create(); + remote.update("master", commit); + + server.getConfig().setBoolean("uploadpack", null, "allowsidebandall", true); + + ByteArrayInputStream recvStream = uploadPackV2("command=fetch\n", + PacketLineIn.DELIM, + "want " + commit.getName() + "\n", + "sideband-all\n", + "done\n", + PacketLineIn.END); + PacketLineIn pckIn = new PacketLineIn(recvStream); + + String s; + // When sideband-all is used, object counting happens before + // "packfile" is written, and object counting outputs progress + // in sideband 2. Skip all these lines. + for (s = pckIn.readString(); s.startsWith("\002"); s = pckIn.readString()) { + // do nothing + } + assertThat(s, is("\001packfile")); + parsePack(recvStream); + } + + @Test + public void testV2FetchPackfileUris() throws Exception { + // Inside the pack + RevCommit commit = remote.commit().message("x").create(); + remote.update("master", commit); + generateBitmaps(server); + + // Outside the pack + RevCommit commit2 = remote.commit().message("x").parent(commit).create(); + remote.update("master", commit2); + + server.getConfig().setBoolean("uploadpack", null, "allowsidebandall", true); + + ByteArrayInputStream recvStream = uploadPackV2( + (UploadPack up) -> { + up.setCachedPackUriProvider(new CachedPackUriProvider() { + @Override + public PackInfo getInfo(CachedPack pack, + Collection<String> protocolsSupported) + throws IOException { + assertThat(protocolsSupported, hasItems("https")); + if (!protocolsSupported.contains("https")) + return null; + return new PackInfo("myhash", "myuri"); + } + + }); + }, + "command=fetch\n", + PacketLineIn.DELIM, + "want " + commit2.getName() + "\n", + "sideband-all\n", + "packfile-uris https\n", + "done\n", + PacketLineIn.END); + PacketLineIn pckIn = new PacketLineIn(recvStream); + + String s; + // skip all \002 strings + for (s = pckIn.readString(); s.startsWith("\002"); s = pckIn.readString()) { + // do nothing + } + assertThat(s, is("\001packfile-uris")); + assertThat(pckIn.readString(), is("\001myhash myuri")); + assertTrue(PacketLineIn.isDelimiter(pckIn.readString())); + assertThat(pckIn.readString(), is("\001packfile")); + parsePack(recvStream); + + assertFalse(client.getObjectDatabase().has(commit.toObjectId())); + assertTrue(client.getObjectDatabase().has(commit2.toObjectId())); + } + + @Test public void testGetPeerAgentProtocolV0() throws Exception { RevCommit one = remote.commit().message("1").create(); remote.update("one", one); @@ -2110,4 +2248,84 @@ public class UploadPackTest { return new HashMap<>(); } } + + @Test + public void testSingleBranchCloneTagChain() throws Exception { + RevBlob blob0 = remote.blob("Initial content of first file"); + RevBlob blob1 = remote.blob("Second file content"); + RevCommit commit0 = remote + .commit(remote.tree(remote.file("prvni.txt", blob0))); + RevCommit commit1 = remote + .commit(remote.tree(remote.file("druhy.txt", blob1)), commit0); + remote.update("master", commit1); + + RevTag heavyTag1 = remote.tag("commitTagRing", commit0); + remote.getRevWalk().parseHeaders(heavyTag1); + RevTag heavyTag2 = remote.tag("middleTagRing", heavyTag1); + remote.lightweightTag("refTagRing", heavyTag2); + + UploadPack uploadPack = new UploadPack(remote.getRepository()); + + ByteArrayOutputStream cli = new ByteArrayOutputStream(); + PacketLineOut clientWant = new PacketLineOut(cli); + clientWant.writeString("want " + commit1.name() + + " multi_ack_detailed include-tag thin-pack ofs-delta agent=tempo/pflaska"); + clientWant.end(); + clientWant.writeString("done\n"); + + try (ByteArrayOutputStream serverResponse = new ByteArrayOutputStream()) { + + uploadPack.setPreUploadHook(new PreUploadHook() { + @Override + public void onBeginNegotiateRound(UploadPack up, + Collection<? extends ObjectId> wants, int cntOffered) + throws ServiceMayNotContinueException { + // Do nothing. + } + + @Override + public void onEndNegotiateRound(UploadPack up, + Collection<? extends ObjectId> wants, int cntCommon, + int cntNotFound, boolean ready) + throws ServiceMayNotContinueException { + // Do nothing. + } + + @Override + public void onSendPack(UploadPack up, + Collection<? extends ObjectId> wants, + Collection<? extends ObjectId> haves) + throws ServiceMayNotContinueException { + // collect pack data + serverResponse.reset(); + } + }); + uploadPack.upload(new ByteArrayInputStream(cli.toByteArray()), + serverResponse, System.err); + InputStream packReceived = new ByteArrayInputStream( + serverResponse.toByteArray()); + PackLock lock = null; + try (ObjectInserter ins = client.newObjectInserter()) { + PackParser parser = ins.newPackParser(packReceived); + parser.setAllowThin(true); + parser.setLockMessage("receive-tag-chain"); + ProgressMonitor mlc = NullProgressMonitor.INSTANCE; + lock = parser.parse(mlc, mlc); + ins.flush(); + } finally { + if (lock != null) { + lock.unlock(); + } + } + InMemoryRepository.MemObjDatabase objDb = client + .getObjectDatabase(); + assertTrue(objDb.has(blob0.toObjectId())); + assertTrue(objDb.has(blob1.toObjectId())); + assertTrue(objDb.has(commit0.toObjectId())); + assertTrue(objDb.has(commit1.toObjectId())); + assertTrue(objDb.has(heavyTag1.toObjectId())); + assertTrue(objDb.has(heavyTag2.toObjectId())); + } + } + } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FS_POSIXTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FS_POSIXTest.java index 87349a25a4..d7a7d86775 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FS_POSIXTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FS_POSIXTest.java @@ -59,7 +59,7 @@ import org.junit.Before; import org.junit.Test; public class FS_POSIXTest { - private SystemReader originalSystemReaderInstance; + private FileBasedConfig jgitConfig; private FileBasedConfig systemConfig; @@ -70,6 +70,7 @@ public class FS_POSIXTest { @Before public void setUp() throws Exception { tmp = Files.createTempDirectory("jgit_test_"); + MockSystemReader mockSystemReader = new MockSystemReader(); SystemReader.setInstance(mockSystemReader); @@ -78,7 +79,10 @@ public class FS_POSIXTest { // The MockSystemReader must be configured first since we need to use // the same one here FS.getFileStoreAttributes(tmp.getParent()); - systemConfig = new FileBasedConfig( + + jgitConfig = new FileBasedConfig(new File(tmp.toFile(), "jgitconfig"), + FS.DETECTED); + systemConfig = new FileBasedConfig(jgitConfig, new File(tmp.toFile(), "systemgitconfig"), FS.DETECTED); userConfig = new FileBasedConfig(systemConfig, new File(tmp.toFile(), "usergitconfig"), FS.DETECTED); @@ -89,16 +93,14 @@ public class FS_POSIXTest { userConfig.setBoolean(ConfigConstants.CONFIG_GC_SECTION, null, ConfigConstants.CONFIG_KEY_AUTODETACH, false); userConfig.save(); + mockSystemReader.setJGitConfig(jgitConfig); mockSystemReader.setSystemGitConfig(systemConfig); mockSystemReader.setUserGitConfig(userConfig); - - originalSystemReaderInstance = SystemReader.getInstance(); - SystemReader.setInstance(mockSystemReader); } @After public void tearDown() throws IOException { - SystemReader.setInstance(originalSystemReaderInstance); + SystemReader.setInstance(null); FileUtils.delete(tmp.toFile(), FileUtils.RECURSIVE | FileUtils.RETRY); } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitDateFormatterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitDateFormatterTest.java index d52166f2ba..59ff2adcdd 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitDateFormatterTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/GitDateFormatterTest.java @@ -44,6 +44,7 @@ package org.eclipse.jgit.util; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import org.eclipse.jgit.junit.MockSystemReader; import org.eclipse.jgit.lib.PersonIdent; @@ -120,13 +121,16 @@ public class GitDateFormatterTest { @Test public void LOCALE() { - assertEquals("Sep 20, 2011 7:09:25 PM -0400", new GitDateFormatter( - Format.LOCALE).formatDate(ident)); + String date = new GitDateFormatter(Format.LOCALE).formatDate(ident); + assertTrue("Sep 20, 2011 7:09:25 PM -0400".equals(date) + || "Sep 20, 2011, 7:09:25 PM -0400".equals(date)); // JDK-8206961 } @Test public void LOCALELOCAL() { - assertEquals("Sep 20, 2011 7:39:25 PM", new GitDateFormatter( - Format.LOCALELOCAL).formatDate(ident)); + String date = new GitDateFormatter(Format.LOCALELOCAL) + .formatDate(ident); + assertTrue("Sep 20, 2011 7:39:25 PM".equals(date) + || "Sep 20, 2011, 7:39:25 PM".equals(date)); // JDK-8206961 } } |