Merge "buck: set Bundle-Version for :jgit_bin"
diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java
index ac9685d..c941afc 100644
--- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java
+++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java
@@ -822,7 +822,7 @@ public void fsck(RevObject... tips) throws MissingObjectException,
 					break;
 
 				final byte[] bin = db.open(o, o.getType()).getCachedBytes();
-				oc.checkCommit(bin);
+				oc.checkCommit(o, bin);
 				assertHash(o, bin);
 			}
 
@@ -832,7 +832,7 @@ public void fsck(RevObject... tips) throws MissingObjectException,
 					break;
 
 				final byte[] bin = db.open(o, o.getType()).getCachedBytes();
-				oc.check(o.getType(), bin);
+				oc.check(o, o.getType(), bin);
 				assertHash(o, bin);
 			}
 		}
diff --git "a/org.eclipse.jgit.pgm.test/org.eclipse.jgit.pgm--All-Tests \050Java8\051 \050de\051.launch" "b/org.eclipse.jgit.pgm.test/org.eclipse.jgit.pgm--All-Tests \050Java8\051 \050de\051.launch"
new file mode 100644
index 0000000..5c137f2
--- /dev/null
+++ "b/org.eclipse.jgit.pgm.test/org.eclipse.jgit.pgm--All-Tests \050Java8\051 \050de\051.launch"
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<launchConfiguration type="org.eclipse.jdt.junit.launchconfig">
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
+<listEntry value="/org.eclipse.jgit.pgm.test/tst"/>
+</listAttribute>
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
+<listEntry value="2"/>
+</listAttribute>
+<booleanAttribute key="org.eclipse.debug.core.appendEnvironmentVariables" value="true"/>
+<mapAttribute key="org.eclipse.debug.core.environmentVariables">
+<mapEntry key="LANG" value="de_DE.UTF-8"/>
+</mapAttribute>
+<listAttribute key="org.eclipse.debug.ui.favoriteGroups">
+<listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
+<listEntry value="org.eclipse.debug.ui.launchGroup.run"/>
+</listAttribute>
+<stringAttribute key="org.eclipse.jdt.junit.CONTAINER" value="=org.eclipse.jgit.pgm.test/tst"/>
+<booleanAttribute key="org.eclipse.jdt.junit.KEEPRUNNING_ATTR" value="false"/>
+<stringAttribute key="org.eclipse.jdt.junit.TESTNAME" value=""/>
+<stringAttribute key="org.eclipse.jdt.junit.TEST_KIND" value="org.eclipse.jdt.junit.loader.junit4"/>
+<booleanAttribute key="org.eclipse.jdt.launching.ATTR_USE_START_ON_FIRST_THREAD" value="true"/>
+<listAttribute key="org.eclipse.jdt.launching.CLASSPATH">
+<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry containerPath=&quot;org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.7&quot; path=&quot;1&quot; type=&quot;4&quot;/&gt;&#10;"/>
+<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry id=&quot;org.eclipse.jdt.launching.classpathentry.defaultClasspath&quot;&gt;&#10;&lt;memento exportedEntriesOnly=&quot;false&quot; project=&quot;org.eclipse.jgit.pgm.test&quot;/&gt;&#10;&lt;/runtimeClasspathEntry&gt;&#10;"/>
+</listAttribute>
+<booleanAttribute key="org.eclipse.jdt.launching.DEFAULT_CLASSPATH" value="false"/>
+<stringAttribute key="org.eclipse.jdt.launching.JRE_CONTAINER" value="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
+<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value=""/>
+<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="org.eclipse.jgit.pgm.test"/>
+</launchConfiguration>
diff --git a/org.eclipse.jgit.pgm.test/src/org/eclipse/jgit/lib/CLIRepositoryTestCase.java b/org.eclipse.jgit.pgm.test/src/org/eclipse/jgit/lib/CLIRepositoryTestCase.java
index a72af9a..a3436a0 100644
--- a/org.eclipse.jgit.pgm.test/src/org/eclipse/jgit/lib/CLIRepositoryTestCase.java
+++ b/org.eclipse.jgit.pgm.test/src/org/eclipse/jgit/lib/CLIRepositoryTestCase.java
@@ -223,20 +223,24 @@ protected void assertArrayOfLinesEquals(String[] expected, String[] actual) {
 		assertEquals(toString(expected), toString(actual));
 	}
 
-	public static String toString(String[] lines) {
+	public static String toString(String... lines) {
 		return toString(Arrays.asList(lines));
 	}
 
 	public static String toString(List<String> lines) {
 		StringBuilder b = new StringBuilder();
 		for (String s : lines) {
+			// trim indentation, to simplify tests
+			s = s.trim();
 			if (s != null && !s.isEmpty()) {
 				b.append(s);
-				if (!s.endsWith("\n")) {
-					b.append('\n');
-				}
+				b.append('\n');
 			}
 		}
+		// delete last line break to allow simpler tests with one line compare
+		if (b.length() > 0 && b.charAt(b.length() - 1) == '\n') {
+			b.deleteCharAt(b.length() - 1);
+		}
 		return b.toString();
 	}
 
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/AddTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/AddTest.java
index 5970913..3edd9b8 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/AddTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/AddTest.java
@@ -45,6 +45,7 @@
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
 
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.dircache.DirCache;
@@ -64,8 +65,12 @@ public void setUp() throws Exception {
 
 	@Test
 	public void testAddNothing() throws Exception {
-		assertEquals("fatal: Argument \"filepattern\" is required", //
-				executeUnchecked("git add")[0]);
+		try {
+			execute("git add");
+			fail("Must die");
+		} catch (Die e) {
+			// expected, requires argument
+		}
 	}
 
 	@Test
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/BranchTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/BranchTest.java
index f369577..74506bc 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/BranchTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/BranchTest.java
@@ -43,6 +43,11 @@
 package org.eclipse.jgit.pgm;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.File;
 
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.lib.CLIRepositoryTestCase;
@@ -62,8 +67,9 @@ public void setUp() throws Exception {
 
 	@Test
 	public void testList() throws Exception {
+		assertEquals("* master", toString(execute("git branch")));
 		assertEquals("* master 6fd41be initial commit",
-				execute("git branch -v")[0]);
+				toString(execute("git branch -v")));
 	}
 
 	@Test
@@ -71,8 +77,10 @@ public void testListDetached() throws Exception {
 		RefUpdate updateRef = db.updateRef(Constants.HEAD, true);
 		updateRef.setNewObjectId(db.resolve("6fd41be"));
 		updateRef.update();
-		assertEquals("* (no branch) 6fd41be initial commit",
-				execute("git branch -v")[0]);
+		assertEquals(
+				toString("* (no branch) 6fd41be initial commit",
+						"master      6fd41be initial commit"),
+				toString(execute("git branch -v")));
 	}
 
 	@Test
@@ -80,15 +88,175 @@ public void testListContains() throws Exception {
 		new Git(db).branchCreate().setName("initial").call();
 		RevCommit second = new Git(db).commit().setMessage("second commit")
 				.call();
-		assertArrayOfLinesEquals(new String[] { "  initial", "* master", "" },
-				execute("git branch --contains 6fd41be"));
-		assertArrayOfLinesEquals(new String[] { "* master", "" },
-				execute("git branch --contains " + second.name()));
+		assertEquals(toString("  initial", "* master"),
+				toString(execute("git branch --contains 6fd41be")));
+		assertEquals("* master",
+				toString(execute("git branch --contains " + second.name())));
 	}
 
 	@Test
 	public void testExistingBranch() throws Exception {
 		assertEquals("fatal: A branch named 'master' already exists.",
-				executeUnchecked("git branch master")[0]);
+				toString(executeUnchecked("git branch master")));
+	}
+
+	@Test
+	public void testRenameSingleArg() throws Exception {
+		try {
+			toString(execute("git branch -m"));
+			fail("Must die");
+		} catch (Die e) {
+			// expected, requires argument
+		}
+		String result = toString(execute("git branch -m slave"));
+		assertEquals("", result);
+		result = toString(execute("git branch -a"));
+		assertEquals("* slave", result);
+	}
+
+	@Test
+	public void testRenameTwoArgs() throws Exception {
+		String result = toString(execute("git branch -m master slave"));
+		assertEquals("", result);
+		result = toString(execute("git branch -a"));
+		assertEquals("* slave", result);
+	}
+
+	@Test
+	public void testCreate() throws Exception {
+		try {
+			toString(execute("git branch a b"));
+			fail("Must die");
+		} catch (Die e) {
+			// expected, too many arguments
+		}
+		String result = toString(execute("git branch second"));
+		assertEquals("", result);
+		result = toString(execute("git branch"));
+		assertEquals(toString("* master", "second"), result);
+		result = toString(execute("git branch -v"));
+		assertEquals(toString("* master 6fd41be initial commit",
+				"second 6fd41be initial commit"), result);
+	}
+
+	@Test
+	public void testDelete() throws Exception {
+		try {
+			toString(execute("git branch -d"));
+			fail("Must die");
+		} catch (Die e) {
+			// expected, requires argument
+		}
+		String result = toString(execute("git branch second"));
+		assertEquals("", result);
+		result = toString(execute("git branch -d second"));
+		assertEquals("", result);
+		result = toString(execute("git branch"));
+		assertEquals("* master", result);
+	}
+
+	@Test
+	public void testDeleteMultiple() throws Exception {
+		String result = toString(execute("git branch second",
+				"git branch third", "git branch fourth"));
+		assertEquals("", result);
+		result = toString(execute("git branch -d second third fourth"));
+		assertEquals("", result);
+		result = toString(execute("git branch"));
+		assertEquals("* master", result);
+	}
+
+	@Test
+	public void testDeleteForce() throws Exception {
+		try {
+			toString(execute("git branch -D"));
+			fail("Must die");
+		} catch (Die e) {
+			// expected, requires argument
+		}
+		String result = toString(execute("git branch second"));
+		assertEquals("", result);
+		result = toString(execute("git checkout second"));
+		assertEquals("Switched to branch 'second'", result);
+
+		File a = writeTrashFile("a", "a");
+		assertTrue(a.exists());
+		execute("git add a", "git commit -m 'added a'");
+
+		result = toString(execute("git checkout master"));
+		assertEquals("Switched to branch 'master'", result);
+
+		result = toString(execute("git branch"));
+		assertEquals(toString("* master", "second"), result);
+
+		try {
+			toString(execute("git branch -d second"));
+			fail("Must die");
+		} catch (Die e) {
+			// expected, the current HEAD is on second and not merged to master
+		}
+		result = toString(execute("git branch -D second"));
+		assertEquals("", result);
+
+		result = toString(execute("git branch"));
+		assertEquals("* master", result);
+	}
+
+	@Test
+	public void testDeleteForceMultiple() throws Exception {
+		String result = toString(execute("git branch second",
+				"git branch third", "git branch fourth"));
+
+		assertEquals("", result);
+		result = toString(execute("git checkout second"));
+		assertEquals("Switched to branch 'second'", result);
+
+		File a = writeTrashFile("a", "a");
+		assertTrue(a.exists());
+		execute("git add a", "git commit -m 'added a'");
+
+		result = toString(execute("git checkout master"));
+		assertEquals("Switched to branch 'master'", result);
+
+		result = toString(execute("git branch"));
+		assertEquals(toString("fourth", "* master", "second", "third"), result);
+
+		try {
+			toString(execute("git branch -d second third fourth"));
+			fail("Must die");
+		} catch (Die e) {
+			// expected, the current HEAD is on second and not merged to master
+		}
+		result = toString(execute("git branch"));
+		assertEquals(toString("fourth", "* master", "second", "third"), result);
+
+		result = toString(execute("git branch -D second third fourth"));
+		assertEquals("", result);
+
+		result = toString(execute("git branch"));
+		assertEquals("* master", result);
+	}
+
+	@Test
+	public void testCreateFromOldCommit() throws Exception {
+		File a = writeTrashFile("a", "a");
+		assertTrue(a.exists());
+		execute("git add a", "git commit -m 'added a'");
+		File b = writeTrashFile("b", "b");
+		assertTrue(b.exists());
+		execute("git add b", "git commit -m 'added b'");
+		String result = toString(execute("git log -n 1 --reverse"));
+		String firstCommitId = result.substring("commit ".length(),
+				result.indexOf('\n'));
+
+		result = toString(execute("git branch -f second " + firstCommitId));
+		assertEquals("", result);
+
+		result = toString(execute("git branch"));
+		assertEquals(toString("* master", "second"), result);
+
+		result = toString(execute("git checkout second"));
+		assertEquals("Switched to branch 'second'", result);
+		assertFalse(b.exists());
 	}
 }
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CommitTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CommitTest.java
index 721ed15..6bccb6d 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CommitTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CommitTest.java
@@ -42,7 +42,7 @@
  */
 package org.eclipse.jgit.pgm;
 
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
 
 import org.eclipse.jgit.lib.CLIRepositoryTestCase;
 import org.junit.Test;
@@ -54,26 +54,27 @@ public void testCommitPath() throws Exception {
 		writeTrashFile("a", "a");
 		writeTrashFile("b", "a");
 		String result = toString(execute("git add a"));
-		assertTrue("Unexpected output: " + result, result.isEmpty());
+		assertEquals("", result);
 
 		result = toString(execute("git status -- a"));
-		assertTrue("Unexpected output: " + result,
-				result.contains("new file:   a"));
+		assertEquals(toString("On branch master", "Changes to be committed:",
+				"new file:   a"), result);
 
 		result = toString(execute("git status -- b"));
-		assertTrue("Unexpected output: " + result,
-				result.trim().contains("Untracked files:\n	b"));
+		assertEquals(toString("On branch master", "Untracked files:", "b"),
+				result);
 
 		result = toString(execute("git commit a -m 'added a'"));
-		assertTrue("Unexpected output: " + result, result.contains("added a"));
+		assertEquals(
+				"[master 8cb3ef7e5171aaee1792df6302a5a0cd30425f7a] added a",
+				result);
 
 		result = toString(execute("git status -- a"));
-		assertTrue("Unexpected output: " + result,
-				result.trim().equals("On branch master"));
+		assertEquals("On branch master", result);
 
 		result = toString(execute("git status -- b"));
-		assertTrue("Unexpected output: " + result,
-				result.trim().contains("Untracked files:\n	b"));
+		assertEquals(toString("On branch master", "Untracked files:", "b"),
+				result);
 	}
 
 	@Test
@@ -81,21 +82,19 @@ public void testCommitAll() throws Exception {
 		writeTrashFile("a", "a");
 		writeTrashFile("b", "a");
 		String result = toString(execute("git add a b"));
-		assertTrue("Unexpected output: " + result, result.isEmpty());
+		assertEquals("", result);
 
 		result = toString(execute("git status -- a b"));
-		assertTrue("Unexpected output: " + result,
-				result.contains("new file:   a"));
-		assertTrue("Unexpected output: " + result,
-				result.contains("new file:   b"));
+		assertEquals(toString("On branch master", "Changes to be committed:",
+				"new file:   a", "new file:   b"), result);
 
 		result = toString(execute("git commit -m 'added a b'"));
-		assertTrue("Unexpected output: " + result,
-				result.contains("added a b"));
+		assertEquals(
+				"[master 3c93fa8e3a28ee26690498be78016edcb3a38c73] added a b",
+				result);
 
 		result = toString(execute("git status -- a b"));
-		assertTrue("Unexpected output: " + result,
-				result.trim().equals("On branch master"));
+		assertEquals("On branch master", result);
 	}
 
 }
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/RepoTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/RepoTest.java
index 7156827..0eee771 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/RepoTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/RepoTest.java
@@ -42,9 +42,9 @@
  */
 package org.eclipse.jgit.pgm;
 
-import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import java.io.File;
 import java.util.Arrays;
@@ -102,8 +102,12 @@ public void setUp() throws Exception {
 
 	@Test
 	public void testMissingPath() throws Exception {
-		assertEquals("fatal: Argument \"path\" is required",
-				executeUnchecked("git repo")[0]);
+		try {
+			execute("git repo");
+			fail("Must die");
+		} catch (Die e) {
+			// expected, requires argument
+		}
 	}
 
 	/**
diff --git a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
index 3dcbda3..a24b817 100644
--- a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
+++ b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties
@@ -20,6 +20,7 @@
 branchCreatedFrom=branch: Created from {0}
 branchDetachedHEAD=detached HEAD
 branchIsNotAnAncestorOfYourCurrentHEAD=The branch ''{0}'' is not an ancestor of your current HEAD.\nIf you are sure you want to delete it, run ''jgit branch -D {0}''.
+branchNameRequired=branch name required
 branchNotFound=branch ''{0}'' not found.
 cacheTreePathInfo="{0}": {1} entries, {2} children
 cannotBeRenamed={0} cannot be renamed
@@ -89,7 +90,9 @@
 metaVar_base=base
 metaVar_blameL=START,END
 metaVar_blameReverse=START..END
+metaVar_branchAndStartPoint=branch [start-name]
 metaVar_branchName=branch
+metaVar_branchNames=branch ...
 metaVar_bucket=BUCKET
 metaVar_command=command
 metaVar_commandDetail=DETAIL
@@ -109,6 +112,7 @@
 metaVar_n=n
 metaVar_name=name
 metaVar_object=object
+metaVar_oldNewBranchNames=[oldbranch] newbranch
 metaVar_op=OP
 metaVar_pass=PASS
 metaVar_path=path
@@ -125,6 +129,7 @@
 metaVar_uriish=uri-ish
 metaVar_url=URL
 metaVar_user=USER
+metaVar_values=value ...
 metaVar_version=VERSION
 mostCommonlyUsedCommandsAre=The most commonly used commands are:
 needApprovalToDestroyCurrentRepository=Need approval to destroy current repository
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Branch.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Branch.java
index 65aa24f..045f357 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Branch.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Branch.java
@@ -45,7 +45,6 @@
 
 import java.io.IOException;
 import java.text.MessageFormat;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -65,15 +64,18 @@
 import org.eclipse.jgit.lib.RefUpdate.Result;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.pgm.internal.CLIText;
-import org.eclipse.jgit.pgm.opt.CmdLineParser;
+import org.eclipse.jgit.pgm.opt.OptionWithValuesListHandler;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.kohsuke.args4j.Argument;
-import org.kohsuke.args4j.ExampleMode;
 import org.kohsuke.args4j.Option;
 
 @Command(common = true, usage = "usage_listCreateOrDeleteBranches")
 class Branch extends TextBuiltin {
 
+	private String otherBranch;
+	private boolean createForce;
+	private boolean rename;
+
 	@Option(name = "--remote", aliases = { "-r" }, usage = "usage_actOnRemoteTrackingBranches")
 	private boolean remote = false;
 
@@ -83,23 +85,69 @@ class Branch extends TextBuiltin {
 	@Option(name = "--contains", metaVar = "metaVar_commitish", usage = "usage_printOnlyBranchesThatContainTheCommit")
 	private String containsCommitish;
 
-	@Option(name = "--delete", aliases = { "-d" }, usage = "usage_deleteFullyMergedBranch")
-	private boolean delete = false;
+	private List<String> delete;
 
-	@Option(name = "--delete-force", aliases = { "-D" }, usage = "usage_deleteBranchEvenIfNotMerged")
-	private boolean deleteForce = false;
+	@Option(name = "--delete", aliases = {
+			"-d" }, metaVar = "metaVar_branchNames", usage = "usage_deleteFullyMergedBranch", handler = OptionWithValuesListHandler.class)
+	public void delete(List<String> names) {
+		if (names.isEmpty()) {
+			throw die(CLIText.get().branchNameRequired);
+		}
+		delete = names;
+	}
 
-	@Option(name = "--create-force", aliases = { "-f" }, usage = "usage_forceCreateBranchEvenExists")
-	private boolean createForce = false;
+	private List<String> deleteForce;
 
-	@Option(name = "-m", usage = "usage_moveRenameABranch")
-	private boolean rename = false;
+	@Option(name = "--delete-force", aliases = {
+			"-D" }, metaVar = "metaVar_branchNames", usage = "usage_deleteBranchEvenIfNotMerged", handler = OptionWithValuesListHandler.class)
+	public void deleteForce(List<String> names) {
+		if (names.isEmpty()) {
+			throw die(CLIText.get().branchNameRequired);
+		}
+		deleteForce = names;
+	}
+
+	@Option(name = "--create-force", aliases = {
+			"-f" }, metaVar = "metaVar_branchAndStartPoint", usage = "usage_forceCreateBranchEvenExists", handler = OptionWithValuesListHandler.class)
+	public void createForce(List<String> branchAndStartPoint) {
+		createForce = true;
+		if (branchAndStartPoint.isEmpty()) {
+			throw die(CLIText.get().branchNameRequired);
+		}
+		if (branchAndStartPoint.size() > 2) {
+			throw die(CLIText.get().tooManyRefsGiven);
+		}
+		if (branchAndStartPoint.size() == 1) {
+			branch = branchAndStartPoint.get(0);
+		} else {
+			branch = branchAndStartPoint.get(0);
+			otherBranch = branchAndStartPoint.get(1);
+		}
+	}
+
+	@Option(name = "--move", aliases = {
+			"-m" }, metaVar = "metaVar_oldNewBranchNames", usage = "usage_moveRenameABranch", handler = OptionWithValuesListHandler.class)
+	public void moveRename(List<String> currentAndNew) {
+		rename = true;
+		if (currentAndNew.isEmpty()) {
+			throw die(CLIText.get().branchNameRequired);
+		}
+		if (currentAndNew.size() > 2) {
+			throw die(CLIText.get().tooManyRefsGiven);
+		}
+		if (currentAndNew.size() == 1) {
+			branch = currentAndNew.get(0);
+		} else {
+			branch = currentAndNew.get(0);
+			otherBranch = currentAndNew.get(1);
+		}
+	}
 
 	@Option(name = "--verbose", aliases = { "-v" }, usage = "usage_beVerbose")
 	private boolean verbose = false;
 
-	@Argument
-	private List<String> branches = new ArrayList<String>();
+	@Argument(metaVar = "metaVar_name")
+	private String branch;
 
 	private final Map<String, Ref> printRefs = new LinkedHashMap<String, Ref>();
 
@@ -110,30 +158,33 @@ class Branch extends TextBuiltin {
 
 	@Override
 	protected void run() throws Exception {
-		if (delete || deleteForce)
-			delete(deleteForce);
-		else {
-			if (branches.size() > 2)
-				throw die(CLIText.get().tooManyRefsGiven + new CmdLineParser(this).printExample(ExampleMode.ALL));
-
+		if (delete != null || deleteForce != null) {
+			if (delete != null) {
+				delete(delete, false);
+			}
+			if (deleteForce != null) {
+				delete(deleteForce, true);
+			}
+		} else {
 			if (rename) {
 				String src, dst;
-				if (branches.size() == 1) {
+				if (otherBranch == null) {
 					final Ref head = db.getRef(Constants.HEAD);
-					if (head != null && head.isSymbolic())
+					if (head != null && head.isSymbolic()) {
 						src = head.getLeaf().getName();
-					else
+					} else {
 						throw die(CLIText.get().cannotRenameDetachedHEAD);
-					dst = branches.get(0);
+					}
+					dst = branch;
 				} else {
-					src = branches.get(0);
+					src = branch;
 					final Ref old = db.getRef(src);
 					if (old == null)
 						throw die(MessageFormat.format(CLIText.get().doesNotExist, src));
 					if (!old.getName().startsWith(Constants.R_HEADS))
 						throw die(MessageFormat.format(CLIText.get().notABranch, src));
 					src = old.getName();
-					dst = branches.get(1);
+					dst = otherBranch;
 				}
 
 				if (!dst.startsWith(Constants.R_HEADS))
@@ -145,13 +196,14 @@ protected void run() throws Exception {
 				if (r.rename() != Result.RENAMED)
 					throw die(MessageFormat.format(CLIText.get().cannotBeRenamed, src));
 
-			} else if (branches.size() > 0) {
-				String newHead = branches.get(0);
+			} else if (createForce || branch != null) {
+				String newHead = branch;
 				String startBranch;
-				if (branches.size() == 2)
-					startBranch = branches.get(1);
-				else
+				if (createForce) {
+					startBranch = otherBranch;
+				} else {
 					startBranch = Constants.HEAD;
+				}
 				Ref startRef = db.getRef(startBranch);
 				ObjectId startAt = db.resolve(startBranch + "^0"); //$NON-NLS-1$
 				if (startRef != null) {
@@ -164,22 +216,27 @@ protected void run() throws Exception {
 				}
 				startBranch = Repository.shortenRefName(startBranch);
 				String newRefName = newHead;
-				if (!newRefName.startsWith(Constants.R_HEADS))
+				if (!newRefName.startsWith(Constants.R_HEADS)) {
 					newRefName = Constants.R_HEADS + newRefName;
-				if (!Repository.isValidRefName(newRefName))
+				}
+				if (!Repository.isValidRefName(newRefName)) {
 					throw die(MessageFormat.format(CLIText.get().notAValidRefName, newRefName));
-				if (!createForce && db.resolve(newRefName) != null)
+				}
+				if (!createForce && db.resolve(newRefName) != null) {
 					throw die(MessageFormat.format(CLIText.get().branchAlreadyExists, newHead));
+				}
 				RefUpdate updateRef = db.updateRef(newRefName);
 				updateRef.setNewObjectId(startAt);
 				updateRef.setForceUpdate(createForce);
 				updateRef.setRefLogMessage(MessageFormat.format(CLIText.get().branchCreatedFrom, startBranch), false);
 				Result update = updateRef.update();
-				if (update == Result.REJECTED)
+				if (update == Result.REJECTED) {
 					throw die(MessageFormat.format(CLIText.get().couldNotCreateBranch, newHead, update.toString()));
+				}
 			} else {
-				if (verbose)
+				if (verbose) {
 					rw = new RevWalk(db);
+				}
 				list();
 			}
 		}
@@ -249,7 +306,8 @@ private void printHead(final ObjectReader reader, final String ref,
 		outw.println();
 	}
 
-	private void delete(boolean force) throws IOException {
+	private void delete(List<String> branches, boolean force)
+			throws IOException {
 		String current = db.getBranch();
 		ObjectId head = db.resolve(Constants.HEAD);
 		for (String branch : branches) {
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java
index f5d581a..fd86ddf 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/internal/CLIText.java
@@ -85,6 +85,7 @@ public static String formatLine(String line) {
 	/***/ public String branchCreatedFrom;
 	/***/ public String branchDetachedHEAD;
 	/***/ public String branchIsNotAnAncestorOfYourCurrentHEAD;
+	/***/ public String branchNameRequired;
 	/***/ public String branchNotFound;
 	/***/ public String cacheTreePathInfo;
 	/***/ public String configFileNotFound;
@@ -184,6 +185,7 @@ public static String formatLine(String line) {
 	/***/ public String metaVar_uriish;
 	/***/ public String metaVar_url;
 	/***/ public String metaVar_user;
+	/***/ public String metaVar_values;
 	/***/ public String metaVar_version;
 	/***/ public String mostCommonlyUsedCommandsAre;
 	/***/ public String needApprovalToDestroyCurrentRepository;
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/opt/CmdLineParser.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/opt/CmdLineParser.java
index 66d4816..a1e3950 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/opt/CmdLineParser.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/opt/CmdLineParser.java
@@ -86,6 +86,7 @@ public class CmdLineParser extends org.kohsuke.args4j.CmdLineParser {
 		registerHandler(RefSpec.class, RefSpecHandler.class);
 		registerHandler(RevCommit.class, RevCommitHandler.class);
 		registerHandler(RevTree.class, RevTreeHandler.class);
+		registerHandler(List.class, OptionWithValuesListHandler.class);
 	}
 
 	private final Repository db;
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/opt/OptionWithValuesListHandler.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/opt/OptionWithValuesListHandler.java
new file mode 100644
index 0000000..3de7a81
--- /dev/null
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/opt/OptionWithValuesListHandler.java
@@ -0,0 +1,52 @@
+package org.eclipse.jgit.pgm.opt;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jgit.pgm.internal.CLIText;
+import org.kohsuke.args4j.CmdLineException;
+import org.kohsuke.args4j.CmdLineParser;
+import org.kohsuke.args4j.OptionDef;
+import org.kohsuke.args4j.spi.OptionHandler;
+import org.kohsuke.args4j.spi.Parameters;
+import org.kohsuke.args4j.spi.Setter;
+
+/**
+ * Handler which allows to parse option with few values
+ *
+ * @since 4.2
+ */
+public class OptionWithValuesListHandler extends OptionHandler<List<?>> {
+
+	/**
+	 * @param parser
+	 * @param option
+	 * @param setter
+	 */
+	public OptionWithValuesListHandler(CmdLineParser parser,
+			OptionDef option, Setter<List<?>> setter) {
+		super(parser, option, setter);
+	}
+
+	@Override
+	public int parseArguments(Parameters params) throws CmdLineException {
+		final List<String> list = new ArrayList<>();
+		for (int idx = 0; idx < params.size(); idx++) {
+			final String p;
+			try {
+				p = params.getParameter(idx);
+			} catch (CmdLineException cle) {
+				break;
+			}
+			list.add(p);
+		}
+		setter.addValue(list);
+		return list.size();
+	}
+
+	@Override
+	public String getDefaultMetaVariable() {
+		return CLIText.get().metaVar_values;
+	}
+
+}
diff --git "a/org.eclipse.jgit.test/org.eclipse.jgit.core--All-Tests \050Java 8\051 \050de\051.launch" "b/org.eclipse.jgit.test/org.eclipse.jgit.core--All-Tests \050Java 8\051 \050de\051.launch"
new file mode 100644
index 0000000..f12a529
--- /dev/null
+++ "b/org.eclipse.jgit.test/org.eclipse.jgit.core--All-Tests \050Java 8\051 \050de\051.launch"
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<launchConfiguration type="org.eclipse.jdt.junit.launchconfig">
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
+<listEntry value="/org.eclipse.jgit.test/tst"/>
+</listAttribute>
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
+<listEntry value="2"/>
+</listAttribute>
+<booleanAttribute key="org.eclipse.debug.core.appendEnvironmentVariables" value="true"/>
+<mapAttribute key="org.eclipse.debug.core.environmentVariables">
+<mapEntry key="LANG" value="de_DE.UTF-8"/>
+</mapAttribute>
+<listAttribute key="org.eclipse.debug.ui.favoriteGroups">
+<listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
+<listEntry value="org.eclipse.debug.ui.launchGroup.run"/>
+</listAttribute>
+<stringAttribute key="org.eclipse.jdt.junit.CONTAINER" value="=org.eclipse.jgit.test/tst"/>
+<booleanAttribute key="org.eclipse.jdt.junit.KEEPRUNNING_ATTR" value="false"/>
+<stringAttribute key="org.eclipse.jdt.junit.TESTNAME" value=""/>
+<stringAttribute key="org.eclipse.jdt.junit.TEST_KIND" value="org.eclipse.jdt.junit.loader.junit4"/>
+<booleanAttribute key="org.eclipse.jdt.launching.ATTR_USE_START_ON_FIRST_THREAD" value="true"/>
+<listAttribute key="org.eclipse.jdt.launching.CLASSPATH">
+<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry containerPath=&quot;org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6&quot; path=&quot;1&quot; type=&quot;4&quot;/&gt;&#10;"/>
+<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry id=&quot;org.eclipse.jdt.launching.classpathentry.defaultClasspath&quot;&gt;&#10;&lt;memento exportedEntriesOnly=&quot;false&quot; project=&quot;org.eclipse.jgit.test&quot;/&gt;&#10;&lt;/runtimeClasspathEntry&gt;&#10;"/>
+</listAttribute>
+<booleanAttribute key="org.eclipse.jdt.launching.DEFAULT_CLASSPATH" value="false"/>
+<stringAttribute key="org.eclipse.jdt.launching.JRE_CONTAINER" value="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
+<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value=""/>
+<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="org.eclipse.jgit.test"/>
+<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Xmx256m"/>
+</launchConfiguration>
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 3abe81c..80230dc 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,8 +45,25 @@
 package org.eclipse.jgit.lib;
 
 import static java.lang.Integer.valueOf;
-import static java.lang.Long.valueOf;
+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;
+import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT;
+import static org.eclipse.jgit.lib.Constants.OBJ_TAG;
+import static org.eclipse.jgit.lib.Constants.OBJ_TREE;
+import static org.eclipse.jgit.lib.Constants.encode;
+import static org.eclipse.jgit.lib.Constants.encodeASCII;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.DUPLICATE_ENTRIES;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.EMPTY_NAME;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.FULL_PATHNAME;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.HAS_DOT;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.HAS_DOTDOT;
+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.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
 import static org.junit.Assert.fail;
 
 import java.io.UnsupportedEncodingException;
@@ -67,15 +84,10 @@ public void setUp() throws Exception {
 
 	@Test
 	public void testInvalidType() {
-		try {
-			checker.check(Constants.OBJ_BAD, new byte[0]);
-			fail("Did not throw CorruptObjectException");
-		} catch (CorruptObjectException e) {
-			final String m = e.getMessage();
-			assertEquals(MessageFormat.format(
-					JGitText.get().corruptObjectInvalidType2,
-					valueOf(Constants.OBJ_BAD)), m);
-		}
+		String msg = MessageFormat.format(
+				JGitText.get().corruptObjectInvalidType2,
+				valueOf(OBJ_BAD));
+		assertCorrupt(msg, OBJ_BAD, new byte[0]);
 	}
 
 	@Test
@@ -84,13 +96,13 @@ public void testCheckBlob() throws CorruptObjectException {
 		checker.checkBlob(new byte[0]);
 		checker.checkBlob(new byte[1]);
 
-		checker.check(Constants.OBJ_BLOB, new byte[0]);
-		checker.check(Constants.OBJ_BLOB, new byte[1]);
+		checker.check(OBJ_BLOB, new byte[0]);
+		checker.check(OBJ_BLOB, new byte[1]);
 	}
 
 	@Test
 	public void testValidCommitNoParent() throws CorruptObjectException {
-		final StringBuilder b = new StringBuilder();
+		StringBuilder b = new StringBuilder();
 
 		b.append("tree ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
@@ -99,14 +111,14 @@ public void testValidCommitNoParent() throws CorruptObjectException {
 		b.append("author A. U. Thor <author@localhost> 1 +0000\n");
 		b.append("committer A. U. Thor <author@localhost> 1 +0000\n");
 
-		final byte[] data = Constants.encodeASCII(b.toString());
+		byte[] data = encodeASCII(b.toString());
 		checker.checkCommit(data);
-		checker.check(Constants.OBJ_COMMIT, data);
+		checker.check(OBJ_COMMIT, data);
 	}
 
 	@Test
 	public void testValidCommitBlankAuthor() throws CorruptObjectException {
-		final StringBuilder b = new StringBuilder();
+		StringBuilder b = new StringBuilder();
 
 		b.append("tree ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
@@ -115,9 +127,9 @@ public void testValidCommitBlankAuthor() throws CorruptObjectException {
 		b.append("author <> 0 +0000\n");
 		b.append("committer <> 0 +0000\n");
 
-		final byte[] data = Constants.encodeASCII(b.toString());
+		byte[] data = encodeASCII(b.toString());
 		checker.checkCommit(data);
-		checker.check(Constants.OBJ_COMMIT, data);
+		checker.check(OBJ_COMMIT, data);
 	}
 
 	@Test
@@ -127,15 +139,13 @@ public void testCommitCorruptAuthor() throws CorruptObjectException {
 		b.append("author b <b@c> <b@c> 0 +0000\n");
 		b.append("committer <> 0 +0000\n");
 
-		byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkCommit(data);
-			fail("Did not catch corrupt object");
-		} catch (CorruptObjectException e) {
-			assertEquals("invalid author", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		assertCorrupt("bad date", OBJ_COMMIT, data);
 		checker.setAllowInvalidPersonIdent(true);
 		checker.checkCommit(data);
+
+		checker.setAllowInvalidPersonIdent(false);
+		assertSkipListAccepts(OBJ_COMMIT, data);
 	}
 
 	@Test
@@ -145,20 +155,18 @@ public void testCommitCorruptCommitter() throws CorruptObjectException {
 		b.append("author <> 0 +0000\n");
 		b.append("committer b <b@c> <b@c> 0 +0000\n");
 
-		byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkCommit(data);
-			fail("Did not catch corrupt object");
-		} catch (CorruptObjectException e) {
-			assertEquals("invalid committer", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		assertCorrupt("bad date", OBJ_COMMIT, data);
 		checker.setAllowInvalidPersonIdent(true);
 		checker.checkCommit(data);
+
+		checker.setAllowInvalidPersonIdent(false);
+		assertSkipListAccepts(OBJ_COMMIT, data);
 	}
 
 	@Test
 	public void testValidCommit1Parent() throws CorruptObjectException {
-		final StringBuilder b = new StringBuilder();
+		StringBuilder b = new StringBuilder();
 
 		b.append("tree ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
@@ -171,14 +179,14 @@ public void testValidCommit1Parent() throws CorruptObjectException {
 		b.append("author A. U. Thor <author@localhost> 1 +0000\n");
 		b.append("committer A. U. Thor <author@localhost> 1 +0000\n");
 
-		final byte[] data = Constants.encodeASCII(b.toString());
+		byte[] data = encodeASCII(b.toString());
 		checker.checkCommit(data);
-		checker.check(Constants.OBJ_COMMIT, data);
+		checker.check(OBJ_COMMIT, data);
 	}
 
 	@Test
 	public void testValidCommit2Parent() throws CorruptObjectException {
-		final StringBuilder b = new StringBuilder();
+		StringBuilder b = new StringBuilder();
 
 		b.append("tree ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
@@ -195,14 +203,14 @@ public void testValidCommit2Parent() throws CorruptObjectException {
 		b.append("author A. U. Thor <author@localhost> 1 +0000\n");
 		b.append("committer A. U. Thor <author@localhost> 1 +0000\n");
 
-		final byte[] data = Constants.encodeASCII(b.toString());
+		byte[] data = encodeASCII(b.toString());
 		checker.checkCommit(data);
-		checker.check(Constants.OBJ_COMMIT, data);
+		checker.check(OBJ_COMMIT, data);
 	}
 
 	@Test
 	public void testValidCommit128Parent() throws CorruptObjectException {
-		final StringBuilder b = new StringBuilder();
+		StringBuilder b = new StringBuilder();
 
 		b.append("tree ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
@@ -217,15 +225,15 @@ public void testValidCommit128Parent() throws CorruptObjectException {
 		b.append("author A. U. Thor <author@localhost> 1 +0000\n");
 		b.append("committer A. U. Thor <author@localhost> 1 +0000\n");
 
-		final byte[] data = Constants.encodeASCII(b.toString());
+		byte[] data = encodeASCII(b.toString());
 		checker.checkCommit(data);
-		checker.check(Constants.OBJ_COMMIT, data);
+		checker.check(OBJ_COMMIT, data);
 	}
 
 	@Test
 	public void testValidCommitNormalTime() throws CorruptObjectException {
-		final StringBuilder b = new StringBuilder();
-		final String when = "1222757360 -0730";
+		StringBuilder b = new StringBuilder();
+		String when = "1222757360 -0730";
 
 		b.append("tree ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
@@ -234,843 +242,539 @@ public void testValidCommitNormalTime() throws CorruptObjectException {
 		b.append("author A. U. Thor <author@localhost> " + when + "\n");
 		b.append("committer A. U. Thor <author@localhost> " + when + "\n");
 
-		final byte[] data = Constants.encodeASCII(b.toString());
+		byte[] data = encodeASCII(b.toString());
 		checker.checkCommit(data);
-		checker.check(Constants.OBJ_COMMIT, data);
+		checker.check(OBJ_COMMIT, data);
 	}
 
 	@Test
 	public void testInvalidCommitNoTree1() {
-		final StringBuilder b = new StringBuilder();
-
+		StringBuilder b = new StringBuilder();
 		b.append("parent ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append('\n');
-
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkCommit(data);
-			fail("Did not catch corrupt object");
-		} catch (CorruptObjectException e) {
-			assertEquals("no tree header", e.getMessage());
-		}
+		assertCorrupt("no tree header", OBJ_COMMIT, b);
 	}
 
 	@Test
 	public void testInvalidCommitNoTree2() {
-		final StringBuilder b = new StringBuilder();
-
+		StringBuilder b = new StringBuilder();
 		b.append("trie ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append('\n');
-
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkCommit(data);
-			fail("Did not catch corrupt object");
-		} catch (CorruptObjectException e) {
-			assertEquals("no tree header", e.getMessage());
-		}
+		assertCorrupt("no tree header", OBJ_COMMIT, b);
 	}
 
 	@Test
 	public void testInvalidCommitNoTree3() {
-		final StringBuilder b = new StringBuilder();
-
+		StringBuilder b = new StringBuilder();
 		b.append("tree");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append('\n');
-
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkCommit(data);
-			fail("Did not catch corrupt object");
-		} catch (CorruptObjectException e) {
-			assertEquals("no tree header", e.getMessage());
-		}
+		assertCorrupt("no tree header", OBJ_COMMIT, b);
 	}
 
 	@Test
 	public void testInvalidCommitNoTree4() {
-		final StringBuilder b = new StringBuilder();
-
+		StringBuilder b = new StringBuilder();
 		b.append("tree\t");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append('\n');
-
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkCommit(data);
-			fail("Did not catch corrupt object");
-		} catch (CorruptObjectException e) {
-			assertEquals("no tree header", e.getMessage());
-		}
+		assertCorrupt("no tree header", OBJ_COMMIT, b);
 	}
 
 	@Test
 	public void testInvalidCommitInvalidTree1() {
-		final StringBuilder b = new StringBuilder();
-
+		StringBuilder b = new StringBuilder();
 		b.append("tree ");
 		b.append("zzzzfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append('\n');
-
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkCommit(data);
-			fail("Did not catch corrupt object");
-		} catch (CorruptObjectException e) {
-			assertEquals("invalid tree", e.getMessage());
-		}
+		assertCorrupt("invalid tree", OBJ_COMMIT, b);
 	}
 
 	@Test
 	public void testInvalidCommitInvalidTree2() {
-		final StringBuilder b = new StringBuilder();
-
+		StringBuilder b = new StringBuilder();
 		b.append("tree ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append("z\n");
-
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkCommit(data);
-			fail("Did not catch corrupt object");
-		} catch (CorruptObjectException e) {
-			assertEquals("invalid tree", e.getMessage());
-		}
+		assertCorrupt("invalid tree", OBJ_COMMIT, b);
 	}
 
 	@Test
 	public void testInvalidCommitInvalidTree3() {
-		final StringBuilder b = new StringBuilder();
-
+		StringBuilder b = new StringBuilder();
 		b.append("tree ");
 		b.append("be9b");
 		b.append("\n");
 
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkCommit(data);
-			fail("Did not catch corrupt object");
-		} catch (CorruptObjectException e) {
-			assertEquals("invalid tree", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		assertCorrupt("invalid tree", OBJ_COMMIT, data);
 	}
 
 	@Test
 	public void testInvalidCommitInvalidTree4() {
-		final StringBuilder b = new StringBuilder();
-
+		StringBuilder b = new StringBuilder();
 		b.append("tree  ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append('\n');
-
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkCommit(data);
-			fail("Did not catch corrupt object");
-		} catch (CorruptObjectException e) {
-			assertEquals("invalid tree", e.getMessage());
-		}
+		assertCorrupt("invalid tree", OBJ_COMMIT, b);
 	}
 
 	@Test
 	public void testInvalidCommitInvalidParent1() {
-		final StringBuilder b = new StringBuilder();
-
+		StringBuilder b = new StringBuilder();
 		b.append("tree ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append('\n');
-
 		b.append("parent ");
 		b.append("\n");
-
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkCommit(data);
-			fail("Did not catch corrupt object");
-		} catch (CorruptObjectException e) {
-			assertEquals("invalid parent", e.getMessage());
-		}
+		assertCorrupt("invalid parent", OBJ_COMMIT, b);
 	}
 
 	@Test
 	public void testInvalidCommitInvalidParent2() {
-		final StringBuilder b = new StringBuilder();
-
+		StringBuilder b = new StringBuilder();
 		b.append("tree ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append('\n');
-
 		b.append("parent ");
 		b.append("zzzzfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append("\n");
-
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkCommit(data);
-			fail("Did not catch corrupt object");
-		} catch (CorruptObjectException e) {
-			assertEquals("invalid parent", e.getMessage());
-		}
+		assertCorrupt("invalid parent", OBJ_COMMIT, b);
 	}
 
 	@Test
 	public void testInvalidCommitInvalidParent3() {
-		final StringBuilder b = new StringBuilder();
-
+		StringBuilder b = new StringBuilder();
 		b.append("tree ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append('\n');
-
 		b.append("parent  ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append("\n");
-
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkCommit(data);
-			fail("Did not catch corrupt object");
-		} catch (CorruptObjectException e) {
-			assertEquals("invalid parent", e.getMessage());
-		}
+		assertCorrupt("invalid parent", OBJ_COMMIT, b);
 	}
 
 	@Test
 	public void testInvalidCommitInvalidParent4() {
-		final StringBuilder b = new StringBuilder();
-
+		StringBuilder b = new StringBuilder();
 		b.append("tree ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append('\n');
-
 		b.append("parent  ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append("z\n");
-
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkCommit(data);
-			fail("Did not catch corrupt object");
-		} catch (CorruptObjectException e) {
-			assertEquals("invalid parent", e.getMessage());
-		}
+		assertCorrupt("invalid parent", OBJ_COMMIT, b);
 	}
 
 	@Test
 	public void testInvalidCommitInvalidParent5() {
-		final StringBuilder b = new StringBuilder();
-
+		StringBuilder b = new StringBuilder();
 		b.append("tree ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append('\n');
-
 		b.append("parent\t");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append("\n");
 
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkCommit(data);
-			fail("Did not catch corrupt object");
-		} catch (CorruptObjectException e) {
-			// Yes, really, we complain about author not being
-			// found as the invalid parent line wasn't consumed.
-			assertEquals("no author", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		// Yes, really, we complain about author not being
+		// found as the invalid parent line wasn't consumed.
+		assertCorrupt("no author", OBJ_COMMIT, data);
 	}
 
 	@Test
-	public void testInvalidCommitNoAuthor() {
-		final StringBuilder b = new StringBuilder();
-
+	public void testInvalidCommitNoAuthor() throws CorruptObjectException {
+		StringBuilder b = new StringBuilder();
 		b.append("tree ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append('\n');
-
 		b.append("committer A. U. Thor <author@localhost> 1 +0000\n");
 
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkCommit(data);
-			fail("Did not catch corrupt object");
-		} catch (CorruptObjectException e) {
-			// Yes, really, we complain about author not being
-			// found as the invalid parent line wasn't consumed.
-			assertEquals("no author", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		assertCorrupt("no author", OBJ_COMMIT, data);
+		assertSkipListAccepts(OBJ_COMMIT, data);
 	}
 
 	@Test
-	public void testInvalidCommitNoCommitter1() {
-		final StringBuilder b = new StringBuilder();
-
+	public void testInvalidCommitNoCommitter1() throws CorruptObjectException {
+		StringBuilder b = new StringBuilder();
 		b.append("tree ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append('\n');
-
 		b.append("author A. U. Thor <author@localhost> 1 +0000\n");
 
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkCommit(data);
-			fail("Did not catch corrupt object");
-		} catch (CorruptObjectException e) {
-			// Yes, really, we complain about author not being
-			// found as the invalid parent line wasn't consumed.
-			assertEquals("no committer", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		assertCorrupt("no committer", OBJ_COMMIT, data);
+		assertSkipListAccepts(OBJ_COMMIT, data);
 	}
 
 	@Test
-	public void testInvalidCommitNoCommitter2() {
-		final StringBuilder b = new StringBuilder();
-
+	public void testInvalidCommitNoCommitter2() throws CorruptObjectException {
+		StringBuilder b = new StringBuilder();
 		b.append("tree ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append('\n');
-
 		b.append("author A. U. Thor <author@localhost> 1 +0000\n");
 		b.append("\n");
 
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkCommit(data);
-			fail("Did not catch corrupt object");
-		} catch (CorruptObjectException e) {
-			// Yes, really, we complain about author not being
-			// found as the invalid parent line wasn't consumed.
-			assertEquals("no committer", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		assertCorrupt("no committer", OBJ_COMMIT, data);
+		assertSkipListAccepts(OBJ_COMMIT, data);
 	}
 
 	@Test
-	public void testInvalidCommitInvalidAuthor1() {
-		final StringBuilder b = new StringBuilder();
-
+	public void testInvalidCommitInvalidAuthor1()
+			throws CorruptObjectException {
+		StringBuilder b = new StringBuilder();
 		b.append("tree ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append('\n');
-
 		b.append("author A. U. Thor <foo 1 +0000\n");
 
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkCommit(data);
-			fail("Did not catch corrupt object");
-		} catch (CorruptObjectException e) {
-			// Yes, really, we complain about author not being
-			// found as the invalid parent line wasn't consumed.
-			assertEquals("invalid author", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		assertCorrupt("bad email", OBJ_COMMIT, data);
+		assertSkipListAccepts(OBJ_COMMIT, data);
 	}
 
 	@Test
-	public void testInvalidCommitInvalidAuthor2() {
-		final StringBuilder b = new StringBuilder();
-
+	public void testInvalidCommitInvalidAuthor2()
+			throws CorruptObjectException {
+		StringBuilder b = new StringBuilder();
 		b.append("tree ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append('\n');
-
 		b.append("author A. U. Thor foo> 1 +0000\n");
 
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkCommit(data);
-			fail("Did not catch corrupt object");
-		} catch (CorruptObjectException e) {
-			// Yes, really, we complain about author not being
-			// found as the invalid parent line wasn't consumed.
-			assertEquals("invalid author", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		assertCorrupt("missing email", OBJ_COMMIT, data);
+		assertSkipListAccepts(OBJ_COMMIT, data);
 	}
 
 	@Test
-	public void testInvalidCommitInvalidAuthor3() {
-		final StringBuilder b = new StringBuilder();
-
+	public void testInvalidCommitInvalidAuthor3()
+			throws CorruptObjectException {
+		StringBuilder b = new StringBuilder();
 		b.append("tree ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append('\n');
-
 		b.append("author 1 +0000\n");
 
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkCommit(data);
-			fail("Did not catch corrupt object");
-		} catch (CorruptObjectException e) {
-			// Yes, really, we complain about author not being
-			// found as the invalid parent line wasn't consumed.
-			assertEquals("invalid author", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		assertCorrupt("missing email", OBJ_COMMIT, data);
+		assertSkipListAccepts(OBJ_COMMIT, data);
 	}
 
 	@Test
-	public void testInvalidCommitInvalidAuthor4() {
-		final StringBuilder b = new StringBuilder();
-
+	public void testInvalidCommitInvalidAuthor4()
+			throws CorruptObjectException {
+		StringBuilder b = new StringBuilder();
 		b.append("tree ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append('\n');
-
 		b.append("author a <b> +0000\n");
 
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkCommit(data);
-			fail("Did not catch corrupt object");
-		} catch (CorruptObjectException e) {
-			// Yes, really, we complain about author not being
-			// found as the invalid parent line wasn't consumed.
-			assertEquals("invalid author", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		assertCorrupt("bad date", OBJ_COMMIT, data);
+		assertSkipListAccepts(OBJ_COMMIT, data);
 	}
 
 	@Test
-	public void testInvalidCommitInvalidAuthor5() {
-		final StringBuilder b = new StringBuilder();
-
+	public void testInvalidCommitInvalidAuthor5()
+			throws CorruptObjectException {
+		StringBuilder b = new StringBuilder();
 		b.append("tree ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append('\n');
-
 		b.append("author a <b>\n");
 
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkCommit(data);
-			fail("Did not catch corrupt object");
-		} catch (CorruptObjectException e) {
-			// Yes, really, we complain about author not being
-			// found as the invalid parent line wasn't consumed.
-			assertEquals("invalid author", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		assertCorrupt("bad date", OBJ_COMMIT, data);
+		assertSkipListAccepts(OBJ_COMMIT, data);
 	}
 
 	@Test
-	public void testInvalidCommitInvalidAuthor6() {
-		final StringBuilder b = new StringBuilder();
-
+	public void testInvalidCommitInvalidAuthor6()
+			throws CorruptObjectException {
+		StringBuilder b = new StringBuilder();
 		b.append("tree ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append('\n');
-
 		b.append("author a <b> z");
 
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkCommit(data);
-			fail("Did not catch corrupt object");
-		} catch (CorruptObjectException e) {
-			// Yes, really, we complain about author not being
-			// found as the invalid parent line wasn't consumed.
-			assertEquals("invalid author", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		assertCorrupt("bad date", OBJ_COMMIT, data);
+		assertSkipListAccepts(OBJ_COMMIT, data);
 	}
 
 	@Test
-	public void testInvalidCommitInvalidAuthor7() {
-		final StringBuilder b = new StringBuilder();
-
+	public void testInvalidCommitInvalidAuthor7()
+			throws CorruptObjectException {
+		StringBuilder b = new StringBuilder();
 		b.append("tree ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append('\n');
-
 		b.append("author a <b> 1 z");
 
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkCommit(data);
-			fail("Did not catch corrupt object");
-		} catch (CorruptObjectException e) {
-			// Yes, really, we complain about author not being
-			// found as the invalid parent line wasn't consumed.
-			assertEquals("invalid author", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		assertCorrupt("bad time zone", OBJ_COMMIT, data);
+		assertSkipListAccepts(OBJ_COMMIT, data);
 	}
 
 	@Test
-	public void testInvalidCommitInvalidCommitter() {
-		final StringBuilder b = new StringBuilder();
-
+	public void testInvalidCommitInvalidCommitter()
+			throws CorruptObjectException {
+		StringBuilder b = new StringBuilder();
 		b.append("tree ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append('\n');
-
 		b.append("author a <b> 1 +0000\n");
 		b.append("committer a <");
 
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkCommit(data);
-			fail("Did not catch corrupt object");
-		} catch (CorruptObjectException e) {
-			// Yes, really, we complain about author not being
-			// found as the invalid parent line wasn't consumed.
-			assertEquals("invalid committer", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		assertCorrupt("bad email", OBJ_COMMIT, data);
+		assertSkipListAccepts(OBJ_COMMIT, data);
 	}
 
 	@Test
 	public void testValidTag() throws CorruptObjectException {
-		final StringBuilder b = new StringBuilder();
-
+		StringBuilder b = new StringBuilder();
 		b.append("object ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		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");
 
-		final byte[] data = Constants.encodeASCII(b.toString());
+		byte[] data = encodeASCII(b.toString());
 		checker.checkTag(data);
-		checker.check(Constants.OBJ_TAG, data);
+		checker.check(OBJ_TAG, data);
 	}
 
 	@Test
 	public void testInvalidTagNoObject1() {
-		final StringBuilder b = new StringBuilder();
-
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTag(data);
-			fail("incorrectly accepted invalid tag");
-		} catch (CorruptObjectException e) {
-			assertEquals("no object header", e.getMessage());
-		}
+		assertCorrupt("no object header", OBJ_TAG, new byte[0]);
 	}
 
 	@Test
 	public void testInvalidTagNoObject2() {
-		final StringBuilder b = new StringBuilder();
-
+		StringBuilder b = new StringBuilder();
 		b.append("object\t");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append('\n');
-
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTag(data);
-			fail("incorrectly accepted invalid tag");
-		} catch (CorruptObjectException e) {
-			assertEquals("no object header", e.getMessage());
-		}
+		assertCorrupt("no object header", OBJ_TAG, b);
 	}
 
 	@Test
 	public void testInvalidTagNoObject3() {
-		final StringBuilder b = new StringBuilder();
-
+		StringBuilder b = new StringBuilder();
 		b.append("obejct ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append('\n');
-
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTag(data);
-			fail("incorrectly accepted invalid tag");
-		} catch (CorruptObjectException e) {
-			assertEquals("no object header", e.getMessage());
-		}
+		assertCorrupt("no object header", OBJ_TAG, b);
 	}
 
 	@Test
 	public void testInvalidTagNoObject4() {
-		final StringBuilder b = new StringBuilder();
-
+		StringBuilder b = new StringBuilder();
 		b.append("object ");
 		b.append("zz9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append('\n');
-
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTag(data);
-			fail("incorrectly accepted invalid tag");
-		} catch (CorruptObjectException e) {
-			assertEquals("invalid object", e.getMessage());
-		}
+		assertCorrupt("invalid object", OBJ_TAG, b);
 	}
 
 	@Test
 	public void testInvalidTagNoObject5() {
-		final StringBuilder b = new StringBuilder();
-
+		StringBuilder b = new StringBuilder();
 		b.append("object ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append(" \n");
-
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTag(data);
-			fail("incorrectly accepted invalid tag");
-		} catch (CorruptObjectException e) {
-			assertEquals("invalid object", e.getMessage());
-		}
+		assertCorrupt("invalid object", OBJ_TAG, b);
 	}
 
 	@Test
 	public void testInvalidTagNoObject6() {
-		final StringBuilder b = new StringBuilder();
-
+		StringBuilder b = new StringBuilder();
 		b.append("object ");
 		b.append("be9");
-
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTag(data);
-			fail("incorrectly accepted invalid tag");
-		} catch (CorruptObjectException e) {
-			assertEquals("invalid object", e.getMessage());
-		}
+		assertCorrupt("invalid object", OBJ_TAG, b);
 	}
 
 	@Test
 	public void testInvalidTagNoType1() {
-		final StringBuilder b = new StringBuilder();
-
+		StringBuilder b = new StringBuilder();
 		b.append("object ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append('\n');
-
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTag(data);
-			fail("incorrectly accepted invalid tag");
-		} catch (CorruptObjectException e) {
-			assertEquals("no type header", e.getMessage());
-		}
+		assertCorrupt("no type header", OBJ_TAG, b);
 	}
 
 	@Test
 	public void testInvalidTagNoType2() {
-		final StringBuilder b = new StringBuilder();
-
+		StringBuilder b = new StringBuilder();
 		b.append("object ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append('\n');
-
 		b.append("type\tcommit\n");
-
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTag(data);
-			fail("incorrectly accepted invalid tag");
-		} catch (CorruptObjectException e) {
-			assertEquals("no type header", e.getMessage());
-		}
+		assertCorrupt("no type header", OBJ_TAG, b);
 	}
 
 	@Test
 	public void testInvalidTagNoType3() {
-		final StringBuilder b = new StringBuilder();
-
+		StringBuilder b = new StringBuilder();
 		b.append("object ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append('\n');
-
 		b.append("tpye commit\n");
-
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTag(data);
-			fail("incorrectly accepted invalid tag");
-		} catch (CorruptObjectException e) {
-			assertEquals("no type header", e.getMessage());
-		}
+		assertCorrupt("no type header", OBJ_TAG, b);
 	}
 
 	@Test
 	public void testInvalidTagNoType4() {
-		final StringBuilder b = new StringBuilder();
-
+		StringBuilder b = new StringBuilder();
 		b.append("object ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append('\n');
-
 		b.append("type commit");
-
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTag(data);
-			fail("incorrectly accepted invalid tag");
-		} catch (CorruptObjectException e) {
-			assertEquals("no tag header", e.getMessage());
-		}
+		assertCorrupt("no tag header", OBJ_TAG, b);
 	}
 
 	@Test
 	public void testInvalidTagNoTagHeader1() {
-		final StringBuilder b = new StringBuilder();
-
+		StringBuilder b = new StringBuilder();
 		b.append("object ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append('\n');
-
 		b.append("type commit\n");
-
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTag(data);
-			fail("incorrectly accepted invalid tag");
-		} catch (CorruptObjectException e) {
-			assertEquals("no tag header", e.getMessage());
-		}
+		assertCorrupt("no tag header", OBJ_TAG, b);
 	}
 
 	@Test
 	public void testInvalidTagNoTagHeader2() {
-		final StringBuilder b = new StringBuilder();
-
+		StringBuilder b = new StringBuilder();
 		b.append("object ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append('\n');
-
 		b.append("type commit\n");
 		b.append("tag\tfoo\n");
-
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTag(data);
-			fail("incorrectly accepted invalid tag");
-		} catch (CorruptObjectException e) {
-			assertEquals("no tag header", e.getMessage());
-		}
+		assertCorrupt("no tag header", OBJ_TAG, b);
 	}
 
 	@Test
 	public void testInvalidTagNoTagHeader3() {
-		final StringBuilder b = new StringBuilder();
-
+		StringBuilder b = new StringBuilder();
 		b.append("object ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append('\n');
-
 		b.append("type commit\n");
 		b.append("tga foo\n");
-
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTag(data);
-			fail("incorrectly accepted invalid tag");
-		} catch (CorruptObjectException e) {
-			assertEquals("no tag header", e.getMessage());
-		}
+		assertCorrupt("no tag header", OBJ_TAG, b);
 	}
 
 	@Test
 	public void testValidTagHasNoTaggerHeader() throws CorruptObjectException {
-		final StringBuilder b = new StringBuilder();
-
+		StringBuilder b = new StringBuilder();
 		b.append("object ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append('\n');
-
 		b.append("type commit\n");
 		b.append("tag foo\n");
-
-		checker.checkTag(Constants.encodeASCII(b.toString()));
+		checker.checkTag(encodeASCII(b.toString()));
 	}
 
 	@Test
 	public void testInvalidTagInvalidTaggerHeader1()
 			throws CorruptObjectException {
-		final StringBuilder b = new StringBuilder();
-
+		StringBuilder b = new StringBuilder();
 		b.append("object ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append('\n');
-
 		b.append("type commit\n");
 		b.append("tag foo\n");
 		b.append("tagger \n");
 
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTag(data);
-			fail("incorrectly accepted invalid tag");
-		} catch (CorruptObjectException e) {
-			assertEquals("invalid tagger", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		assertCorrupt("missing email", OBJ_TAG, data);
 		checker.setAllowInvalidPersonIdent(true);
 		checker.checkTag(data);
+
+		checker.setAllowInvalidPersonIdent(false);
+		assertSkipListAccepts(OBJ_TAG, data);
 	}
 
 	@Test
-	public void testInvalidTagInvalidTaggerHeader3() {
-		final StringBuilder b = new StringBuilder();
-
+	public void testInvalidTagInvalidTaggerHeader3()
+			throws CorruptObjectException {
+		StringBuilder b = new StringBuilder();
 		b.append("object ");
 		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
 		b.append('\n');
-
 		b.append("type commit\n");
 		b.append("tag foo\n");
 		b.append("tagger a < 1 +000\n");
 
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTag(data);
-			fail("incorrectly accepted invalid tag");
-		} catch (CorruptObjectException e) {
-			assertEquals("invalid tagger", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		assertCorrupt("bad email", OBJ_TAG, data);
+		assertSkipListAccepts(OBJ_TAG, data);
 	}
 
 	@Test
 	public void testValidEmptyTree() throws CorruptObjectException {
 		checker.checkTree(new byte[0]);
-		checker.check(Constants.OBJ_TREE, new byte[0]);
+		checker.check(OBJ_TREE, new byte[0]);
 	}
 
 	@Test
 	public void testValidTree1() throws CorruptObjectException {
-		final StringBuilder b = new StringBuilder();
+		StringBuilder b = new StringBuilder();
 		entry(b, "100644 regular-file");
-		final byte[] data = Constants.encodeASCII(b.toString());
-		checker.checkTree(data);
+		checker.checkTree(encodeASCII(b.toString()));
 	}
 
 	@Test
 	public void testValidTree2() throws CorruptObjectException {
-		final StringBuilder b = new StringBuilder();
+		StringBuilder b = new StringBuilder();
 		entry(b, "100755 executable");
-		final byte[] data = Constants.encodeASCII(b.toString());
-		checker.checkTree(data);
+		checker.checkTree(encodeASCII(b.toString()));
 	}
 
 	@Test
 	public void testValidTree3() throws CorruptObjectException {
-		final StringBuilder b = new StringBuilder();
+		StringBuilder b = new StringBuilder();
 		entry(b, "40000 tree");
-		final byte[] data = Constants.encodeASCII(b.toString());
-		checker.checkTree(data);
+		checker.checkTree(encodeASCII(b.toString()));
 	}
 
 	@Test
 	public void testValidTree4() throws CorruptObjectException {
-		final StringBuilder b = new StringBuilder();
+		StringBuilder b = new StringBuilder();
 		entry(b, "120000 symlink");
-		final byte[] data = Constants.encodeASCII(b.toString());
-		checker.checkTree(data);
+		checker.checkTree(encodeASCII(b.toString()));
 	}
 
 	@Test
 	public void testValidTree5() throws CorruptObjectException {
-		final StringBuilder b = new StringBuilder();
+		StringBuilder b = new StringBuilder();
 		entry(b, "160000 git link");
-		final byte[] data = Constants.encodeASCII(b.toString());
-		checker.checkTree(data);
+		checker.checkTree(encodeASCII(b.toString()));
 	}
 
 	@Test
 	public void testValidTree6() throws CorruptObjectException {
-		final StringBuilder b = new StringBuilder();
+		StringBuilder b = new StringBuilder();
 		entry(b, "100644 .a");
-		final byte[] data = Constants.encodeASCII(b.toString());
+		checker.checkTree(encodeASCII(b.toString()));
+	}
+
+	@Test
+	public void testNullSha1InTreeEntry() throws CorruptObjectException {
+		byte[] data = concat(
+				encodeASCII("100644 A"), new byte[] { '\0' },
+				new byte[OBJECT_ID_LENGTH]);
+		assertCorrupt("entry points to null SHA-1", OBJ_TREE, data);
+		assertSkipListAccepts(OBJ_TREE, data);
+		checker.setIgnore(NULL_SHA1, true);
 		checker.checkTree(data);
 	}
 
@@ -1084,357 +788,326 @@ public void testValidPosixTree() throws CorruptObjectException {
 
 	@Test
 	public void testValidTreeSorting1() throws CorruptObjectException {
-		final StringBuilder b = new StringBuilder();
+		StringBuilder b = new StringBuilder();
 		entry(b, "100644 fooaaa");
 		entry(b, "100755 foobar");
-		final byte[] data = Constants.encodeASCII(b.toString());
-		checker.checkTree(data);
+		checker.checkTree(encodeASCII(b.toString()));
 	}
 
 	@Test
 	public void testValidTreeSorting2() throws CorruptObjectException {
-		final StringBuilder b = new StringBuilder();
+		StringBuilder b = new StringBuilder();
 		entry(b, "100755 fooaaa");
 		entry(b, "100644 foobar");
-		final byte[] data = Constants.encodeASCII(b.toString());
-		checker.checkTree(data);
+		checker.checkTree(encodeASCII(b.toString()));
 	}
 
 	@Test
 	public void testValidTreeSorting3() throws CorruptObjectException {
-		final StringBuilder b = new StringBuilder();
+		StringBuilder b = new StringBuilder();
 		entry(b, "40000 a");
 		entry(b, "100644 b");
-		final byte[] data = Constants.encodeASCII(b.toString());
-		checker.checkTree(data);
+		checker.checkTree(encodeASCII(b.toString()));
 	}
 
 	@Test
 	public void testValidTreeSorting4() throws CorruptObjectException {
-		final StringBuilder b = new StringBuilder();
+		StringBuilder b = new StringBuilder();
 		entry(b, "100644 a");
 		entry(b, "40000 b");
-		final byte[] data = Constants.encodeASCII(b.toString());
-		checker.checkTree(data);
+		checker.checkTree(encodeASCII(b.toString()));
 	}
 
 	@Test
 	public void testValidTreeSorting5() throws CorruptObjectException {
-		final StringBuilder b = new StringBuilder();
+		StringBuilder b = new StringBuilder();
 		entry(b, "100644 a.c");
 		entry(b, "40000 a");
 		entry(b, "100644 a0c");
-		final byte[] data = Constants.encodeASCII(b.toString());
-		checker.checkTree(data);
+		checker.checkTree(encodeASCII(b.toString()));
 	}
 
 	@Test
 	public void testValidTreeSorting6() throws CorruptObjectException {
-		final StringBuilder b = new StringBuilder();
+		StringBuilder b = new StringBuilder();
 		entry(b, "40000 a");
 		entry(b, "100644 apple");
-		final byte[] data = Constants.encodeASCII(b.toString());
-		checker.checkTree(data);
+		checker.checkTree(encodeASCII(b.toString()));
 	}
 
 	@Test
 	public void testValidTreeSorting7() throws CorruptObjectException {
-		final StringBuilder b = new StringBuilder();
+		StringBuilder b = new StringBuilder();
 		entry(b, "40000 an orang");
 		entry(b, "40000 an orange");
-		final byte[] data = Constants.encodeASCII(b.toString());
-		checker.checkTree(data);
+		checker.checkTree(encodeASCII(b.toString()));
 	}
 
 	@Test
 	public void testValidTreeSorting8() throws CorruptObjectException {
-		final StringBuilder b = new StringBuilder();
+		StringBuilder b = new StringBuilder();
 		entry(b, "100644 a");
 		entry(b, "100644 a0c");
 		entry(b, "100644 b");
-		final byte[] data = Constants.encodeASCII(b.toString());
-		checker.checkTree(data);
+		checker.checkTree(encodeASCII(b.toString()));
 	}
 
 	@Test
 	public void testAcceptTreeModeWithZero() throws CorruptObjectException {
 		StringBuilder b = new StringBuilder();
 		entry(b, "040000 a");
+		byte[] data = encodeASCII(b.toString());
 		checker.setAllowLeadingZeroFileMode(true);
-		checker.checkTree(Constants.encodeASCII(b.toString()));
+		checker.checkTree(data);
+
+		checker.setAllowLeadingZeroFileMode(false);
+		assertSkipListAccepts(OBJ_TREE, data);
+
+		checker.setIgnore(ZERO_PADDED_FILEMODE, true);
+		checker.checkTree(data);
 	}
 
 	@Test
 	public void testInvalidTreeModeStartsWithZero1() {
-		final StringBuilder b = new StringBuilder();
+		StringBuilder b = new StringBuilder();
 		entry(b, "0 a");
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTree(data);
-			fail("incorrectly accepted an invalid tree");
-		} catch (CorruptObjectException e) {
-			assertEquals("mode starts with '0'", e.getMessage());
-		}
+		assertCorrupt("mode starts with '0'", OBJ_TREE, b);
 	}
 
 	@Test
 	public void testInvalidTreeModeStartsWithZero2() {
-		final StringBuilder b = new StringBuilder();
+		StringBuilder b = new StringBuilder();
 		entry(b, "0100644 a");
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTree(data);
-			fail("incorrectly accepted an invalid tree");
-		} catch (CorruptObjectException e) {
-			assertEquals("mode starts with '0'", e.getMessage());
-		}
+		assertCorrupt("mode starts with '0'", OBJ_TREE, b);
 	}
 
 	@Test
 	public void testInvalidTreeModeStartsWithZero3() {
-		final StringBuilder b = new StringBuilder();
+		StringBuilder b = new StringBuilder();
 		entry(b, "040000 a");
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTree(data);
-			fail("incorrectly accepted an invalid tree");
-		} catch (CorruptObjectException e) {
-			assertEquals("mode starts with '0'", e.getMessage());
-		}
+		assertCorrupt("mode starts with '0'", OBJ_TREE, b);
 	}
 
 	@Test
 	public void testInvalidTreeModeNotOctal1() {
-		final StringBuilder b = new StringBuilder();
+		StringBuilder b = new StringBuilder();
 		entry(b, "8 a");
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTree(data);
-			fail("incorrectly accepted an invalid tree");
-		} catch (CorruptObjectException e) {
-			assertEquals("invalid mode character", e.getMessage());
-		}
+		assertCorrupt("invalid mode character", OBJ_TREE, b);
 	}
 
 	@Test
 	public void testInvalidTreeModeNotOctal2() {
-		final StringBuilder b = new StringBuilder();
+		StringBuilder b = new StringBuilder();
 		entry(b, "Z a");
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTree(data);
-			fail("incorrectly accepted an invalid tree");
-		} catch (CorruptObjectException e) {
-			assertEquals("invalid mode character", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		assertCorrupt("invalid mode character", OBJ_TREE, data);
+		assertSkipListRejects("invalid mode character", OBJ_TREE, data);
 	}
 
 	@Test
 	public void testInvalidTreeModeNotSupportedMode1() {
-		final StringBuilder b = new StringBuilder();
+		StringBuilder b = new StringBuilder();
 		entry(b, "1 a");
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTree(data);
-			fail("incorrectly accepted an invalid tree");
-		} catch (CorruptObjectException e) {
-			assertEquals("invalid mode 1", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		assertCorrupt("invalid mode 1", OBJ_TREE, data);
+		assertSkipListRejects("invalid mode 1", OBJ_TREE, data);
 	}
 
 	@Test
 	public void testInvalidTreeModeNotSupportedMode2() {
-		final StringBuilder b = new StringBuilder();
+		StringBuilder b = new StringBuilder();
 		entry(b, "170000 a");
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTree(data);
-			fail("incorrectly accepted an invalid tree");
-		} catch (CorruptObjectException e) {
-			assertEquals("invalid mode " + 0170000, e.getMessage());
-		}
+		assertCorrupt("invalid mode " + 0170000, OBJ_TREE, b);
 	}
 
 	@Test
 	public void testInvalidTreeModeMissingName() {
-		final StringBuilder b = new StringBuilder();
+		StringBuilder b = new StringBuilder();
 		b.append("100644");
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTree(data);
-			fail("incorrectly accepted an invalid tree");
-		} catch (CorruptObjectException e) {
-			assertEquals("truncated in mode", e.getMessage());
-		}
+		assertCorrupt("truncated in mode", OBJ_TREE, b);
 	}
 
 	@Test
-	public void testInvalidTreeNameContainsSlash() {
-		final StringBuilder b = new StringBuilder();
+	public void testInvalidTreeNameContainsSlash()
+			throws CorruptObjectException {
+		StringBuilder b = new StringBuilder();
 		entry(b, "100644 a/b");
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTree(data);
-			fail("incorrectly accepted an invalid tree");
-		} catch (CorruptObjectException e) {
-			assertEquals("name contains '/'", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		assertCorrupt("name contains '/'", OBJ_TREE, data);
+		assertSkipListAccepts(OBJ_TREE, data);
+		checker.setIgnore(FULL_PATHNAME, true);
+		checker.checkTree(data);
 	}
 
 	@Test
-	public void testInvalidTreeNameIsEmpty() {
-		final StringBuilder b = new StringBuilder();
+	public void testInvalidTreeNameIsEmpty() throws CorruptObjectException {
+		StringBuilder b = new StringBuilder();
 		entry(b, "100644 ");
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTree(data);
-			fail("incorrectly accepted an invalid tree");
-		} catch (CorruptObjectException e) {
-			assertEquals("zero length name", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		assertCorrupt("zero length name", OBJ_TREE, data);
+		assertSkipListAccepts(OBJ_TREE, data);
+		checker.setIgnore(EMPTY_NAME, true);
+		checker.checkTree(data);
 	}
 
 	@Test
-	public void testInvalidTreeNameIsDot() {
-		final StringBuilder b = new StringBuilder();
+	public void testInvalidTreeNameIsDot() throws CorruptObjectException {
+		StringBuilder b = new StringBuilder();
 		entry(b, "100644 .");
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTree(data);
-			fail("incorrectly accepted an invalid tree");
-		} catch (CorruptObjectException e) {
-			assertEquals("invalid name '.'", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		assertCorrupt("invalid name '.'", OBJ_TREE, data);
+		assertSkipListAccepts(OBJ_TREE, data);
+		checker.setIgnore(HAS_DOT, true);
+		checker.checkTree(data);
 	}
 
 	@Test
-	public void testInvalidTreeNameIsDotDot() {
-		final StringBuilder b = new StringBuilder();
+	public void testInvalidTreeNameIsDotDot() throws CorruptObjectException {
+		StringBuilder b = new StringBuilder();
 		entry(b, "100644 ..");
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTree(data);
-			fail("incorrectly accepted an invalid tree");
-		} catch (CorruptObjectException e) {
-			assertEquals("invalid name '..'", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		assertCorrupt("invalid name '..'", OBJ_TREE, data);
+		assertSkipListAccepts(OBJ_TREE, data);
+		checker.setIgnore(HAS_DOTDOT, true);
+		checker.checkTree(data);
 	}
 
 	@Test
-	public void testInvalidTreeNameIsGit() {
+	public void testInvalidTreeNameIsGit() throws CorruptObjectException {
 		StringBuilder b = new StringBuilder();
 		entry(b, "100644 .git");
-		byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTree(data);
-			fail("incorrectly accepted an invalid tree");
-		} catch (CorruptObjectException e) {
-			assertEquals("invalid name '.git'", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		assertCorrupt("invalid name '.git'", OBJ_TREE, data);
+		assertSkipListAccepts(OBJ_TREE, data);
+		checker.setIgnore(HAS_DOTGIT, true);
+		checker.checkTree(data);
 	}
 
 	@Test
-	public void testInvalidTreeNameIsMixedCaseGit() {
+	public void testInvalidTreeNameIsMixedCaseGit()
+			throws CorruptObjectException {
 		StringBuilder b = new StringBuilder();
 		entry(b, "100644 .GiT");
-		byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTree(data);
-			fail("incorrectly accepted an invalid tree");
-		} catch (CorruptObjectException e) {
-			assertEquals("invalid name '.GiT'", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		assertCorrupt("invalid name '.GiT'", OBJ_TREE, data);
+		assertSkipListAccepts(OBJ_TREE, data);
+		checker.setIgnore(HAS_DOTGIT, true);
+		checker.checkTree(data);
 	}
 
 	@Test
-	public void testInvalidTreeNameIsMacHFSGit() {
+	public void testInvalidTreeNameIsMacHFSGit() throws CorruptObjectException {
 		StringBuilder b = new StringBuilder();
 		entry(b, "100644 .gi\u200Ct");
-		byte[] data = Constants.encode(b.toString());
-		try {
-			checker.setSafeForMacOS(true);
-			checker.checkTree(data);
-			fail("incorrectly accepted an invalid tree");
-		} catch (CorruptObjectException e) {
-			assertEquals(
-					"invalid name '.gi\u200Ct' contains ignorable Unicode characters",
-					e.getMessage());
-		}
+		byte[] data = encode(b.toString());
+
+		// Fine on POSIX.
+		checker.checkTree(data);
+
+		// Rejected on Mac OS.
+		checker.setSafeForMacOS(true);
+		assertCorrupt(
+				"invalid name '.gi\u200Ct' contains ignorable Unicode characters",
+				OBJ_TREE, data);
+		assertSkipListAccepts(OBJ_TREE, data);
+		checker.setIgnore(HAS_DOTGIT, true);
+		checker.checkTree(data);
 	}
 
 	@Test
-	public void testInvalidTreeNameIsMacHFSGit2() {
+	public void testInvalidTreeNameIsMacHFSGit2()
+			throws CorruptObjectException {
 		StringBuilder b = new StringBuilder();
 		entry(b, "100644 \u206B.git");
-		byte[] data = Constants.encode(b.toString());
-		try {
-			checker.setSafeForMacOS(true);
-			checker.checkTree(data);
-			fail("incorrectly accepted an invalid tree");
-		} catch (CorruptObjectException e) {
-			assertEquals(
-					"invalid name '\u206B.git' contains ignorable Unicode characters",
-					e.getMessage());
-		}
+		byte[] data = encode(b.toString());
+
+		// Fine on POSIX.
+		checker.checkTree(data);
+
+		// Rejected on Mac OS.
+		checker.setSafeForMacOS(true);
+		assertCorrupt(
+				"invalid name '\u206B.git' contains ignorable Unicode characters",
+				OBJ_TREE, data);
+		assertSkipListAccepts(OBJ_TREE, data);
+		checker.setIgnore(HAS_DOTGIT, true);
+		checker.checkTree(data);
 	}
 
 	@Test
-	public void testInvalidTreeNameIsMacHFSGit3() {
+	public void testInvalidTreeNameIsMacHFSGit3()
+			throws CorruptObjectException {
 		StringBuilder b = new StringBuilder();
 		entry(b, "100644 .git\uFEFF");
-		byte[] data = Constants.encode(b.toString());
-		try {
-			checker.setSafeForMacOS(true);
-			checker.checkTree(data);
-			fail("incorrectly accepted an invalid tree");
-		} catch (CorruptObjectException e) {
-			assertEquals(
-					"invalid name '.git\uFEFF' contains ignorable Unicode characters",
-					e.getMessage());
-		}
+		byte[] data = encode(b.toString());
+
+		// Fine on POSIX.
+		checker.checkTree(data);
+
+		// Rejected on Mac OS.
+		checker.setSafeForMacOS(true);
+		assertCorrupt(
+				"invalid name '.git\uFEFF' contains ignorable Unicode characters",
+				OBJ_TREE, data);
+		assertSkipListAccepts(OBJ_TREE, data);
+		checker.setIgnore(HAS_DOTGIT, true);
+		checker.checkTree(data);
 	}
 
-	private static byte[] concat(byte[] b1, byte[] b2) {
-		byte[] data = new byte[b1.length + b2.length];
-		System.arraycopy(b1, 0, data, 0, b1.length);
-		System.arraycopy(b2, 0, data, b1.length, b2.length);
+	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() {
-		byte[] data = concat(Constants.encode("100644 .git"),
+	public void testInvalidTreeNameIsMacHFSGitCorruptUTF8AtEnd()
+			throws CorruptObjectException {
+		byte[] data = concat(encode("100644 .git"),
 				new byte[] { (byte) 0xef });
 		StringBuilder b = new StringBuilder();
 		entry(b, "");
-		data = concat(data, Constants.encode(b.toString()));
-		try {
-			checker.setSafeForMacOS(true);
-			checker.checkTree(data);
-			fail("incorrectly accepted an invalid tree");
-		} catch (CorruptObjectException e) {
-			assertEquals(
-					"invalid name contains byte sequence '0xef' which is not a valid UTF-8 character",
-					e.getMessage());
-		}
+		data = concat(data, encode(b.toString()));
+
+		// Fine on POSIX.
+		checker.checkTree(data);
+
+		// Rejected on Mac OS.
+		checker.setSafeForMacOS(true);
+		assertCorrupt(
+				"invalid name contains byte sequence '0xef' which is not a valid UTF-8 character",
+				OBJ_TREE, data);
+		assertSkipListAccepts(OBJ_TREE, data);
 	}
 
 	@Test
-	public void testInvalidTreeNameIsMacHFSGitCorruptUTF8AtEnd2() {
-		byte[] data = concat(Constants.encode("100644 .git"), new byte[] {
+	public void testInvalidTreeNameIsMacHFSGitCorruptUTF8AtEnd2()
+			throws CorruptObjectException {
+		byte[] data = concat(encode("100644 .git"),
+				new byte[] {
 				(byte) 0xe2, (byte) 0xab });
 		StringBuilder b = new StringBuilder();
 		entry(b, "");
-		data = concat(data, Constants.encode(b.toString()));
-		try {
-			checker.setSafeForMacOS(true);
-			checker.checkTree(data);
-			fail("incorrectly accepted an invalid tree");
-		} catch (CorruptObjectException e) {
-			assertEquals(
-					"invalid name contains byte sequence '0xe2ab' which is not a valid UTF-8 character",
-					e.getMessage());
-		}
+		data = concat(data, encode(b.toString()));
+
+		// Fine on POSIX.
+		checker.checkTree(data);
+
+		// Rejected on Mac OS.
+		checker.setSafeForMacOS(true);
+		assertCorrupt(
+				"invalid name contains byte sequence '0xe2ab' which is not a valid UTF-8 character",
+				OBJ_TREE, data);
+		assertSkipListAccepts(OBJ_TREE, data);
 	}
 
 	@Test
@@ -1442,7 +1115,7 @@ public void testInvalidTreeNameIsNotMacHFSGit()
 			throws CorruptObjectException {
 		StringBuilder b = new StringBuilder();
 		entry(b, "100644 .git\u200Cx");
-		byte[] data = Constants.encode(b.toString());
+		byte[] data = encode(b.toString());
 		checker.setSafeForMacOS(true);
 		checker.checkTree(data);
 	}
@@ -1452,7 +1125,7 @@ public void testInvalidTreeNameIsNotMacHFSGit2()
 			throws CorruptObjectException {
 		StringBuilder b = new StringBuilder();
 		entry(b, "100644 .kit\u200C");
-		byte[] data = Constants.encode(b.toString());
+		byte[] data = encode(b.toString());
 		checker.setSafeForMacOS(true);
 		checker.checkTree(data);
 	}
@@ -1462,21 +1135,19 @@ public void testInvalidTreeNameIsNotMacHFSGitOtherPlatform()
 			throws CorruptObjectException {
 		StringBuilder b = new StringBuilder();
 		entry(b, "100644 .git\u200C");
-		byte[] data = Constants.encode(b.toString());
+		byte[] data = encode(b.toString());
 		checker.checkTree(data);
 	}
 
 	@Test
-	public void testInvalidTreeNameIsDotGitDot() {
+	public void testInvalidTreeNameIsDotGitDot() throws CorruptObjectException {
 		StringBuilder b = new StringBuilder();
 		entry(b, "100644 .git.");
-		byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTree(data);
-			fail("incorrectly accepted an invalid tree");
-		} catch (CorruptObjectException e) {
-			assertEquals("invalid name '.git.'", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		assertCorrupt("invalid name '.git.'", OBJ_TREE, data);
+		assertSkipListAccepts(OBJ_TREE, data);
+		checker.setIgnore(HAS_DOTGIT, true);
+		checker.checkTree(data);
 	}
 
 	@Test
@@ -1484,20 +1155,19 @@ public void testValidTreeNameIsDotGitDotDot()
 			throws CorruptObjectException {
 		StringBuilder b = new StringBuilder();
 		entry(b, "100644 .git..");
-		checker.checkTree(Constants.encodeASCII(b.toString()));
+		checker.checkTree(encodeASCII(b.toString()));
 	}
 
 	@Test
-	public void testInvalidTreeNameIsDotGitSpace() {
+	public void testInvalidTreeNameIsDotGitSpace()
+			throws CorruptObjectException {
 		StringBuilder b = new StringBuilder();
 		entry(b, "100644 .git ");
-		byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTree(data);
-			fail("incorrectly accepted an invalid tree");
-		} catch (CorruptObjectException e) {
-			assertEquals("invalid name '.git '", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		assertCorrupt("invalid name '.git '", OBJ_TREE, data);
+		assertSkipListAccepts(OBJ_TREE, data);
+		checker.setIgnore(HAS_DOTGIT, true);
+		checker.checkTree(data);
 	}
 
 	@Test
@@ -1505,7 +1175,7 @@ public void testInvalidTreeNameIsDotGitSomething()
 			throws CorruptObjectException {
 		StringBuilder b = new StringBuilder();
 		entry(b, "100644 .gitfoobar");
-		byte[] data = Constants.encodeASCII(b.toString());
+		byte[] data = encodeASCII(b.toString());
 		checker.checkTree(data);
 	}
 
@@ -1514,7 +1184,7 @@ public void testInvalidTreeNameIsDotGitSomethingSpaceSomething()
 			throws CorruptObjectException {
 		StringBuilder b = new StringBuilder();
 		entry(b, "100644 .gitfoo bar");
-		byte[] data = Constants.encodeASCII(b.toString());
+		byte[] data = encodeASCII(b.toString());
 		checker.checkTree(data);
 	}
 
@@ -1523,7 +1193,7 @@ public void testInvalidTreeNameIsDotGitSomethingDot()
 			throws CorruptObjectException {
 		StringBuilder b = new StringBuilder();
 		entry(b, "100644 .gitfoobar.");
-		byte[] data = Constants.encodeASCII(b.toString());
+		byte[] data = encodeASCII(b.toString());
 		checker.checkTree(data);
 	}
 
@@ -1532,251 +1202,222 @@ public void testInvalidTreeNameIsDotGitSomethingDotDot()
 			throws CorruptObjectException {
 		StringBuilder b = new StringBuilder();
 		entry(b, "100644 .gitfoobar..");
-		byte[] data = Constants.encodeASCII(b.toString());
+		byte[] data = encodeASCII(b.toString());
 		checker.checkTree(data);
 	}
 
 	@Test
-	public void testInvalidTreeNameIsDotGitDotSpace() {
+	public void testInvalidTreeNameIsDotGitDotSpace()
+			throws CorruptObjectException {
 		StringBuilder b = new StringBuilder();
 		entry(b, "100644 .git. ");
-		byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTree(data);
-			fail("incorrectly accepted an invalid tree");
-		} catch (CorruptObjectException e) {
-			assertEquals("invalid name '.git. '", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		assertCorrupt("invalid name '.git. '", OBJ_TREE, data);
+		assertSkipListAccepts(OBJ_TREE, data);
+		checker.setIgnore(HAS_DOTGIT, true);
+		checker.checkTree(data);
 	}
 
 	@Test
-	public void testInvalidTreeNameIsDotGitSpaceDot() {
+	public void testInvalidTreeNameIsDotGitSpaceDot()
+			throws CorruptObjectException {
 		StringBuilder b = new StringBuilder();
 		entry(b, "100644 .git . ");
-		byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTree(data);
-			fail("incorrectly accepted an invalid tree");
-		} catch (CorruptObjectException e) {
-			assertEquals("invalid name '.git . '", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		assertCorrupt("invalid name '.git . '", OBJ_TREE, data);
+		assertSkipListAccepts(OBJ_TREE, data);
+		checker.setIgnore(HAS_DOTGIT, true);
+		checker.checkTree(data);
 	}
 
 	@Test
-	public void testInvalidTreeNameIsGITTilde1() {
+	public void testInvalidTreeNameIsGITTilde1() throws CorruptObjectException {
 		StringBuilder b = new StringBuilder();
 		entry(b, "100644 GIT~1");
-		byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTree(data);
-			fail("incorrectly accepted an invalid tree");
-		} catch (CorruptObjectException e) {
-			assertEquals("invalid name 'GIT~1'", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		assertCorrupt("invalid name 'GIT~1'", OBJ_TREE, data);
+		assertSkipListAccepts(OBJ_TREE, data);
+		checker.setIgnore(HAS_DOTGIT, true);
+		checker.checkTree(data);
 	}
 
 	@Test
-	public void testInvalidTreeNameIsGiTTilde1() {
+	public void testInvalidTreeNameIsGiTTilde1() throws CorruptObjectException {
 		StringBuilder b = new StringBuilder();
 		entry(b, "100644 GiT~1");
-		byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTree(data);
-			fail("incorrectly accepted an invalid tree");
-		} catch (CorruptObjectException e) {
-			assertEquals("invalid name 'GiT~1'", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		assertCorrupt("invalid name 'GiT~1'", OBJ_TREE, data);
+		assertSkipListAccepts(OBJ_TREE, data);
+		checker.setIgnore(HAS_DOTGIT, true);
+		checker.checkTree(data);
 	}
 
 	@Test
 	public void testValidTreeNameIsGitTilde11() throws CorruptObjectException {
 		StringBuilder b = new StringBuilder();
 		entry(b, "100644 GIT~11");
-		byte[] data = Constants.encodeASCII(b.toString());
+		byte[] data = encodeASCII(b.toString());
 		checker.checkTree(data);
 	}
 
 	@Test
 	public void testInvalidTreeTruncatedInName() {
-		final StringBuilder b = new StringBuilder();
+		StringBuilder b = new StringBuilder();
 		b.append("100644 b");
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTree(data);
-			fail("incorrectly accepted an invalid tree");
-		} catch (CorruptObjectException e) {
-			assertEquals("truncated in name", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		assertCorrupt("truncated in name", OBJ_TREE, data);
+		assertSkipListRejects("truncated in name", OBJ_TREE, data);
 	}
 
 	@Test
 	public void testInvalidTreeTruncatedInObjectId() {
-		final StringBuilder b = new StringBuilder();
+		StringBuilder b = new StringBuilder();
 		b.append("100644 b\0\1\2");
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTree(data);
-			fail("incorrectly accepted an invalid tree");
-		} catch (CorruptObjectException e) {
-			assertEquals("truncated in object id", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		assertCorrupt("truncated in object id", OBJ_TREE, data);
+		assertSkipListRejects("truncated in object id", OBJ_TREE, data);
 	}
 
 	@Test
-	public void testInvalidTreeBadSorting1() {
-		final StringBuilder b = new StringBuilder();
+	public void testInvalidTreeBadSorting1() throws CorruptObjectException {
+		StringBuilder b = new StringBuilder();
 		entry(b, "100644 foobar");
 		entry(b, "100644 fooaaa");
-		final byte[] data = Constants.encodeASCII(b.toString());
+		byte[] data = encodeASCII(b.toString());
+
+		assertCorrupt("incorrectly sorted", OBJ_TREE, data);
+
+		ObjectId id = idFor(OBJ_TREE, data);
 		try {
-			checker.checkTree(data);
-			fail("incorrectly accepted an invalid tree");
+			checker.check(id, OBJ_TREE, data);
+			fail("Did not throw CorruptObjectException");
 		} catch (CorruptObjectException e) {
-			assertEquals("incorrectly sorted", e.getMessage());
+			assertSame(TREE_NOT_SORTED, e.getErrorType());
+			assertEquals("treeNotSorted: object " + id.name()
+					+ ": incorrectly sorted", e.getMessage());
 		}
+
+		assertSkipListAccepts(OBJ_TREE, data);
+		checker.setIgnore(TREE_NOT_SORTED, true);
+		checker.checkTree(data);
 	}
 
 	@Test
-	public void testInvalidTreeBadSorting2() {
-		final StringBuilder b = new StringBuilder();
+	public void testInvalidTreeBadSorting2() throws CorruptObjectException {
+		StringBuilder b = new StringBuilder();
 		entry(b, "40000 a");
 		entry(b, "100644 a.c");
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTree(data);
-			fail("incorrectly accepted an invalid tree");
-		} catch (CorruptObjectException e) {
-			assertEquals("incorrectly sorted", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		assertCorrupt("incorrectly sorted", OBJ_TREE, data);
+		assertSkipListAccepts(OBJ_TREE, data);
+		checker.setIgnore(TREE_NOT_SORTED, true);
+		checker.checkTree(data);
 	}
 
 	@Test
-	public void testInvalidTreeBadSorting3() {
-		final StringBuilder b = new StringBuilder();
+	public void testInvalidTreeBadSorting3() throws CorruptObjectException {
+		StringBuilder b = new StringBuilder();
 		entry(b, "100644 a0c");
 		entry(b, "40000 a");
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTree(data);
-			fail("incorrectly accepted an invalid tree");
-		} catch (CorruptObjectException e) {
-			assertEquals("incorrectly sorted", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		assertCorrupt("incorrectly sorted", OBJ_TREE, data);
+		assertSkipListAccepts(OBJ_TREE, data);
+		checker.setIgnore(TREE_NOT_SORTED, true);
+		checker.checkTree(data);
 	}
 
 	@Test
-	public void testInvalidTreeDuplicateNames1() {
-		final StringBuilder b = new StringBuilder();
+	public void testInvalidTreeDuplicateNames1() throws CorruptObjectException {
+		StringBuilder b = new StringBuilder();
 		entry(b, "100644 a");
 		entry(b, "100644 a");
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTree(data);
-			fail("incorrectly accepted an invalid tree");
-		} catch (CorruptObjectException e) {
-			assertEquals("duplicate entry names", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		assertCorrupt("duplicate entry names", OBJ_TREE, data);
+		assertSkipListAccepts(OBJ_TREE, data);
+		checker.setIgnore(DUPLICATE_ENTRIES, true);
+		checker.checkTree(data);
 	}
 
 	@Test
-	public void testInvalidTreeDuplicateNames2() {
-		final StringBuilder b = new StringBuilder();
+	public void testInvalidTreeDuplicateNames2() throws CorruptObjectException {
+		StringBuilder b = new StringBuilder();
 		entry(b, "100644 a");
 		entry(b, "100755 a");
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTree(data);
-			fail("incorrectly accepted an invalid tree");
-		} catch (CorruptObjectException e) {
-			assertEquals("duplicate entry names", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		assertCorrupt("duplicate entry names", OBJ_TREE, data);
+		assertSkipListAccepts(OBJ_TREE, data);
+		checker.setIgnore(DUPLICATE_ENTRIES, true);
+		checker.checkTree(data);
 	}
 
 	@Test
-	public void testInvalidTreeDuplicateNames3() {
-		final StringBuilder b = new StringBuilder();
+	public void testInvalidTreeDuplicateNames3() throws CorruptObjectException {
+		StringBuilder b = new StringBuilder();
 		entry(b, "100644 a");
 		entry(b, "40000 a");
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTree(data);
-			fail("incorrectly accepted an invalid tree");
-		} catch (CorruptObjectException e) {
-			assertEquals("duplicate entry names", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		assertCorrupt("duplicate entry names", OBJ_TREE, data);
+		assertSkipListAccepts(OBJ_TREE, data);
+		checker.setIgnore(DUPLICATE_ENTRIES, true);
+		checker.checkTree(data);
 	}
 
 	@Test
-	public void testInvalidTreeDuplicateNames4() {
-		final StringBuilder b = new StringBuilder();
+	public void testInvalidTreeDuplicateNames4() throws CorruptObjectException {
+		StringBuilder b = new StringBuilder();
 		entry(b, "100644 a");
 		entry(b, "100644 a.c");
 		entry(b, "100644 a.d");
 		entry(b, "100644 a.e");
 		entry(b, "40000 a");
 		entry(b, "100644 zoo");
-		final byte[] data = Constants.encodeASCII(b.toString());
-		try {
-			checker.checkTree(data);
-			fail("incorrectly accepted an invalid tree");
-		} catch (CorruptObjectException e) {
-			assertEquals("duplicate entry names", e.getMessage());
-		}
+		byte[] data = encodeASCII(b.toString());
+		assertCorrupt("duplicate entry names", OBJ_TREE, data);
+		assertSkipListAccepts(OBJ_TREE, data);
+		checker.setIgnore(DUPLICATE_ENTRIES, true);
+		checker.checkTree(data);
 	}
 
 	@Test
 	public void testInvalidTreeDuplicateNames5()
-			throws UnsupportedEncodingException {
+			throws UnsupportedEncodingException, CorruptObjectException {
 		StringBuilder b = new StringBuilder();
-		entry(b, "100644 a");
 		entry(b, "100644 A");
+		entry(b, "100644 a");
 		byte[] data = b.toString().getBytes("UTF-8");
-		try {
-			checker.setSafeForWindows(true);
-			checker.checkTree(data);
-			fail("incorrectly accepted an invalid tree");
-		} catch (CorruptObjectException e) {
-			assertEquals("duplicate entry names", e.getMessage());
-		}
+		checker.setSafeForWindows(true);
+		assertCorrupt("duplicate entry names", OBJ_TREE, data);
+		assertSkipListAccepts(OBJ_TREE, data);
+		checker.setIgnore(DUPLICATE_ENTRIES, true);
+		checker.checkTree(data);
 	}
 
 	@Test
 	public void testInvalidTreeDuplicateNames6()
-			throws UnsupportedEncodingException {
+			throws UnsupportedEncodingException, CorruptObjectException {
 		StringBuilder b = new StringBuilder();
-		entry(b, "100644 a");
 		entry(b, "100644 A");
+		entry(b, "100644 a");
 		byte[] data = b.toString().getBytes("UTF-8");
-		try {
-			checker.setSafeForMacOS(true);
-			checker.checkTree(data);
-			fail("incorrectly accepted an invalid tree");
-		} catch (CorruptObjectException e) {
-			assertEquals("duplicate entry names", e.getMessage());
-		}
+		checker.setSafeForMacOS(true);
+		assertCorrupt("duplicate entry names", OBJ_TREE, data);
+		assertSkipListAccepts(OBJ_TREE, data);
+		checker.setIgnore(DUPLICATE_ENTRIES, true);
+		checker.checkTree(data);
 	}
 
 	@Test
 	public void testInvalidTreeDuplicateNames7()
-			throws UnsupportedEncodingException {
-		try {
-			Class.forName("java.text.Normalizer");
-		} catch (ClassNotFoundException e) {
-			// Ignore this test on Java 5 platform.
-			return;
-		}
-
+			throws UnsupportedEncodingException, CorruptObjectException {
 		StringBuilder b = new StringBuilder();
 		entry(b, "100644 \u0065\u0301");
 		entry(b, "100644 \u00e9");
 		byte[] data = b.toString().getBytes("UTF-8");
-		try {
-			checker.setSafeForMacOS(true);
-			checker.checkTree(data);
-			fail("incorrectly accepted an invalid tree");
-		} catch (CorruptObjectException e) {
-			assertEquals("duplicate entry names", e.getMessage());
-		}
+		checker.setSafeForMacOS(true);
+		assertCorrupt("duplicate entry names", OBJ_TREE, data);
+		assertSkipListAccepts(OBJ_TREE, data);
+		checker.setIgnore(DUPLICATE_ENTRIES, true);
+		checker.checkTree(data);
 	}
 
 	@Test
@@ -1791,7 +1432,7 @@ public void testInvalidTreeDuplicateNames8()
 	@Test
 	public void testRejectNulInPathSegment() {
 		try {
-			checker.checkPathSegment(Constants.encodeASCII("a\u0000b"), 0, 3);
+			checker.checkPathSegment(encodeASCII("a\u0000b"), 0, 3);
 			fail("incorrectly accepted NUL in middle of name");
 		} catch (CorruptObjectException e) {
 			assertEquals("name contains byte 0x00", e.getMessage());
@@ -1893,13 +1534,65 @@ private void rejectName(byte c) {
 	private void checkOneName(String name) throws CorruptObjectException {
 		StringBuilder b = new StringBuilder();
 		entry(b, "100644 " + name);
-		checker.checkTree(Constants.encodeASCII(b.toString()));
+		checker.checkTree(encodeASCII(b.toString()));
 	}
 
-	private static void entry(final StringBuilder b, final String modeName) {
+	private static void entry(StringBuilder b, final String modeName) {
 		b.append(modeName);
 		b.append('\0');
-		for (int i = 0; i < Constants.OBJECT_ID_LENGTH; i++)
+		for (int i = 0; i < OBJECT_ID_LENGTH; i++)
 			b.append((char) i);
 	}
+
+	private void assertCorrupt(String msg, int type, StringBuilder b) {
+		assertCorrupt(msg, type, encodeASCII(b.toString()));
+	}
+
+	private void assertCorrupt(String msg, int type, byte[] data) {
+		try {
+			checker.check(type, data);
+			fail("Did not throw CorruptObjectException");
+		} catch (CorruptObjectException e) {
+			assertEquals(msg, e.getMessage());
+		}
+	}
+
+	private void assertSkipListAccepts(int type, byte[] data)
+			throws CorruptObjectException {
+		ObjectId id = idFor(type, data);
+		checker.setSkipList(set(id));
+		checker.check(id, type, data);
+		checker.setSkipList(null);
+	}
+
+	private void assertSkipListRejects(String msg, int type, byte[] data) {
+		ObjectId id = idFor(type, data);
+		checker.setSkipList(set(id));
+		try {
+			checker.check(id, type, data);
+			fail("Did not throw CorruptObjectException");
+		} catch (CorruptObjectException e) {
+			assertEquals(msg, e.getMessage());
+		}
+		checker.setSkipList(null);
+	}
+
+	private static ObjectIdSet set(final ObjectId... ids) {
+		return new ObjectIdSet() {
+			@Override
+			public boolean contains(AnyObjectId objectId) {
+				for (ObjectId id : ids) {
+					if (id.equals(objectId)) {
+						return true;
+					}
+				}
+				return false;
+			}
+		};
+	}
+
+	@SuppressWarnings("resource")
+	private static ObjectId idFor(int type, byte[] raw) {
+		return new ObjectInserter.Formatter().idFor(type, raw);
+	}
 }
diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
index 840059d..992e10b 100644
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
@@ -126,14 +126,15 @@
 connectionTimeOut=Connection time out: {0}
 contextMustBeNonNegative=context must be >= 0
 corruptionDetectedReReadingAt=Corruption detected re-reading at {0}
+corruptObjectBadDate=bad date
+corruptObjectBadEmail=bad email
 corruptObjectBadStream=bad stream
 corruptObjectBadStreamCorruptHeader=bad stream, corrupt header
+corruptObjectBadTimezone=bad time zone
 corruptObjectDuplicateEntryNames=duplicate entry names
 corruptObjectGarbageAfterSize=garbage after size
 corruptObjectIncorrectLength=incorrect length
 corruptObjectIncorrectSorting=incorrectly sorted
-corruptObjectInvalidAuthor=invalid author
-corruptObjectInvalidCommitter=invalid committer
 corruptObjectInvalidEntryMode=invalid entry mode
 corruptObjectInvalidMode=invalid mode
 corruptObjectInvalidModeChar=invalid mode character
@@ -152,11 +153,11 @@
 corruptObjectInvalidNamePrn=invalid name 'PRN'
 corruptObjectInvalidObject=invalid object
 corruptObjectInvalidParent=invalid parent
-corruptObjectInvalidTagger=invalid tagger
 corruptObjectInvalidTree=invalid tree
 corruptObjectInvalidType=invalid type
 corruptObjectInvalidType2=invalid type {0}
 corruptObjectMalformedHeader=malformed header: {0}
+corruptObjectMissingEmail=missing email
 corruptObjectNameContainsByte=name contains byte 0x%x
 corruptObjectNameContainsChar=name contains '%c'
 corruptObjectNameContainsNullByte=name contains byte 0x00
@@ -182,6 +183,7 @@
 corruptObjectTruncatedInMode=truncated in mode
 corruptObjectTruncatedInName=truncated in name
 corruptObjectTruncatedInObjectId=truncated in object id
+corruptObjectZeroId=entry points to null SHA-1
 couldNotCheckOutBecauseOfConflicts=Could not check out because of conflicts
 couldNotDeleteLockFileShouldNotHappen=Could not delete lock file. Should not happen
 couldNotDeleteTemporaryIndexFileShouldNotHappen=Could not delete temporary index file. Should not happen
@@ -433,6 +435,7 @@
 objectAtHasBadZlibStream=Object at {0} in {1} has bad zlib stream
 objectAtPathDoesNotHaveId=Object at path "{0}" does not have an id assigned. All object ids must be assigned prior to writing a tree.
 objectIsCorrupt=Object {0} is corrupt: {1}
+objectIsCorrupt3={0}: object {1}: {2}
 objectIsNotA=Object {0} is not a {1}.
 objectNotFound=Object {0} not found.
 objectNotFoundIn=Object {0} not found in {1}.
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/CorruptObjectException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/CorruptObjectException.java
index c6ea093..e4db40b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/errors/CorruptObjectException.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/CorruptObjectException.java
@@ -49,8 +49,10 @@
 import java.io.IOException;
 import java.text.MessageFormat;
 
+import org.eclipse.jgit.annotations.Nullable;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.ObjectChecker;
 import org.eclipse.jgit.lib.ObjectId;
 
 /**
@@ -59,15 +61,24 @@
 public class CorruptObjectException extends IOException {
 	private static final long serialVersionUID = 1L;
 
+	private ObjectChecker.ErrorType errorType;
+
 	/**
-	 * Construct a CorruptObjectException for reporting a problem specified
-	 * object id
+	 * Report a specific error condition discovered in an object.
 	 *
+	 * @param type
+	 *            type of error
 	 * @param id
+	 *            identity of the bad object
 	 * @param why
+	 *            description of the error.
+	 * @since 4.2
 	 */
-	public CorruptObjectException(final AnyObjectId id, final String why) {
-		this(id.toObjectId(), why);
+	public CorruptObjectException(ObjectChecker.ErrorType type, AnyObjectId id,
+			String why) {
+		super(MessageFormat.format(JGitText.get().objectIsCorrupt3,
+				type.getMessageId(), id.name(), why));
+		this.errorType = type;
 	}
 
 	/**
@@ -77,7 +88,18 @@ public CorruptObjectException(final AnyObjectId id, final String why) {
 	 * @param id
 	 * @param why
 	 */
-	public CorruptObjectException(final ObjectId id, final String why) {
+	public CorruptObjectException(AnyObjectId id, String why) {
+		super(MessageFormat.format(JGitText.get().objectIsCorrupt, id.name(), why));
+	}
+
+	/**
+	 * Construct a CorruptObjectException for reporting a problem specified
+	 * object id
+	 *
+	 * @param id
+	 * @param why
+	 */
+	public CorruptObjectException(ObjectId id, String why) {
 		super(MessageFormat.format(JGitText.get().objectIsCorrupt, id.name(), why));
 	}
 
@@ -87,7 +109,7 @@ public CorruptObjectException(final ObjectId id, final String why) {
 	 *
 	 * @param why
 	 */
-	public CorruptObjectException(final String why) {
+	public CorruptObjectException(String why) {
 		super(why);
 	}
 
@@ -105,4 +127,15 @@ public CorruptObjectException(String why, Throwable cause) {
 		super(why);
 		initCause(cause);
 	}
+
+	/**
+	 * Specific error condition identified by {@link ObjectChecker}.
+	 *
+	 * @return error condition or null.
+	 * @since 4.2
+	 */
+	@Nullable
+	public ObjectChecker.ErrorType getErrorType() {
+		return errorType;
+	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
index c476f17..7740a2b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
@@ -185,14 +185,15 @@ public static JGitText get() {
 	/***/ public String connectionTimeOut;
 	/***/ public String contextMustBeNonNegative;
 	/***/ public String corruptionDetectedReReadingAt;
+	/***/ public String corruptObjectBadDate;
+	/***/ public String corruptObjectBadEmail;
 	/***/ public String corruptObjectBadStream;
 	/***/ public String corruptObjectBadStreamCorruptHeader;
+	/***/ public String corruptObjectBadTimezone;
 	/***/ public String corruptObjectDuplicateEntryNames;
 	/***/ public String corruptObjectGarbageAfterSize;
 	/***/ public String corruptObjectIncorrectLength;
 	/***/ public String corruptObjectIncorrectSorting;
-	/***/ public String corruptObjectInvalidAuthor;
-	/***/ public String corruptObjectInvalidCommitter;
 	/***/ public String corruptObjectInvalidEntryMode;
 	/***/ public String corruptObjectInvalidMode;
 	/***/ public String corruptObjectInvalidModeChar;
@@ -211,11 +212,11 @@ public static JGitText get() {
 	/***/ public String corruptObjectInvalidNamePrn;
 	/***/ public String corruptObjectInvalidObject;
 	/***/ public String corruptObjectInvalidParent;
-	/***/ public String corruptObjectInvalidTagger;
 	/***/ public String corruptObjectInvalidTree;
 	/***/ public String corruptObjectInvalidType;
 	/***/ public String corruptObjectInvalidType2;
 	/***/ public String corruptObjectMalformedHeader;
+	/***/ public String corruptObjectMissingEmail;
 	/***/ public String corruptObjectNameContainsByte;
 	/***/ public String corruptObjectNameContainsChar;
 	/***/ public String corruptObjectNameContainsNullByte;
@@ -241,6 +242,7 @@ public static JGitText get() {
 	/***/ public String corruptObjectTruncatedInMode;
 	/***/ public String corruptObjectTruncatedInName;
 	/***/ public String corruptObjectTruncatedInObjectId;
+	/***/ public String corruptObjectZeroId;
 	/***/ public String corruptPack;
 	/***/ public String couldNotCheckOutBecauseOfConflicts;
 	/***/ public String couldNotDeleteLockFileShouldNotHappen;
@@ -492,6 +494,7 @@ public static JGitText get() {
 	/***/ public String objectAtHasBadZlibStream;
 	/***/ public String objectAtPathDoesNotHaveId;
 	/***/ public String objectIsCorrupt;
+	/***/ public String objectIsCorrupt3;
 	/***/ public String objectIsNotA;
 	/***/ public String objectNotFound;
 	/***/ public String objectNotFoundIn;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LazyObjectIdSetFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LazyObjectIdSetFile.java
new file mode 100644
index 0000000..1e2617c
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LazyObjectIdSetFile.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2015, 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.nio.charset.StandardCharsets.UTF_8;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.MutableObjectId;
+import org.eclipse.jgit.lib.ObjectIdOwnerMap;
+import org.eclipse.jgit.lib.ObjectIdSet;
+
+/** Lazily loads a set of ObjectIds, one per line. */
+public class LazyObjectIdSetFile implements ObjectIdSet {
+	private final File src;
+	private ObjectIdOwnerMap<Entry> set;
+
+	/**
+	 * Create a new lazy set from a file.
+	 *
+	 * @param src
+	 *            the source file.
+	 */
+	public LazyObjectIdSetFile(File src) {
+		this.src = src;
+	}
+
+	@Override
+	public boolean contains(AnyObjectId objectId) {
+		if (set == null) {
+			set = load();
+		}
+		return set.contains(objectId);
+	}
+
+	private ObjectIdOwnerMap<Entry> load() {
+		ObjectIdOwnerMap<Entry> r = new ObjectIdOwnerMap<>();
+		try (FileInputStream fin = new FileInputStream(src);
+				Reader rin = new InputStreamReader(fin, UTF_8);
+				BufferedReader br = new BufferedReader(rin)) {
+			MutableObjectId id = new MutableObjectId();
+			for (String line; (line = br.readLine()) != null;) {
+				id.fromString(line);
+				if (!r.contains(id)) {
+					r.add(new Entry(id));
+				}
+			}
+		} catch (IOException e) {
+			// Ignore IO errors accessing the lazy set.
+		}
+		return r;
+	}
+
+	static class Entry extends ObjectIdOwnerMap.Entry {
+		Entry(AnyObjectId id) {
+			super(id);
+		}
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java
index 855d9d7..858a038 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java
@@ -44,20 +44,56 @@
 
 package org.eclipse.jgit.lib;
 
-import static org.eclipse.jgit.util.RawParseUtils.match;
+import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH;
+import static org.eclipse.jgit.lib.Constants.OBJECT_ID_STRING_LENGTH;
+import static org.eclipse.jgit.lib.Constants.OBJ_BAD;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT;
+import static org.eclipse.jgit.lib.Constants.OBJ_TAG;
+import static org.eclipse.jgit.lib.Constants.OBJ_TREE;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.BAD_DATE;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.BAD_EMAIL;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.BAD_OBJECT_SHA1;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.BAD_PARENT_SHA1;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.BAD_TIMEZONE;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.BAD_TREE_SHA1;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.BAD_UTF8;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.DUPLICATE_ENTRIES;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.EMPTY_NAME;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.FULL_PATHNAME;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.HAS_DOT;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.HAS_DOTDOT;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.HAS_DOTGIT;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.MISSING_AUTHOR;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.MISSING_COMMITTER;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.MISSING_EMAIL;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.MISSING_OBJECT;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.MISSING_SPACE_BEFORE_DATE;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.MISSING_TAG_ENTRY;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.MISSING_TREE;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.MISSING_TYPE_ENTRY;
+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.UNKNOWN_TYPE;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.WIN32_BAD_NAME;
+import static org.eclipse.jgit.lib.ObjectChecker.ErrorType.ZERO_PADDED_FILEMODE;
 import static org.eclipse.jgit.util.RawParseUtils.nextLF;
 import static org.eclipse.jgit.util.RawParseUtils.parseBase10;
 
 import java.text.MessageFormat;
 import java.text.Normalizer;
+import java.util.EnumSet;
 import java.util.HashSet;
 import java.util.Locale;
 import java.util.Set;
 
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.annotations.Nullable;
 import org.eclipse.jgit.errors.CorruptObjectException;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.util.MutableInteger;
 import org.eclipse.jgit.util.RawParseUtils;
+import org.eclipse.jgit.util.StringUtils;
 
 /**
  * Verifies that an object is formatted correctly.
@@ -98,31 +134,135 @@ public class ObjectChecker {
 	/** Header "tagger " */
 	public static final byte[] tagger = Constants.encodeASCII("tagger "); //$NON-NLS-1$
 
+	/**
+	 * Potential issues identified by the checker.
+	 *
+	 * @since 4.2
+	 */
+	public enum ErrorType {
+		// @formatter:off
+		// These names match git-core so that fsck section keys also match.
+		/***/ NULL_SHA1,
+		/***/ DUPLICATE_ENTRIES,
+		/***/ TREE_NOT_SORTED,
+		/***/ ZERO_PADDED_FILEMODE,
+		/***/ EMPTY_NAME,
+		/***/ FULL_PATHNAME,
+		/***/ HAS_DOT,
+		/***/ HAS_DOTDOT,
+		/***/ HAS_DOTGIT,
+		/***/ BAD_OBJECT_SHA1,
+		/***/ BAD_PARENT_SHA1,
+		/***/ BAD_TREE_SHA1,
+		/***/ MISSING_AUTHOR,
+		/***/ MISSING_COMMITTER,
+		/***/ MISSING_OBJECT,
+		/***/ MISSING_TREE,
+		/***/ MISSING_TYPE_ENTRY,
+		/***/ MISSING_TAG_ENTRY,
+		/***/ BAD_DATE,
+		/***/ BAD_EMAIL,
+		/***/ BAD_TIMEZONE,
+		/***/ MISSING_EMAIL,
+		/***/ MISSING_SPACE_BEFORE_DATE,
+		/***/ UNKNOWN_TYPE,
+
+		// These are unique to JGit.
+		/***/ WIN32_BAD_NAME,
+		/***/ BAD_UTF8;
+		// @formatter:on
+
+		/** @return camelCaseVersion of the name. */
+		public String getMessageId() {
+			String n = name();
+			StringBuilder r = new StringBuilder(n.length());
+			for (int i = 0; i < n.length(); i++) {
+				char c = n.charAt(i);
+				if (c != '_') {
+					r.append(StringUtils.toLowerCase(c));
+				} else {
+					r.append(n.charAt(++i));
+				}
+			}
+			return r.toString();
+		}
+	}
+
 	private final MutableObjectId tempId = new MutableObjectId();
+	private final MutableInteger bufPtr = new MutableInteger();
 
-	private final MutableInteger ptrout = new MutableInteger();
-
-	private boolean allowZeroMode;
-
+	private EnumSet<ErrorType> errors = EnumSet.allOf(ErrorType.class);
+	private ObjectIdSet skipList;
 	private boolean allowInvalidPersonIdent;
 	private boolean windows;
 	private boolean macosx;
 
 	/**
+	 * Enable accepting specific malformed (but not horribly broken) objects.
+	 *
+	 * @param objects
+	 *            collection of object names known to be broken in a non-fatal
+	 *            way that should be ignored by the checker.
+	 * @return {@code this}
+	 * @since 4.2
+	 */
+	public ObjectChecker setSkipList(@Nullable ObjectIdSet objects) {
+		skipList = objects;
+		return this;
+	}
+
+	/**
+	 * Configure error types to be ignored across all objects.
+	 *
+	 * @param ids
+	 *            error types to ignore. The caller's set is copied.
+	 * @return {@code this}
+	 * @since 4.2
+	 */
+	public ObjectChecker setIgnore(@Nullable Set<ErrorType> ids) {
+		errors = EnumSet.allOf(ErrorType.class);
+		if (ids != null) {
+			errors.removeAll(ids);
+		}
+		return this;
+	}
+
+	/**
+	 * Add message type to be ignored across all objects.
+	 *
+	 * @param id
+	 *            error type to ignore.
+	 * @param ignore
+	 *            true to ignore this error; false to treat the error as an
+	 *            error and throw.
+	 * @return {@code this}
+	 * @since 4.2
+	 */
+	public ObjectChecker setIgnore(ErrorType id, boolean ignore) {
+		if (ignore) {
+			errors.remove(id);
+		} else {
+			errors.add(id);
+		}
+		return this;
+	}
+
+	/**
 	 * Enable accepting leading zero mode in tree entries.
 	 * <p>
 	 * Some broken Git libraries generated leading zeros in the mode part of
 	 * tree entries. This is technically incorrect but gracefully allowed by
 	 * git-core. JGit rejects such trees by default, but may need to accept
 	 * them on broken histories.
+	 * <p>
+	 * Same as {@code setIgnore(ZERO_PADDED_FILEMODE, allow)}.
 	 *
 	 * @param allow allow leading zero mode.
 	 * @return {@code this}.
 	 * @since 3.4
 	 */
 	public ObjectChecker setAllowLeadingZeroFileMode(boolean allow) {
-		allowZeroMode = allow;
-		return this;
+		return setIgnore(ZERO_PADDED_FILEMODE, allow);
 	}
 
 	/**
@@ -183,62 +323,117 @@ public ObjectChecker setSafeForMacOS(boolean mac) {
 	 * @throws CorruptObjectException
 	 *             if an error is identified.
 	 */
-	public void check(final int objType, final byte[] raw)
+	public void check(int objType, byte[] raw)
+			throws CorruptObjectException {
+		check(idFor(objType, raw), objType, raw);
+	}
+
+	/**
+	 * Check an object for parsing errors.
+	 *
+	 * @param id
+	 *            identify of the object being checked.
+	 * @param objType
+	 *            type of the object. Must be a valid object type code in
+	 *            {@link Constants}.
+	 * @param raw
+	 *            the raw data which comprises the object. This should be in the
+	 *            canonical format (that is the format used to generate the
+	 *            ObjectId of the object). The array is never modified.
+	 * @throws CorruptObjectException
+	 *             if an error is identified.
+	 * @since 4.2
+	 */
+	public void check(@Nullable AnyObjectId id, int objType, byte[] raw)
 			throws CorruptObjectException {
 		switch (objType) {
-		case Constants.OBJ_COMMIT:
-			checkCommit(raw);
+		case OBJ_COMMIT:
+			checkCommit(id, raw);
 			break;
-		case Constants.OBJ_TAG:
-			checkTag(raw);
+		case OBJ_TAG:
+			checkTag(id, raw);
 			break;
-		case Constants.OBJ_TREE:
-			checkTree(raw);
+		case OBJ_TREE:
+			checkTree(id, raw);
 			break;
-		case Constants.OBJ_BLOB:
+		case OBJ_BLOB:
 			checkBlob(raw);
 			break;
 		default:
-			throw new CorruptObjectException(MessageFormat.format(
+			report(UNKNOWN_TYPE, id, MessageFormat.format(
 					JGitText.get().corruptObjectInvalidType2,
 					Integer.valueOf(objType)));
 		}
 	}
 
-	private int id(final byte[] raw, final int ptr) {
+	private boolean checkId(byte[] raw) {
+		int p = bufPtr.value;
 		try {
-			tempId.fromString(raw, ptr);
-			return ptr + Constants.OBJECT_ID_STRING_LENGTH;
+			tempId.fromString(raw, p);
 		} catch (IllegalArgumentException e) {
-			return -1;
+			bufPtr.value = nextLF(raw, p);
+			return false;
 		}
+
+		p += OBJECT_ID_STRING_LENGTH;
+		if (raw[p] == '\n') {
+			bufPtr.value = p + 1;
+			return true;
+		}
+		bufPtr.value = nextLF(raw, p);
+		return false;
 	}
 
-	private int personIdent(final byte[] raw, int ptr) {
-		if (allowInvalidPersonIdent)
-			return nextLF(raw, ptr) - 1;
+	private void checkPersonIdent(byte[] raw, @Nullable AnyObjectId id)
+			throws CorruptObjectException {
+		if (allowInvalidPersonIdent) {
+			bufPtr.value = nextLF(raw, bufPtr.value);
+			return;
+		}
 
-		final int emailB = nextLF(raw, ptr, '<');
-		if (emailB == ptr || raw[emailB - 1] != '<')
-			return -1;
+		final int emailB = nextLF(raw, bufPtr.value, '<');
+		if (emailB == bufPtr.value || raw[emailB - 1] != '<') {
+			report(MISSING_EMAIL, id, JGitText.get().corruptObjectMissingEmail);
+			bufPtr.value = nextLF(raw, bufPtr.value);
+			return;
+		}
 
 		final int emailE = nextLF(raw, emailB, '>');
-		if (emailE == emailB || raw[emailE - 1] != '>')
-			return -1;
-		if (emailE == raw.length || raw[emailE] != ' ')
-			return -1;
+		if (emailE == emailB || raw[emailE - 1] != '>') {
+			report(BAD_EMAIL, id, JGitText.get().corruptObjectBadEmail);
+			bufPtr.value = nextLF(raw, bufPtr.value);
+			return;
+		}
+		if (emailE == raw.length || raw[emailE] != ' ') {
+			report(MISSING_SPACE_BEFORE_DATE, id,
+					JGitText.get().corruptObjectBadDate);
+			bufPtr.value = nextLF(raw, bufPtr.value);
+			return;
+		}
 
-		parseBase10(raw, emailE + 1, ptrout); // when
-		ptr = ptrout.value;
-		if (emailE + 1 == ptr)
-			return -1;
-		if (ptr == raw.length || raw[ptr] != ' ')
-			return -1;
+		parseBase10(raw, emailE + 1, bufPtr); // when
+		if (emailE + 1 == bufPtr.value || bufPtr.value == raw.length
+				|| raw[bufPtr.value] != ' ') {
+			report(BAD_DATE, id, JGitText.get().corruptObjectBadDate);
+			bufPtr.value = nextLF(raw, bufPtr.value);
+			return;
+		}
 
-		parseBase10(raw, ptr + 1, ptrout); // tz offset
-		if (ptr + 1 == ptrout.value)
-			return -1;
-		return ptrout.value;
+		int p = bufPtr.value + 1;
+		parseBase10(raw, p, bufPtr); // tz offset
+		if (p == bufPtr.value) {
+			report(BAD_TIMEZONE, id, JGitText.get().corruptObjectBadTimezone);
+			bufPtr.value = nextLF(raw, bufPtr.value);
+			return;
+		}
+
+		p = bufPtr.value;
+		if (raw[p] == '\n') {
+			bufPtr.value = p + 1;
+		} else {
+			report(BAD_TIMEZONE, id, JGitText.get().corruptObjectBadTimezone);
+			bufPtr.value = nextLF(raw, p);
+		}
 	}
 
 	/**
@@ -249,36 +444,50 @@ private int personIdent(final byte[] raw, int ptr) {
 	 * @throws CorruptObjectException
 	 *             if any error was detected.
 	 */
-	public void checkCommit(final byte[] raw) throws CorruptObjectException {
-		int ptr = 0;
+	public void checkCommit(byte[] raw) throws CorruptObjectException {
+		checkCommit(idFor(OBJ_COMMIT, raw), raw);
+	}
 
-		if ((ptr = match(raw, ptr, tree)) < 0)
-			throw new CorruptObjectException(
-					JGitText.get().corruptObjectNotreeHeader);
-		if ((ptr = id(raw, ptr)) < 0 || raw[ptr++] != '\n')
-			throw new CorruptObjectException(
-					JGitText.get().corruptObjectInvalidTree);
+	/**
+	 * Check a commit for errors.
+	 *
+	 * @param id
+	 *            identity of the object being checked.
+	 * @param raw
+	 *            the commit data. The array is never modified.
+	 * @throws CorruptObjectException
+	 *             if any error was detected.
+	 * @since 4.2
+	 */
+	public void checkCommit(@Nullable AnyObjectId id, byte[] raw)
+			throws CorruptObjectException {
+		bufPtr.value = 0;
 
-		while (match(raw, ptr, parent) >= 0) {
-			ptr += parent.length;
-			if ((ptr = id(raw, ptr)) < 0 || raw[ptr++] != '\n')
-				throw new CorruptObjectException(
-						JGitText.get().corruptObjectInvalidParent);
+		if (!match(raw, tree)) {
+			report(MISSING_TREE, id, JGitText.get().corruptObjectNotreeHeader);
+		} else if (!checkId(raw)) {
+			report(BAD_TREE_SHA1, id, JGitText.get().corruptObjectInvalidTree);
 		}
 
-		if ((ptr = match(raw, ptr, author)) < 0)
-			throw new CorruptObjectException(
-					JGitText.get().corruptObjectNoAuthor);
-		if ((ptr = personIdent(raw, ptr)) < 0 || raw[ptr++] != '\n')
-			throw new CorruptObjectException(
-					JGitText.get().corruptObjectInvalidAuthor);
+		while (match(raw, parent)) {
+			if (!checkId(raw)) {
+				report(BAD_PARENT_SHA1, id,
+						JGitText.get().corruptObjectInvalidParent);
+			}
+		}
 
-		if ((ptr = match(raw, ptr, committer)) < 0)
-			throw new CorruptObjectException(
+		if (match(raw, author)) {
+			checkPersonIdent(raw, id);
+		} else {
+			report(MISSING_AUTHOR, id, JGitText.get().corruptObjectNoAuthor);
+		}
+
+		if (match(raw, committer)) {
+			checkPersonIdent(raw, id);
+		} else {
+			report(MISSING_COMMITTER, id,
 					JGitText.get().corruptObjectNoCommitter);
-		if ((ptr = personIdent(raw, ptr)) < 0 || raw[ptr++] != '\n')
-			throw new CorruptObjectException(
-					JGitText.get().corruptObjectInvalidCommitter);
+		}
 	}
 
 	/**
@@ -289,30 +498,46 @@ public void checkCommit(final byte[] raw) throws CorruptObjectException {
 	 * @throws CorruptObjectException
 	 *             if any error was detected.
 	 */
-	public void checkTag(final byte[] raw) throws CorruptObjectException {
-		int ptr = 0;
+	public void checkTag(byte[] raw) throws CorruptObjectException {
+		checkTag(idFor(OBJ_TAG, raw), raw);
+	}
 
-		if ((ptr = match(raw, ptr, object)) < 0)
-			throw new CorruptObjectException(
+	/**
+	 * Check an annotated tag for errors.
+	 *
+	 * @param id
+	 *            identity of the object being checked.
+	 * @param raw
+	 *            the tag data. The array is never modified.
+	 * @throws CorruptObjectException
+	 *             if any error was detected.
+	 * @since 4.2
+	 */
+	public void checkTag(@Nullable AnyObjectId id, byte[] raw)
+			throws CorruptObjectException {
+		bufPtr.value = 0;
+		if (!match(raw, object)) {
+			report(MISSING_OBJECT, id,
 					JGitText.get().corruptObjectNoObjectHeader);
-		if ((ptr = id(raw, ptr)) < 0 || raw[ptr++] != '\n')
-			throw new CorruptObjectException(
+		} else if (!checkId(raw)) {
+			report(BAD_OBJECT_SHA1, id,
 					JGitText.get().corruptObjectInvalidObject);
+		}
 
-		if ((ptr = match(raw, ptr, type)) < 0)
-			throw new CorruptObjectException(
+		if (!match(raw, type)) {
+			report(MISSING_TYPE_ENTRY, id,
 					JGitText.get().corruptObjectNoTypeHeader);
-		ptr = nextLF(raw, ptr);
+		}
+		bufPtr.value = nextLF(raw, bufPtr.value);
 
-		if ((ptr = match(raw, ptr, tag)) < 0)
-			throw new CorruptObjectException(
+		if (!match(raw, tag)) {
+			report(MISSING_TAG_ENTRY, id,
 					JGitText.get().corruptObjectNoTagHeader);
-		ptr = nextLF(raw, ptr);
+		}
+		bufPtr.value = nextLF(raw, bufPtr.value);
 
-		if ((ptr = match(raw, ptr, tagger)) > 0) {
-			if ((ptr = personIdent(raw, ptr)) < 0 || raw[ptr++] != '\n')
-				throw new CorruptObjectException(
-						JGitText.get().corruptObjectInvalidTagger);
+		if (match(raw, tagger)) {
+			checkPersonIdent(raw, id);
 		}
 	}
 
@@ -381,7 +606,23 @@ else if (cmp == 0)
 	 * @throws CorruptObjectException
 	 *             if any error was detected.
 	 */
-	public void checkTree(final byte[] raw) throws CorruptObjectException {
+	public void checkTree(byte[] raw) throws CorruptObjectException {
+		checkTree(idFor(OBJ_TREE, raw), raw);
+	}
+
+	/**
+	 * Check a canonical formatted tree for errors.
+	 *
+	 * @param id
+	 *            identity of the object being checked.
+	 * @param raw
+	 *            the raw tree data. The array is never modified.
+	 * @throws CorruptObjectException
+	 *             if any error was detected.
+	 * @since 4.2
+	 */
+	public void checkTree(@Nullable AnyObjectId id, byte[] raw)
+			throws CorruptObjectException {
 		final int sz = raw.length;
 		int ptr = 0;
 		int lastNameB = 0, lastNameE = 0, lastMode = 0;
@@ -392,74 +633,89 @@ public void checkTree(final byte[] raw) throws CorruptObjectException {
 		while (ptr < sz) {
 			int thisMode = 0;
 			for (;;) {
-				if (ptr == sz)
+				if (ptr == sz) {
 					throw new CorruptObjectException(
 							JGitText.get().corruptObjectTruncatedInMode);
+				}
 				final byte c = raw[ptr++];
 				if (' ' == c)
 					break;
-				if (c < '0' || c > '7')
+				if (c < '0' || c > '7') {
 					throw new CorruptObjectException(
 							JGitText.get().corruptObjectInvalidModeChar);
-				if (thisMode == 0 && c == '0' && !allowZeroMode)
-					throw new CorruptObjectException(
+				}
+				if (thisMode == 0 && c == '0') {
+					report(ZERO_PADDED_FILEMODE, id,
 							JGitText.get().corruptObjectInvalidModeStartsZero);
+				}
 				thisMode <<= 3;
 				thisMode += c - '0';
 			}
 
-			if (FileMode.fromBits(thisMode).getObjectType() == Constants.OBJ_BAD)
+			if (FileMode.fromBits(thisMode).getObjectType() == OBJ_BAD) {
 				throw new CorruptObjectException(MessageFormat.format(
 						JGitText.get().corruptObjectInvalidMode2,
 						Integer.valueOf(thisMode)));
+			}
 
 			final int thisNameB = ptr;
-			ptr = scanPathSegment(raw, ptr, sz);
-			if (ptr == sz || raw[ptr] != 0)
+			ptr = scanPathSegment(raw, ptr, sz, id);
+			if (ptr == sz || raw[ptr] != 0) {
 				throw new CorruptObjectException(
 						JGitText.get().corruptObjectTruncatedInName);
-			checkPathSegment2(raw, thisNameB, ptr);
+			}
+			checkPathSegment2(raw, thisNameB, ptr, id);
 			if (normalized != null) {
-				if (!normalized.add(normalize(raw, thisNameB, ptr)))
-					throw new CorruptObjectException(
+				if (!normalized.add(normalize(raw, thisNameB, ptr))) {
+					report(DUPLICATE_ENTRIES, id,
 							JGitText.get().corruptObjectDuplicateEntryNames);
-			} else if (duplicateName(raw, thisNameB, ptr))
-				throw new CorruptObjectException(
+				}
+			} else if (duplicateName(raw, thisNameB, ptr)) {
+				report(DUPLICATE_ENTRIES, id,
 						JGitText.get().corruptObjectDuplicateEntryNames);
+			}
 
 			if (lastNameB != 0) {
 				final int cmp = pathCompare(raw, lastNameB, lastNameE,
 						lastMode, thisNameB, ptr, thisMode);
-				if (cmp > 0)
-					throw new CorruptObjectException(
+				if (cmp > 0) {
+					report(TREE_NOT_SORTED, id,
 							JGitText.get().corruptObjectIncorrectSorting);
+				}
 			}
 
 			lastNameB = thisNameB;
 			lastNameE = ptr;
 			lastMode = thisMode;
 
-			ptr += 1 + Constants.OBJECT_ID_LENGTH;
-			if (ptr > sz)
+			ptr += 1 + OBJECT_ID_LENGTH;
+			if (ptr > sz) {
 				throw new CorruptObjectException(
 						JGitText.get().corruptObjectTruncatedInObjectId);
+			}
+			if (ObjectId.zeroId().compareTo(raw, ptr - OBJECT_ID_LENGTH) == 0) {
+				report(NULL_SHA1, id, JGitText.get().corruptObjectZeroId);
+			}
 		}
 	}
 
-	private int scanPathSegment(byte[] raw, int ptr, int end)
-			throws CorruptObjectException {
+	private int scanPathSegment(byte[] raw, int ptr, int end,
+			@Nullable AnyObjectId id) throws CorruptObjectException {
 		for (; ptr < end; ptr++) {
 			byte c = raw[ptr];
-			if (c == 0)
+			if (c == 0) {
 				return ptr;
-			if (c == '/')
-				throw new CorruptObjectException(
+			}
+			if (c == '/') {
+				report(FULL_PATHNAME, id,
 						JGitText.get().corruptObjectNameContainsSlash);
+			}
 			if (windows && isInvalidOnWindows(c)) {
-				if (c > 31)
+				if (c > 31) {
 					throw new CorruptObjectException(String.format(
 							JGitText.get().corruptObjectNameContainsChar,
 							Byte.valueOf(c)));
+				}
 				throw new CorruptObjectException(String.format(
 						JGitText.get().corruptObjectNameContainsByte,
 						Integer.valueOf(c & 0xff)));
@@ -468,6 +724,26 @@ private int scanPathSegment(byte[] raw, int ptr, int end)
 		return ptr;
 	}
 
+	@SuppressWarnings("resource")
+	@Nullable
+	private ObjectId idFor(int objType, byte[] raw) {
+		if (skipList != null) {
+			return new ObjectInserter.Formatter().idFor(objType, raw);
+		}
+		return null;
+	}
+
+	private void report(@NonNull ErrorType err, @Nullable AnyObjectId id,
+			String why) throws CorruptObjectException {
+		if (errors.contains(err)
+				&& (id == null || skipList == null || !skipList.contains(id))) {
+			if (id != null) {
+				throw new CorruptObjectException(err, id, why);
+			}
+			throw new CorruptObjectException(why);
+		}
+	}
+
 	/**
 	 * Check tree path entry for validity.
 	 * <p>
@@ -518,73 +794,82 @@ public void checkPath(byte[] raw, int ptr, int end)
 	 */
 	public void checkPathSegment(byte[] raw, int ptr, int end)
 			throws CorruptObjectException {
-		int e = scanPathSegment(raw, ptr, end);
+		int e = scanPathSegment(raw, ptr, end, null);
 		if (e < end && raw[e] == 0)
 			throw new CorruptObjectException(
 					JGitText.get().corruptObjectNameContainsNullByte);
-		checkPathSegment2(raw, ptr, end);
+		checkPathSegment2(raw, ptr, end, null);
 	}
 
-	private void checkPathSegment2(byte[] raw, int ptr, int end)
-			throws CorruptObjectException {
-		if (ptr == end)
-			throw new CorruptObjectException(
-					JGitText.get().corruptObjectNameZeroLength);
+	private void checkPathSegment2(byte[] raw, int ptr, int end,
+			@Nullable AnyObjectId id) throws CorruptObjectException {
+		if (ptr == end) {
+			report(EMPTY_NAME, id, JGitText.get().corruptObjectNameZeroLength);
+			return;
+		}
+
 		if (raw[ptr] == '.') {
 			switch (end - ptr) {
 			case 1:
-				throw new CorruptObjectException(
-						JGitText.get().corruptObjectNameDot);
+				report(HAS_DOT, id, JGitText.get().corruptObjectNameDot);
+				break;
 			case 2:
-				if (raw[ptr + 1] == '.')
-					throw new CorruptObjectException(
+				if (raw[ptr + 1] == '.') {
+					report(HAS_DOTDOT, id,
 							JGitText.get().corruptObjectNameDotDot);
+				}
 				break;
 			case 4:
-				if (isGit(raw, ptr + 1))
-					throw new CorruptObjectException(String.format(
+				if (isGit(raw, ptr + 1)) {
+					report(HAS_DOTGIT, id, String.format(
 							JGitText.get().corruptObjectInvalidName,
 							RawParseUtils.decode(raw, ptr, end)));
+				}
 				break;
 			default:
-				if (end - ptr > 4 && isNormalizedGit(raw, ptr + 1, end))
-					throw new CorruptObjectException(String.format(
+				if (end - ptr > 4 && isNormalizedGit(raw, ptr + 1, end)) {
+					report(HAS_DOTGIT, id, String.format(
 							JGitText.get().corruptObjectInvalidName,
 							RawParseUtils.decode(raw, ptr, end)));
+				}
 			}
 		} else if (isGitTilde1(raw, ptr, end)) {
-			throw new CorruptObjectException(String.format(
+			report(HAS_DOTGIT, id, String.format(
 					JGitText.get().corruptObjectInvalidName,
 					RawParseUtils.decode(raw, ptr, end)));
 		}
-
-		if (macosx && isMacHFSGit(raw, ptr, end))
-			throw new CorruptObjectException(String.format(
+		if (macosx && isMacHFSGit(raw, ptr, end, id)) {
+			report(HAS_DOTGIT, id, String.format(
 					JGitText.get().corruptObjectInvalidNameIgnorableUnicode,
 					RawParseUtils.decode(raw, ptr, end)));
+		}
 
 		if (windows) {
 			// Windows ignores space and dot at end of file name.
-			if (raw[end - 1] == ' ' || raw[end - 1] == '.')
-				throw new CorruptObjectException(String.format(
+			if (raw[end - 1] == ' ' || raw[end - 1] == '.') {
+				report(WIN32_BAD_NAME, id, String.format(
 						JGitText.get().corruptObjectInvalidNameEnd,
 						Character.valueOf(((char) raw[end - 1]))));
-			if (end - ptr >= 3)
-				checkNotWindowsDevice(raw, ptr, end);
+			}
+			if (end - ptr >= 3) {
+				checkNotWindowsDevice(raw, ptr, end, id);
+			}
 		}
 	}
 
 	// Mac's HFS+ folds permutations of ".git" and Unicode ignorable characters
 	// to ".git" therefore we should prevent such names
-	private static boolean isMacHFSGit(byte[] raw, int ptr, int end)
-			throws CorruptObjectException {
+	private boolean isMacHFSGit(byte[] raw, int ptr, int end,
+			@Nullable AnyObjectId id) throws CorruptObjectException {
 		boolean ignorable = false;
 		byte[] git = new byte[] { '.', 'g', 'i', 't' };
 		int g = 0;
 		while (ptr < end) {
 			switch (raw[ptr]) {
 			case (byte) 0xe2: // http://www.utf8-chartable.de/unicode-utf8-table.pl?start=8192
-				checkTruncatedIgnorableUTF8(raw, ptr, end);
+				if (!checkTruncatedIgnorableUTF8(raw, ptr, end, id)) {
+					return false;
+				}
 				switch (raw[ptr + 1]) {
 				case (byte) 0x80:
 					switch (raw[ptr + 2]) {
@@ -621,7 +906,9 @@ private static boolean isMacHFSGit(byte[] raw, int ptr, int end)
 					return false;
 				}
 			case (byte) 0xef: // http://www.utf8-chartable.de/unicode-utf8-table.pl?start=65024
-				checkTruncatedIgnorableUTF8(raw, ptr, end);
+				if (!checkTruncatedIgnorableUTF8(raw, ptr, end, id)) {
+					return false;
+				}
 				// U+FEFF 0xefbbbf ZERO WIDTH NO-BREAK SPACE
 				if ((raw[ptr + 1] == (byte) 0xbb)
 						&& (raw[ptr + 2] == (byte) 0xbf)) {
@@ -642,12 +929,15 @@ private static boolean isMacHFSGit(byte[] raw, int ptr, int end)
 		return false;
 	}
 
-	private static void checkTruncatedIgnorableUTF8(byte[] raw, int ptr, int end)
-			throws CorruptObjectException {
-		if ((ptr + 2) >= end)
-			throw new CorruptObjectException(MessageFormat.format(
+	private boolean checkTruncatedIgnorableUTF8(byte[] raw, int ptr, int end,
+			@Nullable AnyObjectId id) throws CorruptObjectException {
+		if ((ptr + 2) >= end) {
+			report(BAD_UTF8, id, MessageFormat.format(
 					JGitText.get().corruptObjectInvalidNameInvalidUtf8,
 					toHexString(raw, ptr, end)));
+			return false;
+		}
+		return true;
 	}
 
 	private static String toHexString(byte[] raw, int ptr, int end) {
@@ -657,33 +947,36 @@ private static String toHexString(byte[] raw, int ptr, int end) {
 		return b.toString();
 	}
 
-	private static void checkNotWindowsDevice(byte[] raw, int ptr, int end)
-			throws CorruptObjectException {
+	private void checkNotWindowsDevice(byte[] raw, int ptr, int end,
+			@Nullable AnyObjectId id) throws CorruptObjectException {
 		switch (toLower(raw[ptr])) {
 		case 'a': // AUX
 			if (end - ptr >= 3
 					&& toLower(raw[ptr + 1]) == 'u'
 					&& toLower(raw[ptr + 2]) == 'x'
-					&& (end - ptr == 3 || raw[ptr + 3] == '.'))
-				throw new CorruptObjectException(
+					&& (end - ptr == 3 || raw[ptr + 3] == '.')) {
+				report(WIN32_BAD_NAME, id,
 						JGitText.get().corruptObjectInvalidNameAux);
+			}
 			break;
 
 		case 'c': // CON, COM[1-9]
 			if (end - ptr >= 3
 					&& toLower(raw[ptr + 2]) == 'n'
 					&& toLower(raw[ptr + 1]) == 'o'
-					&& (end - ptr == 3 || raw[ptr + 3] == '.'))
-				throw new CorruptObjectException(
+					&& (end - ptr == 3 || raw[ptr + 3] == '.')) {
+				report(WIN32_BAD_NAME, id,
 						JGitText.get().corruptObjectInvalidNameCon);
+			}
 			if (end - ptr >= 4
 					&& toLower(raw[ptr + 2]) == 'm'
 					&& toLower(raw[ptr + 1]) == 'o'
 					&& isPositiveDigit(raw[ptr + 3])
-					&& (end - ptr == 4 || raw[ptr + 4] == '.'))
-				throw new CorruptObjectException(String.format(
+					&& (end - ptr == 4 || raw[ptr + 4] == '.')) {
+				report(WIN32_BAD_NAME, id, String.format(
 						JGitText.get().corruptObjectInvalidNameCom,
 						Character.valueOf(((char) raw[ptr + 3]))));
+			}
 			break;
 
 		case 'l': // LPT[1-9]
@@ -691,28 +984,31 @@ && isPositiveDigit(raw[ptr + 3])
 					&& toLower(raw[ptr + 1]) == 'p'
 					&& toLower(raw[ptr + 2]) == 't'
 					&& isPositiveDigit(raw[ptr + 3])
-					&& (end - ptr == 4 || raw[ptr + 4] == '.'))
-				throw new CorruptObjectException(String.format(
+					&& (end - ptr == 4 || raw[ptr + 4] == '.')) {
+				report(WIN32_BAD_NAME, id, String.format(
 						JGitText.get().corruptObjectInvalidNameLpt,
 						Character.valueOf(((char) raw[ptr + 3]))));
+			}
 			break;
 
 		case 'n': // NUL
 			if (end - ptr >= 3
 					&& toLower(raw[ptr + 1]) == 'u'
 					&& toLower(raw[ptr + 2]) == 'l'
-					&& (end - ptr == 3 || raw[ptr + 3] == '.'))
-				throw new CorruptObjectException(
+					&& (end - ptr == 3 || raw[ptr + 3] == '.')) {
+				report(WIN32_BAD_NAME, id,
 						JGitText.get().corruptObjectInvalidNameNul);
+			}
 			break;
 
 		case 'p': // PRN
 			if (end - ptr >= 3
 					&& toLower(raw[ptr + 1]) == 'r'
 					&& toLower(raw[ptr + 2]) == 'n'
-					&& (end - ptr == 3 || raw[ptr + 3] == '.'))
-				throw new CorruptObjectException(
+					&& (end - ptr == 3 || raw[ptr + 3] == '.')) {
+				report(WIN32_BAD_NAME, id,
 						JGitText.get().corruptObjectInvalidNamePrn);
+			}
 			break;
 		}
 	}
@@ -765,6 +1061,15 @@ else if (raw[p] == ' ')
 		return false;
 	}
 
+	private boolean match(byte[] b, byte[] src) {
+		int r = RawParseUtils.match(b, bufPtr.value, src);
+		if (r < 0) {
+			return false;
+		}
+		bufPtr.value = r;
+		return true;
+	}
+
 	private static char toLower(byte b) {
 		if ('A' <= b && b <= 'Z')
 			return (char) (b + ('a' - 'A'));
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackParser.java
index 6e5fc9f..b96fe88 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackParser.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackParser.java
@@ -1049,8 +1049,11 @@ private void verifySafeObject(final AnyObjectId id, final int type,
 			final byte[] data) throws IOException {
 		if (objCheck != null) {
 			try {
-				objCheck.check(type, data);
+				objCheck.check(id, type, data);
 			} catch (CorruptObjectException e) {
+				if (e.getErrorType() != null) {
+					throw e;
+				}
 				throw new CorruptObjectException(MessageFormat.format(
 						JGitText.get().invalidObject,
 						Constants.typeString(type),
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java
index f9b74c8..72c9c8b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java
@@ -43,13 +43,20 @@
 
 package org.eclipse.jgit.transport;
 
+import static org.eclipse.jgit.util.StringUtils.equalsIgnoreCase;
+import static org.eclipse.jgit.util.StringUtils.toLowerCase;
+
+import java.io.File;
+import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.Map;
 
 import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.internal.storage.file.LazyObjectIdSetFile;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Config.SectionParser;
 import org.eclipse.jgit.lib.ObjectChecker;
+import org.eclipse.jgit.lib.ObjectIdSet;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.util.SystemReader;
@@ -59,6 +66,8 @@
  * parameters.
  */
 public class TransferConfig {
+	private static final String FSCK = "fsck"; //$NON-NLS-1$
+
 	/** Key for {@link Config#get(SectionParser)}. */
 	public static final Config.SectionParser<TransferConfig> KEY = new SectionParser<TransferConfig>() {
 		public TransferConfig parse(final Config cfg) {
@@ -66,9 +75,14 @@ public TransferConfig parse(final Config cfg) {
 		}
 	};
 
+	enum FsckMode {
+		ERROR, WARN, IGNORE;
+	}
+
 	private final boolean fetchFsck;
 	private final boolean receiveFsck;
-	private final boolean allowLeadingZeroFileMode;
+	private final String fsckSkipList;
+	private final EnumSet<ObjectChecker.ErrorType> ignore;
 	private final boolean allowInvalidPersonIdent;
 	private final boolean safeForWindows;
 	private final boolean safeForMacOS;
@@ -84,13 +98,44 @@ public TransferConfig parse(final Config cfg) {
 		boolean fsck = rc.getBoolean("transfer", "fsckobjects", false); //$NON-NLS-1$ //$NON-NLS-2$
 		fetchFsck = rc.getBoolean("fetch", "fsckobjects", fsck); //$NON-NLS-1$ //$NON-NLS-2$
 		receiveFsck = rc.getBoolean("receive", "fsckobjects", fsck); //$NON-NLS-1$ //$NON-NLS-2$
-		allowLeadingZeroFileMode = rc.getBoolean("fsck", "allowLeadingZeroFileMode", false); //$NON-NLS-1$ //$NON-NLS-2$
-		allowInvalidPersonIdent = rc.getBoolean("fsck", "allowInvalidPersonIdent", false); //$NON-NLS-1$ //$NON-NLS-2$
-		safeForWindows = rc.getBoolean("fsck", "safeForWindows", //$NON-NLS-1$ //$NON-NLS-2$
+		fsckSkipList = rc.getString(FSCK, null, "skipList"); //$NON-NLS-1$
+		allowInvalidPersonIdent = rc.getBoolean(FSCK, "allowInvalidPersonIdent", false); //$NON-NLS-1$
+		safeForWindows = rc.getBoolean(FSCK, "safeForWindows", //$NON-NLS-1$
 						SystemReader.getInstance().isWindows());
-		safeForMacOS = rc.getBoolean("fsck", "safeForMacOS", //$NON-NLS-1$ //$NON-NLS-2$
+		safeForMacOS = rc.getBoolean(FSCK, "safeForMacOS", //$NON-NLS-1$
 						SystemReader.getInstance().isMacOS());
 
+		ignore = EnumSet.noneOf(ObjectChecker.ErrorType.class);
+		EnumSet<ObjectChecker.ErrorType> set = EnumSet
+				.noneOf(ObjectChecker.ErrorType.class);
+		for (String key : rc.getNames(FSCK)) {
+			if (equalsIgnoreCase(key, "skipList") //$NON-NLS-1$
+					|| equalsIgnoreCase(key, "allowLeadingZeroFileMode") //$NON-NLS-1$
+					|| equalsIgnoreCase(key, "allowInvalidPersonIdent") //$NON-NLS-1$
+					|| equalsIgnoreCase(key, "safeForWindows") //$NON-NLS-1$
+					|| equalsIgnoreCase(key, "safeForMacOS")) { //$NON-NLS-1$
+				continue;
+			}
+
+			ObjectChecker.ErrorType id = FsckKeyNameHolder.parse(key);
+			if (id != null) {
+				switch (rc.getEnum(FSCK, null, key, FsckMode.ERROR)) {
+				case ERROR:
+					ignore.remove(id);
+					break;
+				case WARN:
+				case IGNORE:
+					ignore.add(id);
+					break;
+				}
+				set.add(id);
+			}
+		}
+		if (!set.contains(ObjectChecker.ErrorType.ZERO_PADDED_FILEMODE)
+				&& rc.getBoolean(FSCK, "allowLeadingZeroFileMode", false)) { //$NON-NLS-1$
+			ignore.add(ObjectChecker.ErrorType.ZERO_PADDED_FILEMODE);
+		}
+
 		allowTipSha1InWant = rc.getBoolean(
 				"uploadpack", "allowtipsha1inwant", false); //$NON-NLS-1$ //$NON-NLS-2$
 		allowReachableSha1InWant = rc.getBoolean(
@@ -123,10 +168,18 @@ private ObjectChecker newObjectChecker(boolean check) {
 			return null;
 		}
 		return new ObjectChecker()
-			.setAllowLeadingZeroFileMode(allowLeadingZeroFileMode)
+			.setIgnore(ignore)
 			.setAllowInvalidPersonIdent(allowInvalidPersonIdent)
 			.setSafeForWindows(safeForWindows)
-			.setSafeForMacOS(safeForMacOS);
+			.setSafeForMacOS(safeForMacOS)
+			.setSkipList(skipList());
+	}
+
+	private ObjectIdSet skipList() {
+		if (fsckSkipList != null && !fsckSkipList.isEmpty()) {
+			return new LazyObjectIdSetFile(new File(fsckSkipList));
+		}
+		return null;
 	}
 
 	/**
@@ -175,4 +228,34 @@ private boolean prefixMatch(String p, String s) {
 			}
 		};
 	}
+
+	static class FsckKeyNameHolder {
+		private static final Map<String, ObjectChecker.ErrorType> errors;
+
+		static {
+			errors = new HashMap<>();
+			for (ObjectChecker.ErrorType m : ObjectChecker.ErrorType.values()) {
+				errors.put(keyNameFor(m.name()), m);
+			}
+		}
+
+		@Nullable
+		static ObjectChecker.ErrorType parse(String key) {
+			return errors.get(toLowerCase(key));
+		}
+
+		private static String keyNameFor(String name) {
+			StringBuilder r = new StringBuilder(name.length());
+			for (int i = 0; i < name.length(); i++) {
+				char c = name.charAt(i);
+				if (c != '_') {
+					r.append(c);
+				}
+			}
+			return toLowerCase(r.toString());
+		}
+
+		private FsckKeyNameHolder() {
+		}
+	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkFetchConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkFetchConnection.java
index dfc3ee4..17edfdc 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkFetchConnection.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkFetchConnection.java
@@ -637,10 +637,11 @@ private void verifyAndInsertLooseObject(final AnyObjectId id,
 		final byte[] raw = uol.getCachedBytes();
 		if (objCheck != null) {
 			try {
-				objCheck.check(type, raw);
+				objCheck.check(id, type, raw);
 			} catch (CorruptObjectException e) {
-				throw new TransportException(MessageFormat.format(JGitText.get().transportExceptionInvalid
-						, Constants.typeString(type), id.name(), e.getMessage()));
+				throw new TransportException(MessageFormat.format(
+						JGitText.get().transportExceptionInvalid,
+						Constants.typeString(type), id.name(), e.getMessage()));
 			}
 		}