Merge branch 'stable-4.2'

* stable-4.2:
  BundleWriterTest: Open RevWalk in try-with-resource
  DiffFormatterTest: Remove accidentally added trailing whitespace
  CherryPickCommandTest: Create Git instances in try-with-resource
  DiffFormatterTest: Create auto-closeable instances in try-with-resource
  ConfigTest: Create Git instance in try-with-resource
  CommitAndLogCommandTest: Use assumeFalse to skip test on Windows
  CommitAndLogCommandTest: Create Git instances in try-with-resource
  AddCommandTest: Create Git instances in try-with-resource
  ArchiveCommandTest: Create Git instances in try-with-resource
  TagCommandTest: Instantiate Git and RevWalk objects in try-with-resource
  BlameCommandTest: Instantiate Git objects in try-with-resource
  SideBandOutputStreamTest: Use try-with-resource
  FileTreeIteratorJava7Test: Create Git instances in try-with-resource

Change-Id: Ib572e98e6117b70442aee9cd7e7b8c3cf65562a7
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
diff --git a/.buckconfig b/.buckconfig
new file mode 100644
index 0000000..b2e07ac
--- /dev/null
+++ b/.buckconfig
@@ -0,0 +1,15 @@
+[buildfile]
+  includes = //tools/default.defs
+
+[java]
+  src_roots = src, resources, tst
+
+[project]
+  ignore = .git
+
+[cache]
+  mode = dir
+
+[download]
+  maven_repo = http://repo1.maven.org/maven2
+  in_build = true
diff --git a/.buckversion b/.buckversion
new file mode 100644
index 0000000..9daac2c
--- /dev/null
+++ b/.buckversion
@@ -0,0 +1 @@
+1b03b4313b91b634bd604fc3487a05f877e59dee
diff --git a/.gitignore b/.gitignore
index 139e5ae..6c62199 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
 /target
 /.project
+/buck-cache
+/buck-out
diff --git a/BUCK b/BUCK
new file mode 100644
index 0000000..f19b7bd
--- /dev/null
+++ b/BUCK
@@ -0,0 +1,45 @@
+java_library(
+  name = 'jgit',
+  exported_deps = ['//org.eclipse.jgit:jgit'],
+  visibility = ['PUBLIC'],
+)
+
+genrule(
+  name = 'jgit_src',
+  cmd = 'ln -s $(location //org.eclipse.jgit:jgit_src) $OUT',
+  out = 'jgit_src.zip',
+  visibility = ['PUBLIC'],
+)
+
+java_library(
+  name = 'jgit-servlet',
+  exported_deps = [
+    ':jgit',
+    '//org.eclipse.jgit.http.server:jgit-servlet'
+  ],
+  visibility = ['PUBLIC'],
+)
+
+java_library(
+  name = 'jgit-archive',
+  exported_deps = [
+    ':jgit',
+    '//org.eclipse.jgit.archive:jgit-archive'
+  ],
+  visibility = ['PUBLIC'],
+)
+
+java_library(
+  name = 'junit',
+  exported_deps = [
+    ':jgit',
+    '//org.eclipse.jgit.junit:junit'
+  ],
+  visibility = ['PUBLIC'],
+)
+
+genrule(
+  name = 'jgit_bin',
+  cmd = 'ln -s $(location //org.eclipse.jgit.pgm:jgit) $OUT',
+  out = 'jgit_bin',
+)
diff --git a/lib/BUCK b/lib/BUCK
new file mode 100644
index 0000000..524612b
--- /dev/null
+++ b/lib/BUCK
@@ -0,0 +1,125 @@
+maven_jar(
+  name = 'jsch',
+  bin_sha1 = '658b682d5c817b27ae795637dfec047c63d29935',
+  src_sha1 = '791359d94d6edcace686a56d0727ee093a2f7c33',
+  group = 'com.jcraft',
+  artifact = 'jsch',
+  version = '0.1.53',
+)
+
+maven_jar(
+  name = 'javaewah',
+  bin_sha1 = 'eceaf316a8faf0e794296ebe158ae110c7d72a5a',
+  src_sha1 = 'a50d78eb630e05439461f3130b94b3bcd1ea6f03',
+  group = 'com.googlecode.javaewah',
+  artifact = 'JavaEWAH',
+  version = '0.7.9',
+)
+
+maven_jar(
+  name = 'httpcomponents',
+  bin_sha1 = '4c47155e3e6c9a41a28db36680b828ced53b8af4',
+  src_sha1 = 'af4d76be0c46ee26b0d9d1d4a34d244a633cac84',
+  group = 'org.apache.httpcomponents',
+  artifact = 'httpclient',
+  version = '4.3.6',
+)
+
+maven_jar(
+  name = 'httpcore',
+  bin_sha1 = 'f91b7a4aadc5cf486df6e4634748d7dd7a73f06d',
+  src_sha1 = '1b0aa62a6a91e9fa00c16f0a4a2c874804ed3b1e',
+  group = 'org.apache.httpcomponents',
+  artifact = 'httpcore',
+  version = '4.3.3',
+)
+
+maven_jar(
+  name = 'commons-logging',
+  bin_sha1 = 'f6f66e966c70a83ffbdb6f17a0919eaf7c8aca7f',
+  src_sha1 = '28bb0405fddaf04f15058fbfbe01fe2780d7d3b6',
+  group = 'commons-logging',
+  artifact = 'commons-logging',
+  version = '1.1.3',
+)
+
+maven_jar(
+  name = 'slf4j-api',
+  bin_sha1 = '0081d61b7f33ebeab314e07de0cc596f8e858d97',
+  src_sha1 = '58d38f68d4a867d4552ae27960bb348d7eaa1297',
+  group = 'org.slf4j',
+  artifact = 'slf4j-api',
+  version = '1.7.2',
+)
+
+maven_jar(
+  name = 'slf4j-simple',
+  bin_sha1 = '760055906d7353ba4f7ce1b8908bc6b2e91f39fa',
+  src_sha1 = '09474919128b3a7fcf21a5f9c907f5251f234544',
+  group = 'org.slf4j',
+  artifact = 'slf4j-simple',
+  version = '1.7.2',
+)
+
+maven_jar(
+  name = 'servlet-api',
+  bin_sha1 = '3cd63d075497751784b2fa84be59432f4905bf7c',
+  src_sha1 = 'ab3976d4574c48d22dc1abf6a9e8bd0fdf928223',
+  group = 'javax.servlet',
+  artifact = 'javax.servlet-api',
+  version = '3.1.0',
+)
+
+maven_jar(
+  name = 'commons-compress',
+  bin_sha1 = 'c7d9b580aff9e9f1998361f16578e63e5c064699',
+  src_sha1 = '396b81bdfd0fb617178e1707ef64832215307c78',
+  group = 'org.apache.commons',
+  artifact = 'commons-compress',
+  version = '1.6',
+)
+
+maven_jar(
+  name = 'tukaani-xz',
+  bin_sha1 = '66db21c8484120cb6a51b5b3ea47b6f383942bec',
+  src_sha1 = '6396220725701d767c553902c41120d7bf38e9f5',
+  group = 'org.tukaani',
+  artifact = 'xz',
+  version = '1.3',
+)
+
+maven_jar(
+  name = 'args4j',
+  bin_sha1 = '139441471327b9cc6d56436cb2a31e60eb6ed2ba',
+  src_sha1 = '22631b78cc8f60a6918557e8cbdb33e90f63a77f',
+  group = 'args4j',
+  artifact = 'args4j',
+  version = '2.0.15',
+)
+
+maven_jar(
+  name = 'junit',
+  bin_sha1 = '4e031bb61df09069aeb2bffb4019e7a5034a4ee0',
+  src_sha1 = '28e0ad201304e4a4abf999ca0570b7cffc352c3c',
+  group = 'junit',
+  artifact = 'junit',
+  version = '4.11',
+)
+
+maven_jar(
+  name = 'hamcrest-library',
+  bin_sha1 = '4785a3c21320980282f9f33d0d1264a69040538f',
+  src_sha1 = '047a7ee46628ab7133129cd7cef1e92657bc275e',
+  group = 'org.hamcrest',
+  artifact = 'hamcrest-library',
+  version = '1.3',
+)
+
+maven_jar(
+  name = 'hamcrest-core',
+  bin_sha1 = '42a25dc3219429f0e5d060061f71acb49bf010a0',
+  src_sha1 = '1dc37250fbc78e23a65a67fbbaf71d2e9cbc3c0b',
+  group = 'org.hamcrest',
+  artifact = 'hamcrest-core',
+  version = '1.3',
+)
diff --git a/lib/jetty/BUCK b/lib/jetty/BUCK
new file mode 100644
index 0000000..6e7dec3
--- /dev/null
+++ b/lib/jetty/BUCK
@@ -0,0 +1,56 @@
+VERSION = '9.2.13.v20150730'
+GROUP = 'org.eclipse.jetty'
+
+maven_jar(
+  name = 'servlet',
+  bin_sha1 = '5ad6e38015a97ae9a60b6c2ad744ccfa9cf93a50',
+  src_sha1 = '78fbec19321150552d91f9e079c2f2ca33222b01',
+  group = GROUP,
+  artifact = 'jetty-servlet',
+  version = VERSION,
+)
+
+maven_jar(
+  name = 'security',
+  bin_sha1 = 'cc7c7f27ec4cc279253be1675d9e47e58b995943',
+  src_sha1 = '75632ebdf8bd651faafb97106c92496db59e165d',
+  group = GROUP,
+  artifact = 'jetty-security',
+  version = VERSION,
+)
+
+maven_jar(
+  name = 'server',
+  bin_sha1 = '5be7d1da0a7abffd142de3091d160717c120b6ab',
+  src_sha1 = '203e123f83efe2a5b8a9c74854c7897fe3563302',
+  group = GROUP,
+  artifact = 'jetty-server',
+  version = VERSION,
+)
+
+maven_jar(
+  name = 'http',
+  bin_sha1 = '23a745d9177ef67ef53cc46b9b70c5870082efc2',
+  src_sha1 = '5f87f7ff2057cd4b0995bc4fffe17b2aff64c130',
+  group = GROUP,
+  artifact = 'jetty-http',
+  version = VERSION,
+)
+
+maven_jar(
+  name = 'io',
+  bin_sha1 = '7a351e6a1b63dfd56b6632623f7ca2793ffb67ad',
+  src_sha1 = 'bbd61a84b748fc295456e1c5c3070aaf40a68f62',
+  group = GROUP,
+  artifact = 'jetty-io',
+  version = VERSION,
+)
+
+maven_jar(
+  name = 'util',
+  bin_sha1 = 'c101476360a7cdd0670462de04053507d5e70c97',
+  src_sha1 = '15ceecce141971b4e0facb861b3d10120ad6ce03',
+  group = GROUP,
+  artifact = 'jetty-util',
+  version = VERSION,
+)
diff --git a/org.eclipse.jgit.archive/BUCK b/org.eclipse.jgit.archive/BUCK
new file mode 100644
index 0000000..ae17032
--- /dev/null
+++ b/org.eclipse.jgit.archive/BUCK
@@ -0,0 +1,13 @@
+java_library(
+  name = 'jgit-archive',
+  srcs = glob(
+    ['src/**'],
+    excludes = ['src/org/eclipse/jgit/archive/FormatActivator.java'],
+  ),
+  resources = glob(['resources/**']),
+  provided_deps = [
+    '//org.eclipse.jgit:jgit',
+    '//lib:commons-compress',
+  ],
+  visibility = ['PUBLIC'],
+)
diff --git a/org.eclipse.jgit.http.apache/BUCK b/org.eclipse.jgit.http.apache/BUCK
new file mode 100644
index 0000000..f48f33a
--- /dev/null
+++ b/org.eclipse.jgit.http.apache/BUCK
@@ -0,0 +1,12 @@
+java_library(
+  name = 'http-apache',
+  srcs = glob(['src/**']),
+  resources = glob(['resources/**']),
+  deps = [
+    '//org.eclipse.jgit:jgit',
+    '//lib:commons-logging',
+    '//lib:httpcomponents',
+    '//lib:httpcore',
+  ],
+  visibility = ['PUBLIC'],
+)
diff --git a/org.eclipse.jgit.http.apache/META-INF/MANIFEST.MF b/org.eclipse.jgit.http.apache/META-INF/MANIFEST.MF
index 07f364c..8058f30 100644
--- a/org.eclipse.jgit.http.apache/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.http.apache/META-INF/MANIFEST.MF
@@ -7,7 +7,8 @@
 Bundle-Localization: plugin
 Bundle-Vendor: %Provider-Name
 Bundle-ActivationPolicy: lazy
-Import-Package: org.apache.http;version="[4.1.0,5.0.0)",
+Import-Package: org.apache.commons.logging;version="[1.1.1,2.0.0)",
+ org.apache.http;version="[4.1.0,5.0.0)",
  org.apache.http.client;version="[4.1.0,5.0.0)",
  org.apache.http.client.methods;version="[4.1.0,5.0.0)",
  org.apache.http.client.params;version="[4.1.0,5.0.0)",
diff --git a/org.eclipse.jgit.http.apache/src/org/eclipse/jgit/transport/http/apache/HttpClientConnection.java b/org.eclipse.jgit.http.apache/src/org/eclipse/jgit/transport/http/apache/HttpClientConnection.java
index d42d6f2..de81bf8 100644
--- a/org.eclipse.jgit.http.apache/src/org/eclipse/jgit/transport/http/apache/HttpClientConnection.java
+++ b/org.eclipse.jgit.http.apache/src/org/eclipse/jgit/transport/http/apache/HttpClientConnection.java
@@ -100,7 +100,7 @@
 public class HttpClientConnection implements HttpConnection {
 	HttpClient client;
 
-	String urlStr;
+	URL url;
 
 	HttpUriRequest req;
 
@@ -176,16 +176,19 @@ public void setBuffer(TemporaryBuffer buffer) {
 
 	/**
 	 * @param urlStr
+	 * @throws MalformedURLException
 	 */
-	public HttpClientConnection(String urlStr) {
+	public HttpClientConnection(String urlStr) throws MalformedURLException {
 		this(urlStr, null);
 	}
 
 	/**
 	 * @param urlStr
 	 * @param proxy
+	 * @throws MalformedURLException
 	 */
-	public HttpClientConnection(String urlStr, Proxy proxy) {
+	public HttpClientConnection(String urlStr, Proxy proxy)
+			throws MalformedURLException {
 		this(urlStr, proxy, null);
 	}
 
@@ -193,10 +196,12 @@ public HttpClientConnection(String urlStr, Proxy proxy) {
 	 * @param urlStr
 	 * @param proxy
 	 * @param cl
+	 * @throws MalformedURLException
 	 */
-	public HttpClientConnection(String urlStr, Proxy proxy, HttpClient cl) {
+	public HttpClientConnection(String urlStr, Proxy proxy, HttpClient cl)
+			throws MalformedURLException {
 		this.client = cl;
-		this.urlStr = urlStr;
+		this.url = new URL(urlStr);
 		this.proxy = proxy;
 	}
 
@@ -206,11 +211,7 @@ public int getResponseCode() throws IOException {
 	}
 
 	public URL getURL() {
-		try {
-			return new URL(urlStr);
-		} catch (MalformedURLException e) {
-			return null;
-		}
+		return url;
 	}
 
 	public String getResponseMessage() throws IOException {
@@ -250,11 +251,11 @@ public void setRequestProperty(String name, String value) {
 	public void setRequestMethod(String method) throws ProtocolException {
 		this.method = method;
 		if ("GET".equalsIgnoreCase(method)) //$NON-NLS-1$
-			req = new HttpGet(urlStr);
+			req = new HttpGet(url.toString());
 		else if ("PUT".equalsIgnoreCase(method)) //$NON-NLS-1$
-			req = new HttpPut(urlStr);
+			req = new HttpPut(url.toString());
 		else if ("POST".equalsIgnoreCase(method)) //$NON-NLS-1$
-			req = new HttpPost(urlStr);
+			req = new HttpPost(url.toString());
 		else {
 			this.method = null;
 			throw new UnsupportedOperationException();
diff --git a/org.eclipse.jgit.http.server/BUCK b/org.eclipse.jgit.http.server/BUCK
new file mode 100644
index 0000000..3743557
--- /dev/null
+++ b/org.eclipse.jgit.http.server/BUCK
@@ -0,0 +1,10 @@
+java_library(
+  name = 'jgit-servlet',
+  srcs = glob(['src/**']),
+  resources = glob(['resources/**']),
+  provided_deps = [
+    '//org.eclipse.jgit:jgit',
+    '//lib:servlet-api',
+  ],
+  visibility = ['PUBLIC'],
+)
diff --git a/org.eclipse.jgit.http.test/BUCK b/org.eclipse.jgit.http.test/BUCK
new file mode 100644
index 0000000..d2ced7a
--- /dev/null
+++ b/org.eclipse.jgit.http.test/BUCK
@@ -0,0 +1,40 @@
+TESTS = glob(['tst/**/*.java'])
+
+for t in TESTS:
+  n = t[len('tst/'):len(t)-len('.java')].replace('/', '.')
+  java_test(
+    name = n,
+    labels = ['http'],
+    srcs = [t],
+    deps = [
+      ':helpers',
+      '//org.eclipse.jgit:jgit',
+      '//org.eclipse.jgit.http.apache:http-apache',
+      '//org.eclipse.jgit.http.server:jgit-servlet',
+      '//org.eclipse.jgit.junit:junit',
+      '//org.eclipse.jgit.junit.http:junit-http',
+      '//lib:hamcrest-core',
+      '//lib:hamcrest-library',
+      '//lib:junit',
+      '//lib:servlet-api',
+      '//lib/jetty:http',
+      '//lib/jetty:io',
+      '//lib/jetty:server',
+      '//lib/jetty:servlet',
+      '//lib/jetty:security',
+      '//lib/jetty:util',
+    ],
+    source_under_test = ['//org.eclipse.jgit.http.server:jgit-servlet'],
+  )
+
+java_library(
+  name = 'helpers',
+  srcs = glob(['src/**/*.java']),
+  deps = [
+    '//org.eclipse.jgit:jgit',
+    '//org.eclipse.jgit.http.server:jgit-servlet',
+    '//org.eclipse.jgit.junit:junit',
+    '//org.eclipse.jgit.junit.http:junit-http',
+    '//lib:junit',
+  ],
+)
diff --git a/org.eclipse.jgit.http.test/pom.xml b/org.eclipse.jgit.http.test/pom.xml
index dd52a89..0af30f6 100644
--- a/org.eclipse.jgit.http.test/pom.xml
+++ b/org.eclipse.jgit.http.test/pom.xml
@@ -134,6 +134,10 @@
         <artifactId>maven-surefire-plugin</artifactId>
         <configuration>
           <argLine>-Djava.io.tmpdir=${project.build.directory}  -Xmx300m</argLine>
+          <includes>
+            <include>**/*Test.java</include>
+            <include>**/*Tests.java</include>
+          </includes>
         </configuration>
       </plugin>
     </plugins>
diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/DumbClientDumbServerTest.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/DumbClientDumbServerTest.java
index 362a09d..677132d 100644
--- a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/DumbClientDumbServerTest.java
+++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/DumbClientDumbServerTest.java
@@ -140,8 +140,7 @@ public void testListRemote() throws IOException {
 		assertEquals("http", remoteURI.getScheme());
 
 		Map<String, Ref> map;
-		Transport t = Transport.open(dst, remoteURI);
-		try {
+		try (Transport t = Transport.open(dst, remoteURI)) {
 			// I didn't make up these public interface names, I just
 			// approved them for inclusion into the code base. Sorry.
 			// --spearce
@@ -149,14 +148,9 @@ public void testListRemote() throws IOException {
 			assertTrue("isa TransportHttp", t instanceof TransportHttp);
 			assertTrue("isa HttpTransport", t instanceof HttpTransport);
 
-			FetchConnection c = t.openFetch();
-			try {
+			try (FetchConnection c = t.openFetch()) {
 				map = c.getRefsMap();
-			} finally {
-				c.close();
 			}
-		} finally {
-			t.close();
 		}
 
 		assertNotNull("have map of refs", map);
@@ -201,11 +195,8 @@ public void testInitialClone_Loose() throws Exception {
 		Repository dst = createBareRepository();
 		assertFalse(dst.hasObject(A_txt));
 
-		Transport t = Transport.open(dst, remoteURI);
-		try {
+		try (Transport t = Transport.open(dst, remoteURI)) {
 			t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
-		} finally {
-			t.close();
 		}
 
 		assertTrue(dst.hasObject(A_txt));
@@ -226,11 +217,8 @@ public void testInitialClone_Packed() throws Exception {
 		Repository dst = createBareRepository();
 		assertFalse(dst.hasObject(A_txt));
 
-		Transport t = Transport.open(dst, remoteURI);
-		try {
+		try (Transport t = Transport.open(dst, remoteURI)) {
 			t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
-		} finally {
-			t.close();
 		}
 
 		assertTrue(dst.hasObject(A_txt));
@@ -265,8 +253,7 @@ public void testPushNotSupported() throws Exception {
 		final RevCommit Q = src.commit().create();
 		final Repository db = src.getRepository();
 
-		Transport t = Transport.open(db, remoteURI);
-		try {
+		try (Transport t = Transport.open(db, remoteURI)) {
 			try {
 				t.push(NullProgressMonitor.INSTANCE, push(src, Q));
 				fail("push incorrectly completed against a dumb server");
@@ -274,8 +261,6 @@ public void testPushNotSupported() throws Exception {
 				String exp = "remote does not support smart HTTP push";
 				assertEquals(exp, nse.getMessage());
 			}
-		} finally {
-			t.close();
 		}
 	}
 }
diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/GitServletResponseTests.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/GitServletResponseTests.java
index fba1a52..4b15d4b 100644
--- a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/GitServletResponseTests.java
+++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/GitServletResponseTests.java
@@ -60,6 +60,7 @@
 import org.eclipse.jgit.http.server.resolver.DefaultReceivePackFactory;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.junit.http.HttpTestCase;
+import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectChecker;
@@ -221,8 +222,9 @@ public void testObjectCheckerException() throws Exception {
 		preHook = null;
 		oc = new ObjectChecker() {
 			@Override
-			public void checkCommit(byte[] raw) throws CorruptObjectException {
-				throw new IllegalStateException();
+			public void checkCommit(AnyObjectId id, byte[] raw)
+					throws CorruptObjectException {
+				throw new CorruptObjectException("refusing all commits");
 			}
 		};
 
diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/HttpClientTests.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/HttpClientTests.java
index 6fb1302..ce78442 100644
--- a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/HttpClientTests.java
+++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/HttpClientTests.java
@@ -157,8 +157,7 @@ private static String nameOf(final Repository db) {
 	public void testRepositoryNotFound_Dumb() throws Exception {
 		URIish uri = toURIish("/dumb.none/not-found");
 		Repository dst = createBareRepository();
-		Transport t = Transport.open(dst, uri);
-		try {
+		try (Transport t = Transport.open(dst, uri)) {
 			try {
 				t.openFetch();
 				fail("connection opened to not found repository");
@@ -167,8 +166,6 @@ public void testRepositoryNotFound_Dumb() throws Exception {
 						+ "/info/refs?service=git-upload-pack not found";
 				assertEquals(exp, err.getMessage());
 			}
-		} finally {
-			t.close();
 		}
 	}
 
@@ -176,8 +173,7 @@ public void testRepositoryNotFound_Dumb() throws Exception {
 	public void testRepositoryNotFound_Smart() throws Exception {
 		URIish uri = toURIish("/smart.none/not-found");
 		Repository dst = createBareRepository();
-		Transport t = Transport.open(dst, uri);
-		try {
+		try (Transport t = Transport.open(dst, uri)) {
 			try {
 				t.openFetch();
 				fail("connection opened to not found repository");
@@ -186,8 +182,6 @@ public void testRepositoryNotFound_Smart() throws Exception {
 						+ "/info/refs?service=git-upload-pack not found";
 				assertEquals(exp, err.getMessage());
 			}
-		} finally {
-			t.close();
 		}
 	}
 
@@ -201,16 +195,9 @@ public void testListRemote_Dumb_DetachedHEAD() throws Exception {
 
 		Repository dst = createBareRepository();
 		Ref head;
-		Transport t = Transport.open(dst, dumbAuthNoneURI);
-		try {
-			FetchConnection c = t.openFetch();
-			try {
-				head = c.getRef(Constants.HEAD);
-			} finally {
-				c.close();
-			}
-		} finally {
-			t.close();
+		try (Transport t = Transport.open(dst, dumbAuthNoneURI);
+				FetchConnection c = t.openFetch()) {
+			head = c.getRef(Constants.HEAD);
 		}
 		assertNotNull("has " + Constants.HEAD, head);
 		assertEquals(Q, head.getObjectId());
@@ -225,16 +212,9 @@ public void testListRemote_Dumb_NoHEAD() throws Exception {
 
 		Repository dst = createBareRepository();
 		Ref head;
-		Transport t = Transport.open(dst, dumbAuthNoneURI);
-		try {
-			FetchConnection c = t.openFetch();
-			try {
-				head = c.getRef(Constants.HEAD);
-			} finally {
-				c.close();
-			}
-		} finally {
-			t.close();
+		try (Transport t = Transport.open(dst, dumbAuthNoneURI);
+				FetchConnection c = t.openFetch()) {
+			head = c.getRef(Constants.HEAD);
 		}
 		assertNull("has no " + Constants.HEAD, head);
 	}
@@ -249,16 +229,9 @@ public void testListRemote_Smart_DetachedHEAD() throws Exception {
 
 		Repository dst = createBareRepository();
 		Ref head;
-		Transport t = Transport.open(dst, smartAuthNoneURI);
-		try {
-			FetchConnection c = t.openFetch();
-			try {
-				head = c.getRef(Constants.HEAD);
-			} finally {
-				c.close();
-			}
-		} finally {
-			t.close();
+		try (Transport t = Transport.open(dst, smartAuthNoneURI);
+				FetchConnection c = t.openFetch()) {
+			head = c.getRef(Constants.HEAD);
 		}
 		assertNotNull("has " + Constants.HEAD, head);
 		assertEquals(Q, head.getObjectId());
@@ -268,16 +241,13 @@ public void testListRemote_Smart_DetachedHEAD() throws Exception {
 	public void testListRemote_Smart_WithQueryParameters() throws Exception {
 		URIish myURI = toURIish("/snone/do?r=1&p=test.git");
 		Repository dst = createBareRepository();
-		Transport t = Transport.open(dst, myURI);
-		try {
+		try (Transport t = Transport.open(dst, myURI)) {
 			try {
 				t.openFetch();
 				fail("test did not fail to find repository as expected");
 			} catch (NoRemoteRepositoryException err) {
 				// expected
 			}
-		} finally {
-			t.close();
 		}
 
 		List<AccessEvent> requests = getRequests();
@@ -296,62 +266,52 @@ public void testListRemote_Smart_WithQueryParameters() throws Exception {
 	@Test
 	public void testListRemote_Dumb_NeedsAuth() throws Exception {
 		Repository dst = createBareRepository();
-		Transport t = Transport.open(dst, dumbAuthBasicURI);
-		try {
+		try (Transport t = Transport.open(dst, dumbAuthBasicURI)) {
 			try {
 				t.openFetch();
 				fail("connection opened even info/refs needs auth basic");
 			} catch (TransportException err) {
 				String exp = dumbAuthBasicURI + ": "
-						+ JGitText.get().notAuthorized;
+						+ JGitText.get().noCredentialsProvider;
 				assertEquals(exp, err.getMessage());
 			}
-		} finally {
-			t.close();
 		}
 	}
 
 	@Test
 	public void testListRemote_Dumb_Auth() throws Exception {
 		Repository dst = createBareRepository();
-		Transport t = Transport.open(dst, dumbAuthBasicURI);
-		t.setCredentialsProvider(new UsernamePasswordCredentialsProvider(
-				AppServer.username, AppServer.password));
-		try {
-			t.openFetch();
-		} finally {
-			t.close();
+		try (Transport t = Transport.open(dst, dumbAuthBasicURI)) {
+			t.setCredentialsProvider(new UsernamePasswordCredentialsProvider(
+					AppServer.username, AppServer.password));
+			t.openFetch().close();
 		}
-		t = Transport.open(dst, dumbAuthBasicURI);
-		t.setCredentialsProvider(new UsernamePasswordCredentialsProvider(
-				AppServer.username, ""));
-		try {
-			t.openFetch();
-			fail("connection opened even info/refs needs auth basic and we provide wrong password");
-		} catch (TransportException err) {
-			String exp = dumbAuthBasicURI + ": "
-					+ JGitText.get().notAuthorized;
-			assertEquals(exp, err.getMessage());
-		} finally {
-			t.close();
+		try (Transport t = Transport.open(dst, dumbAuthBasicURI)) {
+			t.setCredentialsProvider(new UsernamePasswordCredentialsProvider(
+					AppServer.username, ""));
+			try {
+				t.openFetch();
+				fail("connection opened even info/refs needs auth basic and we provide wrong password");
+			} catch (TransportException err) {
+				String exp = dumbAuthBasicURI + ": "
+						+ JGitText.get().notAuthorized;
+				assertEquals(exp, err.getMessage());
+			}
 		}
 	}
 
 	@Test
 	public void testListRemote_Smart_UploadPackNeedsAuth() throws Exception {
 		Repository dst = createBareRepository();
-		Transport t = Transport.open(dst, smartAuthBasicURI);
-		try {
+		try (Transport t = Transport.open(dst, smartAuthBasicURI)) {
 			try {
 				t.openFetch();
 				fail("connection opened even though service disabled");
 			} catch (TransportException err) {
 				String exp = smartAuthBasicURI + ": "
-						+ JGitText.get().notAuthorized;
+						+ JGitText.get().noCredentialsProvider;
 				assertEquals(exp, err.getMessage());
 			}
-		} finally {
-			t.close();
 		}
 	}
 
@@ -363,33 +323,24 @@ public void testListRemote_Smart_UploadPackDisabled() throws Exception {
 		cfg.save();
 
 		Repository dst = createBareRepository();
-		Transport t = Transport.open(dst, smartAuthNoneURI);
-		try {
+		try (Transport t = Transport.open(dst, smartAuthNoneURI)) {
 			try {
 				t.openFetch();
 				fail("connection opened even though service disabled");
 			} catch (TransportException err) {
-				String exp = smartAuthNoneURI + ": Git access forbidden";
+				String exp = smartAuthNoneURI + ": "
+						+ JGitText.get().serviceNotEnabledNoName;
 				assertEquals(exp, err.getMessage());
 			}
-		} finally {
-			t.close();
 		}
 	}
 
 	@Test
 	public void testListRemoteWithoutLocalRepository() throws Exception {
-		Transport t = Transport.open(smartAuthNoneURI);
-		try {
-			FetchConnection c = t.openFetch();
-			try {
-				Ref head = c.getRef(Constants.HEAD);
-				assertNotNull(head);
-			} finally {
-				c.close();
-			}
-		} finally {
-			t.close();
+		try (Transport t = Transport.open(smartAuthNoneURI);
+				FetchConnection c = t.openFetch()) {
+			Ref head = c.getRef(Constants.HEAD);
+			assertNotNull(head);
 		}
 	}
 }
diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerTest.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerTest.java
index 9ca0789..82861ed 100644
--- a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerTest.java
+++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerTest.java
@@ -211,8 +211,7 @@ public void testListRemote() throws IOException {
 		assertEquals("http", remoteURI.getScheme());
 
 		Map<String, Ref> map;
-		Transport t = Transport.open(dst, remoteURI);
-		try {
+		try (Transport t = Transport.open(dst, remoteURI)) {
 			// I didn't make up these public interface names, I just
 			// approved them for inclusion into the code base. Sorry.
 			// --spearce
@@ -226,8 +225,6 @@ public void testListRemote() throws IOException {
 			} finally {
 				c.close();
 			}
-		} finally {
-			t.close();
 		}
 
 		assertNotNull("have map of refs", map);
@@ -257,8 +254,7 @@ public void testListRemote() throws IOException {
 	public void testListRemote_BadName() throws IOException, URISyntaxException {
 		Repository dst = createBareRepository();
 		URIish uri = new URIish(this.remoteURI.toString() + ".invalid");
-		Transport t = Transport.open(dst, uri);
-		try {
+		try (Transport t = Transport.open(dst, uri)) {
 			try {
 				t.openFetch();
 				fail("fetch connection opened");
@@ -266,8 +262,6 @@ public void testListRemote_BadName() throws IOException, URISyntaxException {
 				assertEquals(uri + ": Git repository not found",
 						notFound.getMessage());
 			}
-		} finally {
-			t.close();
 		}
 
 		List<AccessEvent> requests = getRequests();
@@ -288,11 +282,8 @@ public void testInitialClone_Small() throws Exception {
 		Repository dst = createBareRepository();
 		assertFalse(dst.hasObject(A_txt));
 
-		Transport t = Transport.open(dst, remoteURI);
-		try {
+		try (Transport t = Transport.open(dst, remoteURI)) {
 			t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
-		} finally {
-			t.close();
 		}
 
 		assertTrue(dst.hasObject(A_txt));
@@ -331,11 +322,8 @@ public void testFetch_FewLocalCommits() throws Exception {
 		// Bootstrap by doing the clone.
 		//
 		TestRepository dst = createTestRepository();
-		Transport t = Transport.open(dst.getRepository(), remoteURI);
-		try {
+		try (Transport t = Transport.open(dst.getRepository(), remoteURI)) {
 			t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
-		} finally {
-			t.close();
 		}
 		assertEquals(B, dst.getRepository().exactRef(master).getObjectId());
 		List<AccessEvent> cloneRequests = getRequests();
@@ -352,11 +340,8 @@ public void testFetch_FewLocalCommits() throws Exception {
 
 		// Now incrementally update.
 		//
-		t = Transport.open(dst.getRepository(), remoteURI);
-		try {
+		try (Transport t = Transport.open(dst.getRepository(), remoteURI)) {
 			t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
-		} finally {
-			t.close();
 		}
 		assertEquals(Z, dst.getRepository().exactRef(master).getObjectId());
 
@@ -394,11 +379,8 @@ public void testFetch_TooManyLocalCommits() throws Exception {
 		// Bootstrap by doing the clone.
 		//
 		TestRepository dst = createTestRepository();
-		Transport t = Transport.open(dst.getRepository(), remoteURI);
-		try {
+		try (Transport t = Transport.open(dst.getRepository(), remoteURI)) {
 			t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
-		} finally {
-			t.close();
 		}
 		assertEquals(B, dst.getRepository().exactRef(master).getObjectId());
 		List<AccessEvent> cloneRequests = getRequests();
@@ -418,11 +400,8 @@ public void testFetch_TooManyLocalCommits() throws Exception {
 
 		// Now incrementally update.
 		//
-		t = Transport.open(dst.getRepository(), remoteURI);
-		try {
+		try (Transport t = Transport.open(dst.getRepository(), remoteURI)) {
 			t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
-		} finally {
-			t.close();
 		}
 		assertEquals(Z, dst.getRepository().exactRef(master).getObjectId());
 
@@ -474,8 +453,7 @@ public void testInitialClone_BrokenServer() throws Exception {
 		Repository dst = createBareRepository();
 		assertFalse(dst.hasObject(A_txt));
 
-		Transport t = Transport.open(dst, brokenURI);
-		try {
+		try (Transport t = Transport.open(dst, brokenURI)) {
 			try {
 				t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
 				fail("fetch completed despite upload-pack being broken");
@@ -485,8 +463,6 @@ public void testInitialClone_BrokenServer() throws Exception {
 						+ " received Content-Type text/plain; charset=UTF-8";
 				assertEquals(exp, err.getMessage());
 			}
-		} finally {
-			t.close();
 		}
 
 		List<AccessEvent> requests = getRequests();
@@ -517,12 +493,10 @@ public void testPush_NotAuthorized() throws Exception {
 		final RevCommit Q = src.commit().add("Q", Q_txt).create();
 		final Repository db = src.getRepository();
 		final String dstName = Constants.R_HEADS + "new.branch";
-		Transport t;
 
 		// push anonymous shouldn't be allowed.
 		//
-		t = Transport.open(db, remoteURI);
-		try {
+		try (Transport t = Transport.open(db, remoteURI)) {
 			final String srcExpr = Q.name();
 			final boolean forceUpdate = false;
 			final String localName = null;
@@ -538,8 +512,6 @@ public void testPush_NotAuthorized() throws Exception {
 						+ JGitText.get().authenticationNotSupported;
 				assertEquals(exp, e.getMessage());
 			}
-		} finally {
-			t.close();
 		}
 
 		List<AccessEvent> requests = getRequests();
@@ -560,12 +532,10 @@ public void testPush_CreateBranch() throws Exception {
 		final RevCommit Q = src.commit().add("Q", Q_txt).create();
 		final Repository db = src.getRepository();
 		final String dstName = Constants.R_HEADS + "new.branch";
-		Transport t;
 
 		enableReceivePack();
 
-		t = Transport.open(db, remoteURI);
-		try {
+		try (Transport t = Transport.open(db, remoteURI)) {
 			final String srcExpr = Q.name();
 			final boolean forceUpdate = false;
 			final String localName = null;
@@ -574,8 +544,6 @@ public void testPush_CreateBranch() throws Exception {
 			RemoteRefUpdate u = new RemoteRefUpdate(src.getRepository(),
 					srcExpr, dstName, forceUpdate, localName, oldId);
 			t.push(NullProgressMonitor.INSTANCE, Collections.singleton(u));
-		} finally {
-			t.close();
 		}
 
 		assertTrue(remoteRepository.hasObject(Q_txt));
@@ -633,7 +601,6 @@ public void testPush_ChunkedEncoding() throws Exception {
 		final RevCommit Q = src.commit().add("Q", Q_bin).create();
 		final Repository db = src.getRepository();
 		final String dstName = Constants.R_HEADS + "new.branch";
-		Transport t;
 
 		enableReceivePack();
 
@@ -642,8 +609,7 @@ public void testPush_ChunkedEncoding() throws Exception {
 		cfg.setInt("http", null, "postbuffer", 8 * 1024);
 		cfg.save();
 
-		t = Transport.open(db, remoteURI);
-		try {
+		try (Transport t = Transport.open(db, remoteURI)) {
 			final String srcExpr = Q.name();
 			final boolean forceUpdate = false;
 			final String localName = null;
@@ -652,8 +618,6 @@ public void testPush_ChunkedEncoding() throws Exception {
 			RemoteRefUpdate u = new RemoteRefUpdate(src.getRepository(),
 					srcExpr, dstName, forceUpdate, localName, oldId);
 			t.push(NullProgressMonitor.INSTANCE, Collections.singleton(u));
-		} finally {
-			t.close();
 		}
 
 		assertTrue(remoteRepository.hasObject(Q_bin));
diff --git a/org.eclipse.jgit.junit.http/BUCK b/org.eclipse.jgit.junit.http/BUCK
new file mode 100644
index 0000000..68976a6
--- /dev/null
+++ b/org.eclipse.jgit.junit.http/BUCK
@@ -0,0 +1,18 @@
+java_library(
+  name = 'junit-http',
+  srcs = glob(['src/**']),
+  resources = glob(['resources/**']),
+  provided_deps = [
+    '//org.eclipse.jgit:jgit',
+    '//org.eclipse.jgit.http.server:jgit-servlet',
+    '//org.eclipse.jgit.junit:junit',
+    '//lib:junit',
+    '//lib:servlet-api',
+    '//lib/jetty:http',
+    '//lib/jetty:server',
+    '//lib/jetty:servlet',
+    '//lib/jetty:security',
+    '//lib/jetty:util',
+  ],
+  visibility = ['PUBLIC'],
+)
diff --git a/org.eclipse.jgit.junit/BUCK b/org.eclipse.jgit.junit/BUCK
new file mode 100644
index 0000000..7e25432
--- /dev/null
+++ b/org.eclipse.jgit.junit/BUCK
@@ -0,0 +1,10 @@
+java_library(
+  name = 'junit',
+  srcs = glob(['src/**']),
+  resources = glob(['resources/**']),
+  provided_deps = [
+    '//org.eclipse.jgit:jgit',
+    '//lib:junit',
+  ],
+  visibility = ['PUBLIC'],
+)
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..8439c39 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);
 			}
 		}
@@ -866,7 +866,7 @@ public void packAndPrune() throws Exception {
 				Set<ObjectId> all = new HashSet<ObjectId>();
 				for (Ref r : db.getAllRefs().values())
 					all.add(r.getObjectId());
-				pw.preparePack(m, all, Collections.<ObjectId> emptySet());
+				pw.preparePack(m, all, PackWriter.NONE);
 
 				final ObjectId name = pw.computeName();
 
@@ -1155,8 +1155,7 @@ public RevCommit create() throws Exception {
 			return self;
 		}
 
-		private void insertChangeId(org.eclipse.jgit.lib.CommitBuilder c)
-				throws IOException {
+		private void insertChangeId(org.eclipse.jgit.lib.CommitBuilder c) {
 			if (changeId == null)
 				return;
 			int idx = ChangeIdUtil.indexOfChangeId(message, "\n");
diff --git a/org.eclipse.jgit.pgm.test/BUCK b/org.eclipse.jgit.pgm.test/BUCK
new file mode 100644
index 0000000..a3859c9
--- /dev/null
+++ b/org.eclipse.jgit.pgm.test/BUCK
@@ -0,0 +1,38 @@
+TESTS = glob(['tst/**/*.java'])
+
+for t in TESTS:
+  n = t[len('tst/'):len(t)-len('.java')].replace('/', '.')
+  java_test(
+    name = n,
+    labels = ['pgm'],
+    srcs = [t],
+    deps = [
+      ':helpers',
+      '//org.eclipse.jgit:jgit',
+      '//org.eclipse.jgit.archive:jgit-archive',
+      '//org.eclipse.jgit.junit:junit',
+      '//org.eclipse.jgit.pgm:pgm',
+      '//lib:hamcrest-core',
+      '//lib:hamcrest-library',
+      '//lib:javaewah',
+      '//lib:junit',
+      '//lib:slf4j-api',
+      '//lib:slf4j-simple',
+      '//lib:commons-compress',
+      '//lib:tukaani-xz',
+    ],
+    source_under_test = ['//org.eclipse.jgit.pgm:pgm'],
+    vm_args = ['-Xmx256m', '-Dfile.encoding=UTF-8'],
+  )
+
+java_library(
+  name = 'helpers',
+  srcs = glob(['src/**/*.java']),
+  deps = [
+    '//org.eclipse.jgit:jgit',
+    '//org.eclipse.jgit.pgm:pgm',
+    '//org.eclipse.jgit.junit:junit',
+    '//lib:args4j',
+    '//lib:junit',
+  ],
+)
diff --git a/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF
index db23c3b..2514fdf 100644
--- a/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF
@@ -11,6 +11,7 @@
  org.eclipse.jgit.api.errors;version="[4.2.0,4.3.0)",
  org.eclipse.jgit.diff;version="[4.2.0,4.3.0)",
  org.eclipse.jgit.dircache;version="[4.2.0,4.3.0)",
+ org.eclipse.jgit.internal.storage.file;version="4.2.0",
  org.eclipse.jgit.junit;version="[4.2.0,4.3.0)",
  org.eclipse.jgit.lib;version="[4.2.0,4.3.0)",
  org.eclipse.jgit.merge;version="[4.2.0,4.3.0)",
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 559a6d5..a6af077 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
@@ -46,12 +46,16 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
 import org.eclipse.jgit.junit.JGitTestUtil;
 import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
 import org.eclipse.jgit.pgm.CLIGitCommand;
+import org.eclipse.jgit.pgm.CLIGitCommand.Result;
+import org.eclipse.jgit.pgm.TextBuiltin.TerminatedByHelpException;
 import org.junit.Before;
 
 public class CLIRepositoryTestCase extends LocalDiskRepositoryTestCase {
@@ -69,13 +73,59 @@ public void setUp() throws Exception {
 		trash = db.getWorkTree();
 	}
 
+	/**
+	 * Executes specified git commands (with arguments)
+	 *
+	 * @param cmds
+	 *            each string argument must be a valid git command line, e.g.
+	 *            "git branch -h"
+	 * @return command output
+	 * @throws Exception
+	 */
+	protected String[] executeUnchecked(String... cmds) throws Exception {
+		List<String> result = new ArrayList<String>(cmds.length);
+		for (String cmd : cmds) {
+			result.addAll(CLIGitCommand.executeUnchecked(cmd, db));
+		}
+		return result.toArray(new String[0]);
+	}
+
+	/**
+	 * Executes specified git commands (with arguments), throws exception and
+	 * stops execution on first command which output contains a 'fatal:' error
+	 *
+	 * @param cmds
+	 *            each string argument must be a valid git command line, e.g.
+	 *            "git branch -h"
+	 * @return command output
+	 * @throws Exception
+	 */
 	protected String[] execute(String... cmds) throws Exception {
 		List<String> result = new ArrayList<String>(cmds.length);
-		for (String cmd : cmds)
-			result.addAll(CLIGitCommand.execute(cmd, db));
+		for (String cmd : cmds) {
+			Result r = CLIGitCommand.executeRaw(cmd, db);
+			if (r.ex instanceof TerminatedByHelpException) {
+				result.addAll(r.errLines());
+			} else if (r.ex != null) {
+				throw r.ex;
+			}
+			result.addAll(r.outLines());
+		}
 		return result.toArray(new String[0]);
 	}
 
+	/**
+	 * @param link
+	 *            the path of the symbolic link to create
+	 * @param target
+	 *            the target of the symbolic link
+	 * @return the path to the symbolic link
+	 * @throws Exception
+	 */
+	protected Path writeLink(String link, String target) throws Exception {
+		return JGitTestUtil.writeLink(db, link, target);
+	}
+
 	protected File writeTrashFile(final String name, final String data)
 			throws IOException {
 		return JGitTestUtil.writeTrashFile(db, name, data);
@@ -173,15 +223,36 @@ protected void assertStringArrayEquals(String expected, String[] actual) {
 	}
 
 	protected void assertArrayOfLinesEquals(String[] expected, String[] actual) {
-		assertEquals(toText(expected), toText(actual));
+		assertEquals(toString(expected), toString(actual));
 	}
 
-	private static String toText(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) {
-			b.append(s);
-			b.append('\n');
+			// trim indentation, to simplify tests
+			s = s.trim();
+			if (s != null && !s.isEmpty()) {
+				b.append(s);
+				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();
 	}
+
+	public static boolean contains(List<String> lines, String str) {
+		for (String s : lines) {
+			if (s.contains(str)) {
+				return true;
+			}
+		}
+		return false;
+	}
 }
diff --git a/org.eclipse.jgit.pgm.test/src/org/eclipse/jgit/pgm/CLIGitCommand.java b/org.eclipse.jgit.pgm.test/src/org/eclipse/jgit/pgm/CLIGitCommand.java
index d77b150..3f39656 100644
--- a/org.eclipse.jgit.pgm.test/src/org/eclipse/jgit/pgm/CLIGitCommand.java
+++ b/org.eclipse.jgit.pgm.test/src/org/eclipse/jgit/pgm/CLIGitCommand.java
@@ -42,71 +42,140 @@
  */
 package org.eclipse.jgit.pgm;
 
+import static org.junit.Assert.assertNull;
+
 import java.io.ByteArrayOutputStream;
-import java.text.MessageFormat;
+import java.io.File;
+
+import java.io.IOException;
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
 
+import org.eclipse.jgit.internal.storage.file.FileRepository;
 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.SubcommandHandler;
+import org.eclipse.jgit.pgm.TextBuiltin.TerminatedByHelpException;
 import org.eclipse.jgit.util.IO;
-import org.kohsuke.args4j.Argument;
 
-public class CLIGitCommand {
-	@Argument(index = 0, metaVar = "metaVar_command", required = true, handler = SubcommandHandler.class)
-	private TextBuiltin subcommand;
+public class CLIGitCommand extends Main {
 
-	@Argument(index = 1, metaVar = "metaVar_arg")
-	private List<String> arguments = new ArrayList<String>();
+	private final Result result;
 
-	public TextBuiltin getSubcommand() {
-		return subcommand;
+	private final Repository db;
+
+	public CLIGitCommand(Repository db) {
+		super();
+		this.db = db;
+		result = new Result();
 	}
 
-	public List<String> getArguments() {
-		return arguments;
+	/**
+	 * Executes git commands (with arguments) specified on the command line. The
+	 * git repository (same for all commands) can be specified via system
+	 * property "-Dgit_work_tree=path_to_work_tree". If the property is not set,
+	 * current directory is used.
+	 *
+	 * @param args
+	 *            each element in the array must be a valid git command line,
+	 *            e.g. "git branch -h"
+	 * @throws Exception
+	 */
+	public static void main(String[] args) throws Exception {
+		String workDir = System.getProperty("git_work_tree");
+		if (workDir == null) {
+			workDir = ".";
+			System.out.println(
+					"System property 'git_work_tree' not specified, using current directory: "
+							+ new File(workDir).getAbsolutePath());
+		}
+		try (Repository db = new FileRepository(workDir + "/.git")) {
+			for (String cmd : args) {
+				List<String> result = execute(cmd, db);
+				for (String line : result) {
+					System.out.println(line);
+				}
+			}
+		}
 	}
 
 	public static List<String> execute(String str, Repository db)
 			throws Exception {
+		Result result = executeRaw(str, db);
+		return getOutput(result);
+	}
+
+	public static Result executeRaw(String str, Repository db)
+			throws Exception {
+		CLIGitCommand cmd = new CLIGitCommand(db);
+		cmd.run(str);
+		return cmd.result;
+	}
+
+	public static List<String> executeUnchecked(String str, Repository db)
+			throws Exception {
+		CLIGitCommand cmd = new CLIGitCommand(db);
 		try {
-			return IO.readLines(new String(rawExecute(str, db)));
-		} catch (Die e) {
-			return IO.readLines(MessageFormat.format(CLIText.get().fatalError,
-					e.getMessage()));
+			cmd.run(str);
+			return getOutput(cmd.result);
+		} catch (Throwable e) {
+			return cmd.result.errLines();
 		}
 	}
 
-	public static byte[] rawExecute(String str, Repository db)
+	private static List<String> getOutput(Result result) {
+		if (result.ex instanceof TerminatedByHelpException) {
+			return result.errLines();
+		}
+		return result.outLines();
+	}
+
+	private void run(String commandLine) throws Exception {
+		String[] argv = convertToMainArgs(commandLine);
+		try {
+			super.run(argv);
+		} catch (TerminatedByHelpException e) {
+			// this is not a failure, super called exit() on help
+		} finally {
+			writer.flush();
+		}
+	}
+
+	private static String[] convertToMainArgs(String str)
 			throws Exception {
 		String[] args = split(str);
-		if (!args[0].equalsIgnoreCase("git") || args.length < 2)
+		if (!args[0].equalsIgnoreCase("git") || args.length < 2) {
 			throw new IllegalArgumentException(
 					"Expected 'git <command> [<args>]', was:" + str);
+		}
 		String[] argv = new String[args.length - 1];
 		System.arraycopy(args, 1, argv, 0, args.length - 1);
+		return argv;
+	}
 
-		CLIGitCommand bean = new CLIGitCommand();
-		final CmdLineParser clp = new CmdLineParser(bean);
-		clp.parseArgument(argv);
+	@Override
+	PrintWriter createErrorWriter() {
+		return new PrintWriter(result.err);
+	}
 
-		final TextBuiltin cmd = bean.getSubcommand();
-		ByteArrayOutputStream baos = new ByteArrayOutputStream();
-		cmd.outs = baos;
-		if (cmd.requiresRepository())
-			cmd.init(db, null);
-		else
-			cmd.init(null, null);
-		try {
-			cmd.execute(bean.getArguments().toArray(
-					new String[bean.getArguments().size()]));
-		} finally {
-			if (cmd.outw != null)
-				cmd.outw.flush();
+	void init(final TextBuiltin cmd) throws IOException {
+		cmd.outs = result.out;
+		cmd.errs = result.err;
+		super.init(cmd);
+	}
+
+	@Override
+	protected Repository openGitDir(String aGitdir) throws IOException {
+		assertNull(aGitdir);
+		return db;
+	}
+
+	@Override
+	void exit(int status, Exception t) throws Exception {
+		if (t == null) {
+			t = new IllegalStateException(Integer.toString(status));
 		}
-		return baos.toByteArray();
+		result.ex = t;
+		throw t;
 	}
 
 	/**
@@ -164,4 +233,36 @@ else if (r.length() > 0) {
 		return list.toArray(new String[list.size()]);
 	}
 
+	public static class Result {
+		public final ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+		public final ByteArrayOutputStream err = new ByteArrayOutputStream();
+
+		public Exception ex;
+
+		public byte[] outBytes() {
+			return out.toByteArray();
+		}
+
+		public byte[] errBytes() {
+			return err.toByteArray();
+		}
+
+		public String outString() {
+			return out.toString();
+		}
+
+		public List<String> outLines() {
+			return IO.readLines(out.toString());
+		}
+
+		public String errString() {
+			return err.toString();
+		}
+
+		public List<String> errLines() {
+			return IO.readLines(err.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 4253080..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,15 +45,12 @@
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
-
-import java.lang.Exception;
-import java.lang.String;
+import static org.junit.Assert.fail;
 
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.lib.CLIRepositoryTestCase;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 
 public class AddTest extends CLIRepositoryTestCase {
@@ -66,14 +63,16 @@ public void setUp() throws Exception {
 		git = new Git(db);
 	}
 
-	@Ignore("args4j exit()s on error instead of throwing, JVM goes down")
 	@Test
 	public void testAddNothing() throws Exception {
-		assertEquals("fatal: Argument \"filepattern\" is required", //
-				execute("git add")[0]);
+		try {
+			execute("git add");
+			fail("Must die");
+		} catch (Die e) {
+			// expected, requires argument
+		}
 	}
 
-	@Ignore("args4j exit()s for --help, too")
 	@Test
 	public void testAddUsage() throws Exception {
 		execute("git add --help");
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ArchiveTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ArchiveTest.java
index 4222a2d..a503ffd 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ArchiveTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ArchiveTest.java
@@ -52,17 +52,15 @@
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
-import java.io.InputStreamReader;
 import java.io.IOException;
+import java.io.InputStreamReader;
 import java.io.OutputStream;
-import java.lang.Object;
-import java.lang.String;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.concurrent.Callable;
-import java.util.concurrent.Executors;
 import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipInputStream;
@@ -71,9 +69,7 @@
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.lib.CLIRepositoryTestCase;
 import org.eclipse.jgit.lib.FileMode;
-import org.eclipse.jgit.pgm.CLIGitCommand;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Test;
 
 public class ArchiveTest extends CLIRepositoryTestCase {
@@ -89,25 +85,26 @@ public void setUp() throws Exception {
 		emptyTree = db.resolve("HEAD^{tree}").abbreviate(12).name();
 	}
 
-	@Ignore("Some versions of java.util.zip refuse to write an empty ZIP")
 	@Test
 	public void testEmptyArchive() throws Exception {
-		byte[] result = CLIGitCommand.rawExecute(
-				"git archive --format=zip " + emptyTree, db);
+		byte[] result = CLIGitCommand.executeRaw(
+				"git archive --format=zip " + emptyTree, db).outBytes();
 		assertArrayEquals(new String[0], listZipEntries(result));
 	}
 
 	@Test
 	public void testEmptyTar() throws Exception {
-		byte[] result = CLIGitCommand.rawExecute(
-				"git archive --format=tar " + emptyTree, db);
+		byte[] result = CLIGitCommand.executeRaw(
+				"git archive --format=tar " + emptyTree, db).outBytes();
 		assertArrayEquals(new String[0], listTarEntries(result));
 	}
 
 	@Test
 	public void testUnrecognizedFormat() throws Exception {
-		String[] expect = new String[] { "fatal: Unknown archive format 'nonsense'" };
-		String[] actual = execute("git archive --format=nonsense " + emptyTree);
+		String[] expect = new String[] {
+				"fatal: Unknown archive format 'nonsense'", "" };
+		String[] actual = executeUnchecked(
+				"git archive --format=nonsense " + emptyTree);
 		assertArrayEquals(expect, actual);
 	}
 
@@ -120,8 +117,8 @@ public void testArchiveWithFiles() throws Exception {
 		git.add().addFilepattern("c").call();
 		git.commit().setMessage("populate toplevel").call();
 
-		byte[] result = CLIGitCommand.rawExecute(
-				"git archive --format=zip HEAD", db);
+		byte[] result = CLIGitCommand.executeRaw(
+				"git archive --format=zip HEAD", db).outBytes();
 		assertArrayEquals(new String[] { "a", "c" },
 				listZipEntries(result));
 	}
@@ -135,8 +132,8 @@ private void commitGreeting() throws Exception {
 	@Test
 	public void testDefaultFormatIsTar() throws Exception {
 		commitGreeting();
-		byte[] result = CLIGitCommand.rawExecute(
-				"git archive HEAD", db);
+		byte[] result = CLIGitCommand.executeRaw(
+				"git archive HEAD", db).outBytes();
 		assertArrayEquals(new String[] { "greeting" },
 				listTarEntries(result));
 	}
@@ -302,8 +299,8 @@ public void testArchiveWithSubdir() throws Exception {
 		git.add().addFilepattern("b").call();
 		git.commit().setMessage("add subdir").call();
 
-		byte[] result = CLIGitCommand.rawExecute(
-				"git archive --format=zip master", db);
+		byte[] result = CLIGitCommand.executeRaw(
+				"git archive --format=zip master", db).outBytes();
 		String[] expect = { "a", "b.c", "b0c", "b/", "b/a", "b/b", "c" };
 		String[] actual = listZipEntries(result);
 
@@ -328,8 +325,8 @@ public void testTarWithSubdir() throws Exception {
 		git.add().addFilepattern("b").call();
 		git.commit().setMessage("add subdir").call();
 
-		byte[] result = CLIGitCommand.rawExecute(
-				"git archive --format=tar master", db);
+		byte[] result = CLIGitCommand.executeRaw(
+				"git archive --format=tar master", db).outBytes();
 		String[] expect = { "a", "b.c", "b0c", "b/", "b/a", "b/b", "c" };
 		String[] actual = listTarEntries(result);
 
@@ -349,8 +346,8 @@ private void commitBazAndFooSlashBar() throws Exception {
 	@Test
 	public void testArchivePrefixOption() throws Exception {
 		commitBazAndFooSlashBar();
-		byte[] result = CLIGitCommand.rawExecute(
-				"git archive --prefix=x/ --format=zip master", db);
+		byte[] result = CLIGitCommand.executeRaw(
+				"git archive --prefix=x/ --format=zip master", db).outBytes();
 		String[] expect = { "x/baz", "x/foo/", "x/foo/bar" };
 		String[] actual = listZipEntries(result);
 
@@ -362,8 +359,8 @@ public void testArchivePrefixOption() throws Exception {
 	@Test
 	public void testTarPrefixOption() throws Exception {
 		commitBazAndFooSlashBar();
-		byte[] result = CLIGitCommand.rawExecute(
-				"git archive --prefix=x/ --format=tar master", db);
+		byte[] result = CLIGitCommand.executeRaw(
+				"git archive --prefix=x/ --format=tar master", db).outBytes();
 		String[] expect = { "x/baz", "x/foo/", "x/foo/bar" };
 		String[] actual = listTarEntries(result);
 
@@ -381,8 +378,8 @@ private void commitFoo() throws Exception {
 	@Test
 	public void testPrefixDoesNotNormalizeDoubleSlash() throws Exception {
 		commitFoo();
-		byte[] result = CLIGitCommand.rawExecute(
-				"git archive --prefix=x// --format=zip master", db);
+		byte[] result = CLIGitCommand.executeRaw(
+				"git archive --prefix=x// --format=zip master", db).outBytes();
 		String[] expect = { "x//foo" };
 		assertArrayEquals(expect, listZipEntries(result));
 	}
@@ -390,8 +387,8 @@ public void testPrefixDoesNotNormalizeDoubleSlash() throws Exception {
 	@Test
 	public void testPrefixDoesNotNormalizeDoubleSlashInTar() throws Exception {
 		commitFoo();
-		byte[] result = CLIGitCommand.rawExecute(
-				"git archive --prefix=x// --format=tar master", db);
+		byte[] result = CLIGitCommand.executeRaw(
+				"git archive --prefix=x// --format=tar master", db).outBytes();
 		String[] expect = { "x//foo" };
 		assertArrayEquals(expect, listTarEntries(result));
 	}
@@ -408,8 +405,8 @@ public void testPrefixDoesNotNormalizeDoubleSlashInTar() throws Exception {
 	@Test
 	public void testPrefixWithoutTrailingSlash() throws Exception {
 		commitBazAndFooSlashBar();
-		byte[] result = CLIGitCommand.rawExecute(
-				"git archive --prefix=my- --format=zip master", db);
+		byte[] result = CLIGitCommand.executeRaw(
+				"git archive --prefix=my- --format=zip master", db).outBytes();
 		String[] expect = { "my-baz", "my-foo/", "my-foo/bar" };
 		String[] actual = listZipEntries(result);
 
@@ -421,8 +418,8 @@ public void testPrefixWithoutTrailingSlash() throws Exception {
 	@Test
 	public void testTarPrefixWithoutTrailingSlash() throws Exception {
 		commitBazAndFooSlashBar();
-		byte[] result = CLIGitCommand.rawExecute(
-				"git archive --prefix=my- --format=tar master", db);
+		byte[] result = CLIGitCommand.executeRaw(
+				"git archive --prefix=my- --format=tar master", db).outBytes();
 		String[] expect = { "my-baz", "my-foo/", "my-foo/bar" };
 		String[] actual = listTarEntries(result);
 
@@ -441,8 +438,8 @@ public void testArchiveIncludesSubmoduleDirectory() throws Exception {
 		git.submoduleAdd().setURI("./.").setPath("b").call().close();
 		git.commit().setMessage("add submodule").call();
 
-		byte[] result = CLIGitCommand.rawExecute(
-				"git archive --format=zip master", db);
+		byte[] result = CLIGitCommand.executeRaw(
+				"git archive --format=zip master", db).outBytes();
 		String[] expect = { ".gitmodules", "a", "b/", "c" };
 		String[] actual = listZipEntries(result);
 
@@ -461,8 +458,8 @@ public void testTarIncludesSubmoduleDirectory() throws Exception {
 		git.submoduleAdd().setURI("./.").setPath("b").call().close();
 		git.commit().setMessage("add submodule").call();
 
-		byte[] result = CLIGitCommand.rawExecute(
-				"git archive --format=tar master", db);
+		byte[] result = CLIGitCommand.executeRaw(
+				"git archive --format=tar master", db).outBytes();
 		String[] expect = { ".gitmodules", "a", "b/", "c" };
 		String[] actual = listTarEntries(result);
 
@@ -491,8 +488,8 @@ public void testArchivePreservesMode() throws Exception {
 
 		git.commit().setMessage("three files with different modes").call();
 
-		byte[] zipData = CLIGitCommand.rawExecute(
-				"git archive --format=zip master", db);
+		byte[] zipData = CLIGitCommand.executeRaw(
+				"git archive --format=zip master", db).outBytes();
 		writeRaw("zip-with-modes.zip", zipData);
 		assertContainsEntryWithMode("zip-with-modes.zip", "-rw-", "plain");
 		assertContainsEntryWithMode("zip-with-modes.zip", "-rwx", "executable");
@@ -520,8 +517,8 @@ public void testTarPreservesMode() throws Exception {
 
 		git.commit().setMessage("three files with different modes").call();
 
-		byte[] archive = CLIGitCommand.rawExecute(
-				"git archive --format=tar master", db);
+		byte[] archive = CLIGitCommand.executeRaw(
+				"git archive --format=tar master", db).outBytes();
 		writeRaw("with-modes.tar", archive);
 		assertTarContainsEntry("with-modes.tar", "-rw-r--r--", "plain");
 		assertTarContainsEntry("with-modes.tar", "-rwxr-xr-x", "executable");
@@ -543,8 +540,8 @@ public void testArchiveWithLongFilename() throws Exception {
 		git.add().addFilepattern("1234567890").call();
 		git.commit().setMessage("file with long name").call();
 
-		byte[] result = CLIGitCommand.rawExecute(
-				"git archive --format=zip HEAD", db);
+		byte[] result = CLIGitCommand.executeRaw(
+				"git archive --format=zip HEAD", db).outBytes();
 		assertArrayEquals(l.toArray(new String[l.size()]),
 				listZipEntries(result));
 	}
@@ -563,8 +560,8 @@ public void testTarWithLongFilename() throws Exception {
 		git.add().addFilepattern("1234567890").call();
 		git.commit().setMessage("file with long name").call();
 
-		byte[] result = CLIGitCommand.rawExecute(
-				"git archive --format=tar HEAD", db);
+		byte[] result = CLIGitCommand.executeRaw(
+				"git archive --format=tar HEAD", db).outBytes();
 		assertArrayEquals(l.toArray(new String[l.size()]),
 				listTarEntries(result));
 	}
@@ -576,8 +573,8 @@ public void testArchivePreservesContent() throws Exception {
 		git.add().addFilepattern("xyzzy").call();
 		git.commit().setMessage("add file with content").call();
 
-		byte[] result = CLIGitCommand.rawExecute(
-				"git archive --format=zip HEAD", db);
+		byte[] result = CLIGitCommand.executeRaw(
+				"git archive --format=zip HEAD", db).outBytes();
 		assertArrayEquals(new String[] { payload },
 				zipEntryContent(result, "xyzzy"));
 	}
@@ -589,8 +586,8 @@ public void testTarPreservesContent() throws Exception {
 		git.add().addFilepattern("xyzzy").call();
 		git.commit().setMessage("add file with content").call();
 
-		byte[] result = CLIGitCommand.rawExecute(
-				"git archive --format=tar HEAD", db);
+		byte[] result = CLIGitCommand.executeRaw(
+				"git archive --format=tar HEAD", db).outBytes();
 		assertArrayEquals(new String[] { payload },
 				tarEntryContent(result, "xyzzy"));
 	}
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 d1bd5ba..55f4d8b 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,11 +43,17 @@
 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;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.pgm.internal.CLIText;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Before;
 import org.junit.Test;
@@ -63,9 +69,19 @@ public void setUp() throws Exception {
 	}
 
 	@Test
+	public void testHelpAfterDelete() throws Exception {
+		String err = toString(executeUnchecked("git branch -d"));
+		String help = toString(executeUnchecked("git branch -h"));
+		String errAndHelp = toString(executeUnchecked("git branch -d -h"));
+		assertEquals(CLIText.fatalError(CLIText.get().branchNameRequired), err);
+		assertEquals(toString(err, help), errAndHelp);
+	}
+
+	@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
@@ -73,26 +89,188 @@ 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
 	public void testListContains() throws Exception {
 		try (Git git = new Git(db)) {
-				git.branchCreate().setName("initial").call();
+			git.branchCreate().setName("initial").call();
 			RevCommit second = git.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.",
-				execute("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/CheckoutTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CheckoutTest.java
index cb36d05..e690ad6 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CheckoutTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CheckoutTest.java
@@ -44,9 +44,14 @@
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 
 import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
 import java.util.List;
 
 import org.eclipse.jgit.api.Git;
@@ -59,7 +64,9 @@
 import org.eclipse.jgit.treewalk.FileTreeIterator;
 import org.eclipse.jgit.treewalk.FileTreeIterator.FileEntry;
 import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.FileUtils;
+import org.junit.Assume;
 import org.junit.Test;
 
 public class CheckoutTest extends CLIRepositoryTestCase {
@@ -109,14 +116,14 @@ public void testCheckoutNewBranchThatAlreadyExists() throws Exception {
 
 			assertStringArrayEquals(
 					"fatal: A branch named 'master' already exists.",
-					execute("git checkout -b master"));
+				executeUnchecked("git checkout -b master"));
 		}
 	}
 
 	@Test
 	public void testCheckoutNewBranchOnBranchToBeBorn() throws Exception {
 		assertStringArrayEquals("fatal: You are on a branch yet to be born",
-				execute("git checkout -b side"));
+				executeUnchecked("git checkout -b side"));
 	}
 
 	@Test
@@ -599,4 +606,34 @@ public void testCheckoutPath() throws Exception {
 			assertEquals("Hello world b", read(b));
 		}
 	}
+
+	@Test
+	public void testCheckouSingleFile() throws Exception {
+		try (Git git = new Git(db)) {
+			File a = writeTrashFile("a", "file a");
+			git.add().addFilepattern(".").call();
+			git.commit().setMessage("commit file a").call();
+			writeTrashFile("a", "b");
+			assertEquals("b", read(a));
+			assertEquals("[]", Arrays.toString(execute("git checkout -- a")));
+			assertEquals("file a", read(a));
+		}
+	}
+
+	@Test
+	public void testCheckoutLink() throws Exception {
+		Assume.assumeTrue(FS.DETECTED.supportsSymlinks());
+		try (Git git = new Git(db)) {
+			Path path = writeLink("a", "link_a");
+			assertTrue(Files.isSymbolicLink(path));
+			git.add().addFilepattern(".").call();
+			git.commit().setMessage("commit link a").call();
+			deleteTrashFile("a");
+			writeTrashFile("a", "Hello world a");
+			assertFalse(Files.isSymbolicLink(path));
+			assertEquals("[]", Arrays.toString(execute("git checkout -- a")));
+			assertEquals("link_a", FileUtils.readSymLink(path.toFile()));
+			assertTrue(Files.isSymbolicLink(path));
+		}
+	}
 }
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
new file mode 100644
index 0000000..6bccb6d
--- /dev/null
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CommitTest.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2015, Andrey Loskutov <loskutov@gmx.de>
+ * 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.pgm;
+
+import static org.junit.Assert.assertEquals;
+
+import org.eclipse.jgit.lib.CLIRepositoryTestCase;
+import org.junit.Test;
+
+public class CommitTest extends CLIRepositoryTestCase {
+
+	@Test
+	public void testCommitPath() throws Exception {
+		writeTrashFile("a", "a");
+		writeTrashFile("b", "a");
+		String result = toString(execute("git add a"));
+		assertEquals("", result);
+
+		result = toString(execute("git status -- a"));
+		assertEquals(toString("On branch master", "Changes to be committed:",
+				"new file:   a"), result);
+
+		result = toString(execute("git status -- b"));
+		assertEquals(toString("On branch master", "Untracked files:", "b"),
+				result);
+
+		result = toString(execute("git commit a -m 'added a'"));
+		assertEquals(
+				"[master 8cb3ef7e5171aaee1792df6302a5a0cd30425f7a] added a",
+				result);
+
+		result = toString(execute("git status -- a"));
+		assertEquals("On branch master", result);
+
+		result = toString(execute("git status -- b"));
+		assertEquals(toString("On branch master", "Untracked files:", "b"),
+				result);
+	}
+
+	@Test
+	public void testCommitAll() throws Exception {
+		writeTrashFile("a", "a");
+		writeTrashFile("b", "a");
+		String result = toString(execute("git add a b"));
+		assertEquals("", result);
+
+		result = toString(execute("git status -- a 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'"));
+		assertEquals(
+				"[master 3c93fa8e3a28ee26690498be78016edcb3a38c73] added a b",
+				result);
+
+		result = toString(execute("git status -- a b"));
+		assertEquals("On branch master", result);
+	}
+
+}
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DescribeTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DescribeTest.java
index 6352a26..086e72e 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DescribeTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/DescribeTest.java
@@ -43,9 +43,15 @@
 package org.eclipse.jgit.pgm;
 
 import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Arrays;
 
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.lib.CLIRepositoryTestCase;
+import org.eclipse.jgit.pgm.internal.CLIText;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -67,17 +73,15 @@ private void initialCommitAndTag() throws Exception {
 
 	@Test
 	public void testNoHead() throws Exception {
-		assertArrayEquals(
-				new String[] { "fatal: No names found, cannot describe anything." },
-				execute("git describe"));
+		assertEquals(CLIText.fatalError(CLIText.get().noNamesFound),
+				toString(executeUnchecked("git describe")));
 	}
 
 	@Test
 	public void testHeadNoTag() throws Exception {
 		git.commit().setMessage("initial commit").call();
-		assertArrayEquals(
-				new String[] { "fatal: No names found, cannot describe anything." },
-				execute("git describe"));
+		assertEquals(CLIText.fatalError(CLIText.get().noNamesFound),
+				toString(executeUnchecked("git describe")));
 	}
 
 	@Test
@@ -103,4 +107,22 @@ public void testDescribeTagLong() throws Exception {
 		assertArrayEquals(new String[] { "v1.0-0-g6fd41be", "" },
 				execute("git describe --long HEAD"));
 	}
+
+	@Test
+	public void testHelpArgumentBeforeUnknown() throws Exception {
+		String[] output = execute("git describe -h -XYZ");
+		String all = Arrays.toString(output);
+		assertTrue("Unexpected help output: " + all,
+				all.contains("jgit describe"));
+		assertFalse("Unexpected help output: " + all, all.contains("fatal"));
+	}
+
+	@Test
+	public void testHelpArgumentAfterUnknown() throws Exception {
+		String[] output = executeUnchecked("git describe -XYZ -h");
+		String all = Arrays.toString(output);
+		assertTrue("Unexpected help output: " + all,
+				all.contains("jgit describe"));
+		assertTrue("Unexpected help output: " + all, all.contains("fatal"));
+	}
 }
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeTest.java
index 975e8c4..4719901 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/MergeTest.java
@@ -50,6 +50,7 @@
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.lib.CLIRepositoryTestCase;
 import org.eclipse.jgit.merge.MergeStrategy;
+import org.eclipse.jgit.pgm.internal.CLIText;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Before;
 import org.junit.Test;
@@ -194,8 +195,9 @@ public void testNoFastForward() throws Exception {
 
 	@Test
 	public void testNoFastForwardAndSquash() throws Exception {
-		assertEquals("fatal: You cannot combine --squash with --no-ff.",
-				execute("git merge master --no-ff --squash")[0]);
+		assertEquals(
+				CLIText.fatalError(CLIText.get().cannotCombineSquashWithNoff),
+				executeUnchecked("git merge master --no-ff --squash")[0]);
 	}
 
 	@Test
@@ -209,8 +211,8 @@ public void testFastForwardOnly() throws Exception {
 		git.add().addFilepattern("file").call();
 		git.commit().setMessage("commit#2").call();
 
-		assertEquals("fatal: Not possible to fast-forward, aborting.",
-				execute("git merge master --ff-only")[0]);
+		assertEquals(CLIText.fatalError(CLIText.get().ffNotPossibleAborting),
+				executeUnchecked("git merge master --ff-only")[0]);
 	}
 
 	@Test
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 90efae2..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
@@ -44,8 +44,11 @@
 
 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;
+
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.junit.JGitTestUtil;
 import org.eclipse.jgit.lib.CLIRepositoryTestCase;
@@ -98,6 +101,31 @@ public void setUp() throws Exception {
 	}
 
 	@Test
+	public void testMissingPath() throws Exception {
+		try {
+			execute("git repo");
+			fail("Must die");
+		} catch (Die e) {
+			// expected, requires argument
+		}
+	}
+
+	/**
+	 * See bug 484951: "git repo -h" should not print unexpected values
+	 *
+	 * @throws Exception
+	 */
+	@Test
+	public void testZombieHelpArgument() throws Exception {
+		String[] output = execute("git repo -h");
+		String all = Arrays.toString(output);
+		assertTrue("Unexpected help output: " + all,
+				all.contains("jgit repo"));
+		assertFalse("Unexpected help output: " + all,
+				all.contains("jgit repo VAL"));
+	}
+
+	@Test
 	public void testAddRepoManifest() throws Exception {
 		StringBuilder xmlContent = new StringBuilder();
 		xmlContent.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ResetTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ResetTest.java
index dae4779..16c5889 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ResetTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ResetTest.java
@@ -48,6 +48,7 @@
 import org.eclipse.jgit.lib.CLIRepositoryTestCase;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 
 public class ResetTest extends CLIRepositoryTestCase {
@@ -62,6 +63,20 @@ public void setUp() throws Exception {
 	}
 
 	@Test
+	public void testPathOptionHelp() throws Exception {
+		String[] result = execute("git reset -h");
+		assertTrue("Unexpected argument: " + result[1],
+				result[1].endsWith("[-- path ... ...]"));
+	}
+
+	@Test
+	public void testZombieArgument_Bug484951() throws Exception {
+		String[] result = execute("git reset -h");
+		assertFalse("Unexpected argument: " + result[0],
+				result[0].contains("[VAL ...]"));
+	}
+
+	@Test
 	public void testResetSelf() throws Exception {
 		RevCommit commit = git.commit().setMessage("initial commit").call();
 		assertStringArrayEquals("",
@@ -91,15 +106,28 @@ public void testResetEmptyPath() throws Exception {
 
 	@Test
 	public void testResetPathDoubleDash() throws Exception {
-		resetPath(true);
+		resetPath(true, true);
 	}
 
 	@Test
 	public void testResetPathNoDoubleDash() throws Exception {
-		resetPath(false);
+		resetPath(false, true);
 	}
 
-	private void resetPath(boolean useDoubleDash) throws Exception {
+	@Test
+	public void testResetPathDoubleDashNoRef() throws Exception {
+		resetPath(true, false);
+	}
+
+	@Ignore("Currently we cannote recognize if a name is a commit-ish or a path, "
+			+ "so 'git reset a' will not work if 'a' is not a branch name but a file path")
+	@Test
+	public void testResetPathNoDoubleDashNoRef() throws Exception {
+		resetPath(false, false);
+	}
+
+	private void resetPath(boolean useDoubleDash, boolean supplyCommit)
+			throws Exception {
 		// create files a and b
 		writeTrashFile("a", "Hello world a");
 		writeTrashFile("b", "Hello world b");
@@ -115,8 +143,9 @@ private void resetPath(boolean useDoubleDash) throws Exception {
 		git.add().addFilepattern(".").call();
 
 		// reset only file a
-		String cmd = String.format("git reset %s%s a", commit.getId().name(),
-				(useDoubleDash) ? " --" : "");
+		String cmd = String.format("git reset %s%s a",
+				supplyCommit ? commit.getId().name() : "",
+				useDoubleDash ? " --" : "");
 		assertStringArrayEquals("", execute(cmd));
 		assertEquals(commit.getId(),
 				git.getRepository().exactRef("HEAD").getObjectId());
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/StatusTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/StatusTest.java
index 854c52d..368047c 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/StatusTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/StatusTest.java
@@ -44,6 +44,7 @@
 
 import static org.eclipse.jgit.lib.Constants.MASTER;
 import static org.eclipse.jgit.lib.Constants.R_HEADS;
+import static org.junit.Assert.assertTrue;
 
 import java.io.IOException;
 
@@ -56,6 +57,13 @@
 public class StatusTest extends CLIRepositoryTestCase {
 
 	@Test
+	public void testPathOptionHelp() throws Exception {
+		String[] result = execute("git status -h");
+		assertTrue("Unexpected argument: " + result[1],
+				result[1].endsWith("[-- path ... ...]"));
+	}
+
+	@Test
 	public void testStatusDefault() throws Exception {
 		executeTest("git status", false, true);
 	}
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/TagTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/TagTest.java
index ab09db5..0fe25f5 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/TagTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/TagTest.java
@@ -68,6 +68,6 @@ public void testTagTwice() throws Exception {
 		git.commit().setMessage("commit").call();
 
 		assertEquals("fatal: tag 'test' already exists",
-				execute("git tag test")[0]);
+				executeUnchecked("git tag test")[0]);
 	}
 }
diff --git a/org.eclipse.jgit.pgm/BUCK b/org.eclipse.jgit.pgm/BUCK
new file mode 100644
index 0000000..edcf2fc
--- /dev/null
+++ b/org.eclipse.jgit.pgm/BUCK
@@ -0,0 +1,70 @@
+include_defs('//tools/git.defs')
+
+java_library(
+  name = 'pgm',
+  srcs = glob(['src/**']),
+  resources = glob(['resources/**']),
+  deps = [
+    ':services',
+    '//org.eclipse.jgit:jgit',
+    '//org.eclipse.jgit.archive:jgit-archive',
+    '//org.eclipse.jgit.http.apache:http-apache',
+    '//org.eclipse.jgit.ui:ui',
+    '//lib:args4j',
+  ],
+  visibility = ['PUBLIC'],
+)
+
+prebuilt_jar(
+  name = 'services',
+  binary_jar = ':services__jar',
+)
+
+genrule(
+  name = 'services__jar',
+  cmd = 'cd $SRCDIR ; zip -qr $OUT .',
+  srcs = glob(['META-INF/services/*']),
+  out = 'services.jar',
+)
+
+genrule(
+  name = 'jgit',
+  cmd = ''.join([
+    'mkdir $TMP/META-INF &&',
+    'cp $(location :binary_manifest) $TMP/META-INF/MANIFEST.MF &&',
+    'cp $(location :jgit_jar) $TMP/jgit.jar &&',
+    'cd $TMP && zip $TMP/jgit.jar META-INF/MANIFEST.MF &&',
+    'cat $SRCDIR/jgit.sh $TMP/jgit.jar >$OUT &&',
+    'chmod a+x $OUT',
+  ]),
+  srcs = ['jgit.sh'],
+  out = 'jgit',
+  visibility = ['PUBLIC'],
+)
+
+java_binary(
+  name = 'jgit_jar',
+  deps = [
+    ':pgm',
+    '//lib:slf4j-simple',
+    '//lib:tukaani-xz',
+  ],
+  blacklist = [
+    'META-INF/DEPENDENCIES',
+    'META-INF/maven/.*',
+  ],
+)
+
+genrule(
+  name = 'binary_manifest',
+  cmd = ';'.join(['echo "%s: %s" >>$OUT' % e for e in [
+    ('Manifest-Version', '1.0'),
+    ('Main-Class', 'org.eclipse.jgit.pgm.Main'),
+    ('Bundle-Version', git_version()),
+    ('Implementation-Title', 'JGit Command Line Interface'),
+    ('Implementation-Vendor', 'Eclipse.org - JGit'),
+    ('Implementation-Vendor-URL', 'http://www.eclipse.org/jgit/'),
+    ('Implementation-Vendor-Id', 'org.eclipse.jgit'),
+  ]] + ['echo >>$OUT']),
+  out = 'MANIFEST.MF',
+)
diff --git a/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF b/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF
index 567fd05..bc9205c 100644
--- a/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF
@@ -19,8 +19,10 @@
  org.eclipse.jgit.dircache;version="[4.2.0,4.3.0)",
  org.eclipse.jgit.errors;version="[4.2.0,4.3.0)",
  org.eclipse.jgit.gitrepo;version="[4.2.0,4.3.0)",
+ org.eclipse.jgit.internal.ketch;version="[4.2.0,4.3.0)",
  org.eclipse.jgit.internal.storage.file;version="[4.2.0,4.3.0)",
  org.eclipse.jgit.internal.storage.pack;version="[4.2.0,4.3.0)",
+ org.eclipse.jgit.internal.storage.reftree;version="[4.2.0,4.3.0)",
  org.eclipse.jgit.lib;version="[4.2.0,4.3.0)",
  org.eclipse.jgit.merge;version="4.2.0",
  org.eclipse.jgit.nls;version="[4.2.0,4.3.0)",
@@ -31,6 +33,7 @@
  org.eclipse.jgit.storage.file;version="[4.2.0,4.3.0)",
  org.eclipse.jgit.storage.pack;version="[4.2.0,4.3.0)",
  org.eclipse.jgit.transport;version="[4.2.0,4.3.0)",
+ org.eclipse.jgit.transport.http.apache;version="[4.2.0,4.3.0)",
  org.eclipse.jgit.transport.resolver;version="[4.2.0,4.3.0)",
  org.eclipse.jgit.treewalk;version="[4.2.0,4.3.0)",
  org.eclipse.jgit.treewalk.filter;version="[4.2.0,4.3.0)",
diff --git a/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin b/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin
index c13f63e..6aa2004 100644
--- a/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin
+++ b/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin
@@ -41,6 +41,7 @@
 org.eclipse.jgit.pgm.debug.MakeCacheTree
 org.eclipse.jgit.pgm.debug.ReadDirCache
 org.eclipse.jgit.pgm.debug.RebuildCommitGraph
+org.eclipse.jgit.pgm.debug.RebuildRefTree
 org.eclipse.jgit.pgm.debug.ShowCacheTree
 org.eclipse.jgit.pgm.debug.ShowCommands
 org.eclipse.jgit.pgm.debug.ShowDirCache
diff --git a/org.eclipse.jgit.pgm/pom.xml b/org.eclipse.jgit.pgm/pom.xml
index ca2ead2..2642491 100644
--- a/org.eclipse.jgit.pgm/pom.xml
+++ b/org.eclipse.jgit.pgm/pom.xml
@@ -95,6 +95,17 @@
     </dependency>
 
     <dependency>
+      <groupId>org.eclipse.jgit</groupId>
+      <artifactId>org.eclipse.jgit.http.apache</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.httpcomponents</groupId>
+      <artifactId>httpclient</artifactId>
+    </dependency>
+
+    <dependency>
       <groupId>org.slf4j</groupId>
       <artifactId>slf4j-api</artifactId>
       <version>${slf4j-version}</version>
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 335336d..b4b1261 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
@@ -223,6 +228,7 @@
 usage_MergesTwoDevelopmentHistories=Merges two development histories
 usage_ReadDirCache= Read the DirCache 100 times
 usage_RebuildCommitGraph=Recreate a repository from another one's commit graph
+usage_RebuildRefTree=Copy references into a RefTree
 usage_Remote=Manage set of tracked repositories
 usage_RepositoryToReadFrom=Repository to read from
 usage_RepositoryToReceiveInto=Repository to receive into
@@ -337,6 +343,7 @@
 usage_recurseIntoSubtrees=recurse into subtrees
 usage_renameLimit=limit size of rename matrix
 usage_reset=Reset current HEAD to the specified state
+usage_resetReference=Reset to given reference name
 usage_resetHard=Resets the index and working tree
 usage_resetSoft=Resets without touching the index file nor the working tree
 usage_resetMixed=Resets the index but not the working tree
@@ -353,6 +360,7 @@
 usage_notags=do not fetch tags
 usage_tagMessage=tag message
 usage_untrackedFilesMode=show untracked files
+usage_updateRef=reference to update
 usage_updateRemoteRefsFromAnotherRepository=Update remote refs from another repository
 usage_useNameInsteadOfOriginToTrackUpstream=use <name> instead of 'origin' to track upstream
 usage_checkoutBranchAfterClone=checkout named branch instead of remotes's HEAD
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/Checkout.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Checkout.java
index 4579462..94517db 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Checkout.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Checkout.java
@@ -60,7 +60,7 @@
 import org.eclipse.jgit.pgm.internal.CLIText;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
-import org.kohsuke.args4j.spi.StopOptionHandler;
+import org.kohsuke.args4j.spi.RestOfArgumentsHandler;
 
 @Command(common = true, usage = "usage_checkout")
 class Checkout extends TextBuiltin {
@@ -74,11 +74,10 @@ class Checkout extends TextBuiltin {
 	@Option(name = "--orphan", usage = "usage_orphan")
 	private boolean orphan = false;
 
-	@Argument(required = true, index = 0, metaVar = "metaVar_name", usage = "usage_checkout")
+	@Argument(required = false, index = 0, metaVar = "metaVar_name", usage = "usage_checkout")
 	private String name;
 
-	@Argument(index = 1)
-	@Option(name = "--", metaVar = "metaVar_paths", multiValued = true, handler = StopOptionHandler.class)
+	@Option(name = "--", metaVar = "metaVar_paths", multiValued = true, handler = RestOfArgumentsHandler.class)
 	private List<String> paths = new ArrayList<String>();
 
 	@Override
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Clone.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Clone.java
index cd6953c..0407828 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Clone.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Clone.java
@@ -50,6 +50,7 @@
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.errors.InvalidRemoteException;
 import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.TextProgressMonitor;
 import org.eclipse.jgit.pgm.internal.CLIText;
 import org.eclipse.jgit.transport.URIish;
 import org.eclipse.jgit.util.SystemReader;
@@ -70,6 +71,9 @@ class Clone extends AbstractFetchCommand {
 	@Option(name = "--bare", usage = "usage_bareClone")
 	private boolean isBare;
 
+	@Option(name = "--quiet", usage = "usage_quiet")
+	private Boolean quiet;
+
 	@Argument(index = 0, required = true, metaVar = "metaVar_uriish")
 	private String sourceUri;
 
@@ -109,10 +113,16 @@ protected void run() throws Exception {
 
 		command.setGitDir(gitdir == null ? null : new File(gitdir));
 		command.setDirectory(localNameF);
-		outw.println(MessageFormat.format(CLIText.get().cloningInto, localName));
+		boolean msgs = quiet == null || !quiet.booleanValue();
+		if (msgs) {
+			command.setProgressMonitor(new TextProgressMonitor(errw));
+			outw.println(MessageFormat.format(
+					CLIText.get().cloningInto, localName));
+			outw.flush();
+		}
 		try {
 			db = command.call().getRepository();
-			if (db.resolve(Constants.HEAD) == null)
+			if (msgs && db.resolve(Constants.HEAD) == null)
 				outw.println(CLIText.get().clonedEmptyRepository);
 		} catch (InvalidRemoteException e) {
 			throw die(MessageFormat.format(CLIText.get().doesNotExist,
@@ -121,8 +131,9 @@ protected void run() throws Exception {
 			if (db != null)
 				db.close();
 		}
-
-		outw.println();
-		outw.flush();
+		if (msgs) {
+			outw.println();
+			outw.flush();
+		}
 	}
 }
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Daemon.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Daemon.java
index 04182d6..03f3fac 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Daemon.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Daemon.java
@@ -45,18 +45,29 @@
 
 import java.io.File;
 import java.net.InetSocketAddress;
+import java.net.URISyntaxException;
 import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.Executors;
 
+import org.eclipse.jgit.internal.ketch.KetchLeader;
+import org.eclipse.jgit.internal.ketch.KetchLeaderCache;
+import org.eclipse.jgit.internal.ketch.KetchPreReceive;
+import org.eclipse.jgit.internal.ketch.KetchSystem;
+import org.eclipse.jgit.internal.ketch.KetchText;
+import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.pgm.internal.CLIText;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.eclipse.jgit.storage.file.WindowCacheConfig;
 import org.eclipse.jgit.storage.pack.PackConfig;
 import org.eclipse.jgit.transport.DaemonClient;
 import org.eclipse.jgit.transport.DaemonService;
+import org.eclipse.jgit.transport.ReceivePack;
 import org.eclipse.jgit.transport.resolver.FileResolver;
+import org.eclipse.jgit.transport.resolver.ReceivePackFactory;
+import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException;
+import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException;
 import org.eclipse.jgit.util.FS;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
@@ -90,6 +101,13 @@ class Daemon extends TextBuiltin {
 	@Option(name = "--export-all", usage = "usage_exportWithoutGitDaemonExportOk")
 	boolean exportAll;
 
+	@Option(name = "--ketch")
+	KetchServerType ketchServerType;
+
+	enum KetchServerType {
+		LEADER;
+	}
+
 	@Argument(required = true, metaVar = "metaVar_directory", usage = "usage_directoriesToExport")
 	final List<File> directory = new ArrayList<File>();
 
@@ -146,7 +164,9 @@ protected void run() throws Exception {
 			service(d, n).setOverridable(true);
 		for (final String n : forbidOverride)
 			service(d, n).setOverridable(false);
-
+		if (ketchServerType == KetchServerType.LEADER) {
+			startKetchLeader(d);
+		}
 		d.start();
 		outw.println(MessageFormat.format(CLIText.get().listeningOn, d.getAddress()));
 	}
@@ -159,4 +179,29 @@ private static DaemonService service(
 			throw die(MessageFormat.format(CLIText.get().serviceNotSupported, n));
 		return svc;
 	}
+
+	private void startKetchLeader(org.eclipse.jgit.transport.Daemon daemon) {
+		KetchSystem system = new KetchSystem();
+		final KetchLeaderCache leaders = new KetchLeaderCache(system);
+		final ReceivePackFactory<DaemonClient> factory;
+
+		factory = daemon.getReceivePackFactory();
+		daemon.setReceivePackFactory(new ReceivePackFactory<DaemonClient>() {
+			@Override
+			public ReceivePack create(DaemonClient req, Repository repo)
+					throws ServiceNotEnabledException,
+					ServiceNotAuthorizedException {
+				ReceivePack rp = factory.create(req, repo);
+				KetchLeader leader;
+				try {
+					leader = leaders.get(repo);
+				} catch (URISyntaxException err) {
+					throw new ServiceNotEnabledException(
+							KetchText.get().invalidFollowerUri, err);
+				}
+				rp.setPreReceiveHook(new KetchPreReceive(leader));
+				return rp;
+			}
+		});
+	}
 }
\ No newline at end of file
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Die.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Die.java
index f07df1a..a25f1e9 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Die.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Die.java
@@ -86,6 +86,21 @@ public Die(final String why, final Throwable cause) {
 	 * @since 3.4
 	 */
 	public Die(boolean aborted) {
+		this(aborted, null);
+	}
+
+	/**
+	 * Construct a new exception reflecting the fact that the command execution
+	 * has been aborted before running.
+	 *
+	 * @param aborted
+	 *            boolean indicating the fact the execution has been aborted
+	 * @param cause
+	 *            can be null
+	 * @since 4.2
+	 */
+	public Die(boolean aborted, final Throwable cause) {
+		super(cause != null ? cause.getMessage() : null, cause);
 		this.aborted = aborted;
 	}
 
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Main.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Main.java
index ceb0d6b..d701f22 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Main.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Main.java
@@ -62,6 +62,8 @@
 import org.eclipse.jgit.pgm.internal.CLIText;
 import org.eclipse.jgit.pgm.opt.CmdLineParser;
 import org.eclipse.jgit.pgm.opt.SubcommandHandler;
+import org.eclipse.jgit.transport.HttpTransport;
+import org.eclipse.jgit.transport.http.apache.HttpClientConnectionFactory;
 import org.eclipse.jgit.util.CachedAuthenticator;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.CmdLineException;
@@ -88,13 +90,23 @@ public class Main {
 	@Argument(index = 1, metaVar = "metaVar_arg")
 	private List<String> arguments = new ArrayList<String>();
 
+	PrintWriter writer;
+
+	/**
+	 *
+	 */
+	public Main() {
+		HttpTransport.setConnectionFactory(new HttpClientConnectionFactory());
+	}
+
 	/**
 	 * Execute the command line.
 	 *
 	 * @param argv
 	 *            arguments.
+	 * @throws Exception
 	 */
-	public static void main(final String[] argv) {
+	public static void main(final String[] argv) throws Exception {
 		new Main().run(argv);
 	}
 
@@ -113,8 +125,10 @@ public static void main(final String[] argv) {
 	 *
 	 * @param argv
 	 *            arguments.
+	 * @throws Exception
 	 */
-	protected void run(final String[] argv) {
+	protected void run(final String[] argv) throws Exception {
+		writer = createErrorWriter();
 		try {
 			if (!installConsole()) {
 				AwtAuthenticator.install();
@@ -123,12 +137,14 @@ protected void run(final String[] argv) {
 			configureHttpProxy();
 			execute(argv);
 		} catch (Die err) {
-			if (err.isAborted())
-				System.exit(1);
-			System.err.println(MessageFormat.format(CLIText.get().fatalError, err.getMessage()));
-			if (showStackTrace)
-				err.printStackTrace();
-			System.exit(128);
+			if (err.isAborted()) {
+				exit(1, err);
+			}
+			writer.println(CLIText.fatalError(err.getMessage()));
+			if (showStackTrace) {
+				err.printStackTrace(writer);
+			}
+			exit(128, err);
 		} catch (Exception err) {
 			// Try to detect errno == EPIPE and exit normally if that happens
 			// There may be issues with operating system versions and locale,
@@ -136,46 +152,54 @@ protected void run(final String[] argv) {
 			// under other circumstances.
 			if (err.getClass() == IOException.class) {
 				// Linux, OS X
-				if (err.getMessage().equals("Broken pipe")) //$NON-NLS-1$
-					System.exit(0);
+				if (err.getMessage().equals("Broken pipe")) { //$NON-NLS-1$
+					exit(0, err);
+				}
 				// Windows
-				if (err.getMessage().equals("The pipe is being closed")) //$NON-NLS-1$
-					System.exit(0);
+				if (err.getMessage().equals("The pipe is being closed")) { //$NON-NLS-1$
+					exit(0, err);
+				}
 			}
 			if (!showStackTrace && err.getCause() != null
-					&& err instanceof TransportException)
-				System.err.println(MessageFormat.format(CLIText.get().fatalError, err.getCause().getMessage()));
+					&& err instanceof TransportException) {
+				writer.println(CLIText.fatalError(err.getCause().getMessage()));
+			}
 
 			if (err.getClass().getName().startsWith("org.eclipse.jgit.errors.")) { //$NON-NLS-1$
-				System.err.println(MessageFormat.format(CLIText.get().fatalError, err.getMessage()));
-				if (showStackTrace)
+				writer.println(CLIText.fatalError(err.getMessage()));
+				if (showStackTrace) {
 					err.printStackTrace();
-				System.exit(128);
+				}
+				exit(128, err);
 			}
 			err.printStackTrace();
-			System.exit(1);
+			exit(1, err);
 		}
 		if (System.out.checkError()) {
-			System.err.println(CLIText.get().unknownIoErrorStdout);
-			System.exit(1);
+			writer.println(CLIText.get().unknownIoErrorStdout);
+			exit(1, null);
 		}
-		if (System.err.checkError()) {
+		if (writer.checkError()) {
 			// No idea how to present an error here, most likely disk full or
 			// broken pipe
-			System.exit(1);
+			exit(1, null);
 		}
 	}
 
+	PrintWriter createErrorWriter() {
+		return new PrintWriter(System.err);
+	}
+
 	private void execute(final String[] argv) throws Exception {
-		final CmdLineParser clp = new CmdLineParser(this);
-		PrintWriter writer = new PrintWriter(System.err);
+		final CmdLineParser clp = new SubcommandLineParser(this);
+
 		try {
 			clp.parseArgument(argv);
 		} catch (CmdLineException err) {
 			if (argv.length > 0 && !help && !version) {
-				writer.println(MessageFormat.format(CLIText.get().fatalError, err.getMessage()));
+				writer.println(CLIText.fatalError(err.getMessage()));
 				writer.flush();
-				System.exit(1);
+				exit(1, err);
 			}
 		}
 
@@ -191,22 +215,24 @@ private void execute(final String[] argv) throws Exception {
 				writer.println(CLIText.get().mostCommonlyUsedCommandsAre);
 				final CommandRef[] common = CommandCatalog.common();
 				int width = 0;
-				for (final CommandRef c : common)
+				for (final CommandRef c : common) {
 					width = Math.max(width, c.getName().length());
+				}
 				width += 2;
 
 				for (final CommandRef c : common) {
 					writer.print(' ');
 					writer.print(c.getName());
-					for (int i = c.getName().length(); i < width; i++)
+					for (int i = c.getName().length(); i < width; i++) {
 						writer.print(' ');
+					}
 					writer.print(CLIText.get().resourceBundle().getString(c.getUsage()));
 					writer.println();
 				}
 				writer.println();
 			}
 			writer.flush();
-			System.exit(1);
+			exit(1, null);
 		}
 
 		if (version) {
@@ -215,20 +241,38 @@ private void execute(final String[] argv) throws Exception {
 		}
 
 		final TextBuiltin cmd = subcommand;
-		if (cmd.requiresRepository())
-			cmd.init(openGitDir(gitdir), null);
-		else
-			cmd.init(null, gitdir);
+		init(cmd);
 		try {
 			cmd.execute(arguments.toArray(new String[arguments.size()]));
 		} finally {
-			if (cmd.outw != null)
+			if (cmd.outw != null) {
 				cmd.outw.flush();
-			if (cmd.errw != null)
+			}
+			if (cmd.errw != null) {
 				cmd.errw.flush();
+			}
 		}
 	}
 
+	void init(final TextBuiltin cmd) throws IOException {
+		if (cmd.requiresRepository()) {
+			cmd.init(openGitDir(gitdir), null);
+		} else {
+			cmd.init(null, gitdir);
+		}
+	}
+
+	/**
+	 * @param status
+	 * @param t
+	 *            can be {@code null}
+	 * @throws Exception
+	 */
+	void exit(int status, Exception t) throws Exception {
+		writer.flush();
+		System.exit(status);
+	}
+
 	/**
 	 * Evaluate the {@code --git-dir} option and open the repository.
 	 *
@@ -278,7 +322,7 @@ private static void install(final String name)
 			throws IllegalAccessException, InvocationTargetException,
 			NoSuchMethodException, ClassNotFoundException {
 		try {
-		Class.forName(name).getMethod("install").invoke(null); //$NON-NLS-1$
+			Class.forName(name).getMethod("install").invoke(null); //$NON-NLS-1$
 		} catch (InvocationTargetException e) {
 			if (e.getCause() instanceof RuntimeException)
 				throw (RuntimeException) e.getCause();
@@ -332,4 +376,19 @@ private static void configureHttpProxy() throws MalformedURLException {
 			}
 		}
 	}
+
+	/**
+	 * Parser for subcommands which doesn't stop parsing on help options and so
+	 * proceeds all specified options
+	 */
+	static class SubcommandLineParser extends CmdLineParser {
+		public SubcommandLineParser(Object bean) {
+			super(bean);
+		}
+
+		@Override
+		protected boolean containsHelp(String... args) {
+			return false;
+		}
+	}
 }
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Merge.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Merge.java
index cd65af9..e739b58 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Merge.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Merge.java
@@ -148,9 +148,12 @@ protected void run() throws Exception {
 			break;
 		case FAST_FORWARD:
 			ObjectId oldHeadId = oldHead.getObjectId();
-			outw.println(MessageFormat.format(CLIText.get().updating, oldHeadId
-					.abbreviate(7).name(), result.getNewHead().abbreviate(7)
-					.name()));
+			if (oldHeadId != null) {
+				String oldId = oldHeadId.abbreviate(7).name();
+				String newId = result.getNewHead().abbreviate(7).name();
+				outw.println(MessageFormat.format(CLIText.get().updating, oldId,
+						newId));
+			}
 			outw.println(result.getMergeStatus().toString());
 			break;
 		case CHECKOUT_CONFLICT:
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Remote.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Remote.java
index 70868e9..24916bd 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Remote.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Remote.java
@@ -144,7 +144,7 @@ protected void run() throws Exception {
 	}
 
 	@Override
-	public void printUsageAndExit(final String message, final CmdLineParser clp)
+	public void printUsage(final String message, final CmdLineParser clp)
 			throws IOException {
 		errw.println(message);
 		errw.println("jgit remote [--verbose (-v)] [--help (-h)]"); //$NON-NLS-1$
@@ -160,7 +160,6 @@ public void printUsageAndExit(final String message, final CmdLineParser clp)
 		errw.println();
 
 		errw.flush();
-		throw die(true);
 	}
 
 	private void print(List<RemoteConfig> remotes) throws IOException {
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Repo.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Repo.java
index db88008..ea59527 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Repo.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Repo.java
@@ -55,7 +55,7 @@ class Repo extends TextBuiltin {
 	@Option(name = "--groups", aliases = { "-g" }, usage = "usage_groups")
 	private String groups = "default"; //$NON-NLS-1$
 
-	@Argument(required = true, usage = "usage_pathToXml")
+	@Argument(required = true, metaVar = "metaVar_path", usage = "usage_pathToXml")
 	private String path;
 
 	@Option(name = "--record-remote-branch", usage = "usage_branches")
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Reset.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Reset.java
index 4d3af4b..9cee37b 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Reset.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Reset.java
@@ -51,7 +51,7 @@
 import org.eclipse.jgit.api.ResetCommand.ResetType;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
-import org.kohsuke.args4j.spi.StopOptionHandler;
+import org.kohsuke.args4j.spi.RestOfArgumentsHandler;
 
 @Command(common = true, usage = "usage_reset")
 class Reset extends TextBuiltin {
@@ -65,12 +65,12 @@ class Reset extends TextBuiltin {
 	@Option(name = "--hard", usage = "usage_resetHard")
 	private boolean hard = false;
 
-	@Argument(required = true, index = 0, metaVar = "metaVar_name", usage = "usage_reset")
+	@Argument(required = false, index = 0, metaVar = "metaVar_commitish", usage = "usage_resetReference")
 	private String commit;
 
-	@Argument(index = 1)
-	@Option(name = "--", metaVar = "metaVar_paths", multiValued = true, handler = StopOptionHandler.class)
-	private List<String> paths = new ArrayList<String>();
+	@Argument(required = false, index = 1, metaVar = "metaVar_paths")
+	@Option(name = "--", metaVar = "metaVar_paths", multiValued = true, handler = RestOfArgumentsHandler.class)
+	private List<String> paths = new ArrayList<>();
 
 	@Override
 	protected void run() throws Exception {
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/RevParse.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/RevParse.java
index e32fc9c..c5ecb84 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/RevParse.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/RevParse.java
@@ -75,7 +75,13 @@ protected void run() throws Exception {
 		if (all) {
 			Map<String, Ref> allRefs = db.getRefDatabase().getRefs(ALL);
 			for (final Ref r : allRefs.values()) {
-				outw.println(r.getObjectId().name());
+				ObjectId objectId = r.getObjectId();
+				// getRefs skips dangling symrefs, so objectId should never be
+				// null.
+				if (objectId == null) {
+					throw new NullPointerException();
+				}
+				outw.println(objectId.name());
 			}
 		} else {
 			if (verify && commits.size() > 1) {
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Status.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Status.java
index be82d07..6a63221 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Status.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Status.java
@@ -59,8 +59,9 @@
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.pgm.internal.CLIText;
+import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
-
+import org.kohsuke.args4j.spi.RestOfArgumentsHandler;
 import org.eclipse.jgit.pgm.opt.UntrackedFilesHandler;
 
 /**
@@ -83,7 +84,8 @@ class Status extends TextBuiltin {
 	@Option(name = "--untracked-files", aliases = { "-u", "-uno", "-uall" }, usage = "usage_untrackedFilesMode", handler = UntrackedFilesHandler.class)
 	protected String untrackedFilesMode = "all"; // default value //$NON-NLS-1$
 
-	@Option(name = "--", metaVar = "metaVar_path", multiValued = true)
+	@Argument(required = false, index = 0, metaVar = "metaVar_paths")
+	@Option(name = "--", metaVar = "metaVar_paths", multiValued = true, handler = RestOfArgumentsHandler.class)
 	protected List<String> filterPaths;
 
 	@Override
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/TextBuiltin.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/TextBuiltin.java
index 56cfc7e..0dc549c 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/TextBuiltin.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/TextBuiltin.java
@@ -212,17 +212,20 @@ public final void execute(String[] args) throws Exception {
 	 */
 	protected void parseArguments(final String[] args) throws IOException {
 		final CmdLineParser clp = new CmdLineParser(this);
+		help = containsHelp(args);
 		try {
 			clp.parseArgument(args);
 		} catch (CmdLineException err) {
-			if (!help) {
-				this.errw.println(MessageFormat.format(CLIText.get().fatalError, err.getMessage()));
-				throw die(true);
+			this.errw.println(CLIText.fatalError(err.getMessage()));
+			if (help) {
+				printUsage("", clp); //$NON-NLS-1$
 			}
+			throw die(true, err);
 		}
 
 		if (help) {
-			printUsageAndExit(clp);
+			printUsage("", clp); //$NON-NLS-1$
+			throw new TerminatedByHelpException();
 		}
 
 		argWalk = clp.getRevWalkGently();
@@ -246,6 +249,20 @@ public void printUsageAndExit(final CmdLineParser clp) throws IOException {
 	 * @throws IOException
 	 */
 	public void printUsageAndExit(final String message, final CmdLineParser clp) throws IOException {
+		printUsage(message, clp);
+		throw die(true);
+	}
+
+	/**
+	 * @param message
+	 *            non null
+	 * @param clp
+	 *            parser used to print options
+	 * @throws IOException
+	 * @since 4.2
+	 */
+	protected void printUsage(final String message, final CmdLineParser clp)
+			throws IOException {
 		errw.println(message);
 		errw.print("jgit "); //$NON-NLS-1$
 		errw.print(commandName);
@@ -257,12 +274,19 @@ public void printUsageAndExit(final String message, final CmdLineParser clp) thr
 		errw.println();
 
 		errw.flush();
-		throw die(true);
 	}
 
 	/**
-	 * @return the resource bundle that will be passed to args4j for purpose
-	 *         of string localization
+	 * @return error writer, typically this is standard error.
+	 * @since 4.2
+	 */
+	public ThrowingPrintWriter getErrorWriter() {
+		return errw;
+	}
+
+	/**
+	 * @return the resource bundle that will be passed to args4j for purpose of
+	 *         string localization
 	 */
 	protected ResourceBundle getResourceBundle() {
 		return CLIText.get().resourceBundle();
@@ -324,6 +348,19 @@ protected static Die die(boolean aborted) {
 		return new Die(aborted);
 	}
 
+	/**
+	 * @param aborted
+	 *            boolean indicating that the execution has been aborted before
+	 *            running
+	 * @param cause
+	 *            why the command has failed.
+	 * @return a runtime exception the caller is expected to throw
+	 * @since 4.2
+	 */
+	protected static Die die(boolean aborted, final Throwable cause) {
+		return new Die(aborted, cause);
+	}
+
 	String abbreviateRef(String dst, boolean abbreviateRemote) {
 		if (dst.startsWith(R_HEADS))
 			dst = dst.substring(R_HEADS.length());
@@ -333,4 +370,36 @@ else if (abbreviateRemote && dst.startsWith(R_REMOTES))
 			dst = dst.substring(R_REMOTES.length());
 		return dst;
 	}
+
+	/**
+	 * @param args
+	 *            non null
+	 * @return true if the given array contains help option
+	 * @since 4.2
+	 */
+	public static boolean containsHelp(String[] args) {
+		for (String str : args) {
+			if (str.equals("-h") || str.equals("--help")) { //$NON-NLS-1$ //$NON-NLS-2$
+				return true;
+			}
+		}
+		return false;
+	}
+
+	/**
+	 * Exception thrown by {@link TextBuiltin} if it proceeds 'help' option
+	 *
+	 * @since 4.2
+	 */
+	public static class TerminatedByHelpException extends Die {
+		private static final long serialVersionUID = 1L;
+
+		/**
+		 * Default constructor
+		 */
+		public TerminatedByHelpException() {
+			super(true);
+		}
+
+	}
 }
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/RebuildRefTree.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/RebuildRefTree.java
new file mode 100644
index 0000000..fbd4672
--- /dev/null
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/RebuildRefTree.java
@@ -0,0 +1,168 @@
+/*
+ * 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.pgm.debug;
+
+import static org.eclipse.jgit.lib.Constants.HEAD;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jgit.internal.storage.reftree.RefTree;
+import org.eclipse.jgit.internal.storage.reftree.RefTreeDatabase;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.pgm.Command;
+import org.eclipse.jgit.pgm.TextBuiltin;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.kohsuke.args4j.Option;
+
+@Command(usage = "usage_RebuildRefTree")
+class RebuildRefTree extends TextBuiltin {
+	@Option(name = "--enable", usage = "set extensions.refsStorage = reftree")
+	boolean enable;
+
+	private String txnNamespace;
+	private String txnCommitted;
+
+	@Override
+	protected void run() throws Exception {
+		try (ObjectReader reader = db.newObjectReader();
+				RevWalk rw = new RevWalk(reader);
+				ObjectInserter inserter = db.newObjectInserter()) {
+			RefDatabase refDb = db.getRefDatabase();
+			if (refDb instanceof RefTreeDatabase) {
+				RefTreeDatabase d = (RefTreeDatabase) refDb;
+				refDb = d.getBootstrap();
+				txnNamespace = d.getTxnNamespace();
+				txnCommitted = d.getTxnCommitted();
+			} else {
+				RefTreeDatabase d = new RefTreeDatabase(db, refDb);
+				txnNamespace = d.getTxnNamespace();
+				txnCommitted = d.getTxnCommitted();
+			}
+
+			errw.format("Rebuilding %s from %s", //$NON-NLS-1$
+					txnCommitted, refDb.getClass().getSimpleName());
+			errw.println();
+			errw.flush();
+
+			CommitBuilder b = new CommitBuilder();
+			Ref ref = refDb.exactRef(txnCommitted);
+			RefUpdate update = refDb.newUpdate(txnCommitted, true);
+			ObjectId oldTreeId;
+
+			if (ref != null && ref.getObjectId() != null) {
+				ObjectId oldId = ref.getObjectId();
+				update.setExpectedOldObjectId(oldId);
+				b.setParentId(oldId);
+				oldTreeId = rw.parseCommit(oldId).getTree();
+			} else {
+				update.setExpectedOldObjectId(ObjectId.zeroId());
+				oldTreeId = ObjectId.zeroId();
+			}
+
+			RefTree tree = rebuild(refDb);
+			b.setTreeId(tree.writeTree(inserter));
+			b.setAuthor(new PersonIdent(db));
+			b.setCommitter(b.getAuthor());
+			if (b.getTreeId().equals(oldTreeId)) {
+				return;
+			}
+
+			update.setNewObjectId(inserter.insert(b));
+			inserter.flush();
+
+			RefUpdate.Result result = update.update(rw);
+			switch (result) {
+			case NEW:
+			case FAST_FORWARD:
+				break;
+			default:
+				throw die(String.format("%s: %s", update.getName(), result)); //$NON-NLS-1$
+			}
+
+			if (enable && !(db.getRefDatabase() instanceof RefTreeDatabase)) {
+				StoredConfig cfg = db.getConfig();
+				cfg.setInt("core", null, "repositoryformatversion", 1); //$NON-NLS-1$ //$NON-NLS-2$
+				cfg.setString("extensions", null, "refsStorage", "reftree"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+				cfg.save();
+				errw.println("Enabled reftree."); //$NON-NLS-1$
+				errw.flush();
+			}
+		}
+	}
+
+	private RefTree rebuild(RefDatabase refdb) throws IOException {
+		RefTree tree = RefTree.newEmptyTree();
+		List<org.eclipse.jgit.internal.storage.reftree.Command> cmds
+			= new ArrayList<>();
+
+		Ref head = refdb.exactRef(HEAD);
+		if (head != null) {
+			cmds.add(new org.eclipse.jgit.internal.storage.reftree.Command(
+					null,
+					head));
+		}
+
+		for (Ref r : refdb.getRefs(RefDatabase.ALL).values()) {
+			if (r.getName().equals(txnCommitted)
+					|| r.getName().startsWith(txnNamespace)) {
+				continue;
+			}
+			cmds.add(new org.eclipse.jgit.internal.storage.reftree.Command(
+					null,
+					db.peel(r)));
+		}
+		tree.apply(cmds);
+		return tree;
+	}
+}
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..2812137 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
@@ -74,6 +74,19 @@ public static String formatLine(String line) {
 		return MessageFormat.format(get().lineFormat, line);
 	}
 
+	/**
+	 * Format the given argument as fatal error using the format defined by
+	 * {@link #fatalError} ("fatal: " by default).
+	 *
+	 * @param message
+	 *            the message to format
+	 * @return the formatted line
+	 * @since 4.2
+	 */
+	public static String fatalError(String message) {
+		return MessageFormat.format(get().fatalError, message);
+	}
+
 	// @formatter:off
 	/***/ public String alreadyOnBranch;
 	/***/ public String alreadyUpToDate;
@@ -85,6 +98,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 +198,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 3f77aa6..b531ba6 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
@@ -43,19 +43,18 @@
 
 package org.eclipse.jgit.pgm.opt;
 
+import java.io.IOException;
+import java.io.Writer;
 import java.lang.reflect.Field;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ResourceBundle;
 
-import org.kohsuke.args4j.Argument;
-import org.kohsuke.args4j.CmdLineException;
-import org.kohsuke.args4j.IllegalAnnotationError;
-import org.kohsuke.args4j.NamedOptionDef;
-import org.kohsuke.args4j.Option;
-import org.kohsuke.args4j.OptionDef;
-import org.kohsuke.args4j.spi.OptionHandler;
-import org.kohsuke.args4j.spi.Setter;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.pgm.Die;
 import org.eclipse.jgit.pgm.TextBuiltin;
 import org.eclipse.jgit.pgm.internal.CLIText;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -63,6 +62,15 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.RefSpec;
 import org.eclipse.jgit.treewalk.AbstractTreeIterator;
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.CmdLineException;
+import org.kohsuke.args4j.IllegalAnnotationError;
+import org.kohsuke.args4j.NamedOptionDef;
+import org.kohsuke.args4j.Option;
+import org.kohsuke.args4j.OptionDef;
+import org.kohsuke.args4j.spi.OptionHandler;
+import org.kohsuke.args4j.spi.RestOfArgumentsHandler;
+import org.kohsuke.args4j.spi.Setter;
 
 /**
  * Extended command line parser which handles --foo=value arguments.
@@ -80,12 +88,17 @@ 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;
 
 	private RevWalk walk;
 
+	private boolean seenHelp;
+
+	private TextBuiltin cmd;
+
 	/**
 	 * Creates a new command line owner that parses arguments/options and set
 	 * them into the given object.
@@ -117,8 +130,12 @@ public CmdLineParser(final Object bean) {
 	 */
 	public CmdLineParser(final Object bean, Repository repo) {
 		super(bean);
-		if (repo == null && bean instanceof TextBuiltin)
-			repo = ((TextBuiltin) bean).getRepository();
+		if (bean instanceof TextBuiltin) {
+			cmd = (TextBuiltin) bean;
+		}
+		if (repo == null && cmd != null) {
+			repo = cmd.getRepository();
+		}
 		this.db = repo;
 	}
 
@@ -143,9 +160,75 @@ public void parseArgument(final String... args) throws CmdLineException {
 			}
 
 			tmp.add(str);
+
+			if (containsHelp(args)) {
+				// suppress exceptions on required parameters if help is present
+				seenHelp = true;
+				// stop argument parsing here
+				break;
+			}
+		}
+		List<OptionHandler> backup = null;
+		if (seenHelp) {
+			backup = unsetRequiredOptions();
 		}
 
-		super.parseArgument(tmp.toArray(new String[tmp.size()]));
+		try {
+			super.parseArgument(tmp.toArray(new String[tmp.size()]));
+		} catch (Die e) {
+			if (!seenHelp) {
+				throw e;
+			}
+			printToErrorWriter(CLIText.fatalError(e.getMessage()));
+		} finally {
+			// reset "required" options to defaults for correct command printout
+			if (backup != null && !backup.isEmpty()) {
+				restoreRequiredOptions(backup);
+			}
+			seenHelp = false;
+		}
+	}
+
+	private void printToErrorWriter(String error) {
+		if (cmd == null) {
+			System.err.println(error);
+		} else {
+			try {
+				cmd.getErrorWriter().println(error);
+			} catch (IOException e1) {
+				System.err.println(error);
+			}
+		}
+	}
+
+	private List<OptionHandler> unsetRequiredOptions() {
+		List<OptionHandler> options = getOptions();
+		List<OptionHandler> backup = new ArrayList<>(options);
+		for (Iterator<OptionHandler> iterator = options.iterator(); iterator
+				.hasNext();) {
+			OptionHandler handler = iterator.next();
+			if (handler.option instanceof NamedOptionDef
+					&& handler.option.required()) {
+				iterator.remove();
+			}
+		}
+		return backup;
+	}
+
+	private void restoreRequiredOptions(List<OptionHandler> backup) {
+		List<OptionHandler> options = getOptions();
+		options.clear();
+		options.addAll(backup);
+	}
+
+	/**
+	 * @param args
+	 *            non null
+	 * @return true if the given array contains help option
+	 * @since 4.2
+	 */
+	protected boolean containsHelp(final String... args) {
+		return TextBuiltin.containsHelp(args);
 	}
 
 	/**
@@ -181,7 +264,7 @@ public RevWalk getRevWalkGently() {
 		return walk;
 	}
 
-	static class MyOptionDef extends OptionDef {
+	class MyOptionDef extends OptionDef {
 
 		public MyOptionDef(OptionDef o) {
 			super(o.usage(), o.metaVar(), o.required(), o.handler(), o
@@ -201,6 +284,11 @@ public String toString() {
 				return metaVar();
 			}
 		}
+
+		@Override
+		public boolean required() {
+			return seenHelp ? false : super.required();
+		}
 	}
 
 	@Override
@@ -211,4 +299,55 @@ protected OptionHandler createOptionHandler(OptionDef o, Setter setter) {
 			return super.createOptionHandler(new MyOptionDef(o), setter);
 
 	}
+
+	@SuppressWarnings("unchecked")
+	private List<OptionHandler> getOptions() {
+		List<OptionHandler> options = null;
+		try {
+			Field field = org.kohsuke.args4j.CmdLineParser.class
+					.getDeclaredField("options"); //$NON-NLS-1$
+			field.setAccessible(true);
+			options = (List<OptionHandler>) field.get(this);
+		} catch (NoSuchFieldException | SecurityException
+				| IllegalArgumentException | IllegalAccessException e) {
+			// ignore
+		}
+		if (options == null) {
+			return Collections.emptyList();
+		}
+		return options;
+	}
+
+	@Override
+	public void printSingleLineUsage(Writer w, ResourceBundle rb) {
+		List<OptionHandler> options = getOptions();
+		if (options.isEmpty()) {
+			super.printSingleLineUsage(w, rb);
+			return;
+		}
+		List<OptionHandler> backup = new ArrayList<>(options);
+		boolean changed = sortRestOfArgumentsHandlerToTheEnd(options);
+		try {
+			super.printSingleLineUsage(w, rb);
+		} finally {
+			if (changed) {
+				options.clear();
+				options.addAll(backup);
+			}
+		}
+	}
+
+	private boolean sortRestOfArgumentsHandlerToTheEnd(
+			List<OptionHandler> options) {
+		for (int i = 0; i < options.size(); i++) {
+			OptionHandler handler = options.get(i);
+			if (handler instanceof RestOfArgumentsHandler
+					|| handler instanceof PathTreeFilterHandler) {
+				options.remove(i);
+				options.add(handler);
+				return true;
+			}
+		}
+		return false;
+	}
 }
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/BUCK b/org.eclipse.jgit.test/BUCK
new file mode 100644
index 0000000..3df3336
--- /dev/null
+++ b/org.eclipse.jgit.test/BUCK
@@ -0,0 +1,95 @@
+PKG = 'tst/org/eclipse/jgit/'
+HELPERS = glob(['src/**/*.java']) + [PKG + c for c in [
+  'api/AbstractRemoteCommandTest.java',
+  'diff/AbstractDiffTestCase.java',
+  'internal/storage/file/GcTestCase.java',
+  'internal/storage/file/PackIndexTestCase.java',
+  'internal/storage/file/XInputStream.java',
+  'nls/GermanTranslatedBundle.java',
+  'nls/MissingPropertyBundle.java',
+  'nls/NoPropertiesBundle.java',
+  'nls/NonTranslatedBundle.java',
+  'revwalk/RevQueueTestCase.java',
+  'revwalk/RevWalkTestCase.java',
+  'transport/SpiTransport.java',
+  'treewalk/FileTreeIteratorWithTimeControl.java',
+  'treewalk/filter/AlwaysCloneTreeFilter.java',
+  'test/resources/SampleDataRepositoryTestCase.java',
+  'util/CPUTimeStopWatch.java',
+  'util/io/Strings.java',
+]]
+
+DATA = [
+  PKG + 'lib/empty.gitindex.dat',
+  PKG + 'lib/sorttest.gitindex.dat',
+]
+
+TESTS = glob(
+  ['tst/**/*.java'],
+  excludes = HELPERS + DATA,
+)
+
+DEPS = {
+  PKG + 'nls/RootLocaleTest.java': [
+    '//org.eclipse.jgit.pgm:pgm',
+    '//org.eclipse.jgit.ui:ui',
+  ],
+}
+
+for src in TESTS:
+  name = src[len('tst/'):len(src)-len('.java')].replace('/', '.')
+  labels = []
+  if name.startswith('org.eclipse.jgit.'):
+    l = name[len('org.eclipse.jgit.'):]
+    if l.startswith('internal.storage.'):
+      l = l[len('internal.storage.'):]
+    i = l.find('.')
+    if i > 0:
+      labels.append(l[:i])
+    else:
+      labels.append(i)
+  if 'lib' not in labels:
+    labels.append('lib')
+
+  java_test(
+    name = name,
+    labels = labels,
+    srcs = [src],
+    deps = [
+      ':helpers',
+      ':tst_rsrc',
+      '//org.eclipse.jgit:jgit',
+      '//org.eclipse.jgit.junit:junit',
+      '//lib:hamcrest-core',
+      '//lib:hamcrest-library',
+      '//lib:javaewah',
+      '//lib:junit',
+      '//lib:slf4j-api',
+      '//lib:slf4j-simple',
+    ] + DEPS.get(src, []),
+    source_under_test = ['//org.eclipse.jgit:jgit'],
+    vm_args = ['-Xmx256m', '-Dfile.encoding=UTF-8'],
+  )
+
+java_library(
+  name = 'helpers',
+  srcs = HELPERS,
+  resources = DATA,
+  deps = [
+    '//org.eclipse.jgit:jgit',
+    '//org.eclipse.jgit.junit:junit',
+    '//lib:junit',
+  ],
+)
+
+prebuilt_jar(
+  name = 'tst_rsrc',
+  binary_jar = ':tst_rsrc_jar',
+)
+
+genrule(
+  name = 'tst_rsrc_jar',
+  cmd = 'cd $SRCDIR/tst-rsrc ; zip -qr $OUT .',
+  srcs = glob(['tst-rsrc/**']),
+  out = 'tst_rsrc.jar',
+)
diff --git a/org.eclipse.jgit.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.test/META-INF/MANIFEST.MF
index 37fd367..f78fe5b 100644
--- a/org.eclipse.jgit.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.test/META-INF/MANIFEST.MF
@@ -26,6 +26,7 @@
  org.eclipse.jgit.internal.storage.dfs;version="[4.2.0,4.3.0)",
  org.eclipse.jgit.internal.storage.file;version="[4.2.0,4.3.0)",
  org.eclipse.jgit.internal.storage.pack;version="[4.2.0,4.3.0)",
+ org.eclipse.jgit.internal.storage.reftree;version="[4.2.0,4.3.0)",
  org.eclipse.jgit.junit;version="[4.2.0,4.3.0)",
  org.eclipse.jgit.lib;version="[4.2.0,4.3.0)",
  org.eclipse.jgit.merge;version="[4.2.0,4.3.0)",
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/api/AddCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java
index d4bd68e..4fefdfd 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/AddCommandTest.java
@@ -43,6 +43,7 @@
  */
 package org.eclipse.jgit.api;
 
+import static org.eclipse.jgit.util.FileUtils.RECURSIVE;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
@@ -797,7 +798,6 @@ public void testAssumeUnchanged() throws Exception {
 
 			assertEquals("[a.txt, mode:100644, content:more content,"
 					+ " assume-unchanged:false][b.txt, mode:100644,"
-					+ "" + ""
 					+ " content:content, assume-unchanged:true]",
 					indexState(CONTENT
 					| ASSUME_UNCHANGED));
@@ -805,6 +805,102 @@ public void testAssumeUnchanged() throws Exception {
 	}
 
 	@Test
+	public void testReplaceFileWithDirectory()
+			throws IOException, NoFilepatternException, GitAPIException {
+		try (Git git = new Git(db)) {
+			writeTrashFile("df", "before replacement");
+			git.add().addFilepattern("df").call();
+			assertEquals("[df, mode:100644, content:before replacement]",
+					indexState(CONTENT));
+			FileUtils.delete(new File(db.getWorkTree(), "df"));
+			writeTrashFile("df/f", "after replacement");
+			git.add().addFilepattern("df").call();
+			assertEquals("[df/f, mode:100644, content:after replacement]",
+					indexState(CONTENT));
+		}
+	}
+
+	@Test
+	public void testReplaceDirectoryWithFile()
+			throws IOException, NoFilepatternException, GitAPIException {
+		try (Git git = new Git(db)) {
+			writeTrashFile("df/f", "before replacement");
+			git.add().addFilepattern("df").call();
+			assertEquals("[df/f, mode:100644, content:before replacement]",
+					indexState(CONTENT));
+			FileUtils.delete(new File(db.getWorkTree(), "df"), RECURSIVE);
+			writeTrashFile("df", "after replacement");
+			git.add().addFilepattern("df").call();
+			assertEquals("[df, mode:100644, content:after replacement]",
+					indexState(CONTENT));
+		}
+	}
+
+	@Test
+	public void testReplaceFileByPartOfDirectory()
+			throws IOException, NoFilepatternException, GitAPIException {
+		try (Git git = new Git(db)) {
+			writeTrashFile("src/main", "df", "before replacement");
+			writeTrashFile("src/main", "z", "z");
+			writeTrashFile("z", "z2");
+			git.add().addFilepattern("src/main/df")
+				.addFilepattern("src/main/z")
+				.addFilepattern("z")
+				.call();
+			assertEquals(
+					"[src/main/df, mode:100644, content:before replacement]" +
+					"[src/main/z, mode:100644, content:z]" +
+					"[z, mode:100644, content:z2]",
+					indexState(CONTENT));
+			FileUtils.delete(new File(db.getWorkTree(), "src/main/df"));
+			writeTrashFile("src/main/df", "a", "after replacement");
+			writeTrashFile("src/main/df", "b", "unrelated file");
+			git.add().addFilepattern("src/main/df/a").call();
+			assertEquals(
+					"[src/main/df/a, mode:100644, content:after replacement]" +
+					"[src/main/z, mode:100644, content:z]" +
+					"[z, mode:100644, content:z2]",
+					indexState(CONTENT));
+		}
+	}
+
+	@Test
+	public void testReplaceDirectoryConflictsWithFile()
+			throws IOException, NoFilepatternException, GitAPIException {
+		DirCache dc = db.lockDirCache();
+		try (ObjectInserter oi = db.newObjectInserter()) {
+			DirCacheBuilder builder = dc.builder();
+			File f = writeTrashFile("a", "df", "content");
+			addEntryToBuilder("a", f, oi, builder, 1);
+
+			f = writeTrashFile("a", "df", "other content");
+			addEntryToBuilder("a/df", f, oi, builder, 3);
+
+			f = writeTrashFile("a", "df", "our content");
+			addEntryToBuilder("a/df", f, oi, builder, 2);
+
+			f = writeTrashFile("z", "z");
+			addEntryToBuilder("z", f, oi, builder, 0);
+			builder.commit();
+		}
+		assertEquals(
+				"[a, mode:100644, stage:1, content:content]" +
+				"[a/df, mode:100644, stage:2, content:our content]" +
+				"[a/df, mode:100644, stage:3, content:other content]" +
+				"[z, mode:100644, content:z]",
+				indexState(CONTENT));
+
+		try (Git git = new Git(db)) {
+			FileUtils.delete(new File(db.getWorkTree(), "a"), RECURSIVE);
+			writeTrashFile("a", "merged");
+			git.add().addFilepattern("a").call();
+			assertEquals("[a, mode:100644, content:merged]" +
+					"[z, mode:100644, content:z]",
+					indexState(CONTENT));
+		}
+	}
+
+	@Test
 	public void testExecutableRetention() throws Exception {
 		StoredConfig config = db.getConfig();
 		config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java
index 0d03047..b39a68a 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CommitCommandTest.java
@@ -46,12 +46,15 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.fail;
 
 import java.io.File;
 import java.util.Date;
 import java.util.List;
 import java.util.TimeZone;
 
+import org.eclipse.jgit.api.errors.EmtpyCommitException;
 import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
 import org.eclipse.jgit.diff.DiffEntry;
 import org.eclipse.jgit.dircache.DirCache;
@@ -477,6 +480,34 @@ public void commitAmendWithAuthorShouldUseIt() throws Exception {
 	}
 
 	@Test
+	public void commitEmptyCommits() throws Exception {
+		try (Git git = new Git(db)) {
+
+			writeTrashFile("file1", "file1");
+			git.add().addFilepattern("file1").call();
+			RevCommit initial = git.commit().setMessage("initial commit")
+					.call();
+
+			RevCommit emptyFollowUp = git.commit()
+					.setAuthor("New Author", "newauthor@example.org")
+					.setMessage("no change").call();
+
+			assertNotEquals(initial.getId(), emptyFollowUp.getId());
+			assertEquals(initial.getTree().getId(),
+					emptyFollowUp.getTree().getId());
+
+			try {
+				git.commit().setAuthor("New Author", "newauthor@example.org")
+						.setMessage("again no change").setAllowEmpty(false)
+						.call();
+				fail("Didn't get the expected EmtpyCommitException");
+			} catch (EmtpyCommitException e) {
+				// expect this exception
+			}
+		}
+	}
+
+	@Test
 	public void commitOnlyShouldCommitUnmergedPathAndNotAffectOthers()
 			throws Exception {
 		DirCache index = db.lockDirCache();
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PathCheckoutCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PathCheckoutCommandTest.java
index db811cd..3343af0 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PathCheckoutCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PathCheckoutCommandTest.java
@@ -43,10 +43,12 @@
 package org.eclipse.jgit.api;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 import java.io.File;
 import java.io.IOException;
+import java.nio.file.Path;
 
 import org.eclipse.jgit.api.CheckoutCommand.Stage;
 import org.eclipse.jgit.api.errors.JGitInternalException;
@@ -59,6 +61,9 @@
 import org.eclipse.jgit.lib.RepositoryState;
 import org.eclipse.jgit.lib.StoredConfig;
 import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.FileUtils;
+import org.junit.Assume;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -73,6 +78,8 @@ public class PathCheckoutCommandTest extends RepositoryTestCase {
 
 	private static final String FILE3 = "Test3.txt";
 
+	private static final String LINK = "link";
+
 	Git git;
 
 	RevCommit initialCommit;
@@ -99,6 +106,64 @@ public void setUp() throws Exception {
 	}
 
 	@Test
+	public void testUpdateSymLink() throws Exception {
+		Assume.assumeTrue(FS.DETECTED.supportsSymlinks());
+
+		Path path = writeLink(LINK, FILE1);
+		git.add().addFilepattern(LINK).call();
+		git.commit().setMessage("Added link").call();
+		assertEquals("3", read(path.toFile()));
+
+		writeLink(LINK, FILE2);
+		assertEquals("c", read(path.toFile()));
+
+		CheckoutCommand co = git.checkout();
+		co.addPath(LINK).call();
+
+		assertEquals("3", read(path.toFile()));
+	}
+
+	@Test
+	public void testUpdateBrokenSymLinkToDirectory() throws Exception {
+		Assume.assumeTrue(FS.DETECTED.supportsSymlinks());
+
+		Path path = writeLink(LINK, "f");
+		git.add().addFilepattern(LINK).call();
+		git.commit().setMessage("Added link").call();
+		assertEquals("f", FileUtils.readSymLink(path.toFile()));
+		assertTrue(path.toFile().exists());
+
+		writeLink(LINK, "link_to_nowhere");
+		assertFalse(path.toFile().exists());
+		assertEquals("link_to_nowhere", FileUtils.readSymLink(path.toFile()));
+
+		CheckoutCommand co = git.checkout();
+		co.addPath(LINK).call();
+
+		assertEquals("f", FileUtils.readSymLink(path.toFile()));
+	}
+
+	@Test
+	public void testUpdateBrokenSymLink() throws Exception {
+		Assume.assumeTrue(FS.DETECTED.supportsSymlinks());
+
+		Path path = writeLink(LINK, FILE1);
+		git.add().addFilepattern(LINK).call();
+		git.commit().setMessage("Added link").call();
+		assertEquals("3", read(path.toFile()));
+		assertEquals(FILE1, FileUtils.readSymLink(path.toFile()));
+
+		writeLink(LINK, "link_to_nowhere");
+		assertFalse(path.toFile().exists());
+		assertEquals("link_to_nowhere", FileUtils.readSymLink(path.toFile()));
+
+		CheckoutCommand co = git.checkout();
+		co.addPath(LINK).call();
+
+		assertEquals("3", read(path.toFile()));
+	}
+
+	@Test
 	public void testUpdateWorkingDirectory() throws Exception {
 		CheckoutCommand co = git.checkout();
 		File written = writeTrashFile(FILE1, "");
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java
index a67f2b9..66f25e8 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/ResetCommandTest.java
@@ -65,6 +65,7 @@
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.treewalk.TreeWalk;
@@ -139,8 +140,8 @@ public void testHardReset() throws JGitInternalException,
 			AmbiguousObjectException, IOException, GitAPIException {
 		setupRepository();
 		ObjectId prevHead = db.resolve(Constants.HEAD);
-		git.reset().setMode(ResetType.HARD).setRef(initialCommit.getName())
-				.call();
+		assertSameAsHead(git.reset().setMode(ResetType.HARD)
+				.setRef(initialCommit.getName()).call());
 		// check if HEAD points to initial commit now
 		ObjectId head = db.resolve(Constants.HEAD);
 		assertEquals(initialCommit, head);
@@ -176,8 +177,8 @@ public void testSoftReset() throws JGitInternalException,
 			AmbiguousObjectException, IOException, GitAPIException {
 		setupRepository();
 		ObjectId prevHead = db.resolve(Constants.HEAD);
-		git.reset().setMode(ResetType.SOFT).setRef(initialCommit.getName())
-				.call();
+		assertSameAsHead(git.reset().setMode(ResetType.SOFT)
+				.setRef(initialCommit.getName()).call());
 		// check if HEAD points to initial commit now
 		ObjectId head = db.resolve(Constants.HEAD);
 		assertEquals(initialCommit, head);
@@ -197,8 +198,8 @@ public void testMixedReset() throws JGitInternalException,
 			AmbiguousObjectException, IOException, GitAPIException {
 		setupRepository();
 		ObjectId prevHead = db.resolve(Constants.HEAD);
-		git.reset().setMode(ResetType.MIXED).setRef(initialCommit.getName())
-				.call();
+		assertSameAsHead(git.reset().setMode(ResetType.MIXED)
+				.setRef(initialCommit.getName()).call());
 		// check if HEAD points to initial commit now
 		ObjectId head = db.resolve(Constants.HEAD);
 		assertEquals(initialCommit, head);
@@ -241,7 +242,8 @@ public void testMixedResetRetainsSizeAndModifiedTime() throws Exception {
 		assertTrue(bEntry.getLength() > 0);
 		assertTrue(bEntry.getLastModified() > 0);
 
-		git.reset().setMode(ResetType.MIXED).setRef(commit2.getName()).call();
+		assertSameAsHead(git.reset().setMode(ResetType.MIXED)
+				.setRef(commit2.getName()).call());
 
 		cache = db.readDirCache();
 
@@ -280,7 +282,7 @@ public void testMixedResetWithUnmerged() throws Exception {
 				+ "[a.txt, mode:100644, stage:3]",
 				indexState(0));
 
-		git.reset().setMode(ResetType.MIXED).call();
+		assertSameAsHead(git.reset().setMode(ResetType.MIXED).call());
 
 		assertEquals("[a.txt, mode:100644]" + "[b.txt, mode:100644]",
 				indexState(0));
@@ -298,8 +300,8 @@ public void testPathsReset() throws Exception {
 
 		// 'a.txt' has already been modified in setupRepository
 		// 'notAddedToIndex.txt' has been added to repository
-		git.reset().addPath(indexFile.getName())
-				.addPath(untrackedFile.getName()).call();
+		assertSameAsHead(git.reset().addPath(indexFile.getName())
+				.addPath(untrackedFile.getName()).call());
 
 		DirCacheEntry postReset = DirCache.read(db.getIndexFile(), db.getFS())
 				.getEntry(indexFile.getName());
@@ -329,7 +331,7 @@ public void testPathsResetOnDirs() throws Exception {
 		git.add().addFilepattern(untrackedFile.getName()).call();
 
 		// 'dir/b.txt' has already been modified in setupRepository
-		git.reset().addPath("dir").call();
+		assertSameAsHead(git.reset().addPath("dir").call());
 
 		DirCacheEntry postReset = DirCache.read(db.getIndexFile(), db.getFS())
 				.getEntry("dir/b.txt");
@@ -358,9 +360,9 @@ public void testPathsResetWithRef() throws Exception {
 		// 'a.txt' has already been modified in setupRepository
 		// 'notAddedToIndex.txt' has been added to repository
 		// reset to the inital commit
-		git.reset().setRef(initialCommit.getName())
-				.addPath(indexFile.getName())
-				.addPath(untrackedFile.getName()).call();
+		assertSameAsHead(git.reset().setRef(initialCommit.getName())
+				.addPath(indexFile.getName()).addPath(untrackedFile.getName())
+				.call());
 
 		// check that HEAD hasn't moved
 		ObjectId head = db.resolve(Constants.HEAD);
@@ -397,7 +399,7 @@ public void testPathsResetWithUnmerged() throws Exception {
 				+ "[b.txt, mode:100644]",
 				indexState(0));
 
-		git.reset().addPath(file).call();
+		assertSameAsHead(git.reset().addPath(file).call());
 
 		assertEquals("[a.txt, mode:100644]" + "[b.txt, mode:100644]",
 				indexState(0));
@@ -409,7 +411,7 @@ public void testPathsResetOnUnbornBranch() throws Exception {
 		writeTrashFile("a.txt", "content");
 		git.add().addFilepattern("a.txt").call();
 		// Should assume an empty tree, like in C Git 1.8.2
-		git.reset().addPath("a.txt").call();
+		assertSameAsHead(git.reset().addPath("a.txt").call());
 
 		DirCache cache = db.readDirCache();
 		DirCacheEntry aEntry = cache.getEntry("a.txt");
@@ -421,7 +423,8 @@ public void testPathsResetToNonexistingRef() throws Exception {
 		git = new Git(db);
 		writeTrashFile("a.txt", "content");
 		git.add().addFilepattern("a.txt").call();
-		git.reset().setRef("doesnotexist").addPath("a.txt").call();
+		assertSameAsHead(
+				git.reset().setRef("doesnotexist").addPath("a.txt").call());
 	}
 
 	@Test
@@ -431,7 +434,7 @@ public void testResetDefaultMode() throws Exception {
 		git.add().addFilepattern("a.txt").call();
 		writeTrashFile("a.txt", "modified");
 		// should use default mode MIXED
-		git.reset().call();
+		assertSameAsHead(git.reset().call());
 
 		DirCache cache = db.readDirCache();
 		DirCacheEntry aEntry = cache.getEntry("a.txt");
@@ -452,7 +455,7 @@ public void testHardResetOnTag() throws Exception {
 
 		git.add().addFilepattern(untrackedFile.getName()).call();
 
-		git.reset().setRef(tagName).setMode(HARD).call();
+		assertSameAsHead(git.reset().setRef(tagName).setMode(HARD).call());
 
 		ObjectId head = db.resolve(Constants.HEAD);
 		assertEquals(secondCommit, head);
@@ -486,7 +489,8 @@ public void testHardResetAfterSquashMerge() throws Exception {
 				result.getMergeStatus());
 		assertNotNull(db.readSquashCommitMsg());
 
-		g.reset().setMode(ResetType.HARD).setRef(first.getName()).call();
+		assertSameAsHead(g.reset().setMode(ResetType.HARD)
+				.setRef(first.getName()).call());
 
 		assertNull(db.readSquashCommitMsg());
 	}
@@ -497,7 +501,7 @@ public void testHardResetOnUnbornBranch() throws Exception {
 		File fileA = writeTrashFile("a.txt", "content");
 		git.add().addFilepattern("a.txt").call();
 		// Should assume an empty tree, like in C Git 1.8.2
-		git.reset().setMode(ResetType.HARD).call();
+		assertSameAsHead(git.reset().setMode(ResetType.HARD).call());
 
 		DirCache cache = db.readDirCache();
 		DirCacheEntry aEntry = cache.getEntry("a.txt");
@@ -558,4 +562,14 @@ private boolean inIndex(String path) throws IOException {
 		return dc.getEntry(path) != null;
 	}
 
+	/**
+	 * Asserts that a certain ref is similar to repos HEAD.
+	 * @param ref
+	 * @throws IOException
+	 */
+	private void assertSameAsHead(Ref ref) throws IOException {
+		Ref headRef = db.getRef(Constants.HEAD);
+		assertEquals(headRef.getName(), ref.getName());
+		assertEquals(headRef.getObjectId(), ref.getObjectId());
+	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeWorkingTreeIteratorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeWorkingTreeIteratorTest.java
index 6ad19a2..4215ba2 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeWorkingTreeIteratorTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeWorkingTreeIteratorTest.java
@@ -56,7 +56,6 @@
 import java.util.List;
 
 import org.eclipse.jgit.attributes.Attribute.State;
-import org.eclipse.jgit.errors.CorruptObjectException;
 import org.eclipse.jgit.junit.JGitTestUtil;
 import org.eclipse.jgit.junit.RepositoryTestCase;
 import org.eclipse.jgit.lib.FileMode;
@@ -253,7 +252,7 @@ private void writeAttributesFile(String name, String... rules)
 		writeTrashFile(name, data.toString());
 	}
 
-	private TreeWalk beginWalk() throws CorruptObjectException {
+	private TreeWalk beginWalk() {
 		TreeWalk newWalk = new TreeWalk(db);
 		newWalk.addTree(new FileTreeIterator(db));
 		return newWalk;
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCachePathEditTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCachePathEditTest.java
index 63ec858..c85e156 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCachePathEditTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/dircache/DirCachePathEditTest.java
@@ -43,11 +43,13 @@
 package org.eclipse.jgit.dircache;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
 
 import java.util.ArrayList;
 import java.util.List;
 
 import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
+import org.eclipse.jgit.errors.DirCacheNameConflictException;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Test;
@@ -154,6 +156,125 @@ public void testPathEditShouldBeCalledForEachStage() throws Exception {
 		assertEquals(DirCacheEntry.STAGE_3, entries.get(2).getStage());
 	}
 
+	@Test
+	public void testFileReplacesTree() throws Exception {
+		DirCache dc = DirCache.newInCore();
+		DirCacheEditor editor = dc.editor();
+		editor.add(new AddEdit("a"));
+		editor.add(new AddEdit("b/c"));
+		editor.add(new AddEdit("b/d"));
+		editor.add(new AddEdit("e"));
+		editor.finish();
+
+		editor = dc.editor();
+		editor.add(new AddEdit("b"));
+		editor.finish();
+
+		assertEquals(3, dc.getEntryCount());
+		assertEquals("a", dc.getEntry(0).getPathString());
+		assertEquals("b", dc.getEntry(1).getPathString());
+		assertEquals("e", dc.getEntry(2).getPathString());
+
+		dc.clear();
+		editor = dc.editor();
+		editor.add(new AddEdit("A.c"));
+		editor.add(new AddEdit("A/c"));
+		editor.add(new AddEdit("A0c"));
+		editor.finish();
+
+		editor = dc.editor();
+		editor.add(new AddEdit("A"));
+		editor.finish();
+		assertEquals(3, dc.getEntryCount());
+		assertEquals("A", dc.getEntry(0).getPathString());
+		assertEquals("A.c", dc.getEntry(1).getPathString());
+		assertEquals("A0c", dc.getEntry(2).getPathString());
+	}
+
+	@Test
+	public void testTreeReplacesFile() throws Exception {
+		DirCache dc = DirCache.newInCore();
+		DirCacheEditor editor = dc.editor();
+		editor.add(new AddEdit("a"));
+		editor.add(new AddEdit("ab"));
+		editor.add(new AddEdit("b"));
+		editor.add(new AddEdit("e"));
+		editor.finish();
+
+		editor = dc.editor();
+		editor.add(new AddEdit("b/c/d/f"));
+		editor.add(new AddEdit("b/g/h/i"));
+		editor.finish();
+
+		assertEquals(5, dc.getEntryCount());
+		assertEquals("a", dc.getEntry(0).getPathString());
+		assertEquals("ab", dc.getEntry(1).getPathString());
+		assertEquals("b/c/d/f", dc.getEntry(2).getPathString());
+		assertEquals("b/g/h/i", dc.getEntry(3).getPathString());
+		assertEquals("e", dc.getEntry(4).getPathString());
+	}
+
+	@Test
+	public void testDuplicateFiles() throws Exception {
+		DirCache dc = DirCache.newInCore();
+		DirCacheEditor editor = dc.editor();
+		editor.add(new AddEdit("a"));
+		editor.add(new AddEdit("a"));
+
+		try {
+			editor.finish();
+			fail("Expected DirCacheNameConflictException to be thrown");
+		} catch (DirCacheNameConflictException e) {
+			assertEquals("a a", e.getMessage());
+			assertEquals("a", e.getPath1());
+			assertEquals("a", e.getPath2());
+		}
+	}
+
+	@Test
+	public void testFileOverlapsTree() throws Exception {
+		DirCache dc = DirCache.newInCore();
+		DirCacheEditor editor = dc.editor();
+		editor.add(new AddEdit("a"));
+		editor.add(new AddEdit("a/b").setReplace(false));
+		try {
+			editor.finish();
+			fail("Expected DirCacheNameConflictException to be thrown");
+		} catch (DirCacheNameConflictException e) {
+			assertEquals("a a/b", e.getMessage());
+			assertEquals("a", e.getPath1());
+			assertEquals("a/b", e.getPath2());
+		}
+
+		editor = dc.editor();
+		editor.add(new AddEdit("A.c"));
+		editor.add(new AddEdit("A/c").setReplace(false));
+		editor.add(new AddEdit("A0c"));
+		editor.add(new AddEdit("A"));
+		try {
+			editor.finish();
+			fail("Expected DirCacheNameConflictException to be thrown");
+		} catch (DirCacheNameConflictException e) {
+			assertEquals("A A/c", e.getMessage());
+			assertEquals("A", e.getPath1());
+			assertEquals("A/c", e.getPath2());
+		}
+
+		editor = dc.editor();
+		editor.add(new AddEdit("A.c"));
+		editor.add(new AddEdit("A/b/c/d").setReplace(false));
+		editor.add(new AddEdit("A/b/c"));
+		editor.add(new AddEdit("A0c"));
+		try {
+			editor.finish();
+			fail("Expected DirCacheNameConflictException to be thrown");
+		} catch (DirCacheNameConflictException e) {
+			assertEquals("A/b/c A/b/c/d", e.getMessage());
+			assertEquals("A/b/c", e.getPath1());
+			assertEquals("A/b/c/d", e.getPath2());
+		}
+	}
+
 	private static DirCacheEntry createEntry(String path, int stage) {
 		DirCacheEntry entry = new DirCacheEntry(path, stage);
 		entry.setFileMode(FileMode.REGULAR_FILE);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java
index b6649b3..524d0b8 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java
@@ -409,6 +409,7 @@ public void testCopyFileBare() throws Exception {
 					.append("<project path=\"foo\" name=\"").append(defaultUri)
 					.append("\" revision=\"").append(BRANCH).append("\" >")
 					.append("<copyfile src=\"hello.txt\" dest=\"Hello\" />")
+					.append("<copyfile src=\"hello.txt\" dest=\"foo/Hello\" />")
 					.append("</project>").append("</manifest>");
 			JGitTestUtil.writeTrashFile(tempDb, "manifest.xml",
 					xmlContent.toString());
@@ -423,8 +424,12 @@ public void testCopyFileBare() throws Exception {
 					.getRepository();
 			// The Hello file should exist
 			File hello = new File(localDb.getWorkTree(), "Hello");
-			localDb.close();
 			assertTrue("The Hello file should exist", hello.exists());
+			// The foo/Hello file should be skipped.
+			File foohello = new File(localDb.getWorkTree(), "foo/Hello");
+			assertFalse(
+					"The foo/Hello file should be skipped", foohello.exists());
+			localDb.close();
 			// The content of Hello file should be expected
 			BufferedReader reader = new BufferedReader(new FileReader(hello));
 			String content = reader.readLine();
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreNodeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreNodeTest.java
index 5893d8c..c026efc 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreNodeTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreNodeTest.java
@@ -55,7 +55,6 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 
-import org.eclipse.jgit.errors.CorruptObjectException;
 import org.eclipse.jgit.ignore.IgnoreNode.MatchResult;
 import org.eclipse.jgit.junit.RepositoryTestCase;
 import org.eclipse.jgit.lib.FileMode;
@@ -509,7 +508,7 @@ public void testToString() throws Exception {
 						.toString());
 	}
 
-	private void beginWalk() throws CorruptObjectException {
+	private void beginWalk() {
 		walk = new TreeWalk(db);
 		walk.addTree(new FileTreeIterator(db));
 	}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileRepositoryBuilderTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileRepositoryBuilderTest.java
index 2d72d23..dca3564 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileRepositoryBuilderTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/FileRepositoryBuilderTest.java
@@ -107,7 +107,7 @@ public void unknownRepositoryFormatVersion() throws Exception {
 		Repository r = createWorkRepository();
 		StoredConfig config = r.getConfig();
 		config.setLong(ConfigConstants.CONFIG_CORE_SECTION, null,
-				ConfigConstants.CONFIG_KEY_REPO_FORMAT_VERSION, 1);
+				ConfigConstants.CONFIG_KEY_REPO_FORMAT_VERSION, 999999);
 		config.save();
 
 		try {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java
index bc880a1..01d6ee6 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java
@@ -43,11 +43,13 @@
 
 package org.eclipse.jgit.internal.storage.file;
 
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.eclipse.jgit.internal.storage.pack.PackWriter.NONE;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
@@ -66,19 +68,19 @@
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.internal.storage.file.PackIndex.MutableEntry;
 import org.eclipse.jgit.internal.storage.pack.PackWriter;
-import org.eclipse.jgit.internal.storage.pack.PackWriter.ObjectIdSet;
 import org.eclipse.jgit.junit.JGitTestUtil;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.junit.TestRepository.BranchBuilder;
-import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdSet;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.revwalk.RevBlob;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.storage.pack.PackConfig;
+import org.eclipse.jgit.storage.pack.PackStatistics;
 import org.eclipse.jgit.test.resources.SampleDataRepositoryTestCase;
 import org.eclipse.jgit.transport.PackParser;
 import org.junit.After;
@@ -87,9 +89,6 @@
 
 public class PackWriterTest extends SampleDataRepositoryTestCase {
 
-	private static final Set<ObjectId> EMPTY_SET_OBJECT = Collections
-			.<ObjectId> emptySet();
-
 	private static final List<RevObject> EMPTY_LIST_REVS = Collections
 			.<RevObject> emptyList();
 
@@ -170,7 +169,7 @@ public void testModifySettings() {
 	 */
 	@Test
 	public void testWriteEmptyPack1() throws IOException {
-		createVerifyOpenPack(EMPTY_SET_OBJECT, EMPTY_SET_OBJECT, false, false);
+		createVerifyOpenPack(NONE, NONE, false, false);
 
 		assertEquals(0, writer.getObjectCount());
 		assertEquals(0, pack.getObjectCount());
@@ -203,8 +202,8 @@ public void testNotIgnoreNonExistingObjects() throws IOException {
 		final ObjectId nonExisting = ObjectId
 				.fromString("0000000000000000000000000000000000000001");
 		try {
-			createVerifyOpenPack(EMPTY_SET_OBJECT, Collections.singleton(
-					nonExisting), false, false);
+			createVerifyOpenPack(NONE, Collections.singleton(nonExisting),
+					false, false);
 			fail("Should have thrown MissingObjectException");
 		} catch (MissingObjectException x) {
 			// expected
@@ -220,8 +219,8 @@ public void testNotIgnoreNonExistingObjects() throws IOException {
 	public void testIgnoreNonExistingObjects() throws IOException {
 		final ObjectId nonExisting = ObjectId
 				.fromString("0000000000000000000000000000000000000001");
-		createVerifyOpenPack(EMPTY_SET_OBJECT, Collections.singleton(
-				nonExisting), false, true);
+		createVerifyOpenPack(NONE, Collections.singleton(nonExisting),
+				false, true);
 		// shouldn't throw anything
 	}
 
@@ -239,8 +238,8 @@ public void testIgnoreNonExistingObjectsWithBitmaps() throws IOException,
 		final ObjectId nonExisting = ObjectId
 				.fromString("0000000000000000000000000000000000000001");
 		new GC(db).gc();
-		createVerifyOpenPack(EMPTY_SET_OBJECT,
-				Collections.singleton(nonExisting), false, true, true);
+		createVerifyOpenPack(NONE, Collections.singleton(nonExisting), false,
+				true, true);
 		// shouldn't throw anything
 	}
 
@@ -438,6 +437,38 @@ public void testWritePack4SizeThinVsNoThin() throws Exception {
 	}
 
 	@Test
+	public void testDeltaStatistics() throws Exception {
+		config.setDeltaCompress(true);
+		FileRepository repo = createBareRepository();
+		TestRepository<FileRepository> testRepo = new TestRepository<FileRepository>(repo);
+		ArrayList<RevObject> blobs = new ArrayList<>();
+		blobs.add(testRepo.blob(genDeltableData(1000)));
+		blobs.add(testRepo.blob(genDeltableData(1005)));
+
+		try (PackWriter pw = new PackWriter(repo)) {
+			NullProgressMonitor m = NullProgressMonitor.INSTANCE;
+			pw.preparePack(blobs.iterator());
+			pw.writePack(m, m, os);
+			PackStatistics stats = pw.getStatistics();
+			assertEquals(1, stats.getTotalDeltas());
+			assertTrue("Delta bytes not set.",
+					stats.byObjectType(OBJ_BLOB).getDeltaBytes() > 0);
+		}
+	}
+
+	// Generate consistent junk data for building files that delta well
+	private String genDeltableData(int length) {
+		assertTrue("Generated data must have a length > 0", length > 0);
+		char[] data = {'a', 'b', 'c', '\n'};
+		StringBuilder builder = new StringBuilder(length);
+		for (int i = 0; i < length; i++) {
+			builder.append(data[i % 4]);
+		}
+		return builder.toString();
+	}
+
+
+	@Test
 	public void testWriteIndex() throws Exception {
 		config.setIndexVersion(2);
 		writeVerifyPack4(false);
@@ -494,7 +525,7 @@ public void testExclude() throws Exception {
 		RevCommit c2 = bb.commit().add("f", contentB).create();
 		testRepo.getRevWalk().parseHeaders(c2);
 		PackIndex pf2 = writePack(repo, Collections.singleton(c2),
-				Collections.singleton(objectIdSet(pf1)));
+				Collections.<ObjectIdSet> singleton(pf1));
 		assertContent(
 				pf2,
 				Arrays.asList(c2.getId(), c2.getTree().getId(),
@@ -519,8 +550,7 @@ private static PackIndex writePack(FileRepository repo,
 			pw.setReuseDeltaCommits(false);
 			for (ObjectIdSet idx : excludeObjects)
 				pw.excludeObjects(idx);
-			pw.preparePack(NullProgressMonitor.INSTANCE, want,
-					Collections.<ObjectId> emptySet());
+			pw.preparePack(NullProgressMonitor.INSTANCE, want, NONE);
 			String id = pw.computeName().getName();
 			File packdir = new File(repo.getObjectsDirectory(), "pack");
 			File packFile = new File(packdir, "pack-" + id + ".pack");
@@ -543,7 +573,7 @@ private void writeVerifyPack1() throws IOException {
 		final HashSet<ObjectId> interestings = new HashSet<ObjectId>();
 		interestings.add(ObjectId
 				.fromString("82c6b885ff600be425b4ea96dee75dca255b69e7"));
-		createVerifyOpenPack(interestings, EMPTY_SET_OBJECT, false, false);
+		createVerifyOpenPack(interestings, NONE, false, false);
 
 		final ObjectId expectedOrder[] = new ObjectId[] {
 				ObjectId.fromString("82c6b885ff600be425b4ea96dee75dca255b69e7"),
@@ -699,12 +729,4 @@ public int compare(MutableEntry o1, MutableEntry o2) {
 			assertEquals(objectsOrder[i++].toObjectId(), me.toObjectId());
 		}
 	}
-
-	private static ObjectIdSet objectIdSet(final PackIndex idx) {
-		return new ObjectIdSet() {
-			public boolean contains(AnyObjectId objectId) {
-				return idx.hasObject(objectId);
-			}
-		};
-	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java
index a92ff8d..f4d655f 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java
@@ -67,7 +67,6 @@
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileMode;
-import org.eclipse.jgit.lib.FileTreeEntry;
 import org.eclipse.jgit.lib.ObjectDatabase;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
@@ -75,7 +74,6 @@
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.TagBuilder;
-import org.eclipse.jgit.lib.Tree;
 import org.eclipse.jgit.lib.TreeFormatter;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevTag;
@@ -420,29 +418,6 @@ public void test009_CreateCommitOldFormat() throws IOException {
 	}
 
 	@Test
-	public void test012_SubtreeExternalSorting() throws IOException {
-		final ObjectId emptyBlob = insertEmptyBlob();
-		final Tree t = new Tree(db);
-		final FileTreeEntry e0 = t.addFile("a-");
-		final FileTreeEntry e1 = t.addFile("a-b");
-		final FileTreeEntry e2 = t.addFile("a/b");
-		final FileTreeEntry e3 = t.addFile("a=");
-		final FileTreeEntry e4 = t.addFile("a=b");
-
-		e0.setId(emptyBlob);
-		e1.setId(emptyBlob);
-		e2.setId(emptyBlob);
-		e3.setId(emptyBlob);
-		e4.setId(emptyBlob);
-
-		final Tree a = (Tree) t.findTreeMember("a");
-		a.setId(insertTree(a));
-		assertEquals(ObjectId
-				.fromString("b47a8f0a4190f7572e11212769090523e23eb1ea"),
-				insertTree(t));
-	}
-
-	@Test
 	public void test020_createBlobTag() throws IOException {
 		final ObjectId emptyId = insertEmptyBlob();
 		final TagBuilder t = new TagBuilder();
@@ -465,9 +440,8 @@ public void test020_createBlobTag() throws IOException {
 	@Test
 	public void test021_createTreeTag() throws IOException {
 		final ObjectId emptyId = insertEmptyBlob();
-		final Tree almostEmptyTree = new Tree(db);
-		almostEmptyTree.addEntry(new FileTreeEntry(almostEmptyTree, emptyId,
-				"empty".getBytes(), false));
+		TreeFormatter almostEmptyTree = new TreeFormatter();
+		almostEmptyTree.append("empty", FileMode.REGULAR_FILE, emptyId);
 		final ObjectId almostEmptyTreeId = insertTree(almostEmptyTree);
 		final TagBuilder t = new TagBuilder();
 		t.setObjectId(almostEmptyTreeId, Constants.OBJ_TREE);
@@ -489,9 +463,8 @@ public void test021_createTreeTag() throws IOException {
 	@Test
 	public void test022_createCommitTag() throws IOException {
 		final ObjectId emptyId = insertEmptyBlob();
-		final Tree almostEmptyTree = new Tree(db);
-		almostEmptyTree.addEntry(new FileTreeEntry(almostEmptyTree, emptyId,
-				"empty".getBytes(), false));
+		TreeFormatter almostEmptyTree = new TreeFormatter();
+		almostEmptyTree.append("empty", FileMode.REGULAR_FILE, emptyId);
 		final ObjectId almostEmptyTreeId = insertTree(almostEmptyTree);
 		final CommitBuilder almostEmptyCommit = new CommitBuilder();
 		almostEmptyCommit.setAuthor(new PersonIdent(author, 1154236443000L,
@@ -521,9 +494,8 @@ public void test022_createCommitTag() throws IOException {
 	@Test
 	public void test023_createCommitNonAnullii() throws IOException {
 		final ObjectId emptyId = insertEmptyBlob();
-		final Tree almostEmptyTree = new Tree(db);
-		almostEmptyTree.addEntry(new FileTreeEntry(almostEmptyTree, emptyId,
-				"empty".getBytes(), false));
+		TreeFormatter almostEmptyTree = new TreeFormatter();
+		almostEmptyTree.append("empty", FileMode.REGULAR_FILE, emptyId);
 		final ObjectId almostEmptyTreeId = insertTree(almostEmptyTree);
 		CommitBuilder commit = new CommitBuilder();
 		commit.setTreeId(almostEmptyTreeId);
@@ -543,9 +515,8 @@ public void test023_createCommitNonAnullii() throws IOException {
 	@Test
 	public void test024_createCommitNonAscii() throws IOException {
 		final ObjectId emptyId = insertEmptyBlob();
-		final Tree almostEmptyTree = new Tree(db);
-		almostEmptyTree.addEntry(new FileTreeEntry(almostEmptyTree, emptyId,
-				"empty".getBytes(), false));
+		TreeFormatter almostEmptyTree = new TreeFormatter();
+		almostEmptyTree.append("empty", FileMode.REGULAR_FILE, emptyId);
 		final ObjectId almostEmptyTreeId = insertTree(almostEmptyTree);
 		CommitBuilder commit = new CommitBuilder();
 		commit.setTreeId(almostEmptyTreeId);
@@ -747,14 +718,6 @@ private ObjectId insertEmptyBlob() throws IOException {
 		return emptyId;
 	}
 
-	private ObjectId insertTree(Tree tree) throws IOException {
-		try (ObjectInserter oi = db.newObjectInserter()) {
-			ObjectId id = oi.insert(Constants.OBJ_TREE, tree.format());
-			oi.flush();
-			return id;
-		}
-	}
-
 	private ObjectId insertTree(TreeFormatter tree) throws IOException {
 		try (ObjectInserter oi = db.newObjectInserter()) {
 			ObjectId id = oi.insert(tree);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/LocalDiskRefTreeDatabaseTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/LocalDiskRefTreeDatabaseTest.java
new file mode 100644
index 0000000..47f70d7
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/LocalDiskRefTreeDatabaseTest.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2016 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.reftree;
+
+import static org.eclipse.jgit.lib.Constants.HEAD;
+import static org.eclipse.jgit.lib.Constants.MASTER;
+import static org.eclipse.jgit.lib.Constants.ORIG_HEAD;
+import static org.eclipse.jgit.lib.Constants.R_HEADS;
+import static org.eclipse.jgit.lib.RefDatabase.ALL;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
+import org.junit.Before;
+import org.junit.Test;
+
+public class LocalDiskRefTreeDatabaseTest extends LocalDiskRepositoryTestCase {
+	private FileRepository repo;
+	private RefTreeDatabase refdb;
+	private RefDatabase bootstrap;
+
+	private TestRepository<FileRepository> testRepo;
+	private RevCommit A;
+	private RevCommit B;
+
+	@Before
+	public void setUp() throws Exception {
+		FileRepository init = createWorkRepository();
+		FileBasedConfig cfg = init.getConfig();
+		cfg.setInt("core", null, "repositoryformatversion", 1);
+		cfg.setString("extensions", null, "refsStorage", "reftree");
+		cfg.save();
+
+		repo = (FileRepository) new FileRepositoryBuilder()
+				.setGitDir(init.getDirectory())
+				.build();
+		refdb = (RefTreeDatabase) repo.getRefDatabase();
+		bootstrap = refdb.getBootstrap();
+		addRepoToClose(repo);
+
+		RefUpdate head = refdb.newUpdate(HEAD, true);
+		head.link(R_HEADS + MASTER);
+
+		testRepo = new TestRepository<>(init);
+		A = testRepo.commit().create();
+		B = testRepo.commit(testRepo.getRevWalk().parseCommit(A));
+	}
+
+	@Test
+	public void testHeadOrigHead() throws IOException {
+		RefUpdate master = refdb.newUpdate(HEAD, false);
+		master.setExpectedOldObjectId(ObjectId.zeroId());
+		master.setNewObjectId(A);
+		assertEquals(RefUpdate.Result.NEW, master.update());
+		assertEquals(A, refdb.exactRef(HEAD).getObjectId());
+
+		RefUpdate orig = refdb.newUpdate(ORIG_HEAD, true);
+		orig.setNewObjectId(B);
+		assertEquals(RefUpdate.Result.NEW, orig.update());
+
+		File origFile = new File(repo.getDirectory(), ORIG_HEAD);
+		assertEquals(B.name() + '\n', read(origFile));
+		assertEquals(B, bootstrap.exactRef(ORIG_HEAD).getObjectId());
+		assertEquals(B, refdb.exactRef(ORIG_HEAD).getObjectId());
+		assertFalse(refdb.getRefs(ALL).containsKey(ORIG_HEAD));
+
+		List<Ref> addl = refdb.getAdditionalRefs();
+		assertEquals(2, addl.size());
+		assertEquals(ORIG_HEAD, addl.get(1).getName());
+		assertEquals(B, addl.get(1).getObjectId());
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/RefTreeDatabaseTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/RefTreeDatabaseTest.java
new file mode 100644
index 0000000..e4d0f1d
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/RefTreeDatabaseTest.java
@@ -0,0 +1,718 @@
+/*
+ * Copyright (C) 2010, 2013, 2016 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.reftree;
+
+import static org.eclipse.jgit.lib.Constants.HEAD;
+import static org.eclipse.jgit.lib.Constants.ORIG_HEAD;
+import static org.eclipse.jgit.lib.Constants.R_HEADS;
+import static org.eclipse.jgit.lib.Constants.R_TAGS;
+import static org.eclipse.jgit.lib.Ref.Storage.LOOSE;
+import static org.eclipse.jgit.lib.Ref.Storage.PACKED;
+import static org.eclipse.jgit.lib.RefDatabase.ALL;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.LOCK_FAILURE;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_NONFASTFORWARD;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.SymbolicRef;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevTag;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.junit.Before;
+import org.junit.Test;
+
+public class RefTreeDatabaseTest {
+	private InMemRefTreeRepo repo;
+	private RefTreeDatabase refdb;
+	private RefDatabase bootstrap;
+
+	private TestRepository<InMemRefTreeRepo> testRepo;
+	private RevCommit A;
+	private RevCommit B;
+	private RevTag v1_0;
+
+	@Before
+	public void setUp() throws Exception {
+		repo = new InMemRefTreeRepo(new DfsRepositoryDescription("test"));
+		bootstrap = refdb.getBootstrap();
+
+		testRepo = new TestRepository<>(repo);
+		A = testRepo.commit().create();
+		B = testRepo.commit(testRepo.getRevWalk().parseCommit(A));
+		v1_0 = testRepo.tag("v1_0", B);
+		testRepo.getRevWalk().parseBody(v1_0);
+	}
+
+	@Test
+	public void testSupportsAtomic() {
+		assertTrue(refdb.performsAtomicTransactions());
+	}
+
+	@Test
+	public void testGetRefs_EmptyDatabase() throws IOException {
+		assertTrue("no references", refdb.getRefs(ALL).isEmpty());
+		assertTrue("no references", refdb.getRefs(R_HEADS).isEmpty());
+		assertTrue("no references", refdb.getRefs(R_TAGS).isEmpty());
+		assertTrue("no references", refdb.getAdditionalRefs().isEmpty());
+	}
+
+	@Test
+	public void testGetAdditionalRefs() throws IOException {
+		update("refs/heads/master", A);
+
+		List<Ref> addl = refdb.getAdditionalRefs();
+		assertEquals(1, addl.size());
+		assertEquals("refs/txn/committed", addl.get(0).getName());
+		assertEquals(getTxnCommitted(), addl.get(0).getObjectId());
+	}
+
+	@Test
+	public void testGetRefs_HeadOnOneBranch() throws IOException {
+		symref(HEAD, "refs/heads/master");
+		update("refs/heads/master", A);
+
+		Map<String, Ref> all = refdb.getRefs(ALL);
+		assertEquals(2, all.size());
+		assertTrue("has HEAD", all.containsKey(HEAD));
+		assertTrue("has master", all.containsKey("refs/heads/master"));
+
+		Ref head = all.get(HEAD);
+		Ref master = all.get("refs/heads/master");
+
+		assertEquals(HEAD, head.getName());
+		assertTrue(head.isSymbolic());
+		assertSame(LOOSE, head.getStorage());
+		assertSame("uses same ref as target", master, head.getTarget());
+
+		assertEquals("refs/heads/master", master.getName());
+		assertFalse(master.isSymbolic());
+		assertSame(PACKED, master.getStorage());
+		assertEquals(A, master.getObjectId());
+	}
+
+	@Test
+	public void testGetRefs_DetachedHead() throws IOException {
+		update(HEAD, A);
+
+		Map<String, Ref> all = refdb.getRefs(ALL);
+		assertEquals(1, all.size());
+		assertTrue("has HEAD", all.containsKey(HEAD));
+
+		Ref head = all.get(HEAD);
+		assertEquals(HEAD, head.getName());
+		assertFalse(head.isSymbolic());
+		assertSame(PACKED, head.getStorage());
+		assertEquals(A, head.getObjectId());
+	}
+
+	@Test
+	public void testGetRefs_DeeplyNestedBranch() throws IOException {
+		String name = "refs/heads/a/b/c/d/e/f/g/h/i/j/k";
+		update(name, A);
+
+		Map<String, Ref> all = refdb.getRefs(ALL);
+		assertEquals(1, all.size());
+
+		Ref r = all.get(name);
+		assertEquals(name, r.getName());
+		assertFalse(r.isSymbolic());
+		assertSame(PACKED, r.getStorage());
+		assertEquals(A, r.getObjectId());
+	}
+
+	@Test
+	public void testGetRefs_HeadBranchNotBorn() throws IOException {
+		update("refs/heads/A", A);
+		update("refs/heads/B", B);
+
+		Map<String, Ref> all = refdb.getRefs(ALL);
+		assertEquals(2, all.size());
+		assertFalse("no HEAD", all.containsKey(HEAD));
+
+		Ref a = all.get("refs/heads/A");
+		Ref b = all.get("refs/heads/B");
+
+		assertEquals(A, a.getObjectId());
+		assertEquals(B, b.getObjectId());
+
+		assertEquals("refs/heads/A", a.getName());
+		assertEquals("refs/heads/B", b.getName());
+	}
+
+	@Test
+	public void testGetRefs_HeadsOnly() throws IOException {
+		update("refs/heads/A", A);
+		update("refs/heads/B", B);
+		update("refs/tags/v1.0", v1_0);
+
+		Map<String, Ref> heads = refdb.getRefs(R_HEADS);
+		assertEquals(2, heads.size());
+
+		Ref a = heads.get("A");
+		Ref b = heads.get("B");
+
+		assertEquals("refs/heads/A", a.getName());
+		assertEquals("refs/heads/B", b.getName());
+
+		assertEquals(A, a.getObjectId());
+		assertEquals(B, b.getObjectId());
+	}
+
+	@Test
+	public void testGetRefs_TagsOnly() throws IOException {
+		update("refs/heads/A", A);
+		update("refs/heads/B", B);
+		update("refs/tags/v1.0", v1_0);
+
+		Map<String, Ref> tags = refdb.getRefs(R_TAGS);
+		assertEquals(1, tags.size());
+
+		Ref a = tags.get("v1.0");
+		assertEquals("refs/tags/v1.0", a.getName());
+		assertEquals(v1_0, a.getObjectId());
+		assertTrue(a.isPeeled());
+		assertEquals(v1_0.getObject(), a.getPeeledObjectId());
+	}
+
+	@Test
+	public void testGetRefs_HeadsSymref() throws IOException {
+		symref("refs/heads/other", "refs/heads/master");
+		update("refs/heads/master", A);
+
+		Map<String, Ref> heads = refdb.getRefs(R_HEADS);
+		assertEquals(2, heads.size());
+
+		Ref master = heads.get("master");
+		Ref other = heads.get("other");
+
+		assertEquals("refs/heads/master", master.getName());
+		assertEquals(A, master.getObjectId());
+
+		assertEquals("refs/heads/other", other.getName());
+		assertEquals(A, other.getObjectId());
+		assertSame(master, other.getTarget());
+	}
+
+	@Test
+	public void testGetRefs_InvalidPrefixes() throws IOException {
+		update("refs/heads/A", A);
+
+		assertTrue("empty refs/heads", refdb.getRefs("refs/heads").isEmpty());
+		assertTrue("empty objects", refdb.getRefs("objects").isEmpty());
+		assertTrue("empty objects/", refdb.getRefs("objects/").isEmpty());
+	}
+
+	@Test
+	public void testGetRefs_DiscoversNew() throws IOException {
+		update("refs/heads/master", A);
+		Map<String, Ref> orig = refdb.getRefs(ALL);
+
+		update("refs/heads/next", B);
+		Map<String, Ref> next = refdb.getRefs(ALL);
+
+		assertEquals(1, orig.size());
+		assertEquals(2, next.size());
+
+		assertFalse(orig.containsKey("refs/heads/next"));
+		assertTrue(next.containsKey("refs/heads/next"));
+
+		assertEquals(A, next.get("refs/heads/master").getObjectId());
+		assertEquals(B, next.get("refs/heads/next").getObjectId());
+	}
+
+	@Test
+	public void testGetRefs_DiscoversModified() throws IOException {
+		symref(HEAD, "refs/heads/master");
+		update("refs/heads/master", A);
+
+		Map<String, Ref> all = refdb.getRefs(ALL);
+		assertEquals(A, all.get(HEAD).getObjectId());
+
+		update("refs/heads/master", B);
+		all = refdb.getRefs(ALL);
+		assertEquals(B, all.get(HEAD).getObjectId());
+		assertEquals(B, refdb.exactRef(HEAD).getObjectId());
+	}
+
+	@Test
+	public void testGetRefs_CycleInSymbolicRef() throws IOException {
+		symref("refs/1", "refs/2");
+		symref("refs/2", "refs/3");
+		symref("refs/3", "refs/4");
+		symref("refs/4", "refs/5");
+		symref("refs/5", "refs/end");
+		update("refs/end", A);
+
+		Map<String, Ref> all = refdb.getRefs(ALL);
+		Ref r = all.get("refs/1");
+		assertNotNull("has 1", r);
+
+		assertEquals("refs/1", r.getName());
+		assertEquals(A, r.getObjectId());
+		assertTrue(r.isSymbolic());
+
+		r = r.getTarget();
+		assertEquals("refs/2", r.getName());
+		assertEquals(A, r.getObjectId());
+		assertTrue(r.isSymbolic());
+
+		r = r.getTarget();
+		assertEquals("refs/3", r.getName());
+		assertEquals(A, r.getObjectId());
+		assertTrue(r.isSymbolic());
+
+		r = r.getTarget();
+		assertEquals("refs/4", r.getName());
+		assertEquals(A, r.getObjectId());
+		assertTrue(r.isSymbolic());
+
+		r = r.getTarget();
+		assertEquals("refs/5", r.getName());
+		assertEquals(A, r.getObjectId());
+		assertTrue(r.isSymbolic());
+
+		r = r.getTarget();
+		assertEquals("refs/end", r.getName());
+		assertEquals(A, r.getObjectId());
+		assertFalse(r.isSymbolic());
+
+		symref("refs/5", "refs/6");
+		symref("refs/6", "refs/end");
+		all = refdb.getRefs(ALL);
+		assertNull("mising 1 due to cycle", all.get("refs/1"));
+		assertEquals(A, all.get("refs/2").getObjectId());
+		assertEquals(A, all.get("refs/3").getObjectId());
+		assertEquals(A, all.get("refs/4").getObjectId());
+		assertEquals(A, all.get("refs/5").getObjectId());
+		assertEquals(A, all.get("refs/6").getObjectId());
+		assertEquals(A, all.get("refs/end").getObjectId());
+	}
+
+	@Test
+	public void testGetRef_NonExistingBranchConfig() throws IOException {
+		assertNull("find branch config", refdb.getRef("config"));
+		assertNull("find branch config", refdb.getRef("refs/heads/config"));
+	}
+
+	@Test
+	public void testGetRef_FindBranchConfig() throws IOException {
+		update("refs/heads/config", A);
+
+		for (String t : new String[] { "config", "refs/heads/config" }) {
+			Ref r = refdb.getRef(t);
+			assertNotNull("find branch config (" + t + ")", r);
+			assertEquals("for " + t, "refs/heads/config", r.getName());
+			assertEquals("for " + t, A, r.getObjectId());
+		}
+	}
+
+	@Test
+	public void testFirstExactRef() throws IOException {
+		update("refs/heads/A", A);
+		update("refs/tags/v1.0", v1_0);
+
+		Ref a = refdb.firstExactRef("refs/heads/A", "refs/tags/v1.0");
+		Ref one = refdb.firstExactRef("refs/tags/v1.0", "refs/heads/A");
+
+		assertEquals("refs/heads/A", a.getName());
+		assertEquals("refs/tags/v1.0", one.getName());
+
+		assertEquals(A, a.getObjectId());
+		assertEquals(v1_0, one.getObjectId());
+	}
+
+	@Test
+	public void testExactRef_DiscoversModified() throws IOException {
+		symref(HEAD, "refs/heads/master");
+		update("refs/heads/master", A);
+		assertEquals(A, refdb.exactRef(HEAD).getObjectId());
+
+		update("refs/heads/master", B);
+		assertEquals(B, refdb.exactRef(HEAD).getObjectId());
+	}
+
+	@Test
+	public void testIsNameConflicting() throws IOException {
+		update("refs/heads/a/b", A);
+		update("refs/heads/q", B);
+
+		// new references cannot replace an existing container
+		assertTrue(refdb.isNameConflicting("refs"));
+		assertTrue(refdb.isNameConflicting("refs/heads"));
+		assertTrue(refdb.isNameConflicting("refs/heads/a"));
+
+		// existing reference is not conflicting
+		assertFalse(refdb.isNameConflicting("refs/heads/a/b"));
+
+		// new references are not conflicting
+		assertFalse(refdb.isNameConflicting("refs/heads/a/d"));
+		assertFalse(refdb.isNameConflicting("refs/heads/master"));
+
+		// existing reference must not be used as a container
+		assertTrue(refdb.isNameConflicting("refs/heads/a/b/c"));
+		assertTrue(refdb.isNameConflicting("refs/heads/q/master"));
+
+		// refs/txn/ names always conflict.
+		assertTrue(refdb.isNameConflicting(refdb.getTxnCommitted()));
+		assertTrue(refdb.isNameConflicting("refs/txn/foo"));
+	}
+
+	@Test
+	public void testUpdate_RefusesRefsTxnNamespace() throws IOException {
+		ObjectId txnId = getTxnCommitted();
+
+		RefUpdate u = refdb.newUpdate("refs/txn/tmp", false);
+		u.setNewObjectId(B);
+		assertEquals(RefUpdate.Result.LOCK_FAILURE, u.update());
+		assertEquals(txnId, getTxnCommitted());
+
+		ReceiveCommand cmd = command(null, B, "refs/txn/tmp");
+		BatchRefUpdate batch = refdb.newBatchUpdate();
+		batch.addCommand(cmd);
+		batch.execute(new RevWalk(repo), NullProgressMonitor.INSTANCE);
+
+		assertEquals(REJECTED_OTHER_REASON, cmd.getResult());
+		assertEquals(MessageFormat.format(JGitText.get().invalidRefName,
+				"refs/txn/tmp"), cmd.getMessage());
+		assertEquals(txnId, getTxnCommitted());
+	}
+
+	@Test
+	public void testUpdate_RefusesDotLockInRefName() throws IOException {
+		ObjectId txnId = getTxnCommitted();
+
+		RefUpdate u = refdb.newUpdate("refs/heads/pu.lock", false);
+		u.setNewObjectId(B);
+		assertEquals(RefUpdate.Result.REJECTED, u.update());
+		assertEquals(txnId, getTxnCommitted());
+
+		ReceiveCommand cmd = command(null, B, "refs/heads/pu.lock");
+		BatchRefUpdate batch = refdb.newBatchUpdate();
+		batch.addCommand(cmd);
+		batch.execute(new RevWalk(repo), NullProgressMonitor.INSTANCE);
+
+		assertEquals(REJECTED_OTHER_REASON, cmd.getResult());
+		assertEquals(JGitText.get().funnyRefname, cmd.getMessage());
+		assertEquals(txnId, getTxnCommitted());
+	}
+
+	@Test
+	public void testUpdate_RefusesOrigHeadOnBare() throws IOException {
+		assertTrue(refdb.getRepository().isBare());
+		ObjectId txnId = getTxnCommitted();
+
+		RefUpdate orig = refdb.newUpdate(ORIG_HEAD, true);
+		orig.setNewObjectId(B);
+		assertEquals(RefUpdate.Result.LOCK_FAILURE, orig.update());
+		assertEquals(txnId, getTxnCommitted());
+
+		ReceiveCommand cmd = command(null, B, ORIG_HEAD);
+		BatchRefUpdate batch = refdb.newBatchUpdate();
+		batch.addCommand(cmd);
+		batch.execute(new RevWalk(repo), NullProgressMonitor.INSTANCE);
+		assertEquals(REJECTED_OTHER_REASON, cmd.getResult());
+		assertEquals(
+				MessageFormat.format(JGitText.get().invalidRefName, ORIG_HEAD),
+				cmd.getMessage());
+		assertEquals(txnId, getTxnCommitted());
+	}
+
+	@Test
+	public void testBatchRefUpdate_NonFastForwardAborts() throws IOException {
+		update("refs/heads/master", A);
+		update("refs/heads/masters", B);
+		ObjectId txnId = getTxnCommitted();
+
+		List<ReceiveCommand> commands = Arrays.asList(
+				command(A, B, "refs/heads/master"),
+				command(B, A, "refs/heads/masters"));
+		BatchRefUpdate batchUpdate = refdb.newBatchUpdate();
+		batchUpdate.addCommand(commands);
+		batchUpdate.execute(new RevWalk(repo), NullProgressMonitor.INSTANCE);
+		assertEquals(txnId, getTxnCommitted());
+
+		assertEquals(REJECTED_NONFASTFORWARD,
+				commands.get(1).getResult());
+		assertEquals(REJECTED_OTHER_REASON,
+				commands.get(0).getResult());
+		assertEquals(JGitText.get().transactionAborted,
+				commands.get(0).getMessage());
+	}
+
+	@Test
+	public void testBatchRefUpdate_ForceUpdate() throws IOException {
+		update("refs/heads/master", A);
+		update("refs/heads/masters", B);
+		ObjectId txnId = getTxnCommitted();
+
+		List<ReceiveCommand> commands = Arrays.asList(
+				command(A, B, "refs/heads/master"),
+				command(B, A, "refs/heads/masters"));
+		BatchRefUpdate batchUpdate = refdb.newBatchUpdate();
+		batchUpdate.setAllowNonFastForwards(true);
+		batchUpdate.addCommand(commands);
+		batchUpdate.execute(new RevWalk(repo), NullProgressMonitor.INSTANCE);
+		assertNotEquals(txnId, getTxnCommitted());
+
+		Map<String, Ref> refs = refdb.getRefs(ALL);
+		assertEquals(OK, commands.get(0).getResult());
+		assertEquals(OK, commands.get(1).getResult());
+		assertEquals(
+				"[refs/heads/master, refs/heads/masters]",
+				refs.keySet().toString());
+		assertEquals(B.getId(), refs.get("refs/heads/master").getObjectId());
+		assertEquals(A.getId(), refs.get("refs/heads/masters").getObjectId());
+	}
+
+	@Test
+	public void testBatchRefUpdate_NonFastForwardDoesNotDoExpensiveMergeCheck()
+			throws IOException {
+		update("refs/heads/master", B);
+		ObjectId txnId = getTxnCommitted();
+
+		List<ReceiveCommand> commands = Arrays.asList(
+				command(B, A, "refs/heads/master"));
+		BatchRefUpdate batchUpdate = refdb.newBatchUpdate();
+		batchUpdate.setAllowNonFastForwards(true);
+		batchUpdate.addCommand(commands);
+		batchUpdate.execute(new RevWalk(repo) {
+			@Override
+			public boolean isMergedInto(RevCommit base, RevCommit tip) {
+				fail("isMergedInto() should not be called");
+				return false;
+			}
+		}, NullProgressMonitor.INSTANCE);
+		assertNotEquals(txnId, getTxnCommitted());
+
+		Map<String, Ref> refs = refdb.getRefs(ALL);
+		assertEquals(OK, commands.get(0).getResult());
+		assertEquals(A.getId(), refs.get("refs/heads/master").getObjectId());
+	}
+
+	@Test
+	public void testBatchRefUpdate_ConflictCausesAbort() throws IOException {
+		update("refs/heads/master", A);
+		update("refs/heads/masters", B);
+		ObjectId txnId = getTxnCommitted();
+
+		List<ReceiveCommand> commands = Arrays.asList(
+				command(A, B, "refs/heads/master"),
+				command(null, A, "refs/heads/master/x"),
+				command(null, A, "refs/heads"));
+		BatchRefUpdate batchUpdate = refdb.newBatchUpdate();
+		batchUpdate.setAllowNonFastForwards(true);
+		batchUpdate.addCommand(commands);
+		batchUpdate.execute(new RevWalk(repo), NullProgressMonitor.INSTANCE);
+		assertEquals(txnId, getTxnCommitted());
+
+		assertEquals(LOCK_FAILURE, commands.get(0).getResult());
+
+		assertEquals(REJECTED_OTHER_REASON, commands.get(1).getResult());
+		assertEquals(JGitText.get().transactionAborted,
+				commands.get(1).getMessage());
+
+		assertEquals(REJECTED_OTHER_REASON, commands.get(2).getResult());
+		assertEquals(JGitText.get().transactionAborted,
+				commands.get(2).getMessage());
+	}
+
+	@Test
+	public void testBatchRefUpdate_NoConflictIfDeleted() throws IOException {
+		update("refs/heads/master", A);
+		update("refs/heads/masters", B);
+		ObjectId txnId = getTxnCommitted();
+
+		List<ReceiveCommand> commands = Arrays.asList(
+				command(A, B, "refs/heads/master"),
+				command(null, A, "refs/heads/masters/x"),
+				command(B, null, "refs/heads/masters"));
+		BatchRefUpdate batchUpdate = refdb.newBatchUpdate();
+		batchUpdate.setAllowNonFastForwards(true);
+		batchUpdate.addCommand(commands);
+		batchUpdate.execute(new RevWalk(repo), NullProgressMonitor.INSTANCE);
+		assertNotEquals(txnId, getTxnCommitted());
+
+		assertEquals(OK, commands.get(0).getResult());
+		assertEquals(OK, commands.get(1).getResult());
+		assertEquals(OK, commands.get(2).getResult());
+
+		Map<String, Ref> refs = refdb.getRefs(ALL);
+		assertEquals(
+				"[refs/heads/master, refs/heads/masters/x]",
+				refs.keySet().toString());
+		assertEquals(A.getId(), refs.get("refs/heads/masters/x").getObjectId());
+	}
+
+	private ObjectId getTxnCommitted() throws IOException {
+		Ref r = bootstrap.exactRef(refdb.getTxnCommitted());
+		if (r != null && r.getObjectId() != null) {
+			return r.getObjectId();
+		}
+		return ObjectId.zeroId();
+	}
+
+	private static ReceiveCommand command(AnyObjectId a, AnyObjectId b,
+			String name) {
+		return new ReceiveCommand(
+				a != null ? a.copy() : ObjectId.zeroId(),
+				b != null ? b.copy() : ObjectId.zeroId(),
+				name);
+	}
+
+	private void symref(final String name, final String dst)
+			throws IOException {
+		commit(new Function() {
+			@Override
+			public boolean apply(ObjectReader reader, RefTree tree)
+					throws IOException {
+				Ref old = tree.exactRef(reader, name);
+				Command n = new Command(
+					old,
+					new SymbolicRef(
+						name,
+						new ObjectIdRef.Unpeeled(Ref.Storage.NEW, dst, null)));
+				return tree.apply(Collections.singleton(n));
+			}
+		});
+	}
+
+	private void update(final String name, final ObjectId id)
+			throws IOException {
+		commit(new Function() {
+			@Override
+			public boolean apply(ObjectReader reader, RefTree tree)
+					throws IOException {
+				Ref old = tree.exactRef(reader, name);
+				Command n;
+				try (RevWalk rw = new RevWalk(repo)) {
+					n = new Command(old, Command.toRef(rw, id, name, true));
+				}
+				return tree.apply(Collections.singleton(n));
+			}
+		});
+	}
+
+	interface Function {
+		boolean apply(ObjectReader reader, RefTree tree) throws IOException;
+	}
+
+	private void commit(Function fun) throws IOException {
+		try (ObjectReader reader = repo.newObjectReader();
+				ObjectInserter inserter = repo.newObjectInserter();
+				RevWalk rw = new RevWalk(reader)) {
+			RefUpdate u = bootstrap.newUpdate(refdb.getTxnCommitted(), false);
+			CommitBuilder cb = new CommitBuilder();
+			testRepo.setAuthorAndCommitter(cb);
+
+			Ref ref = bootstrap.exactRef(refdb.getTxnCommitted());
+			RefTree tree;
+			if (ref != null && ref.getObjectId() != null) {
+				tree = RefTree.read(reader, rw.parseTree(ref.getObjectId()));
+				cb.setParentId(ref.getObjectId());
+				u.setExpectedOldObjectId(ref.getObjectId());
+			} else {
+				tree = RefTree.newEmptyTree();
+				u.setExpectedOldObjectId(ObjectId.zeroId());
+			}
+
+			assertTrue(fun.apply(reader, tree));
+			cb.setTreeId(tree.writeTree(inserter));
+			u.setNewObjectId(inserter.insert(cb));
+			inserter.flush();
+			switch (u.update(rw)) {
+			case NEW:
+			case FAST_FORWARD:
+				break;
+			default:
+				fail("Expected " + u.getName() + " to update");
+			}
+		}
+	}
+
+	private class InMemRefTreeRepo extends InMemoryRepository {
+		private final RefTreeDatabase refs;
+
+		InMemRefTreeRepo(DfsRepositoryDescription repoDesc) {
+			super(repoDesc);
+			refs = new RefTreeDatabase(this, super.getRefDatabase(),
+					"refs/txn/committed");
+			RefTreeDatabaseTest.this.refdb = refs;
+		}
+
+		public RefDatabase getRefDatabase() {
+			return refs;
+		}
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/RefTreeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/RefTreeTest.java
new file mode 100644
index 0000000..8e0f38c
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/reftree/RefTreeTest.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright (C) 2016, 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.reftree;
+
+import static org.eclipse.jgit.lib.Constants.HEAD;
+import static org.eclipse.jgit.lib.Constants.R_HEADS;
+import static org.eclipse.jgit.lib.Constants.R_TAGS;
+import static org.eclipse.jgit.lib.Ref.Storage.LOOSE;
+import static org.eclipse.jgit.lib.Ref.Storage.NEW;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.LOCK_FAILURE;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.SymbolicRef;
+import org.eclipse.jgit.revwalk.RevBlob;
+import org.eclipse.jgit.revwalk.RevTag;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.junit.Before;
+import org.junit.Test;
+
+public class RefTreeTest {
+	private static final String R_MASTER = R_HEADS + "master";
+	private InMemoryRepository repo;
+	private TestRepository<InMemoryRepository> git;
+
+	@Before
+	public void setUp() throws IOException {
+		repo = new InMemoryRepository(new DfsRepositoryDescription("RefTree"));
+		git = new TestRepository<>(repo);
+	}
+
+	@Test
+	public void testEmptyTree() throws IOException {
+		RefTree tree = RefTree.newEmptyTree();
+		try (ObjectReader reader = repo.newObjectReader()) {
+			assertNull(HEAD, tree.exactRef(reader, HEAD));
+			assertNull("master", tree.exactRef(reader, R_MASTER));
+		}
+	}
+
+	@Test
+	public void testApplyThenReadMaster() throws Exception {
+		RefTree tree = RefTree.newEmptyTree();
+		RevBlob id = git.blob("A");
+		Command cmd = new Command(null, ref(R_MASTER, id));
+		assertTrue(tree.apply(Collections.singletonList(cmd)));
+		assertSame(NOT_ATTEMPTED, cmd.getResult());
+
+		try (ObjectReader reader = repo.newObjectReader()) {
+			Ref m = tree.exactRef(reader, R_MASTER);
+			assertNotNull(R_MASTER, m);
+			assertEquals(R_MASTER, m.getName());
+			assertEquals(id, m.getObjectId());
+			assertTrue("peeled", m.isPeeled());
+		}
+	}
+
+	@Test
+	public void testUpdateMaster() throws Exception {
+		RefTree tree = RefTree.newEmptyTree();
+		RevBlob id1 = git.blob("A");
+		Command cmd1 = new Command(null, ref(R_MASTER, id1));
+		assertTrue(tree.apply(Collections.singletonList(cmd1)));
+		assertSame(NOT_ATTEMPTED, cmd1.getResult());
+
+		RevBlob id2 = git.blob("B");
+		Command cmd2 = new Command(ref(R_MASTER, id1), ref(R_MASTER, id2));
+		assertTrue(tree.apply(Collections.singletonList(cmd2)));
+		assertSame(NOT_ATTEMPTED, cmd2.getResult());
+
+		try (ObjectReader reader = repo.newObjectReader()) {
+			Ref m = tree.exactRef(reader, R_MASTER);
+			assertNotNull(R_MASTER, m);
+			assertEquals(R_MASTER, m.getName());
+			assertEquals(id2, m.getObjectId());
+			assertTrue("peeled", m.isPeeled());
+		}
+	}
+
+	@Test
+	public void testHeadSymref() throws Exception {
+		RefTree tree = RefTree.newEmptyTree();
+		RevBlob id = git.blob("A");
+		Command cmd1 = new Command(null, ref(R_MASTER, id));
+		Command cmd2 = new Command(null, symref(HEAD, R_MASTER));
+		assertTrue(tree.apply(Arrays.asList(new Command[] { cmd1, cmd2 })));
+		assertSame(NOT_ATTEMPTED, cmd1.getResult());
+		assertSame(NOT_ATTEMPTED, cmd2.getResult());
+
+		try (ObjectReader reader = repo.newObjectReader()) {
+			Ref m = tree.exactRef(reader, HEAD);
+			assertNotNull(HEAD, m);
+			assertEquals(HEAD, m.getName());
+			assertTrue("symbolic", m.isSymbolic());
+			assertNotNull(m.getTarget());
+			assertEquals(R_MASTER, m.getTarget().getName());
+			assertEquals(id, m.getTarget().getObjectId());
+		}
+
+		// Writing flushes some buffers, re-read from blob.
+		ObjectId newId = write(tree);
+		try (ObjectReader reader = repo.newObjectReader();
+				RevWalk rw = new RevWalk(reader)) {
+			tree = RefTree.read(reader, rw.parseTree(newId));
+			Ref m = tree.exactRef(reader, HEAD);
+			assertEquals(R_MASTER, m.getTarget().getName());
+		}
+	}
+
+	@Test
+	public void testTagIsPeeled() throws Exception {
+		String name = "v1.0";
+		RefTree tree = RefTree.newEmptyTree();
+		RevBlob id = git.blob("A");
+		RevTag tag = git.tag(name, id);
+
+		String ref = R_TAGS + name;
+		Command cmd = create(ref, tag);
+		assertTrue(tree.apply(Collections.singletonList(cmd)));
+		assertSame(NOT_ATTEMPTED, cmd.getResult());
+
+		try (ObjectReader reader = repo.newObjectReader()) {
+			Ref m = tree.exactRef(reader, ref);
+			assertNotNull(ref, m);
+			assertEquals(ref, m.getName());
+			assertEquals(tag, m.getObjectId());
+			assertTrue("peeled", m.isPeeled());
+			assertEquals(id, m.getPeeledObjectId());
+		}
+	}
+
+	@Test
+	public void testApplyAlreadyExists() throws Exception {
+		RefTree tree = RefTree.newEmptyTree();
+		RevBlob a = git.blob("A");
+		Command cmd = new Command(null, ref(R_MASTER, a));
+		assertTrue(tree.apply(Collections.singletonList(cmd)));
+		ObjectId treeId = write(tree);
+
+		RevBlob b = git.blob("B");
+		Command cmd1 = create(R_MASTER, b);
+		Command cmd2 = create(R_MASTER, b);
+		assertFalse(tree.apply(Arrays.asList(new Command[] { cmd1, cmd2 })));
+		assertSame(LOCK_FAILURE, cmd1.getResult());
+		assertSame(REJECTED_OTHER_REASON, cmd2.getResult());
+		assertEquals(JGitText.get().transactionAborted, cmd2.getMessage());
+		assertEquals(treeId, write(tree));
+	}
+
+	@Test
+	public void testApplyWrongOldId() throws Exception {
+		RefTree tree = RefTree.newEmptyTree();
+		RevBlob a = git.blob("A");
+		Command cmd = new Command(null, ref(R_MASTER, a));
+		assertTrue(tree.apply(Collections.singletonList(cmd)));
+		ObjectId treeId = write(tree);
+
+		RevBlob b = git.blob("B");
+		RevBlob c = git.blob("C");
+		Command cmd1 = update(R_MASTER, b, c);
+		Command cmd2 = create(R_MASTER, b);
+		assertFalse(tree.apply(Arrays.asList(new Command[] { cmd1, cmd2 })));
+		assertSame(LOCK_FAILURE, cmd1.getResult());
+		assertSame(REJECTED_OTHER_REASON, cmd2.getResult());
+		assertEquals(JGitText.get().transactionAborted, cmd2.getMessage());
+		assertEquals(treeId, write(tree));
+	}
+
+	@Test
+	public void testApplyWrongOldIdButAlreadyCurrentIsNoOp() throws Exception {
+		RefTree tree = RefTree.newEmptyTree();
+		RevBlob a = git.blob("A");
+		Command cmd = new Command(null, ref(R_MASTER, a));
+		assertTrue(tree.apply(Collections.singletonList(cmd)));
+		ObjectId treeId = write(tree);
+
+		RevBlob b = git.blob("B");
+		cmd = update(R_MASTER, b, a);
+		assertTrue(tree.apply(Collections.singletonList(cmd)));
+		assertEquals(treeId, write(tree));
+	}
+
+	@Test
+	public void testApplyCannotCreateSubdirectory() throws Exception {
+		RefTree tree = RefTree.newEmptyTree();
+		RevBlob a = git.blob("A");
+		Command cmd = new Command(null, ref(R_MASTER, a));
+		assertTrue(tree.apply(Collections.singletonList(cmd)));
+		ObjectId treeId = write(tree);
+
+		RevBlob b = git.blob("B");
+		Command cmd1 = create(R_MASTER + "/fail", b);
+		assertFalse(tree.apply(Collections.singletonList(cmd1)));
+		assertSame(LOCK_FAILURE, cmd1.getResult());
+		assertEquals(treeId, write(tree));
+	}
+
+	@Test
+	public void testApplyCannotCreateParentRef() throws Exception {
+		RefTree tree = RefTree.newEmptyTree();
+		RevBlob a = git.blob("A");
+		Command cmd = new Command(null, ref(R_MASTER, a));
+		assertTrue(tree.apply(Collections.singletonList(cmd)));
+		ObjectId treeId = write(tree);
+
+		RevBlob b = git.blob("B");
+		Command cmd1 = create("refs/heads", b);
+		assertFalse(tree.apply(Collections.singletonList(cmd1)));
+		assertSame(LOCK_FAILURE, cmd1.getResult());
+		assertEquals(treeId, write(tree));
+	}
+
+	private static Ref ref(String name, ObjectId id) {
+		return new ObjectIdRef.PeeledNonTag(LOOSE, name, id);
+	}
+
+	private static Ref symref(String name, String dest) {
+		Ref d = new ObjectIdRef.PeeledNonTag(NEW, dest, null);
+		return new SymbolicRef(name, d);
+	}
+
+	private Command create(String name, ObjectId id)
+			throws MissingObjectException, IOException {
+		return update(name, ObjectId.zeroId(), id);
+	}
+
+	private Command update(String name, ObjectId oldId, ObjectId newId)
+			throws MissingObjectException, IOException {
+		try (RevWalk rw = new RevWalk(repo)) {
+			return new Command(rw, new ReceiveCommand(oldId, newId, name));
+		}
+	}
+
+	private ObjectId write(RefTree tree) throws IOException {
+		try (ObjectInserter ins = repo.newObjectInserter()) {
+			ObjectId id = tree.writeTree(ins);
+			ins.flush();
+			return id;
+		}
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutTest.java
index d768e0f..92901f8 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutTest.java
@@ -1084,7 +1084,7 @@ public void testCheckoutChangeLinkToNonEmptyDirsAndNewIndexEntry()
 		assertWorkDir(mkmap(linkName, "a", fname, "a"));
 
 		Status st = git.status().call();
-		assertFalse(st.isClean());
+		assertTrue(st.isClean());
 	}
 
 	@Test
@@ -1213,9 +1213,7 @@ public void testCheckoutChangeFileToNonEmptyDirsAndNewIndexEntry()
 		assertWorkDir(mkmap(fname, "a"));
 
 		Status st = git.status().call();
-		assertFalse(st.isClean());
-		assertEquals(1, st.getAdded().size());
-		assertTrue(st.getAdded().contains(fname + "/dir/file1"));
+		assertTrue(st.isClean());
 	}
 
 	@Test
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexDiffTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexDiffTest.java
index a5cd7b5..7fcee3d 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexDiffTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/IndexDiffTest.java
@@ -99,8 +99,7 @@ public void apply(DirCacheEntry ent) {
 	public void testAdded() throws IOException {
 		writeTrashFile("file1", "file1");
 		writeTrashFile("dir/subfile", "dir/subfile");
-		Tree tree = new Tree(db);
-		tree.setId(insertTree(tree));
+		ObjectId tree = insertTree(new TreeFormatter());
 
 		DirCache index = db.lockDirCache();
 		DirCacheEditor editor = index.editor();
@@ -108,7 +107,7 @@ public void testAdded() throws IOException {
 		editor.add(add(db, trash, "dir/subfile"));
 		editor.commit();
 		FileTreeIterator iterator = new FileTreeIterator(db);
-		IndexDiff diff = new IndexDiff(db, tree.getId(), iterator);
+		IndexDiff diff = new IndexDiff(db, tree, iterator);
 		diff.diff();
 		assertEquals(2, diff.getAdded().size());
 		assertTrue(diff.getAdded().contains("file1"));
@@ -124,18 +123,16 @@ public void testRemoved() throws IOException {
 		writeTrashFile("file2", "file2");
 		writeTrashFile("dir/file3", "dir/file3");
 
-		Tree tree = new Tree(db);
-		tree.addFile("file2");
-		tree.addFile("dir/file3");
-		assertEquals(2, tree.memberCount());
-		tree.findBlobMember("file2").setId(ObjectId.fromString("30d67d4672d5c05833b7192cc77a79eaafb5c7ad"));
-		Tree tree2 = (Tree) tree.findTreeMember("dir");
-		tree2.findBlobMember("file3").setId(ObjectId.fromString("873fb8d667d05436d728c52b1d7a09528e6eb59b"));
-		tree2.setId(insertTree(tree2));
-		tree.setId(insertTree(tree));
+		TreeFormatter dir = new TreeFormatter();
+		dir.append("file3", FileMode.REGULAR_FILE, ObjectId.fromString("873fb8d667d05436d728c52b1d7a09528e6eb59b"));
+
+		TreeFormatter tree = new TreeFormatter();
+		tree.append("file2", FileMode.REGULAR_FILE, ObjectId.fromString("30d67d4672d5c05833b7192cc77a79eaafb5c7ad"));
+		tree.append("dir", FileMode.TREE, insertTree(dir));
+		ObjectId treeId = insertTree(tree);
 
 		FileTreeIterator iterator = new FileTreeIterator(db);
-		IndexDiff diff = new IndexDiff(db, tree.getId(), iterator);
+		IndexDiff diff = new IndexDiff(db, treeId, iterator);
 		diff.diff();
 		assertEquals(2, diff.getRemoved().size());
 		assertTrue(diff.getRemoved().contains("file2"));
@@ -157,16 +154,16 @@ public void testModified() throws IOException, GitAPIException {
 
 		writeTrashFile("dir/file3", "changed");
 
-		Tree tree = new Tree(db);
-		tree.addFile("file2").setId(ObjectId.fromString("0123456789012345678901234567890123456789"));
-		tree.addFile("dir/file3").setId(ObjectId.fromString("0123456789012345678901234567890123456789"));
-		assertEquals(2, tree.memberCount());
+		TreeFormatter dir = new TreeFormatter();
+		dir.append("file3", FileMode.REGULAR_FILE, ObjectId.fromString("0123456789012345678901234567890123456789"));
 
-		Tree tree2 = (Tree) tree.findTreeMember("dir");
-		tree2.setId(insertTree(tree2));
-		tree.setId(insertTree(tree));
+		TreeFormatter tree = new TreeFormatter();
+		tree.append("dir", FileMode.TREE, insertTree(dir));
+		tree.append("file2", FileMode.REGULAR_FILE, ObjectId.fromString("0123456789012345678901234567890123456789"));
+		ObjectId treeId = insertTree(tree);
+
 		FileTreeIterator iterator = new FileTreeIterator(db);
-		IndexDiff diff = new IndexDiff(db, tree.getId(), iterator);
+		IndexDiff diff = new IndexDiff(db, treeId, iterator);
 		diff.diff();
 		assertEquals(2, diff.getChanged().size());
 		assertTrue(diff.getChanged().contains("file2"));
@@ -314,17 +311,16 @@ public void testUnchangedSimple() throws IOException, GitAPIException {
 		git.add().addFilepattern("a=c").call();
 		git.add().addFilepattern("a=d").call();
 
-		Tree tree = new Tree(db);
+		TreeFormatter tree = new TreeFormatter();
 		// got the hash id'd from the data using echo -n a.b|git hash-object -t blob --stdin
-		tree.addFile("a.b").setId(ObjectId.fromString("f6f28df96c2b40c951164286e08be7c38ec74851"));
-		tree.addFile("a.c").setId(ObjectId.fromString("6bc0e647512d2a0bef4f26111e484dc87df7f5ca"));
-		tree.addFile("a=c").setId(ObjectId.fromString("06022365ddbd7fb126761319633bf73517770714"));
-		tree.addFile("a=d").setId(ObjectId.fromString("fa6414df3da87840700e9eeb7fc261dd77ccd5c2"));
-
-		tree.setId(insertTree(tree));
+		tree.append("a.b", FileMode.REGULAR_FILE, ObjectId.fromString("f6f28df96c2b40c951164286e08be7c38ec74851"));
+		tree.append("a.c", FileMode.REGULAR_FILE, ObjectId.fromString("6bc0e647512d2a0bef4f26111e484dc87df7f5ca"));
+		tree.append("a=c", FileMode.REGULAR_FILE, ObjectId.fromString("06022365ddbd7fb126761319633bf73517770714"));
+		tree.append("a=d", FileMode.REGULAR_FILE, ObjectId.fromString("fa6414df3da87840700e9eeb7fc261dd77ccd5c2"));
+		ObjectId treeId = insertTree(tree);
 
 		FileTreeIterator iterator = new FileTreeIterator(db);
-		IndexDiff diff = new IndexDiff(db, tree.getId(), iterator);
+		IndexDiff diff = new IndexDiff(db, treeId, iterator);
 		diff.diff();
 		assertEquals(0, diff.getChanged().size());
 		assertEquals(0, diff.getAdded().size());
@@ -356,24 +352,27 @@ public void testUnchangedComplex() throws IOException, GitAPIException {
 				.addFilepattern("a/c").addFilepattern("a=c")
 				.addFilepattern("a=d").call();
 
-		Tree tree = new Tree(db);
-		// got the hash id'd from the data using echo -n a.b|git hash-object -t blob --stdin
-		tree.addFile("a.b").setId(ObjectId.fromString("f6f28df96c2b40c951164286e08be7c38ec74851"));
-		tree.addFile("a.c").setId(ObjectId.fromString("6bc0e647512d2a0bef4f26111e484dc87df7f5ca"));
-		tree.addFile("a/b.b/b").setId(ObjectId.fromString("8d840bd4e2f3a48ff417c8e927d94996849933fd"));
-		tree.addFile("a/b").setId(ObjectId.fromString("db89c972fc57862eae378f45b74aca228037d415"));
-		tree.addFile("a/c").setId(ObjectId.fromString("52ad142a008aeb39694bafff8e8f1be75ed7f007"));
-		tree.addFile("a=c").setId(ObjectId.fromString("06022365ddbd7fb126761319633bf73517770714"));
-		tree.addFile("a=d").setId(ObjectId.fromString("fa6414df3da87840700e9eeb7fc261dd77ccd5c2"));
 
-		Tree tree3 = (Tree) tree.findTreeMember("a/b.b");
-		tree3.setId(insertTree(tree3));
-		Tree tree2 = (Tree) tree.findTreeMember("a");
-		tree2.setId(insertTree(tree2));
-		tree.setId(insertTree(tree));
+		// got the hash id'd from the data using echo -n a.b|git hash-object -t blob --stdin
+		TreeFormatter bb = new TreeFormatter();
+		bb.append("b", FileMode.REGULAR_FILE, ObjectId.fromString("8d840bd4e2f3a48ff417c8e927d94996849933fd"));
+
+		TreeFormatter a = new TreeFormatter();
+		a.append("b", FileMode.REGULAR_FILE, ObjectId
+				.fromString("db89c972fc57862eae378f45b74aca228037d415"));
+		a.append("b.b", FileMode.TREE, insertTree(bb));
+		a.append("c", FileMode.REGULAR_FILE, ObjectId.fromString("52ad142a008aeb39694bafff8e8f1be75ed7f007"));
+
+		TreeFormatter tree = new TreeFormatter();
+		tree.append("a.b", FileMode.REGULAR_FILE, ObjectId.fromString("f6f28df96c2b40c951164286e08be7c38ec74851"));
+		tree.append("a.c", FileMode.REGULAR_FILE, ObjectId.fromString("6bc0e647512d2a0bef4f26111e484dc87df7f5ca"));
+		tree.append("a", FileMode.TREE, insertTree(a));
+		tree.append("a=c", FileMode.REGULAR_FILE, ObjectId.fromString("06022365ddbd7fb126761319633bf73517770714"));
+		tree.append("a=d", FileMode.REGULAR_FILE, ObjectId.fromString("fa6414df3da87840700e9eeb7fc261dd77ccd5c2"));
+		ObjectId treeId = insertTree(tree);
 
 		FileTreeIterator iterator = new FileTreeIterator(db);
-		IndexDiff diff = new IndexDiff(db, tree.getId(), iterator);
+		IndexDiff diff = new IndexDiff(db, treeId, iterator);
 		diff.diff();
 		assertEquals(0, diff.getChanged().size());
 		assertEquals(0, diff.getAdded().size());
@@ -383,9 +382,9 @@ public void testUnchangedComplex() throws IOException, GitAPIException {
 		assertEquals(Collections.EMPTY_SET, diff.getUntrackedFolders());
 	}
 
-	private ObjectId insertTree(Tree tree) throws IOException {
+	private ObjectId insertTree(TreeFormatter tree) throws IOException {
 		try (ObjectInserter oi = db.newObjectInserter()) {
-			ObjectId id = oi.insert(Constants.OBJ_TREE, tree.format());
+			ObjectId id = oi.insert(tree);
 			oi.flush();
 			return id;
 		}
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..43160fb 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,236 @@ 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_File()
+			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 testInvalidTreeDuplicateNames1_Tree()
+			throws CorruptObjectException {
+		StringBuilder b = new StringBuilder();
+		entry(b, "40000 a");
+		entry(b, "40000 a");
+		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() 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 +1446,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 +1548,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.test/tst/org/eclipse/jgit/lib/T0002_TreeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/T0002_TreeTest.java
deleted file mode 100644
index 651e62c..0000000
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/T0002_TreeTest.java
+++ /dev/null
@@ -1,319 +0,0 @@
-/*
- * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org>
- * and other copyright owners as documented in the project's IP log.
- *
- * This program and the accompanying materials are made available
- * under the terms of the Eclipse Distribution License v1.0 which
- * accompanies this distribution, is reproduced below, and is
- * available at http://www.eclipse.org/org/documents/edl-v10.php
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or
- * without modification, are permitted provided that the following
- * conditions are met:
- *
- * - Redistributions of source code must retain the above copyright
- *   notice, this list of conditions and the following disclaimer.
- *
- * - Redistributions in binary form must reproduce the above
- *   copyright notice, this list of conditions and the following
- *   disclaimer in the documentation and/or other materials provided
- *   with the distribution.
- *
- * - Neither the name of the Eclipse Foundation, Inc. nor the
- *   names of its contributors may be used to endorse or promote
- *   products derived from this software without specific prior
- *   written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
- * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
- * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
- * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
- * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
- * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
- * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
- * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package org.eclipse.jgit.lib;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
-
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.util.ArrayList;
-import java.util.List;
-
-import org.eclipse.jgit.test.resources.SampleDataRepositoryTestCase;
-import org.junit.Test;
-
-@SuppressWarnings("deprecation")
-public class T0002_TreeTest extends SampleDataRepositoryTestCase {
-	private static final ObjectId SOME_FAKE_ID = ObjectId.fromString(
-			"0123456789abcdef0123456789abcdef01234567");
-
-	private static int compareNamesUsingSpecialCompare(String a, String b)
-			throws UnsupportedEncodingException {
-		char lasta = '\0';
-		byte[] abytes;
-		if (a.length() > 0 && a.charAt(a.length()-1) == '/') {
-			lasta = '/';
-			a = a.substring(0, a.length() - 1);
-		}
-		abytes = a.getBytes("ISO-8859-1");
-		char lastb = '\0';
-		byte[] bbytes;
-		if (b.length() > 0 && b.charAt(b.length()-1) == '/') {
-			lastb = '/';
-			b = b.substring(0, b.length() - 1);
-		}
-		bbytes = b.getBytes("ISO-8859-1");
-		return Tree.compareNames(abytes, bbytes, lasta, lastb);
-	}
-
-	@Test
-	public void test000_sort_01() throws UnsupportedEncodingException {
-		assertEquals(0, compareNamesUsingSpecialCompare("a","a"));
-	}
-
-	@Test
-	public void test000_sort_02() throws UnsupportedEncodingException {
-		assertEquals(-1, compareNamesUsingSpecialCompare("a","b"));
-		assertEquals(1, compareNamesUsingSpecialCompare("b","a"));
-	}
-
-	@Test
-	public void test000_sort_03() throws UnsupportedEncodingException {
-		assertEquals(1, compareNamesUsingSpecialCompare("a:","a"));
-		assertEquals(1, compareNamesUsingSpecialCompare("a/","a"));
-		assertEquals(-1, compareNamesUsingSpecialCompare("a","a/"));
-		assertEquals(-1, compareNamesUsingSpecialCompare("a","a:"));
-		assertEquals(1, compareNamesUsingSpecialCompare("a:","a/"));
-		assertEquals(-1, compareNamesUsingSpecialCompare("a/","a:"));
-	}
-
-	@Test
-	public void test000_sort_04() throws UnsupportedEncodingException {
-		assertEquals(-1, compareNamesUsingSpecialCompare("a.a","a/a"));
-		assertEquals(1, compareNamesUsingSpecialCompare("a/a","a.a"));
-	}
-
-	@Test
-	public void test000_sort_05() throws UnsupportedEncodingException {
-		assertEquals(-1, compareNamesUsingSpecialCompare("a.","a/"));
-		assertEquals(1, compareNamesUsingSpecialCompare("a/","a."));
-
-	}
-
-	@Test
-	public void test001_createEmpty() throws IOException {
-		final Tree t = new Tree(db);
-		assertTrue("isLoaded", t.isLoaded());
-		assertTrue("isModified", t.isModified());
-		assertTrue("no parent", t.getParent() == null);
-		assertTrue("isRoot", t.isRoot());
-		assertTrue("no name", t.getName() == null);
-		assertTrue("no nameUTF8", t.getNameUTF8() == null);
-		assertTrue("has entries array", t.members() != null);
-		assertEquals("entries is empty", 0, t.members().length);
-		assertEquals("full name is empty", "", t.getFullName());
-		assertTrue("no id", t.getId() == null);
-		assertTrue("database is r", t.getRepository() == db);
-		assertTrue("no foo child", t.findTreeMember("foo") == null);
-		assertTrue("no foo child", t.findBlobMember("foo") == null);
-	}
-
-	@Test
-	public void test002_addFile() throws IOException {
-		final Tree t = new Tree(db);
-		t.setId(SOME_FAKE_ID);
-		assertTrue("has id", t.getId() != null);
-		assertFalse("not modified", t.isModified());
-
-		final String n = "bob";
-		final FileTreeEntry f = t.addFile(n);
-		assertNotNull("have file", f);
-		assertEquals("name matches", n, f.getName());
-		assertEquals("name matches", f.getName(), new String(f.getNameUTF8(),
-				"UTF-8"));
-		assertEquals("full name matches", n, f.getFullName());
-		assertTrue("no id", f.getId() == null);
-		assertTrue("is modified", t.isModified());
-		assertTrue("has no id", t.getId() == null);
-		assertTrue("found bob", t.findBlobMember(f.getName()) == f);
-
-		final TreeEntry[] i = t.members();
-		assertNotNull("members array not null", i);
-		assertTrue("iterator is not empty", i != null && i.length > 0);
-		assertTrue("iterator returns file", i != null && i[0] == f);
-		assertTrue("iterator is empty", i != null && i.length == 1);
-	}
-
-	@Test
-	public void test004_addTree() throws IOException {
-		final Tree t = new Tree(db);
-		t.setId(SOME_FAKE_ID);
-		assertTrue("has id", t.getId() != null);
-		assertFalse("not modified", t.isModified());
-
-		final String n = "bob";
-		final Tree f = t.addTree(n);
-		assertNotNull("have tree", f);
-		assertEquals("name matches", n, f.getName());
-		assertEquals("name matches", f.getName(), new String(f.getNameUTF8(),
-				"UTF-8"));
-		assertEquals("full name matches", n, f.getFullName());
-		assertTrue("no id", f.getId() == null);
-		assertTrue("parent matches", f.getParent() == t);
-		assertTrue("repository matches", f.getRepository() == db);
-		assertTrue("isLoaded", f.isLoaded());
-		assertFalse("has items", f.members().length > 0);
-		assertFalse("is root", f.isRoot());
-		assertTrue("parent is modified", t.isModified());
-		assertTrue("parent has no id", t.getId() == null);
-		assertTrue("found bob child", t.findTreeMember(f.getName()) == f);
-
-		final TreeEntry[] i = t.members();
-		assertTrue("iterator is not empty", i.length > 0);
-		assertTrue("iterator returns file", i[0] == f);
-		assertEquals("iterator is empty", 1, i.length);
-	}
-
-	@Test
-	public void test005_addRecursiveFile() throws IOException {
-		final Tree t = new Tree(db);
-		final FileTreeEntry f = t.addFile("a/b/c");
-		assertNotNull("created f", f);
-		assertEquals("c", f.getName());
-		assertEquals("b", f.getParent().getName());
-		assertEquals("a", f.getParent().getParent().getName());
-		assertTrue("t is great-grandparent", t == f.getParent().getParent()
-				.getParent());
-	}
-
-	@Test
-	public void test005_addRecursiveTree() throws IOException {
-		final Tree t = new Tree(db);
-		final Tree f = t.addTree("a/b/c");
-		assertNotNull("created f", f);
-		assertEquals("c", f.getName());
-		assertEquals("b", f.getParent().getName());
-		assertEquals("a", f.getParent().getParent().getName());
-		assertTrue("t is great-grandparent", t == f.getParent().getParent()
-				.getParent());
-	}
-
-	@Test
-	public void test006_addDeepTree() throws IOException {
-		final Tree t = new Tree(db);
-
-		final Tree e = t.addTree("e");
-		assertNotNull("have e", e);
-		assertTrue("e.parent == t", e.getParent() == t);
-		final Tree f = t.addTree("f");
-		assertNotNull("have f", f);
-		assertTrue("f.parent == t", f.getParent() == t);
-		final Tree g = f.addTree("g");
-		assertNotNull("have g", g);
-		assertTrue("g.parent == f", g.getParent() == f);
-		final Tree h = g.addTree("h");
-		assertNotNull("have h", h);
-		assertTrue("h.parent = g", h.getParent() == g);
-
-		h.setId(SOME_FAKE_ID);
-		assertTrue("h not modified", !h.isModified());
-		g.setId(SOME_FAKE_ID);
-		assertTrue("g not modified", !g.isModified());
-		f.setId(SOME_FAKE_ID);
-		assertTrue("f not modified", !f.isModified());
-		e.setId(SOME_FAKE_ID);
-		assertTrue("e not modified", !e.isModified());
-		t.setId(SOME_FAKE_ID);
-		assertTrue("t not modified.", !t.isModified());
-
-		assertEquals("full path of h ok", "f/g/h", h.getFullName());
-		assertTrue("Can find h", t.findTreeMember(h.getFullName()) == h);
-		assertTrue("Can't find f/z", t.findBlobMember("f/z") == null);
-		assertTrue("Can't find y/z", t.findBlobMember("y/z") == null);
-
-		final FileTreeEntry i = h.addFile("i");
-		assertNotNull(i);
-		assertEquals("full path of i ok", "f/g/h/i", i.getFullName());
-		assertTrue("Can find i", t.findBlobMember(i.getFullName()) == i);
-		assertTrue("h modified", h.isModified());
-		assertTrue("g modified", g.isModified());
-		assertTrue("f modified", f.isModified());
-		assertTrue("e not modified", !e.isModified());
-		assertTrue("t modified", t.isModified());
-
-		assertTrue("h no id", h.getId() == null);
-		assertTrue("g no id", g.getId() == null);
-		assertTrue("f no id", f.getId() == null);
-		assertTrue("e has id", e.getId() != null);
-		assertTrue("t no id", t.getId() == null);
-	}
-
-	@Test
-	public void test007_manyFileLookup() throws IOException {
-		final Tree t = new Tree(db);
-		final List<FileTreeEntry> files = new ArrayList<FileTreeEntry>(26 * 26);
-		for (char level1 = 'a'; level1 <= 'z'; level1++) {
-			for (char level2 = 'a'; level2 <= 'z'; level2++) {
-				final String n = "." + level1 + level2 + "9";
-				final FileTreeEntry f = t.addFile(n);
-				assertNotNull("File " + n + " added.", f);
-				assertEquals(n, f.getName());
-				files.add(f);
-			}
-		}
-		assertEquals(files.size(), t.memberCount());
-		final TreeEntry[] ents = t.members();
-		assertNotNull(ents);
-		assertEquals(files.size(), ents.length);
-		for (int k = 0; k < ents.length; k++) {
-			assertTrue("File " + files.get(k).getName()
-					+ " is at " + k + ".", files.get(k) == ents[k]);
-		}
-	}
-
-	@Test
-	public void test008_SubtreeInternalSorting() throws IOException {
-		final Tree t = new Tree(db);
-		final FileTreeEntry e0 = t.addFile("a-b");
-		final FileTreeEntry e1 = t.addFile("a-");
-		final FileTreeEntry e2 = t.addFile("a=b");
-		final Tree e3 = t.addTree("a");
-		final FileTreeEntry e4 = t.addFile("a=");
-
-		final TreeEntry[] ents = t.members();
-		assertSame(e1, ents[0]);
-		assertSame(e0, ents[1]);
-		assertSame(e3, ents[2]);
-		assertSame(e4, ents[3]);
-		assertSame(e2, ents[4]);
-	}
-
-	@Test
-	public void test009_SymlinkAndGitlink() throws IOException {
-		final Tree symlinkTree = mapTree("symlink");
-		assertTrue("Symlink entry exists", symlinkTree.existsBlob("symlink.txt"));
-		final Tree gitlinkTree = mapTree("gitlink");
-		assertTrue("Gitlink entry exists", gitlinkTree.existsBlob("submodule"));
-	}
-
-	private Tree mapTree(String name) throws IOException {
-		ObjectId id = db.resolve(name + "^{tree}");
-		return new Tree(db, id, db.open(id).getCachedBytes());
-	}
-}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/ObjectWalkTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/ObjectWalkTest.java
index 2a59f58..9c9edc1 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/ObjectWalkTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/ObjectWalkTest.java
@@ -47,11 +47,10 @@
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.FileTreeEntry;
+import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.Tree;
+import org.eclipse.jgit.lib.TreeFormatter;
 import org.junit.Test;
 
 @SuppressWarnings("deprecation")
@@ -220,28 +219,24 @@ public void testEmptyTreeCorruption() throws Exception {
 				.fromString("abbbfafe3129f85747aba7bfac992af77134c607");
 		final RevTree tree_root, tree_A, tree_AB;
 		final RevCommit b;
-		{
-			Tree root = new Tree(db);
-			Tree A = root.addTree("A");
-			FileTreeEntry B = root.addFile("B");
-			B.setId(bId);
+		try (ObjectInserter inserter = db.newObjectInserter()) {
+			ObjectId empty = inserter.insert(new TreeFormatter());
 
-			Tree A_A = A.addTree("A");
-			Tree A_B = A.addTree("B");
+			TreeFormatter A = new TreeFormatter();
+			A.append("A", FileMode.TREE, empty);
+			A.append("B", FileMode.TREE, empty);
+			ObjectId idA = inserter.insert(A);
 
-			try (final ObjectInserter inserter = db.newObjectInserter()) {
-				A_A.setId(inserter.insert(Constants.OBJ_TREE, A_A.format()));
-				A_B.setId(inserter.insert(Constants.OBJ_TREE, A_B.format()));
-				A.setId(inserter.insert(Constants.OBJ_TREE, A.format()));
-				root.setId(inserter.insert(Constants.OBJ_TREE, root.format()));
-				inserter.flush();
-			}
+			TreeFormatter root = new TreeFormatter();
+			root.append("A", FileMode.TREE, idA);
+			root.append("B", FileMode.REGULAR_FILE, bId);
+			ObjectId idRoot = inserter.insert(root);
+			inserter.flush();
 
-			tree_root = rw.parseTree(root.getId());
-			tree_A = rw.parseTree(A.getId());
-			tree_AB = rw.parseTree(A_A.getId());
-			assertSame(tree_AB, rw.parseTree(A_B.getId()));
-			b = commit(rw.parseTree(root.getId()));
+			tree_root = objw.parseTree(idRoot);
+			tree_A = objw.parseTree(idA);
+			tree_AB = objw.parseTree(empty);
+			b = commit(tree_root);
 		}
 
 		markStart(b);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevCommitParseTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevCommitParseTest.java
index beda2a7..885c1b5 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevCommitParseTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevCommitParseTest.java
@@ -43,13 +43,18 @@
 
 package org.eclipse.jgit.revwalk;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import java.io.ByteArrayOutputStream;
 import java.io.UnsupportedEncodingException;
+import java.nio.charset.IllegalCharsetNameException;
+import java.nio.charset.UnsupportedCharsetException;
 import java.util.TimeZone;
 
 import org.eclipse.jgit.junit.RepositoryTestCase;
@@ -304,6 +309,86 @@ public void testParse_explicit_bad_encoded2() throws Exception {
 	}
 
 	@Test
+	public void testParse_incorrectUtf8Name() throws Exception {
+		ByteArrayOutputStream b = new ByteArrayOutputStream();
+		b.write("tree 9788669ad918b6fcce64af8882fc9a81cb6aba67\n"
+				.getBytes(UTF_8));
+		b.write("author au <a@example.com> 1218123387 +0700\n".getBytes(UTF_8));
+		b.write("committer co <c@example.com> 1218123390 -0500\n"
+				.getBytes(UTF_8));
+		b.write("encoding 'utf8'\n".getBytes(UTF_8));
+		b.write("\n".getBytes(UTF_8));
+		b.write("Sm\u00f6rg\u00e5sbord\n".getBytes(UTF_8));
+
+		RevCommit c = new RevCommit(
+				id("9473095c4cb2f12aefe1db8a355fe3fafba42f67"));
+		c.parseCanonical(new RevWalk(db), b.toByteArray());
+		assertEquals("'utf8'", c.getEncodingName());
+		assertEquals("Sm\u00f6rg\u00e5sbord\n", c.getFullMessage());
+
+		try {
+			c.getEncoding();
+			fail("Expected " + IllegalCharsetNameException.class);
+		} catch (IllegalCharsetNameException badName) {
+			assertEquals("'utf8'", badName.getMessage());
+		}
+	}
+
+	@Test
+	public void testParse_illegalEncoding() throws Exception {
+		ByteArrayOutputStream b = new ByteArrayOutputStream();
+		b.write("tree 9788669ad918b6fcce64af8882fc9a81cb6aba67\n".getBytes(UTF_8));
+		b.write("author au <a@example.com> 1218123387 +0700\n".getBytes(UTF_8));
+		b.write("committer co <c@example.com> 1218123390 -0500\n".getBytes(UTF_8));
+		b.write("encoding utf-8logoutputencoding=gbk\n".getBytes(UTF_8));
+		b.write("\n".getBytes(UTF_8));
+		b.write("message\n".getBytes(UTF_8));
+
+		RevCommit c = new RevCommit(
+				id("9473095c4cb2f12aefe1db8a355fe3fafba42f67"));
+		c.parseCanonical(new RevWalk(db), b.toByteArray());
+		assertEquals("utf-8logoutputencoding=gbk", c.getEncodingName());
+		assertEquals("message\n", c.getFullMessage());
+		assertEquals("message", c.getShortMessage());
+		assertTrue(c.getFooterLines().isEmpty());
+		assertEquals("au", c.getAuthorIdent().getName());
+
+		try {
+			c.getEncoding();
+			fail("Expected " + IllegalCharsetNameException.class);
+		} catch (IllegalCharsetNameException badName) {
+			assertEquals("utf-8logoutputencoding=gbk", badName.getMessage());
+		}
+	}
+
+	@Test
+	public void testParse_unsupportedEncoding() throws Exception {
+		ByteArrayOutputStream b = new ByteArrayOutputStream();
+		b.write("tree 9788669ad918b6fcce64af8882fc9a81cb6aba67\n".getBytes(UTF_8));
+		b.write("author au <a@example.com> 1218123387 +0700\n".getBytes(UTF_8));
+		b.write("committer co <c@example.com> 1218123390 -0500\n".getBytes(UTF_8));
+		b.write("encoding it_IT.UTF8\n".getBytes(UTF_8));
+		b.write("\n".getBytes(UTF_8));
+		b.write("message\n".getBytes(UTF_8));
+
+		RevCommit c = new RevCommit(
+				id("9473095c4cb2f12aefe1db8a355fe3fafba42f67"));
+		c.parseCanonical(new RevWalk(db), b.toByteArray());
+		assertEquals("it_IT.UTF8", c.getEncodingName());
+		assertEquals("message\n", c.getFullMessage());
+		assertEquals("message", c.getShortMessage());
+		assertTrue(c.getFooterLines().isEmpty());
+		assertEquals("au", c.getAuthorIdent().getName());
+
+		try {
+			c.getEncoding();
+			fail("Expected " + UnsupportedCharsetException.class);
+		} catch (UnsupportedCharsetException badName) {
+			assertEquals("it_IT.UTF8", badName.getMessage());
+		}
+	}
+
+	@Test
 	public void testParse_NoMessage() throws Exception {
 		final String msg = "";
 		final RevCommit c = create(msg);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevTagParseTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevTagParseTest.java
index 614f49b..82505ca 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevTagParseTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevTagParseTest.java
@@ -43,6 +43,7 @@
 
 package org.eclipse.jgit.revwalk;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
@@ -362,6 +363,44 @@ public void testParse_explicit_bad_encoded2() throws Exception {
 	}
 
 	@Test
+	public void testParse_illegalEncoding() throws Exception {
+		ByteArrayOutputStream b = new ByteArrayOutputStream();
+		b.write("object 9788669ad918b6fcce64af8882fc9a81cb6aba67\n".getBytes(UTF_8));
+		b.write("type tree\n".getBytes(UTF_8));
+		b.write("tag v1.0\n".getBytes(UTF_8));
+		b.write("tagger t <t@example.com> 1218123387 +0700\n".getBytes(UTF_8));
+		b.write("encoding utf-8logoutputencoding=gbk\n".getBytes(UTF_8));
+		b.write("\n".getBytes(UTF_8));
+		b.write("message\n".getBytes(UTF_8));
+
+		RevTag t = new RevTag(id("9473095c4cb2f12aefe1db8a355fe3fafba42f67"));
+		t.parseCanonical(new RevWalk(db), b.toByteArray());
+
+		assertEquals("t", t.getTaggerIdent().getName());
+		assertEquals("message", t.getShortMessage());
+		assertEquals("message\n", t.getFullMessage());
+	}
+
+	@Test
+	public void testParse_unsupportedEncoding() throws Exception {
+		ByteArrayOutputStream b = new ByteArrayOutputStream();
+		b.write("object 9788669ad918b6fcce64af8882fc9a81cb6aba67\n".getBytes(UTF_8));
+		b.write("type tree\n".getBytes(UTF_8));
+		b.write("tag v1.0\n".getBytes(UTF_8));
+		b.write("tagger t <t@example.com> 1218123387 +0700\n".getBytes(UTF_8));
+		b.write("encoding it_IT.UTF8\n".getBytes(UTF_8));
+		b.write("\n".getBytes(UTF_8));
+		b.write("message\n".getBytes(UTF_8));
+
+		RevTag t = new RevTag(id("9473095c4cb2f12aefe1db8a355fe3fafba42f67"));
+		t.parseCanonical(new RevWalk(db), b.toByteArray());
+
+		assertEquals("t", t.getTaggerIdent().getName());
+		assertEquals("message", t.getShortMessage());
+		assertEquals("message\n", t.getFullMessage());
+	}
+
+	@Test
 	public void testParse_NoMessage() throws Exception {
 		final String msg = "";
 		final RevTag c = create(msg);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/AtomicPushTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/AtomicPushTest.java
index 782e414..c1e078d 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/AtomicPushTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/AtomicPushTest.java
@@ -112,12 +112,9 @@ private static InMemoryRepository newRepo(String name) {
 	public void pushNonAtomic() throws Exception {
 		PushResult r;
 		server.setPerformsAtomicTransactions(false);
-		Transport tn = testProtocol.open(uri, client, "server");
-		try {
+		try (Transport tn = testProtocol.open(uri, client, "server")) {
 			tn.setPushAtomic(false);
 			r = tn.push(NullProgressMonitor.INSTANCE, commands());
-		} finally {
-			tn.close();
 		}
 
 		RemoteRefUpdate one = r.getRemoteUpdate("refs/heads/one");
@@ -131,12 +128,9 @@ public void pushNonAtomic() throws Exception {
 	@Test
 	public void pushAtomicClientGivesUpEarly() throws Exception {
 		PushResult r;
-		Transport tn = testProtocol.open(uri, client, "server");
-		try {
+		try (Transport tn = testProtocol.open(uri, client, "server")) {
 			tn.setPushAtomic(true);
 			r = tn.push(NullProgressMonitor.INSTANCE, commands());
-		} finally {
-			tn.close();
 		}
 
 		RemoteRefUpdate one = r.getRemoteUpdate("refs/heads/one");
@@ -167,8 +161,7 @@ public void pushAtomicDisabled() throws Exception {
 				ObjectId.zeroId()));
 
 		server.setPerformsAtomicTransactions(false);
-		Transport tn = testProtocol.open(uri, client, "server");
-		try {
+		try (Transport tn = testProtocol.open(uri, client, "server")) {
 			tn.setPushAtomic(true);
 			tn.push(NullProgressMonitor.INSTANCE, cmds);
 			fail("did not throw TransportException");
@@ -176,8 +169,6 @@ public void pushAtomicDisabled() throws Exception {
 			assertEquals(
 					uri + ": " + JGitText.get().atomicPushNotSupported,
 					e.getMessage());
-		} finally {
-			tn.close();
 		}
 	}
 
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BundleWriterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BundleWriterTest.java
index 4615308..a83a993 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BundleWriterTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BundleWriterTest.java
@@ -168,8 +168,10 @@ private static FetchResult fetchFromBundle(final Repository newRepo,
 		final ByteArrayInputStream in = new ByteArrayInputStream(bundle);
 		final RefSpec rs = new RefSpec("refs/heads/*:refs/heads/*");
 		final Set<RefSpec> refs = Collections.singleton(rs);
-		return new TransportBundleStream(newRepo, uri, in).fetch(
-				NullProgressMonitor.INSTANCE, refs);
+		try (TransportBundleStream transport = new TransportBundleStream(
+				newRepo, uri, in)) {
+			return transport.fetch(NullProgressMonitor.INSTANCE, refs);
+		}
 	}
 
 	private byte[] makeBundle(final String name,
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ReceivePackAdvertiseRefsHookTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ReceivePackAdvertiseRefsHookTest.java
index aa5914f..94bc383 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ReceivePackAdvertiseRefsHookTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/ReceivePackAdvertiseRefsHookTest.java
@@ -116,12 +116,9 @@ public void setUp() throws Exception {
 
 		// Clone from dst into src
 		//
-		Transport t = Transport.open(src, uriOf(dst));
-		try {
+		try (Transport t = Transport.open(src, uriOf(dst))) {
 			t.fetch(PM, Collections.singleton(new RefSpec("+refs/*:refs/*")));
 			assertEquals(B, src.resolve(R_MASTER));
-		} finally {
-			t.close();
 		}
 
 		// Now put private stuff into dst.
@@ -144,7 +141,8 @@ public void tearDown() throws Exception {
 	@Test
 	public void testFilterHidesPrivate() throws Exception {
 		Map<String, Ref> refs;
-		TransportLocal t = new TransportLocal(src, uriOf(dst), dst.getDirectory()) {
+		try (TransportLocal t = new TransportLocal(src, uriOf(dst),
+				dst.getDirectory()) {
 			@Override
 			ReceivePack createReceivePack(final Repository db) {
 				db.close();
@@ -154,16 +152,10 @@ ReceivePack createReceivePack(final Repository db) {
 				rp.setAdvertiseRefsHook(new HidePrivateHook());
 				return rp;
 			}
-		};
-		try {
-			PushConnection c = t.openPush();
-			try {
+		}) {
+			try (PushConnection c = t.openPush()) {
 				refs = c.getRefsMap();
-			} finally {
-				c.close();
 			}
-		} finally {
-			t.close();
 		}
 
 		assertNotNull(refs);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RefSpecTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RefSpecTest.java
index 3f5fcbb..4f83350 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RefSpecTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/RefSpecTest.java
@@ -341,6 +341,41 @@ public void testWildcardInMiddleOfDestionation() {
 	}
 
 	@Test
+	public void testWildcardAfterText1() {
+		RefSpec a = new RefSpec("refs/heads/*/for-linus:refs/remotes/mine/*-blah");
+		assertTrue(a.isWildcard());
+		assertTrue(a.matchDestination("refs/remotes/mine/x-blah"));
+		assertTrue(a.matchDestination("refs/remotes/mine/foo-blah"));
+		assertTrue(a.matchDestination("refs/remotes/mine/foo/x-blah"));
+		assertFalse(a.matchDestination("refs/remotes/origin/foo/x-blah"));
+
+		RefSpec b = a.expandFromSource("refs/heads/foo/for-linus");
+		assertEquals("refs/remotes/mine/foo-blah", b.getDestination());
+		RefSpec c = a.expandFromDestination("refs/remotes/mine/foo-blah");
+		assertEquals("refs/heads/foo/for-linus", c.getSource());
+	}
+
+	@Test
+	public void testWildcardAfterText2() {
+		RefSpec a = new RefSpec("refs/heads*/for-linus:refs/remotes/mine/*");
+		assertTrue(a.isWildcard());
+		assertTrue(a.matchSource("refs/headsx/for-linus"));
+		assertTrue(a.matchSource("refs/headsfoo/for-linus"));
+		assertTrue(a.matchSource("refs/headsx/foo/for-linus"));
+		assertFalse(a.matchSource("refs/headx/for-linus"));
+
+		RefSpec b = a.expandFromSource("refs/headsx/for-linus");
+		assertEquals("refs/remotes/mine/x", b.getDestination());
+		RefSpec c = a.expandFromDestination("refs/remotes/mine/x");
+		assertEquals("refs/headsx/for-linus", c.getSource());
+
+		RefSpec d = a.expandFromSource("refs/headsx/foo/for-linus");
+		assertEquals("refs/remotes/mine/x/foo", d.getDestination());
+		RefSpec e = a.expandFromDestination("refs/remotes/mine/x/foo");
+		assertEquals("refs/headsx/foo/for-linus", e.getSource());
+	}
+
+	@Test
 	public void testWildcardMirror() {
 		RefSpec a = new RefSpec("*:*");
 		assertTrue(a.isWildcard());
@@ -404,21 +439,6 @@ public void invalidWhenMoreThanOneWildcardInDestination() {
 	}
 
 	@Test(expected = IllegalArgumentException.class)
-	public void invalidWhenWildcardAfterText() {
-		assertNotNull(new RefSpec("refs/heads/wrong*:refs/heads/right/*"));
-	}
-
-	@Test(expected = IllegalArgumentException.class)
-	public void invalidWhenWildcardBeforeText() {
-		assertNotNull(new RefSpec("*wrong:right/*"));
-	}
-
-	@Test(expected = IllegalArgumentException.class)
-	public void invalidWhenWildcardBeforeTextAtEnd() {
-		assertNotNull(new RefSpec("refs/heads/*wrong:right/*"));
-	}
-
-	@Test(expected = IllegalArgumentException.class)
 	public void invalidSourceDoubleSlashes() {
 		assertNotNull(new RefSpec("refs/heads//wrong"));
 	}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportTest.java
index 55e1e44..5519f61 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportTest.java
@@ -61,13 +61,10 @@
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.test.resources.SampleDataRepositoryTestCase;
-import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
 public class TransportTest extends SampleDataRepositoryTestCase {
-	private Transport transport;
-
 	private RemoteConfig remoteConfig;
 
 	@Override
@@ -77,17 +74,6 @@ public void setUp() throws Exception {
 		final Config config = db.getConfig();
 		remoteConfig = new RemoteConfig(config, "test");
 		remoteConfig.addURI(new URIish("http://everyones.loves.git/u/2"));
-		transport = null;
-	}
-
-	@Override
-	@After
-	public void tearDown() throws Exception {
-		if (transport != null) {
-			transport.close();
-			transport = null;
-		}
-		super.tearDown();
 	}
 
 	/**
@@ -99,10 +85,11 @@ public void tearDown() throws Exception {
 	@Test
 	public void testFindRemoteRefUpdatesNoWildcardNoTracking()
 			throws IOException {
-		transport = Transport.open(db, remoteConfig);
-		final Collection<RemoteRefUpdate> result = transport
-				.findRemoteRefUpdatesFor(Collections.nCopies(1, new RefSpec(
-						"refs/heads/master:refs/heads/x")));
+		Collection<RemoteRefUpdate> result;
+		try (Transport transport = Transport.open(db, remoteConfig)) {
+			result = transport.findRemoteRefUpdatesFor(Collections.nCopies(1,
+					new RefSpec("refs/heads/master:refs/heads/x")));
+		}
 
 		assertEquals(1, result.size());
 		final RemoteRefUpdate rru = result.iterator().next();
@@ -122,10 +109,11 @@ public void testFindRemoteRefUpdatesNoWildcardNoTracking()
 	@Test
 	public void testFindRemoteRefUpdatesNoWildcardNoDestination()
 			throws IOException {
-		transport = Transport.open(db, remoteConfig);
-		final Collection<RemoteRefUpdate> result = transport
-				.findRemoteRefUpdatesFor(Collections.nCopies(1, new RefSpec(
-						"+refs/heads/master")));
+		Collection<RemoteRefUpdate> result;
+		try (Transport transport = Transport.open(db, remoteConfig)) {
+			result = transport.findRemoteRefUpdatesFor(
+					Collections.nCopies(1, new RefSpec("+refs/heads/master")));
+		}
 
 		assertEquals(1, result.size());
 		final RemoteRefUpdate rru = result.iterator().next();
@@ -143,10 +131,11 @@ public void testFindRemoteRefUpdatesNoWildcardNoDestination()
 	 */
 	@Test
 	public void testFindRemoteRefUpdatesWildcardNoTracking() throws IOException {
-		transport = Transport.open(db, remoteConfig);
-		final Collection<RemoteRefUpdate> result = transport
-				.findRemoteRefUpdatesFor(Collections.nCopies(1, new RefSpec(
-						"+refs/heads/*:refs/heads/test/*")));
+		Collection<RemoteRefUpdate> result;
+		try (Transport transport = Transport.open(db, remoteConfig)) {
+			result = transport.findRemoteRefUpdatesFor(Collections.nCopies(1,
+					new RefSpec("+refs/heads/*:refs/heads/test/*")));
+		}
 
 		assertEquals(12, result.size());
 		boolean foundA = false;
@@ -171,12 +160,14 @@ public void testFindRemoteRefUpdatesWildcardNoTracking() throws IOException {
 	 */
 	@Test
 	public void testFindRemoteRefUpdatesTwoRefSpecs() throws IOException {
-		transport = Transport.open(db, remoteConfig);
 		final RefSpec specA = new RefSpec("+refs/heads/a:refs/heads/b");
 		final RefSpec specC = new RefSpec("+refs/heads/c:refs/heads/d");
 		final Collection<RefSpec> specs = Arrays.asList(specA, specC);
-		final Collection<RemoteRefUpdate> result = transport
-				.findRemoteRefUpdatesFor(specs);
+
+		Collection<RemoteRefUpdate> result;
+		try (Transport transport = Transport.open(db, remoteConfig)) {
+			result = transport.findRemoteRefUpdatesFor(specs);
+		}
 
 		assertEquals(2, result.size());
 		boolean foundA = false;
@@ -202,10 +193,12 @@ public void testFindRemoteRefUpdatesTwoRefSpecs() throws IOException {
 	public void testFindRemoteRefUpdatesTrackingRef() throws IOException {
 		remoteConfig.addFetchRefSpec(new RefSpec(
 				"refs/heads/*:refs/remotes/test/*"));
-		transport = Transport.open(db, remoteConfig);
-		final Collection<RemoteRefUpdate> result = transport
-				.findRemoteRefUpdatesFor(Collections.nCopies(1, new RefSpec(
-						"+refs/heads/a:refs/heads/a")));
+
+		Collection<RemoteRefUpdate> result;
+		try (Transport transport = Transport.open(db, remoteConfig)) {
+			result = transport.findRemoteRefUpdatesFor(Collections.nCopies(1,
+					new RefSpec("+refs/heads/a:refs/heads/a")));
+		}
 
 		assertEquals(1, result.size());
 		final TrackingRefUpdate tru = result.iterator().next()
@@ -225,20 +218,18 @@ public void testLocalTransportWithRelativePath() throws Exception {
 		config.addURI(new URIish("../" + otherDir));
 
 		// Should not throw NoRemoteRepositoryException
-		transport = Transport.open(db, config);
+		Transport.open(db, config).close();
 	}
 
 	@Test
 	public void testLocalTransportFetchWithoutLocalRepository()
 			throws Exception {
 		URIish uri = new URIish("file://" + db.getWorkTree().getAbsolutePath());
-		transport = Transport.open(uri);
-		FetchConnection fetchConnection = transport.openFetch();
-		try {
-			Ref head = fetchConnection.getRef(Constants.HEAD);
-			assertNotNull(head);
-		} finally {
-			fetchConnection.close();
+		try (Transport transport = Transport.open(uri)) {
+			try (FetchConnection fetchConnection = transport.openFetch()) {
+				Ref head = fetchConnection.getRef(Constants.HEAD);
+				assertNotNull(head);
+			}
 		}
 	}
 
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/URIishTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/URIishTest.java
index 2078dd3..e55d373 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/URIishTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/URIishTest.java
@@ -460,6 +460,48 @@ public void testSshProtoWithUserPassAndPort() throws Exception {
 	}
 
 	@Test
+	public void testSshProtoWithEmailUserAndPort() throws Exception {
+		final String str = "ssh://user.name@email.com@example.com:33/some/p ath";
+		URIish u = new URIish(str);
+		assertEquals("ssh", u.getScheme());
+		assertTrue(u.isRemote());
+		assertEquals("/some/p ath", u.getRawPath());
+		assertEquals("/some/p ath", u.getPath());
+		assertEquals("example.com", u.getHost());
+		assertEquals("user.name@email.com", u.getUser());
+		assertNull(u.getPass());
+		assertEquals(33, u.getPort());
+		assertEquals("ssh://user.name%40email.com@example.com:33/some/p ath",
+				u.toPrivateString());
+		assertEquals("ssh://user.name%40email.com@example.com:33/some/p%20ath",
+				u.toPrivateASCIIString());
+		assertEquals(u.setPass(null).toPrivateString(), u.toString());
+		assertEquals(u.setPass(null).toPrivateASCIIString(), u.toASCIIString());
+		assertEquals(u, new URIish(str));
+	}
+
+	@Test
+	public void testSshProtoWithEmailUserPassAndPort() throws Exception {
+		final String str = "ssh://user.name@email.com:pass@wor:d@example.com:33/some/p ath";
+		URIish u = new URIish(str);
+		assertEquals("ssh", u.getScheme());
+		assertTrue(u.isRemote());
+		assertEquals("/some/p ath", u.getRawPath());
+		assertEquals("/some/p ath", u.getPath());
+		assertEquals("example.com", u.getHost());
+		assertEquals("user.name@email.com", u.getUser());
+		assertEquals("pass@wor:d", u.getPass());
+		assertEquals(33, u.getPort());
+		assertEquals("ssh://user.name%40email.com:pass%40wor%3ad@example.com:33/some/p ath",
+				u.toPrivateString());
+		assertEquals("ssh://user.name%40email.com:pass%40wor%3ad@example.com:33/some/p%20ath",
+				u.toPrivateASCIIString());
+		assertEquals(u.setPass(null).toPrivateString(), u.toString());
+		assertEquals(u.setPass(null).toPrivateASCIIString(), u.toASCIIString());
+		assertEquals(u, new URIish(str));
+	}
+
+	@Test
 	public void testSshProtoWithADUserPassAndPort() throws Exception {
 		final String str = "ssh://DOMAIN\\user:pass@example.com:33/some/p ath";
 		URIish u = new URIish(str);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/TreeWalkBasicDiffTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/TreeWalkBasicDiffTest.java
index aca7c80..c3ff7df 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/TreeWalkBasicDiffTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/TreeWalkBasicDiffTest.java
@@ -44,7 +44,6 @@
 package org.eclipse.jgit.treewalk;
 
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
-import static org.eclipse.jgit.lib.Constants.OBJ_TREE;
 import static org.eclipse.jgit.lib.Constants.encode;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -54,11 +53,10 @@
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.Tree;
+import org.eclipse.jgit.lib.TreeFormatter;
 import org.eclipse.jgit.treewalk.filter.TreeFilter;
 import org.junit.Test;
 
-@SuppressWarnings("deprecation")
 public class TreeWalkBasicDiffTest extends RepositoryTestCase {
 	@Test
 	public void testMissingSubtree_DetectFileAdded_FileModified()
@@ -72,62 +70,63 @@ public void testMissingSubtree_DetectFileAdded_FileModified()
 
 			// Create sub-a/empty, sub-c/empty = hello.
 			{
-				final Tree root = new Tree(db);
+				TreeFormatter root = new TreeFormatter();
 				{
-					final Tree subA = root.addTree("sub-a");
-					subA.addFile("empty").setId(aFileId);
-					subA.setId(inserter.insert(OBJ_TREE, subA.format()));
+					TreeFormatter subA = new TreeFormatter();
+					subA.append("empty", FileMode.REGULAR_FILE, aFileId);
+					root.append("sub-a", FileMode.TREE, inserter.insert(subA));
 				}
 				{
-					final Tree subC = root.addTree("sub-c");
-					subC.addFile("empty").setId(cFileId1);
-					subC.setId(inserter.insert(OBJ_TREE, subC.format()));
+					TreeFormatter subC = new TreeFormatter();
+					subC.append("empty", FileMode.REGULAR_FILE, cFileId1);
+					root.append("sub-c", FileMode.TREE, inserter.insert(subC));
 				}
-				oldTree = inserter.insert(OBJ_TREE, root.format());
+				oldTree = inserter.insert(root);
 			}
 
 			// Create sub-a/empty, sub-b/empty, sub-c/empty.
 			{
-				final Tree root = new Tree(db);
+				TreeFormatter root = new TreeFormatter();
 				{
-					final Tree subA = root.addTree("sub-a");
-					subA.addFile("empty").setId(aFileId);
-					subA.setId(inserter.insert(OBJ_TREE, subA.format()));
+					TreeFormatter subA = new TreeFormatter();
+					subA.append("empty", FileMode.REGULAR_FILE, aFileId);
+					root.append("sub-a", FileMode.TREE, inserter.insert(subA));
 				}
 				{
-					final Tree subB = root.addTree("sub-b");
-					subB.addFile("empty").setId(bFileId);
-					subB.setId(inserter.insert(OBJ_TREE, subB.format()));
+					TreeFormatter subB = new TreeFormatter();
+					subB.append("empty", FileMode.REGULAR_FILE, bFileId);
+					root.append("sub-b", FileMode.TREE, inserter.insert(subB));
 				}
 				{
-					final Tree subC = root.addTree("sub-c");
-					subC.addFile("empty").setId(cFileId2);
-					subC.setId(inserter.insert(OBJ_TREE, subC.format()));
+					TreeFormatter subC = new TreeFormatter();
+					subC.append("empty", FileMode.REGULAR_FILE, cFileId2);
+					root.append("sub-c", FileMode.TREE, inserter.insert(subC));
 				}
-				newTree = inserter.insert(OBJ_TREE, root.format());
+				newTree = inserter.insert(root);
 			}
 			inserter.flush();
 		}
 
-		final TreeWalk tw = new TreeWalk(db);
-		tw.reset(oldTree, newTree);
-		tw.setRecursive(true);
-		tw.setFilter(TreeFilter.ANY_DIFF);
+		try (TreeWalk tw = new TreeWalk(db)) {
+			tw.reset(oldTree, newTree);
+			tw.setRecursive(true);
+			tw.setFilter(TreeFilter.ANY_DIFF);
 
-		assertTrue(tw.next());
-		assertEquals("sub-b/empty", tw.getPathString());
-		assertEquals(FileMode.MISSING, tw.getFileMode(0));
-		assertEquals(FileMode.REGULAR_FILE, tw.getFileMode(1));
-		assertEquals(ObjectId.zeroId(), tw.getObjectId(0));
-		assertEquals(bFileId, tw.getObjectId(1));
+			assertTrue(tw.next());
+			assertEquals("sub-b/empty", tw.getPathString());
+			assertEquals(FileMode.MISSING, tw.getFileMode(0));
+			assertEquals(FileMode.REGULAR_FILE, tw.getFileMode(1));
+			assertEquals(ObjectId.zeroId(), tw.getObjectId(0));
+			assertEquals(bFileId, tw.getObjectId(1));
 
-		assertTrue(tw.next());
-		assertEquals("sub-c/empty", tw.getPathString());
-		assertEquals(FileMode.REGULAR_FILE, tw.getFileMode(0));
-		assertEquals(FileMode.REGULAR_FILE, tw.getFileMode(1));
-		assertEquals(cFileId1, tw.getObjectId(0));
-		assertEquals(cFileId2, tw.getObjectId(1));
+			assertTrue(tw.next());
+			assertEquals("sub-c/empty", tw.getPathString());
+			assertEquals(FileMode.REGULAR_FILE, tw.getFileMode(0));
+			assertEquals(FileMode.REGULAR_FILE, tw.getFileMode(1));
+			assertEquals(cFileId1, tw.getObjectId(0));
+			assertEquals(cFileId2, tw.getObjectId(1));
 
-		assertFalse(tw.next());
+			assertFalse(tw.next());
+		}
 	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/PathFilterGroupTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/PathFilterGroupTest.java
index d0062e1..5edc192 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/PathFilterGroupTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/treewalk/filter/PathFilterGroupTest.java
@@ -43,11 +43,18 @@
 
 package org.eclipse.jgit.treewalk.filter;
 
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.dircache.DirCacheEditor;
@@ -58,6 +65,7 @@
 import org.eclipse.jgit.errors.StopWalkException;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Sets;
 import org.eclipse.jgit.treewalk.TreeWalk;
 import org.junit.Before;
 import org.junit.Test;
@@ -66,6 +74,8 @@ public class PathFilterGroupTest {
 
 	private TreeFilter filter;
 
+	private Map<String, TreeFilter> singles;
+
 	@Before
 	public void setup() {
 		// @formatter:off
@@ -81,64 +91,75 @@ public void setup() {
 				};
 		// @formatter:on
 		filter = PathFilterGroup.createFromStrings(paths);
+		singles = new HashMap<>();
+		for (String path : paths) {
+			singles.put(path, PathFilterGroup.createFromStrings(path));
+		}
 	}
 
 	@Test
 	public void testExact() throws MissingObjectException,
 			IncorrectObjectTypeException, IOException {
-		assertTrue(filter.include(fakeWalk("a")));
-		assertTrue(filter.include(fakeWalk("b/c")));
-		assertTrue(filter.include(fakeWalk("c/d/e")));
-		assertTrue(filter.include(fakeWalk("c/d/f")));
-		assertTrue(filter.include(fakeWalk("d/e/f/g")));
-		assertTrue(filter.include(fakeWalk("d/e/f/g.x")));
+		assertMatches(Sets.of("a"), fakeWalk("a"));
+		assertMatches(Sets.of("b/c"), fakeWalk("b/c"));
+		assertMatches(Sets.of("c/d/e"), fakeWalk("c/d/e"));
+		assertMatches(Sets.of("c/d/f"), fakeWalk("c/d/f"));
+		assertMatches(Sets.of("d/e/f/g"), fakeWalk("d/e/f/g"));
+		assertMatches(Sets.of("d/e/f/g.x"), fakeWalk("d/e/f/g.x"));
 	}
 
 	@Test
 	public void testNoMatchButClose() throws MissingObjectException,
 			IncorrectObjectTypeException, IOException {
-		assertFalse(filter.include(fakeWalk("a+")));
-		assertFalse(filter.include(fakeWalk("b+/c")));
-		assertFalse(filter.include(fakeWalk("c+/d/e")));
-		assertFalse(filter.include(fakeWalk("c+/d/f")));
-		assertFalse(filter.include(fakeWalk("c/d.a")));
-		assertFalse(filter.include(fakeWalk("d+/e/f/g")));
+		assertNoMatches(fakeWalk("a+"));
+		assertNoMatches(fakeWalk("b+/c"));
+		assertNoMatches(fakeWalk("c+/d/e"));
+		assertNoMatches(fakeWalk("c+/d/f"));
+		assertNoMatches(fakeWalk("c/d.a"));
+		assertNoMatches(fakeWalk("d+/e/f/g"));
 	}
 
 	@Test
 	public void testJustCommonPrefixIsNotMatch() throws MissingObjectException,
 			IncorrectObjectTypeException, IOException {
-		assertFalse(filter.include(fakeWalk("b/a")));
-		assertFalse(filter.include(fakeWalk("b/d")));
-		assertFalse(filter.include(fakeWalk("c/d/a")));
-		assertFalse(filter.include(fakeWalk("d/e/e")));
+		assertNoMatches(fakeWalk("b/a"));
+		assertNoMatches(fakeWalk("b/d"));
+		assertNoMatches(fakeWalk("c/d/a"));
+		assertNoMatches(fakeWalk("d/e/e"));
+		assertNoMatches(fakeWalk("d/e/f/g.y"));
 	}
 
 	@Test
 	public void testKeyIsPrefixOfFilter() throws MissingObjectException,
 			IncorrectObjectTypeException, IOException {
-		assertTrue(filter.include(fakeWalk("b")));
-		assertTrue(filter.include(fakeWalk("c/d")));
-		assertTrue(filter.include(fakeWalk("c/d")));
-		assertTrue(filter.include(fakeWalk("c")));
-		assertTrue(filter.include(fakeWalk("d/e/f")));
-		assertTrue(filter.include(fakeWalk("d/e")));
-		assertTrue(filter.include(fakeWalk("d")));
+		assertMatches(Sets.of("b/c"), fakeWalkAtSubtree("b"));
+		assertMatches(Sets.of("c/d/e", "c/d/f"), fakeWalkAtSubtree("c/d"));
+		assertMatches(Sets.of("c/d/e", "c/d/f"), fakeWalkAtSubtree("c"));
+		assertMatches(Sets.of("d/e/f/g", "d/e/f/g.x"),
+				fakeWalkAtSubtree("d/e/f"));
+		assertMatches(Sets.of("d/e/f/g", "d/e/f/g.x"),
+				fakeWalkAtSubtree("d/e"));
+		assertMatches(Sets.of("d/e/f/g", "d/e/f/g.x"), fakeWalkAtSubtree("d"));
+
+		assertNoMatches(fakeWalk("b"));
+		assertNoMatches(fakeWalk("c/d"));
+		assertNoMatches(fakeWalk("c"));
+		assertNoMatches(fakeWalk("d/e/f"));
+		assertNoMatches(fakeWalk("d/e"));
+		assertNoMatches(fakeWalk("d"));
+
 	}
 
 	@Test
 	public void testFilterIsPrefixOfKey() throws MissingObjectException,
 			IncorrectObjectTypeException, IOException {
-		assertTrue(filter.include(fakeWalk("a/b")));
-		assertTrue(filter.include(fakeWalk("b/c/d")));
-		assertTrue(filter.include(fakeWalk("c/d/e/f")));
-		assertTrue(filter.include(fakeWalk("c/d/f/g")));
-		assertTrue(filter.include(fakeWalk("d/e/f/g/h")));
-		assertTrue(filter.include(fakeWalk("d/e/f/g/y")));
-		assertTrue(filter.include(fakeWalk("d/e/f/g.x/h")));
-		// listed before g/y, so can't StopWalk here, but it's not included
-		// either
-		assertFalse(filter.include(fakeWalk("d/e/f/g.y")));
+		assertMatches(Sets.of("a"), fakeWalk("a/b"));
+		assertMatches(Sets.of("b/c"), fakeWalk("b/c/d"));
+		assertMatches(Sets.of("c/d/e"), fakeWalk("c/d/e/f"));
+		assertMatches(Sets.of("c/d/f"), fakeWalk("c/d/f/g"));
+		assertMatches(Sets.of("d/e/f/g"), fakeWalk("d/e/f/g/h"));
+		assertMatches(Sets.of("d/e/f/g"), fakeWalk("d/e/f/g/y"));
+		assertMatches(Sets.of("d/e/f/g.x"), fakeWalk("d/e/f/g.x/h"));
 	}
 
 	@Test
@@ -182,6 +203,10 @@ public void testStopWalk() throws MissingObjectException,
 		// less obvious #2 due to git sorting order
 		filter.include(fakeWalk("d/e/f/g/h.txt"));
 
+		// listed before g/y, so can't StopWalk here
+		filter.include(fakeWalk("d/e/f/g.y"));
+		singles.get("d/e/f/g").include(fakeWalk("d/e/f/g.y"));
+
 		// non-ascii
 		try {
 			filter.include(fakeWalk("\u00C0"));
@@ -191,6 +216,44 @@ public void testStopWalk() throws MissingObjectException,
 		}
 	}
 
+	private void assertNoMatches(TreeWalk tw) throws MissingObjectException,
+			IncorrectObjectTypeException, IOException {
+		assertMatches(Sets.<String> of(), tw);
+	}
+
+	private void assertMatches(Set<String> expect, TreeWalk tw)
+			throws MissingObjectException, IncorrectObjectTypeException,
+			IOException {
+		List<String> actual = new ArrayList<>();
+		for (String path : singles.keySet()) {
+			if (includes(singles.get(path), tw)) {
+				actual.add(path);
+			}
+		}
+
+		String[] e = expect.toArray(new String[expect.size()]);
+		String[] a = actual.toArray(new String[actual.size()]);
+		Arrays.sort(e);
+		Arrays.sort(a);
+		assertArrayEquals(e, a);
+
+		if (expect.isEmpty()) {
+			assertFalse(includes(filter, tw));
+		} else {
+			assertTrue(includes(filter, tw));
+		}
+	}
+
+	private static boolean includes(TreeFilter f, TreeWalk tw)
+			throws MissingObjectException, IncorrectObjectTypeException,
+			IOException {
+		try {
+			return f.include(tw);
+		} catch (StopWalkException e) {
+			return false;
+		}
+	}
+
 	TreeWalk fakeWalk(final String path) throws IOException {
 		DirCache dc = DirCache.newInCore();
 		DirCacheEditor dce = dc.editor();
@@ -210,4 +273,25 @@ public void apply(DirCacheEntry ent) {
 		return ret;
 	}
 
+	TreeWalk fakeWalkAtSubtree(final String path) throws IOException {
+		DirCache dc = DirCache.newInCore();
+		DirCacheEditor dce = dc.editor();
+		dce.add(new DirCacheEditor.PathEdit(path + "/README") {
+			public void apply(DirCacheEntry ent) {
+				ent.setFileMode(FileMode.REGULAR_FILE);
+			}
+		});
+		dce.finish();
+
+		TreeWalk ret = new TreeWalk((ObjectReader) null);
+		ret.addTree(new DirCacheIterator(dc));
+		ret.next();
+		while (!path.equals(ret.getPathString())) {
+			if (ret.isSubtree()) {
+				ret.enterSubtree();
+			}
+			ret.next();
+		}
+		return ret;
+	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/ChangeIdUtilTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/ChangeIdUtilTest.java
index 7273cdb..aaeb79c 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/ChangeIdUtilTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/ChangeIdUtilTest.java
@@ -45,7 +45,6 @@
 
 import static org.junit.Assert.assertEquals;
 
-import java.io.IOException;
 import java.util.concurrent.TimeUnit;
 
 import org.eclipse.jgit.junit.MockSystemReader;
@@ -113,7 +112,7 @@ public void testClean() {
 	}
 
 	@Test
-	public void testId() throws IOException {
+	public void testId() {
 		String msg = "A\nMessage\n";
 		ObjectId id = ChangeIdUtil.computeChangeId(treeId, parentId, p, q, msg);
 		assertEquals("73f3751208ac92cbb76f9a26ac4a0d9d472e381b", ObjectId
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FileUtilTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FileUtilTest.java
index 0d7d31b..1f78e02 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FileUtilTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FileUtilTest.java
@@ -54,6 +54,7 @@
 
 import org.eclipse.jgit.junit.JGitTestUtil;
 import org.junit.After;
+import org.junit.Assume;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -424,19 +425,28 @@ public void testRenameOverExistingEmptyDirectory() throws IOException {
 	@Test
 	public void testCreateSymlink() throws IOException {
 		FS fs = FS.DETECTED;
-		try {
-			fs.createSymLink(new File(trash, "x"), "y");
-		} catch (IOException e) {
-			if (fs.supportsSymlinks())
-				fail("FS claims to support symlinks but attempt to create symlink failed");
-			return;
-		}
-		assertTrue(fs.supportsSymlinks());
+		// show test as ignored if the FS doesn't support symlinks
+		Assume.assumeTrue(fs.supportsSymlinks());
+		fs.createSymLink(new File(trash, "x"), "y");
 		String target = fs.readSymLink(new File(trash, "x"));
 		assertEquals("y", target);
 	}
 
 	@Test
+	public void testCreateSymlinkOverrideExisting() throws IOException {
+		FS fs = FS.DETECTED;
+		// show test as ignored if the FS doesn't support symlinks
+		Assume.assumeTrue(fs.supportsSymlinks());
+		File file = new File(trash, "x");
+		fs.createSymLink(file, "y");
+		String target = fs.readSymLink(file);
+		assertEquals("y", target);
+		fs.createSymLink(file, "z");
+		target = fs.readSymLink(file);
+		assertEquals("z", target);
+	}
+
+	@Test
 	public void testRelativize_doc() {
 		// This is the javadoc example
 		String base = toOSPathString("c:\\Users\\jdoe\\eclipse\\git\\project");
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/PathsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/PathsTest.java
new file mode 100644
index 0000000..7542ec8
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/PathsTest.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2016, 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.util;
+
+import static org.eclipse.jgit.util.Paths.compare;
+import static org.eclipse.jgit.util.Paths.compareSameName;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.FileMode;
+import org.junit.Test;
+
+public class PathsTest {
+	@Test
+	public void testStripTrailingSeparator() {
+		assertNull(Paths.stripTrailingSeparator(null));
+		assertEquals("", Paths.stripTrailingSeparator(""));
+		assertEquals("a", Paths.stripTrailingSeparator("a"));
+		assertEquals("a/boo", Paths.stripTrailingSeparator("a/boo"));
+		assertEquals("a/boo", Paths.stripTrailingSeparator("a/boo/"));
+		assertEquals("a/boo", Paths.stripTrailingSeparator("a/boo//"));
+		assertEquals("a/boo", Paths.stripTrailingSeparator("a/boo///"));
+	}
+
+	@Test
+	public void testPathCompare() {
+		byte[] a = Constants.encode("afoo/bar.c");
+		byte[] b = Constants.encode("bfoo/bar.c");
+
+		assertEquals(0, compare(a, 1, a.length, 0, b, 1, b.length, 0));
+		assertEquals(-1, compare(a, 0, a.length, 0, b, 0, b.length, 0));
+		assertEquals(1, compare(b, 0, b.length, 0, a, 0, a.length, 0));
+
+		a = Constants.encode("a");
+		b = Constants.encode("aa");
+		assertEquals(-97, compare(a, 0, a.length, 0, b, 0, b.length, 0));
+		assertEquals(0, compare(a, 0, a.length, 0, b, 0, 1, 0));
+		assertEquals(0, compare(a, 0, a.length, 0, b, 1, 2, 0));
+		assertEquals(0, compareSameName(a, 0, a.length, b, 1, b.length, 0));
+		assertEquals(0, compareSameName(a, 0, a.length, b, 0, 1, 0));
+		assertEquals(-50, compareSameName(a, 0, a.length, b, 0, b.length, 0));
+		assertEquals(97, compareSameName(b, 0, b.length, a, 0, a.length, 0));
+
+		a = Constants.encode("a");
+		b = Constants.encode("a");
+		assertEquals(0, compare(
+				a, 0, a.length, FileMode.TREE.getBits(),
+				b, 0, b.length, FileMode.TREE.getBits()));
+		assertEquals(0, compare(
+				a, 0, a.length, FileMode.REGULAR_FILE.getBits(),
+				b, 0, b.length, FileMode.REGULAR_FILE.getBits()));
+		assertEquals(-47, compare(
+				a, 0, a.length, FileMode.REGULAR_FILE.getBits(),
+				b, 0, b.length, FileMode.TREE.getBits()));
+		assertEquals(47, compare(
+				a, 0, a.length, FileMode.TREE.getBits(),
+				b, 0, b.length, FileMode.REGULAR_FILE.getBits()));
+
+		assertEquals(0, compareSameName(
+				a, 0, a.length,
+				b, 0, b.length, FileMode.TREE.getBits()));
+		assertEquals(0, compareSameName(
+				a, 0, a.length,
+				b, 0, b.length, FileMode.REGULAR_FILE.getBits()));
+
+		a = Constants.encode("a.c");
+		b = Constants.encode("a");
+		byte[] c = Constants.encode("a0c");
+		assertEquals(-1, compare(
+				a, 0, a.length, FileMode.REGULAR_FILE.getBits(),
+				b, 0, b.length, FileMode.TREE.getBits()));
+		assertEquals(-1, compare(
+				b, 0, b.length, FileMode.TREE.getBits(),
+				c, 0, c.length, FileMode.REGULAR_FILE.getBits()));
+	}
+}
diff --git a/org.eclipse.jgit.ui/BUCK b/org.eclipse.jgit.ui/BUCK
new file mode 100644
index 0000000..fcd87cf
--- /dev/null
+++ b/org.eclipse.jgit.ui/BUCK
@@ -0,0 +1,7 @@
+java_library(
+  name = 'ui',
+  srcs = glob(['src/**']),
+  resources = glob(['resources/**']),
+  deps = ['//org.eclipse.jgit:jgit'],
+  visibility = ['PUBLIC'],
+)
diff --git a/org.eclipse.jgit.ui/src/org/eclipse/jgit/awtui/AwtCredentialsProvider.java b/org.eclipse.jgit.ui/src/org/eclipse/jgit/awtui/AwtCredentialsProvider.java
index fd26bfa..a9967ae 100644
--- a/org.eclipse.jgit.ui/src/org/eclipse/jgit/awtui/AwtCredentialsProvider.java
+++ b/org.eclipse.jgit.ui/src/org/eclipse/jgit/awtui/AwtCredentialsProvider.java
@@ -56,15 +56,20 @@
 import javax.swing.JTextField;
 
 import org.eclipse.jgit.errors.UnsupportedCredentialItem;
+import org.eclipse.jgit.transport.ChainingCredentialsProvider;
 import org.eclipse.jgit.transport.CredentialItem;
 import org.eclipse.jgit.transport.CredentialsProvider;
+import org.eclipse.jgit.transport.NetRCCredentialsProvider;
 import org.eclipse.jgit.transport.URIish;
 
 /** Interacts with the user during authentication by using AWT/Swing dialogs. */
 public class AwtCredentialsProvider extends CredentialsProvider {
 	/** Install this implementation as the default. */
 	public static void install() {
-		CredentialsProvider.setDefault(new AwtCredentialsProvider());
+		final AwtCredentialsProvider c = new AwtCredentialsProvider();
+		CredentialsProvider cp = new ChainingCredentialsProvider(
+				new NetRCCredentialsProvider(), c);
+		CredentialsProvider.setDefault(cp);
 	}
 
 	@Override
diff --git a/org.eclipse.jgit/.settings/.api_filters b/org.eclipse.jgit/.settings/.api_filters
index b2a8f67..36041f8 100644
--- a/org.eclipse.jgit/.settings/.api_filters
+++ b/org.eclipse.jgit/.settings/.api_filters
@@ -1,5 +1,45 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <component id="org.eclipse.jgit" version="2">
+    <resource path="META-INF/MANIFEST.MF" type="org.eclipse.jgit.lib.FileTreeEntry">
+        <filter id="305324134">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.lib.FileTreeEntry"/>
+                <message_argument value="org.eclipse.jgit_4.2.0"/>
+            </message_arguments>
+        </filter>
+    </resource>
+    <resource path="META-INF/MANIFEST.MF" type="org.eclipse.jgit.lib.GitlinkTreeEntry">
+        <filter id="305324134">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.lib.GitlinkTreeEntry"/>
+                <message_argument value="org.eclipse.jgit_4.2.0"/>
+            </message_arguments>
+        </filter>
+    </resource>
+    <resource path="META-INF/MANIFEST.MF" type="org.eclipse.jgit.lib.SymlinkTreeEntry">
+        <filter id="305324134">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.lib.SymlinkTreeEntry"/>
+                <message_argument value="org.eclipse.jgit_4.2.0"/>
+            </message_arguments>
+        </filter>
+    </resource>
+    <resource path="META-INF/MANIFEST.MF" type="org.eclipse.jgit.lib.Tree">
+        <filter id="305324134">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.lib.Tree"/>
+                <message_argument value="org.eclipse.jgit_4.2.0"/>
+            </message_arguments>
+        </filter>
+    </resource>
+    <resource path="META-INF/MANIFEST.MF" type="org.eclipse.jgit.lib.TreeEntry">
+        <filter id="305324134">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.lib.TreeEntry"/>
+                <message_argument value="org.eclipse.jgit_4.2.0"/>
+            </message_arguments>
+        </filter>
+    </resource>
     <resource path="src/org/eclipse/jgit/attributes/AttributesNode.java" type="org.eclipse.jgit.attributes.AttributesNode">
         <filter comment="attributes weren't really usable in earlier versions" id="338792546">
             <message_arguments>
diff --git a/org.eclipse.jgit/BUCK b/org.eclipse.jgit/BUCK
new file mode 100644
index 0000000..73e2080
--- /dev/null
+++ b/org.eclipse.jgit/BUCK
@@ -0,0 +1,20 @@
+SRCS = glob(['src/**'])
+RESOURCES = glob(['resources/**'])
+
+java_library(
+  name = 'jgit',
+  srcs = SRCS,
+  resources = RESOURCES,
+  deps = [
+    '//lib:javaewah',
+    '//lib:jsch',
+    '//lib:httpcomponents',
+    '//lib:slf4j-api',
+  ],
+  visibility = ['PUBLIC'],
+)
+
+java_sources(
+  name = 'jgit_src',
+  srcs = SRCS + RESOURCES,
+)
diff --git a/org.eclipse.jgit/META-INF/MANIFEST.MF b/org.eclipse.jgit/META-INF/MANIFEST.MF
index 2a953b5..3d3e74f 100644
--- a/org.eclipse.jgit/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit/META-INF/MANIFEST.MF
@@ -59,15 +59,17 @@
  org.eclipse.jgit.ignore;version="4.2.0",
  org.eclipse.jgit.ignore.internal;version="4.2.0";x-friends:="org.eclipse.jgit.test",
  org.eclipse.jgit.internal;version="4.2.0";x-friends:="org.eclipse.jgit.test,org.eclipse.jgit.http.test",
+ org.eclipse.jgit.internal.ketch;version="4.2.0";x-friends:="org.eclipse.jgit.junit,org.eclipse.jgit.test,org.eclipse.jgit.pgm",
  org.eclipse.jgit.internal.storage.dfs;version="4.2.0";x-friends:="org.eclipse.jgit.test,org.eclipse.jgit.http.server",
  org.eclipse.jgit.internal.storage.file;version="4.2.0";
   x-friends:="org.eclipse.jgit.test,
    org.eclipse.jgit.junit,
    org.eclipse.jgit.junit.http,
    org.eclipse.jgit.http.server,
-   org.eclipse.jgit.java7.test,
+   org.eclipse.jgit.pgm.test,
    org.eclipse.jgit.pgm",
  org.eclipse.jgit.internal.storage.pack;version="4.2.0";x-friends:="org.eclipse.jgit.junit,org.eclipse.jgit.test,org.eclipse.jgit.pgm",
+ org.eclipse.jgit.internal.storage.reftree;version="4.2.0";x-friends:="org.eclipse.jgit.junit,org.eclipse.jgit.test,org.eclipse.jgit.pgm",
  org.eclipse.jgit.lib;version="4.2.0";
   uses:="org.eclipse.jgit.revwalk,
    org.eclipse.jgit.treewalk.filter,
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 0e9b0b5..992e10b 100644
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
@@ -99,6 +99,7 @@
 cannotStoreObjects=cannot store objects
 cannotResolveUniquelyAbbrevObjectId=Could not resolve uniquely the abbreviated object ID
 cannotUnloadAModifiedTree=Cannot unload a modified tree.
+cannotUpdateUnbornBranch=Cannot update unborn branch
 cannotWorkWithOtherStagesThanZeroRightNow=Cannot work with other stages than zero right now. Won't write corrupt index.
 cannotWriteObjectsPath=Cannot write {0}/{1}: {2}
 canOnlyCherryPickCommitsWithOneParent=Cannot cherry-pick commit ''{0}'' because it has {1} parents, only commits with exactly one parent are supported.
@@ -125,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
@@ -151,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
@@ -181,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
@@ -432,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}.
@@ -595,6 +599,7 @@
 transportExceptionMissingAssumed=Missing assumed {0}
 transportExceptionReadRef=read {0}
 transportNeedsRepository=Transport needs repository
+transportProvidedRefWithNoObjectId=Transport provided ref {0} with no object id
 transportProtoAmazonS3=Amazon S3
 transportProtoBundleFile=Git Bundle File
 transportProtoFTP=FTP
diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/ketch/KetchText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/ketch/KetchText.properties
new file mode 100644
index 0000000..1fbb7cb
--- /dev/null
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/ketch/KetchText.properties
@@ -0,0 +1,13 @@
+accepted=accepted.
+cannotFetchFromLocalReplica=cannot fetch from LocalReplica
+failed=failed!
+invalidFollowerUri=invalid follower URI
+leaderFailedToStore=leader failed to store
+localReplicaRequired=LocalReplica instance is required
+mismatchedTxnNamespace=mismatched txnNamespace; expected {0} found {1}
+outsideTxnNamespace=ref {0} is outside of txnNamespace {1}
+proposingUpdates=Proposing updates
+queuedProposalFailedToApply=queued proposal failed to apply
+starting=starting!
+unsupportedVoterCount=unsupported voter count {0}, expected one of {1}
+waitingForQueue=Waiting for queue
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java
index 67fb342..3b94f16 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/AddCommand.java
@@ -43,6 +43,10 @@
  */
 package org.eclipse.jgit.api;
 
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+import static org.eclipse.jgit.lib.FileMode.GITLINK;
+import static org.eclipse.jgit.lib.FileMode.TYPE_TREE;
+
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.Collection;
@@ -58,12 +62,12 @@
 import org.eclipse.jgit.dircache.DirCacheEntry;
 import org.eclipse.jgit.dircache.DirCacheIterator;
 import org.eclipse.jgit.internal.JGitText;
-import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.treewalk.FileTreeIterator;
-import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.NameConflictTreeWalk;
 import org.eclipse.jgit.treewalk.TreeWalk.OperationType;
 import org.eclipse.jgit.treewalk.WorkingTreeIterator;
 import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
@@ -135,15 +139,12 @@ public DirCache call() throws GitAPIException, NoFilepatternException {
 			throw new NoFilepatternException(JGitText.get().atLeastOnePatternIsRequired);
 		checkCallable();
 		DirCache dc = null;
-		boolean addAll = false;
-		if (filepatterns.contains(".")) //$NON-NLS-1$
-			addAll = true;
+		boolean addAll = filepatterns.contains("."); //$NON-NLS-1$
 
 		try (ObjectInserter inserter = repo.newObjectInserter();
-				final TreeWalk tw = new TreeWalk(repo)) {
+				NameConflictTreeWalk tw = new NameConflictTreeWalk(repo)) {
 			tw.setOperationType(OperationType.CHECKIN_OP);
 			dc = repo.lockDirCache();
-			DirCacheIterator c;
 
 			DirCacheBuilder builder = dc.builder();
 			tw.addTree(new DirCacheBuildIterator(builder));
@@ -151,62 +152,85 @@ public DirCache call() throws GitAPIException, NoFilepatternException {
 				workingTreeIterator = new FileTreeIterator(repo);
 			workingTreeIterator.setDirCacheIterator(tw, 0);
 			tw.addTree(workingTreeIterator);
-			tw.setRecursive(true);
 			if (!addAll)
 				tw.setFilter(PathFilterGroup.createFromStrings(filepatterns));
 
-			String lastAddedFile = null;
+			byte[] lastAdded = null;
 
 			while (tw.next()) {
-				String path = tw.getPathString();
-
+				DirCacheIterator c = tw.getTree(0, DirCacheIterator.class);
 				WorkingTreeIterator f = tw.getTree(1, WorkingTreeIterator.class);
-				if (tw.getTree(0, DirCacheIterator.class) == null &&
-						f != null && f.isEntryIgnored()) {
+				if (c == null && f != null && f.isEntryIgnored()) {
 					// file is not in index but is ignored, do nothing
+					continue;
+				} else if (c == null && update) {
+					// Only update of existing entries was requested.
+					continue;
 				}
-				// In case of an existing merge conflict the
-				// DirCacheBuildIterator iterates over all stages of
-				// this path, we however want to add only one
-				// new DirCacheEntry per path.
-				else if (!(path.equals(lastAddedFile))) {
-					if (!(update && tw.getTree(0, DirCacheIterator.class) == null)) {
-						c = tw.getTree(0, DirCacheIterator.class);
-						if (f != null) { // the file exists
-							long sz = f.getEntryLength();
-							DirCacheEntry entry = new DirCacheEntry(path);
-							if (c == null || c.getDirCacheEntry() == null
-									|| !c.getDirCacheEntry().isAssumeValid()) {
-								FileMode mode = f.getIndexFileMode(c);
-								entry.setFileMode(mode);
 
-								if (FileMode.GITLINK != mode) {
-									entry.setLength(sz);
-									entry.setLastModified(f
-											.getEntryLastModified());
-									long contentSize = f
-											.getEntryContentLength();
-									InputStream in = f.openEntryStream();
-									try {
-										entry.setObjectId(inserter.insert(
-												Constants.OBJ_BLOB, contentSize, in));
-									} finally {
-										in.close();
-									}
-								} else
-									entry.setObjectId(f.getEntryObjectId());
-								builder.add(entry);
-								lastAddedFile = path;
-							} else {
-								builder.add(c.getDirCacheEntry());
-							}
+				DirCacheEntry entry = c != null ? c.getDirCacheEntry() : null;
+				if (entry != null && entry.getStage() > 0
+						&& lastAdded != null
+						&& lastAdded.length == tw.getPathLength()
+						&& tw.isPathPrefix(lastAdded, lastAdded.length) == 0) {
+					// In case of an existing merge conflict the
+					// DirCacheBuildIterator iterates over all stages of
+					// this path, we however want to add only one
+					// new DirCacheEntry per path.
+					continue;
+				}
 
-						} else if (c != null
-								&& (!update || FileMode.GITLINK == c
-										.getEntryFileMode()))
-							builder.add(c.getDirCacheEntry());
+				if (tw.isSubtree() && !tw.isDirectoryFileConflict()) {
+					tw.enterSubtree();
+					continue;
+				}
+
+				if (f == null) { // working tree file does not exist
+					if (entry != null
+							&& (!update || GITLINK == entry.getFileMode())) {
+						builder.add(entry);
 					}
+					continue;
 				}
+
+				if (entry != null && entry.isAssumeValid()) {
+					// Index entry is marked assume valid. Even though
+					// the user specified the file to be added JGit does
+					// not consider the file for addition.
+					builder.add(entry);
+					continue;
+				}
+
+				if (f.getEntryRawMode() == TYPE_TREE) {
+					// Index entry exists and is symlink, gitlink or file,
+					// otherwise the tree would have been entered above.
+					// Replace the index entry by diving into tree of files.
+					tw.enterSubtree();
+					continue;
+				}
+
+				byte[] path = tw.getRawPath();
+				if (entry == null || entry.getStage() > 0) {
+					entry = new DirCacheEntry(path);
+				}
+				FileMode mode = f.getIndexFileMode(c);
+				entry.setFileMode(mode);
+
+				if (GITLINK != mode) {
+					entry.setLength(f.getEntryLength());
+					entry.setLastModified(f.getEntryLastModified());
+					long len = f.getEntryContentLength();
+					try (InputStream in = f.openEntryStream()) {
+						ObjectId id = inserter.insert(OBJ_BLOB, len, in);
+						entry.setObjectId(id);
+					}
+				} else {
+					entry.setLength(0);
+					entry.setLastModified(0);
+					entry.setObjectId(f.getEntryObjectId());
+				}
+				builder.add(entry);
+				lastAdded = path;
 			}
 			inserter.flush();
 			builder.commit();
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java
index 6a945e4..676ae03 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/ApplyCommand.java
@@ -47,6 +47,7 @@
 import java.io.FileWriter;
 import java.io.IOException;
 import java.io.InputStream;
+import java.nio.file.StandardCopyOption;
 import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.List;
@@ -141,9 +142,13 @@ public ApplyResult call() throws GitAPIException, PatchFormatException,
 				case RENAME:
 					f = getFile(fh.getOldPath(), false);
 					File dest = getFile(fh.getNewPath(), false);
-					if (!f.renameTo(dest))
+					try {
+						FileUtils.rename(f, dest,
+								StandardCopyOption.ATOMIC_MOVE);
+					} catch (IOException e) {
 						throw new PatchApplyException(MessageFormat.format(
-								JGitText.get().renameFileFailed, f, dest));
+								JGitText.get().renameFileFailed, f, dest), e);
+					}
 					break;
 				case COPY:
 					f = getFile(fh.getOldPath(), false);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java
index 8743ea9..4f918fa 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java
@@ -331,9 +331,16 @@ else if (orphan) {
 	}
 
 	private String getShortBranchName(Ref headRef) {
-		if (headRef.getTarget().getName().equals(headRef.getName()))
-			return headRef.getTarget().getObjectId().getName();
-		return Repository.shortenRefName(headRef.getTarget().getName());
+		if (headRef.isSymbolic()) {
+			return Repository.shortenRefName(headRef.getTarget().getName());
+		}
+		// Detached HEAD. Every non-symbolic ref in the ref database has an
+		// object id, so this cannot be null.
+		ObjectId id = headRef.getObjectId();
+		if (id == null) {
+			throw new NullPointerException();
+		}
+		return id.getName();
 	}
 
 	/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java
index b3bc319..2ac8729 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java
@@ -61,6 +61,7 @@
 import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefUpdate;
@@ -235,7 +236,7 @@ private void checkout(Repository clonedRepo, FetchResult result)
 		}
 
 		if (head == null || head.getObjectId() == null)
-			return; // throw exception?
+			return; // TODO throw exception?
 
 		if (head.getName().startsWith(Constants.R_HEADS)) {
 			final RefUpdate newHead = clonedRepo.updateRef(Constants.HEAD);
@@ -287,20 +288,24 @@ private void cloneSubmodules(Repository clonedRepo) throws IOException,
 
 	private Ref findBranchToCheckout(FetchResult result) {
 		final Ref idHEAD = result.getAdvertisedRef(Constants.HEAD);
-		if (idHEAD == null)
+		ObjectId headId = idHEAD != null ? idHEAD.getObjectId() : null;
+		if (headId == null) {
 			return null;
+		}
 
 		Ref master = result.getAdvertisedRef(Constants.R_HEADS
 				+ Constants.MASTER);
-		if (master != null && master.getObjectId().equals(idHEAD.getObjectId()))
+		ObjectId objectId = master != null ? master.getObjectId() : null;
+		if (headId.equals(objectId)) {
 			return master;
+		}
 
 		Ref foundBranch = null;
 		for (final Ref r : result.getAdvertisedRefs()) {
 			final String n = r.getName();
 			if (!n.startsWith(Constants.R_HEADS))
 				continue;
-			if (r.getObjectId().equals(idHEAD.getObjectId())) {
+			if (headId.equals(r.getObjectId())) {
 				foundBranch = r;
 				break;
 			}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java
index 6828ed3..b5057ad 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java
@@ -53,6 +53,7 @@
 
 import org.eclipse.jgit.api.errors.AbortedByHookException;
 import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
+import org.eclipse.jgit.api.errors.EmtpyCommitException;
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.api.errors.JGitInternalException;
 import org.eclipse.jgit.api.errors.NoFilepatternException;
@@ -130,6 +131,8 @@ public class CommitCommand extends GitCommand<RevCommit> {
 
 	private PrintStream hookOutRedirect;
 
+	private Boolean allowEmpty;
+
 	/**
 	 * @param repo
 	 */
@@ -231,6 +234,16 @@ public RevCommit call() throws GitAPIException, NoHeadException,
 				if (insertChangeId)
 					insertChangeId(indexTreeId);
 
+				// Check for empty commits
+				if (headId != null && !allowEmpty.booleanValue()) {
+					RevCommit headCommit = rw.parseCommit(headId);
+					headCommit.getTree();
+					if (indexTreeId.equals(headCommit.getTree())) {
+						throw new EmtpyCommitException(
+								JGitText.get().emptyCommit);
+					}
+				}
+
 				// Create a Commit object, populate it and write it
 				CommitBuilder commit = new CommitBuilder();
 				commit.setCommitter(committer);
@@ -457,6 +470,8 @@ private DirCache createTemporaryIndex(ObjectId headId, DirCache index,
 
 		// there must be at least one change
 		if (emptyCommit)
+			// Would like to throw a EmptyCommitException. But this would break the API
+			// TODO(ch): Change this in the next release
 			throw new JGitInternalException(JGitText.get().emptyCommit);
 
 		// update index
@@ -510,6 +525,12 @@ private void processOptions(RepositoryState state, RevWalk rw)
 			committer = new PersonIdent(repo);
 		if (author == null && !amend)
 			author = committer;
+		if (allowEmpty == null)
+			// JGit allows empty commits by default. Only when pathes are
+			// specified the commit should not be empty. This behaviour differs
+			// from native git but can only be adapted in the next release.
+			// TODO(ch) align the defaults with native git
+			allowEmpty = (only.isEmpty()) ? Boolean.TRUE : Boolean.FALSE;
 
 		// when doing a merge commit parse MERGE_HEAD and MERGE_MSG files
 		if (state == RepositoryState.MERGING_RESOLVED
@@ -579,6 +600,27 @@ public CommitCommand setMessage(String message) {
 	}
 
 	/**
+	 * @param allowEmpty
+	 *            whether it should be allowed to create a commit which has the
+	 *            same tree as it's sole predecessor (a commit which doesn't
+	 *            change anything). By default when creating standard commits
+	 *            (without specifying paths) JGit allows to create such commits.
+	 *            When this flag is set to false an attempt to create an "empty"
+	 *            standard commit will lead to an EmptyCommitException.
+	 *            <p>
+	 *            By default when creating a commit containing only specified
+	 *            paths an attempt to create an empty commit leads to a
+	 *            {@link JGitInternalException}. By setting this flag to
+	 *            <code>true</code> this exception will not be thrown.
+	 * @return {@code this}
+	 * @since 4.2
+	 */
+	public CommitCommand setAllowEmpty(boolean allowEmpty) {
+		this.allowEmpty = Boolean.valueOf(allowEmpty);
+		return this;
+	}
+
+	/**
 	 * @return the commit message used for the <code>commit</code>
 	 */
 	public String getMessage() {
@@ -681,7 +723,7 @@ public PersonIdent getAuthor() {
 	 */
 	public CommitCommand setAll(boolean all) {
 		checkCallable();
-		if (!only.isEmpty())
+		if (all && !only.isEmpty())
 			throw new JGitInternalException(MessageFormat.format(
 					JGitText.get().illegalCombinationOfArguments, "--all", //$NON-NLS-1$
 					"--only")); //$NON-NLS-1$
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java
index 9620089..de51276 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java
@@ -116,22 +116,17 @@ public FetchResult call() throws GitAPIException, InvalidRemoteException,
 			org.eclipse.jgit.api.errors.TransportException {
 		checkCallable();
 
-		try {
-			Transport transport = Transport.open(repo, remote);
-			try {
-				transport.setCheckFetchedObjects(checkFetchedObjects);
-				transport.setRemoveDeletedRefs(isRemoveDeletedRefs());
-				transport.setDryRun(dryRun);
-				if (tagOption != null)
-					transport.setTagOpt(tagOption);
-				transport.setFetchThin(thin);
-				configure(transport);
+		try (Transport transport = Transport.open(repo, remote)) {
+			transport.setCheckFetchedObjects(checkFetchedObjects);
+			transport.setRemoveDeletedRefs(isRemoveDeletedRefs());
+			transport.setDryRun(dryRun);
+			if (tagOption != null)
+				transport.setTagOpt(tagOption);
+			transport.setFetchThin(thin);
+			configure(transport);
 
-				FetchResult result = transport.fetch(monitor, refSpecs);
-				return result;
-			} finally {
-				transport.close();
-			}
+			FetchResult result = transport.fetch(monitor, refSpecs);
+			return result;
 		} catch (NoRemoteRepositoryException e) {
 			throw new InvalidRemoteException(MessageFormat.format(
 					JGitText.get().invalidRemote, remote), e);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/LsRemoteCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/LsRemoteCommand.java
index 3363a0f..f3527fd 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/LsRemoteCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/LsRemoteCommand.java
@@ -182,13 +182,9 @@ private Map<String, Ref> execute() throws GitAPIException,
 			org.eclipse.jgit.api.errors.TransportException {
 		checkCallable();
 
-		Transport transport = null;
-		FetchConnection fc = null;
-		try {
-			if (repo != null)
-				transport = Transport.open(repo, remote);
-			else
-				transport = Transport.open(new URIish(remote));
+		try (Transport transport = repo != null
+				? Transport.open(repo, remote)
+				: Transport.open(new URIish(remote))) {
 			transport.setOptionUploadPack(uploadPack);
 			configure(transport);
 			Collection<RefSpec> refSpecs = new ArrayList<RefSpec>(1);
@@ -199,19 +195,20 @@ private Map<String, Ref> execute() throws GitAPIException,
 				refSpecs.add(new RefSpec("refs/heads/*:refs/remotes/origin/*")); //$NON-NLS-1$
 			Collection<Ref> refs;
 			Map<String, Ref> refmap = new HashMap<String, Ref>();
-			fc = transport.openFetch();
-			refs = fc.getRefs();
-			if (refSpecs.isEmpty())
-				for (Ref r : refs)
-					refmap.put(r.getName(), r);
-			else
-				for (Ref r : refs)
-					for (RefSpec rs : refSpecs)
-						if (rs.matchSource(r)) {
-							refmap.put(r.getName(), r);
-							break;
-						}
-			return refmap;
+			try (FetchConnection fc = transport.openFetch()) {
+				refs = fc.getRefs();
+				if (refSpecs.isEmpty())
+					for (Ref r : refs)
+						refmap.put(r.getName(), r);
+				else
+					for (Ref r : refs)
+						for (RefSpec rs : refSpecs)
+							if (rs.matchSource(r)) {
+								refmap.put(r.getName(), r);
+								break;
+							}
+				return refmap;
+			}
 		} catch (URISyntaxException e) {
 			throw new InvalidRemoteException(MessageFormat.format(
 					JGitText.get().invalidRemote, remote));
@@ -223,11 +220,6 @@ private Map<String, Ref> execute() throws GitAPIException,
 			throw new org.eclipse.jgit.api.errors.TransportException(
 					e.getMessage(),
 					e);
-		} finally {
-			if (fc != null)
-				fc.close();
-			if (transport != null)
-				transport.close();
 		}
 	}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java
index d2075a7..bfe90a3 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java
@@ -1,6 +1,7 @@
 /*
  * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com>
  * Copyright (C) 2010-2014, Stefan Lay <stefan.lay@sap.com>
+ * Copyright (C) 2016, Laurent Delaigue <laurent.delaigue@obeo.fr>
  * and other copyright owners as documented in the project's IP log.
  *
  * This program and the accompanying materials are made available
@@ -65,8 +66,10 @@
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Config.ConfigEnum;
 import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Ref.Storage;
 import org.eclipse.jgit.lib.RefUpdate;
@@ -106,6 +109,8 @@ public class MergeCommand extends GitCommand<MergeResult> {
 
 	private String message;
 
+	private ProgressMonitor monitor = NullProgressMonitor.INSTANCE;
+
 	/**
 	 * The modes available for fast forward merges corresponding to the
 	 * <code>--ff</code>, <code>--no-ff</code> and <code>--ff-only</code>
@@ -330,6 +335,7 @@ public MergeResult call() throws GitAPIException, NoHeadException,
 					repo.writeSquashCommitMsg(squashMessage);
 				}
 				Merger merger = mergeStrategy.newMerger(repo);
+				merger.setProgressMonitor(monitor);
 				boolean noProblems;
 				Map<String, org.eclipse.jgit.merge.MergeResult<?>> lowLevelResults = null;
 				Map<String, MergeFailureReason> failingPaths = null;
@@ -586,4 +592,23 @@ public MergeCommand setMessage(String message) {
 		this.message = message;
 		return this;
 	}
+
+	/**
+	 * The progress monitor associated with the diff operation. By default, this
+	 * is set to <code>NullProgressMonitor</code>
+	 *
+	 * @see NullProgressMonitor
+	 *
+	 * @param monitor
+	 *            A progress monitor
+	 * @return this instance
+	 * @since 4.2
+	 */
+	public MergeCommand setProgressMonitor(ProgressMonitor monitor) {
+		if (monitor == null) {
+			monitor = NullProgressMonitor.INSTANCE;
+		}
+		this.monitor = monitor;
+		return this;
+	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java
index 2783edd..549ef6c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java
@@ -1,6 +1,7 @@
 /*
  * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com>
  * Copyright (C) 2010, Mathias Kinzler <mathias.kinzler@sap.com>
+ * Copyright (C) 2016, Laurent Delaigue <laurent.delaigue@obeo.fr>
  * and other copyright owners as documented in the project's IP log.
  *
  * This program and the accompanying materials are made available
@@ -326,6 +327,7 @@ public PullResult call() throws GitAPIException,
 			MergeCommand merge = new MergeCommand(repo);
 			merge.include(upstreamName, commitToMerge);
 			merge.setStrategy(strategy);
+			merge.setProgressMonitor(monitor);
 			MergeResult mergeRes = merge.call();
 			monitor.update(1);
 			result = new PullResult(fetchRes, remote, mergeRes);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
index 8582bbb..643ec7a 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
@@ -1,5 +1,6 @@
 /*
  * Copyright (C) 2010, 2013 Mathias Kinzler <mathias.kinzler@sap.com>
+ * Copyright (C) 2016, Laurent Delaigue <laurent.delaigue@obeo.fr>
  * and other copyright owners as documented in the project's IP log.
  *
  * This program and the accompanying materials are made available
@@ -560,6 +561,8 @@ private RebaseResult cherryPickCommitPreservingMerges(RevCommit commitToPick)
 		lastStepWasForward = newHead != null;
 		if (!lastStepWasForward) {
 			ObjectId headId = getHead().getObjectId();
+			// getHead() checks for null
+			assert headId != null;
 			if (!AnyObjectId.equals(headId, newParents.get(0)))
 				checkoutCommit(headId.getName(), newParents.get(0));
 
@@ -609,6 +612,7 @@ private RebaseResult cherryPickCommitPreservingMerges(RevCommit commitToPick)
 					// their non-first parents rewritten
 					MergeCommand merge = git.merge()
 							.setFastForward(MergeCommand.FastForwardMode.NO_FF)
+							.setProgressMonitor(monitor)
 							.setCommit(false);
 					for (int i = 1; i < commitToPick.getParentCount(); i++)
 						merge.include(newParents.get(i));
@@ -674,6 +678,8 @@ private void writeRewrittenHashes() throws RevisionSyntaxException,
 			return;
 
 		ObjectId headId = getHead().getObjectId();
+		// getHead() checks for null
+		assert headId != null;
 		String head = headId.getName();
 		String currentCommits = rebaseState.readFile(CURRENT_COMMIT);
 		for (String current : currentCommits.split("\n")) //$NON-NLS-1$
@@ -1073,11 +1079,12 @@ private RebaseResult initFilesAndRewind() throws IOException,
 
 		Ref head = getHead();
 
-		String headName = getHeadName(head);
 		ObjectId headId = head.getObjectId();
-		if (headId == null)
+		if (headId == null) {
 			throw new RefNotFoundException(MessageFormat.format(
 					JGitText.get().refNotResolved, Constants.HEAD));
+		}
+		String headName = getHeadName(head);
 		RevCommit headCommit = walk.lookupCommit(headId);
 		RevCommit upstream = walk.lookupCommit(upstreamCommit.getId());
 
@@ -1188,10 +1195,14 @@ private List<RevCommit> calculatePickList(RevCommit headCommit)
 
 	private static String getHeadName(Ref head) {
 		String headName;
-		if (head.isSymbolic())
+		if (head.isSymbolic()) {
 			headName = head.getTarget().getName();
-		else
-			headName = head.getObjectId().getName();
+		} else {
+			ObjectId headId = head.getObjectId();
+			// the callers are checking this already
+			assert headId != null;
+			headName = headId.getName();
+		}
 		return headName;
 	}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java
index 8f4bc4f..4c91e6c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java
@@ -190,10 +190,8 @@ public Ref call() throws GitAPIException, CheckoutConflictException {
 				ObjectId origHead = ru.getOldObjectId();
 				if (origHead != null)
 					repo.writeOrigHead(origHead);
-				result = ru.getRef();
-			} else {
-				result = repo.getRef(Constants.HEAD);
 			}
+			result = repo.exactRef(Constants.HEAD);
 
 			if (mode == null)
 				mode = ResetType.MIXED;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashDropCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashDropCommand.java
index 7923fd4..f6903be 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashDropCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashDropCommand.java
@@ -46,6 +46,7 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.nio.file.StandardCopyOption;
 import java.text.MessageFormat;
 import java.util.List;
 
@@ -220,12 +221,14 @@ public ObjectId call() throws GitAPIException {
 						entry.getWho(), entry.getComment());
 				entryId = entry.getNewId();
 			}
-			if (!stashLockFile.renameTo(stashFile)) {
-				FileUtils.delete(stashFile);
-				if (!stashLockFile.renameTo(stashFile))
+			try {
+				FileUtils.rename(stashLockFile, stashFile,
+						StandardCopyOption.ATOMIC_MOVE);
+			} catch (IOException e) {
 					throw new JGitInternalException(MessageFormat.format(
 							JGitText.get().renameFileFailed,
-							stashLockFile.getPath(), stashFile.getPath()));
+								stashLockFile.getPath(), stashFile.getPath()),
+						e);
 			}
 		} catch (IOException e) {
 			throw new JGitInternalException(JGitText.get().stashDropFailed, e);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleUpdateCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleUpdateCommand.java
index e288d77..342d7f4 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleUpdateCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleUpdateCommand.java
@@ -1,5 +1,6 @@
 /*
  * Copyright (C) 2011, GitHub Inc.
+ * Copyright (C) 2016, Laurent Delaigue <laurent.delaigue@obeo.fr>
  * and other copyright owners as documented in the project's IP log.
  *
  * This program and the accompanying materials are made available
@@ -178,11 +179,13 @@ public Collection<String> call() throws InvalidConfigurationException,
 					if (ConfigConstants.CONFIG_KEY_MERGE.equals(update)) {
 						MergeCommand merge = new MergeCommand(submoduleRepo);
 						merge.include(commit);
+						merge.setProgressMonitor(monitor);
 						merge.setStrategy(strategy);
 						merge.call();
 					} else if (ConfigConstants.CONFIG_KEY_REBASE.equals(update)) {
 						RebaseCommand rebase = new RebaseCommand(submoduleRepo);
 						rebase.setUpstream(commit);
+						rebase.setProgressMonitor(monitor);
 						rebase.setStrategy(strategy);
 						rebase.call();
 					} else {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/TransportCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/TransportCommand.java
index 1aeb610..3d2e46b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/TransportCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/TransportCommand.java
@@ -79,6 +79,7 @@ public abstract class TransportCommand<C extends GitCommand, T> extends
 	 */
 	protected TransportCommand(final Repository repo) {
 		super(repo);
+		setCredentialsProvider(CredentialsProvider.getDefault());
 	}
 
 	/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/EmtpyCommitException.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/EmtpyCommitException.java
new file mode 100644
index 0000000..b3cc1bf
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/EmtpyCommitException.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2015, Christian Halstrick <christian.halstrick@sap.com>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v1.0 which accompanies this
+ * distribution, is reproduced below, and is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from this
+ * software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.api.errors;
+
+/**
+ * Exception thrown when a newly created commit does not contain any changes
+ *
+ * @since 4.2
+ */
+public class EmtpyCommitException extends GitAPIException {
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * @param message
+	 * @param cause
+	 */
+	public EmtpyCommitException(String message, Throwable cause) {
+		super(message, cause);
+	}
+
+	/**
+	 * @param message
+	 */
+	public EmtpyCommitException(String message) {
+		super(message);
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/BaseDirCacheEditor.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/BaseDirCacheEditor.java
index 70f80ae..0fbc1f8 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/BaseDirCacheEditor.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/BaseDirCacheEditor.java
@@ -44,8 +44,13 @@
 
 package org.eclipse.jgit.dircache;
 
+import static org.eclipse.jgit.lib.FileMode.TYPE_TREE;
+import static org.eclipse.jgit.util.Paths.compareSameName;
+
 import java.io.IOException;
 
+import org.eclipse.jgit.errors.DirCacheNameConflictException;
+
 /**
  * Generic update/editing support for {@link DirCache}.
  * <p>
@@ -168,6 +173,7 @@ protected void fastKeep(final int pos, int cnt) {
 	 * {@link #finish()}, and only after {@link #entries} is sorted.
 	 */
 	protected void replace() {
+		checkNameConflicts();
 		if (entryCnt < entries.length / 2) {
 			final DirCacheEntry[] n = new DirCacheEntry[entryCnt];
 			System.arraycopy(entries, 0, n, 0, entryCnt);
@@ -176,6 +182,76 @@ protected void replace() {
 		cache.replace(entries, entryCnt);
 	}
 
+	private void checkNameConflicts() {
+		int end = entryCnt - 1;
+		for (int eIdx = 0; eIdx < end; eIdx++) {
+			DirCacheEntry e = entries[eIdx];
+			if (e.getStage() != 0) {
+				continue;
+			}
+
+			byte[] ePath = e.path;
+			int prefixLen = lastSlash(ePath) + 1;
+
+			for (int nIdx = eIdx + 1; nIdx < entryCnt; nIdx++) {
+				DirCacheEntry n = entries[nIdx];
+				if (n.getStage() != 0) {
+					continue;
+				}
+
+				byte[] nPath = n.path;
+				if (!startsWith(ePath, nPath, prefixLen)) {
+					// Different prefix; this entry is in another directory.
+					break;
+				}
+
+				int s = nextSlash(nPath, prefixLen);
+				int m = s < nPath.length ? TYPE_TREE : n.getRawMode();
+				int cmp = compareSameName(
+						ePath, prefixLen, ePath.length,
+						nPath, prefixLen, s, m);
+				if (cmp < 0) {
+					break;
+				} else if (cmp == 0) {
+					throw new DirCacheNameConflictException(
+							e.getPathString(),
+							n.getPathString());
+				}
+			}
+		}
+	}
+
+	private static int lastSlash(byte[] path) {
+		for (int i = path.length - 1; i >= 0; i--) {
+			if (path[i] == '/') {
+				return i;
+			}
+		}
+		return -1;
+	}
+
+	private static int nextSlash(byte[] b, int p) {
+		final int n = b.length;
+		for (; p < n; p++) {
+			if (b[p] == '/') {
+				return p;
+			}
+		}
+		return n;
+	}
+
+	private static boolean startsWith(byte[] a, byte[] b, int n) {
+		if (b.length < n) {
+			return false;
+		}
+		for (n--; n >= 0; n--) {
+			if (a[n] != b[n]) {
+				return false;
+			}
+		}
+		return true;
+	}
+
 	/**
 	 * Finish, write, commit this change, and release the index lock.
 	 * <p>
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java
index fa03395..ecdfe82 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCache.java
@@ -800,8 +800,11 @@ public int findEntry(final String path) {
 	 *         information. If &lt; 0 the entry does not exist in the index.
 	 * @since 3.4
 	 */
-	public int findEntry(final byte[] p, final int pLen) {
-		int low = 0;
+	public int findEntry(byte[] p, int pLen) {
+		return findEntry(0, p, pLen);
+	}
+
+	int findEntry(int low, byte[] p, int pLen) {
 		int high = entryCnt;
 		while (low < high) {
 			int mid = (low + high) >>> 1;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheBuildIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheBuildIterator.java
index da55306..c10e416 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheBuildIterator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheBuildIterator.java
@@ -130,4 +130,9 @@ public void stopWalk() {
 		if (cur < cnt)
 			builder.keep(cur, cnt - cur);
 	}
+
+	@Override
+	protected boolean needsStopWalk() {
+		return ptr < cache.getEntryCount();
+	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
index 4eb6881..a1e1d15 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
@@ -46,6 +46,7 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
+import java.nio.file.StandardCopyOption;
 import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -1319,11 +1320,12 @@ public static void checkoutEntry(Repository repo, DirCacheEntry entry,
 			if (deleteRecursive && f.isDirectory()) {
 				FileUtils.delete(f, FileUtils.RECURSIVE);
 			}
-			FileUtils.rename(tmpFile, f);
+			FileUtils.rename(tmpFile, f, StandardCopyOption.ATOMIC_MOVE);
 		} catch (IOException e) {
-			throw new IOException(MessageFormat.format(
-					JGitText.get().renameFileFailed, tmpFile.getPath(),
-					f.getPath()));
+			throw new IOException(
+					MessageFormat.format(JGitText.get().renameFileFailed,
+							tmpFile.getPath(), f.getPath()),
+					e);
 		} finally {
 			if (tmpFile.exists()) {
 				FileUtils.delete(tmpFile);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEditor.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEditor.java
index 13885d3..c987c96 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEditor.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEditor.java
@@ -44,6 +44,10 @@
 
 package org.eclipse.jgit.dircache;
 
+import static org.eclipse.jgit.dircache.DirCache.cmp;
+import static org.eclipse.jgit.dircache.DirCacheTree.peq;
+import static org.eclipse.jgit.lib.FileMode.TYPE_TREE;
+
 import java.io.IOException;
 import java.text.MessageFormat;
 import java.util.ArrayList;
@@ -53,6 +57,7 @@
 
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.util.Paths;
 
 /**
  * Updates a {@link DirCache} by supplying discrete edit commands.
@@ -72,11 +77,12 @@ public class DirCacheEditor extends BaseDirCacheEditor {
 		public int compare(final PathEdit o1, final PathEdit o2) {
 			final byte[] a = o1.path;
 			final byte[] b = o2.path;
-			return DirCache.cmp(a, a.length, b, b.length);
+			return cmp(a, a.length, b, b.length);
 		}
 	};
 
 	private final List<PathEdit> edits;
+	private int editIdx;
 
 	/**
 	 * Construct a new editor.
@@ -126,35 +132,44 @@ public void finish() {
 
 	private void applyEdits() {
 		Collections.sort(edits, EDIT_CMP);
+		editIdx = 0;
 
 		final int maxIdx = cache.getEntryCount();
 		int lastIdx = 0;
-		for (final PathEdit e : edits) {
-			int eIdx = cache.findEntry(e.path, e.path.length);
+		while (editIdx < edits.size()) {
+			PathEdit e = edits.get(editIdx++);
+			int eIdx = cache.findEntry(lastIdx, e.path, e.path.length);
 			final boolean missing = eIdx < 0;
 			if (eIdx < 0)
 				eIdx = -(eIdx + 1);
 			final int cnt = Math.min(eIdx, maxIdx) - lastIdx;
 			if (cnt > 0)
 				fastKeep(lastIdx, cnt);
-			lastIdx = missing ? eIdx : cache.nextEntry(eIdx);
 
-			if (e instanceof DeletePath)
+			if (e instanceof DeletePath) {
+				lastIdx = missing ? eIdx : cache.nextEntry(eIdx);
 				continue;
+			}
 			if (e instanceof DeleteTree) {
 				lastIdx = cache.nextEntry(e.path, e.path.length, eIdx);
 				continue;
 			}
 
 			if (missing) {
-				final DirCacheEntry ent = new DirCacheEntry(e.path);
+				DirCacheEntry ent = new DirCacheEntry(e.path);
 				e.apply(ent);
-				if (ent.getRawMode() == 0)
-					throw new IllegalArgumentException(MessageFormat.format(JGitText.get().fileModeNotSetForPath
-							, ent.getPathString()));
+				if (ent.getRawMode() == 0) {
+					throw new IllegalArgumentException(MessageFormat.format(
+							JGitText.get().fileModeNotSetForPath,
+							ent.getPathString()));
+				}
+				lastIdx = e.replace
+					? deleteOverlappingSubtree(ent, eIdx)
+					: eIdx;
 				fastAdd(ent);
 			} else {
 				// Apply to all entries of the current path (different stages)
+				lastIdx = cache.nextEntry(eIdx);
 				for (int i = eIdx; i < lastIdx; i++) {
 					final DirCacheEntry ent = cache.getEntry(i);
 					e.apply(ent);
@@ -168,6 +183,102 @@ private void applyEdits() {
 			fastKeep(lastIdx, cnt);
 	}
 
+	private int deleteOverlappingSubtree(DirCacheEntry ent, int eIdx) {
+		byte[] entPath = ent.path;
+		int entLen = entPath.length;
+
+		// Delete any file that was previously processed and overlaps
+		// the parent directory for the new entry. Since the editor
+		// always processes entries in path order, binary search back
+		// for the overlap for each parent directory.
+		for (int p = pdir(entPath, entLen); p > 0; p = pdir(entPath, p)) {
+			int i = findEntry(entPath, p);
+			if (i >= 0) {
+				// A file does overlap, delete the file from the array.
+				// No other parents can have overlaps as the file should
+				// have taken care of that itself.
+				int n = --entryCnt - i;
+				System.arraycopy(entries, i + 1, entries, i, n);
+				break;
+			}
+
+			// If at least one other entry already exists in this parent
+			// directory there is no need to continue searching up the tree.
+			i = -(i + 1);
+			if (i < entryCnt && inDir(entries[i], entPath, p)) {
+				break;
+			}
+		}
+
+		int maxEnt = cache.getEntryCount();
+		if (eIdx >= maxEnt) {
+			return maxEnt;
+		}
+
+		DirCacheEntry next = cache.getEntry(eIdx);
+		if (Paths.compare(next.path, 0, next.path.length, 0,
+				entPath, 0, entLen, TYPE_TREE) < 0) {
+			// Next DirCacheEntry sorts before new entry as tree. Defer a
+			// DeleteTree command to delete any entries if they exist. This
+			// case only happens for A, A.c, A/c type of conflicts (rare).
+			insertEdit(new DeleteTree(entPath));
+			return eIdx;
+		}
+
+		// Next entry may be contained by the entry-as-tree, skip if so.
+		while (eIdx < maxEnt && inDir(cache.getEntry(eIdx), entPath, entLen)) {
+			eIdx++;
+		}
+		return eIdx;
+	}
+
+	private int findEntry(byte[] p, int pLen) {
+		int low = 0;
+		int high = entryCnt;
+		while (low < high) {
+			int mid = (low + high) >>> 1;
+			int cmp = cmp(p, pLen, entries[mid]);
+			if (cmp < 0) {
+				high = mid;
+			} else if (cmp == 0) {
+				while (mid > 0 && cmp(p, pLen, entries[mid - 1]) == 0) {
+					mid--;
+				}
+				return mid;
+			} else {
+				low = mid + 1;
+			}
+		}
+		return -(low + 1);
+	}
+
+	private void insertEdit(DeleteTree d) {
+		for (int i = editIdx; i < edits.size(); i++) {
+			int cmp = EDIT_CMP.compare(d, edits.get(i));
+			if (cmp < 0) {
+				edits.add(i, d);
+				return;
+			} else if (cmp == 0) {
+				return;
+			}
+		}
+		edits.add(d);
+	}
+
+	private static boolean inDir(DirCacheEntry e, byte[] path, int pLen) {
+		return e.path.length > pLen && e.path[pLen] == '/'
+				&& peq(path, e.path, pLen);
+	}
+
+	private static int pdir(byte[] path, int e) {
+		for (e--; e > 0; e--) {
+			if (path[e] == '/') {
+				return e;
+			}
+		}
+		return 0;
+	}
+
 	/**
 	 * Any index record update.
 	 * <p>
@@ -179,6 +290,7 @@ private void applyEdits() {
 	 */
 	public abstract static class PathEdit {
 		final byte[] path;
+		boolean replace = true;
 
 		/**
 		 * Create a new update command by path name.
@@ -190,6 +302,10 @@ public PathEdit(final String entryPath) {
 			path = Constants.encode(entryPath);
 		}
 
+		PathEdit(byte[] path) {
+			this.path = path;
+		}
+
 		/**
 		 * Create a new update command for an existing entry instance.
 		 *
@@ -202,6 +318,22 @@ public PathEdit(final DirCacheEntry ent) {
 		}
 
 		/**
+		 * Configure if a file can replace a directory (or vice versa).
+		 * <p>
+		 * Default is {@code true} as this is usually the desired behavior.
+		 *
+		 * @param ok
+		 *            if true a file can replace a directory, or a directory can
+		 *            replace a file.
+		 * @return {@code this}
+		 * @since 4.2
+		 */
+		public PathEdit setReplace(boolean ok) {
+			replace = ok;
+			return this;
+		}
+
+		/**
 		 * Apply the update to a single cache entry matching the path.
 		 * <p>
 		 * After apply is invoked the entry is added to the output table, and
@@ -212,6 +344,12 @@ public PathEdit(final DirCacheEntry ent) {
 		 *            the path is a new path in the index.
 		 */
 		public abstract void apply(DirCacheEntry ent);
+
+		@Override
+		public String toString() {
+			String p = DirCacheEntry.toString(path);
+			return getClass().getSimpleName() + '[' + p + ']';
+		}
 	}
 
 	/**
@@ -272,10 +410,26 @@ public static final class DeleteTree extends PathEdit {
 		 *            only the subtree's contents are matched by the command.
 		 *            The special case "" (not "/"!) deletes all entries.
 		 */
-		public DeleteTree(final String entryPath) {
-			super(
-					(entryPath.endsWith("/") || entryPath.length() == 0) ? entryPath //$NON-NLS-1$
-							: entryPath + "/"); //$NON-NLS-1$
+		public DeleteTree(String entryPath) {
+			super(entryPath.isEmpty()
+					|| entryPath.charAt(entryPath.length() - 1) == '/'
+					? entryPath
+					: entryPath + '/');
+		}
+
+		DeleteTree(byte[] path) {
+			super(appendSlash(path));
+		}
+
+		private static byte[] appendSlash(byte[] path) {
+			int n = path.length;
+			if (n > 0 && path[n - 1] != '/') {
+				byte[] r = new byte[n + 1];
+				System.arraycopy(path, 0, r, 0, n);
+				r[n] = '/';
+				return r;
+			}
+			return path;
 		}
 
 		public void apply(final DirCacheEntry ent) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java
index c8bc096..4ebf2e0 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheEntry.java
@@ -294,6 +294,23 @@ public DirCacheEntry(byte[] path, final int stage) {
 		NB.encodeInt16(info, infoOffset + P_FLAGS, flags);
 	}
 
+	/**
+	 * Duplicate DirCacheEntry with same path and copied info.
+	 * <p>
+	 * The same path buffer is reused (avoiding copying), however a new info
+	 * buffer is created and its contents are copied.
+	 *
+	 * @param src
+	 *            entry to clone.
+	 * @since 4.2
+	 */
+	public DirCacheEntry(DirCacheEntry src) {
+		path = src.path;
+		info = new byte[INFO_LEN];
+		infoOffset = 0;
+		System.arraycopy(src.info, src.infoOffset, info, 0, INFO_LEN);
+	}
+
 	void write(final OutputStream os) throws IOException {
 		final int len = isExtended() ? INFO_LEN_EXTENDED : INFO_LEN;
 		final int pathLen = path.length;
@@ -745,7 +762,7 @@ private static void checkPath(byte[] path) {
 		}
 	}
 
-	private static String toString(final byte[] path) {
+	static String toString(final byte[] path) {
 		return Constants.CHARSET.decode(ByteBuffer.wrap(path)).toString();
 	}
 
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/lib/SymlinkTreeEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/DirCacheNameConflictException.java
similarity index 61%
copy from org.eclipse.jgit/src/org/eclipse/jgit/lib/SymlinkTreeEntry.java
copy to org.eclipse.jgit/src/org/eclipse/jgit/errors/DirCacheNameConflictException.java
index c7e41bc..5f67e34 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/SymlinkTreeEntry.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/DirCacheNameConflictException.java
@@ -1,6 +1,5 @@
 /*
- * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org>
+ * 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
@@ -42,44 +41,40 @@
  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-package org.eclipse.jgit.lib;
+package org.eclipse.jgit.errors;
 
 /**
- * A tree entry representing a symbolic link.
+ * Thrown by DirCache code when entries overlap in impossible way.
  *
- * Note. Java cannot really handle these as file system objects.
- *
- * @deprecated To look up information about a single path, use
- * {@link org.eclipse.jgit.treewalk.TreeWalk#forPath(Repository, String, org.eclipse.jgit.revwalk.RevTree)}.
- * To lookup information about multiple paths at once, use a
- * {@link org.eclipse.jgit.treewalk.TreeWalk} and obtain the current entry's
- * information from its getter methods.
+ * @since 4.2
  */
-@Deprecated
-public class SymlinkTreeEntry extends TreeEntry {
+public class DirCacheNameConflictException extends IllegalStateException {
+	private static final long serialVersionUID = 1L;
+
+	private final String path1;
+	private final String path2;
 
 	/**
-	 * Construct a {@link SymlinkTreeEntry} with the specified name and SHA-1 in
-	 * the specified parent
+	 * Construct an exception for a specific path.
 	 *
-	 * @param parent
-	 * @param id
-	 * @param nameUTF8
+	 * @param path1
+	 *            one path that conflicts.
+	 * @param path2
+	 *            another path that conflicts.
 	 */
-	public SymlinkTreeEntry(final Tree parent, final ObjectId id,
-			final byte[] nameUTF8) {
-		super(parent, id, nameUTF8);
+	public DirCacheNameConflictException(String path1, String path2) {
+		super(path1 + ' ' + path2);
+		this.path1 = path1;
+		this.path2 = path2;
 	}
 
-	public FileMode getMode() {
-		return FileMode.SYMLINK;
+	/** @return one of the paths that has a conflict. */
+	public String getPath1() {
+		return path1;
 	}
 
-	public String toString() {
-		final StringBuilder r = new StringBuilder();
-		r.append(ObjectId.toString(getId()));
-		r.append(" S "); //$NON-NLS-1$
-		r.append(getFullName());
-		return r.toString();
+	/** @return another path that has a conflict. */
+	public String getPath2() {
+		return path2;
 	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java
index 891479d..7eb9550 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java
@@ -338,6 +338,20 @@ void removeOverlaps() {
 			else
 				last = p;
 		}
+		removeNestedCopyfiles();
+	}
+
+	/** Remove copyfiles that sit in a subdirectory of any other project. */
+	void removeNestedCopyfiles() {
+		for (RepoProject proj : filteredProjects) {
+			List<CopyFile> copyfiles = new ArrayList<>(proj.getCopyFiles());
+			proj.clearCopyFiles();
+			for (CopyFile copyfile : copyfiles) {
+				if (!isNestedCopyfile(copyfile)) {
+					proj.addCopyFile(copyfile);
+				}
+			}
+		}
 	}
 
 	boolean inGroups(RepoProject proj) {
@@ -357,4 +371,22 @@ boolean inGroups(RepoProject proj) {
 		}
 		return false;
 	}
+
+	private boolean isNestedCopyfile(CopyFile copyfile) {
+		if (copyfile.dest.indexOf('/') == -1) {
+			// If the copyfile is at root level then it won't be nested.
+			return false;
+		}
+		for (RepoProject proj : filteredProjects) {
+			if (proj.getPath().compareTo(copyfile.dest) > 0) {
+				// Early return as remaining projects can't be ancestor of this
+				// copyfile config (filteredProjects is sorted).
+				return false;
+			}
+			if (proj.isAncestorOf(copyfile.dest)) {
+				return true;
+			}
+		}
+		return false;
+	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java
index 9a07211..915066d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java
@@ -258,6 +258,15 @@ public void addCopyFiles(Collection<CopyFile> copyfiles) {
 		this.copyfiles.addAll(copyfiles);
 	}
 
+	/**
+	 * Clear all the copyfiles.
+	 *
+	 * @since 4.2
+	 */
+	public void clearCopyFiles() {
+		this.copyfiles.clear();
+	}
+
 	private String getPathWithSlash() {
 		if (path.endsWith("/")) //$NON-NLS-1$
 			return path;
@@ -273,7 +282,19 @@ private String getPathWithSlash() {
 	 * @return true if this sub repo is the ancestor of given sub repo.
 	 */
 	public boolean isAncestorOf(RepoProject that) {
-		return that.getPathWithSlash().startsWith(this.getPathWithSlash());
+		return isAncestorOf(that.getPathWithSlash());
+	}
+
+	/**
+	 * Check if this sub repo is an ancestor of the given path.
+	 *
+	 * @param path
+	 *            path to be checked to see if it is within this repository
+	 * @return true if this sub repo is an ancestor of the given path.
+	 * @since 4.2
+	 */
+	public boolean isAncestorOf(String path) {
+		return path.startsWith(getPathWithSlash());
 	}
 
 	@Override
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 796eaae..7740a2b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
@@ -158,6 +158,7 @@ public static JGitText get() {
 	/***/ public String cannotStoreObjects;
 	/***/ public String cannotResolveUniquelyAbbrevObjectId;
 	/***/ public String cannotUnloadAModifiedTree;
+	/***/ public String cannotUpdateUnbornBranch;
 	/***/ public String cannotWorkWithOtherStagesThanZeroRightNow;
 	/***/ public String cannotWriteObjectsPath;
 	/***/ public String canOnlyCherryPickCommitsWithOneParent;
@@ -184,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;
@@ -210,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;
@@ -240,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;
@@ -491,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;
@@ -663,6 +667,7 @@ public static JGitText get() {
 	/***/ public String transportProtoSFTP;
 	/***/ public String transportProtoSSH;
 	/***/ public String transportProtoTest;
+	/***/ public String transportProvidedRefWithNoObjectId;
 	/***/ public String transportSSHRetryInterrupt;
 	/***/ public String treeEntryAlreadyExists;
 	/***/ public String treeFilterMarkerTooManyFilters;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ElectionRound.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ElectionRound.java
new file mode 100644
index 0000000..014eab2
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ElectionRound.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2016, 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.ketch;
+
+import static org.eclipse.jgit.internal.ketch.KetchConstants.TERM;
+
+import java.io.IOException;
+import java.util.List;
+
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.TreeFormatter;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The initial {@link Round} for a leaderless repository, used to establish a
+ * leader.
+ */
+class ElectionRound extends Round {
+	private static final Logger log = LoggerFactory.getLogger(ElectionRound.class);
+
+	private long term;
+
+	ElectionRound(KetchLeader leader, LogIndex head) {
+		super(leader, head);
+	}
+
+	@Override
+	void start() throws IOException {
+		ObjectId id;
+		try (Repository git = leader.openRepository();
+				ObjectInserter inserter = git.newObjectInserter()) {
+			id = bumpTerm(git, inserter);
+			inserter.flush();
+		}
+		runAsync(id);
+	}
+
+	@Override
+	void success() {
+		// Do nothing upon election, KetchLeader will copy the term.
+	}
+
+	long getTerm() {
+		return term;
+	}
+
+	private ObjectId bumpTerm(Repository git, ObjectInserter inserter)
+			throws IOException {
+		CommitBuilder b = new CommitBuilder();
+		if (!ObjectId.zeroId().equals(acceptedOldIndex)) {
+			try (RevWalk rw = new RevWalk(git)) {
+				RevCommit c = rw.parseCommit(acceptedOldIndex);
+				b.setTreeId(c.getTree());
+				b.setParentId(acceptedOldIndex);
+				term = parseTerm(c.getFooterLines(TERM)) + 1;
+			}
+		} else {
+			term = 1;
+			b.setTreeId(inserter.insert(new TreeFormatter()));
+		}
+
+		StringBuilder msg = new StringBuilder();
+		msg.append(KetchConstants.TERM.getName())
+				.append(": ") //$NON-NLS-1$
+				.append(term);
+
+		String tag = leader.getSystem().newLeaderTag();
+		if (tag != null && !tag.isEmpty()) {
+			msg.append(' ').append(tag);
+		}
+
+		b.setAuthor(leader.getSystem().newCommitter());
+		b.setCommitter(b.getAuthor());
+		b.setMessage(msg.toString());
+
+		if (log.isDebugEnabled()) {
+			log.debug("Trying to elect myself " + b.getMessage()); //$NON-NLS-1$
+		}
+		return inserter.insert(b);
+	}
+
+	private static long parseTerm(List<String> footer) {
+		if (footer.isEmpty()) {
+			return 0;
+		}
+
+		String s = footer.get(0);
+		int p = s.indexOf(' ');
+		if (p > 0) {
+			s = s.substring(0, p);
+		}
+		return Long.parseLong(s, 10);
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchConstants.java
new file mode 100644
index 0000000..171c059
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchConstants.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2016, 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.ketch;
+
+import org.eclipse.jgit.revwalk.FooterKey;
+
+/** Frequently used constants in a Ketch system. */
+public class KetchConstants {
+	/**
+	 * Default reference namespace holding {@link #ACCEPTED} and
+	 * {@link #COMMITTED} references and the {@link #STAGE} sub-namespace.
+	 */
+	public static final String DEFAULT_TXN_NAMESPACE = "refs/txn/"; //$NON-NLS-1$
+
+	/** Reference name holding the RefTree accepted by a follower. */
+	public static final String ACCEPTED = "accepted"; //$NON-NLS-1$
+
+	/** Reference name holding the RefTree known to be committed. */
+	public static final String COMMITTED = "committed"; //$NON-NLS-1$
+
+	/** Reference subdirectory holding proposed heads. */
+	public static final String STAGE = "stage/"; //$NON-NLS-1$
+
+	/** Footer containing the current term. */
+	public static final FooterKey TERM = new FooterKey("Term"); //$NON-NLS-1$
+
+	/** Section for Ketch configuration ({@code ketch}). */
+	public static final String CONFIG_SECTION_KETCH = "ketch"; //$NON-NLS-1$
+
+	/** Behavior for a replica ({@code remote.$name.ketch-type}) */
+	public static final String CONFIG_KEY_TYPE = "ketch-type"; //$NON-NLS-1$
+
+	/** Behavior for a replica ({@code remote.$name.ketch-commit}) */
+	public static final String CONFIG_KEY_COMMIT = "ketch-commit"; //$NON-NLS-1$
+
+	/** Behavior for a replica ({@code remote.$name.ketch-speed}) */
+	public static final String CONFIG_KEY_SPEED = "ketch-speed"; //$NON-NLS-1$
+
+	private KetchConstants() {
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchLeader.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchLeader.java
new file mode 100644
index 0000000..3bcd6bc
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchLeader.java
@@ -0,0 +1,624 @@
+/*
+ * Copyright (C) 2016, 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.ketch;
+
+import static org.eclipse.jgit.internal.ketch.KetchLeader.State.CANDIDATE;
+import static org.eclipse.jgit.internal.ketch.KetchLeader.State.LEADER;
+import static org.eclipse.jgit.internal.ketch.KetchLeader.State.SHUTDOWN;
+import static org.eclipse.jgit.internal.ketch.KetchReplica.Participation.FOLLOWER_ONLY;
+import static org.eclipse.jgit.internal.ketch.Proposal.State.QUEUED;
+
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+import org.eclipse.jgit.internal.storage.reftree.RefTree;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A leader managing consensus across remote followers.
+ * <p>
+ * A leader instance starts up in {@link State#CANDIDATE} and tries to begin a
+ * new term by sending an {@link ElectionRound} to all replicas. Its term starts
+ * if a majority of replicas have accepted this leader instance for the term.
+ * <p>
+ * Once elected by a majority the instance enters {@link State#LEADER} and runs
+ * proposals offered to {@link #queueProposal(Proposal)}. This continues until
+ * the leader is timed out for inactivity, or is deposed by a competing leader
+ * gaining its own majority.
+ * <p>
+ * Once timed out or deposed this {@code KetchLeader} instance should be
+ * discarded, and a new instance takes over.
+ * <p>
+ * Each leader instance coordinates a group of {@link KetchReplica}s. Replica
+ * instances are owned by the leader instance and must be discarded when the
+ * leader is discarded.
+ * <p>
+ * In Ketch all push requests are issued through the leader. The steps are as
+ * follows (see {@link KetchPreReceive} for an example):
+ * <ul>
+ * <li>Create a {@link Proposal} with the
+ * {@link org.eclipse.jgit.transport.ReceiveCommand}s that represent the push.
+ * <li>Invoke {@link #queueProposal(Proposal)} on the leader instance.
+ * <li>Wait for consensus with {@link Proposal#await()}.
+ * <li>To examine the status of the push, check {@link Proposal#getCommands()},
+ * looking at
+ * {@link org.eclipse.jgit.internal.storage.reftree.Command#getResult()}.
+ * </ul>
+ * <p>
+ * The leader gains consensus by first pushing the needed objects and a
+ * {@link RefTree} representing the desired target repository state to the
+ * {@code refs/txn/accepted} branch on each of the replicas. Once a majority has
+ * succeeded, the leader commits the state by either pushing the
+ * {@code refs/txn/accepted} value to {@code refs/txn/committed} (for
+ * Ketch-aware replicas) or by pushing updates to {@code refs/heads/master},
+ * etc. for stock Git replicas.
+ * <p>
+ * Internally, the actual transport to replicas is performed on background
+ * threads via the {@link KetchSystem}'s executor service. For performance, the
+ * {@link KetchLeader}, {@link KetchReplica} and {@link Proposal} objects share
+ * some state, and may invoke each other's methods on different threads. This
+ * access is protected by the leader's {@link #lock} object. Care must be taken
+ * to prevent concurrent access by correctly obtaining the leader's lock.
+ */
+public abstract class KetchLeader {
+	private static final Logger log = LoggerFactory.getLogger(KetchLeader.class);
+
+	/** Current state of the leader instance. */
+	public static enum State {
+		/** Newly created instance trying to elect itself leader. */
+		CANDIDATE,
+
+		/** Leader instance elected by a majority. */
+		LEADER,
+
+		/** Instance has been deposed by another with a more recent term. */
+		DEPOSED,
+
+		/** Leader has been gracefully shutdown, e.g. due to inactivity. */
+		SHUTDOWN;
+	}
+
+	private final KetchSystem system;
+
+	/** Leader's knowledge of replicas for this repository. */
+	private KetchReplica[] voters;
+	private KetchReplica[] followers;
+	private LocalReplica self;
+
+	/**
+	 * Lock protecting all data within this leader instance.
+	 * <p>
+	 * This lock extends into the {@link KetchReplica} instances used by the
+	 * leader. They share the same lock instance to simplify concurrency.
+	 */
+	final Lock lock;
+
+	private State state = CANDIDATE;
+
+	/** Term of this leader, once elected. */
+	private long term;
+
+	/**
+	 * Pending proposals accepted into the queue in FIFO order.
+	 * <p>
+	 * These proposals were preflighted and do not contain any conflicts with
+	 * each other and their expectations matched the leader's local view of the
+	 * agreed upon {@code refs/txn/accepted} tree.
+	 */
+	private final List<Proposal> queued;
+
+	/**
+	 * State of the repository's RefTree after applying all entries in
+	 * {@link #queued}. New proposals must be consistent with this tree to be
+	 * appended to the end of {@link #queued}.
+	 * <p>
+	 * Must be deep-copied with {@link RefTree#copy()} if
+	 * {@link #roundHoldsReferenceToRefTree} is {@code true}.
+	 */
+	private RefTree refTree;
+
+	/**
+	 * If {@code true} {@link #refTree} must be duplicated before queuing the
+	 * next proposal. The {@link #refTree} was passed into the constructor of a
+	 * {@link ProposalRound}, and that external reference to the {@link RefTree}
+	 * object is held by the proposal until it materializes the tree object in
+	 * the object store. This field is set {@code true} when the proposal begins
+	 * execution and set {@code false} once tree objects are persisted in the
+	 * local repository's object store or {@link #refTree} is replaced with a
+	 * copy to isolate it from any running rounds.
+	 * <p>
+	 * If proposals arrive less frequently than the {@code RefTree} is written
+	 * out to the repository the {@link #roundHoldsReferenceToRefTree} behavior
+	 * avoids duplicating {@link #refTree}, reducing both time and memory used.
+	 * However if proposals arrive more frequently {@link #refTree} must be
+	 * duplicated to prevent newly queued proposals from corrupting the
+	 * {@link #runningRound}.
+	 */
+	volatile boolean roundHoldsReferenceToRefTree;
+
+	/** End of the leader's log. */
+	private LogIndex headIndex;
+
+	/** Leader knows this (and all prior) states are committed. */
+	private LogIndex committedIndex;
+
+	/**
+	 * Is the leader idle with no work pending? If {@code true} there is no work
+	 * for the leader (normal state). This field is {@code false} when the
+	 * leader thread is scheduled for execution, or while {@link #runningRound}
+	 * defines a round in progress.
+	 */
+	private boolean idle;
+
+	/** Current round the leader is preparing and waiting for a vote on. */
+	private Round runningRound;
+
+	/**
+	 * Construct a leader for a Ketch instance.
+	 *
+	 * @param system
+	 *            Ketch system configuration the leader must adhere to.
+	 */
+	protected KetchLeader(KetchSystem system) {
+		this.system = system;
+		this.lock = new ReentrantLock(true /* fair */);
+		this.queued = new ArrayList<>(4);
+		this.idle = true;
+	}
+
+	/** @return system configuration. */
+	KetchSystem getSystem() {
+		return system;
+	}
+
+	/**
+	 * Configure the replicas used by this Ketch instance.
+	 * <p>
+	 * Replicas should be configured once at creation before any proposals are
+	 * executed. Once elections happen, <b>reconfiguration is a complicated
+	 * concept that is not currently supported</b>.
+	 *
+	 * @param replicas
+	 *            members participating with the same repository.
+	 */
+	public void setReplicas(Collection<KetchReplica> replicas) {
+		List<KetchReplica> v = new ArrayList<>(5);
+		List<KetchReplica> f = new ArrayList<>(5);
+		for (KetchReplica r : replicas) {
+			switch (r.getParticipation()) {
+			case FULL:
+				v.add(r);
+				break;
+
+			case FOLLOWER_ONLY:
+				f.add(r);
+				break;
+			}
+		}
+
+		Collection<Integer> validVoters = validVoterCounts();
+		if (!validVoters.contains(Integer.valueOf(v.size()))) {
+			throw new IllegalArgumentException(MessageFormat.format(
+					KetchText.get().unsupportedVoterCount,
+					Integer.valueOf(v.size()),
+					validVoters));
+		}
+
+		LocalReplica me = findLocal(v);
+		if (me == null) {
+			throw new IllegalArgumentException(
+					KetchText.get().localReplicaRequired);
+		}
+
+		lock.lock();
+		try {
+			voters = v.toArray(new KetchReplica[v.size()]);
+			followers = f.toArray(new KetchReplica[f.size()]);
+			self = me;
+		} finally {
+			lock.unlock();
+		}
+	}
+
+	private static Collection<Integer> validVoterCounts() {
+		@SuppressWarnings("boxing")
+		Integer[] valid = {
+				// An odd number of voting replicas is required.
+				1, 3, 5, 7, 9 };
+		return Arrays.asList(valid);
+	}
+
+	private static LocalReplica findLocal(Collection<KetchReplica> voters) {
+		for (KetchReplica r : voters) {
+			if (r instanceof LocalReplica) {
+				return (LocalReplica) r;
+			}
+		}
+		return null;
+	}
+
+	/**
+	 * Get an instance of the repository for use by a leader thread.
+	 * <p>
+	 * The caller will close the repository.
+	 *
+	 * @return opened repository for use by the leader thread.
+	 * @throws IOException
+	 *             cannot reopen the repository for the leader.
+	 */
+	protected abstract Repository openRepository() throws IOException;
+
+	/**
+	 * Queue a reference update proposal for consensus.
+	 * <p>
+	 * This method does not wait for consensus to be reached. The proposal is
+	 * checked to look for risks of conflicts, and then submitted into the queue
+	 * for distribution as soon as possible.
+	 * <p>
+	 * Callers must use {@link Proposal#await()} to see if the proposal is done.
+	 *
+	 * @param proposal
+	 *            the proposed reference updates to queue for consideration.
+	 *            Once execution is complete the individual reference result
+	 *            fields will be populated with the outcome.
+	 * @throws InterruptedException
+	 *             current thread was interrupted. The proposal may have been
+	 *             aborted if it was not yet queued for execution.
+	 * @throws IOException
+	 *             unrecoverable error preventing proposals from being attempted
+	 *             by this leader.
+	 */
+	public void queueProposal(Proposal proposal)
+			throws InterruptedException, IOException {
+		try {
+			lock.lockInterruptibly();
+		} catch (InterruptedException e) {
+			proposal.abort();
+			throw e;
+		}
+		try {
+			if (refTree == null) {
+				initialize();
+				for (Proposal p : queued) {
+					refTree.apply(p.getCommands());
+				}
+			} else if (roundHoldsReferenceToRefTree) {
+				refTree = refTree.copy();
+				roundHoldsReferenceToRefTree = false;
+			}
+
+			if (!refTree.apply(proposal.getCommands())) {
+				// A conflict exists so abort the proposal.
+				proposal.abort();
+				return;
+			}
+
+			queued.add(proposal);
+			proposal.notifyState(QUEUED);
+
+			if (idle) {
+				scheduleLeader();
+			}
+		} finally {
+			lock.unlock();
+		}
+	}
+
+	private void initialize() throws IOException {
+		try (Repository git = openRepository(); RevWalk rw = new RevWalk(git)) {
+			self.initialize(git);
+
+			ObjectId accepted = self.getTxnAccepted();
+			if (!ObjectId.zeroId().equals(accepted)) {
+				RevCommit c = rw.parseCommit(accepted);
+				headIndex = LogIndex.unknown(accepted);
+				refTree = RefTree.read(rw.getObjectReader(), c.getTree());
+			} else {
+				headIndex = LogIndex.unknown(ObjectId.zeroId());
+				refTree = RefTree.newEmptyTree();
+			}
+		}
+	}
+
+	private void scheduleLeader() {
+		idle = false;
+		system.getExecutor().execute(new Runnable() {
+			@Override
+			public void run() {
+				runLeader();
+			}
+		});
+	}
+
+	private void runLeader() {
+		Round round;
+		lock.lock();
+		try {
+			switch (state) {
+			case CANDIDATE:
+				round = new ElectionRound(this, headIndex);
+				break;
+
+			case LEADER:
+				round = newProposalRound();
+				break;
+
+			case DEPOSED:
+			case SHUTDOWN:
+			default:
+				log.warn("Leader cannot run {}", state); //$NON-NLS-1$
+				// TODO(sop): Redirect proposals.
+				return;
+			}
+		} finally {
+			lock.unlock();
+		}
+
+		try {
+			round.start();
+		} catch (IOException e) {
+			// TODO(sop) Depose leader if it cannot use its repository.
+			log.error(KetchText.get().leaderFailedToStore, e);
+			lock.lock();
+			try {
+				nextRound();
+			} finally {
+				lock.unlock();
+			}
+		}
+	}
+
+	private ProposalRound newProposalRound() {
+		List<Proposal> todo = new ArrayList<>(queued);
+		queued.clear();
+		roundHoldsReferenceToRefTree = true;
+		return new ProposalRound(this, headIndex, todo, refTree);
+	}
+
+	/** @return term of this leader's reign. */
+	long getTerm() {
+		return term;
+	}
+
+	/** @return end of the leader's log. */
+	LogIndex getHead() {
+		return headIndex;
+	}
+
+	/**
+	 * @return state leader knows it has committed across a quorum of replicas.
+	 */
+	LogIndex getCommitted() {
+		return committedIndex;
+	}
+
+	boolean isIdle() {
+		return idle;
+	}
+
+	void runAsync(Round round) {
+		lock.lock();
+		try {
+			// End of the log is this round. Once transport begins it is
+			// reasonable to assume at least one replica will eventually get
+			// this, and there is reasonable probability it commits.
+			headIndex = round.acceptedNewIndex;
+			runningRound = round;
+
+			for (KetchReplica replica : voters) {
+				replica.pushTxnAcceptedAsync(round);
+			}
+			for (KetchReplica replica : followers) {
+				replica.pushTxnAcceptedAsync(round);
+			}
+		} finally {
+			lock.unlock();
+		}
+	}
+
+	/**
+	 * Asynchronous signal from a replica after completion.
+	 * <p>
+	 * Must be called while {@link #lock} is held by the replica.
+	 *
+	 * @param replica
+	 *            replica posting a completion event.
+	 */
+	void onReplicaUpdate(KetchReplica replica) {
+		if (log.isDebugEnabled()) {
+			log.debug("Replica {} finished:\n{}", //$NON-NLS-1$
+					replica.describeForLog(), snapshot());
+		}
+
+		if (replica.getParticipation() == FOLLOWER_ONLY) {
+			// Followers cannot vote, so votes haven't changed.
+			return;
+		} else if (runningRound == null) {
+			// No round running, no need to tally votes.
+			return;
+		}
+
+		assert headIndex.equals(runningRound.acceptedNewIndex);
+		int matching = 0;
+		for (KetchReplica r : voters) {
+			if (r.hasAccepted(headIndex)) {
+				matching++;
+			}
+		}
+
+		int quorum = voters.length / 2 + 1;
+		boolean success = matching >= quorum;
+		if (!success) {
+			return;
+		}
+
+		switch (state) {
+		case CANDIDATE:
+			term = ((ElectionRound) runningRound).getTerm();
+			state = LEADER;
+			if (log.isDebugEnabled()) {
+				log.debug("Won election, running term " + term); //$NON-NLS-1$
+			}
+
+			//$FALL-THROUGH$
+		case LEADER:
+			committedIndex = headIndex;
+			if (log.isDebugEnabled()) {
+				log.debug("Committed {} in term {}", //$NON-NLS-1$
+						committedIndex.describeForLog(),
+						Long.valueOf(term));
+			}
+			nextRound();
+			commitAsync(replica);
+			notifySuccess(runningRound);
+			if (log.isDebugEnabled()) {
+				log.debug("Leader state:\n{}", snapshot()); //$NON-NLS-1$
+			}
+			break;
+
+		default:
+			log.debug("Leader ignoring replica while in {}", state); //$NON-NLS-1$
+			break;
+		}
+	}
+
+	private void notifySuccess(Round round) {
+		// Drop the leader lock while notifying Proposal listeners.
+		lock.unlock();
+		try {
+			round.success();
+		} finally {
+			lock.lock();
+		}
+	}
+
+	private void commitAsync(KetchReplica caller) {
+		for (KetchReplica r : voters) {
+			if (r == caller) {
+				continue;
+			}
+			if (r.shouldPushUnbatchedCommit(committedIndex, isIdle())) {
+				r.pushCommitAsync(committedIndex);
+			}
+		}
+		for (KetchReplica r : followers) {
+			if (r == caller) {
+				continue;
+			}
+			if (r.shouldPushUnbatchedCommit(committedIndex, isIdle())) {
+				r.pushCommitAsync(committedIndex);
+			}
+		}
+	}
+
+	/** Schedule the next round; invoked while {@link #lock} is held. */
+	void nextRound() {
+		runningRound = null;
+
+		if (queued.isEmpty()) {
+			idle = true;
+		} else {
+			// Caller holds lock. Reschedule leader on a new thread so
+			// the call stack can unwind and lock is not held unexpectedly
+			// during prepare for the next round.
+			scheduleLeader();
+		}
+	}
+
+	/** @return snapshot this leader. */
+	public LeaderSnapshot snapshot() {
+		lock.lock();
+		try {
+			LeaderSnapshot s = new LeaderSnapshot();
+			s.state = state;
+			s.term = term;
+			s.headIndex = headIndex;
+			s.committedIndex = committedIndex;
+			s.idle = isIdle();
+			for (KetchReplica r : voters) {
+				s.replicas.add(r.snapshot());
+			}
+			for (KetchReplica r : followers) {
+				s.replicas.add(r.snapshot());
+			}
+			return s;
+		} finally {
+			lock.unlock();
+		}
+	}
+
+	/** Gracefully shutdown this leader and cancel outstanding operations. */
+	public void shutdown() {
+		lock.lock();
+		try {
+			if (state != SHUTDOWN) {
+				state = SHUTDOWN;
+				for (KetchReplica r : voters) {
+					r.shutdown();
+				}
+				for (KetchReplica r : followers) {
+					r.shutdown();
+				}
+			}
+		} finally {
+			lock.unlock();
+		}
+	}
+
+	@Override
+	public String toString() {
+		return snapshot().toString();
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchLeaderCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchLeaderCache.java
new file mode 100644
index 0000000..ba033c1
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchLeaderCache.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2016, 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.ketch;
+
+import java.net.URISyntaxException;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+import org.eclipse.jgit.internal.storage.dfs.DfsRepository;
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * A cache of live leader instances, keyed by repository.
+ * <p>
+ * Ketch only assigns a leader to a repository when needed. If
+ * {@link #get(Repository)} is called for a repository that does not have a
+ * leader, the leader is created and added to the cache.
+ */
+public class KetchLeaderCache {
+	private final KetchSystem system;
+	private final ConcurrentMap<String, KetchLeader> leaders;
+	private final Lock startLock;
+
+	/**
+	 * Initialize a new leader cache.
+	 *
+	 * @param system
+	 *            system configuration for the leaders
+	 */
+	public KetchLeaderCache(KetchSystem system) {
+		this.system = system;
+		leaders = new ConcurrentHashMap<>();
+		startLock = new ReentrantLock(true /* fair */);
+	}
+
+	/**
+	 * Lookup the leader instance for a given repository.
+	 *
+	 * @param repo
+	 *            repository to get the leader for.
+	 * @return the leader instance for the repository.
+	 * @throws URISyntaxException
+	 *             remote configuration contains an invalid URL.
+	 */
+	public KetchLeader get(Repository repo)
+			throws URISyntaxException {
+		String key = computeKey(repo);
+		KetchLeader leader = leaders.get(key);
+		if (leader != null) {
+			return leader;
+		}
+		return startLeader(key, repo);
+	}
+
+	private KetchLeader startLeader(String key, Repository repo)
+			throws URISyntaxException {
+		startLock.lock();
+		try {
+			KetchLeader leader = leaders.get(key);
+			if (leader != null) {
+				return leader;
+			}
+			leader = system.createLeader(repo);
+			leaders.put(key, leader);
+			return leader;
+		} finally {
+			startLock.unlock();
+		}
+	}
+
+	private static String computeKey(Repository repo) {
+		if (repo instanceof DfsRepository) {
+			DfsRepository dfs = (DfsRepository) repo;
+			return dfs.getDescription().getRepositoryName();
+		}
+
+		if (repo.getDirectory() != null) {
+			return repo.getDirectory().toURI().toString();
+		}
+
+		throw new IllegalArgumentException();
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchPreReceive.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchPreReceive.java
new file mode 100644
index 0000000..1b4307f
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchPreReceive.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2016, 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.ketch;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.eclipse.jgit.internal.ketch.Proposal.State.EXECUTED;
+import static org.eclipse.jgit.internal.ketch.Proposal.State.QUEUED;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
+
+import java.io.IOException;
+import java.util.Collection;
+
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.transport.PreReceiveHook;
+import org.eclipse.jgit.transport.ProgressSpinner;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.transport.ReceivePack;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * PreReceiveHook for handling push traffic in a Ketch system.
+ * <p>
+ * Install an instance on {@link ReceivePack} to capture the commands and other
+ * connection state and relay them through the {@link KetchLeader}, allowing the
+ * leader to gain consensus about the new reference state.
+ */
+public class KetchPreReceive implements PreReceiveHook {
+	private static final Logger log = LoggerFactory.getLogger(KetchPreReceive.class);
+
+	private final KetchLeader leader;
+
+	/**
+	 * Construct a hook executing updates through a {@link KetchLeader}.
+	 *
+	 * @param leader
+	 *            leader for this repository.
+	 */
+	public KetchPreReceive(KetchLeader leader) {
+		this.leader = leader;
+	}
+
+	@Override
+	public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> cmds) {
+		cmds = ReceiveCommand.filter(cmds, NOT_ATTEMPTED);
+		if (cmds.isEmpty()) {
+			return;
+		}
+
+		try {
+			Proposal proposal = new Proposal(rp.getRevWalk(), cmds)
+				.setPushCertificate(rp.getPushCertificate())
+				.setAuthor(rp.getRefLogIdent())
+				.setMessage("push"); //$NON-NLS-1$
+			leader.queueProposal(proposal);
+			if (proposal.isDone()) {
+				// This failed fast, e.g. conflict or bad precondition.
+				return;
+			}
+
+			ProgressSpinner spinner = new ProgressSpinner(
+					rp.getMessageOutputStream());
+			if (proposal.getState() == QUEUED) {
+				waitForQueue(proposal, spinner);
+			}
+			if (!proposal.isDone()) {
+				waitForPropose(proposal, spinner);
+			}
+		} catch (IOException | InterruptedException e) {
+			String msg = JGitText.get().transactionAborted;
+			for (ReceiveCommand cmd : cmds) {
+				if (cmd.getResult() == NOT_ATTEMPTED) {
+					cmd.setResult(REJECTED_OTHER_REASON, msg);
+				}
+			}
+			log.error(msg, e);
+		}
+	}
+
+	private void waitForQueue(Proposal proposal, ProgressSpinner spinner)
+			throws InterruptedException {
+		spinner.beginTask(KetchText.get().waitingForQueue, 1, SECONDS);
+		while (!proposal.awaitStateChange(QUEUED, 250, MILLISECONDS)) {
+			spinner.update();
+		}
+		switch (proposal.getState()) {
+		case RUNNING:
+		default:
+			spinner.endTask(KetchText.get().starting);
+			break;
+
+		case EXECUTED:
+			spinner.endTask(KetchText.get().accepted);
+			break;
+
+		case ABORTED:
+			spinner.endTask(KetchText.get().failed);
+			break;
+		}
+	}
+
+	private void waitForPropose(Proposal proposal, ProgressSpinner spinner)
+			throws InterruptedException {
+		spinner.beginTask(KetchText.get().proposingUpdates, 2, SECONDS);
+		while (!proposal.await(250, MILLISECONDS)) {
+			spinner.update();
+		}
+		spinner.endTask(proposal.getState() == EXECUTED
+				? KetchText.get().accepted
+				: KetchText.get().failed);
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchReplica.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchReplica.java
new file mode 100644
index 0000000..a30bbb2
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchReplica.java
@@ -0,0 +1,755 @@
+/*
+ * Copyright (C) 2016, 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.ketch;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static org.eclipse.jgit.internal.ketch.KetchReplica.CommitSpeed.BATCHED;
+import static org.eclipse.jgit.internal.ketch.KetchReplica.CommitSpeed.FAST;
+import static org.eclipse.jgit.internal.ketch.KetchReplica.State.CURRENT;
+import static org.eclipse.jgit.internal.ketch.KetchReplica.State.LAGGING;
+import static org.eclipse.jgit.internal.ketch.KetchReplica.State.OFFLINE;
+import static org.eclipse.jgit.internal.ketch.KetchReplica.State.UNKNOWN;
+import static org.eclipse.jgit.lib.Constants.HEAD;
+import static org.eclipse.jgit.lib.FileMode.TYPE_GITLINK;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
+import static org.eclipse.jgit.transport.ReceiveCommand.Type.CREATE;
+
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Future;
+
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.internal.storage.reftree.RefTree;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.util.SystemReader;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A Ketch replica, either {@link LocalReplica} or {@link RemoteGitReplica}.
+ * <p>
+ * Replicas can be either a stock Git replica, or a Ketch-aware replica.
+ * <p>
+ * A stock Git replica has no special knowledge of Ketch and simply stores
+ * objects and references. Ketch communicates with the stock Git replica using
+ * the Git push wire protocol. The {@link KetchLeader} commits an agreed upon
+ * state by pushing all references to the Git replica, for example
+ * {@code "refs/heads/master"} is pushed during commit. Stock Git replicas use
+ * {@link CommitMethod#ALL_REFS} to record the final state.
+ * <p>
+ * Ketch-aware replicas understand the {@code RefTree} sent during the proposal
+ * and during commit are able to update their own reference space to match the
+ * state represented by the {@code RefTree}. Ketch-aware replicas typically use
+ * a {@link org.eclipse.jgit.internal.storage.reftree.RefTreeDatabase} and
+ * {@link CommitMethod#TXN_COMMITTED} to record the final state.
+ * <p>
+ * KetchReplica instances are tightly coupled with a single {@link KetchLeader}.
+ * Some state may be accessed by the leader thread and uses the leader's own
+ * {@link KetchLeader#lock} to protect shared data.
+ */
+public abstract class KetchReplica {
+	static final Logger log = LoggerFactory.getLogger(KetchReplica.class);
+	private static final byte[] PEEL = { ' ', '^' };
+
+	/** Participation of a replica in establishing consensus. */
+	public enum Participation {
+		/** Replica can vote. */
+		FULL,
+
+		/** Replica does not vote, but tracks leader. */
+		FOLLOWER_ONLY;
+	}
+
+	/** How this replica wants to receive Ketch commit operations. */
+	public enum CommitMethod {
+		/** All references are pushed to the peer as standard Git. */
+		ALL_REFS,
+
+		/** Only {@code refs/txn/committed} is written/updated. */
+		TXN_COMMITTED;
+	}
+
+	/** Delay before committing to a replica. */
+	public enum CommitSpeed {
+		/**
+		 * Send the commit immediately, even if it could be batched with the
+		 * next proposal.
+		 */
+		FAST,
+
+		/**
+		 * If the next proposal is available, batch the commit with it,
+		 * otherwise just send the commit. This generates less network use, but
+		 * may provide slower consistency on the replica.
+		 */
+		BATCHED;
+	}
+
+	/** Current state of a replica. */
+	public enum State {
+		/** Leader has not yet contacted the replica. */
+		UNKNOWN,
+
+		/** Replica is behind the consensus. */
+		LAGGING,
+
+		/** Replica matches the consensus. */
+		CURRENT,
+
+		/** Replica has a different (or unknown) history. */
+		DIVERGENT,
+
+		/** Replica's history contains the leader's history. */
+		AHEAD,
+
+		/** Replica can not be contacted. */
+		OFFLINE;
+	}
+
+	private final KetchLeader leader;
+	private final String replicaName;
+	private final Participation participation;
+	private final CommitMethod commitMethod;
+	private final CommitSpeed commitSpeed;
+	private final long minRetryMillis;
+	private final long maxRetryMillis;
+	private final Map<ObjectId, List<ReceiveCommand>> staged;
+	private final Map<String, ReceiveCommand> running;
+	private final Map<String, ReceiveCommand> waiting;
+	private final List<ReplicaPushRequest> queued;
+
+	/**
+	 * Value known for {@code "refs/txn/accepted"}.
+	 * <p>
+	 * Raft literature refers to this as {@code matchIndex}.
+	 */
+	private ObjectId txnAccepted;
+
+	/**
+	 * Value known for {@code "refs/txn/committed"}.
+	 * <p>
+	 * Raft literature refers to this as {@code commitIndex}. In traditional
+	 * Raft this is a state variable inside the follower implementation, but
+	 * Ketch keeps it in the leader.
+	 */
+	private ObjectId txnCommitted;
+
+	/** What is happening with this replica. */
+	private State state = UNKNOWN;
+	private String error;
+
+	/** Scheduled retry due to communication failure. */
+	private Future<?> retryFuture;
+	private long lastRetryMillis;
+	private long retryAtMillis;
+
+	/**
+	 * Configure a replica representation.
+	 *
+	 * @param leader
+	 *            instance this replica follows.
+	 * @param name
+	 *            unique-ish name identifying this replica for debugging.
+	 * @param cfg
+	 *            how Ketch should treat the replica.
+	 */
+	protected KetchReplica(KetchLeader leader, String name, ReplicaConfig cfg) {
+		this.leader = leader;
+		this.replicaName = name;
+		this.participation = cfg.getParticipation();
+		this.commitMethod = cfg.getCommitMethod();
+		this.commitSpeed = cfg.getCommitSpeed();
+		this.minRetryMillis = cfg.getMinRetry(MILLISECONDS);
+		this.maxRetryMillis = cfg.getMaxRetry(MILLISECONDS);
+		this.staged = new HashMap<>();
+		this.running = new HashMap<>();
+		this.waiting = new HashMap<>();
+		this.queued = new ArrayList<>(4);
+	}
+
+	/** @return system configuration. */
+	public KetchSystem getSystem() {
+		return getLeader().getSystem();
+	}
+
+	/** @return leader instance this replica follows. */
+	public KetchLeader getLeader() {
+		return leader;
+	}
+
+	/** @return unique-ish name for debugging. */
+	public String getName() {
+		return replicaName;
+	}
+
+	/** @return description of this replica for error/debug logging purposes. */
+	protected String describeForLog() {
+		return getName();
+	}
+
+	/** @return how the replica participates in this Ketch system. */
+	public Participation getParticipation() {
+		return participation;
+	}
+
+	/** @return how Ketch will commit to the repository. */
+	public CommitMethod getCommitMethod() {
+		return commitMethod;
+	}
+
+	/** @return when Ketch will commit to the repository. */
+	public CommitSpeed getCommitSpeed() {
+		return commitSpeed;
+	}
+
+	/**
+	 * Called by leader to perform graceful shutdown.
+	 * <p>
+	 * Default implementation cancels any scheduled retry. Subclasses may add
+	 * additional logic before or after calling {@code super.shutdown()}.
+	 * <p>
+	 * Called with {@link KetchLeader#lock} held by caller.
+	 */
+	protected void shutdown() {
+		Future<?> f = retryFuture;
+		if (f != null) {
+			retryFuture = null;
+			f.cancel(true);
+		}
+	}
+
+	ReplicaSnapshot snapshot() {
+		ReplicaSnapshot s = new ReplicaSnapshot(this);
+		s.accepted = txnAccepted;
+		s.committed = txnCommitted;
+		s.state = state;
+		s.error = error;
+		s.retryAtMillis = waitingForRetry() ? retryAtMillis : 0;
+		return s;
+	}
+
+	/**
+	 * Update the leader's view of the replica after a poll.
+	 * <p>
+	 * Called with {@link KetchLeader#lock} held by caller.
+	 *
+	 * @param refs
+	 *            map of refs from the replica.
+	 */
+	void initialize(Map<String, Ref> refs) {
+		if (txnAccepted == null) {
+			txnAccepted = getId(refs.get(getSystem().getTxnAccepted()));
+		}
+		if (txnCommitted == null) {
+			txnCommitted = getId(refs.get(getSystem().getTxnCommitted()));
+		}
+	}
+
+	ObjectId getTxnAccepted() {
+		return txnAccepted;
+	}
+
+	boolean hasAccepted(LogIndex id) {
+		return equals(txnAccepted, id);
+	}
+
+	private static boolean equals(@Nullable ObjectId a, LogIndex b) {
+		return a != null && b != null && AnyObjectId.equals(a, b);
+	}
+
+	/**
+	 * Schedule a proposal round with the replica.
+	 * <p>
+	 * Called with {@link KetchLeader#lock} held by caller.
+	 *
+	 * @param round
+	 *            current round being run by the leader.
+	 */
+	void pushTxnAcceptedAsync(Round round) {
+		List<ReceiveCommand> cmds = new ArrayList<>();
+		if (commitSpeed == BATCHED) {
+			LogIndex committedIndex = leader.getCommitted();
+			if (equals(txnAccepted, committedIndex)
+					&& !equals(txnCommitted, committedIndex)) {
+				prepareTxnCommitted(cmds, committedIndex);
+			}
+		}
+
+		// TODO(sop) Lagging replicas should build accept on the fly.
+		if (round.stageCommands != null) {
+			for (ReceiveCommand cmd : round.stageCommands) {
+				// TODO(sop): Do not send certain object graphs to replica.
+				cmds.add(copy(cmd));
+			}
+		}
+		cmds.add(new ReceiveCommand(
+				round.acceptedOldIndex, round.acceptedNewIndex,
+				getSystem().getTxnAccepted()));
+		pushAsync(new ReplicaPushRequest(this, cmds));
+	}
+
+	private static ReceiveCommand copy(ReceiveCommand c) {
+		return new ReceiveCommand(c.getOldId(), c.getNewId(), c.getRefName());
+	}
+
+	boolean shouldPushUnbatchedCommit(LogIndex committed, boolean leaderIdle) {
+		return (leaderIdle || commitSpeed == FAST) && hasAccepted(committed);
+	}
+
+	void pushCommitAsync(LogIndex committed) {
+		List<ReceiveCommand> cmds = new ArrayList<>();
+		prepareTxnCommitted(cmds, committed);
+		pushAsync(new ReplicaPushRequest(this, cmds));
+	}
+
+	private void prepareTxnCommitted(List<ReceiveCommand> cmds,
+			ObjectId committed) {
+		removeStaged(cmds, committed);
+		cmds.add(new ReceiveCommand(
+				txnCommitted, committed,
+				getSystem().getTxnCommitted()));
+	}
+
+	private void removeStaged(List<ReceiveCommand> cmds, ObjectId committed) {
+		List<ReceiveCommand> a = staged.remove(committed);
+		if (a != null) {
+			delete(cmds, a);
+		}
+		if (staged.isEmpty() || !(committed instanceof LogIndex)) {
+			return;
+		}
+
+		LogIndex committedIndex = (LogIndex) committed;
+		Iterator<Map.Entry<ObjectId, List<ReceiveCommand>>> itr = staged
+				.entrySet().iterator();
+		while (itr.hasNext()) {
+			Map.Entry<ObjectId, List<ReceiveCommand>> e = itr.next();
+			if (e.getKey() instanceof LogIndex) {
+				LogIndex stagedIndex = (LogIndex) e.getKey();
+				if (stagedIndex.isBefore(committedIndex)) {
+					delete(cmds, e.getValue());
+					itr.remove();
+				}
+			}
+		}
+	}
+
+	private static void delete(List<ReceiveCommand> cmds,
+			List<ReceiveCommand> createCmds) {
+		for (ReceiveCommand cmd : createCmds) {
+			ObjectId id = cmd.getNewId();
+			String name = cmd.getRefName();
+			cmds.add(new ReceiveCommand(id, ObjectId.zeroId(), name));
+		}
+	}
+
+	/**
+	 * Determine the next push for this replica (if any) and start it.
+	 * <p>
+	 * If the replica has successfully accepted the committed state of the
+	 * leader, this method will push all references to the replica using the
+	 * configured {@link CommitMethod}.
+	 * <p>
+	 * If the replica is {@link State#LAGGING} this method will begin catch up
+	 * by sending a more recent {@code refs/txn/accepted}.
+	 * <p>
+	 * Must be invoked with {@link KetchLeader#lock} held by caller.
+	 */
+	private void runNextPushRequest() {
+		LogIndex committed = leader.getCommitted();
+		if (!equals(txnCommitted, committed)
+				&& shouldPushUnbatchedCommit(committed, leader.isIdle())) {
+			pushCommitAsync(committed);
+		}
+
+		if (queued.isEmpty() || !running.isEmpty() || waitingForRetry()) {
+			return;
+		}
+
+		// Collapse all queued requests into a single request.
+		Map<String, ReceiveCommand> cmdMap = new HashMap<>();
+		for (ReplicaPushRequest req : queued) {
+			for (ReceiveCommand cmd : req.getCommands()) {
+				String name = cmd.getRefName();
+				ReceiveCommand old = cmdMap.remove(name);
+				if (old != null) {
+					cmd = new ReceiveCommand(
+							old.getOldId(), cmd.getNewId(),
+							name);
+				}
+				cmdMap.put(name, cmd);
+			}
+		}
+		queued.clear();
+		waiting.clear();
+
+		List<ReceiveCommand> next = new ArrayList<>(cmdMap.values());
+		for (ReceiveCommand cmd : next) {
+			running.put(cmd.getRefName(), cmd);
+		}
+		startPush(new ReplicaPushRequest(this, next));
+	}
+
+	private void pushAsync(ReplicaPushRequest req) {
+		if (defer(req)) {
+			// TODO(sop) Collapse during long retry outage.
+			for (ReceiveCommand cmd : req.getCommands()) {
+				waiting.put(cmd.getRefName(), cmd);
+			}
+			queued.add(req);
+		} else {
+			for (ReceiveCommand cmd : req.getCommands()) {
+				running.put(cmd.getRefName(), cmd);
+			}
+			startPush(req);
+		}
+	}
+
+	private boolean defer(ReplicaPushRequest req) {
+		if (waitingForRetry()) {
+			// Prior communication failure; everything is deferred.
+			return true;
+		}
+
+		for (ReceiveCommand nextCmd : req.getCommands()) {
+			ReceiveCommand priorCmd = waiting.get(nextCmd.getRefName());
+			if (priorCmd == null) {
+				priorCmd = running.get(nextCmd.getRefName());
+			}
+			if (priorCmd != null) {
+				// Another request pending on same ref; that must go first.
+				// Verify priorCmd.newId == nextCmd.oldId?
+				return true;
+			}
+		}
+		return false;
+	}
+
+	private boolean waitingForRetry() {
+		Future<?> f = retryFuture;
+		return f != null && !f.isDone();
+	}
+
+	private void retryLater(ReplicaPushRequest req) {
+		Collection<ReceiveCommand> cmds = req.getCommands();
+		for (ReceiveCommand cmd : cmds) {
+			cmd.setResult(NOT_ATTEMPTED, null);
+			if (!waiting.containsKey(cmd.getRefName())) {
+				waiting.put(cmd.getRefName(), cmd);
+			}
+		}
+		queued.add(0, new ReplicaPushRequest(this, cmds));
+
+		if (!waitingForRetry()) {
+			long delay = KetchSystem.delay(
+					lastRetryMillis,
+					minRetryMillis, maxRetryMillis);
+			if (log.isDebugEnabled()) {
+				log.debug("Retrying {} after {} ms", //$NON-NLS-1$
+						describeForLog(), Long.valueOf(delay));
+			}
+			lastRetryMillis = delay;
+			retryAtMillis = SystemReader.getInstance().getCurrentTime() + delay;
+			retryFuture = getSystem().getExecutor()
+					.schedule(new WeakRetryPush(this), delay, MILLISECONDS);
+		}
+	}
+
+	/** Weakly holds a retrying replica, allowing it to garbage collect. */
+	static class WeakRetryPush extends WeakReference<KetchReplica>
+			implements Callable<Void> {
+		WeakRetryPush(KetchReplica r) {
+			super(r);
+		}
+
+		@Override
+		public Void call() throws Exception {
+			KetchReplica r = get();
+			if (r != null) {
+				r.doRetryPush();
+			}
+			return null;
+		}
+	}
+
+	private void doRetryPush() {
+		leader.lock.lock();
+		try {
+			retryFuture = null;
+			runNextPushRequest();
+		} finally {
+			leader.lock.unlock();
+		}
+	}
+
+	/**
+	 * Begin executing a single push.
+	 * <p>
+	 * This method must move processing onto another thread.
+	 * Called with {@link KetchLeader#lock} held by caller.
+	 *
+	 * @param req
+	 *            the request to send to the replica.
+	 */
+	protected abstract void startPush(ReplicaPushRequest req);
+
+	/**
+	 * Callback from {@link ReplicaPushRequest} upon success or failure.
+	 * <p>
+	 * Acquires the {@link KetchLeader#lock} and updates the leader's internal
+	 * knowledge about this replica to reflect what has been learned during a
+	 * push to the replica. In some cases of divergence this method may take
+	 * some time to determine how the replica has diverged; to reduce contention
+	 * this is evaluated before acquiring the leader lock.
+	 *
+	 * @param repo
+	 *            local repository instance used by the push thread.
+	 * @param req
+	 *            push request just attempted.
+	 */
+	void afterPush(@Nullable Repository repo, ReplicaPushRequest req) {
+		ReceiveCommand acceptCmd = null;
+		ReceiveCommand commitCmd = null;
+		List<ReceiveCommand> stages = null;
+
+		for (ReceiveCommand cmd : req.getCommands()) {
+			String name = cmd.getRefName();
+			if (name.equals(getSystem().getTxnAccepted())) {
+				acceptCmd = cmd;
+			} else if (name.equals(getSystem().getTxnCommitted())) {
+				commitCmd = cmd;
+			} else if (cmd.getResult() == OK && cmd.getType() == CREATE
+					&& name.startsWith(getSystem().getTxnStage())) {
+				if (stages == null) {
+					stages = new ArrayList<>();
+				}
+				stages.add(cmd);
+			}
+		}
+
+		State newState = null;
+		ObjectId acceptId = readId(req, acceptCmd);
+		if (repo != null && acceptCmd != null && acceptCmd.getResult() != OK
+				&& req.getException() == null) {
+			try (LagCheck lag = new LagCheck(this, repo)) {
+				newState = lag.check(acceptId, acceptCmd);
+				acceptId = lag.getRemoteId();
+			}
+		}
+
+		leader.lock.lock();
+		try {
+			for (ReceiveCommand cmd : req.getCommands()) {
+				running.remove(cmd.getRefName());
+			}
+
+			Throwable err = req.getException();
+			if (err != null) {
+				state = OFFLINE;
+				error = err.toString();
+				retryLater(req);
+				leader.onReplicaUpdate(this);
+				return;
+			}
+
+			lastRetryMillis = 0;
+			error = null;
+			updateView(req, acceptId, commitCmd);
+
+			if (acceptCmd != null && acceptCmd.getResult() == OK) {
+				state = hasAccepted(leader.getHead()) ? CURRENT : LAGGING;
+				if (stages != null) {
+					staged.put(acceptCmd.getNewId(), stages);
+				}
+			} else if (newState != null) {
+				state = newState;
+			}
+
+			leader.onReplicaUpdate(this);
+			runNextPushRequest();
+		} finally {
+			leader.lock.unlock();
+		}
+	}
+
+	private void updateView(ReplicaPushRequest req, @Nullable ObjectId acceptId,
+			ReceiveCommand commitCmd) {
+		if (acceptId != null) {
+			txnAccepted = acceptId;
+		}
+
+		ObjectId committed = readId(req, commitCmd);
+		if (committed != null) {
+			txnCommitted = committed;
+		} else if (acceptId != null && txnCommitted == null) {
+			// Initialize during first conversation.
+			Map<String, Ref> adv = req.getRefs();
+			if (adv != null) {
+				Ref refs = adv.get(getSystem().getTxnCommitted());
+				txnCommitted = getId(refs);
+			}
+		}
+	}
+
+	@Nullable
+	private static ObjectId readId(ReplicaPushRequest req,
+			@Nullable ReceiveCommand cmd) {
+		if (cmd == null) {
+			// Ref was not in the command list, do not trust advertisement.
+			return null;
+
+		} else if (cmd.getResult() == OK) {
+			// Currently at newId.
+			return cmd.getNewId();
+		}
+
+		Map<String, Ref> refs = req.getRefs();
+		return refs != null ? getId(refs.get(cmd.getRefName())) : null;
+	}
+
+	/**
+	 * Fetch objects from the remote using the calling thread.
+	 * <p>
+	 * Called without {@link KetchLeader#lock}.
+	 *
+	 * @param repo
+	 *            local repository to fetch objects into.
+	 * @param req
+	 *            the request to fetch from a replica.
+	 * @throws IOException
+	 *             communication with the replica was not possible.
+	 */
+	protected abstract void blockingFetch(Repository repo,
+			ReplicaFetchRequest req) throws IOException;
+
+	/**
+	 * Build a list of commands to commit {@link CommitMethod#ALL_REFS}.
+	 *
+	 * @param git
+	 *            local leader repository to read committed state from.
+	 * @param current
+	 *            all known references in the replica's repository. Typically
+	 *            this comes from a push advertisement.
+	 * @param committed
+	 *            state being pushed to {@code refs/txn/committed}.
+	 * @return commands to update during commit.
+	 * @throws IOException
+	 *             cannot read the committed state.
+	 */
+	protected Collection<ReceiveCommand> prepareCommit(Repository git,
+			Map<String, Ref> current, ObjectId committed) throws IOException {
+		List<ReceiveCommand> delta = new ArrayList<>();
+		Map<String, Ref> remote = new HashMap<>(current);
+		try (RevWalk rw = new RevWalk(git);
+				TreeWalk tw = new TreeWalk(rw.getObjectReader())) {
+			tw.setRecursive(true);
+			tw.addTree(rw.parseCommit(committed).getTree());
+			while (tw.next()) {
+				if (tw.getRawMode(0) != TYPE_GITLINK
+						|| tw.isPathSuffix(PEEL, 2)) {
+					// Symbolic references cannot be pushed.
+					// Caching peeled values is handled remotely.
+					continue;
+				}
+
+				// TODO(sop) Do not send certain ref names to replica.
+				String name = RefTree.refName(tw.getPathString());
+				Ref oldRef = remote.remove(name);
+				ObjectId oldId = getId(oldRef);
+				ObjectId newId = tw.getObjectId(0);
+				if (!AnyObjectId.equals(oldId, newId)) {
+					delta.add(new ReceiveCommand(oldId, newId, name));
+				}
+			}
+		}
+
+		// Delete any extra references not in the committed state.
+		for (Ref ref : remote.values()) {
+			if (canDelete(ref)) {
+				delta.add(new ReceiveCommand(
+					ref.getObjectId(), ObjectId.zeroId(),
+					ref.getName()));
+			}
+		}
+		return delta;
+	}
+
+	boolean canDelete(Ref ref) {
+		String name = ref.getName();
+		if (HEAD.equals(name)) {
+			return false;
+		}
+		if (name.startsWith(getSystem().getTxnNamespace())) {
+			return false;
+		}
+		// TODO(sop) Do not delete precious names from replica.
+		return true;
+	}
+
+	@NonNull
+	static ObjectId getId(@Nullable Ref ref) {
+		if (ref != null) {
+			ObjectId id = ref.getObjectId();
+			if (id != null) {
+				return id;
+			}
+		}
+		return ObjectId.zeroId();
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchSystem.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchSystem.java
new file mode 100644
index 0000000..71e872e
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchSystem.java
@@ -0,0 +1,301 @@
+/*
+ * Copyright (C) 2016, 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.ketch;
+
+import static org.eclipse.jgit.internal.ketch.KetchConstants.ACCEPTED;
+import static org.eclipse.jgit.internal.ketch.KetchConstants.COMMITTED;
+import static org.eclipse.jgit.internal.ketch.KetchConstants.CONFIG_KEY_TYPE;
+import static org.eclipse.jgit.internal.ketch.KetchConstants.CONFIG_SECTION_KETCH;
+import static org.eclipse.jgit.internal.ketch.KetchConstants.DEFAULT_TXN_NAMESPACE;
+import static org.eclipse.jgit.internal.ketch.KetchConstants.STAGE;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_NAME;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_REMOTE;
+
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.RemoteConfig;
+import org.eclipse.jgit.transport.URIish;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Ketch system-wide configuration.
+ * <p>
+ * This class provides useful defaults for testing and small proof of concepts.
+ * Full scale installations are expected to subclass and override methods to
+ * provide consistent configuration across all managed repositories.
+ * <p>
+ * Servers should configure their own {@link ScheduledExecutorService}.
+ */
+public class KetchSystem {
+	private static final Random RNG = new Random();
+
+	/** @return default executor, one thread per available processor. */
+	public static ScheduledExecutorService defaultExecutor() {
+		return DefaultExecutorHolder.I;
+	}
+
+	private final ScheduledExecutorService executor;
+	private final String txnNamespace;
+	private final String txnAccepted;
+	private final String txnCommitted;
+	private final String txnStage;
+
+	/** Create a default system with a thread pool of 1 thread per CPU. */
+	public KetchSystem() {
+		this(defaultExecutor(), DEFAULT_TXN_NAMESPACE);
+	}
+
+	/**
+	 * Create a Ketch system with the provided executor service.
+	 *
+	 * @param executor
+	 *            thread pool to run background operations.
+	 * @param txnNamespace
+	 *            reference namespace for the RefTree graph and associated
+	 *            transaction state. Must begin with {@code "refs/"} and end
+	 *            with {@code '/'}, for example {@code "refs/txn/"}.
+	 */
+	public KetchSystem(ScheduledExecutorService executor, String txnNamespace) {
+		this.executor = executor;
+		this.txnNamespace = txnNamespace;
+		this.txnAccepted = txnNamespace + ACCEPTED;
+		this.txnCommitted = txnNamespace + COMMITTED;
+		this.txnStage = txnNamespace + STAGE;
+	}
+
+	/** @return executor to perform background operations. */
+	public ScheduledExecutorService getExecutor() {
+		return executor;
+	}
+
+	/**
+	 * Get the namespace used for the RefTree graph and transaction management.
+	 *
+	 * @return reference namespace such as {@code "refs/txn/"}.
+	 */
+	public String getTxnNamespace() {
+		return txnNamespace;
+	}
+
+	/** @return name of the accepted RefTree graph. */
+	public String getTxnAccepted() {
+		return txnAccepted;
+	}
+
+	/** @return name of the committed RefTree graph. */
+	public String getTxnCommitted() {
+		return txnCommitted;
+	}
+
+	/** @return prefix for staged objects, e.g. {@code "refs/txn/stage/"}. */
+	public String getTxnStage() {
+		return txnStage;
+	}
+
+	/** @return identity line for the committer header of a RefTreeGraph. */
+	public PersonIdent newCommitter() {
+		String name = "ketch"; //$NON-NLS-1$
+		String email = "ketch@system"; //$NON-NLS-1$
+		return new PersonIdent(name, email);
+	}
+
+	/**
+	 * Construct a random tag to identify a candidate during leader election.
+	 * <p>
+	 * Multiple processes trying to elect themselves leaders at exactly the same
+	 * time (rounded to seconds) using the same {@link #newCommitter()} identity
+	 * strings, for the same term, may generate the same ObjectId for the
+	 * election commit and falsely assume they have both won.
+	 * <p>
+	 * Candidates add this tag to their election ballot commit to disambiguate
+	 * the election. The tag only needs to be unique for a given triplet of
+	 * {@link #newCommitter()}, system time (rounded to seconds), and term. If
+	 * every replica in the system uses a unique {@code newCommitter} (such as
+	 * including the host name after the {@code "@"} in the email address) the
+	 * tag could be the empty string.
+	 * <p>
+	 * The default implementation generates a few bytes of random data.
+	 *
+	 * @return unique tag; null or empty string if {@code newCommitter()} is
+	 *         sufficiently unique to identify the leader.
+	 */
+	@Nullable
+	public String newLeaderTag() {
+		int n = RNG.nextInt(1 << (6 * 4));
+		return String.format("%06x", Integer.valueOf(n)); //$NON-NLS-1$
+	}
+
+	/**
+	 * Construct the KetchLeader instance of a repository.
+	 *
+	 * @param repo
+	 *            local repository stored by the leader.
+	 * @return leader instance.
+	 * @throws URISyntaxException
+	 *             a follower configuration contains an unsupported URI.
+	 */
+	public KetchLeader createLeader(final Repository repo)
+			throws URISyntaxException {
+		KetchLeader leader = new KetchLeader(this) {
+			@Override
+			protected Repository openRepository() {
+				repo.incrementOpen();
+				return repo;
+			}
+		};
+		leader.setReplicas(createReplicas(leader, repo));
+		return leader;
+	}
+
+	/**
+	 * Get the collection of replicas for a repository.
+	 * <p>
+	 * The collection of replicas must include the local repository.
+	 *
+	 * @param leader
+	 *            the leader driving these replicas.
+	 * @param repo
+	 *            repository to get the replicas of.
+	 * @return collection of replicas for the specified repository.
+	 * @throws URISyntaxException
+	 *             a configured URI is invalid.
+	 */
+	protected List<KetchReplica> createReplicas(KetchLeader leader,
+			Repository repo) throws URISyntaxException {
+		List<KetchReplica> replicas = new ArrayList<>();
+		Config cfg = repo.getConfig();
+		String localName = getLocalName(cfg);
+		for (String name : cfg.getSubsections(CONFIG_KEY_REMOTE)) {
+			if (!hasParticipation(cfg, name)) {
+				continue;
+			}
+
+			ReplicaConfig kc = ReplicaConfig.newFromConfig(cfg, name);
+			if (name.equals(localName)) {
+				replicas.add(new LocalReplica(leader, name, kc));
+				continue;
+			}
+
+			RemoteConfig rc = new RemoteConfig(cfg, name);
+			List<URIish> uris = rc.getPushURIs();
+			if (uris.isEmpty()) {
+				uris = rc.getURIs();
+			}
+			for (URIish uri : uris) {
+				String n = uris.size() == 1 ? name : uri.getHost();
+				replicas.add(new RemoteGitReplica(leader, n, uri, kc, rc));
+			}
+		}
+		return replicas;
+	}
+
+	private static boolean hasParticipation(Config cfg, String name) {
+		return cfg.getString(CONFIG_KEY_REMOTE, name, CONFIG_KEY_TYPE) != null;
+	}
+
+	private static String getLocalName(Config cfg) {
+		return cfg.getString(CONFIG_SECTION_KETCH, null, CONFIG_KEY_NAME);
+	}
+
+	static class DefaultExecutorHolder {
+		private static final Logger log = LoggerFactory.getLogger(KetchSystem.class);
+		static final ScheduledExecutorService I = create();
+
+		private static ScheduledExecutorService create() {
+			int cores = Runtime.getRuntime().availableProcessors();
+			int threads = Math.max(5, cores);
+			log.info("Using {} threads", Integer.valueOf(threads)); //$NON-NLS-1$
+			return Executors.newScheduledThreadPool(
+				threads,
+				new ThreadFactory() {
+					private final AtomicInteger threadCnt = new AtomicInteger();
+
+					@Override
+					public Thread newThread(Runnable r) {
+						int id = threadCnt.incrementAndGet();
+						Thread thr = new Thread(r);
+						thr.setName("KetchExecutor-" + id); //$NON-NLS-1$
+						return thr;
+					}
+				});
+		}
+
+		private DefaultExecutorHolder() {
+		}
+	}
+
+	/**
+	 * Compute a delay in a {@code min..max} interval with random jitter.
+	 *
+	 * @param last
+	 *            amount of delay waited before the last attempt. This is used
+	 *            to seed the next delay interval. Should be 0 if there was no
+	 *            prior delay.
+	 * @param min
+	 *            shortest amount of allowable delay between attempts.
+	 * @param max
+	 *            longest amount of allowable delay between attempts.
+	 * @return new amount of delay to wait before the next attempt.
+	 */
+	static long delay(long last, long min, long max) {
+		long r = Math.max(0, last * 3 - min);
+		if (r > 0) {
+			int c = (int) Math.min(r + 1, Integer.MAX_VALUE);
+			r = RNG.nextInt(c);
+		}
+		return Math.max(Math.min(min + r, max), min);
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/SymlinkTreeEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchText.java
similarity index 60%
copy from org.eclipse.jgit/src/org/eclipse/jgit/lib/SymlinkTreeEntry.java
copy to org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchText.java
index c7e41bc..b6c3bc9 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/SymlinkTreeEntry.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/KetchText.java
@@ -1,6 +1,5 @@
 /*
- * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org>
+ * Copyright (C) 2016, Google Inc.
  * and other copyright owners as documented in the project's IP log.
  *
  * This program and the accompanying materials are made available
@@ -42,44 +41,30 @@
  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-package org.eclipse.jgit.lib;
+package org.eclipse.jgit.internal.ketch;
 
-/**
- * A tree entry representing a symbolic link.
- *
- * Note. Java cannot really handle these as file system objects.
- *
- * @deprecated To look up information about a single path, use
- * {@link org.eclipse.jgit.treewalk.TreeWalk#forPath(Repository, String, org.eclipse.jgit.revwalk.RevTree)}.
- * To lookup information about multiple paths at once, use a
- * {@link org.eclipse.jgit.treewalk.TreeWalk} and obtain the current entry's
- * information from its getter methods.
- */
-@Deprecated
-public class SymlinkTreeEntry extends TreeEntry {
+import org.eclipse.jgit.nls.NLS;
+import org.eclipse.jgit.nls.TranslationBundle;
 
-	/**
-	 * Construct a {@link SymlinkTreeEntry} with the specified name and SHA-1 in
-	 * the specified parent
-	 *
-	 * @param parent
-	 * @param id
-	 * @param nameUTF8
-	 */
-	public SymlinkTreeEntry(final Tree parent, final ObjectId id,
-			final byte[] nameUTF8) {
-		super(parent, id, nameUTF8);
+/** Translation bundle for the Ketch implementation. */
+public class KetchText extends TranslationBundle {
+	/** @return instance of this translation bundle. */
+	public static KetchText get() {
+		return NLS.getBundleFor(KetchText.class);
 	}
 
-	public FileMode getMode() {
-		return FileMode.SYMLINK;
-	}
-
-	public String toString() {
-		final StringBuilder r = new StringBuilder();
-		r.append(ObjectId.toString(getId()));
-		r.append(" S "); //$NON-NLS-1$
-		r.append(getFullName());
-		return r.toString();
-	}
+	// @formatter:off
+	/***/ public String accepted;
+	/***/ public String cannotFetchFromLocalReplica;
+	/***/ public String failed;
+	/***/ public String invalidFollowerUri;
+	/***/ public String leaderFailedToStore;
+	/***/ public String localReplicaRequired;
+	/***/ public String mismatchedTxnNamespace;
+	/***/ public String outsideTxnNamespace;
+	/***/ public String proposingUpdates;
+	/***/ public String queuedProposalFailedToApply;
+	/***/ public String starting;
+	/***/ public String unsupportedVoterCount;
+	/***/ public String waitingForQueue;
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LagCheck.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LagCheck.java
new file mode 100644
index 0000000..35327ea
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LagCheck.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2016, 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.ketch;
+
+import static org.eclipse.jgit.internal.ketch.KetchReplica.State.AHEAD;
+import static org.eclipse.jgit.internal.ketch.KetchReplica.State.DIVERGENT;
+import static org.eclipse.jgit.internal.ketch.KetchReplica.State.LAGGING;
+import static org.eclipse.jgit.internal.ketch.KetchReplica.State.UNKNOWN;
+import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Map;
+
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+/**
+ * A helper to check if a {@link KetchReplica} is ahead or behind the leader.
+ */
+class LagCheck implements AutoCloseable {
+	private final KetchReplica replica;
+	private final Repository repo;
+	private RevWalk rw;
+	private ObjectId remoteId;
+
+	LagCheck(KetchReplica replica, Repository repo) {
+		this.replica = replica;
+		this.repo = repo;
+		initRevWalk();
+	}
+
+	private void initRevWalk() {
+		if (rw != null) {
+			rw.close();
+		}
+
+		rw = new RevWalk(repo);
+		rw.setRetainBody(false);
+	}
+
+	public void close() {
+		if (rw != null) {
+			rw.close();
+			rw = null;
+		}
+	}
+
+	ObjectId getRemoteId() {
+		return remoteId;
+	}
+
+	KetchReplica.State check(ObjectId acceptId, ReceiveCommand acceptCmd) {
+		remoteId = acceptId;
+		if (remoteId == null) {
+			// Nothing advertised by the replica, value is unknown.
+			return UNKNOWN;
+		}
+
+		if (AnyObjectId.equals(remoteId, ObjectId.zeroId())) {
+			// Replica does not have the txnAccepted reference.
+			return LAGGING;
+		}
+
+		try {
+			RevCommit remote;
+			try {
+				remote = parseRemoteCommit(acceptCmd.getRefName());
+			} catch (RefGoneException gone) {
+				// Replica does not have the txnAccepted reference.
+				return LAGGING;
+			} catch (MissingObjectException notFound) {
+				// Local repository does not know this commit so it cannot
+				// be including the replica's log.
+				return DIVERGENT;
+			}
+
+			RevCommit head = rw.parseCommit(acceptCmd.getNewId());
+			if (rw.isMergedInto(remote, head)) {
+				return LAGGING;
+			}
+
+			// TODO(sop) Check term to see if my leader was deposed.
+			if (rw.isMergedInto(head, remote)) {
+				return AHEAD;
+			} else {
+				return DIVERGENT;
+			}
+		} catch (IOException err) {
+			KetchReplica.log.error(String.format(
+					"Cannot compare %s", //$NON-NLS-1$
+					acceptCmd.getRefName()), err);
+			return UNKNOWN;
+		}
+	}
+
+	private RevCommit parseRemoteCommit(String refName)
+			throws IOException, MissingObjectException, RefGoneException {
+		try {
+			return rw.parseCommit(remoteId);
+		} catch (MissingObjectException notLocal) {
+			// Fall through and try to acquire the object by fetching it.
+		}
+
+		ReplicaFetchRequest fetch = new ReplicaFetchRequest(
+				Collections.singleton(refName),
+				Collections.<ObjectId> emptySet());
+		try {
+			replica.blockingFetch(repo, fetch);
+		} catch (IOException fetchErr) {
+			KetchReplica.log.error(String.format(
+					"Cannot fetch %s (%s) from %s", //$NON-NLS-1$
+					remoteId.abbreviate(8).name(), refName,
+					replica.describeForLog()), fetchErr);
+			throw new MissingObjectException(remoteId, OBJ_COMMIT);
+		}
+
+		Map<String, Ref> adv = fetch.getRefs();
+		if (adv == null) {
+			throw new MissingObjectException(remoteId, OBJ_COMMIT);
+		}
+
+		Ref ref = adv.get(refName);
+		if (ref == null || ref.getObjectId() == null) {
+			throw new RefGoneException();
+		}
+
+		initRevWalk();
+		remoteId = ref.getObjectId();
+		return rw.parseCommit(remoteId);
+	}
+
+	private static class RefGoneException extends Exception {
+		private static final long serialVersionUID = 1L;
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LeaderSnapshot.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LeaderSnapshot.java
new file mode 100644
index 0000000..28a49df
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LeaderSnapshot.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2016, 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.ketch;
+
+import static org.eclipse.jgit.internal.ketch.KetchReplica.State.OFFLINE;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.lib.ObjectId;
+
+/** A snapshot of a leader and its view of the world. */
+public class LeaderSnapshot {
+	final List<ReplicaSnapshot> replicas = new ArrayList<>();
+	KetchLeader.State state;
+	long term;
+	LogIndex headIndex;
+	LogIndex committedIndex;
+	boolean idle;
+
+	LeaderSnapshot() {
+	}
+
+	/** @return unmodifiable view of configured replicas. */
+	public Collection<ReplicaSnapshot> getReplicas() {
+		return Collections.unmodifiableList(replicas);
+	}
+
+	/** @return current state of the leader. */
+	public KetchLeader.State getState() {
+		return state;
+	}
+
+	/**
+	 * @return {@code true} if the leader is not running a round to reach
+	 *         consensus, and has no rounds queued.
+	 */
+	public boolean isIdle() {
+		return idle;
+	}
+
+	/**
+	 * @return term of this leader. Valid only if {@link #getState()} is
+	 *         currently {@link KetchLeader.State#LEADER}.
+	 */
+	public long getTerm() {
+		return term;
+	}
+
+	/**
+	 * @return end of the leader's log; null if leader hasn't started up enough
+	 *         to begin its own election.
+	 */
+	@Nullable
+	public LogIndex getHead() {
+		return headIndex;
+	}
+
+	/**
+	 * @return state the leader knows is committed on a majority of participant
+	 *         replicas. Null until the leader instance has committed a log
+	 *         index within its own term.
+	 */
+	@Nullable
+	public LogIndex getCommitted() {
+		return committedIndex;
+	}
+
+	@Override
+	public String toString() {
+		StringBuilder s = new StringBuilder();
+		s.append(isIdle() ? "IDLE" : "RUNNING"); //$NON-NLS-1$ //$NON-NLS-2$
+		s.append(" state ").append(getState()); //$NON-NLS-1$
+		if (getTerm() > 0) {
+			s.append(" term ").append(getTerm()); //$NON-NLS-1$
+		}
+		s.append('\n');
+		s.append(String.format(
+				"%-10s %12s %12s\n", //$NON-NLS-1$
+				"Replica", "Accepted", "Committed")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+		s.append("------------------------------------\n"); //$NON-NLS-1$
+		debug(s, "(leader)", getHead(), getCommitted()); //$NON-NLS-1$
+		s.append('\n');
+		for (ReplicaSnapshot r : getReplicas()) {
+			debug(s, r);
+			s.append('\n');
+		}
+		s.append('\n');
+		return s.toString();
+	}
+
+	private static void debug(StringBuilder b, ReplicaSnapshot s) {
+		KetchReplica replica = s.getReplica();
+		debug(b, replica.getName(), s.getAccepted(), s.getCommitted());
+		b.append(String.format(" %-8s %s", //$NON-NLS-1$
+				replica.getParticipation(), s.getState()));
+		if (s.getState() == OFFLINE) {
+			String err = s.getErrorMessage();
+			if (err != null) {
+				b.append(" (").append(err).append(')'); //$NON-NLS-1$
+			}
+		}
+	}
+
+	private static void debug(StringBuilder s, String name,
+			ObjectId accepted, ObjectId committed) {
+		s.append(String.format(
+				"%-10s %-12s %-12s", //$NON-NLS-1$
+				name, str(accepted), str(committed)));
+	}
+
+	static String str(ObjectId c) {
+		if (c instanceof LogIndex) {
+			return ((LogIndex) c).describeForLog();
+		} else if (c != null) {
+			return c.abbreviate(8).name();
+		}
+		return "-"; //$NON-NLS-1$
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LocalReplica.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LocalReplica.java
new file mode 100644
index 0000000..e297bca
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LocalReplica.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2016, 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.ketch;
+
+import static org.eclipse.jgit.internal.ketch.KetchReplica.CommitMethod.ALL_REFS;
+import static org.eclipse.jgit.internal.ketch.KetchReplica.CommitMethod.TXN_COMMITTED;
+import static org.eclipse.jgit.lib.RefDatabase.ALL;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
+
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jgit.internal.storage.reftree.RefTreeDatabase;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+/** Ketch replica running on the same system as the {@link KetchLeader}. */
+public class LocalReplica extends KetchReplica {
+	/**
+	 * Configure a local replica.
+	 *
+	 * @param leader
+	 *            instance this replica follows.
+	 * @param name
+	 *            unique-ish name identifying this replica for debugging.
+	 * @param cfg
+	 *            how Ketch should treat the local system.
+	 */
+	public LocalReplica(KetchLeader leader, String name, ReplicaConfig cfg) {
+		super(leader, name, cfg);
+	}
+
+	@Override
+	protected String describeForLog() {
+		return String.format("%s (leader)", getName()); //$NON-NLS-1$
+	}
+
+	/**
+	 * Initializes local replica by reading accepted and committed references.
+	 * <p>
+	 * Loads accepted and committed references from the reference database of
+	 * the local replica and stores their current ObjectIds in memory.
+	 *
+	 * @param repo
+	 *            repository to initialize state from.
+	 * @throws IOException
+	 *             cannot read repository state.
+	 */
+	void initialize(Repository repo) throws IOException {
+		RefDatabase refdb = repo.getRefDatabase();
+		if (refdb instanceof RefTreeDatabase) {
+			RefTreeDatabase treeDb = (RefTreeDatabase) refdb;
+			String txnNamespace = getSystem().getTxnNamespace();
+			if (!txnNamespace.equals(treeDb.getTxnNamespace())) {
+				throw new IOException(MessageFormat.format(
+						KetchText.get().mismatchedTxnNamespace,
+						txnNamespace, treeDb.getTxnNamespace()));
+			}
+			refdb = treeDb.getBootstrap();
+		}
+		initialize(refdb.exactRef(
+				getSystem().getTxnAccepted(),
+				getSystem().getTxnCommitted()));
+	}
+
+	@Override
+	protected void startPush(final ReplicaPushRequest req) {
+		getSystem().getExecutor().execute(new Runnable() {
+			@Override
+			public void run() {
+				try (Repository git = getLeader().openRepository()) {
+					try {
+						update(git, req);
+						req.done(git);
+					} catch (Throwable err) {
+						req.setException(git, err);
+					}
+				} catch (IOException err) {
+					req.setException(null, err);
+				}
+			}
+		});
+	}
+
+	@Override
+	protected void blockingFetch(Repository repo, ReplicaFetchRequest req)
+			throws IOException {
+		throw new IOException(KetchText.get().cannotFetchFromLocalReplica);
+	}
+
+	private void update(Repository git, ReplicaPushRequest req)
+			throws IOException {
+		RefDatabase refdb = git.getRefDatabase();
+		CommitMethod method = getCommitMethod();
+
+		// Local replica probably uses RefTreeDatabase, the request should
+		// be only for the txnNamespace, so drop to the bootstrap layer.
+		if (refdb instanceof RefTreeDatabase) {
+			if (!isOnlyTxnNamespace(req.getCommands())) {
+				return;
+			}
+
+			refdb = ((RefTreeDatabase) refdb).getBootstrap();
+			method = TXN_COMMITTED;
+		}
+
+		BatchRefUpdate batch = refdb.newBatchUpdate();
+		batch.setRefLogIdent(getSystem().newCommitter());
+		batch.setRefLogMessage("ketch", false); //$NON-NLS-1$
+		batch.setAllowNonFastForwards(true);
+
+		// RefDirectory updates multiple references sequentially.
+		// Run everything else first, then accepted (if present),
+		// then committed (if present). This ensures an earlier
+		// failure will not update these critical references.
+		ReceiveCommand accepted = null;
+		ReceiveCommand committed = null;
+		for (ReceiveCommand cmd : req.getCommands()) {
+			String name = cmd.getRefName();
+			if (name.equals(getSystem().getTxnAccepted())) {
+				accepted = cmd;
+			} else if (name.equals(getSystem().getTxnCommitted())) {
+				committed = cmd;
+			} else {
+				batch.addCommand(cmd);
+			}
+		}
+		if (committed != null && method == ALL_REFS) {
+			Map<String, Ref> refs = refdb.getRefs(ALL);
+			batch.addCommand(prepareCommit(git, refs, committed.getNewId()));
+		}
+		if (accepted != null) {
+			batch.addCommand(accepted);
+		}
+		if (committed != null) {
+			batch.addCommand(committed);
+		}
+
+		try (RevWalk rw = new RevWalk(git)) {
+			batch.execute(rw, NullProgressMonitor.INSTANCE);
+		}
+
+		// KetchReplica only cares about accepted and committed in
+		// advertisement. If they failed, store the current values
+		// back in the ReplicaPushRequest.
+		List<String> failed = new ArrayList<>(2);
+		checkFailed(failed, accepted);
+		checkFailed(failed, committed);
+		if (!failed.isEmpty()) {
+			String[] arr = failed.toArray(new String[failed.size()]);
+			req.setRefs(refdb.exactRef(arr));
+		}
+	}
+
+	private static void checkFailed(List<String> failed, ReceiveCommand cmd) {
+		if (cmd != null && cmd.getResult() != OK) {
+			failed.add(cmd.getRefName());
+		}
+	}
+
+	private boolean isOnlyTxnNamespace(Collection<ReceiveCommand> cmdList) {
+		// Be paranoid and reject non txnNamespace names, this
+		// is a programming error in Ketch that should not occur.
+
+		String txnNamespace = getSystem().getTxnNamespace();
+		for (ReceiveCommand cmd : cmdList) {
+			if (!cmd.getRefName().startsWith(txnNamespace)) {
+				cmd.setResult(REJECTED_OTHER_REASON,
+						MessageFormat.format(
+								KetchText.get().outsideTxnNamespace,
+								cmd.getRefName(), txnNamespace));
+				ReceiveCommand.abort(cmdList);
+				return false;
+			}
+		}
+		return true;
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LogIndex.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LogIndex.java
new file mode 100644
index 0000000..350c8ed
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/LogIndex.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2016, 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.ketch;
+
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.ObjectId;
+
+/**
+ * An ObjectId for a commit extended with incrementing log index.
+ * <p>
+ * For any two LogIndex instances, {@code A} is an ancestor of {@code C}
+ * reachable through parent edges in the graph if {@code A.index < C.index}.
+ * LogIndex provides a performance optimization for Ketch, the same information
+ * can be obtained from {@link org.eclipse.jgit.revwalk.RevWalk}.
+ * <p>
+ * Index values are only valid within a single {@link KetchLeader} instance
+ * after it has won an election. By restricting scope to a single leader new
+ * leaders do not need to traverse the entire history to determine the next
+ * {@code index} for new proposals. This differs from Raft, where leader
+ * election uses the log index and the term number to determine which replica
+ * holds a sufficiently up-to-date log. Since Ketch uses Git objects for storage
+ * of its replicated log, it keeps the term number as Raft does but uses
+ * standard Git operations to imply the log index.
+ * <p>
+ * {@link Round#runAsync(AnyObjectId)} bumps the index as each new round is
+ * constructed.
+ */
+public class LogIndex extends ObjectId {
+	static LogIndex unknown(AnyObjectId id) {
+		return new LogIndex(id, 0);
+	}
+
+	private final long index;
+
+	private LogIndex(AnyObjectId id, long index) {
+		super(id);
+		this.index = index;
+	}
+
+	LogIndex nextIndex(AnyObjectId id) {
+		return new LogIndex(id, index + 1);
+	}
+
+	/** @return index provided by the current leader instance. */
+	public long getIndex() {
+		return index;
+	}
+
+	/**
+	 * Check if this log position committed before another log position.
+	 * <p>
+	 * Only valid for log positions in memory for the current leader.
+	 *
+	 * @param c
+	 *            other (more recent) log position.
+	 * @return true if this log position was before {@code c} or equal to c and
+	 *         therefore any agreement of {@code c} implies agreement on this
+	 *         log position.
+	 */
+	boolean isBefore(LogIndex c) {
+		return index <= c.index;
+	}
+
+	/**
+	 * @return string suitable for debug logging containing the log index and
+	 *         abbreviated ObjectId.
+	 */
+	@SuppressWarnings("boxing")
+	public String describeForLog() {
+		return String.format("%5d/%s", index, abbreviate(6).name()); //$NON-NLS-1$
+	}
+
+	@SuppressWarnings("boxing")
+	@Override
+	public String toString() {
+		return String.format("LogId[%5d/%s]", index, name()); //$NON-NLS-1$
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/Proposal.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/Proposal.java
new file mode 100644
index 0000000..0876eb5
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/Proposal.java
@@ -0,0 +1,391 @@
+/*
+ * Copyright (C) 2016, 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.ketch;
+
+import static org.eclipse.jgit.internal.ketch.Proposal.State.ABORTED;
+import static org.eclipse.jgit.internal.ketch.Proposal.State.EXECUTED;
+import static org.eclipse.jgit.internal.ketch.Proposal.State.NEW;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.internal.storage.reftree.Command;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.PushCertificate;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+/**
+ * A proposal to be applied in a Ketch system.
+ * <p>
+ * Pushing to a Ketch leader results in the leader making a proposal. The
+ * proposal includes the list of reference updates. The leader attempts to send
+ * the proposal to a quorum of replicas by pushing the proposal to a "staging"
+ * area under the {@code refs/txn/stage/} namespace. If the proposal succeeds
+ * then the changes are durable and the leader can commit the proposal.
+ * <p>
+ * Proposals are executed by {@link KetchLeader#queueProposal(Proposal)}, which
+ * runs them asynchronously in the background. Proposals are thread-safe futures
+ * allowing callers to {@link #await()} for results or be notified by callback
+ * using {@link #addListener(Runnable)}.
+ */
+public class Proposal {
+	/** Current state of the proposal. */
+	public enum State {
+		/** Proposal has not yet been given to a {@link KetchLeader}. */
+		NEW(false),
+
+		/**
+		 * Proposal was validated and has entered the queue, but a round
+		 * containing this proposal has not started yet.
+		 */
+		QUEUED(false),
+
+		/** Round containing the proposal has begun and is in progress. */
+		RUNNING(false),
+
+		/**
+		 * Proposal was executed through a round. Individual results from
+		 * {@link Proposal#getCommands()}, {@link Command#getResult()} explain
+		 * the success or failure outcome.
+		 */
+		EXECUTED(true),
+
+		/** Proposal was aborted and did not reach consensus. */
+		ABORTED(true);
+
+		private final boolean done;
+
+		private State(boolean done) {
+			this.done = done;
+		}
+
+		/** @return true if this is a terminal state. */
+		public boolean isDone() {
+			return done;
+		}
+	}
+
+	private final List<Command> commands;
+	private PersonIdent author;
+	private String message;
+	private PushCertificate pushCert;
+	private final List<Runnable> listeners = new CopyOnWriteArrayList<>();
+	private final AtomicReference<State> state = new AtomicReference<>(NEW);
+
+	/**
+	 * Create a proposal from a list of Ketch commands.
+	 *
+	 * @param cmds
+	 *            prepared list of commands.
+	 */
+	public Proposal(List<Command> cmds) {
+		commands = Collections.unmodifiableList(new ArrayList<>(cmds));
+	}
+
+	/**
+	 * Create a proposal from a collection of received commands.
+	 *
+	 * @param rw
+	 *            walker to assist in preparing commands.
+	 * @param cmds
+	 *            list of pending commands.
+	 * @throws MissingObjectException
+	 *             newId of a command is not found locally.
+	 * @throws IOException
+	 *             local objects cannot be accessed.
+	 */
+	public Proposal(RevWalk rw, Collection<ReceiveCommand> cmds)
+			throws MissingObjectException, IOException {
+		commands = asCommandList(rw, cmds);
+	}
+
+	private static List<Command> asCommandList(RevWalk rw,
+			Collection<ReceiveCommand> cmds)
+					throws MissingObjectException, IOException {
+		List<Command> commands = new ArrayList<>(cmds.size());
+		for (ReceiveCommand cmd : cmds) {
+			commands.add(new Command(rw, cmd));
+		}
+		return Collections.unmodifiableList(commands);
+	}
+
+	/** @return commands from this proposal. */
+	public Collection<Command> getCommands() {
+		return commands;
+	}
+
+	/** @return optional author of the proposal. */
+	@Nullable
+	public PersonIdent getAuthor() {
+		return author;
+	}
+
+	/**
+	 * Set the author for the proposal.
+	 *
+	 * @param who
+	 *            optional identity of the author of the proposal.
+	 * @return {@code this}
+	 */
+	public Proposal setAuthor(@Nullable PersonIdent who) {
+		author = who;
+		return this;
+	}
+
+	/** @return optional message for the commit log of the RefTree. */
+	@Nullable
+	public String getMessage() {
+		return message;
+	}
+
+	/**
+	 * Set the message to appear in the commit log of the RefTree.
+	 *
+	 * @param msg
+	 *            message text for the commit.
+	 * @return {@code this}
+	 */
+	public Proposal setMessage(@Nullable String msg) {
+		message = msg != null && !msg.isEmpty() ? msg : null;
+		return this;
+	}
+
+	/** @return optional certificate signing the references. */
+	@Nullable
+	public PushCertificate getPushCertificate() {
+		return pushCert;
+	}
+
+	/**
+	 * Set the push certificate signing the references.
+	 *
+	 * @param cert
+	 *            certificate, may be null.
+	 * @return {@code this}
+	 */
+	public Proposal setPushCertificate(@Nullable PushCertificate cert) {
+		pushCert = cert;
+		return this;
+	}
+
+	/**
+	 * Add a callback to be invoked when the proposal is done.
+	 * <p>
+	 * A proposal is done when it has entered either {@link State#EXECUTED} or
+	 * {@link State#ABORTED} state. If the proposal is already done
+	 * {@code callback.run()} is immediately invoked on the caller's thread.
+	 *
+	 * @param callback
+	 *            method to run after the proposal is done. The callback may be
+	 *            run on a Ketch system thread and should be completed quickly.
+	 */
+	public void addListener(Runnable callback) {
+		boolean runNow = false;
+		synchronized (state) {
+			if (state.get().isDone()) {
+				runNow = true;
+			} else {
+				listeners.add(callback);
+			}
+		}
+		if (runNow) {
+			callback.run();
+		}
+	}
+
+	/** Set command result as OK. */
+	void success() {
+		for (Command c : commands) {
+			if (c.getResult() == NOT_ATTEMPTED) {
+				c.setResult(OK);
+			}
+		}
+		notifyState(EXECUTED);
+	}
+
+	/** Mark commands as "transaction aborted". */
+	void abort() {
+		Command.abort(commands, null);
+		notifyState(ABORTED);
+	}
+
+	/** @return read the current state of the proposal. */
+	public State getState() {
+		return state.get();
+	}
+
+	/**
+	 * @return {@code true} if the proposal was attempted. A true value does not
+	 *         mean consensus was reached, only that the proposal was considered
+	 *         and will not be making any more progress beyond its current
+	 *         state.
+	 */
+	public boolean isDone() {
+		return state.get().isDone();
+	}
+
+	/**
+	 * Wait for the proposal to be attempted and {@link #isDone()} to be true.
+	 *
+	 * @throws InterruptedException
+	 *             caller was interrupted before proposal executed.
+	 */
+	public void await() throws InterruptedException {
+		synchronized (state) {
+			while (!state.get().isDone()) {
+				state.wait();
+			}
+		}
+	}
+
+	/**
+	 * Wait for the proposal to be attempted and {@link #isDone()} to be true.
+	 *
+	 * @param wait
+	 *            how long to wait.
+	 * @param unit
+	 *            unit describing the wait time.
+	 * @return true if the proposal is done; false if the method timed out.
+	 * @throws InterruptedException
+	 *             caller was interrupted before proposal executed.
+	 */
+	public boolean await(long wait, TimeUnit unit) throws InterruptedException {
+		synchronized (state) {
+			if (state.get().isDone()) {
+				return true;
+			}
+			state.wait(unit.toMillis(wait));
+			return state.get().isDone();
+		}
+	}
+
+	/**
+	 * Wait for the proposal to exit a state.
+	 *
+	 * @param notIn
+	 *            state the proposal should not be in to return.
+	 * @param wait
+	 *            how long to wait.
+	 * @param unit
+	 *            unit describing the wait time.
+	 * @return true if the proposal exited the state; false on time out.
+	 * @throws InterruptedException
+	 *             caller was interrupted before proposal executed.
+	 */
+	public boolean awaitStateChange(State notIn, long wait, TimeUnit unit)
+			throws InterruptedException {
+		synchronized (state) {
+			if (state.get() != notIn) {
+				return true;
+			}
+			state.wait(unit.toMillis(wait));
+			return state.get() != notIn;
+		}
+	}
+
+	void notifyState(State s) {
+		synchronized (state) {
+			state.set(s);
+			state.notifyAll();
+		}
+		if (s.isDone()) {
+			for (Runnable callback : listeners) {
+				callback.run();
+			}
+			listeners.clear();
+		}
+	}
+
+	@Override
+	public String toString() {
+		StringBuilder s = new StringBuilder();
+		s.append("Ketch Proposal {\n"); //$NON-NLS-1$
+		s.append("  ").append(state.get()).append('\n'); //$NON-NLS-1$
+		if (author != null) {
+			s.append("  author ").append(author).append('\n'); //$NON-NLS-1$
+		}
+		if (message != null) {
+			s.append("  message ").append(message).append('\n'); //$NON-NLS-1$
+		}
+		for (Command c : commands) {
+			s.append("  "); //$NON-NLS-1$
+			format(s, c.getOldRef(), "CREATE"); //$NON-NLS-1$
+			s.append(' ');
+			format(s, c.getNewRef(), "DELETE"); //$NON-NLS-1$
+			s.append(' ').append(c.getRefName());
+			if (c.getResult() != ReceiveCommand.Result.NOT_ATTEMPTED) {
+				s.append(' ').append(c.getResult()); // $NON-NLS-1$
+			}
+			s.append('\n');
+		}
+		s.append('}');
+		return s.toString();
+	}
+
+	private static void format(StringBuilder s, @Nullable Ref r, String n) {
+		if (r == null) {
+			s.append(n);
+		} else if (r.isSymbolic()) {
+			s.append(r.getTarget().getName());
+		} else {
+			ObjectId id = r.getObjectId();
+			if (id != null) {
+				s.append(id.abbreviate(8).name());
+			}
+		}
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ProposalRound.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ProposalRound.java
new file mode 100644
index 0000000..d34477a
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ProposalRound.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright (C) 2016, 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.ketch;
+
+import static org.eclipse.jgit.internal.ketch.Proposal.State.RUNNING;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.internal.storage.reftree.Command;
+import org.eclipse.jgit.internal.storage.reftree.RefTree;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+/** A {@link Round} that aggregates and sends user {@link Proposal}s. */
+class ProposalRound extends Round {
+	private final List<Proposal> todo;
+	private RefTree queuedTree;
+
+	ProposalRound(KetchLeader leader, LogIndex head, List<Proposal> todo,
+			@Nullable RefTree tree) {
+		super(leader, head);
+		this.todo = todo;
+
+		if (tree != null && canCombine(todo)) {
+			this.queuedTree = tree;
+		} else {
+			leader.roundHoldsReferenceToRefTree = false;
+		}
+	}
+
+	private static boolean canCombine(List<Proposal> todo) {
+		Proposal first = todo.get(0);
+		for (int i = 1; i < todo.size(); i++) {
+			if (!canCombine(first, todo.get(i))) {
+				return false;
+			}
+		}
+		return true;
+	}
+
+	private static boolean canCombine(Proposal a, Proposal b) {
+		String aMsg = nullToEmpty(a.getMessage());
+		String bMsg = nullToEmpty(b.getMessage());
+		return aMsg.equals(bMsg) && canCombine(a.getAuthor(), b.getAuthor());
+	}
+
+	private static String nullToEmpty(@Nullable String str) {
+		return str != null ? str : ""; //$NON-NLS-1$
+	}
+
+	private static boolean canCombine(@Nullable PersonIdent a,
+			@Nullable PersonIdent b) {
+		if (a != null && b != null) {
+			// Same name and email address. Combine timestamp as the two
+			// proposals are running concurrently and appear together or
+			// not at all from the point of view of an outside reader.
+			return a.getName().equals(b.getName())
+					&& a.getEmailAddress().equals(b.getEmailAddress());
+		}
+
+		// If a and b are null, both will be the system identity.
+		return a == null && b == null;
+	}
+
+	void start() throws IOException {
+		for (Proposal p : todo) {
+			p.notifyState(RUNNING);
+		}
+		try {
+			ObjectId id;
+			try (Repository git = leader.openRepository()) {
+				id = insertProposals(git);
+			}
+			runAsync(id);
+		} catch (NoOp e) {
+			for (Proposal p : todo) {
+				p.success();
+			}
+			leader.lock.lock();
+			try {
+				leader.nextRound();
+			} finally {
+				leader.lock.unlock();
+			}
+		} catch (IOException e) {
+			abort();
+			throw e;
+		}
+	}
+
+	private ObjectId insertProposals(Repository git)
+			throws IOException, NoOp {
+		ObjectId id;
+		try (ObjectInserter inserter = git.newObjectInserter()) {
+			// TODO(sop) Process signed push certificates.
+
+			if (queuedTree != null) {
+				id = insertSingleProposal(git, inserter);
+			} else {
+				id = insertMultiProposal(git, inserter);
+			}
+
+			stageCommands = makeStageList(git, inserter);
+			inserter.flush();
+		}
+		return id;
+	}
+
+	private ObjectId insertSingleProposal(Repository git,
+			ObjectInserter inserter) throws IOException, NoOp {
+		// Fast path: tree is passed in with all proposals applied.
+		ObjectId treeId = queuedTree.writeTree(inserter);
+		queuedTree = null;
+		leader.roundHoldsReferenceToRefTree = false;
+
+		if (!ObjectId.zeroId().equals(acceptedOldIndex)) {
+			try (RevWalk rw = new RevWalk(git)) {
+				RevCommit c = rw.parseCommit(acceptedOldIndex);
+				if (treeId.equals(c.getTree())) {
+					throw new NoOp();
+				}
+			}
+		}
+
+		Proposal p = todo.get(0);
+		CommitBuilder b = new CommitBuilder();
+		b.setTreeId(treeId);
+		if (!ObjectId.zeroId().equals(acceptedOldIndex)) {
+			b.setParentId(acceptedOldIndex);
+		}
+		b.setCommitter(leader.getSystem().newCommitter());
+		b.setAuthor(p.getAuthor() != null ? p.getAuthor() : b.getCommitter());
+		b.setMessage(message(p));
+		return inserter.insert(b);
+	}
+
+	private ObjectId insertMultiProposal(Repository git,
+			ObjectInserter inserter) throws IOException, NoOp {
+		// The tree was not passed in, or there are multiple proposals
+		// each needing their own commit. Reset the tree and replay each
+		// proposal in order as individual commits.
+		ObjectId lastIndex = acceptedOldIndex;
+		ObjectId oldTreeId;
+		RefTree tree;
+		if (ObjectId.zeroId().equals(lastIndex)) {
+			oldTreeId = ObjectId.zeroId();
+			tree = RefTree.newEmptyTree();
+		} else {
+			try (RevWalk rw = new RevWalk(git)) {
+				RevCommit c = rw.parseCommit(lastIndex);
+				oldTreeId = c.getTree();
+				tree = RefTree.read(rw.getObjectReader(), c.getTree());
+			}
+		}
+
+		PersonIdent committer = leader.getSystem().newCommitter();
+		for (Proposal p : todo) {
+			if (!tree.apply(p.getCommands())) {
+				// This should not occur, previously during queuing the
+				// commands were successfully applied to the pending tree.
+				// Abort the entire round.
+				throw new IOException(
+						KetchText.get().queuedProposalFailedToApply);
+			}
+
+			ObjectId treeId = tree.writeTree(inserter);
+			if (treeId.equals(oldTreeId)) {
+				continue;
+			}
+
+			CommitBuilder b = new CommitBuilder();
+			b.setTreeId(treeId);
+			if (!ObjectId.zeroId().equals(lastIndex)) {
+				b.setParentId(lastIndex);
+			}
+			b.setAuthor(p.getAuthor() != null ? p.getAuthor() : committer);
+			b.setCommitter(committer);
+			b.setMessage(message(p));
+			lastIndex = inserter.insert(b);
+		}
+		if (lastIndex.equals(acceptedOldIndex)) {
+			throw new NoOp();
+		}
+		return lastIndex;
+	}
+
+	private String message(Proposal p) {
+		StringBuilder m = new StringBuilder();
+		String msg = p.getMessage();
+		if (msg != null && !msg.isEmpty()) {
+			m.append(msg);
+			while (m.length() < 2 || m.charAt(m.length() - 2) != '\n'
+					|| m.charAt(m.length() - 1) != '\n') {
+				m.append('\n');
+			}
+		}
+		m.append(KetchConstants.TERM.getName())
+				.append(": ") //$NON-NLS-1$
+				.append(leader.getTerm());
+		return m.toString();
+	}
+
+	void abort() {
+		for (Proposal p : todo) {
+			p.abort();
+		}
+	}
+
+	void success() {
+		for (Proposal p : todo) {
+			p.success();
+		}
+	}
+
+	private List<ReceiveCommand> makeStageList(Repository git,
+			ObjectInserter inserter) throws IOException {
+		// For each branch, collapse consecutive updates to only most recent,
+		// avoiding sending multiple objects in a rapid fast-forward chain, or
+		// rewritten content.
+		Map<String, ObjectId> byRef = new HashMap<>();
+		for (Proposal p : todo) {
+			for (Command c : p.getCommands()) {
+				Ref n = c.getNewRef();
+				if (n != null && !n.isSymbolic()) {
+					byRef.put(n.getName(), n.getObjectId());
+				}
+			}
+		}
+		if (byRef.isEmpty()) {
+			return Collections.emptyList();
+		}
+
+		Set<ObjectId> newObjs = new HashSet<>(byRef.values());
+		StageBuilder b = new StageBuilder(
+				leader.getSystem().getTxnStage(),
+				acceptedNewIndex);
+		return b.makeStageList(newObjs, git, inserter);
+	}
+
+
+	private static class NoOp extends Exception {
+		private static final long serialVersionUID = 1L;
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/RemoteGitReplica.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/RemoteGitReplica.java
new file mode 100644
index 0000000..6f4a178
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/RemoteGitReplica.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2016, 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.ketch;
+
+import static org.eclipse.jgit.internal.ketch.KetchReplica.CommitMethod.ALL_REFS;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.LOCK_FAILURE;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_NODELETE;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_NONFASTFORWARD;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
+import static org.eclipse.jgit.lib.Ref.Storage.NETWORK;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.errors.NotSupportedException;
+import org.eclipse.jgit.errors.TransportException;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.FetchConnection;
+import org.eclipse.jgit.transport.PushConnection;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.transport.RemoteConfig;
+import org.eclipse.jgit.transport.RemoteRefUpdate;
+import org.eclipse.jgit.transport.Transport;
+import org.eclipse.jgit.transport.URIish;
+
+/**
+ * Representation of a Git repository on a remote replica system.
+ * <p>
+ * {@link KetchLeader} will contact the replica using the Git wire protocol.
+ * <p>
+ * The remote replica may be fully Ketch-aware, or a standard Git server.
+ */
+public class RemoteGitReplica extends KetchReplica {
+	private final URIish uri;
+	private final RemoteConfig remoteConfig;
+
+	/**
+	 * Configure a new remote.
+	 *
+	 * @param leader
+	 *            instance this replica follows.
+	 * @param name
+	 *            unique-ish name identifying this remote for debugging.
+	 * @param uri
+	 *            URI to connect to the follower's repository.
+	 * @param cfg
+	 *            how Ketch should treat the remote system.
+	 * @param rc
+	 *            optional remote configuration describing how to contact the
+	 *            peer repository.
+	 */
+	public RemoteGitReplica(KetchLeader leader, String name, URIish uri,
+			ReplicaConfig cfg, @Nullable RemoteConfig rc) {
+		super(leader, name, cfg);
+		this.uri = uri;
+		this.remoteConfig = rc;
+	}
+
+	/** @return URI to contact the remote peer repository. */
+	public URIish getURI() {
+		return uri;
+	}
+
+	/** @return optional configuration describing how to contact the peer. */
+	@Nullable
+	protected RemoteConfig getRemoteConfig() {
+		return remoteConfig;
+	}
+
+	@Override
+	protected String describeForLog() {
+		return String.format("%s @ %s", getName(), getURI()); //$NON-NLS-1$
+	}
+
+	@Override
+	protected void startPush(final ReplicaPushRequest req) {
+		getSystem().getExecutor().execute(new Runnable() {
+			@Override
+			public void run() {
+				try (Repository git = getLeader().openRepository()) {
+					try {
+						push(git, req);
+						req.done(git);
+					} catch (Throwable err) {
+						req.setException(git, err);
+					}
+				} catch (IOException err) {
+					req.setException(null, err);
+				}
+			}
+		});
+	}
+
+	private void push(Repository repo, ReplicaPushRequest req)
+			throws NotSupportedException, TransportException, IOException {
+		Map<String, Ref> adv;
+		List<RemoteCommand> cmds = asUpdateList(req.getCommands());
+		try (Transport transport = Transport.open(repo, uri)) {
+			RemoteConfig rc = getRemoteConfig();
+			if (rc != null) {
+				transport.applyConfig(rc);
+			}
+			transport.setPushAtomic(true);
+			adv = push(repo, transport, cmds);
+		}
+		for (RemoteCommand c : cmds) {
+			c.copyStatusToResult();
+		}
+		req.setRefs(adv);
+	}
+
+	private Map<String, Ref> push(Repository git, Transport transport,
+			List<RemoteCommand> cmds) throws IOException {
+		Map<String, RemoteRefUpdate> updates = asUpdateMap(cmds);
+		try (PushConnection connection = transport.openPush()) {
+			Map<String, Ref> adv = connection.getRefsMap();
+			RemoteRefUpdate accepted = updates.get(getSystem().getTxnAccepted());
+			if (accepted != null && !isExpectedValue(adv, accepted)) {
+				abort(cmds);
+				return adv;
+			}
+
+			RemoteRefUpdate committed = updates.get(getSystem().getTxnCommitted());
+			if (committed != null && !isExpectedValue(adv, committed)) {
+				abort(cmds);
+				return adv;
+			}
+			if (committed != null && getCommitMethod() == ALL_REFS) {
+				prepareCommit(git, cmds, updates, adv,
+						committed.getNewObjectId());
+			}
+
+			connection.push(NullProgressMonitor.INSTANCE, updates);
+			return adv;
+		}
+	}
+
+	private static boolean isExpectedValue(Map<String, Ref> adv,
+			RemoteRefUpdate u) {
+		Ref r = adv.get(u.getRemoteName());
+		if (!AnyObjectId.equals(getId(r), u.getExpectedOldObjectId())) {
+			((RemoteCommand) u).cmd.setResult(LOCK_FAILURE);
+			return false;
+		}
+		return true;
+	}
+
+	private void prepareCommit(Repository git, List<RemoteCommand> cmds,
+			Map<String, RemoteRefUpdate> updates, Map<String, Ref> adv,
+			ObjectId committed) throws IOException {
+		for (ReceiveCommand cmd : prepareCommit(git, adv, committed)) {
+			RemoteCommand c = new RemoteCommand(cmd);
+			cmds.add(c);
+			updates.put(c.getRemoteName(), c);
+		}
+	}
+
+	private static List<RemoteCommand> asUpdateList(
+			Collection<ReceiveCommand> cmds) {
+		try {
+			List<RemoteCommand> toPush = new ArrayList<>(cmds.size());
+			for (ReceiveCommand cmd : cmds) {
+				toPush.add(new RemoteCommand(cmd));
+			}
+			return toPush;
+		} catch (IOException e) {
+			// Cannot occur as no IO was required to build the command.
+			throw new IllegalStateException(e);
+		}
+	}
+
+	private static Map<String, RemoteRefUpdate> asUpdateMap(
+			List<RemoteCommand> cmds) {
+		Map<String, RemoteRefUpdate> m = new LinkedHashMap<>();
+		for (RemoteCommand cmd : cmds) {
+			m.put(cmd.getRemoteName(), cmd);
+		}
+		return m;
+	}
+
+	private static void abort(List<RemoteCommand> cmds) {
+		List<ReceiveCommand> tmp = new ArrayList<>(cmds.size());
+		for (RemoteCommand cmd : cmds) {
+			tmp.add(cmd.cmd);
+		}
+		ReceiveCommand.abort(tmp);
+	}
+
+	protected void blockingFetch(Repository repo, ReplicaFetchRequest req)
+			throws NotSupportedException, TransportException {
+		try (Transport transport = Transport.open(repo, uri)) {
+			RemoteConfig rc = getRemoteConfig();
+			if (rc != null) {
+				transport.applyConfig(rc);
+			}
+			fetch(transport, req);
+		}
+	}
+
+	private void fetch(Transport transport, ReplicaFetchRequest req)
+			throws NotSupportedException, TransportException {
+		try (FetchConnection conn = transport.openFetch()) {
+			Map<String, Ref> remoteRefs = conn.getRefsMap();
+			req.setRefs(remoteRefs);
+
+			List<Ref> want = new ArrayList<>();
+			for (String name : req.getWantRefs()) {
+				Ref ref = remoteRefs.get(name);
+				if (ref != null && ref.getObjectId() != null) {
+					want.add(ref);
+				}
+			}
+			for (ObjectId id : req.getWantObjects()) {
+				want.add(new ObjectIdRef.Unpeeled(NETWORK, id.name(), id));
+			}
+
+			conn.fetch(NullProgressMonitor.INSTANCE, want,
+					Collections.<ObjectId> emptySet());
+		}
+	}
+
+	static class RemoteCommand extends RemoteRefUpdate {
+		final ReceiveCommand cmd;
+
+		RemoteCommand(ReceiveCommand cmd) throws IOException {
+			super(null, null,
+					cmd.getNewId(), cmd.getRefName(),
+					true /* force update */,
+					null /* no local tracking ref */,
+					cmd.getOldId());
+			this.cmd = cmd;
+		}
+
+		void copyStatusToResult() {
+			if (cmd.getResult() == NOT_ATTEMPTED) {
+				switch (getStatus()) {
+				case OK:
+				case UP_TO_DATE:
+				case NON_EXISTING:
+					cmd.setResult(OK);
+					break;
+
+				case REJECTED_NODELETE:
+					cmd.setResult(REJECTED_NODELETE);
+					break;
+
+				case REJECTED_NONFASTFORWARD:
+					cmd.setResult(REJECTED_NONFASTFORWARD);
+					break;
+
+				case REJECTED_OTHER_REASON:
+					cmd.setResult(REJECTED_OTHER_REASON, getMessage());
+					break;
+
+				default:
+					cmd.setResult(REJECTED_OTHER_REASON, getStatus().name());
+					break;
+				}
+			}
+		}
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaConfig.java
new file mode 100644
index 0000000..e16e63a
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaConfig.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2016, 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.ketch;
+
+import static java.util.concurrent.TimeUnit.DAYS;
+import static java.util.concurrent.TimeUnit.HOURS;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.eclipse.jgit.internal.ketch.KetchConstants.CONFIG_KEY_COMMIT;
+import static org.eclipse.jgit.internal.ketch.KetchConstants.CONFIG_KEY_SPEED;
+import static org.eclipse.jgit.internal.ketch.KetchConstants.CONFIG_KEY_TYPE;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_REMOTE;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.eclipse.jgit.internal.ketch.KetchReplica.CommitMethod;
+import org.eclipse.jgit.internal.ketch.KetchReplica.CommitSpeed;
+import org.eclipse.jgit.internal.ketch.KetchReplica.Participation;
+import org.eclipse.jgit.lib.Config;
+
+/** Configures a {@link KetchReplica}. */
+public class ReplicaConfig {
+	/**
+	 * Read a configuration from a config block.
+	 *
+	 * @param cfg
+	 *            configuration to read.
+	 * @param name
+	 *            of the replica being configured.
+	 * @return replica configuration for {@code name}.
+	 */
+	public static ReplicaConfig newFromConfig(Config cfg, String name) {
+		return new ReplicaConfig().fromConfig(cfg, name);
+	}
+
+	private Participation participation = Participation.FULL;
+	private CommitMethod commitMethod = CommitMethod.ALL_REFS;
+	private CommitSpeed commitSpeed = CommitSpeed.BATCHED;
+	private long minRetry = SECONDS.toMillis(5);
+	private long maxRetry = MINUTES.toMillis(1);
+
+	/** @return participation of the replica in the system. */
+	public Participation getParticipation() {
+		return participation;
+	}
+
+	/** @return how Ketch should apply committed changes. */
+	public CommitMethod getCommitMethod() {
+		return commitMethod;
+	}
+
+	/** @return how quickly should Ketch commit. */
+	public CommitSpeed getCommitSpeed() {
+		return commitSpeed;
+	}
+
+	/**
+	 * Returns the minimum wait delay before retrying a failure.
+	 *
+	 * @param unit
+	 *            to get retry delay in.
+	 * @return minimum delay before retrying a failure.
+	 */
+	public long getMinRetry(TimeUnit unit) {
+		return unit.convert(minRetry, MILLISECONDS);
+	}
+
+	/**
+	 * Returns the maximum wait delay before retrying a failure.
+	 *
+	 * @param unit
+	 *            to get retry delay in.
+	 * @return maximum delay before retrying a failure.
+	 */
+	public long getMaxRetry(TimeUnit unit) {
+		return unit.convert(maxRetry, MILLISECONDS);
+	}
+
+	/**
+	 * Update the configuration from a config block.
+	 *
+	 * @param cfg
+	 *            configuration to read.
+	 * @param name
+	 *            of the replica being configured.
+	 * @return {@code this}
+	 */
+	public ReplicaConfig fromConfig(Config cfg, String name) {
+		participation = cfg.getEnum(
+				CONFIG_KEY_REMOTE, name, CONFIG_KEY_TYPE,
+				participation);
+		commitMethod = cfg.getEnum(
+				CONFIG_KEY_REMOTE, name, CONFIG_KEY_COMMIT,
+				commitMethod);
+		commitSpeed = cfg.getEnum(
+				CONFIG_KEY_REMOTE, name, CONFIG_KEY_SPEED,
+				commitSpeed);
+		minRetry = getMillis(cfg, name, "ketch-minRetry", minRetry); //$NON-NLS-1$
+		maxRetry = getMillis(cfg, name, "ketch-maxRetry", maxRetry); //$NON-NLS-1$
+		return this;
+	}
+
+	private static long getMillis(Config cfg, String name, String key,
+			long defaultValue) {
+		String valStr = cfg.getString(CONFIG_KEY_REMOTE, name, key);
+		if (valStr == null) {
+			return defaultValue;
+		}
+
+		valStr = valStr.trim();
+		if (valStr.isEmpty()) {
+			return defaultValue;
+		}
+
+		Matcher m = UnitMap.PATTERN.matcher(valStr);
+		if (!m.matches()) {
+			return defaultValue;
+		}
+
+		String digits = m.group(1);
+		String unitName = m.group(2).trim();
+		TimeUnit unit = UnitMap.UNITS.get(unitName);
+		if (unit == null) {
+			return defaultValue;
+		}
+
+		try {
+			if (digits.indexOf('.') == -1) {
+				return unit.toMillis(Long.parseLong(digits));
+			}
+
+			double val = Double.parseDouble(digits);
+			return (long) (val * unit.toMillis(1));
+		} catch (NumberFormatException nfe) {
+			return defaultValue;
+		}
+	}
+
+	static class UnitMap {
+		static final Pattern PATTERN = Pattern
+				.compile("^([1-9][0-9]*(?:\\.[0-9]*)?)\\s*(.*)$"); //$NON-NLS-1$
+
+		static final Map<String, TimeUnit> UNITS;
+
+		static {
+			Map<String, TimeUnit> m = new HashMap<>();
+			TimeUnit u = MILLISECONDS;
+			m.put("", u); //$NON-NLS-1$
+			m.put("ms", u); //$NON-NLS-1$
+			m.put("millis", u); //$NON-NLS-1$
+			m.put("millisecond", u); //$NON-NLS-1$
+			m.put("milliseconds", u); //$NON-NLS-1$
+
+			u = SECONDS;
+			m.put("s", u); //$NON-NLS-1$
+			m.put("sec", u); //$NON-NLS-1$
+			m.put("secs", u); //$NON-NLS-1$
+			m.put("second", u); //$NON-NLS-1$
+			m.put("seconds", u); //$NON-NLS-1$
+
+			u = MINUTES;
+			m.put("m", u); //$NON-NLS-1$
+			m.put("min", u); //$NON-NLS-1$
+			m.put("mins", u); //$NON-NLS-1$
+			m.put("minute", u); //$NON-NLS-1$
+			m.put("minutes", u); //$NON-NLS-1$
+
+			u = HOURS;
+			m.put("h", u); //$NON-NLS-1$
+			m.put("hr", u); //$NON-NLS-1$
+			m.put("hrs", u); //$NON-NLS-1$
+			m.put("hour", u); //$NON-NLS-1$
+			m.put("hours", u); //$NON-NLS-1$
+
+			u = DAYS;
+			m.put("d", u); //$NON-NLS-1$
+			m.put("day", u); //$NON-NLS-1$
+			m.put("days", u); //$NON-NLS-1$
+
+			UNITS = Collections.unmodifiableMap(m);
+		}
+
+		private UnitMap() {
+		}
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/SymlinkTreeEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaFetchRequest.java
similarity index 60%
copy from org.eclipse.jgit/src/org/eclipse/jgit/lib/SymlinkTreeEntry.java
copy to org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaFetchRequest.java
index c7e41bc..201d9e9 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/SymlinkTreeEntry.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaFetchRequest.java
@@ -1,6 +1,5 @@
 /*
- * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org>
+ * Copyright (C) 2016, Google Inc.
  * and other copyright owners as documented in the project's IP log.
  *
  * This program and the accompanying materials are made available
@@ -42,44 +41,56 @@
  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-package org.eclipse.jgit.lib;
+package org.eclipse.jgit.internal.ketch;
 
-/**
- * A tree entry representing a symbolic link.
- *
- * Note. Java cannot really handle these as file system objects.
- *
- * @deprecated To look up information about a single path, use
- * {@link org.eclipse.jgit.treewalk.TreeWalk#forPath(Repository, String, org.eclipse.jgit.revwalk.RevTree)}.
- * To lookup information about multiple paths at once, use a
- * {@link org.eclipse.jgit.treewalk.TreeWalk} and obtain the current entry's
- * information from its getter methods.
- */
-@Deprecated
-public class SymlinkTreeEntry extends TreeEntry {
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+
+/** A fetch request to obtain objects from a replica, and its result. */
+public class ReplicaFetchRequest {
+	private final Set<String> wantRefs;
+	private final Set<ObjectId> wantObjects;
+	private Map<String, Ref> refs;
 
 	/**
-	 * Construct a {@link SymlinkTreeEntry} with the specified name and SHA-1 in
-	 * the specified parent
+	 * Construct a new fetch request for a replica.
 	 *
-	 * @param parent
-	 * @param id
-	 * @param nameUTF8
+	 * @param wantRefs
+	 *            named references to be fetched.
+	 * @param wantObjects
+	 *            specific objects to be fetched.
 	 */
-	public SymlinkTreeEntry(final Tree parent, final ObjectId id,
-			final byte[] nameUTF8) {
-		super(parent, id, nameUTF8);
+	public ReplicaFetchRequest(Set<String> wantRefs,
+			Set<ObjectId> wantObjects) {
+		this.wantRefs = wantRefs;
+		this.wantObjects = wantObjects;
 	}
 
-	public FileMode getMode() {
-		return FileMode.SYMLINK;
+	/** @return references to be fetched. */
+	public Set<String> getWantRefs() {
+		return wantRefs;
 	}
 
-	public String toString() {
-		final StringBuilder r = new StringBuilder();
-		r.append(ObjectId.toString(getId()));
-		r.append(" S "); //$NON-NLS-1$
-		r.append(getFullName());
-		return r.toString();
+	/** @return objects to be fetched. */
+	public Set<ObjectId> getWantObjects() {
+		return wantObjects;
+	}
+
+	/** @return remote references, usually from the advertisement. */
+	@Nullable
+	public Map<String, Ref> getRefs() {
+		return refs;
+	}
+
+	/**
+	 * @param refs
+	 *            references observed from the replica.
+	 */
+	public void setRefs(Map<String, Ref> refs) {
+		this.refs = refs;
 	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaPushRequest.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaPushRequest.java
new file mode 100644
index 0000000..691b142
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaPushRequest.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2016, 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.ketch;
+
+import java.util.Collection;
+import java.util.Map;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+/**
+ * A push request sending objects to a replica, and its result.
+ * <p>
+ * Implementors of {@link KetchReplica} must populate the command result fields,
+ * {@link #setRefs(Map)}, and call one of
+ * {@link #setException(Repository, Throwable)} or {@link #done(Repository)} to
+ * finish processing.
+ */
+public class ReplicaPushRequest {
+	private final KetchReplica replica;
+	private final Collection<ReceiveCommand> commands;
+	private Map<String, Ref> refs;
+	private Throwable exception;
+	private boolean notified;
+
+	/**
+	 * Construct a new push request for a replica.
+	 *
+	 * @param replica
+	 *            the replica being pushed to.
+	 * @param commands
+	 *            commands to be executed.
+	 */
+	public ReplicaPushRequest(KetchReplica replica,
+			Collection<ReceiveCommand> commands) {
+		this.replica = replica;
+		this.commands = commands;
+	}
+
+	/** @return commands to be executed, and their results. */
+	public Collection<ReceiveCommand> getCommands() {
+		return commands;
+	}
+
+	/** @return remote references, usually from the advertisement. */
+	@Nullable
+	public Map<String, Ref> getRefs() {
+		return refs;
+	}
+
+	/**
+	 * @param refs
+	 *            references observed from the replica.
+	 */
+	public void setRefs(Map<String, Ref> refs) {
+		this.refs = refs;
+	}
+
+	/** @return exception thrown, if any. */
+	@Nullable
+	public Throwable getException() {
+		return exception;
+	}
+
+	/**
+	 * Mark the request as crashing with a communication error.
+	 * <p>
+	 * This method may take significant time acquiring the leader lock and
+	 * updating the Ketch state machine with the failure.
+	 *
+	 * @param repo
+	 *            local repository reference used by the push attempt.
+	 * @param err
+	 *            exception thrown during communication.
+	 */
+	public void setException(@Nullable Repository repo, Throwable err) {
+		if (KetchReplica.log.isErrorEnabled()) {
+			KetchReplica.log.error(describe("failed"), err); //$NON-NLS-1$
+		}
+		if (!notified) {
+			notified = true;
+			exception = err;
+			replica.afterPush(repo, this);
+		}
+	}
+
+	/**
+	 * Mark the request as completed without exception.
+	 * <p>
+	 * This method may take significant time acquiring the leader lock and
+	 * updating the Ketch state machine with results from this replica.
+	 *
+	 * @param repo
+	 *            local repository reference used by the push attempt.
+	 */
+	public void done(Repository repo) {
+		if (KetchReplica.log.isDebugEnabled()) {
+			KetchReplica.log.debug(describe("completed")); //$NON-NLS-1$
+		}
+		if (!notified) {
+			notified = true;
+			replica.afterPush(repo, this);
+		}
+	}
+
+	private String describe(String heading) {
+		StringBuilder b = new StringBuilder();
+		b.append("push to "); //$NON-NLS-1$
+		b.append(replica.describeForLog());
+		b.append(' ').append(heading).append(":\n"); //$NON-NLS-1$
+		for (ReceiveCommand cmd : commands) {
+			b.append(String.format(
+					"  %-12s %-12s %s %s", //$NON-NLS-1$
+					LeaderSnapshot.str(cmd.getOldId()),
+					LeaderSnapshot.str(cmd.getNewId()),
+					cmd.getRefName(),
+					cmd.getResult()));
+			if (cmd.getMessage() != null) {
+				b.append(' ').append(cmd.getMessage());
+			}
+			b.append('\n');
+		}
+		return b.toString();
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaSnapshot.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaSnapshot.java
new file mode 100644
index 0000000..8c3de02
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/ReplicaSnapshot.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2016, 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.ketch;
+
+import java.util.Date;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.lib.ObjectId;
+
+/**
+ * A snapshot of a replica.
+ *
+ * @see LeaderSnapshot
+ */
+public class ReplicaSnapshot {
+	final KetchReplica replica;
+	ObjectId accepted;
+	ObjectId committed;
+	KetchReplica.State state;
+	String error;
+	long retryAtMillis;
+
+	ReplicaSnapshot(KetchReplica replica) {
+		this.replica = replica;
+	}
+
+	/** @return the replica this snapshot describes the state of. */
+	public KetchReplica getReplica() {
+		return replica;
+	}
+
+	/** @return current state of the replica. */
+	public KetchReplica.State getState() {
+		return state;
+	}
+
+	/** @return last known Git commit at {@code refs/txn/accepted}. */
+	@Nullable
+	public ObjectId getAccepted() {
+		return accepted;
+	}
+
+	/** @return last known Git commit at {@code refs/txn/committed}. */
+	@Nullable
+	public ObjectId getCommitted() {
+		return committed;
+	}
+
+	/**
+	 * @return if {@link #getState()} == {@link KetchReplica.State#OFFLINE} an
+	 *         optional human-readable message from the transport system
+	 *         explaining the failure.
+	 */
+	@Nullable
+	public String getErrorMessage() {
+		return error;
+	}
+
+	/**
+	 * @return time (usually in the future) when the leader will retry
+	 *         communication with the offline or lagging replica; null if no
+	 *         retry is scheduled or necessary.
+	 */
+	@Nullable
+	public Date getRetryAt() {
+		return retryAtMillis > 0 ? new Date(retryAtMillis) : null;
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/Round.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/Round.java
new file mode 100644
index 0000000..1335b85
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/Round.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2016, 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.ketch;
+
+import java.io.IOException;
+import java.util.List;
+
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+/**
+ * One round-trip to all replicas proposing a log entry.
+ * <p>
+ * In Raft a log entry represents a state transition at a specific index in the
+ * replicated log. The leader can only append log entries to the log.
+ * <p>
+ * In Ketch a log entry is recorded under the {@code refs/txn} namespace. This
+ * occurs when:
+ * <ul>
+ * <li>a replica wants to establish itself as a new leader by proposing a new
+ * term (see {@link ElectionRound})
+ * <li>an established leader wants to gain consensus on new {@link Proposal}s
+ * (see {@link ProposalRound})
+ * </ul>
+ */
+abstract class Round {
+	final KetchLeader leader;
+	final LogIndex acceptedOldIndex;
+	LogIndex acceptedNewIndex;
+	List<ReceiveCommand> stageCommands;
+
+	Round(KetchLeader leader, LogIndex head) {
+		this.leader = leader;
+		this.acceptedOldIndex = head;
+	}
+
+	/**
+	 * Creates a commit for {@code refs/txn/accepted} and calls
+	 * {@link #runAsync(AnyObjectId)} to begin execution of the round across
+	 * the system.
+	 * <p>
+	 * If references are being updated (such as in a {@link ProposalRound}) the
+	 * RefTree may be modified.
+	 * <p>
+	 * Invoked without {@link KetchLeader#lock} to build objects.
+	 *
+	 * @throws IOException
+	 *             the round cannot build new objects within the leader's
+	 *             repository. The leader may be unable to execute.
+	 */
+	abstract void start() throws IOException;
+
+	/**
+	 * Asynchronously distribute the round's new value for
+	 * {@code refs/txn/accepted} to all replicas.
+	 * <p>
+	 * Invoked by {@link #start()} after new commits have been created for the
+	 * log. The method passes {@code newId} to {@link KetchLeader} to be
+	 * distributed to all known replicas.
+	 *
+	 * @param newId
+	 *            new value for {@code refs/txn/accepted}.
+	 */
+	void runAsync(AnyObjectId newId) {
+		acceptedNewIndex = acceptedOldIndex.nextIndex(newId);
+		leader.runAsync(this);
+	}
+
+	/**
+	 * Notify the round it was accepted by a majority of the system.
+	 * <p>
+	 * Invoked by the leader with {@link KetchLeader#lock} held by the caller.
+	 */
+	abstract void success();
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/StageBuilder.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/StageBuilder.java
new file mode 100644
index 0000000..61871a4
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/StageBuilder.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright (C) 2016, 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.ketch;
+
+import static org.eclipse.jgit.lib.FileMode.TYPE_GITLINK;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.treewalk.EmptyTreeIterator;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.filter.TreeFilter;
+
+/** Constructs a set of commands to stage content during a proposal. */
+public class StageBuilder {
+	/**
+	 * Acceptable number of references to send in a single stage transaction.
+	 * <p>
+	 * If the number of unique objects exceeds this amount the builder will
+	 * attempt to decrease the reference count by chaining commits..
+	 */
+	private static final int SMALL_BATCH_SIZE = 5;
+
+	/**
+	 * Acceptable number of commits to chain together using parent pointers.
+	 * <p>
+	 * When staging many unique commits the {@link StageBuilder} batches
+	 * together unrelated commits as parents of a temporary commit. After the
+	 * proposal completes the temporary commit is discarded and can be garbage
+	 * collected by all replicas.
+	 */
+	private static final int TEMP_PARENT_BATCH_SIZE = 128;
+
+	private static final byte[] PEEL = { ' ', '^' };
+
+	private final String txnStage;
+	private final String txnId;
+
+	/**
+	 * Construct a stage builder for a transaction.
+	 *
+	 * @param txnStageNamespace
+	 *            namespace for transaction references to build
+	 *            {@code "txnStageNamespace/txnId.n"} style names.
+	 * @param txnId
+	 *            identifier used to name temporary staging refs.
+	 */
+	public StageBuilder(String txnStageNamespace, ObjectId txnId) {
+		this.txnStage = txnStageNamespace;
+		this.txnId = txnId.name();
+	}
+
+	/**
+	 * Compare two RefTrees and return commands to stage new objects.
+	 * <p>
+	 * This method ignores the lineage between the two RefTrees and does a
+	 * straight diff on the two trees. New objects will be staged. The diff
+	 * strategy is useful to catch-up a lagging replica, without sending every
+	 * intermediate step. This may mean the replica does not have the same
+	 * object set as other replicas if there are rewinds or branch deletes.
+	 *
+	 * @param git
+	 *            source repository to read {@code oldTree} and {@code newTree}
+	 *            from.
+	 * @param oldTree
+	 *            accepted RefTree on the replica ({@code refs/txn/accepted}).
+	 *            Use {@link ObjectId#zeroId()} if the remote does not have any
+	 *            ref tree, e.g. a new replica catching up.
+	 * @param newTree
+	 *            RefTree being sent to the replica. The trees will be compared.
+	 * @return list of commands to create {@code "refs/txn/stage/..."}
+	 *         references on replicas anchoring new objects into the repository
+	 *         while a transaction gains consensus.
+	 * @throws IOException
+	 *             {@code git} cannot be accessed to compare {@code oldTree} and
+	 *             {@code newTree} to build the object set.
+	 */
+	public List<ReceiveCommand> makeStageList(Repository git, ObjectId oldTree,
+			ObjectId newTree) throws IOException {
+		try (RevWalk rw = new RevWalk(git);
+				TreeWalk tw = new TreeWalk(rw.getObjectReader());
+				ObjectInserter ins = git.newObjectInserter()) {
+			if (AnyObjectId.equals(oldTree, ObjectId.zeroId())) {
+				tw.addTree(new EmptyTreeIterator());
+			} else {
+				tw.addTree(rw.parseTree(oldTree));
+			}
+			tw.addTree(rw.parseTree(newTree));
+			tw.setFilter(TreeFilter.ANY_DIFF);
+			tw.setRecursive(true);
+
+			Set<ObjectId> newObjs = new HashSet<>();
+			while (tw.next()) {
+				if (tw.getRawMode(1) == TYPE_GITLINK
+						&& !tw.isPathSuffix(PEEL, 2)) {
+					newObjs.add(tw.getObjectId(1));
+				}
+			}
+
+			List<ReceiveCommand> cmds = makeStageList(newObjs, git, ins);
+			ins.flush();
+			return cmds;
+		}
+	}
+
+	/**
+	 * Construct a set of commands to stage objects on a replica.
+	 *
+	 * @param newObjs
+	 *            objects to send to a replica.
+	 * @param git
+	 *            local repository to read source objects from. Required to
+	 *            perform minification of {@code newObjs}.
+	 * @param inserter
+	 *            inserter to write temporary commit objects during minification
+	 *            if many new branches are created by {@code newObjs}.
+	 * @return list of commands to create {@code "refs/txn/stage/..."}
+	 *         references on replicas anchoring {@code newObjs} into the
+	 *         repository while a transaction gains consensus.
+	 * @throws IOException
+	 *             {@code git} cannot be accessed to perform minification of
+	 *             {@code newObjs}.
+	 */
+	public List<ReceiveCommand> makeStageList(Set<ObjectId> newObjs,
+			@Nullable Repository git, @Nullable ObjectInserter inserter)
+					throws IOException {
+		if (git == null || newObjs.size() <= SMALL_BATCH_SIZE) {
+			// Without a source repository can only construct unique set.
+			List<ReceiveCommand> cmds = new ArrayList<>(newObjs.size());
+			for (ObjectId id : newObjs) {
+				stage(cmds, id);
+			}
+			return cmds;
+		}
+
+		List<ReceiveCommand> cmds = new ArrayList<>();
+		List<RevCommit> commits = new ArrayList<>();
+		reduceObjects(cmds, commits, git, newObjs);
+
+		if (inserter == null || commits.size() <= 1
+				|| (cmds.size() + commits.size()) <= SMALL_BATCH_SIZE) {
+			// Without an inserter to aggregate commits, or for a small set of
+			// commits just send one stage ref per commit.
+			for (RevCommit c : commits) {
+				stage(cmds, c.copy());
+			}
+			return cmds;
+		}
+
+		// 'commits' is sorted most recent to least recent commit.
+		// Group batches of commits and build a chain.
+		// TODO(sop) Cluster by restricted graphs to support filtering.
+		ObjectId tip = null;
+		for (int end = commits.size(); end > 0;) {
+			int start = Math.max(0, end - TEMP_PARENT_BATCH_SIZE);
+			List<RevCommit> batch = commits.subList(start, end);
+			List<ObjectId> parents = new ArrayList<>(1 + batch.size());
+			if (tip != null) {
+				parents.add(tip);
+			}
+			parents.addAll(batch);
+
+			CommitBuilder b = new CommitBuilder();
+			b.setTreeId(batch.get(0).getTree());
+			b.setParentIds(parents);
+			b.setAuthor(tmpAuthor(batch));
+			b.setCommitter(b.getAuthor());
+			tip = inserter.insert(b);
+			end = start;
+		}
+		stage(cmds, tip);
+		return cmds;
+	}
+
+	private static PersonIdent tmpAuthor(List<RevCommit> commits) {
+		// Construct a predictable author using most recent commit time.
+		int t = 0;
+		for (int i = 0; i < commits.size();) {
+			t = Math.max(t, commits.get(i).getCommitTime());
+		}
+		String name = "Ketch Stage"; //$NON-NLS-1$
+		String email = "tmp@tmp"; //$NON-NLS-1$
+		return new PersonIdent(name, email, t * 1000L, 0);
+	}
+
+	private void reduceObjects(List<ReceiveCommand> cmds,
+			List<RevCommit> commits, Repository git,
+			Set<ObjectId> newObjs) throws IOException {
+		try (RevWalk rw = new RevWalk(git)) {
+			rw.setRetainBody(false);
+
+			for (ObjectId id : newObjs) {
+				RevObject obj = rw.parseAny(id);
+				if (obj instanceof RevCommit) {
+					rw.markStart((RevCommit) obj);
+				} else {
+					stage(cmds, id);
+				}
+			}
+
+			for (RevCommit c; (c = rw.next()) != null;) {
+				commits.add(c);
+				rw.markUninteresting(c);
+			}
+		}
+	}
+
+	private void stage(List<ReceiveCommand> cmds, ObjectId id) {
+		int estLen = txnStage.length() + txnId.length() + 5;
+		StringBuilder n = new StringBuilder(estLen);
+		n.append(txnStage).append(txnId).append('.');
+		n.append(Integer.toHexString(cmds.size()));
+		cmds.add(new ReceiveCommand(ObjectId.zeroId(), id, n.toString()));
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/package-info.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/package-info.java
new file mode 100644
index 0000000..dfe0375
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/ketch/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Distributed consensus system built on Git.
+ */
+package org.eclipse.jgit.internal.ketch;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollector.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollector.java
index faf27e3..33be3b1 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollector.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollector.java
@@ -44,18 +44,17 @@
 package org.eclipse.jgit.internal.storage.dfs;
 
 import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.GC;
+import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.GC_TXN;
 import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.UNREACHABLE_GARBAGE;
 import static org.eclipse.jgit.internal.storage.pack.PackExt.BITMAP_INDEX;
 import static org.eclipse.jgit.internal.storage.pack.PackExt.INDEX;
 import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK;
-import static org.eclipse.jgit.lib.RefDatabase.ALL;
 
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Collections;
+import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 
 import org.eclipse.jgit.internal.JGitText;
@@ -63,13 +62,15 @@
 import org.eclipse.jgit.internal.storage.file.PackIndex;
 import org.eclipse.jgit.internal.storage.pack.PackExt;
 import org.eclipse.jgit.internal.storage.pack.PackWriter;
+import org.eclipse.jgit.internal.storage.reftree.RefTreeNames;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectIdOwnerMap;
+import org.eclipse.jgit.lib.ObjectIdSet;
 import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.storage.pack.PackConfig;
 import org.eclipse.jgit.storage.pack.PackStatistics;
@@ -78,16 +79,14 @@
 /** Repack and garbage collect a repository. */
 public class DfsGarbageCollector {
 	private final DfsRepository repo;
-
-	private final DfsRefDatabase refdb;
-
+	private final RefDatabase refdb;
 	private final DfsObjDatabase objdb;
 
 	private final List<DfsPackDescription> newPackDesc;
 
 	private final List<PackStatistics> newPackStats;
 
-	private final List<PackWriter.ObjectIdSet> newPackObj;
+	private final List<ObjectIdSet> newPackObj;
 
 	private DfsReader ctx;
 
@@ -95,14 +94,11 @@ public class DfsGarbageCollector {
 
 	private long coalesceGarbageLimit = 50 << 20;
 
-	private Map<String, Ref> refsBefore;
-
 	private List<DfsPackFile> packsBefore;
 
 	private Set<ObjectId> allHeads;
-
 	private Set<ObjectId> nonHeads;
-
+	private Set<ObjectId> txnHeads;
 	private Set<ObjectId> tagTargets;
 
 	/**
@@ -117,7 +113,7 @@ public DfsGarbageCollector(DfsRepository repository) {
 		objdb = repo.getObjectDatabase();
 		newPackDesc = new ArrayList<DfsPackDescription>(4);
 		newPackStats = new ArrayList<PackStatistics>(4);
-		newPackObj = new ArrayList<PackWriter.ObjectIdSet>(4);
+		newPackObj = new ArrayList<ObjectIdSet>(4);
 
 		packConfig = new PackConfig(repo);
 		packConfig.setIndexVersion(2);
@@ -195,22 +191,25 @@ public boolean pack(ProgressMonitor pm) throws IOException {
 
 		ctx = (DfsReader) objdb.newReader();
 		try {
-			refdb.clearCache();
+			refdb.refresh();
 			objdb.clearCache();
 
-			refsBefore = refdb.getRefs(ALL);
+			Collection<Ref> refsBefore = getAllRefs();
 			packsBefore = packsToRebuild();
 			if (packsBefore.isEmpty())
 				return true;
 
 			allHeads = new HashSet<ObjectId>();
 			nonHeads = new HashSet<ObjectId>();
+			txnHeads = new HashSet<ObjectId>();
 			tagTargets = new HashSet<ObjectId>();
-			for (Ref ref : refsBefore.values()) {
+			for (Ref ref : refsBefore) {
 				if (ref.isSymbolic() || ref.getObjectId() == null)
 					continue;
 				if (isHead(ref))
 					allHeads.add(ref.getObjectId());
+				else if (RefTreeNames.isRefTree(refdb, ref.getName()))
+					txnHeads.add(ref.getObjectId());
 				else
 					nonHeads.add(ref.getObjectId());
 				if (ref.getPeeledObjectId() != null)
@@ -222,6 +221,7 @@ public boolean pack(ProgressMonitor pm) throws IOException {
 			try {
 				packHeads(pm);
 				packRest(pm);
+				packRefTreeGraph(pm);
 				packGarbage(pm);
 				objdb.commitPack(newPackDesc, toPrune());
 				rollback = false;
@@ -235,6 +235,18 @@ public boolean pack(ProgressMonitor pm) throws IOException {
 		}
 	}
 
+	private Collection<Ref> getAllRefs() throws IOException {
+		Collection<Ref> refs = refdb.getRefs(RefDatabase.ALL).values();
+		List<Ref> addl = refdb.getAdditionalRefs();
+		if (!addl.isEmpty()) {
+			List<Ref> all = new ArrayList<>(refs.size() + addl.size());
+			all.addAll(refs);
+			all.addAll(addl);
+			return all;
+		}
+		return refs;
+	}
+
 	private List<DfsPackFile> packsToRebuild() throws IOException {
 		DfsPackFile[] packs = objdb.getPacks();
 		List<DfsPackFile> out = new ArrayList<DfsPackFile>(packs.length);
@@ -277,18 +289,17 @@ private void packHeads(ProgressMonitor pm) throws IOException {
 
 		try (PackWriter pw = newPackWriter()) {
 			pw.setTagTargets(tagTargets);
-			pw.preparePack(pm, allHeads, Collections.<ObjectId> emptySet());
+			pw.preparePack(pm, allHeads, PackWriter.NONE);
 			if (0 < pw.getObjectCount())
 				writePack(GC, pw, pm);
 		}
 	}
-
 	private void packRest(ProgressMonitor pm) throws IOException {
 		if (nonHeads.isEmpty())
 			return;
 
 		try (PackWriter pw = newPackWriter()) {
-			for (PackWriter.ObjectIdSet packedObjs : newPackObj)
+			for (ObjectIdSet packedObjs : newPackObj)
 				pw.excludeObjects(packedObjs);
 			pw.preparePack(pm, nonHeads, allHeads);
 			if (0 < pw.getObjectCount())
@@ -296,6 +307,19 @@ private void packRest(ProgressMonitor pm) throws IOException {
 		}
 	}
 
+	private void packRefTreeGraph(ProgressMonitor pm) throws IOException {
+		if (txnHeads.isEmpty())
+			return;
+
+		try (PackWriter pw = newPackWriter()) {
+			for (ObjectIdSet packedObjs : newPackObj)
+				pw.excludeObjects(packedObjs);
+			pw.preparePack(pm, txnHeads, PackWriter.NONE);
+			if (0 < pw.getObjectCount())
+				writePack(GC_TXN, pw, pm);
+		}
+	}
+
 	private void packGarbage(ProgressMonitor pm) throws IOException {
 		// TODO(sop) This is ugly. The garbage pack needs to be deleted.
 		PackConfig cfg = new PackConfig(packConfig);
@@ -328,7 +352,7 @@ private void packGarbage(ProgressMonitor pm) throws IOException {
 	}
 
 	private boolean anyPackHas(AnyObjectId id) {
-		for (PackWriter.ObjectIdSet packedObjs : newPackObj)
+		for (ObjectIdSet packedObjs : newPackObj)
 			if (packedObjs.contains(id))
 				return true;
 		return false;
@@ -389,17 +413,10 @@ private DfsPackDescription writePack(PackSource source, PackWriter pw,
 			}
 		}
 
-		final ObjectIdOwnerMap<ObjectIdOwnerMap.Entry> packedObjs = pw
-				.getObjectSet();
-		newPackObj.add(new PackWriter.ObjectIdSet() {
-			public boolean contains(AnyObjectId objectId) {
-				return packedObjs.contains(objectId);
-			}
-		});
-
 		PackStatistics stats = pw.getStatistics();
 		pack.setPackStats(stats);
 		newPackStats.add(stats);
+		newPackObj.add(pw.getObjectSet());
 
 		DfsBlockCache.getInstance().getOrCreate(pack, null);
 		return pack;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjDatabase.java
index 5f491ff..3641560 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjDatabase.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsObjDatabase.java
@@ -91,6 +91,13 @@ public static enum PackSource {
 		GC(1),
 
 		/**
+		 * RefTreeGraph pack was created by Git garbage collection.
+		 *
+		 * @see DfsGarbageCollector
+		 */
+		GC_TXN(1),
+
+		/**
 		 * The pack was created by compacting multiple packs together.
 		 * <p>
 		 * Packs created by compacting multiple packs together aren't nearly as
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackCompactor.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackCompactor.java
index 7073763..11aef7f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackCompactor.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackCompactor.java
@@ -62,6 +62,7 @@
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdSet;
 import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.revwalk.RevFlag;
 import org.eclipse.jgit.revwalk.RevObject;
@@ -91,7 +92,7 @@ public class DfsPackCompactor {
 
 	private final List<DfsPackFile> srcPacks;
 
-	private final List<PackWriter.ObjectIdSet> exclude;
+	private final List<ObjectIdSet> exclude;
 
 	private final List<DfsPackDescription> newPacks;
 
@@ -113,7 +114,7 @@ public DfsPackCompactor(DfsRepository repository) {
 		repo = repository;
 		autoAddSize = 5 * 1024 * 1024; // 5 MiB
 		srcPacks = new ArrayList<DfsPackFile>();
-		exclude = new ArrayList<PackWriter.ObjectIdSet>(4);
+		exclude = new ArrayList<ObjectIdSet>(4);
 		newPacks = new ArrayList<DfsPackDescription>(1);
 		newStats = new ArrayList<PackStatistics>(1);
 	}
@@ -164,7 +165,7 @@ public DfsPackCompactor autoAdd() throws IOException {
 	 *            objects to not include.
 	 * @return {@code this}.
 	 */
-	public DfsPackCompactor exclude(PackWriter.ObjectIdSet set) {
+	public DfsPackCompactor exclude(ObjectIdSet set) {
 		exclude.add(set);
 		return this;
 	}
@@ -183,11 +184,7 @@ public DfsPackCompactor exclude(DfsPackFile pack) throws IOException {
 		try (DfsReader ctx = (DfsReader) repo.newObjectReader()) {
 			idx = pack.getPackIndex(ctx);
 		}
-		return exclude(new PackWriter.ObjectIdSet() {
-			public boolean contains(AnyObjectId id) {
-				return idx.hasObject(id);
-			}
-		});
+		return exclude(idx);
 	}
 
 	/**
@@ -343,7 +340,7 @@ private List<ObjectIdWithOffset> toInclude(DfsPackFile src, DfsReader ctx)
 			RevObject obj = rw.lookupOrNull(id);
 			if (obj != null && (obj.has(added) || obj.has(isBase)))
 				continue;
-			for (PackWriter.ObjectIdSet e : exclude)
+			for (ObjectIdSet e : exclude)
 				if (e.contains(id))
 					continue SCAN;
 			want.add(new ObjectIdWithOffset(id, ent.getOffset()));
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRefDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRefDatabase.java
index a1035a1..e5469f6 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRefDatabase.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRefDatabase.java
@@ -262,6 +262,11 @@ public void create() {
 	}
 
 	@Override
+	public void refresh() {
+		clearCache();
+	}
+
+	@Override
 	public void close() {
 		clearCache();
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRepository.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRepository.java
index 0d5fd0f..ef88450 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRepository.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRepository.java
@@ -79,9 +79,6 @@ protected DfsRepository(DfsRepositoryBuilder builder) {
 	@Override
 	public abstract DfsObjDatabase getObjectDatabase();
 
-	@Override
-	public abstract DfsRefDatabase getRefDatabase();
-
 	/** @return a description of this repository. */
 	public DfsRepositoryDescription getDescription() {
 		return description;
@@ -95,7 +92,10 @@ public DfsRepositoryDescription getDescription() {
 	 *             the repository cannot be checked.
 	 */
 	public boolean exists() throws IOException {
-		return getRefDatabase().exists();
+		if (getRefDatabase() instanceof DfsRefDatabase) {
+			return ((DfsRefDatabase) getRefDatabase()).exists();
+		}
+		return true;
 	}
 
 	@Override
@@ -117,7 +117,7 @@ public StoredConfig getConfig() {
 
 	@Override
 	public void scanForRepoChanges() throws IOException {
-		getRefDatabase().clearCache();
+		getRefDatabase().refresh();
 		getObjectDatabase().clearCache();
 	}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/InMemoryRepository.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/InMemoryRepository.java
index 1c664b4..5e246b4 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/InMemoryRepository.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/InMemoryRepository.java
@@ -16,7 +16,6 @@
 import java.util.concurrent.locks.ReadWriteLock;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
 
-import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.internal.storage.pack.PackExt;
 import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.ObjectId;
@@ -24,6 +23,7 @@
 import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Ref.Storage;
+import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.lib.SymbolicRef;
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevTag;
@@ -54,7 +54,7 @@ public InMemoryRepository build() throws IOException {
 	static final AtomicInteger packId = new AtomicInteger();
 
 	private final DfsObjDatabase objdb;
-	private final DfsRefDatabase refdb;
+	private final RefDatabase refdb;
 	private boolean performsAtomicTransactions = true;
 
 	/**
@@ -80,7 +80,7 @@ public DfsObjDatabase getObjectDatabase() {
 	}
 
 	@Override
-	public DfsRefDatabase getRefDatabase() {
+	public RefDatabase getRefDatabase() {
 		return refdb;
 	}
 
@@ -310,6 +310,11 @@ private void batch(RevWalk walk, List<ReceiveCommand> cmds) {
 			Map<ObjectId, ObjectId> peeled = new HashMap<>();
 			try (RevWalk rw = new RevWalk(getRepository())) {
 				for (ReceiveCommand c : cmds) {
+					if (c.getResult() != ReceiveCommand.Result.NOT_ATTEMPTED) {
+						ReceiveCommand.abort(cmds);
+						return;
+					}
+
 					if (!ObjectId.zeroId().equals(c.getNewId())) {
 						try {
 							RevObject o = rw.parseAny(c.getNewId());
@@ -318,7 +323,7 @@ private void batch(RevWalk walk, List<ReceiveCommand> cmds) {
 							}
 						} catch (IOException e) {
 							c.setResult(ReceiveCommand.Result.REJECTED_MISSING_OBJECT);
-							reject(cmds);
+							ReceiveCommand.abort(cmds);
 							return;
 						}
 					}
@@ -331,14 +336,17 @@ private void batch(RevWalk walk, List<ReceiveCommand> cmds) {
 				if (r == null) {
 					if (c.getType() != ReceiveCommand.Type.CREATE) {
 						c.setResult(ReceiveCommand.Result.LOCK_FAILURE);
-						reject(cmds);
+						ReceiveCommand.abort(cmds);
 						return;
 					}
-				} else if (r.isSymbolic() || r.getObjectId() == null
-						|| !r.getObjectId().equals(c.getOldId())) {
-					c.setResult(ReceiveCommand.Result.LOCK_FAILURE);
-					reject(cmds);
-					return;
+				} else {
+					ObjectId objectId = r.getObjectId();
+					if (r.isSymbolic() || objectId == null
+							|| !objectId.equals(c.getOldId())) {
+						c.setResult(ReceiveCommand.Result.LOCK_FAILURE);
+						ReceiveCommand.abort(cmds);
+						return;
+					}
 				}
 			}
 
@@ -365,15 +373,6 @@ private void batch(RevWalk walk, List<ReceiveCommand> cmds) {
 			clearCache();
 		}
 
-		private void reject(List<ReceiveCommand> cmds) {
-			for (ReceiveCommand c : cmds) {
-				if (c.getResult() == ReceiveCommand.Result.NOT_ATTEMPTED) {
-					c.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON,
-							JGitText.get().transactionAborted);
-				}
-			}
-		}
-
 		@Override
 		protected boolean compareAndPut(Ref oldRef, Ref newRef)
 				throws IOException {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java
index 490cbca..62d2d69 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/FileRepository.java
@@ -63,6 +63,7 @@
 import org.eclipse.jgit.events.ConfigChangedListener;
 import org.eclipse.jgit.events.IndexChangedEvent;
 import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.storage.reftree.RefTreeDatabase;
 import org.eclipse.jgit.internal.storage.file.ObjectDirectory.AlternateHandle;
 import org.eclipse.jgit.internal.storage.file.ObjectDirectory.AlternateRepository;
 import org.eclipse.jgit.lib.BaseRepositoryBuilder;
@@ -201,7 +202,22 @@ public void onConfigChanged(ConfigChangedEvent event) {
 			}
 		});
 
-		refs = new RefDirectory(this);
+		final long repositoryFormatVersion = getConfig().getLong(
+				ConfigConstants.CONFIG_CORE_SECTION, null,
+				ConfigConstants.CONFIG_KEY_REPO_FORMAT_VERSION, 0);
+
+		String reftype = repoConfig.getString(
+				"extensions", null, "refsStorage"); //$NON-NLS-1$ //$NON-NLS-2$
+		if (repositoryFormatVersion >= 1 && reftype != null) {
+			if (StringUtils.equalsIgnoreCase(reftype, "reftree")) { //$NON-NLS-1$
+				refs = new RefTreeDatabase(this, new RefDirectory(this));
+			} else {
+				throw new IOException(JGitText.get().unknownRepositoryFormat);
+			}
+		} else {
+			refs = new RefDirectory(this);
+		}
+
 		objectDatabase = new ObjectDirectory(repoConfig, //
 				options.getObjectDirectory(), //
 				options.getAlternateObjectDirectories(), //
@@ -209,10 +225,7 @@ public void onConfigChanged(ConfigChangedEvent event) {
 				new File(getDirectory(), Constants.SHALLOW));
 
 		if (objectDatabase.exists()) {
-			final long repositoryFormatVersion = getConfig().getLong(
-					ConfigConstants.CONFIG_CORE_SECTION, null,
-					ConfigConstants.CONFIG_KEY_REPO_FORMAT_VERSION, 0);
-			if (repositoryFormatVersion > 0)
+			if (repositoryFormatVersion > 1)
 				throw new IOException(MessageFormat.format(
 						JGitText.get().unknownRepositoryFormat2,
 						Long.valueOf(repositoryFormatVersion)));
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java
index 4c40538..49f9335 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java
@@ -45,7 +45,6 @@
 
 import static org.eclipse.jgit.internal.storage.pack.PackExt.BITMAP_INDEX;
 import static org.eclipse.jgit.internal.storage.pack.PackExt.INDEX;
-import static org.eclipse.jgit.lib.RefDatabase.ALL;
 
 import java.io.File;
 import java.io.FileOutputStream;
@@ -53,6 +52,7 @@
 import java.io.OutputStream;
 import java.nio.channels.Channels;
 import java.nio.channels.FileChannel;
+import java.nio.file.StandardCopyOption;
 import java.text.MessageFormat;
 import java.text.ParseException;
 import java.util.ArrayList;
@@ -62,14 +62,14 @@
 import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
-import java.util.Map.Entry;
+import java.util.Objects;
 import java.util.Set;
 import java.util.TreeMap;
 
+import org.eclipse.jgit.annotations.NonNull;
 import org.eclipse.jgit.dircache.DirCacheIterator;
 import org.eclipse.jgit.errors.CorruptObjectException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -78,13 +78,13 @@
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.internal.storage.pack.PackExt;
 import org.eclipse.jgit.internal.storage.pack.PackWriter;
-import org.eclipse.jgit.internal.storage.pack.PackWriter.ObjectIdSet;
-import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.internal.storage.reftree.RefTreeNames;
 import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdSet;
 import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Ref.Storage;
@@ -127,7 +127,7 @@ public class GC {
 	 * difference between the current refs and the refs which existed during
 	 * last {@link #repack()}.
 	 */
-	private Map<String, Ref> lastPackedRefs;
+	private Collection<Ref> lastPackedRefs;
 
 	/**
 	 * Holds the starting time of the last repack() execution. This is needed in
@@ -361,17 +361,20 @@ public void prune(Set<ObjectId> objectsToKeep) throws IOException,
 		// during last repack(). Only those refs will survive which have been
 		// added or modified since the last repack. Only these can save existing
 		// loose refs from being pruned.
-		Map<String, Ref> newRefs;
+		Collection<Ref> newRefs;
 		if (lastPackedRefs == null || lastPackedRefs.isEmpty())
 			newRefs = getAllRefs();
 		else {
-			newRefs = new HashMap<String, Ref>();
-			for (Iterator<Map.Entry<String, Ref>> i = getAllRefs().entrySet()
-					.iterator(); i.hasNext();) {
-				Entry<String, Ref> newEntry = i.next();
-				Ref old = lastPackedRefs.get(newEntry.getKey());
-				if (!equals(newEntry.getValue(), old))
-					newRefs.put(newEntry.getKey(), newEntry.getValue());
+			Map<String, Ref> last = new HashMap<>();
+			for (Ref r : lastPackedRefs) {
+				last.put(r.getName(), r);
+			}
+			newRefs = new ArrayList<>();
+			for (Ref r : getAllRefs()) {
+				Ref old = last.get(r.getName());
+				if (!equals(r, old)) {
+					newRefs.add(r);
+				}
 			}
 		}
 
@@ -383,10 +386,10 @@ public void prune(Set<ObjectId> objectsToKeep) throws IOException,
 			// leave this method.
 			ObjectWalk w = new ObjectWalk(repo);
 			try {
-				for (Ref cr : newRefs.values())
+				for (Ref cr : newRefs)
 					w.markStart(w.parseAny(cr.getObjectId()));
 				if (lastPackedRefs != null)
-					for (Ref lpr : lastPackedRefs.values())
+					for (Ref lpr : lastPackedRefs)
 						w.markUninteresting(w.parseAny(lpr.getObjectId()));
 				removeReferenced(deletionCandidates, w);
 			} finally {
@@ -404,11 +407,11 @@ public void prune(Set<ObjectId> objectsToKeep) throws IOException,
 		// additional reflog entries not handled during last repack()
 		ObjectWalk w = new ObjectWalk(repo);
 		try {
-			for (Ref ar : getAllRefs().values())
+			for (Ref ar : getAllRefs())
 				for (ObjectId id : listRefLogObjects(ar, lastRepackTime))
 					w.markStart(w.parseAny(id));
 			if (lastPackedRefs != null)
-				for (Ref lpr : lastPackedRefs.values())
+				for (Ref lpr : lastPackedRefs)
 					w.markUninteresting(w.parseAny(lpr.getObjectId()));
 			removeReferenced(deletionCandidates, w);
 		} finally {
@@ -483,9 +486,10 @@ private static boolean equals(Ref r1, Ref r2) {
 				return false;
 			return r1.getTarget().getName().equals(r2.getTarget().getName());
 		} else {
-			if (r2.isSymbolic())
+			if (r2.isSymbolic()) {
 				return false;
-			return r1.getObjectId().equals(r2.getObjectId());
+			}
+			return Objects.equals(r1.getObjectId(), r2.getObjectId());
 		}
 	}
 
@@ -528,19 +532,23 @@ public Collection<PackFile> repack() throws IOException {
 		Collection<PackFile> toBeDeleted = repo.getObjectDatabase().getPacks();
 
 		long time = System.currentTimeMillis();
-		Map<String, Ref> refsBefore = getAllRefs();
+		Collection<Ref> refsBefore = getAllRefs();
 
 		Set<ObjectId> allHeads = new HashSet<ObjectId>();
 		Set<ObjectId> nonHeads = new HashSet<ObjectId>();
+		Set<ObjectId> txnHeads = new HashSet<ObjectId>();
 		Set<ObjectId> tagTargets = new HashSet<ObjectId>();
 		Set<ObjectId> indexObjects = listNonHEADIndexObjects();
+		RefDatabase refdb = repo.getRefDatabase();
 
-		for (Ref ref : refsBefore.values()) {
+		for (Ref ref : refsBefore) {
 			nonHeads.addAll(listRefLogObjects(ref, 0));
 			if (ref.isSymbolic() || ref.getObjectId() == null)
 				continue;
 			if (ref.getName().startsWith(Constants.R_HEADS))
 				allHeads.add(ref.getObjectId());
+			else if (RefTreeNames.isRefTree(refdb, ref.getName()))
+				txnHeads.add(ref.getObjectId());
 			else
 				nonHeads.add(ref.getObjectId());
 			if (ref.getPeeledObjectId() != null)
@@ -550,7 +558,7 @@ public Collection<PackFile> repack() throws IOException {
 		List<ObjectIdSet> excluded = new LinkedList<ObjectIdSet>();
 		for (final PackFile f : repo.getObjectDatabase().getPacks())
 			if (f.shouldBeKept())
-				excluded.add(objectIdSet(f.getIndex()));
+				excluded.add(f.getIndex());
 
 		tagTargets.addAll(allHeads);
 		nonHeads.addAll(indexObjects);
@@ -562,7 +570,7 @@ public Collection<PackFile> repack() throws IOException {
 					tagTargets, excluded);
 			if (heads != null) {
 				ret.add(heads);
-				excluded.add(0, objectIdSet(heads.getIndex()));
+				excluded.add(0, heads.getIndex());
 			}
 		}
 		if (!nonHeads.isEmpty()) {
@@ -570,6 +578,11 @@ public Collection<PackFile> repack() throws IOException {
 			if (rest != null)
 				ret.add(rest);
 		}
+		if (!txnHeads.isEmpty()) {
+			PackFile txn = writePack(txnHeads, PackWriter.NONE, null, excluded);
+			if (txn != null)
+				ret.add(txn);
+		}
 		try {
 			deleteOldPacks(toBeDeleted, ret);
 		} catch (ParseException e) {
@@ -616,17 +629,23 @@ private Set<ObjectId> listRefLogObjects(Ref ref, long minTime) throws IOExceptio
 	}
 
 	/**
-	 * Returns a map of all refs and additional refs (e.g. FETCH_HEAD,
+	 * Returns a collection of all refs and additional refs (e.g. FETCH_HEAD,
 	 * MERGE_HEAD, ...)
 	 *
-	 * @return a map where names of refs point to ref objects
+	 * @return a collection of refs pointing to live objects.
 	 * @throws IOException
 	 */
-	private Map<String, Ref> getAllRefs() throws IOException {
-		Map<String, Ref> ret = repo.getRefDatabase().getRefs(ALL);
-		for (Ref ref : repo.getRefDatabase().getAdditionalRefs())
-			ret.put(ref.getName(), ref);
-		return ret;
+	private Collection<Ref> getAllRefs() throws IOException {
+		RefDatabase refdb = repo.getRefDatabase();
+		Collection<Ref> refs = refdb.getRefs(RefDatabase.ALL).values();
+		List<Ref> addl = refdb.getAdditionalRefs();
+		if (!addl.isEmpty()) {
+			List<Ref> all = new ArrayList<>(refs.size() + addl.size());
+			all.addAll(refs);
+			all.addAll(addl);
+			return all;
+		}
+		return refs;
 	}
 
 	/**
@@ -681,8 +700,8 @@ private Set<ObjectId> listNonHEADIndexObjects()
 		}
 	}
 
-	private PackFile writePack(Set<? extends ObjectId> want,
-			Set<? extends ObjectId> have, Set<ObjectId> tagTargets,
+	private PackFile writePack(@NonNull Set<? extends ObjectId> want,
+			@NonNull Set<? extends ObjectId> have, Set<ObjectId> tagTargets,
 			List<ObjectIdSet> excludeObjects) throws IOException {
 		File tmpPack = null;
 		Map<PackExt, File> tmpExts = new TreeMap<PackExt, File>(
@@ -788,39 +807,33 @@ public int compare(PackExt o1, PackExt o2) {
 						break;
 					}
 			tmpPack.setReadOnly();
-			boolean delete = true;
-			try {
-				FileUtils.rename(tmpPack, realPack);
-				delete = false;
-				for (Map.Entry<PackExt, File> tmpEntry : tmpExts.entrySet()) {
-					File tmpExt = tmpEntry.getValue();
-					tmpExt.setReadOnly();
 
-					File realExt = nameFor(
-							id, "." + tmpEntry.getKey().getExtension()); //$NON-NLS-1$
+			FileUtils.rename(tmpPack, realPack, StandardCopyOption.ATOMIC_MOVE);
+			for (Map.Entry<PackExt, File> tmpEntry : tmpExts.entrySet()) {
+				File tmpExt = tmpEntry.getValue();
+				tmpExt.setReadOnly();
+
+				File realExt = nameFor(id,
+						"." + tmpEntry.getKey().getExtension()); //$NON-NLS-1$
+				try {
+					FileUtils.rename(tmpExt, realExt,
+							StandardCopyOption.ATOMIC_MOVE);
+				} catch (IOException e) {
+					File newExt = new File(realExt.getParentFile(),
+							realExt.getName() + ".new"); //$NON-NLS-1$
 					try {
-						FileUtils.rename(tmpExt, realExt);
-					} catch (IOException e) {
-						File newExt = new File(realExt.getParentFile(),
-								realExt.getName() + ".new"); //$NON-NLS-1$
-						if (!tmpExt.renameTo(newExt))
-							newExt = tmpExt;
-						throw new IOException(MessageFormat.format(
-								JGitText.get().panicCantRenameIndexFile, newExt,
-								realExt));
+						FileUtils.rename(tmpExt, newExt,
+								StandardCopyOption.ATOMIC_MOVE);
+					} catch (IOException e2) {
+						newExt = tmpExt;
+						e = e2;
 					}
-				}
-
-			} finally {
-				if (delete) {
-					if (tmpPack.exists())
-						tmpPack.delete();
-					for (File tmpExt : tmpExts.values()) {
-						if (tmpExt.exists())
-							tmpExt.delete();
-					}
+					throw new IOException(MessageFormat.format(
+							JGitText.get().panicCantRenameIndexFile, newExt,
+							realExt), e);
 				}
 			}
+
 			return repo.getObjectDatabase().openPack(realPack);
 		} finally {
 			if (tmpPack != null && tmpPack.exists())
@@ -998,12 +1011,4 @@ public void setExpire(Date expire) {
 		this.expire = expire;
 		expireAgeMillis = -1;
 	}
-
-	private static ObjectIdSet objectIdSet(final PackIndex idx) {
-		return new ObjectIdSet() {
-			public boolean contains(AnyObjectId objectId) {
-				return idx.hasObject(objectId);
-			}
-		};
-	}
 }
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/internal/storage/file/LockFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java
index e23ca74..ce9677a 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java
@@ -54,6 +54,7 @@
 import java.nio.ByteBuffer;
 import java.nio.channels.Channels;
 import java.nio.channels.FileChannel;
+import java.nio.file.StandardCopyOption;
 import java.text.MessageFormat;
 
 import org.eclipse.jgit.errors.LockFailedException;
@@ -128,8 +129,6 @@ public boolean accept(File dir, String name) {
 
 	private FileSnapshot commitSnapshot;
 
-	private final FS fs;
-
 	/**
 	 * Create a new lock for any file.
 	 *
@@ -138,11 +137,24 @@ public boolean accept(File dir, String name) {
 	 * @param fs
 	 *            the file system abstraction which will be necessary to perform
 	 *            certain file system operations.
+	 * @deprecated use {@link LockFile#LockFile(File)} instead
 	 */
+	@Deprecated
 	public LockFile(final File f, final FS fs) {
 		ref = f;
 		lck = getLockFile(ref);
-		this.fs = fs;
+	}
+
+	/**
+	 * Create a new lock for any file.
+	 *
+	 * @param f
+	 *            the file that will be locked.
+	 * @since 4.2
+	 */
+	public LockFile(final File f) {
+		ref = f;
+		lck = getLockFile(ref);
 	}
 
 	/**
@@ -441,56 +453,14 @@ public boolean commit() {
 		}
 
 		saveStatInformation();
-		if (lck.renameTo(ref)) {
+		try {
+			FileUtils.rename(lck, ref, StandardCopyOption.ATOMIC_MOVE);
 			haveLck = false;
 			return true;
+		} catch (IOException e) {
+			unlock();
+			return false;
 		}
-		if (!ref.exists() || deleteRef()) {
-			if (renameLock()) {
-				haveLck = false;
-				return true;
-			}
-		}
-		unlock();
-		return false;
-	}
-
-	private boolean deleteRef() {
-		if (!fs.retryFailedLockFileCommit())
-			return ref.delete();
-
-		// File deletion fails on windows if another thread is
-		// concurrently reading the same file. So try a few times.
-		//
-		for (int attempts = 0; attempts < 10; attempts++) {
-			if (ref.delete())
-				return true;
-			try {
-				Thread.sleep(100);
-			} catch (InterruptedException e) {
-				return false;
-			}
-		}
-		return false;
-	}
-
-	private boolean renameLock() {
-		if (!fs.retryFailedLockFileCommit())
-			return lck.renameTo(ref);
-
-		// File renaming fails on windows if another thread is
-		// concurrently reading the same file. So try a few times.
-		//
-		for (int attempts = 0; attempts < 10; attempts++) {
-			if (lck.renameTo(ref))
-				return true;
-			try {
-				Thread.sleep(100);
-			} catch (InterruptedException e) {
-				return false;
-			}
-		}
-		return false;
 	}
 
 	private void saveStatInformation() {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java
index bd1d488..ea80528 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java
@@ -52,6 +52,9 @@
 import java.io.FileNotFoundException;
 import java.io.FileReader;
 import java.io.IOException;
+import java.nio.file.AtomicMoveNotSupportedException;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
 import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -608,10 +611,16 @@ InsertLooseObjectResult insertUnpackedObject(File tmp, ObjectId id,
 			FileUtils.delete(tmp, FileUtils.RETRY);
 			return InsertLooseObjectResult.EXISTS_LOOSE;
 		}
-		if (tmp.renameTo(dst)) {
+		try {
+			Files.move(tmp.toPath(), dst.toPath(),
+					StandardCopyOption.ATOMIC_MOVE);
 			dst.setReadOnly();
 			unpackedObjectCache.add(id);
 			return InsertLooseObjectResult.INSERTED;
+		} catch (AtomicMoveNotSupportedException e) {
+			LOG.error(e.getMessage(), e);
+		} catch (IOException e) {
+			// ignore
 		}
 
 		// Maybe the directory doesn't exist yet as the object
@@ -619,10 +628,16 @@ InsertLooseObjectResult insertUnpackedObject(File tmp, ObjectId id,
 		// try the rename first as the directory likely does exist.
 		//
 		FileUtils.mkdir(dst.getParentFile(), true);
-		if (tmp.renameTo(dst)) {
+		try {
+			Files.move(tmp.toPath(), dst.toPath(),
+					StandardCopyOption.ATOMIC_MOVE);
 			dst.setReadOnly();
 			unpackedObjectCache.add(id);
 			return InsertLooseObjectResult.INSERTED;
+		} catch (AtomicMoveNotSupportedException e) {
+			LOG.error(e.getMessage(), e);
+		} catch (IOException e) {
+			LOG.debug(e.getMessage(), e);
 		}
 
 		if (!createDuplicate && has(id)) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryPackParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryPackParser.java
index 1c076ee..2e6c245 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryPackParser.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectoryPackParser.java
@@ -50,6 +50,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.RandomAccessFile;
+import java.nio.file.StandardCopyOption;
 import java.security.MessageDigest;
 import java.text.MessageFormat;
 import java.util.Arrays;
@@ -476,20 +477,25 @@ private PackLock renameAndOpenPack(final String lockMessage)
 			}
 		}
 
-		if (!tmpPack.renameTo(finalPack)) {
+		try {
+			FileUtils.rename(tmpPack, finalPack,
+					StandardCopyOption.ATOMIC_MOVE);
+		} catch (IOException e) {
 			cleanupTemporaryFiles();
 			keep.unlock();
 			throw new IOException(MessageFormat.format(
-					JGitText.get().cannotMovePackTo, finalPack));
+					JGitText.get().cannotMovePackTo, finalPack), e);
 		}
 
-		if (!tmpIdx.renameTo(finalIdx)) {
+		try {
+			FileUtils.rename(tmpIdx, finalIdx, StandardCopyOption.ATOMIC_MOVE);
+		} catch (IOException e) {
 			cleanupTemporaryFiles();
 			keep.unlock();
 			if (!finalPack.delete())
 				finalPack.deleteOnExit();
 			throw new IOException(MessageFormat.format(
-					JGitText.get().cannotMoveIndexTo, finalIdx));
+					JGitText.get().cannotMoveIndexTo, finalIdx), e);
 		}
 
 		try {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndex.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndex.java
index 0040aea..f36bd4d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndex.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackIndex.java
@@ -60,6 +60,7 @@
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.MutableObjectId;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdSet;
 import org.eclipse.jgit.util.IO;
 import org.eclipse.jgit.util.NB;
 
@@ -72,7 +73,8 @@
  * by ObjectId.
  * </p>
  */
-public abstract class PackIndex implements Iterable<PackIndex.MutableEntry> {
+public abstract class PackIndex
+		implements Iterable<PackIndex.MutableEntry>, ObjectIdSet {
 	/**
 	 * Open an existing pack <code>.idx</code> file for reading.
 	 * <p>
@@ -166,6 +168,11 @@ public boolean hasObject(final AnyObjectId id) {
 		return findOffset(id) != -1;
 	}
 
+	@Override
+	public boolean contains(AnyObjectId id) {
+		return findOffset(id) != -1;
+	}
+
 	/**
 	 * Provide iterator that gives access to index entries. Note, that iterator
 	 * returns reference to mutable object, the same reference in each call -
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java
index 69f7e97..2c8e5f9 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectory.java
@@ -73,6 +73,7 @@
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 
+import org.eclipse.jgit.annotations.NonNull;
 import org.eclipse.jgit.errors.InvalidObjectIdException;
 import org.eclipse.jgit.errors.LockFailedException;
 import org.eclipse.jgit.errors.MissingObjectException;
@@ -715,16 +716,20 @@ public void pack(List<String> refs) throws IOException {
 	 */
 	private Ref peeledPackedRef(Ref f)
 			throws MissingObjectException, IOException {
-		if (f.getStorage().isPacked() && f.isPeeled())
+		if (f.getStorage().isPacked() && f.isPeeled()) {
 			return f;
-		if (!f.isPeeled())
+		}
+		if (!f.isPeeled()) {
 			f = peel(f);
-		if (f.getPeeledObjectId() != null)
+		}
+		ObjectId peeledObjectId = f.getPeeledObjectId();
+		if (peeledObjectId != null) {
 			return new ObjectIdRef.PeeledTag(PACKED, f.getName(),
-					f.getObjectId(), f.getPeeledObjectId());
-		else
+					f.getObjectId(), peeledObjectId);
+		} else {
 			return new ObjectIdRef.PeeledNonTag(PACKED, f.getName(),
 					f.getObjectId());
+		}
 	}
 
 	void log(final RefUpdate update, final String msg, final boolean deref)
@@ -985,7 +990,7 @@ LooseRef scanRef(LooseRef ref, String name) throws IOException {
 		try {
 			id = ObjectId.fromString(buf, 0);
 			if (ref != null && !ref.isSymbolic()
-					&& ref.getTarget().getObjectId().equals(id)) {
+					&& id.equals(ref.getTarget().getObjectId())) {
 				assert(currentSnapshot != null);
 				currentSnapshot.setClean(otherSnapshot);
 				return ref;
@@ -1103,8 +1108,8 @@ private final static class LoosePeeledTag extends ObjectIdRef.PeeledTag
 			implements LooseRef {
 		private final FileSnapshot snapShot;
 
-		LoosePeeledTag(FileSnapshot snapshot, String refName, ObjectId id,
-				ObjectId p) {
+		LoosePeeledTag(FileSnapshot snapshot, @NonNull String refName,
+				@NonNull ObjectId id, @NonNull ObjectId p) {
 			super(LOOSE, refName, id, p);
 			this.snapShot = snapshot;
 		}
@@ -1122,7 +1127,8 @@ private final static class LooseNonTag extends ObjectIdRef.PeeledNonTag
 			implements LooseRef {
 		private final FileSnapshot snapShot;
 
-		LooseNonTag(FileSnapshot snapshot, String refName, ObjectId id) {
+		LooseNonTag(FileSnapshot snapshot, @NonNull String refName,
+				@NonNull ObjectId id) {
 			super(LOOSE, refName, id);
 			this.snapShot = snapshot;
 		}
@@ -1140,7 +1146,8 @@ private final static class LooseUnpeeled extends ObjectIdRef.Unpeeled
 			implements LooseRef {
 		private FileSnapshot snapShot;
 
-		LooseUnpeeled(FileSnapshot snapShot, String refName, ObjectId id) {
+		LooseUnpeeled(FileSnapshot snapShot, @NonNull String refName,
+				@NonNull ObjectId id) {
 			super(LOOSE, refName, id);
 			this.snapShot = snapShot;
 		}
@@ -1149,13 +1156,24 @@ public FileSnapshot getSnapShot() {
 			return snapShot;
 		}
 
+		@NonNull
+		@Override
+		public ObjectId getObjectId() {
+			ObjectId id = super.getObjectId();
+			assert id != null; // checked in constructor
+			return id;
+		}
+
 		public LooseRef peel(ObjectIdRef newLeaf) {
-			if (newLeaf.getPeeledObjectId() != null)
+			ObjectId peeledObjectId = newLeaf.getPeeledObjectId();
+			ObjectId objectId = getObjectId();
+			if (peeledObjectId != null) {
 				return new LoosePeeledTag(snapShot, getName(),
-						getObjectId(), newLeaf.getPeeledObjectId());
-			else
+						objectId, peeledObjectId);
+			} else {
 				return new LooseNonTag(snapShot, getName(),
-						getObjectId());
+						objectId);
+			}
 		}
 	}
 
@@ -1163,7 +1181,8 @@ private final static class LooseSymbolicRef extends SymbolicRef implements
 			LooseRef {
 		private final FileSnapshot snapShot;
 
-		LooseSymbolicRef(FileSnapshot snapshot, String refName, Ref target) {
+		LooseSymbolicRef(FileSnapshot snapshot, @NonNull String refName,
+				@NonNull Ref target) {
 			super(refName, target);
 			this.snapShot = snapshot;
 		}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectoryRename.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectoryRename.java
index ba4a63d..4b803a5 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectoryRename.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectoryRename.java
@@ -46,6 +46,8 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.nio.file.AtomicMoveNotSupportedException;
+import java.nio.file.StandardCopyOption;
 
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
@@ -54,6 +56,8 @@
 import org.eclipse.jgit.lib.RefUpdate.Result;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.util.FileUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * Rename any reference stored by {@link RefDirectory}.
@@ -66,6 +70,9 @@
  * directory that happens to match the source name.
  */
 class RefDirectoryRename extends RefRename {
+	private static final Logger LOG = LoggerFactory
+			.getLogger(RefDirectoryRename.class);
+
 	private final RefDirectory refdb;
 
 	/**
@@ -201,13 +208,25 @@ private boolean renameLog(RefUpdate src, RefUpdate dst) {
 	}
 
 	private static boolean rename(File src, File dst) {
-		if (src.renameTo(dst))
+		try {
+			FileUtils.rename(src, dst, StandardCopyOption.ATOMIC_MOVE);
 			return true;
+		} catch (AtomicMoveNotSupportedException e) {
+			LOG.error(e.getMessage(), e);
+		} catch (IOException e) {
+			// ignore
+		}
 
 		File dir = dst.getParentFile();
 		if ((dir.exists() || !dir.mkdirs()) && !dir.isDirectory())
 			return false;
-		return src.renameTo(dst);
+		try {
+			FileUtils.rename(src, dst, StandardCopyOption.ATOMIC_MOVE);
+			return true;
+		} catch (IOException e) {
+			LOG.error(e.getMessage(), e);
+			return false;
+		}
 	}
 
 	private boolean linkHEAD(RefUpdate target) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriter.java
index 19b6b08..525f9ae 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriter.java
@@ -80,6 +80,7 @@
 import java.util.zip.Deflater;
 import java.util.zip.DeflaterOutputStream;
 
+import org.eclipse.jgit.annotations.NonNull;
 import org.eclipse.jgit.errors.CorruptObjectException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.LargeObjectException;
@@ -99,6 +100,7 @@
 import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectIdOwnerMap;
+import org.eclipse.jgit.lib.ObjectIdSet;
 import org.eclipse.jgit.lib.ObjectLoader;
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.ProgressMonitor;
@@ -161,17 +163,8 @@
 public class PackWriter implements AutoCloseable {
 	private static final int PACK_VERSION_GENERATED = 2;
 
-	/** A collection of object ids. */
-	public interface ObjectIdSet {
-		/**
-		 * Returns true if the objectId is contained within the collection.
-		 *
-		 * @param objectId
-		 *            the objectId to find
-		 * @return whether the collection contains the objectId.
-		 */
-		boolean contains(AnyObjectId objectId);
-	}
+	/** Empty set of objects for {@code preparePack()}. */
+	public static Set<ObjectId> NONE = Collections.emptySet();
 
 	private static final Map<WeakReference<PackWriter>, Boolean> instances =
 			new ConcurrentHashMap<WeakReference<PackWriter>, Boolean>();
@@ -681,7 +674,7 @@ public void excludeObjects(ObjectIdSet idx) {
 	 * @throws IOException
 	 *             when some I/O problem occur during reading objects.
 	 */
-	public void preparePack(final Iterator<RevObject> objectsSource)
+	public void preparePack(@NonNull Iterator<RevObject> objectsSource)
 			throws IOException {
 		while (objectsSource.hasNext()) {
 			addObject(objectsSource.next());
@@ -704,16 +697,18 @@ public void preparePack(final Iterator<RevObject> objectsSource)
 	 *            progress during object enumeration.
 	 * @param want
 	 *            collection of objects to be marked as interesting (start
-	 *            points of graph traversal).
+	 *            points of graph traversal). Must not be {@code null}.
 	 * @param have
 	 *            collection of objects to be marked as uninteresting (end
-	 *            points of graph traversal).
+	 *            points of graph traversal). Pass {@link #NONE} if all objects
+	 *            reachable from {@code want} are desired, such as when serving
+	 *            a clone.
 	 * @throws IOException
 	 *             when some I/O problem occur during reading objects.
 	 */
 	public void preparePack(ProgressMonitor countingMonitor,
-			Set<? extends ObjectId> want,
-			Set<? extends ObjectId> have) throws IOException {
+			@NonNull Set<? extends ObjectId> want,
+			@NonNull Set<? extends ObjectId> have) throws IOException {
 		ObjectWalk ow;
 		if (shallowPack)
 			ow = new DepthWalk.ObjectWalk(reader, depth);
@@ -740,17 +735,19 @@ public void preparePack(ProgressMonitor countingMonitor,
 	 *            ObjectWalk to perform enumeration.
 	 * @param interestingObjects
 	 *            collection of objects to be marked as interesting (start
-	 *            points of graph traversal).
+	 *            points of graph traversal). Must not be {@code null}.
 	 * @param uninterestingObjects
 	 *            collection of objects to be marked as uninteresting (end
-	 *            points of graph traversal).
+	 *            points of graph traversal). Pass {@link #NONE} if all objects
+	 *            reachable from {@code want} are desired, such as when serving
+	 *            a clone.
 	 * @throws IOException
 	 *             when some I/O problem occur during reading objects.
 	 */
 	public void preparePack(ProgressMonitor countingMonitor,
-			ObjectWalk walk,
-			final Set<? extends ObjectId> interestingObjects,
-			final Set<? extends ObjectId> uninterestingObjects)
+			@NonNull ObjectWalk walk,
+			@NonNull Set<? extends ObjectId> interestingObjects,
+			@NonNull Set<? extends ObjectId> uninterestingObjects)
 			throws IOException {
 		if (countingMonitor == null)
 			countingMonitor = NullProgressMonitor.INSTANCE;
@@ -1551,6 +1548,8 @@ private void writeDeltaObjectDeflate(PackOutputStream out,
 			if (zbuf != null) {
 				out.writeHeader(otp, otp.getCachedSize());
 				out.write(zbuf);
+				typeStats.cntDeltas++;
+				typeStats.deltaBytes += out.length() - otp.getOffset();
 				return;
 			}
 		}
@@ -1606,17 +1605,12 @@ private void writeChecksum(PackOutputStream out) throws IOException {
 		out.write(packcsum);
 	}
 
-	private void findObjectsToPack(final ProgressMonitor countingMonitor,
-			final ObjectWalk walker, final Set<? extends ObjectId> want,
-			Set<? extends ObjectId> have)
-			throws MissingObjectException, IOException,
-			IncorrectObjectTypeException {
+	private void findObjectsToPack(@NonNull ProgressMonitor countingMonitor,
+			@NonNull ObjectWalk walker, @NonNull Set<? extends ObjectId> want,
+			@NonNull Set<? extends ObjectId> have) throws IOException {
 		final long countingStart = System.currentTimeMillis();
 		beginPhase(PackingPhase.COUNTING, countingMonitor, ProgressMonitor.UNKNOWN);
 
-		if (have == null)
-			have = Collections.emptySet();
-
 		stats.interestingObjects = Collections.unmodifiableSet(new HashSet<ObjectId>(want));
 		stats.uninterestingObjects = Collections.unmodifiableSet(new HashSet<ObjectId>(have));
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/SymlinkTreeEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/AlwaysFailUpdate.java
similarity index 60%
copy from org.eclipse.jgit/src/org/eclipse/jgit/lib/SymlinkTreeEntry.java
copy to org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/AlwaysFailUpdate.java
index c7e41bc..12ef873 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/SymlinkTreeEntry.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/AlwaysFailUpdate.java
@@ -1,6 +1,5 @@
 /*
- * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org>
+ * Copyright (C) 2016, Google Inc.
  * and other copyright owners as documented in the project's IP log.
  *
  * This program and the accompanying materials are made available
@@ -42,44 +41,58 @@
  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-package org.eclipse.jgit.lib;
+package org.eclipse.jgit.internal.storage.reftree;
 
-/**
- * A tree entry representing a symbolic link.
- *
- * Note. Java cannot really handle these as file system objects.
- *
- * @deprecated To look up information about a single path, use
- * {@link org.eclipse.jgit.treewalk.TreeWalk#forPath(Repository, String, org.eclipse.jgit.revwalk.RevTree)}.
- * To lookup information about multiple paths at once, use a
- * {@link org.eclipse.jgit.treewalk.TreeWalk} and obtain the current entry's
- * information from its getter methods.
- */
-@Deprecated
-public class SymlinkTreeEntry extends TreeEntry {
+import java.io.IOException;
 
-	/**
-	 * Construct a {@link SymlinkTreeEntry} with the specified name and SHA-1 in
-	 * the specified parent
-	 *
-	 * @param parent
-	 * @param id
-	 * @param nameUTF8
-	 */
-	public SymlinkTreeEntry(final Tree parent, final ObjectId id,
-			final byte[] nameUTF8) {
-		super(parent, id, nameUTF8);
+import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+
+/** Update that always rejects with {@code LOCK_FAILURE}. */
+class AlwaysFailUpdate extends RefUpdate {
+	private final RefTreeDatabase refdb;
+
+	AlwaysFailUpdate(RefTreeDatabase refdb, String name) {
+		super(new ObjectIdRef.Unpeeled(Ref.Storage.NEW, name, null));
+		this.refdb = refdb;
+		setCheckConflicting(false);
 	}
 
-	public FileMode getMode() {
-		return FileMode.SYMLINK;
+	@Override
+	protected RefDatabase getRefDatabase() {
+		return refdb;
 	}
 
-	public String toString() {
-		final StringBuilder r = new StringBuilder();
-		r.append(ObjectId.toString(getId()));
-		r.append(" S "); //$NON-NLS-1$
-		r.append(getFullName());
-		return r.toString();
+	@Override
+	protected Repository getRepository() {
+		return refdb.getRepository();
+	}
+
+	@Override
+	protected boolean tryLock(boolean deref) throws IOException {
+		return false;
+	}
+
+	@Override
+	protected void unlock() {
+		// No locks are held here.
+	}
+
+	@Override
+	protected Result doUpdate(Result desiredResult) {
+		return Result.LOCK_FAILURE;
+	}
+
+	@Override
+	protected Result doDelete(Result desiredResult) {
+		return Result.LOCK_FAILURE;
+	}
+
+	@Override
+	protected Result doLink(String target) {
+		return Result.LOCK_FAILURE;
 	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/Command.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/Command.java
new file mode 100644
index 0000000..dd08375
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/Command.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2016, 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.reftree;
+
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+import static org.eclipse.jgit.lib.Constants.encode;
+import static org.eclipse.jgit.lib.FileMode.TYPE_GITLINK;
+import static org.eclipse.jgit.lib.FileMode.TYPE_SYMLINK;
+import static org.eclipse.jgit.lib.Ref.Storage.NETWORK;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
+
+import java.io.IOException;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.dircache.DirCacheEntry;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevTag;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.transport.ReceiveCommand.Result;
+
+/**
+ * Command to create, update or delete an entry inside a {@link RefTree}.
+ * <p>
+ * Unlike {@link ReceiveCommand} (which can only update a reference to an
+ * {@link ObjectId}), a RefTree Command can also create, modify or delete
+ * symbolic references to a target reference.
+ * <p>
+ * RefTree Commands may wrap a {@code ReceiveCommand} to allow callers to
+ * process an existing ReceiveCommand against a RefTree.
+ * <p>
+ * Commands should be passed into {@link RefTree#apply(java.util.Collection)}
+ * for processing.
+ */
+public class Command {
+	/**
+	 * Set unprocessed commands as failed due to transaction aborted.
+	 * <p>
+	 * If a command is still {@link Result#NOT_ATTEMPTED} it will be set to
+	 * {@link Result#REJECTED_OTHER_REASON}. If {@code why} is non-null its
+	 * contents will be used as the message for the first command status.
+	 *
+	 * @param commands
+	 *            commands to mark as failed.
+	 * @param why
+	 *            optional message to set on the first aborted command.
+	 */
+	public static void abort(Iterable<Command> commands, @Nullable String why) {
+		if (why == null || why.isEmpty()) {
+			why = JGitText.get().transactionAborted;
+		}
+		for (Command c : commands) {
+			if (c.getResult() == NOT_ATTEMPTED) {
+				c.setResult(REJECTED_OTHER_REASON, why);
+				why = JGitText.get().transactionAborted;
+			}
+		}
+	}
+
+	private final Ref oldRef;
+	private final Ref newRef;
+	private final ReceiveCommand cmd;
+	private Result result;
+
+	/**
+	 * Create a command to create, update or delete a reference.
+	 * <p>
+	 * At least one of {@code oldRef} or {@code newRef} must be supplied.
+	 *
+	 * @param oldRef
+	 *            expected value. Null if the ref should not exist.
+	 * @param newRef
+	 *            desired value, must be peeled if not null and not symbolic.
+	 *            Null to delete the ref.
+	 */
+	public Command(@Nullable Ref oldRef, @Nullable Ref newRef) {
+		this.oldRef = oldRef;
+		this.newRef = newRef;
+		this.cmd = null;
+		this.result = NOT_ATTEMPTED;
+
+		if (oldRef == null && newRef == null) {
+			throw new IllegalArgumentException();
+		}
+		if (newRef != null && !newRef.isPeeled() && !newRef.isSymbolic()) {
+			throw new IllegalArgumentException();
+		}
+		if (oldRef != null && newRef != null
+				&& !oldRef.getName().equals(newRef.getName())) {
+			throw new IllegalArgumentException();
+		}
+	}
+
+	/**
+	 * Construct a RefTree command wrapped around a ReceiveCommand.
+	 *
+	 * @param rw
+	 *            walk instance to peel the {@code newId}.
+	 * @param cmd
+	 *            command received from a push client.
+	 * @throws MissingObjectException
+	 *             {@code oldId} or {@code newId} is missing.
+	 * @throws IOException
+	 *             {@code oldId} or {@code newId} cannot be peeled.
+	 */
+	public Command(RevWalk rw, ReceiveCommand cmd)
+			throws MissingObjectException, IOException {
+		this.oldRef = toRef(rw, cmd.getOldId(), cmd.getRefName(), false);
+		this.newRef = toRef(rw, cmd.getNewId(), cmd.getRefName(), true);
+		this.cmd = cmd;
+	}
+
+	static Ref toRef(RevWalk rw, ObjectId id, String name,
+			boolean mustExist) throws MissingObjectException, IOException {
+		if (ObjectId.zeroId().equals(id)) {
+			return null;
+		}
+
+		try {
+			RevObject o = rw.parseAny(id);
+			if (o instanceof RevTag) {
+				RevObject p = rw.peel(o);
+				return new ObjectIdRef.PeeledTag(NETWORK, name, id, p.copy());
+			}
+			return new ObjectIdRef.PeeledNonTag(NETWORK, name, id);
+		} catch (MissingObjectException e) {
+			if (mustExist) {
+				throw e;
+			}
+			return new ObjectIdRef.Unpeeled(NETWORK, name, id);
+		}
+	}
+
+	/** @return name of the reference affected by this command. */
+	public String getRefName() {
+		if (cmd != null) {
+			return cmd.getRefName();
+		} else if (newRef != null) {
+			return newRef.getName();
+		}
+		return oldRef.getName();
+	}
+
+	/**
+	 * Set the result of this command.
+	 *
+	 * @param result
+	 *            the command result.
+	 */
+	public void setResult(Result result) {
+		setResult(result, null);
+	}
+
+	/**
+	 * Set the result of this command.
+	 *
+	 * @param result
+	 *            the command result.
+	 * @param why
+	 *            optional message explaining the result status.
+	 */
+	public void setResult(Result result, @Nullable String why) {
+		if (cmd != null) {
+			cmd.setResult(result, why);
+		} else {
+			this.result = result;
+		}
+	}
+
+	/** @return result of executing this command. */
+	public Result getResult() {
+		return cmd != null ? cmd.getResult() : result;
+	}
+
+	/** @return optional message explaining command failure. */
+	@Nullable
+	public String getMessage() {
+		return cmd != null ? cmd.getMessage() : null;
+	}
+
+	/**
+	 * Old peeled reference.
+	 *
+	 * @return the old reference; null if the command is creating the reference.
+	 */
+	@Nullable
+	public Ref getOldRef() {
+		return oldRef;
+	}
+
+	/**
+	 * New peeled reference.
+	 *
+	 * @return the new reference; null if the command is deleting the reference.
+	 */
+	@Nullable
+	public Ref getNewRef() {
+		return newRef;
+	}
+
+	@Override
+	public String toString() {
+		StringBuilder s = new StringBuilder();
+		append(s, oldRef, "CREATE"); //$NON-NLS-1$
+		s.append(' ');
+		append(s, newRef, "DELETE"); //$NON-NLS-1$
+		s.append(' ').append(getRefName());
+		s.append(' ').append(getResult());
+		if (getMessage() != null) {
+			s.append(' ').append(getMessage());
+		}
+		return s.toString();
+	}
+
+	private static void append(StringBuilder s, Ref r, String nullName) {
+		if (r == null) {
+			s.append(nullName);
+		} else if (r.isSymbolic()) {
+			s.append(r.getTarget().getName());
+		} else {
+			ObjectId id = r.getObjectId();
+			if (id != null) {
+				s.append(id.name());
+			}
+		}
+	}
+
+	/**
+	 * Check the entry is consistent with either the old or the new ref.
+	 *
+	 * @param entry
+	 *            current entry; null if the entry does not exist.
+	 * @return true if entry matches {@link #getOldRef()} or
+	 *         {@link #getNewRef()}; otherwise false.
+	 */
+	boolean checkRef(@Nullable DirCacheEntry entry) {
+		if (entry != null && entry.getRawMode() == 0) {
+			entry = null;
+		}
+		return check(entry, oldRef) || check(entry, newRef);
+	}
+
+	private static boolean check(@Nullable DirCacheEntry cur,
+			@Nullable Ref exp) {
+		if (cur == null) {
+			// Does not exist, ok if oldRef does not exist.
+			return exp == null;
+		} else if (exp == null) {
+			// Expected to not exist, but currently exists, fail.
+			return false;
+		}
+
+		if (exp.isSymbolic()) {
+			String dst = exp.getTarget().getName();
+			return cur.getRawMode() == TYPE_SYMLINK
+					&& cur.getObjectId().equals(symref(dst));
+		}
+
+		return cur.getRawMode() == TYPE_GITLINK
+				&& cur.getObjectId().equals(exp.getObjectId());
+	}
+
+	static ObjectId symref(String s) {
+		@SuppressWarnings("resource")
+		ObjectInserter.Formatter fmt = new ObjectInserter.Formatter();
+		return fmt.idFor(OBJ_BLOB, encode(s));
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTree.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTree.java
new file mode 100644
index 0000000..85690c8
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTree.java
@@ -0,0 +1,411 @@
+/*
+ * Copyright (C) 2016, 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.reftree;
+
+import static org.eclipse.jgit.lib.Constants.HEAD;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+import static org.eclipse.jgit.lib.Constants.R_REFS;
+import static org.eclipse.jgit.lib.Constants.encode;
+import static org.eclipse.jgit.lib.FileMode.GITLINK;
+import static org.eclipse.jgit.lib.FileMode.SYMLINK;
+import static org.eclipse.jgit.lib.FileMode.TYPE_GITLINK;
+import static org.eclipse.jgit.lib.FileMode.TYPE_SYMLINK;
+import static org.eclipse.jgit.lib.Ref.Storage.NEW;
+import static org.eclipse.jgit.lib.Ref.Storage.PACKED;
+import static org.eclipse.jgit.lib.RefDatabase.MAX_SYMBOLIC_REF_DEPTH;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.LOCK_FAILURE;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.dircache.DirCache;
+import org.eclipse.jgit.dircache.DirCacheBuilder;
+import org.eclipse.jgit.dircache.DirCacheEditor;
+import org.eclipse.jgit.dircache.DirCacheEditor.DeletePath;
+import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
+import org.eclipse.jgit.dircache.DirCacheEntry;
+import org.eclipse.jgit.errors.CorruptObjectException;
+import org.eclipse.jgit.errors.DirCacheNameConflictException;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.SymbolicRef;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.util.RawParseUtils;
+
+/**
+ * Tree of references in the reference graph.
+ * <p>
+ * The root corresponds to the {@code "refs/"} subdirectory, for example the
+ * default reference {@code "refs/heads/master"} is stored at path
+ * {@code "heads/master"} in a {@code RefTree}.
+ * <p>
+ * Normal references are stored as {@link FileMode#GITLINK} tree entries. The
+ * ObjectId in the tree entry is the ObjectId the reference refers to.
+ * <p>
+ * Symbolic references are stored as {@link FileMode#SYMLINK} entries, with the
+ * blob storing the name of the target reference.
+ * <p>
+ * Annotated tags also store the peeled object using a {@code GITLINK} entry
+ * with the suffix <code>" ^"</code> (space carrot), for example
+ * {@code "tags/v1.0"} stores the annotated tag object, while
+ * <code>"tags/v1.0 ^"</code> stores the commit the tag annotates.
+ * <p>
+ * {@code HEAD} is a special case and stored as {@code "..HEAD"}.
+ */
+public class RefTree {
+	/** Suffix applied to GITLINK to indicate its the peeled value of a tag. */
+	public static final String PEELED_SUFFIX = " ^"; //$NON-NLS-1$
+	static final String ROOT_DOTDOT = ".."; //$NON-NLS-1$
+
+	/**
+	 * Create an empty reference tree.
+	 *
+	 * @return a new empty reference tree.
+	 */
+	public static RefTree newEmptyTree() {
+		return new RefTree(DirCache.newInCore());
+	}
+
+	/**
+	 * Load a reference tree.
+	 *
+	 * @param reader
+	 *            reader to scan the reference tree with.
+	 * @param tree
+	 *            the tree to read.
+	 * @return the ref tree read from the commit.
+	 * @throws IOException
+	 *             the repository cannot be accessed through the reader.
+	 * @throws CorruptObjectException
+	 *             a tree object is corrupt and cannot be read.
+	 * @throws IncorrectObjectTypeException
+	 *             a tree object wasn't actually a tree.
+	 * @throws MissingObjectException
+	 *             a reference tree object doesn't exist.
+	 */
+	public static RefTree read(ObjectReader reader, RevTree tree)
+			throws MissingObjectException, IncorrectObjectTypeException,
+			CorruptObjectException, IOException {
+		return new RefTree(DirCache.read(reader, tree));
+	}
+
+	private DirCache contents;
+	private Map<ObjectId, String> pendingBlobs;
+
+	private RefTree(DirCache dc) {
+		this.contents = dc;
+	}
+
+	/**
+	 * Read one reference.
+	 * <p>
+	 * References are always returned peeled ({@link Ref#isPeeled()} is true).
+	 * If the reference points to an annotated tag, the returned reference will
+	 * be peeled and contain {@link Ref#getPeeledObjectId()}.
+	 * <p>
+	 * If the reference is a symbolic reference and the chain depth is less than
+	 * {@link org.eclipse.jgit.lib.RefDatabase#MAX_SYMBOLIC_REF_DEPTH} the
+	 * returned reference is resolved. If the chain depth is longer, the
+	 * symbolic reference is returned without resolving.
+	 *
+	 * @param reader
+	 *            to access objects necessary to read the requested reference.
+	 * @param name
+	 *            name of the reference to read.
+	 * @return the reference; null if it does not exist.
+	 * @throws IOException
+	 *             cannot read a symbolic reference target.
+	 */
+	@Nullable
+	public Ref exactRef(ObjectReader reader, String name) throws IOException {
+		Ref r = readRef(reader, name);
+		if (r == null) {
+			return null;
+		} else if (r.isSymbolic()) {
+			return resolve(reader, r, 0);
+		}
+
+		DirCacheEntry p = contents.getEntry(peeledPath(name));
+		if (p != null && p.getRawMode() == TYPE_GITLINK) {
+			return new ObjectIdRef.PeeledTag(PACKED, r.getName(),
+					r.getObjectId(), p.getObjectId());
+		}
+		return r;
+	}
+
+	private Ref readRef(ObjectReader reader, String name) throws IOException {
+		DirCacheEntry e = contents.getEntry(refPath(name));
+		return e != null ? toRef(reader, e, name) : null;
+	}
+
+	private Ref toRef(ObjectReader reader, DirCacheEntry e, String name)
+			throws IOException {
+		int mode = e.getRawMode();
+		if (mode == TYPE_GITLINK) {
+			ObjectId id = e.getObjectId();
+			return new ObjectIdRef.PeeledNonTag(PACKED, name, id);
+		}
+
+		if (mode == TYPE_SYMLINK) {
+			ObjectId id = e.getObjectId();
+			String n = pendingBlobs != null ? pendingBlobs.get(id) : null;
+			if (n == null) {
+				byte[] bin = reader.open(id, OBJ_BLOB).getCachedBytes();
+				n = RawParseUtils.decode(bin);
+			}
+			Ref dst = new ObjectIdRef.Unpeeled(NEW, n, null);
+			return new SymbolicRef(name, dst);
+		}
+
+		return null; // garbage file or something; not a reference.
+	}
+
+	private Ref resolve(ObjectReader reader, Ref ref, int depth)
+			throws IOException {
+		if (ref.isSymbolic() && depth < MAX_SYMBOLIC_REF_DEPTH) {
+			Ref r = readRef(reader, ref.getTarget().getName());
+			if (r == null) {
+				return ref;
+			}
+			Ref dst = resolve(reader, r, depth + 1);
+			return new SymbolicRef(ref.getName(), dst);
+		}
+		return ref;
+	}
+
+	/**
+	 * Attempt a batch of commands against this RefTree.
+	 * <p>
+	 * The batch is applied atomically, either all commands apply at once, or
+	 * they all reject and the RefTree is left unmodified.
+	 * <p>
+	 * On success (when this method returns {@code true}) the command results
+	 * are left as-is (probably {@code NOT_ATTEMPTED}). Result fields are set
+	 * only when this method returns {@code false} to indicate failure.
+	 *
+	 * @param cmdList
+	 *            to apply. All commands should still have result NOT_ATTEMPTED.
+	 * @return true if the commands applied; false if they were rejected.
+	 */
+	public boolean apply(Collection<Command> cmdList) {
+		try {
+			DirCacheEditor ed = contents.editor();
+			for (Command cmd : cmdList) {
+				if (!isValidRef(cmd)) {
+					cmd.setResult(REJECTED_OTHER_REASON,
+							JGitText.get().funnyRefname);
+					Command.abort(cmdList, null);
+					return false;
+				}
+				apply(ed, cmd);
+			}
+			ed.finish();
+			return true;
+		} catch (DirCacheNameConflictException e) {
+			String r1 = refName(e.getPath1());
+			String r2 = refName(e.getPath2());
+			for (Command cmd : cmdList) {
+				if (r1.equals(cmd.getRefName())
+						|| r2.equals(cmd.getRefName())) {
+					cmd.setResult(LOCK_FAILURE);
+					break;
+				}
+			}
+			Command.abort(cmdList, null);
+			return false;
+		} catch (LockFailureException e) {
+			Command.abort(cmdList, null);
+			return false;
+		}
+	}
+
+	private static boolean isValidRef(Command cmd) {
+		String n = cmd.getRefName();
+		return HEAD.equals(n) || Repository.isValidRefName(n);
+	}
+
+	private void apply(DirCacheEditor ed, final Command cmd) {
+		String path = refPath(cmd.getRefName());
+		Ref oldRef = cmd.getOldRef();
+		final Ref newRef = cmd.getNewRef();
+
+		if (newRef == null) {
+			checkRef(contents.getEntry(path), cmd);
+			ed.add(new DeletePath(path));
+			cleanupPeeledRef(ed, oldRef);
+			return;
+		}
+
+		if (newRef.isSymbolic()) {
+			final String dst = newRef.getTarget().getName();
+			ed.add(new PathEdit(path) {
+				@Override
+				public void apply(DirCacheEntry ent) {
+					checkRef(ent, cmd);
+					ObjectId id = Command.symref(dst);
+					ent.setFileMode(SYMLINK);
+					ent.setObjectId(id);
+					if (pendingBlobs == null) {
+						pendingBlobs = new HashMap<>(4);
+					}
+					pendingBlobs.put(id, dst);
+				}
+			}.setReplace(false));
+			cleanupPeeledRef(ed, oldRef);
+			return;
+		}
+
+		ed.add(new PathEdit(path) {
+			@Override
+			public void apply(DirCacheEntry ent) {
+				checkRef(ent, cmd);
+				ent.setFileMode(GITLINK);
+				ent.setObjectId(newRef.getObjectId());
+			}
+		}.setReplace(false));
+
+		if (newRef.getPeeledObjectId() != null) {
+			ed.add(new PathEdit(peeledPath(newRef.getName())) {
+				@Override
+				public void apply(DirCacheEntry ent) {
+					ent.setFileMode(GITLINK);
+					ent.setObjectId(newRef.getPeeledObjectId());
+				}
+			}.setReplace(false));
+		} else {
+			cleanupPeeledRef(ed, oldRef);
+		}
+	}
+
+	private static void checkRef(@Nullable DirCacheEntry ent, Command cmd) {
+		if (!cmd.checkRef(ent)) {
+			cmd.setResult(LOCK_FAILURE);
+			throw new LockFailureException();
+		}
+	}
+
+	private static void cleanupPeeledRef(DirCacheEditor ed, Ref ref) {
+		if (ref != null && !ref.isSymbolic()
+				&& (!ref.isPeeled() || ref.getPeeledObjectId() != null)) {
+			ed.add(new DeletePath(peeledPath(ref.getName())));
+		}
+	}
+
+	/**
+	 * Convert a path name in a RefTree to the reference name known by Git.
+	 *
+	 * @param path
+	 *            name read from the RefTree structure, for example
+	 *            {@code "heads/master"}.
+	 * @return reference name for the path, {@code "refs/heads/master"}.
+	 */
+	public static String refName(String path) {
+		if (path.startsWith(ROOT_DOTDOT)) {
+			return path.substring(2);
+		}
+		return R_REFS + path;
+	}
+
+	static String refPath(String name) {
+		if (name.startsWith(R_REFS)) {
+			return name.substring(R_REFS.length());
+		}
+		return ROOT_DOTDOT + name;
+	}
+
+	private static String peeledPath(String name) {
+		return refPath(name) + PEELED_SUFFIX;
+	}
+
+	/**
+	 * Write this reference tree.
+	 *
+	 * @param inserter
+	 *            inserter to use when writing trees to the object database.
+	 *            Caller is responsible for flushing the inserter before trying
+	 *            to read the objects, or exposing them through a reference.
+	 * @return the top level tree.
+	 * @throws IOException
+	 *             a tree could not be written.
+	 */
+	public ObjectId writeTree(ObjectInserter inserter) throws IOException {
+		if (pendingBlobs != null) {
+			for (String s : pendingBlobs.values()) {
+				inserter.insert(OBJ_BLOB, encode(s));
+			}
+			pendingBlobs = null;
+		}
+		return contents.writeTree(inserter);
+	}
+
+	/** @return a deep copy of this RefTree. */
+	public RefTree copy() {
+		RefTree r = new RefTree(DirCache.newInCore());
+		DirCacheBuilder b = r.contents.builder();
+		for (int i = 0; i < contents.getEntryCount(); i++) {
+			b.add(new DirCacheEntry(contents.getEntry(i)));
+		}
+		b.finish();
+		if (pendingBlobs != null) {
+			r.pendingBlobs = new HashMap<>(pendingBlobs);
+		}
+		return r;
+	}
+
+	private static class LockFailureException extends RuntimeException {
+		private static final long serialVersionUID = 1L;
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeBatch.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeBatch.java
new file mode 100644
index 0000000..a55a9f5
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeBatch.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2016, 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.reftree;
+
+import static org.eclipse.jgit.lib.Constants.OBJ_TREE;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_NONFASTFORWARD;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
+import static org.eclipse.jgit.transport.ReceiveCommand.Type.UPDATE;
+import static org.eclipse.jgit.transport.ReceiveCommand.Type.UPDATE_NONFASTFORWARD;
+
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+/** Batch update a {@link RefTreeDatabase}. */
+class RefTreeBatch extends BatchRefUpdate {
+	private final RefTreeDatabase refdb;
+	private Ref src;
+	private ObjectId parentCommitId;
+	private ObjectId parentTreeId;
+	private RefTree tree;
+	private PersonIdent author;
+	private ObjectId newCommitId;
+
+	RefTreeBatch(RefTreeDatabase refdb) {
+		super(refdb);
+		this.refdb = refdb;
+	}
+
+	@Override
+	public void execute(RevWalk rw, ProgressMonitor monitor)
+			throws IOException {
+		List<Command> todo = new ArrayList<>(getCommands().size());
+		for (ReceiveCommand c : getCommands()) {
+			if (!isAllowNonFastForwards()) {
+				if (c.getType() == UPDATE) {
+					c.updateType(rw);
+				}
+				if (c.getType() == UPDATE_NONFASTFORWARD) {
+					c.setResult(REJECTED_NONFASTFORWARD);
+					ReceiveCommand.abort(getCommands());
+					return;
+				}
+			}
+			todo.add(new Command(rw, c));
+		}
+		init(rw);
+		execute(rw, todo);
+	}
+
+	void init(RevWalk rw) throws IOException {
+		src = refdb.getBootstrap().exactRef(refdb.getTxnCommitted());
+		if (src != null && src.getObjectId() != null) {
+			RevCommit c = rw.parseCommit(src.getObjectId());
+			parentCommitId = c;
+			parentTreeId = c.getTree();
+			tree = RefTree.read(rw.getObjectReader(), c.getTree());
+		} else {
+			parentCommitId = ObjectId.zeroId();
+			parentTreeId = new ObjectInserter.Formatter()
+					.idFor(OBJ_TREE, new byte[] {});
+			tree = RefTree.newEmptyTree();
+		}
+	}
+
+	@Nullable
+	Ref exactRef(ObjectReader reader, String name) throws IOException {
+		return tree.exactRef(reader, name);
+	}
+
+	/**
+	 * Execute an update from {@link RefTreeUpdate} or {@link RefTreeRename}.
+	 *
+	 * @param rw
+	 *            current RevWalk handling the update or rename.
+	 * @param todo
+	 *            commands to execute. Must never be a bootstrap reference name.
+	 * @throws IOException
+	 *             the storage system is unable to read or write data.
+	 */
+	void execute(RevWalk rw, List<Command> todo) throws IOException {
+		for (Command c : todo) {
+			if (c.getResult() != NOT_ATTEMPTED) {
+				Command.abort(todo, null);
+				return;
+			}
+			if (refdb.conflictsWithBootstrap(c.getRefName())) {
+				c.setResult(REJECTED_OTHER_REASON, MessageFormat
+						.format(JGitText.get().invalidRefName, c.getRefName()));
+				Command.abort(todo, null);
+				return;
+			}
+		}
+
+		if (apply(todo) && newCommitId != null) {
+			commit(rw, todo);
+		}
+	}
+
+	private boolean apply(List<Command> todo) throws IOException {
+		if (!tree.apply(todo)) {
+			// apply set rejection information on commands.
+			return false;
+		}
+
+		Repository repo = refdb.getRepository();
+		try (ObjectInserter ins = repo.newObjectInserter()) {
+			CommitBuilder b = new CommitBuilder();
+			b.setTreeId(tree.writeTree(ins));
+			if (parentTreeId.equals(b.getTreeId())) {
+				for (Command c : todo) {
+					c.setResult(OK);
+				}
+				return true;
+			}
+			if (!parentCommitId.equals(ObjectId.zeroId())) {
+				b.setParentId(parentCommitId);
+			}
+
+			author = getRefLogIdent();
+			if (author == null) {
+				author = new PersonIdent(repo);
+			}
+			b.setAuthor(author);
+			b.setCommitter(author);
+			b.setMessage(getRefLogMessage());
+			newCommitId = ins.insert(b);
+			ins.flush();
+		}
+		return true;
+	}
+
+	private void commit(RevWalk rw, List<Command> todo) throws IOException {
+		ReceiveCommand commit = new ReceiveCommand(
+				parentCommitId, newCommitId,
+				refdb.getTxnCommitted());
+		updateBootstrap(rw, commit);
+
+		if (commit.getResult() == OK) {
+			for (Command c : todo) {
+				c.setResult(OK);
+			}
+		} else {
+			Command.abort(todo, commit.getResult().name());
+		}
+	}
+
+	private void updateBootstrap(RevWalk rw, ReceiveCommand commit)
+			throws IOException {
+		BatchRefUpdate u = refdb.getBootstrap().newBatchUpdate();
+		u.setAllowNonFastForwards(true);
+		u.setPushCertificate(getPushCertificate());
+		if (isRefLogDisabled()) {
+			u.disableRefLog();
+		} else {
+			u.setRefLogIdent(author);
+			u.setRefLogMessage(getRefLogMessage(), false);
+		}
+		u.addCommand(commit);
+		u.execute(rw, NullProgressMonitor.INSTANCE);
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeDatabase.java
new file mode 100644
index 0000000..df93ce8
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeDatabase.java
@@ -0,0 +1,372 @@
+/*
+ * Copyright (C) 2016, 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.reftree;
+
+import static org.eclipse.jgit.lib.Constants.HEAD;
+import static org.eclipse.jgit.lib.Ref.Storage.LOOSE;
+import static org.eclipse.jgit.lib.Ref.Storage.PACKED;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Ref.Storage;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.RefRename;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.SymbolicRef;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevTag;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.util.RefList;
+import org.eclipse.jgit.util.RefMap;
+
+/**
+ * Reference database backed by a {@link RefTree}.
+ * <p>
+ * The storage for RefTreeDatabase has two parts. The main part is a native Git
+ * tree object stored under the {@code refs/txn} namespace. To avoid cycles,
+ * references to {@code refs/txn} are not stored in that tree object, but
+ * instead in a "bootstrap" layer, which is a separate {@link RefDatabase} such
+ * as {@link org.eclipse.jgit.internal.storage.file.RefDirectory} using local
+ * reference files inside of {@code $GIT_DIR/refs}.
+ */
+public class RefTreeDatabase extends RefDatabase {
+	private final Repository repo;
+	private final RefDatabase bootstrap;
+	private final String txnCommitted;
+
+	@Nullable
+	private final String txnNamespace;
+	private volatile Scanner.Result refs;
+
+	/**
+	 * Create a RefTreeDb for a repository.
+	 *
+	 * @param repo
+	 *            the repository using references in this database.
+	 * @param bootstrap
+	 *            bootstrap reference database storing the references that
+	 *            anchor the {@link RefTree}.
+	 */
+	public RefTreeDatabase(Repository repo, RefDatabase bootstrap) {
+		Config cfg = repo.getConfig();
+		String committed = cfg.getString("reftree", null, "committedRef"); //$NON-NLS-1$ //$NON-NLS-2$
+		if (committed == null || committed.isEmpty()) {
+			committed = "refs/txn/committed"; //$NON-NLS-1$
+		}
+
+		this.repo = repo;
+		this.bootstrap = bootstrap;
+		this.txnNamespace = initNamespace(committed);
+		this.txnCommitted = committed;
+	}
+
+	/**
+	 * Create a RefTreeDb for a repository.
+	 *
+	 * @param repo
+	 *            the repository using references in this database.
+	 * @param bootstrap
+	 *            bootstrap reference database storing the references that
+	 *            anchor the {@link RefTree}.
+	 * @param txnCommitted
+	 *            name of the bootstrap reference holding the committed RefTree.
+	 */
+	public RefTreeDatabase(Repository repo, RefDatabase bootstrap,
+			String txnCommitted) {
+		this.repo = repo;
+		this.bootstrap = bootstrap;
+		this.txnNamespace = initNamespace(txnCommitted);
+		this.txnCommitted = txnCommitted;
+	}
+
+	private static String initNamespace(String committed) {
+		int s = committed.lastIndexOf('/');
+		if (s < 0) {
+			return null;
+		}
+		return committed.substring(0, s + 1); // Keep trailing '/'.
+	}
+
+	Repository getRepository() {
+		return repo;
+	}
+
+	/**
+	 * @return the bootstrap reference database, which must be used to access
+	 *         {@link #getTxnCommitted()}, {@link #getTxnNamespace()}.
+	 */
+	public RefDatabase getBootstrap() {
+		return bootstrap;
+	}
+
+	/** @return name of bootstrap reference anchoring committed RefTree. */
+	public String getTxnCommitted() {
+		return txnCommitted;
+	}
+
+	/**
+	 * @return namespace used by bootstrap layer, e.g. {@code refs/txn/}.
+	 *         Always ends in {@code '/'}.
+	 */
+	@Nullable
+	public String getTxnNamespace() {
+		return txnNamespace;
+	}
+
+	@Override
+	public void create() throws IOException {
+		bootstrap.create();
+	}
+
+	@Override
+	public boolean performsAtomicTransactions() {
+		return true;
+	}
+
+	@Override
+	public void refresh() {
+		bootstrap.refresh();
+	}
+
+	@Override
+	public void close() {
+		refs = null;
+		bootstrap.close();
+	}
+
+	@Override
+	public Ref getRef(String name) throws IOException {
+		String[] needle = new String[SEARCH_PATH.length];
+		for (int i = 0; i < SEARCH_PATH.length; i++) {
+			needle[i] = SEARCH_PATH[i] + name;
+		}
+		return firstExactRef(needle);
+	}
+
+	@Override
+	public Ref exactRef(String name) throws IOException {
+		if (!repo.isBare() && name.indexOf('/') < 0 && !HEAD.equals(name)) {
+			// Pass through names like MERGE_HEAD, ORIG_HEAD, FETCH_HEAD.
+			return bootstrap.exactRef(name);
+		} else if (conflictsWithBootstrap(name)) {
+			return null;
+		}
+
+		boolean partial = false;
+		Ref src = bootstrap.exactRef(txnCommitted);
+		Scanner.Result c = refs;
+		if (c == null || !c.refTreeId.equals(idOf(src))) {
+			c = Scanner.scanRefTree(repo, src, prefixOf(name), false);
+			partial = true;
+		}
+
+		Ref r = c.all.get(name);
+		if (r != null && r.isSymbolic()) {
+			r = c.sym.get(name);
+			if (partial && r.getObjectId() == null) {
+				// Attempting exactRef("HEAD") with partial scan will leave
+				// an unresolved symref as its target e.g. refs/heads/master
+				// was not read by the partial scan. Scan everything instead.
+				return getRefs(ALL).get(name);
+			}
+		}
+		return r;
+	}
+
+	private static String prefixOf(String name) {
+		int s = name.lastIndexOf('/');
+		if (s >= 0) {
+			return name.substring(0, s);
+		}
+		return ""; //$NON-NLS-1$
+	}
+
+	@Override
+	public Map<String, Ref> getRefs(String prefix) throws IOException {
+		if (!prefix.isEmpty() && prefix.charAt(prefix.length() - 1) != '/') {
+			return new HashMap<>(0);
+		}
+
+		Ref src = bootstrap.exactRef(txnCommitted);
+		Scanner.Result c = refs;
+		if (c == null || !c.refTreeId.equals(idOf(src))) {
+			c = Scanner.scanRefTree(repo, src, prefix, true);
+			if (prefix.isEmpty()) {
+				refs = c;
+			}
+		}
+		return new RefMap(prefix, RefList.<Ref> emptyList(), c.all, c.sym);
+	}
+
+	private static ObjectId idOf(@Nullable Ref src) {
+		return src != null && src.getObjectId() != null
+				? src.getObjectId()
+				: ObjectId.zeroId();
+	}
+
+	@Override
+	public List<Ref> getAdditionalRefs() throws IOException {
+		Collection<Ref> txnRefs;
+		if (txnNamespace != null) {
+			txnRefs = bootstrap.getRefs(txnNamespace).values();
+		} else {
+			Ref r = bootstrap.exactRef(txnCommitted);
+			if (r != null && r.getObjectId() != null) {
+				txnRefs = Collections.singleton(r);
+			} else {
+				txnRefs = Collections.emptyList();
+			}
+		}
+
+		List<Ref> otherRefs = bootstrap.getAdditionalRefs();
+		List<Ref> all = new ArrayList<>(txnRefs.size() + otherRefs.size());
+		all.addAll(txnRefs);
+		all.addAll(otherRefs);
+		return all;
+	}
+
+	@Override
+	public Ref peel(Ref ref) throws IOException {
+		Ref i = ref.getLeaf();
+		ObjectId id = i.getObjectId();
+		if (i.isPeeled() || id == null) {
+			return ref;
+		}
+		try (RevWalk rw = new RevWalk(repo)) {
+			RevObject obj = rw.parseAny(id);
+			if (obj instanceof RevTag) {
+				ObjectId p = rw.peel(obj).copy();
+				i = new ObjectIdRef.PeeledTag(PACKED, i.getName(), id, p);
+			} else {
+				i = new ObjectIdRef.PeeledNonTag(PACKED, i.getName(), id);
+			}
+		}
+		return recreate(ref, i);
+	}
+
+	private static Ref recreate(Ref old, Ref leaf) {
+		if (old.isSymbolic()) {
+			Ref dst = recreate(old.getTarget(), leaf);
+			return new SymbolicRef(old.getName(), dst);
+		}
+		return leaf;
+	}
+
+	@Override
+	public boolean isNameConflicting(String name) throws IOException {
+		return conflictsWithBootstrap(name)
+				|| !getConflictingNames(name).isEmpty();
+	}
+
+	@Override
+	public BatchRefUpdate newBatchUpdate() {
+		return new RefTreeBatch(this);
+	}
+
+	@Override
+	public RefUpdate newUpdate(String name, boolean detach) throws IOException {
+		if (!repo.isBare() && name.indexOf('/') < 0 && !HEAD.equals(name)) {
+			return bootstrap.newUpdate(name, detach);
+		}
+		if (conflictsWithBootstrap(name)) {
+			return new AlwaysFailUpdate(this, name);
+		}
+
+		Ref r = exactRef(name);
+		if (r == null) {
+			r = new ObjectIdRef.Unpeeled(Storage.NEW, name, null);
+		}
+
+		boolean detaching = detach && r.isSymbolic();
+		if (detaching) {
+			r = new ObjectIdRef.Unpeeled(LOOSE, name, r.getObjectId());
+		}
+
+		RefTreeUpdate u = new RefTreeUpdate(this, r);
+		if (detaching) {
+			u.setDetachingSymbolicRef();
+		}
+		return u;
+	}
+
+	@Override
+	public RefRename newRename(String fromName, String toName)
+			throws IOException {
+		RefUpdate from = newUpdate(fromName, true);
+		RefUpdate to = newUpdate(toName, true);
+		return new RefTreeRename(this, from, to);
+	}
+
+	boolean conflictsWithBootstrap(String name) {
+		if (txnNamespace != null && name.startsWith(txnNamespace)) {
+			return true;
+		} else if (txnCommitted.equals(name)) {
+			return true;
+		}
+
+		if (name.indexOf('/') < 0 && !HEAD.equals(name)) {
+			return true;
+		}
+
+		if (name.length() > txnCommitted.length()
+				&& name.charAt(txnCommitted.length()) == '/'
+				&& name.startsWith(txnCommitted)) {
+			return true;
+		}
+		return false;
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeNames.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeNames.java
new file mode 100644
index 0000000..c53d6de
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeNames.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2016, 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.reftree;
+
+import org.eclipse.jgit.lib.RefDatabase;
+
+/** Magic reference name logic for RefTrees. */
+public class RefTreeNames {
+	/**
+	 * Suffix used on a {@link RefTreeDatabase#getTxnNamespace()} for user data.
+	 * <p>
+	 * A {@link RefTreeDatabase}'s namespace may include a subspace (e.g.
+	 * {@code "refs/txn/stage/"}) containing commit objects from the usual user
+	 * portion of the repository (e.g. {@code "refs/heads/"}). These should be
+	 * packed by the garbage collector alongside other user content rather than
+	 * with the RefTree.
+	 */
+	private static final String STAGE = "stage/"; //$NON-NLS-1$
+
+	/**
+	 * Determine if the reference is likely to be a RefTree.
+	 *
+	 * @param refdb
+	 *            database instance.
+	 * @param ref
+	 *            reference name.
+	 * @return {@code true} if the reference is a RefTree.
+	 */
+	public static boolean isRefTree(RefDatabase refdb, String ref) {
+		if (refdb instanceof RefTreeDatabase) {
+			RefTreeDatabase b = (RefTreeDatabase) refdb;
+			if (ref.equals(b.getTxnCommitted())) {
+				return true;
+			}
+
+			String namespace = b.getTxnNamespace();
+			if (namespace != null
+					&& ref.startsWith(namespace)
+					&& !ref.startsWith(namespace + STAGE)) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	private RefTreeNames() {
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeRename.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeRename.java
new file mode 100644
index 0000000..5fd7ecd
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeRename.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2016, 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.reftree;
+
+import static org.eclipse.jgit.lib.Constants.HEAD;
+import static org.eclipse.jgit.lib.RefUpdate.Result.REJECTED;
+import static org.eclipse.jgit.lib.RefUpdate.Result.RENAMED;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefRename;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.RefUpdate.Result;
+import org.eclipse.jgit.lib.SymbolicRef;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+/** Single reference rename to {@link RefTreeDatabase}. */
+class RefTreeRename extends RefRename {
+	private final RefTreeDatabase refdb;
+
+	RefTreeRename(RefTreeDatabase refdb, RefUpdate src, RefUpdate dst) {
+		super(src, dst);
+		this.refdb = refdb;
+	}
+
+	@Override
+	protected Result doRename() throws IOException {
+		try (RevWalk rw = new RevWalk(refdb.getRepository())) {
+			RefTreeBatch batch = new RefTreeBatch(refdb);
+			batch.setRefLogIdent(getRefLogIdent());
+			batch.setRefLogMessage(getRefLogMessage(), false);
+			batch.init(rw);
+
+			Ref head = batch.exactRef(rw.getObjectReader(), HEAD);
+			Ref oldRef = batch.exactRef(rw.getObjectReader(), source.getName());
+			if (oldRef == null) {
+				return REJECTED;
+			}
+
+			Ref newRef = asNew(oldRef);
+			List<Command> mv = new ArrayList<>(3);
+			mv.add(new Command(oldRef, null));
+			mv.add(new Command(null, newRef));
+			if (head != null && head.isSymbolic()
+					&& head.getTarget().getName().equals(oldRef.getName())) {
+				mv.add(new Command(
+					head,
+					new SymbolicRef(head.getName(), newRef)));
+			}
+			batch.execute(rw, mv);
+			return RefTreeUpdate.translate(mv.get(1).getResult(), RENAMED);
+		}
+	}
+
+	private Ref asNew(Ref src) {
+		String name = destination.getName();
+		if (src.isSymbolic()) {
+			return new SymbolicRef(name, src.getTarget());
+		}
+
+		ObjectId peeled = src.getPeeledObjectId();
+		if (peeled != null) {
+			return new ObjectIdRef.PeeledTag(
+					src.getStorage(),
+					name,
+					src.getObjectId(),
+					peeled);
+		}
+
+		return new ObjectIdRef.PeeledNonTag(
+				src.getStorage(),
+				name,
+				src.getObjectId());
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeUpdate.java
new file mode 100644
index 0000000..8829c11
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/RefTreeUpdate.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2016, 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.reftree;
+
+import static org.eclipse.jgit.lib.Ref.Storage.LOOSE;
+import static org.eclipse.jgit.lib.Ref.Storage.NEW;
+
+import java.io.IOException;
+import java.util.Collections;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.SymbolicRef;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevTag;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+/** Single reference update to {@link RefTreeDatabase}. */
+class RefTreeUpdate extends RefUpdate {
+	private final RefTreeDatabase refdb;
+	private RevWalk rw;
+	private RefTreeBatch batch;
+	private Ref oldRef;
+
+	RefTreeUpdate(RefTreeDatabase refdb, Ref ref) {
+		super(ref);
+		this.refdb = refdb;
+		setCheckConflicting(false); // Done automatically by doUpdate.
+	}
+
+	@Override
+	protected RefDatabase getRefDatabase() {
+		return refdb;
+	}
+
+	@Override
+	protected Repository getRepository() {
+		return refdb.getRepository();
+	}
+
+	@Override
+	protected boolean tryLock(boolean deref) throws IOException {
+		rw = new RevWalk(getRepository());
+		batch = new RefTreeBatch(refdb);
+		batch.init(rw);
+		oldRef = batch.exactRef(rw.getObjectReader(), getName());
+		if (oldRef != null && oldRef.getObjectId() != null) {
+			setOldObjectId(oldRef.getObjectId());
+		} else if (oldRef == null && getExpectedOldObjectId() != null) {
+			setOldObjectId(ObjectId.zeroId());
+		}
+		return true;
+	}
+
+	@Override
+	protected void unlock() {
+		batch = null;
+		if (rw != null) {
+			rw.close();
+			rw = null;
+		}
+	}
+
+	@Override
+	protected Result doUpdate(Result desiredResult) throws IOException {
+		return run(newRef(getName(), getNewObjectId()), desiredResult);
+	}
+
+	private Ref newRef(String name, ObjectId id)
+			throws MissingObjectException, IOException {
+		RevObject o = rw.parseAny(id);
+		if (o instanceof RevTag) {
+			RevObject p = rw.peel(o);
+			return new ObjectIdRef.PeeledTag(LOOSE, name, id, p.copy());
+		}
+		return new ObjectIdRef.PeeledNonTag(LOOSE, name, id);
+	}
+
+	@Override
+	protected Result doDelete(Result desiredResult) throws IOException {
+		return run(null, desiredResult);
+	}
+
+	@Override
+	protected Result doLink(String target) throws IOException {
+		Ref dst = new ObjectIdRef.Unpeeled(NEW, target, null);
+		SymbolicRef n = new SymbolicRef(getName(), dst);
+		Result desiredResult = getRef().getStorage() == NEW
+			? Result.NEW
+			: Result.FORCED;
+		return run(n, desiredResult);
+	}
+
+	private Result run(@Nullable Ref newRef, Result desiredResult)
+			throws IOException {
+		Command c = new Command(oldRef, newRef);
+		batch.setRefLogIdent(getRefLogIdent());
+		batch.setRefLogMessage(getRefLogMessage(), isRefLogIncludingResult());
+		batch.execute(rw, Collections.singletonList(c));
+		return translate(c.getResult(), desiredResult);
+	}
+
+	static Result translate(ReceiveCommand.Result r, Result desiredResult) {
+		switch (r) {
+		case OK:
+			return desiredResult;
+
+		case LOCK_FAILURE:
+			return Result.LOCK_FAILURE;
+
+		case NOT_ATTEMPTED:
+			return Result.NOT_ATTEMPTED;
+
+		case REJECTED_MISSING_OBJECT:
+			return Result.IO_FAILURE;
+
+		case REJECTED_CURRENT_BRANCH:
+			return Result.REJECTED_CURRENT_BRANCH;
+
+		case REJECTED_OTHER_REASON:
+		case REJECTED_NOCREATE:
+		case REJECTED_NODELETE:
+		case REJECTED_NONFASTFORWARD:
+		default:
+			return Result.REJECTED;
+		}
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/Scanner.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/Scanner.java
new file mode 100644
index 0000000..d383abf
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/reftree/Scanner.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2016, 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.reftree;
+
+import static org.eclipse.jgit.lib.RefDatabase.MAX_SYMBOLIC_REF_DEPTH;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+import static org.eclipse.jgit.lib.Constants.R_REFS;
+import static org.eclipse.jgit.lib.Constants.encode;
+import static org.eclipse.jgit.lib.FileMode.TYPE_GITLINK;
+import static org.eclipse.jgit.lib.FileMode.TYPE_SYMLINK;
+import static org.eclipse.jgit.lib.FileMode.TYPE_TREE;
+import static org.eclipse.jgit.lib.Ref.Storage.NEW;
+import static org.eclipse.jgit.lib.Ref.Storage.PACKED;
+
+import java.io.IOException;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.SymbolicRef;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.AbstractTreeIterator;
+import org.eclipse.jgit.treewalk.CanonicalTreeParser;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.util.Paths;
+import org.eclipse.jgit.util.RawParseUtils;
+import org.eclipse.jgit.util.RefList;
+
+/** A tree parser that extracts references from a {@link RefTree}. */
+class Scanner {
+	private static final int MAX_SYMLINK_BYTES = 10 << 10;
+	private static final byte[] BINARY_R_REFS = encode(R_REFS);
+	private static final byte[] REFS_DOT_DOT = encode("refs/.."); //$NON-NLS-1$
+
+	static class Result {
+		final ObjectId refTreeId;
+		final RefList<Ref> all;
+		final RefList<Ref> sym;
+
+		Result(ObjectId id, RefList<Ref> all, RefList<Ref> sym) {
+			this.refTreeId = id;
+			this.all = all;
+			this.sym = sym;
+		}
+	}
+
+	/**
+	 * Scan a {@link RefTree} and parse entries into {@link Ref} instances.
+	 *
+	 * @param repo
+	 *            source repository containing the commit and tree objects that
+	 *            make up the RefTree.
+	 * @param src
+	 *            bootstrap reference such as {@code refs/txn/committed} to read
+	 *            the reference tree tip from. The current ObjectId will be
+	 *            included in {@link Result#refTreeId}.
+	 * @param prefix
+	 *            if non-empty a reference prefix to scan only a subdirectory.
+	 *            For example {@code prefix = "refs/heads/"} will limit the scan
+	 *            to only the {@code "heads"} directory of the RefTree, avoiding
+	 *            other directories like {@code "tags"}. Empty string reads all
+	 *            entries in the RefTree.
+	 * @param recursive
+	 *            if true recurse into subdirectories of the reference tree;
+	 *            false to read only one level. Callers may use false during an
+	 *            implementation of {@code exactRef(String)} where only one
+	 *            reference is needed out of a specific subtree.
+	 * @return sorted list of references after parsing.
+	 * @throws IOException
+	 *             tree cannot be accessed from the repository.
+	 */
+	static Result scanRefTree(Repository repo, @Nullable Ref src, String prefix,
+			boolean recursive) throws IOException {
+		RefList.Builder<Ref> all = new RefList.Builder<>();
+		RefList.Builder<Ref> sym = new RefList.Builder<>();
+
+		ObjectId srcId;
+		if (src != null && src.getObjectId() != null) {
+			try (ObjectReader reader = repo.newObjectReader()) {
+				srcId = src.getObjectId();
+				scan(reader, srcId, prefix, recursive, all, sym);
+			}
+		} else {
+			srcId = ObjectId.zeroId();
+		}
+
+		RefList<Ref> aList = all.toRefList();
+		for (int idx = 0; idx < sym.size();) {
+			Ref s = sym.get(idx);
+			Ref r = resolve(s, 0, aList);
+			if (r != null) {
+				sym.set(idx++, r);
+			} else {
+				// Remove broken symbolic reference, they don't exist.
+				sym.remove(idx);
+				int rm = aList.find(s.getName());
+				if (0 <= rm) {
+					aList = aList.remove(rm);
+				}
+			}
+		}
+		return new Result(srcId, aList, sym.toRefList());
+	}
+
+	private static void scan(ObjectReader reader, AnyObjectId srcId,
+			String prefix, boolean recursive,
+			RefList.Builder<Ref> all, RefList.Builder<Ref> sym)
+					throws IncorrectObjectTypeException, IOException {
+		CanonicalTreeParser p = createParserAtPath(reader, srcId, prefix);
+		if (p == null) {
+			return;
+		}
+
+		while (!p.eof()) {
+			int mode = p.getEntryRawMode();
+			if (mode == TYPE_TREE) {
+				if (recursive) {
+					p = p.createSubtreeIterator(reader);
+				} else {
+					p = p.next();
+				}
+				continue;
+			}
+
+			if (!curElementHasPeelSuffix(p)) {
+				Ref r = toRef(reader, mode, p);
+				if (r != null) {
+					all.add(r);
+					if (r.isSymbolic()) {
+						sym.add(r);
+					}
+				}
+			} else if (mode == TYPE_GITLINK) {
+				peel(all, p);
+			}
+			p = p.next();
+		}
+	}
+
+	private static CanonicalTreeParser createParserAtPath(ObjectReader reader,
+			AnyObjectId srcId, String prefix) throws IOException {
+		ObjectId root = toTree(reader, srcId);
+		if (prefix.isEmpty()) {
+			return new CanonicalTreeParser(BINARY_R_REFS, reader, root);
+		}
+
+		String dir = RefTree.refPath(Paths.stripTrailingSeparator(prefix));
+		TreeWalk tw = TreeWalk.forPath(reader, dir, root);
+		if (tw == null || !tw.isSubtree()) {
+			return null;
+		}
+
+		ObjectId id = tw.getObjectId(0);
+		return new CanonicalTreeParser(encode(prefix), reader, id);
+	}
+
+	private static Ref resolve(Ref ref, int depth, RefList<Ref> refs)
+			throws IOException {
+		if (!ref.isSymbolic()) {
+			return ref;
+		} else if (MAX_SYMBOLIC_REF_DEPTH <= depth) {
+			return null;
+		}
+
+		Ref r = refs.get(ref.getTarget().getName());
+		if (r == null) {
+			return ref;
+		}
+
+		Ref dst = resolve(r, depth + 1, refs);
+		if (dst == null) {
+			return null;
+		}
+		return new SymbolicRef(ref.getName(), dst);
+	}
+
+	@SuppressWarnings("resource")
+	private static RevTree toTree(ObjectReader reader, AnyObjectId id)
+			throws IOException {
+		return new RevWalk(reader).parseTree(id);
+	}
+
+	private static boolean curElementHasPeelSuffix(AbstractTreeIterator itr) {
+		int n = itr.getEntryPathLength();
+		byte[] c = itr.getEntryPathBuffer();
+		return n > 2 && c[n - 2] == ' ' && c[n - 1] == '^';
+	}
+
+	private static void peel(RefList.Builder<Ref> all, CanonicalTreeParser p) {
+		String name = refName(p, true);
+		for (int idx = all.size() - 1; 0 <= idx; idx--) {
+			Ref r = all.get(idx);
+			int cmp = r.getName().compareTo(name);
+			if (cmp == 0) {
+				all.set(idx, new ObjectIdRef.PeeledTag(PACKED, r.getName(),
+						r.getObjectId(), p.getEntryObjectId()));
+				break;
+			} else if (cmp < 0) {
+				// Stray peeled name without matching base name; skip entry.
+				break;
+			}
+		}
+	}
+
+	private static Ref toRef(ObjectReader reader, int mode,
+			CanonicalTreeParser p) throws IOException {
+		if (mode == TYPE_GITLINK) {
+			String name = refName(p, false);
+			ObjectId id = p.getEntryObjectId();
+			return new ObjectIdRef.PeeledNonTag(PACKED, name, id);
+
+		} else if (mode == TYPE_SYMLINK) {
+			ObjectId id = p.getEntryObjectId();
+			byte[] bin = reader.open(id, OBJ_BLOB)
+					.getCachedBytes(MAX_SYMLINK_BYTES);
+			String dst = RawParseUtils.decode(bin);
+			Ref trg = new ObjectIdRef.Unpeeled(NEW, dst, null);
+			String name = refName(p, false);
+			return new SymbolicRef(name, trg);
+		}
+		return null;
+	}
+
+	private static String refName(CanonicalTreeParser p, boolean peel) {
+		byte[] buf = p.getEntryPathBuffer();
+		int len = p.getEntryPathLength();
+		if (peel) {
+			len -= 2;
+		}
+		int ptr = 0;
+		if (RawParseUtils.match(buf, ptr, REFS_DOT_DOT) > 0) {
+			ptr = 7;
+		}
+		return RawParseUtils.decode(buf, ptr, len);
+	}
+
+	private Scanner() {
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java
index 45dd7ee..670f9a9 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java
@@ -109,7 +109,8 @@ private static File getSymRef(File workTree, File dotGit, FS fs)
 
 		int pathStart = 8;
 		int lineEnd = RawParseUtils.nextLF(content, pathStart);
-		if (content[lineEnd - 1] == '\n')
+		while (content[lineEnd - 1] == '\n' ||
+		       (content[lineEnd - 1] == '\r' && SystemReader.getInstance().isWindows()))
 			lineEnd--;
 		if (lineEnd == pathStart)
 			throw new IOException(MessageFormat.format(
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BlobBasedConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BlobBasedConfig.java
index cbb2f5b..7d52991 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BlobBasedConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BlobBasedConfig.java
@@ -79,7 +79,15 @@ public class BlobBasedConfig extends Config {
 	public BlobBasedConfig(Config base, final byte[] blob)
 			throws ConfigInvalidException {
 		super(base);
-		fromText(RawParseUtils.decode(blob));
+		final String decoded;
+		if (blob.length >= 3 && blob[0] == (byte) 0xEF
+				&& blob[1] == (byte) 0xBB && blob[2] == (byte) 0xBF) {
+			decoded = RawParseUtils.decode(RawParseUtils.UTF8_CHARSET,
+					blob, 3, blob.length);
+		} else {
+			decoded = RawParseUtils.decode(blob);
+		}
+		fromText(decoded);
 	}
 
 	/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileTreeEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileTreeEntry.java
deleted file mode 100644
index 6811417..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/FileTreeEntry.java
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org>
- * and other copyright owners as documented in the project's IP log.
- *
- * This program and the accompanying materials are made available
- * under the terms of the Eclipse Distribution License v1.0 which
- * accompanies this distribution, is reproduced below, and is
- * available at http://www.eclipse.org/org/documents/edl-v10.php
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or
- * without modification, are permitted provided that the following
- * conditions are met:
- *
- * - Redistributions of source code must retain the above copyright
- *   notice, this list of conditions and the following disclaimer.
- *
- * - Redistributions in binary form must reproduce the above
- *   copyright notice, this list of conditions and the following
- *   disclaimer in the documentation and/or other materials provided
- *   with the distribution.
- *
- * - Neither the name of the Eclipse Foundation, Inc. nor the
- *   names of its contributors may be used to endorse or promote
- *   products derived from this software without specific prior
- *   written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
- * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
- * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
- * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
- * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
- * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
- * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
- * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package org.eclipse.jgit.lib;
-
-import java.io.IOException;
-
-/**
- * A representation of a file (blob) object in a {@link Tree}.
- *
- * @deprecated To look up information about a single path, use
- * {@link org.eclipse.jgit.treewalk.TreeWalk#forPath(Repository, String, org.eclipse.jgit.revwalk.RevTree)}.
- * To lookup information about multiple paths at once, use a
- * {@link org.eclipse.jgit.treewalk.TreeWalk} and obtain the current entry's
- * information from its getter methods.
- */
-@Deprecated
-public class FileTreeEntry extends TreeEntry {
-	private FileMode mode;
-
-	/**
-	 * Constructor for a File (blob) object.
-	 *
-	 * @param parent
-	 *            The {@link Tree} holding this object (or null)
-	 * @param id
-	 *            the SHA-1 of the blob (or null for a yet unhashed file)
-	 * @param nameUTF8
-	 *            raw object name in the parent tree
-	 * @param execute
-	 *            true if the executable flag is set
-	 */
-	public FileTreeEntry(final Tree parent, final ObjectId id,
-			final byte[] nameUTF8, final boolean execute) {
-		super(parent, id, nameUTF8);
-		setExecutable(execute);
-	}
-
-	public FileMode getMode() {
-		return mode;
-	}
-
-	/**
-	 * @return true if this file is executable
-	 */
-	public boolean isExecutable() {
-		return getMode().equals(FileMode.EXECUTABLE_FILE);
-	}
-
-	/**
-	 * @param execute set/reset the executable flag
-	 */
-	public void setExecutable(final boolean execute) {
-		mode = execute ? FileMode.EXECUTABLE_FILE : FileMode.REGULAR_FILE;
-	}
-
-	/**
-	 * @return an {@link ObjectLoader} that will return the data
-	 * @throws IOException
-	 */
-	public ObjectLoader openReader() throws IOException {
-		return getRepository().open(getId(), Constants.OBJ_BLOB);
-	}
-
-	public String toString() {
-		final StringBuilder r = new StringBuilder();
-		r.append(ObjectId.toString(getId()));
-		r.append(' ');
-		r.append(isExecutable() ? 'X' : 'F');
-		r.append(' ');
-		r.append(getFullName());
-		return r.toString();
-	}
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GitlinkTreeEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GitlinkTreeEntry.java
deleted file mode 100644
index 936fd82..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GitlinkTreeEntry.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright (C) 2009, Jonas Fonseca <fonseca@diku.dk>
- * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2007, Shawn O. Pearce <spearce@spearce.org>
- * and other copyright owners as documented in the project's IP log.
- *
- * This program and the accompanying materials are made available
- * under the terms of the Eclipse Distribution License v1.0 which
- * accompanies this distribution, is reproduced below, and is
- * available at http://www.eclipse.org/org/documents/edl-v10.php
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or
- * without modification, are permitted provided that the following
- * conditions are met:
- *
- * - Redistributions of source code must retain the above copyright
- *   notice, this list of conditions and the following disclaimer.
- *
- * - Redistributions in binary form must reproduce the above
- *   copyright notice, this list of conditions and the following
- *   disclaimer in the documentation and/or other materials provided
- *   with the distribution.
- *
- * - Neither the name of the Eclipse Foundation, Inc. nor the
- *   names of its contributors may be used to endorse or promote
- *   products derived from this software without specific prior
- *   written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
- * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
- * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
- * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
- * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
- * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
- * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
- * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package org.eclipse.jgit.lib;
-
-/**
- * A tree entry representing a gitlink entry used for submodules.
- *
- * Note. Java cannot really handle these as file system objects.
- *
- * @deprecated To look up information about a single path, use
- * {@link org.eclipse.jgit.treewalk.TreeWalk#forPath(Repository, String, org.eclipse.jgit.revwalk.RevTree)}.
- * To lookup information about multiple paths at once, use a
- * {@link org.eclipse.jgit.treewalk.TreeWalk} and obtain the current entry's
- * information from its getter methods.
- */
-@Deprecated
-public class GitlinkTreeEntry extends TreeEntry {
-
-	/**
-	 * Construct a {@link GitlinkTreeEntry} with the specified name and SHA-1 in
-	 * the specified parent
-	 *
-	 * @param parent
-	 * @param id
-	 * @param nameUTF8
-	 */
-	public GitlinkTreeEntry(final Tree parent, final ObjectId id,
-			final byte[] nameUTF8) {
-		super(parent, id, nameUTF8);
-	}
-
-	public FileMode getMode() {
-		return FileMode.GITLINK;
-	}
-
-	@Override
-	public String toString() {
-		final StringBuilder r = new StringBuilder();
-		r.append(ObjectId.toString(getId()));
-		r.append(" G "); //$NON-NLS-1$
-		r.append(getFullName());
-		return r.toString();
-	}
-}
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 a7a67a8..0b5efd7 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java
@@ -44,21 +44,58 @@
 
 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.Paths.compare;
+import static org.eclipse.jgit.util.Paths.compareSameName;
 import static org.eclipse.jgit.util.RawParseUtils.nextLF;
 import static org.eclipse.jgit.util.RawParseUtils.parseBase10;
 
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
 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.
@@ -99,31 +136,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);
 	}
 
 	/**
@@ -184,62 +325,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);
+		}
 	}
 
 	/**
@@ -250,36 +446,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);
+		}
 	}
 
 	/**
@@ -290,50 +500,47 @@ 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);
-
-		if ((ptr = match(raw, ptr, tagger)) > 0) {
-			if ((ptr = personIdent(raw, ptr)) < 0 || raw[ptr++] != '\n')
-				throw new CorruptObjectException(
-						JGitText.get().corruptObjectInvalidTagger);
 		}
-	}
+		bufPtr.value = nextLF(raw, bufPtr.value);
 
-	private static int lastPathChar(final int mode) {
-		return FileMode.TREE.equals(mode) ? '/' : '\0';
-	}
-
-	private static int pathCompare(final byte[] raw, int aPos, final int aEnd,
-			final int aMode, int bPos, final int bEnd, final int bMode) {
-		while (aPos < aEnd && bPos < bEnd) {
-			final int cmp = (raw[aPos++] & 0xff) - (raw[bPos++] & 0xff);
-			if (cmp != 0)
-				return cmp;
+		if (match(raw, tagger)) {
+			checkPersonIdent(raw, id);
 		}
-
-		if (aPos < aEnd)
-			return (raw[aPos] & 0xff) - lastPathChar(bMode);
-		if (bPos < bEnd)
-			return lastPathChar(aMode) - (raw[bPos] & 0xff);
-		return 0;
 	}
 
 	private static boolean duplicateName(final byte[] raw,
@@ -363,8 +570,9 @@ private static boolean duplicateName(final byte[] raw,
 			if (nextNamePos + 1 == nextPtr)
 				return false;
 
-			final int cmp = pathCompare(raw, thisNamePos, thisNameEnd,
-					FileMode.TREE.getBits(), nextNamePos, nextPtr - 1, nextMode);
+			int cmp = compareSameName(
+					raw, thisNamePos, thisNameEnd,
+					raw, nextNamePos, nextPtr - 1, nextMode);
 			if (cmp < 0)
 				return false;
 			else if (cmp == 0)
@@ -382,7 +590,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;
@@ -393,74 +617,90 @@ 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(
+				int cmp = compare(
+						raw, lastNameB, lastNameE, lastMode,
+						raw, thisNameB, ptr, thisMode);
+				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)));
@@ -469,6 +709,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>
@@ -519,73 +779,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]) {
@@ -622,7 +891,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)) {
@@ -643,12 +914,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) {
@@ -658,33 +932,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]
@@ -692,28 +969,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;
 		}
 	}
@@ -766,6 +1046,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'));
@@ -790,58 +1079,6 @@ public void checkBlob(final byte[] raw) throws CorruptObjectException {
 
 	private String normalize(byte[] raw, int ptr, int end) {
 		String n = RawParseUtils.decode(raw, ptr, end).toLowerCase(Locale.US);
-		return macosx ? Normalizer.normalize(n) : n;
-	}
-
-	private static class Normalizer {
-		// TODO Simplify invocation to Normalizer after dropping Java 5.
-		private static final Method normalize;
-		private static final Object nfc;
-		static {
-			Method method;
-			Object formNfc;
-			try {
-				Class<?> formClazz = Class.forName("java.text.Normalizer$Form"); //$NON-NLS-1$
-				formNfc = formClazz.getField("NFC").get(null); //$NON-NLS-1$
-				method = Class.forName("java.text.Normalizer") //$NON-NLS-1$
-					.getMethod("normalize", CharSequence.class, formClazz); //$NON-NLS-1$
-			} catch (ClassNotFoundException e) {
-				method = null;
-				formNfc = null;
-			} catch (NoSuchFieldException e) {
-				method = null;
-				formNfc = null;
-			} catch (NoSuchMethodException e) {
-				method = null;
-				formNfc = null;
-			} catch (SecurityException e) {
-				method = null;
-				formNfc = null;
-			} catch (IllegalArgumentException e) {
-				method = null;
-				formNfc = null;
-			} catch (IllegalAccessException e) {
-				method = null;
-				formNfc = null;
-			}
-			normalize = method;
-			nfc = formNfc;
-		}
-
-		static String normalize(String in) {
-			if (normalize == null)
-				return in;
-			try {
-				return (String) normalize.invoke(null, in, nfc);
-			} catch (IllegalAccessException e) {
-				return in;
-			} catch (InvocationTargetException e) {
-				if (e.getCause() instanceof RuntimeException)
-					throw (RuntimeException) e.getCause();
-				if (e.getCause() instanceof Error)
-					throw (Error) e.getCause();
-				return in;
-			}
-		}
+		return macosx ? Normalizer.normalize(n, Normalizer.Form.NFC) : n;
 	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdOwnerMap.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdOwnerMap.java
index 95b16d9..442261c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdOwnerMap.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdOwnerMap.java
@@ -67,8 +67,8 @@
  * @param <V>
  *            type of subclass of ObjectId that will be stored in the map.
  */
-public class ObjectIdOwnerMap<V extends ObjectIdOwnerMap.Entry> implements
-		Iterable<V> {
+public class ObjectIdOwnerMap<V extends ObjectIdOwnerMap.Entry>
+		implements Iterable<V>, ObjectIdSet {
 	/** Size of the initial directory, will grow as necessary. */
 	private static final int INITIAL_DIRECTORY = 1024;
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdRef.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdRef.java
index f481c77..c286f5e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdRef.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdRef.java
@@ -44,6 +44,9 @@
 
 package org.eclipse.jgit.lib;
 
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.annotations.Nullable;
+
 /** A {@link Ref} that points directly at an {@link ObjectId}. */
 public abstract class ObjectIdRef implements Ref {
 	/** Any reference whose peeled value is not yet known. */
@@ -56,13 +59,15 @@ public static class Unpeeled extends ObjectIdRef {
 		 * @param name
 		 *            name of this ref.
 		 * @param id
-		 *            current value of the ref. May be null to indicate a ref
-		 *            that does not exist yet.
+		 *            current value of the ref. May be {@code null} to indicate
+		 *            a ref that does not exist yet.
 		 */
-		public Unpeeled(Storage st, String name, ObjectId id) {
+		public Unpeeled(@NonNull Storage st, @NonNull String name,
+				@Nullable ObjectId id) {
 			super(st, name, id);
 		}
 
+		@Nullable
 		public ObjectId getPeeledObjectId() {
 			return null;
 		}
@@ -88,11 +93,13 @@ public static class PeeledTag extends ObjectIdRef {
 		 * @param p
 		 *            the first non-tag object that tag {@code id} points to.
 		 */
-		public PeeledTag(Storage st, String name, ObjectId id, ObjectId p) {
+		public PeeledTag(@NonNull Storage st, @NonNull String name,
+				@Nullable ObjectId id, @NonNull ObjectId p) {
 			super(st, name, id);
 			peeledObjectId = p;
 		}
 
+		@NonNull
 		public ObjectId getPeeledObjectId() {
 			return peeledObjectId;
 		}
@@ -112,13 +119,15 @@ public static class PeeledNonTag extends ObjectIdRef {
 		 * @param name
 		 *            name of this ref.
 		 * @param id
-		 *            current value of the ref. May be null to indicate a ref
-		 *            that does not exist yet.
+		 *            current value of the ref. May be {@code null} to indicate
+		 *            a ref that does not exist yet.
 		 */
-		public PeeledNonTag(Storage st, String name, ObjectId id) {
+		public PeeledNonTag(@NonNull Storage st, @NonNull String name,
+				@Nullable ObjectId id) {
 			super(st, name, id);
 		}
 
+		@Nullable
 		public ObjectId getPeeledObjectId() {
 			return null;
 		}
@@ -142,15 +151,17 @@ public boolean isPeeled() {
 	 * @param name
 	 *            name of this ref.
 	 * @param id
-	 *            current value of the ref. May be null to indicate a ref that
-	 *            does not exist yet.
+	 *            current value of the ref. May be {@code null} to indicate a
+	 *            ref that does not exist yet.
 	 */
-	protected ObjectIdRef(Storage st, String name, ObjectId id) {
+	protected ObjectIdRef(@NonNull Storage st, @NonNull String name,
+			@Nullable ObjectId id) {
 		this.name = name;
 		this.storage = st;
 		this.objectId = id;
 	}
 
+	@NonNull
 	public String getName() {
 		return name;
 	}
@@ -159,22 +170,27 @@ public boolean isSymbolic() {
 		return false;
 	}
 
+	@NonNull
 	public Ref getLeaf() {
 		return this;
 	}
 
+	@NonNull
 	public Ref getTarget() {
 		return this;
 	}
 
+	@Nullable
 	public ObjectId getObjectId() {
 		return objectId;
 	}
 
+	@NonNull
 	public Storage getStorage() {
 		return storage;
 	}
 
+	@NonNull
 	@Override
 	public String toString() {
 		StringBuilder r = new StringBuilder();
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/SymlinkTreeEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdSet.java
similarity index 61%
rename from org.eclipse.jgit/src/org/eclipse/jgit/lib/SymlinkTreeEntry.java
rename to org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdSet.java
index c7e41bc..0b58484 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/SymlinkTreeEntry.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdSet.java
@@ -1,6 +1,5 @@
 /*
- * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org>
+ * 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
@@ -45,41 +44,21 @@
 package org.eclipse.jgit.lib;
 
 /**
- * A tree entry representing a symbolic link.
+ * Simple set of ObjectIds.
+ * <p>
+ * Usually backed by a read-only data structure such as
+ * {@link org.eclipse.jgit.internal.storage.file.PackIndex}. Mutable types like
+ * {@link ObjectIdOwnerMap} also implement the interface by checking keys.
  *
- * Note. Java cannot really handle these as file system objects.
- *
- * @deprecated To look up information about a single path, use
- * {@link org.eclipse.jgit.treewalk.TreeWalk#forPath(Repository, String, org.eclipse.jgit.revwalk.RevTree)}.
- * To lookup information about multiple paths at once, use a
- * {@link org.eclipse.jgit.treewalk.TreeWalk} and obtain the current entry's
- * information from its getter methods.
+ * @since 4.2
  */
-@Deprecated
-public class SymlinkTreeEntry extends TreeEntry {
-
+public interface ObjectIdSet {
 	/**
-	 * Construct a {@link SymlinkTreeEntry} with the specified name and SHA-1 in
-	 * the specified parent
+	 * Returns true if the objectId is contained within the collection.
 	 *
-	 * @param parent
-	 * @param id
-	 * @param nameUTF8
+	 * @param objectId
+	 *            the objectId to find
+	 * @return whether the collection contains the objectId.
 	 */
-	public SymlinkTreeEntry(final Tree parent, final ObjectId id,
-			final byte[] nameUTF8) {
-		super(parent, id, nameUTF8);
-	}
-
-	public FileMode getMode() {
-		return FileMode.SYMLINK;
-	}
-
-	public String toString() {
-		final StringBuilder r = new StringBuilder();
-		r.append(ObjectId.toString(getId()));
-		r.append(" S "); //$NON-NLS-1$
-		r.append(getFullName());
-		return r.toString();
-	}
+	boolean contains(AnyObjectId objectId);
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdSubclassMap.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdSubclassMap.java
index 48aa109..faed64b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdSubclassMap.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectIdSubclassMap.java
@@ -60,7 +60,8 @@
  * @param <V>
  *            type of subclass of ObjectId that will be stored in the map.
  */
-public class ObjectIdSubclassMap<V extends ObjectId> implements Iterable<V> {
+public class ObjectIdSubclassMap<V extends ObjectId>
+		implements Iterable<V>, ObjectIdSet {
 	private static final int INITIAL_TABLE_SIZE = 2048;
 
 	int size;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Ref.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Ref.java
index f119c44..a78a90f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Ref.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Ref.java
@@ -43,6 +43,9 @@
 
 package org.eclipse.jgit.lib;
 
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.annotations.Nullable;
+
 /**
  * Pairing of a name and the {@link ObjectId} it currently has.
  * <p>
@@ -126,6 +129,7 @@ public boolean isPacked() {
 	 *
 	 * @return name of this ref.
 	 */
+	@NonNull
 	public String getName();
 
 	/**
@@ -156,6 +160,7 @@ public boolean isPacked() {
 	 *
 	 * @return the reference that actually stores the ObjectId value.
 	 */
+	@NonNull
 	public abstract Ref getLeaf();
 
 	/**
@@ -170,22 +175,27 @@ public boolean isPacked() {
 	 *
 	 * @return the target reference, or {@code this}.
 	 */
+	@NonNull
 	public abstract Ref getTarget();
 
 	/**
 	 * Cached value of this ref.
 	 *
-	 * @return the value of this ref at the last time we read it.
+	 * @return the value of this ref at the last time we read it. May be
+	 *         {@code null} to indicate a ref that does not exist yet or a
+	 *         symbolic ref pointing to an unborn branch.
 	 */
+	@Nullable
 	public abstract ObjectId getObjectId();
 
 	/**
 	 * Cached value of <code>ref^{}</code> (the ref peeled to commit).
 	 *
 	 * @return if this ref is an annotated tag the id of the commit (or tree or
-	 *         blob) that the annotated tag refers to; null if this ref does not
-	 *         refer to an annotated tag.
+	 *         blob) that the annotated tag refers to; {@code null} if this ref
+	 *         does not refer to an annotated tag.
 	 */
+	@Nullable
 	public abstract ObjectId getPeeledObjectId();
 
 	/**
@@ -201,5 +211,6 @@ public boolean isPacked() {
 	 *
 	 * @return type of ref.
 	 */
+	@NonNull
 	public abstract Storage getStorage();
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java
index 986666f..c0c3862 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java
@@ -82,8 +82,10 @@ public abstract class RefDatabase {
 	 * <p>
 	 * If the reference is nested deeper than this depth, the implementation
 	 * should either fail, or at least claim the reference does not exist.
+	 *
+	 * @since 4.2
 	 */
-	protected static final int MAX_SYMBOLIC_REF_DEPTH = 5;
+	public static final int MAX_SYMBOLIC_REF_DEPTH = 5;
 
 	/** Magic value for {@link #getRefs(String)} to return all references. */
 	public static final String ALL = "";//$NON-NLS-1$
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefWriter.java
index 747fa62..3a02b22 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefWriter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefWriter.java
@@ -119,13 +119,20 @@ public void writeInfoRefs() throws IOException {
 				continue;
 			}
 
-			r.getObjectId().copyTo(tmp, w);
+			ObjectId objectId = r.getObjectId();
+			if (objectId == null) {
+				// Symrefs to unborn branches aren't advertised in the info/refs
+				// file.
+				continue;
+			}
+			objectId.copyTo(tmp, w);
 			w.write('\t');
 			w.write(r.getName());
 			w.write('\n');
 
-			if (r.getPeeledObjectId() != null) {
-				r.getPeeledObjectId().copyTo(tmp, w);
+			ObjectId peeledObjectId = r.getPeeledObjectId();
+			if (peeledObjectId != null) {
+				peeledObjectId.copyTo(tmp, w);
 				w.write('\t');
 				w.write(r.getName());
 				w.write("^{}\n"); //$NON-NLS-1$
@@ -167,14 +174,21 @@ public void writePackedRefs() throws IOException {
 			if (r.getStorage() != Ref.Storage.PACKED)
 				continue;
 
-			r.getObjectId().copyTo(tmp, w);
+			ObjectId objectId = r.getObjectId();
+			if (objectId == null) {
+				// A packed ref cannot be a symref, let alone a symref
+				// to an unborn branch.
+				throw new NullPointerException();
+			}
+			objectId.copyTo(tmp, w);
 			w.write(' ');
 			w.write(r.getName());
 			w.write('\n');
 
-			if (r.getPeeledObjectId() != null) {
+			ObjectId peeledObjectId = r.getPeeledObjectId();
+			if (peeledObjectId != null) {
 				w.write('^');
-				r.getPeeledObjectId().copyTo(tmp, w);
+				peeledObjectId.copyTo(tmp, w);
 				w.write('\n');
 			}
 		}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java
index 49a970d..f826613 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java
@@ -911,12 +911,16 @@ public String toString() {
 	@Nullable
 	public String getFullBranch() throws IOException {
 		Ref head = getRef(Constants.HEAD);
-		if (head == null)
+		if (head == null) {
 			return null;
-		if (head.isSymbolic())
+		}
+		if (head.isSymbolic()) {
 			return head.getTarget().getName();
-		if (head.getObjectId() != null)
-			return head.getObjectId().name();
+		}
+		ObjectId objectId = head.getObjectId();
+		if (objectId != null) {
+			return objectId.name();
+		}
 		return null;
 	}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/SymbolicRef.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/SymbolicRef.java
index 43b1510..eeab921 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/SymbolicRef.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/SymbolicRef.java
@@ -43,6 +43,9 @@
 
 package org.eclipse.jgit.lib;
 
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.annotations.Nullable;
+
 /**
  * A reference that indirectly points at another {@link Ref}.
  * <p>
@@ -62,11 +65,12 @@ public class SymbolicRef implements Ref {
 	 * @param target
 	 *            the ref we reference and derive our value from.
 	 */
-	public SymbolicRef(String refName, Ref target) {
+	public SymbolicRef(@NonNull String refName, @NonNull Ref target) {
 		this.name = refName;
 		this.target = target;
 	}
 
+	@NonNull
 	public String getName() {
 		return name;
 	}
@@ -75,6 +79,7 @@ public boolean isSymbolic() {
 		return true;
 	}
 
+	@NonNull
 	public Ref getLeaf() {
 		Ref dst = getTarget();
 		while (dst.isSymbolic())
@@ -82,18 +87,22 @@ public Ref getLeaf() {
 		return dst;
 	}
 
+	@NonNull
 	public Ref getTarget() {
 		return target;
 	}
 
+	@Nullable
 	public ObjectId getObjectId() {
 		return getLeaf().getObjectId();
 	}
 
+	@NonNull
 	public Storage getStorage() {
 		return Storage.LOOSE;
 	}
 
+	@Nullable
 	public ObjectId getPeeledObjectId() {
 		return getLeaf().getPeeledObjectId();
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Tree.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Tree.java
deleted file mode 100644
index 43bd489..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Tree.java
+++ /dev/null
@@ -1,601 +0,0 @@
-/*
- * Copyright (C) 2007, Robin Rosenberg <me@lathund.dewire.com>
- * Copyright (C) 2007-2008, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org>
- * and other copyright owners as documented in the project's IP log.
- *
- * This program and the accompanying materials are made available
- * under the terms of the Eclipse Distribution License v1.0 which
- * accompanies this distribution, is reproduced below, and is
- * available at http://www.eclipse.org/org/documents/edl-v10.php
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or
- * without modification, are permitted provided that the following
- * conditions are met:
- *
- * - Redistributions of source code must retain the above copyright
- *   notice, this list of conditions and the following disclaimer.
- *
- * - Redistributions in binary form must reproduce the above
- *   copyright notice, this list of conditions and the following
- *   disclaimer in the documentation and/or other materials provided
- *   with the distribution.
- *
- * - Neither the name of the Eclipse Foundation, Inc. nor the
- *   names of its contributors may be used to endorse or promote
- *   products derived from this software without specific prior
- *   written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
- * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
- * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
- * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
- * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
- * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
- * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
- * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package org.eclipse.jgit.lib;
-
-import java.io.IOException;
-import java.text.MessageFormat;
-
-import org.eclipse.jgit.errors.CorruptObjectException;
-import org.eclipse.jgit.errors.EntryExistsException;
-import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.errors.ObjectWritingException;
-import org.eclipse.jgit.internal.JGitText;
-import org.eclipse.jgit.util.RawParseUtils;
-
-/**
- * A representation of a Git tree entry. A Tree is a directory in Git.
- *
- * @deprecated To look up information about a single path, use
- * {@link org.eclipse.jgit.treewalk.TreeWalk#forPath(Repository, String, org.eclipse.jgit.revwalk.RevTree)}.
- * To lookup information about multiple paths at once, use a
- * {@link org.eclipse.jgit.treewalk.TreeWalk} and obtain the current entry's
- * information from its getter methods.
- */
-@Deprecated
-public class Tree extends TreeEntry {
-	private static final TreeEntry[] EMPTY_TREE = {};
-
-	/**
-	 * Compare two names represented as bytes. Since git treats names of trees and
-	 * blobs differently we have one parameter that represents a '/' for trees. For
-	 * other objects the value should be NUL. The names are compare by their positive
-	 * byte value (0..255).
-	 *
-	 * A blob and a tree with the same name will not compare equal.
-	 *
-	 * @param a name
-	 * @param b name
-	 * @param lasta '/' if a is a tree, else NUL
-	 * @param lastb '/' if b is a tree, else NUL
-	 *
-	 * @return &lt; 0 if a is sorted before b, 0 if they are the same, else b
-	 */
-	public static final int compareNames(final byte[] a, final byte[] b, final int lasta,final int lastb) {
-		return compareNames(a, b, 0, b.length, lasta, lastb);
-	}
-
-	private static final int compareNames(final byte[] a, final byte[] nameUTF8,
-			final int nameStart, final int nameEnd, final int lasta, int lastb) {
-		int j,k;
-		for (j = 0, k = nameStart; j < a.length && k < nameEnd; j++, k++) {
-			final int aj = a[j] & 0xff;
-			final int bk = nameUTF8[k] & 0xff;
-			if (aj < bk)
-				return -1;
-			else if (aj > bk)
-				return 1;
-		}
-		if (j < a.length) {
-			int aj = a[j]&0xff;
-			if (aj < lastb)
-				return -1;
-			else if (aj > lastb)
-				return 1;
-			else
-				if (j == a.length - 1)
-					return 0;
-				else
-					return -1;
-		}
-		if (k < nameEnd) {
-			int bk = nameUTF8[k] & 0xff;
-			if (lasta < bk)
-				return -1;
-			else if (lasta > bk)
-				return 1;
-			else
-				if (k == nameEnd - 1)
-					return 0;
-				else
-					return 1;
-		}
-		if (lasta < lastb)
-			return -1;
-		else if (lasta > lastb)
-			return 1;
-
-		final int namelength = nameEnd - nameStart;
-		if (a.length == namelength)
-			return 0;
-		else if (a.length < namelength)
-			return -1;
-		else
-			return 1;
-	}
-
-	private static final byte[] substring(final byte[] s, final int nameStart,
-			final int nameEnd) {
-		if (nameStart == 0 && nameStart == s.length)
-			return s;
-		final byte[] n = new byte[nameEnd - nameStart];
-		System.arraycopy(s, nameStart, n, 0, n.length);
-		return n;
-	}
-
-	private static final int binarySearch(final TreeEntry[] entries,
-			final byte[] nameUTF8, final int nameUTF8last, final int nameStart, final int nameEnd) {
-		if (entries.length == 0)
-			return -1;
-		int high = entries.length;
-		int low = 0;
-		do {
-			final int mid = (low + high) >>> 1;
-			final int cmp = compareNames(entries[mid].getNameUTF8(), nameUTF8,
-					nameStart, nameEnd, TreeEntry.lastChar(entries[mid]), nameUTF8last);
-			if (cmp < 0)
-				low = mid + 1;
-			else if (cmp == 0)
-				return mid;
-			else
-				high = mid;
-		} while (low < high);
-		return -(low + 1);
-	}
-
-	private final Repository db;
-
-	private TreeEntry[] contents;
-
-	/**
-	 * Constructor for a new Tree
-	 *
-	 * @param repo The repository that owns the Tree.
-	 */
-	public Tree(final Repository repo) {
-		super(null, null, null);
-		db = repo;
-		contents = EMPTY_TREE;
-	}
-
-	/**
-	 * Construct a Tree object with known content and hash value
-	 *
-	 * @param repo
-	 * @param myId
-	 * @param raw
-	 * @throws IOException
-	 */
-	public Tree(final Repository repo, final ObjectId myId, final byte[] raw)
-			throws IOException {
-		super(null, myId, null);
-		db = repo;
-		readTree(raw);
-	}
-
-	/**
-	 * Construct a new Tree under another Tree
-	 *
-	 * @param parent
-	 * @param nameUTF8
-	 */
-	public Tree(final Tree parent, final byte[] nameUTF8) {
-		super(parent, null, nameUTF8);
-		db = parent.getRepository();
-		contents = EMPTY_TREE;
-	}
-
-	/**
-	 * Construct a Tree with a known SHA-1 under another tree. Data is not yet
-	 * specified and will have to be loaded on demand.
-	 *
-	 * @param parent
-	 * @param id
-	 * @param nameUTF8
-	 */
-	public Tree(final Tree parent, final ObjectId id, final byte[] nameUTF8) {
-		super(parent, id, nameUTF8);
-		db = parent.getRepository();
-	}
-
-	public FileMode getMode() {
-		return FileMode.TREE;
-	}
-
-	/**
-	 * @return true if this Tree is the top level Tree.
-	 */
-	public boolean isRoot() {
-		return getParent() == null;
-	}
-
-	public Repository getRepository() {
-		return db;
-	}
-
-	/**
-	 * @return true of the data of this Tree is loaded
-	 */
-	public boolean isLoaded() {
-		return contents != null;
-	}
-
-	/**
-	 * Forget the in-memory data for this tree.
-	 */
-	public void unload() {
-		if (isModified())
-			throw new IllegalStateException(JGitText.get().cannotUnloadAModifiedTree);
-		contents = null;
-	}
-
-	/**
-	 * Adds a new or existing file with the specified name to this tree.
-	 * Trees are added if necessary as the name may contain '/':s.
-	 *
-	 * @param name Name
-	 * @return a {@link FileTreeEntry} for the added file.
-	 * @throws IOException
-	 */
-	public FileTreeEntry addFile(final String name) throws IOException {
-		return addFile(Repository.gitInternalSlash(Constants.encode(name)), 0);
-	}
-
-	/**
-	 * Adds a new or existing file with the specified name to this tree.
-	 * Trees are added if necessary as the name may contain '/':s.
-	 *
-	 * @param s an array containing the name
-	 * @param offset when the name starts in the tree.
-	 *
-	 * @return a {@link FileTreeEntry} for the added file.
-	 * @throws IOException
-	 */
-	public FileTreeEntry addFile(final byte[] s, final int offset)
-			throws IOException {
-		int slash;
-		int p;
-
-		for (slash = offset; slash < s.length && s[slash] != '/'; slash++) {
-			// search for path component terminator
-		}
-
-		ensureLoaded();
-		byte xlast = slash<s.length ? (byte)'/' : 0;
-		p = binarySearch(contents, s, xlast, offset, slash);
-		if (p >= 0 && slash < s.length && contents[p] instanceof Tree)
-			return ((Tree) contents[p]).addFile(s, slash + 1);
-
-		final byte[] newName = substring(s, offset, slash);
-		if (p >= 0)
-			throw new EntryExistsException(RawParseUtils.decode(newName));
-		else if (slash < s.length) {
-			final Tree t = new Tree(this, newName);
-			insertEntry(p, t);
-			return t.addFile(s, slash + 1);
-		} else {
-			final FileTreeEntry f = new FileTreeEntry(this, null, newName,
-					false);
-			insertEntry(p, f);
-			return f;
-		}
-	}
-
-	/**
-	 * Adds a new or existing Tree with the specified name to this tree.
-	 * Trees are added if necessary as the name may contain '/':s.
-	 *
-	 * @param name Name
-	 * @return a {@link FileTreeEntry} for the added tree.
-	 * @throws IOException
-	 */
-	public Tree addTree(final String name) throws IOException {
-		return addTree(Repository.gitInternalSlash(Constants.encode(name)), 0);
-	}
-
-	/**
-	 * Adds a new or existing Tree with the specified name to this tree.
-	 * Trees are added if necessary as the name may contain '/':s.
-	 *
-	 * @param s an array containing the name
-	 * @param offset when the name starts in the tree.
-	 *
-	 * @return a {@link FileTreeEntry} for the added tree.
-	 * @throws IOException
-	 */
-	public Tree addTree(final byte[] s, final int offset) throws IOException {
-		int slash;
-		int p;
-
-		for (slash = offset; slash < s.length && s[slash] != '/'; slash++) {
-			// search for path component terminator
-		}
-
-		ensureLoaded();
-		p = binarySearch(contents, s, (byte)'/', offset, slash);
-		if (p >= 0 && slash < s.length && contents[p] instanceof Tree)
-			return ((Tree) contents[p]).addTree(s, slash + 1);
-
-		final byte[] newName = substring(s, offset, slash);
-		if (p >= 0)
-			throw new EntryExistsException(RawParseUtils.decode(newName));
-
-		final Tree t = new Tree(this, newName);
-		insertEntry(p, t);
-		return slash == s.length ? t : t.addTree(s, slash + 1);
-	}
-
-	/**
-	 * Add the specified tree entry to this tree.
-	 *
-	 * @param e
-	 * @throws IOException
-	 */
-	public void addEntry(final TreeEntry e) throws IOException {
-		final int p;
-
-		ensureLoaded();
-		p = binarySearch(contents, e.getNameUTF8(), TreeEntry.lastChar(e), 0, e.getNameUTF8().length);
-		if (p < 0) {
-			e.attachParent(this);
-			insertEntry(p, e);
-		} else {
-			throw new EntryExistsException(e.getName());
-		}
-	}
-
-	private void insertEntry(int p, final TreeEntry e) {
-		final TreeEntry[] c = contents;
-		final TreeEntry[] n = new TreeEntry[c.length + 1];
-		p = -(p + 1);
-		for (int k = c.length - 1; k >= p; k--)
-			n[k + 1] = c[k];
-		n[p] = e;
-		for (int k = p - 1; k >= 0; k--)
-			n[k] = c[k];
-		contents = n;
-		setModified();
-	}
-
-	void removeEntry(final TreeEntry e) {
-		final TreeEntry[] c = contents;
-		final int p = binarySearch(c, e.getNameUTF8(), TreeEntry.lastChar(e), 0,
-				e.getNameUTF8().length);
-		if (p >= 0) {
-			final TreeEntry[] n = new TreeEntry[c.length - 1];
-			for (int k = c.length - 1; k > p; k--)
-				n[k - 1] = c[k];
-			for (int k = p - 1; k >= 0; k--)
-				n[k] = c[k];
-			contents = n;
-			setModified();
-		}
-	}
-
-	/**
-	 * @return number of members in this tree
-	 * @throws IOException
-	 */
-	public int memberCount() throws IOException {
-		ensureLoaded();
-		return contents.length;
-	}
-
-	/**
-	 * Return all members of the tree sorted in Git order.
-	 *
-	 * Entries are sorted by the numerical unsigned byte
-	 * values with (sub)trees having an implicit '/'. An
-	 * example of a tree with three entries. a:b is an
-	 * actual file name here.
-	 *
-	 * <p>
-	 * 100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391    a.b
-	 * 040000 tree 4277b6e69d25e5efa77c455340557b384a4c018a    a
-	 * 100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391    a:b
-	 *
-	 * @return all entries in this Tree, sorted.
-	 * @throws IOException
-	 */
-	public TreeEntry[] members() throws IOException {
-		ensureLoaded();
-		final TreeEntry[] c = contents;
-		if (c.length != 0) {
-			final TreeEntry[] r = new TreeEntry[c.length];
-			for (int k = c.length - 1; k >= 0; k--)
-				r[k] = c[k];
-			return r;
-		} else
-			return c;
-	}
-
-	private boolean exists(final String s, byte slast) throws IOException {
-		return findMember(s, slast) != null;
-	}
-
-	/**
-	 * @param path to the tree.
-	 * @return true if a tree with the specified path can be found under this
-	 *         tree.
-	 * @throws IOException
-	 */
-	public boolean existsTree(String path) throws IOException {
-		return exists(path,(byte)'/');
-	}
-
-	/**
-	 * @param path of the non-tree entry.
-	 * @return true if a blob, symlink, or gitlink with the specified name
-	 *         can be found under this tree.
-	 * @throws IOException
-	 */
-	public boolean existsBlob(String path) throws IOException {
-		return exists(path,(byte)0);
-	}
-
-	private TreeEntry findMember(final String s, byte slast) throws IOException {
-		return findMember(Repository.gitInternalSlash(Constants.encode(s)), slast, 0);
-	}
-
-	private TreeEntry findMember(final byte[] s, final byte slast, final int offset)
-			throws IOException {
-		int slash;
-		int p;
-
-		for (slash = offset; slash < s.length && s[slash] != '/'; slash++) {
-			// search for path component terminator
-		}
-
-		ensureLoaded();
-		byte xlast = slash<s.length ? (byte)'/' : slast;
-		p = binarySearch(contents, s, xlast, offset, slash);
-		if (p >= 0) {
-			final TreeEntry r = contents[p];
-			if (slash < s.length-1)
-				return r instanceof Tree ? ((Tree) r).findMember(s, slast, slash + 1)
-						: null;
-			return r;
-		}
-		return null;
-	}
-
-	/**
-	 * @param s
-	 *            blob name
-	 * @return a {@link TreeEntry} representing an object with the specified
-	 *         relative path.
-	 * @throws IOException
-	 */
-	public TreeEntry findBlobMember(String s) throws IOException {
-		return findMember(s,(byte)0);
-	}
-
-	/**
-	 * @param s Tree Name
-	 * @return a Tree with the name s or null
-	 * @throws IOException
-	 */
-	public TreeEntry findTreeMember(String s) throws IOException {
-		return findMember(s,(byte)'/');
-	}
-
-	private void ensureLoaded() throws IOException, MissingObjectException {
-		if (!isLoaded()) {
-			ObjectLoader ldr = db.open(getId(), Constants.OBJ_TREE);
-			readTree(ldr.getCachedBytes());
-		}
-	}
-
-	private void readTree(final byte[] raw) throws IOException {
-		final int rawSize = raw.length;
-		int rawPtr = 0;
-		TreeEntry[] temp;
-		int nextIndex = 0;
-
-		while (rawPtr < rawSize) {
-			while (rawPtr < rawSize && raw[rawPtr] != 0)
-				rawPtr++;
-			rawPtr++;
-			rawPtr += Constants.OBJECT_ID_LENGTH;
-			nextIndex++;
-		}
-
-		temp = new TreeEntry[nextIndex];
-		rawPtr = 0;
-		nextIndex = 0;
-		while (rawPtr < rawSize) {
-			int c = raw[rawPtr++];
-			if (c < '0' || c > '7')
-				throw new CorruptObjectException(getId(), JGitText.get().corruptObjectInvalidEntryMode);
-			int mode = c - '0';
-			for (;;) {
-				c = raw[rawPtr++];
-				if (' ' == c)
-					break;
-				else if (c < '0' || c > '7')
-					throw new CorruptObjectException(getId(), JGitText.get().corruptObjectInvalidMode);
-				mode <<= 3;
-				mode += c - '0';
-			}
-
-			int nameLen = 0;
-			while (raw[rawPtr + nameLen] != 0)
-				nameLen++;
-			final byte[] name = new byte[nameLen];
-			System.arraycopy(raw, rawPtr, name, 0, nameLen);
-			rawPtr += nameLen + 1;
-
-			final ObjectId id = ObjectId.fromRaw(raw, rawPtr);
-			rawPtr += Constants.OBJECT_ID_LENGTH;
-
-			final TreeEntry ent;
-			if (FileMode.REGULAR_FILE.equals(mode))
-				ent = new FileTreeEntry(this, id, name, false);
-			else if (FileMode.EXECUTABLE_FILE.equals(mode))
-				ent = new FileTreeEntry(this, id, name, true);
-			else if (FileMode.TREE.equals(mode))
-				ent = new Tree(this, id, name);
-			else if (FileMode.SYMLINK.equals(mode))
-				ent = new SymlinkTreeEntry(this, id, name);
-			else if (FileMode.GITLINK.equals(mode))
-				ent = new GitlinkTreeEntry(this, id, name);
-			else
-				throw new CorruptObjectException(getId(), MessageFormat.format(
-						JGitText.get().corruptObjectInvalidMode2, Integer.toOctalString(mode)));
-			temp[nextIndex++] = ent;
-		}
-
-		contents = temp;
-	}
-
-	/**
-	 * Format this Tree in canonical format.
-	 *
-	 * @return canonical encoding of the tree object.
-	 * @throws IOException
-	 *             the tree cannot be loaded, or its not in a writable state.
-	 */
-	public byte[] format() throws IOException {
-		TreeFormatter fmt = new TreeFormatter();
-		for (TreeEntry e : members()) {
-			ObjectId id = e.getId();
-			if (id == null)
-				throw new ObjectWritingException(MessageFormat.format(JGitText
-						.get().objectAtPathDoesNotHaveId, e.getFullName()));
-
-			fmt.append(e.getNameUTF8(), e.getMode(), id);
-		}
-		return fmt.toByteArray();
-	}
-
-	public String toString() {
-		final StringBuilder r = new StringBuilder();
-		r.append(ObjectId.toString(getId()));
-		r.append(" T "); //$NON-NLS-1$
-		r.append(getFullName());
-		return r.toString();
-	}
-
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TreeEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TreeEntry.java
deleted file mode 100644
index a1ffa68..0000000
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TreeEntry.java
+++ /dev/null
@@ -1,256 +0,0 @@
-/*
- * Copyright (C) 2007-2008, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org>
- * and other copyright owners as documented in the project's IP log.
- *
- * This program and the accompanying materials are made available
- * under the terms of the Eclipse Distribution License v1.0 which
- * accompanies this distribution, is reproduced below, and is
- * available at http://www.eclipse.org/org/documents/edl-v10.php
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or
- * without modification, are permitted provided that the following
- * conditions are met:
- *
- * - Redistributions of source code must retain the above copyright
- *   notice, this list of conditions and the following disclaimer.
- *
- * - Redistributions in binary form must reproduce the above
- *   copyright notice, this list of conditions and the following
- *   disclaimer in the documentation and/or other materials provided
- *   with the distribution.
- *
- * - Neither the name of the Eclipse Foundation, Inc. nor the
- *   names of its contributors may be used to endorse or promote
- *   products derived from this software without specific prior
- *   written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
- * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
- * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
- * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
- * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
- * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
- * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
- * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package org.eclipse.jgit.lib;
-
-import java.io.IOException;
-
-import org.eclipse.jgit.util.RawParseUtils;
-
-/**
- * This class represents an entry in a tree, like a blob or another tree.
- *
- * @deprecated To look up information about a single path, use
- * {@link org.eclipse.jgit.treewalk.TreeWalk#forPath(Repository, String, org.eclipse.jgit.revwalk.RevTree)}.
- * To lookup information about multiple paths at once, use a
- * {@link org.eclipse.jgit.treewalk.TreeWalk} and obtain the current entry's
- * information from its getter methods.
- */
-@Deprecated
-public abstract class TreeEntry implements Comparable {
-	private byte[] nameUTF8;
-
-	private Tree parent;
-
-	private ObjectId id;
-
-	/**
-	 * Construct a named tree entry.
-	 *
-	 * @param myParent
-	 * @param myId
-	 * @param myNameUTF8
-	 */
-	protected TreeEntry(final Tree myParent, final ObjectId myId,
-			final byte[] myNameUTF8) {
-		nameUTF8 = myNameUTF8;
-		parent = myParent;
-		id = myId;
-	}
-
-	/**
-	 * @return parent of this tree.
-	 */
-	public Tree getParent() {
-		return parent;
-	}
-
-	/**
-	 * Delete this entry.
-	 */
-	public void delete() {
-		getParent().removeEntry(this);
-		detachParent();
-	}
-
-	/**
-	 * Detach this entry from it's parent.
-	 */
-	public void detachParent() {
-		parent = null;
-	}
-
-	void attachParent(final Tree p) {
-		parent = p;
-	}
-
-	/**
-	 * @return the repository owning this entry.
-	 */
-	public Repository getRepository() {
-		return getParent().getRepository();
-	}
-
-	/**
-	 * @return the raw byte name of this entry.
-	 */
-	public byte[] getNameUTF8() {
-		return nameUTF8;
-	}
-
-	/**
-	 * @return the name of this entry.
-	 */
-	public String getName() {
-		if (nameUTF8 != null)
-			return RawParseUtils.decode(nameUTF8);
-		return null;
-	}
-
-	/**
-	 * Rename this entry.
-	 *
-	 * @param n The new name
-	 * @throws IOException
-	 */
-	public void rename(final String n) throws IOException {
-		rename(Constants.encode(n));
-	}
-
-	/**
-	 * Rename this entry.
-	 *
-	 * @param n The new name
-	 * @throws IOException
-	 */
-	public void rename(final byte[] n) throws IOException {
-		final Tree t = getParent();
-		if (t != null) {
-			delete();
-		}
-		nameUTF8 = n;
-		if (t != null) {
-			t.addEntry(this);
-		}
-	}
-
-	/**
-	 * @return true if this entry is new or modified since being loaded.
-	 */
-	public boolean isModified() {
-		return getId() == null;
-	}
-
-	/**
-	 * Mark this entry as modified.
-	 */
-	public void setModified() {
-		setId(null);
-	}
-
-	/**
-	 * @return SHA-1 of this tree entry (null for new unhashed entries)
-	 */
-	public ObjectId getId() {
-		return id;
-	}
-
-	/**
-	 * Set (update) the SHA-1 of this entry. Invalidates the id's of all
-	 * entries above this entry as they will have to be recomputed.
-	 *
-	 * @param n SHA-1 for this entry.
-	 */
-	public void setId(final ObjectId n) {
-		// If we have a parent and our id is being cleared or changed then force
-		// the parent's id to become unset as it depends on our id.
-		//
-		final Tree p = getParent();
-		if (p != null && id != n) {
-			if ((id == null && n != null) || (id != null && n == null)
-					|| !id.equals(n)) {
-				p.setId(null);
-			}
-		}
-
-		id = n;
-	}
-
-	/**
-	 * @return repository relative name of this entry
-	 */
-	public String getFullName() {
-		final StringBuilder r = new StringBuilder();
-		appendFullName(r);
-		return r.toString();
-	}
-
-	/**
-	 * @return repository relative name of the entry
-	 * FIXME better encoding
-	 */
-	public byte[] getFullNameUTF8() {
-		return getFullName().getBytes();
-	}
-
-	public int compareTo(final Object o) {
-		if (this == o)
-			return 0;
-		if (o instanceof TreeEntry)
-			return Tree.compareNames(nameUTF8, ((TreeEntry) o).nameUTF8, lastChar(this), lastChar((TreeEntry)o));
-		return -1;
-	}
-
-	/**
-	 * Helper for accessing tree/blob methods.
-	 *
-	 * @param treeEntry
-	 * @return '/' for Tree entries and NUL for non-treeish objects.
-	 */
-	final public static int lastChar(TreeEntry treeEntry) {
-		if (!(treeEntry instanceof Tree))
-			return '\0';
-		else
-			return '/';
-	}
-
-	/**
-	 * @return mode (type of object)
-	 */
-	public abstract FileMode getMode();
-
-	private void appendFullName(final StringBuilder r) {
-		final TreeEntry p = getParent();
-		final String n = getName();
-		if (p != null) {
-			p.appendFullName(r);
-			if (r.length() > 0) {
-				r.append('/');
-			}
-		}
-		if (n != null) {
-			r.append(n);
-		}
-	}
-}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java
index 191f3d8..82cbf36 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeMessageFormatter.java
@@ -46,6 +46,7 @@
 import java.util.List;
 
 import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.util.ChangeIdUtil;
@@ -76,22 +77,22 @@ public String format(List<Ref> refsToMerge, Ref target) {
 		List<String> commits = new ArrayList<String>();
 		List<String> others = new ArrayList<String>();
 		for (Ref ref : refsToMerge) {
-			if (ref.getName().startsWith(Constants.R_HEADS))
+			if (ref.getName().startsWith(Constants.R_HEADS)) {
 				branches.add("'" + Repository.shortenRefName(ref.getName()) //$NON-NLS-1$
 						+ "'"); //$NON-NLS-1$
-
-			else if (ref.getName().startsWith(Constants.R_REMOTES))
+			} else if (ref.getName().startsWith(Constants.R_REMOTES)) {
 				remoteBranches.add("'" //$NON-NLS-1$
 						+ Repository.shortenRefName(ref.getName()) + "'"); //$NON-NLS-1$
-
-			else if (ref.getName().startsWith(Constants.R_TAGS))
+			} else if (ref.getName().startsWith(Constants.R_TAGS)) {
 				tags.add("'" + Repository.shortenRefName(ref.getName()) + "'"); //$NON-NLS-1$ //$NON-NLS-2$
-
-			else if (ref.getName().equals(ref.getObjectId().getName()))
-				commits.add("'" + ref.getName() + "'"); //$NON-NLS-1$ //$NON-NLS-2$
-
-			else
-				others.add(ref.getName());
+			} else {
+				ObjectId objectId = ref.getObjectId();
+				if (objectId != null && ref.getName().equals(objectId.getName())) {
+					commits.add("'" + ref.getName() + "'"); //$NON-NLS-1$ //$NON-NLS-2$
+				} else {
+					others.add(ref.getName());
+				}
+			}
 		}
 
 		List<String> listings = new ArrayList<String>();
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/Merger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/Merger.java
index 983bf5c..bee2d03 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/Merger.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/Merger.java
@@ -1,5 +1,6 @@
 /*
  * Copyright (C) 2008-2013, Google Inc.
+ * Copyright (C) 2016, Laurent Delaigue <laurent.delaigue@obeo.fr>
  * and other copyright owners as documented in the project's IP log.
  *
  * This program and the accompanying materials are made available
@@ -51,9 +52,11 @@
 import org.eclipse.jgit.errors.NoMergeBaseException.MergeBaseFailureReason;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevObject;
@@ -88,6 +91,13 @@ public abstract class Merger {
 	protected RevTree[] sourceTrees;
 
 	/**
+	 * A progress monitor.
+	 *
+	 * @since 4.2
+	 */
+	protected ProgressMonitor monitor = NullProgressMonitor.INSTANCE;
+
+	/**
 	 * Create a new merge instance for a repository.
 	 *
 	 * @param local
@@ -290,4 +300,20 @@ protected AbstractTreeIterator openTree(final AnyObjectId treeId)
 	 * @return resulting tree, if {@link #merge(AnyObjectId[])} returned true.
 	 */
 	public abstract ObjectId getResultTreeId();
+
+	/**
+	 * Set a progress monitor.
+	 *
+	 * @param monitor
+	 *            Monitor to use, can be null to indicate no progress reporting
+	 *            is desired.
+	 * @since 4.2
+	 */
+	public void setProgressMonitor(ProgressMonitor monitor) {
+		if (monitor == null) {
+			this.monitor = NullProgressMonitor.INSTANCE;
+		} else {
+			this.monitor = monitor;
+		}
+	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/notes/NonNoteEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NonNoteEntry.java
index 6a2d44b..362328a 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/notes/NonNoteEntry.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/notes/NonNoteEntry.java
@@ -47,6 +47,7 @@
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.TreeFormatter;
+import org.eclipse.jgit.util.Paths;
 
 /** A tree entry found in a note branch that isn't a valid note. */
 class NonNoteEntry extends ObjectId {
@@ -74,27 +75,8 @@ int treeEntrySize() {
 	}
 
 	int pathCompare(byte[] bBuf, int bPos, int bLen, FileMode bMode) {
-		return pathCompare(name, 0, name.length, mode, //
-				bBuf, bPos, bLen, bMode);
-	}
-
-	private static int pathCompare(final byte[] aBuf, int aPos, final int aEnd,
-			final FileMode aMode, final byte[] bBuf, int bPos, final int bEnd,
-			final FileMode bMode) {
-		while (aPos < aEnd && bPos < bEnd) {
-			int cmp = (aBuf[aPos++] & 0xff) - (bBuf[bPos++] & 0xff);
-			if (cmp != 0)
-				return cmp;
-		}
-
-		if (aPos < aEnd)
-			return (aBuf[aPos] & 0xff) - lastPathChar(bMode);
-		if (bPos < bEnd)
-			return lastPathChar(aMode) - (bBuf[bPos] & 0xff);
-		return 0;
-	}
-
-	private static int lastPathChar(final FileMode mode) {
-		return FileMode.TREE.equals(mode.getBits()) ? '/' : '\0';
+		return Paths.compare(
+				name, 0, name.length, mode.getBits(),
+				bBuf, bPos, bLen, bMode.getBits());
 	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommit.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommit.java
index c23e4e3..e67ada6 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommit.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevCommit.java
@@ -44,12 +44,17 @@
 
 package org.eclipse.jgit.revwalk;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import java.io.IOException;
 import java.nio.charset.Charset;
+import java.nio.charset.IllegalCharsetNameException;
+import java.nio.charset.UnsupportedCharsetException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
+import org.eclipse.jgit.annotations.Nullable;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.lib.AnyObjectId;
@@ -441,12 +446,12 @@ public final PersonIdent getCommitterIdent() {
 	 * @return decoded commit message as a string. Never null.
 	 */
 	public final String getFullMessage() {
-		final byte[] raw = buffer;
-		final int msgB = RawParseUtils.commitMessage(raw, 0);
-		if (msgB < 0)
+		byte[] raw = buffer;
+		int msgB = RawParseUtils.commitMessage(raw, 0);
+		if (msgB < 0) {
 			return ""; //$NON-NLS-1$
-		final Charset enc = RawParseUtils.parseEncoding(raw);
-		return RawParseUtils.decode(enc, raw, msgB, raw.length);
+		}
+		return RawParseUtils.decode(guessEncoding(), raw, msgB, raw.length);
 	}
 
 	/**
@@ -465,16 +470,17 @@ public final String getFullMessage() {
 	 *         spanned multiple lines. Embedded LFs are converted to spaces.
 	 */
 	public final String getShortMessage() {
-		final byte[] raw = buffer;
-		final int msgB = RawParseUtils.commitMessage(raw, 0);
-		if (msgB < 0)
+		byte[] raw = buffer;
+		int msgB = RawParseUtils.commitMessage(raw, 0);
+		if (msgB < 0) {
 			return ""; //$NON-NLS-1$
+		}
 
-		final Charset enc = RawParseUtils.parseEncoding(raw);
-		final int msgE = RawParseUtils.endOfParagraph(raw, msgB);
-		String str = RawParseUtils.decode(enc, raw, msgB, msgE);
-		if (hasLF(raw, msgB, msgE))
+		int msgE = RawParseUtils.endOfParagraph(raw, msgB);
+		String str = RawParseUtils.decode(guessEncoding(), raw, msgB, msgE);
+		if (hasLF(raw, msgB, msgE)) {
 			str = StringUtils.replaceLineBreaksWithSpace(str);
+		}
 		return str;
 	}
 
@@ -488,18 +494,49 @@ static boolean hasLF(final byte[] r, int b, final int e) {
 	/**
 	 * Determine the encoding of the commit message buffer.
 	 * <p>
+	 * Locates the "encoding" header (if present) and returns its value. Due to
+	 * corruption in the wild this may be an invalid encoding name that is not
+	 * recognized by any character encoding library.
+	 * <p>
+	 * If no encoding header is present, null.
+	 *
+	 * @return the preferred encoding of {@link #getRawBuffer()}; or null.
+	 * @since 4.2
+	 */
+	@Nullable
+	public final String getEncodingName() {
+		return RawParseUtils.parseEncodingName(buffer);
+	}
+
+	/**
+	 * Determine the encoding of the commit message buffer.
+	 * <p>
 	 * Locates the "encoding" header (if present) and then returns the proper
 	 * character set to apply to this buffer to evaluate its contents as
 	 * character data.
 	 * <p>
-	 * If no encoding header is present, {@link Constants#CHARSET} is assumed.
+	 * If no encoding header is present {@code UTF-8} is assumed.
 	 *
 	 * @return the preferred encoding of {@link #getRawBuffer()}.
+	 * @throws IllegalCharsetNameException
+	 *             if the character set requested by the encoding header is
+	 *             malformed and unsupportable.
+	 * @throws UnsupportedCharsetException
+	 *             if the JRE does not support the character set requested by
+	 *             the encoding header.
 	 */
 	public final Charset getEncoding() {
 		return RawParseUtils.parseEncoding(buffer);
 	}
 
+	private Charset guessEncoding() {
+		try {
+			return getEncoding();
+		} catch (IllegalCharsetNameException | UnsupportedCharsetException e) {
+			return UTF_8;
+		}
+	}
+
 	/**
 	 * Parse the footer lines (e.g. "Signed-off-by") for machine processing.
 	 * <p>
@@ -529,7 +566,7 @@ public final List<FooterLine> getFooterLines() {
 
 		final int msgB = RawParseUtils.commitMessage(raw, 0);
 		final ArrayList<FooterLine> r = new ArrayList<FooterLine>(4);
-		final Charset enc = getEncoding();
+		final Charset enc = guessEncoding();
 		for (;;) {
 			ptr = RawParseUtils.prevLF(raw, ptr);
 			if (ptr <= msgB)
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTag.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTag.java
index bf2785e..81a54bf 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTag.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTag.java
@@ -45,8 +45,12 @@
 
 package org.eclipse.jgit.revwalk;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import java.io.IOException;
 import java.nio.charset.Charset;
+import java.nio.charset.IllegalCharsetNameException;
+import java.nio.charset.UnsupportedCharsetException;
 
 import org.eclipse.jgit.errors.CorruptObjectException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -162,7 +166,7 @@ void parseCanonical(final RevWalk walk, final byte[] rawTag)
 
 		int p = pos.value += 4; // "tag "
 		final int nameEnd = RawParseUtils.nextLF(rawTag, p) - 1;
-		tagName = RawParseUtils.decode(Constants.CHARSET, rawTag, p, nameEnd);
+		tagName = RawParseUtils.decode(UTF_8, rawTag, p, nameEnd);
 
 		if (walk.isRetainBody())
 			buffer = rawTag;
@@ -207,12 +211,12 @@ public final PersonIdent getTaggerIdent() {
 	 * @return decoded tag message as a string. Never null.
 	 */
 	public final String getFullMessage() {
-		final byte[] raw = buffer;
-		final int msgB = RawParseUtils.tagMessage(raw, 0);
-		if (msgB < 0)
+		byte[] raw = buffer;
+		int msgB = RawParseUtils.tagMessage(raw, 0);
+		if (msgB < 0) {
 			return ""; //$NON-NLS-1$
-		final Charset enc = RawParseUtils.parseEncoding(raw);
-		return RawParseUtils.decode(enc, raw, msgB, raw.length);
+		}
+		return RawParseUtils.decode(guessEncoding(), raw, msgB, raw.length);
 	}
 
 	/**
@@ -231,19 +235,28 @@ public final String getFullMessage() {
 	 *         multiple lines. Embedded LFs are converted to spaces.
 	 */
 	public final String getShortMessage() {
-		final byte[] raw = buffer;
-		final int msgB = RawParseUtils.tagMessage(raw, 0);
-		if (msgB < 0)
+		byte[] raw = buffer;
+		int msgB = RawParseUtils.tagMessage(raw, 0);
+		if (msgB < 0) {
 			return ""; //$NON-NLS-1$
+		}
 
-		final Charset enc = RawParseUtils.parseEncoding(raw);
-		final int msgE = RawParseUtils.endOfParagraph(raw, msgB);
-		String str = RawParseUtils.decode(enc, raw, msgB, msgE);
-		if (RevCommit.hasLF(raw, msgB, msgE))
+		int msgE = RawParseUtils.endOfParagraph(raw, msgB);
+		String str = RawParseUtils.decode(guessEncoding(), raw, msgB, msgE);
+		if (RevCommit.hasLF(raw, msgB, msgE)) {
 			str = StringUtils.replaceLineBreaksWithSpace(str);
+		}
 		return str;
 	}
 
+	private Charset guessEncoding() {
+		try {
+			return RawParseUtils.parseEncoding(buffer);
+		} catch (IllegalCharsetNameException | UnsupportedCharsetException e) {
+			return UTF_8;
+		}
+	}
+
 	/**
 	 * Get a reference to the object this tag was placed on.
 	 * <p>
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java
index 7f9cec7..aa36aeb 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java
@@ -143,7 +143,9 @@ protected final void init(InputStream myIn, OutputStream myOut) {
 		final int timeout = transport.getTimeout();
 		if (timeout > 0) {
 			final Thread caller = Thread.currentThread();
-			myTimer = new InterruptTimer(caller.getName() + "-Timer"); //$NON-NLS-1$
+			if (myTimer == null) {
+				myTimer = new InterruptTimer(caller.getName() + "-Timer"); //$NON-NLS-1$
+			}
 			timeoutIn = new TimeoutInputStream(myIn, myTimer);
 			timeoutOut = new TimeoutOutputStream(myOut, myTimer);
 			timeoutIn.setTimeout(timeout * 1000);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java
index cf13582..754cf36 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java
@@ -464,8 +464,12 @@ private boolean sendWants(final Collection<Ref> want) throws IOException {
 		final PacketLineOut p = statelessRPC ? pckState : pckOut;
 		boolean first = true;
 		for (final Ref r : want) {
+			ObjectId objectId = r.getObjectId();
+			if (objectId == null) {
+				continue;
+			}
 			try {
-				if (walk.parseAny(r.getObjectId()).has(REACHABLE)) {
+				if (walk.parseAny(objectId).has(REACHABLE)) {
 					// We already have this object. Asking for it is
 					// not a very good idea.
 					//
@@ -478,7 +482,7 @@ private boolean sendWants(final Collection<Ref> want) throws IOException {
 
 			final StringBuilder line = new StringBuilder(46);
 			line.append("want "); //$NON-NLS-1$
-			line.append(r.getObjectId().name());
+			line.append(objectId.name());
 			if (first) {
 				line.append(enableCapabilities());
 				first = false;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java
index 0834c35..963de35 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java
@@ -239,8 +239,11 @@ private void writeCommands(final Collection<RemoteRefUpdate> refUpdates,
 			final StringBuilder sb = new StringBuilder();
 			ObjectId oldId = rru.getExpectedOldObjectId();
 			if (oldId == null) {
-				Ref adv = getRef(rru.getRemoteName());
-				oldId = adv != null ? adv.getObjectId() : ObjectId.zeroId();
+				final Ref advertised = getRef(rru.getRemoteName());
+				oldId = advertised != null ? advertised.getObjectId() : null;
+				if (oldId == null) {
+					oldId = ObjectId.zeroId();
+				}
 			}
 			sb.append(oldId.name());
 			sb.append(' ');
@@ -382,7 +385,8 @@ private String readStringLongTimeout() throws IOException {
 		final int oldTimeout = timeoutIn.getTimeout();
 		final int sendTime = (int) Math.min(packTransferTime, 28800000L);
 		try {
-			timeoutIn.setTimeout(10 * Math.max(sendTime, oldTimeout));
+			int timeout = 10 * Math.max(sendTime, oldTimeout);
+			timeoutIn.setTimeout((timeout < 0) ? Integer.MAX_VALUE : timeout);
 			return pckIn.readString();
 		} finally {
 			timeoutIn.setTimeout(oldTimeout);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java
index 776a9f6..a20e652 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java
@@ -293,18 +293,20 @@ protected BaseReceivePack(final Repository into) {
 		db = into;
 		walk = new RevWalk(db);
 
-		final ReceiveConfig cfg = db.getConfig().get(ReceiveConfig.KEY);
-		objectChecker = cfg.newObjectChecker();
-		allowCreates = cfg.allowCreates;
+		TransferConfig tc = db.getConfig().get(TransferConfig.KEY);
+		objectChecker = tc.newReceiveObjectChecker();
+
+		ReceiveConfig rc = db.getConfig().get(ReceiveConfig.KEY);
+		allowCreates = rc.allowCreates;
 		allowAnyDeletes = true;
-		allowBranchDeletes = cfg.allowDeletes;
-		allowNonFastForwards = cfg.allowNonFastForwards;
-		allowOfsDelta = cfg.allowOfsDelta;
+		allowBranchDeletes = rc.allowDeletes;
+		allowNonFastForwards = rc.allowNonFastForwards;
+		allowOfsDelta = rc.allowOfsDelta;
 		advertiseRefsHook = AdvertiseRefsHook.DEFAULT;
 		refFilter = RefFilter.DEFAULT;
 		advertisedHaves = new HashSet<ObjectId>();
 		clientShallowCommits = new HashSet<ObjectId>();
-		signedPushConfig = cfg.signedPush;
+		signedPushConfig = rc.signedPush;
 	}
 
 	/** Configuration for receive operations. */
@@ -315,32 +317,13 @@ public ReceiveConfig parse(final Config cfg) {
 			}
 		};
 
-		final boolean checkReceivedObjects;
-		final boolean allowLeadingZeroFileMode;
-		final boolean allowInvalidPersonIdent;
-		final boolean safeForWindows;
-		final boolean safeForMacOS;
-
 		final boolean allowCreates;
 		final boolean allowDeletes;
 		final boolean allowNonFastForwards;
 		final boolean allowOfsDelta;
-
 		final SignedPushConfig signedPush;
 
 		ReceiveConfig(final Config config) {
-			checkReceivedObjects = config.getBoolean(
-					"receive", "fsckobjects", //$NON-NLS-1$ //$NON-NLS-2$
-					config.getBoolean("transfer", "fsckobjects", false)); //$NON-NLS-1$ //$NON-NLS-2$
-			allowLeadingZeroFileMode = checkReceivedObjects
-					&& config.getBoolean("fsck", "allowLeadingZeroFileMode", false); //$NON-NLS-1$ //$NON-NLS-2$
-			allowInvalidPersonIdent = checkReceivedObjects
-					&& config.getBoolean("fsck", "allowInvalidPersonIdent", false); //$NON-NLS-1$ //$NON-NLS-2$
-			safeForWindows = checkReceivedObjects
-					&& config.getBoolean("fsck", "safeForWindows", false); //$NON-NLS-1$ //$NON-NLS-2$
-			safeForMacOS = checkReceivedObjects
-					&& config.getBoolean("fsck", "safeForMacOS", false); //$NON-NLS-1$ //$NON-NLS-2$
-
 			allowCreates = true;
 			allowDeletes = !config.getBoolean("receive", "denydeletes", false); //$NON-NLS-1$ //$NON-NLS-2$
 			allowNonFastForwards = !config.getBoolean("receive", //$NON-NLS-1$
@@ -349,16 +332,6 @@ public ReceiveConfig parse(final Config cfg) {
 					true);
 			signedPush = SignedPushConfig.KEY.parse(config);
 		}
-
-		ObjectChecker newObjectChecker() {
-			if (!checkReceivedObjects)
-				return null;
-			return new ObjectChecker()
-				.setAllowLeadingZeroFileMode(allowLeadingZeroFileMode)
-				.setAllowInvalidPersonIdent(allowInvalidPersonIdent)
-				.setSafeForWindows(safeForWindows)
-				.setSafeForMacOS(safeForMacOS);
-		}
 	}
 
 	/**
@@ -1372,16 +1345,21 @@ protected void validateCommands() {
 				}
 			}
 
-			if (cmd.getType() == ReceiveCommand.Type.DELETE && ref != null
-					&& !ObjectId.zeroId().equals(cmd.getOldId())
-					&& !ref.getObjectId().equals(cmd.getOldId())) {
-				// Delete commands can be sent with the old id matching our
-				// advertised value, *OR* with the old id being 0{40}. Any
-				// other requested old id is invalid.
-				//
-				cmd.setResult(Result.REJECTED_OTHER_REASON,
-						JGitText.get().invalidOldIdSent);
-				continue;
+			if (cmd.getType() == ReceiveCommand.Type.DELETE && ref != null) {
+				ObjectId id = ref.getObjectId();
+				if (id == null) {
+					id = ObjectId.zeroId();
+				}
+				if (!ObjectId.zeroId().equals(cmd.getOldId())
+						&& !id.equals(cmd.getOldId())) {
+					// Delete commands can be sent with the old id matching our
+					// advertised value, *OR* with the old id being 0{40}. Any
+					// other requested old id is invalid.
+					//
+					cmd.setResult(Result.REJECTED_OTHER_REASON,
+							JGitText.get().invalidOldIdSent);
+					continue;
+				}
 			}
 
 			if (cmd.getType() == ReceiveCommand.Type.UPDATE) {
@@ -1391,8 +1369,15 @@ protected void validateCommands() {
 					cmd.setResult(Result.REJECTED_OTHER_REASON, JGitText.get().noSuchRef);
 					continue;
 				}
+				ObjectId id = ref.getObjectId();
+				if (id == null) {
+					// We cannot update unborn branch
+					cmd.setResult(Result.REJECTED_OTHER_REASON,
+							JGitText.get().cannotUpdateUnbornBranch);
+					continue;
+				}
 
-				if (!ref.getObjectId().equals(cmd.getOldId())) {
+				if (!id.equals(cmd.getOldId())) {
 					// A properly functioning client will send the same
 					// object id we advertised.
 					//
@@ -1468,10 +1453,7 @@ protected boolean anyRejects() {
 	 * @since 3.6
 	 */
 	protected void failPendingCommands() {
-		for (ReceiveCommand cmd : commands) {
-			if (cmd.getResult() == Result.NOT_ATTEMPTED)
-				cmd.setResult(Result.REJECTED_OTHER_REASON, JGitText.get().transactionAborted);
-		}
+		ReceiveCommand.abort(commands);
 	}
 
 	/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BundleFetchConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BundleFetchConnection.java
index e53c04b..8038fa4 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BundleFetchConnection.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BundleFetchConnection.java
@@ -161,16 +161,23 @@ private PackProtocolException duplicateAdvertisement(final String name) {
 	}
 
 	private String readLine(final byte[] hdrbuf) throws IOException {
-		bin.mark(hdrbuf.length);
-		final int cnt = bin.read(hdrbuf);
-		int lf = 0;
-		while (lf < cnt && hdrbuf[lf] != '\n')
-			lf++;
-		bin.reset();
-		IO.skipFully(bin, lf);
-		if (lf < cnt && hdrbuf[lf] == '\n')
-			IO.skipFully(bin, 1);
-		return RawParseUtils.decode(Constants.CHARSET, hdrbuf, 0, lf);
+		StringBuilder line = new StringBuilder();
+		boolean done = false;
+		while (!done) {
+			bin.mark(hdrbuf.length);
+			final int cnt = bin.read(hdrbuf);
+			int lf = 0;
+			while (lf < cnt && hdrbuf[lf] != '\n')
+				lf++;
+			bin.reset();
+			IO.skipFully(bin, lf);
+			if (lf < cnt && hdrbuf[lf] == '\n') {
+				IO.skipFully(bin, 1);
+				done = true;
+			}
+			line.append(RawParseUtils.decode(Constants.CHARSET, hdrbuf, 0, lf));
+		}
+		return line.toString();
 	}
 
 	public boolean didFetchTestConnectivity() {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ChainingCredentialsProvider.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ChainingCredentialsProvider.java
index 3e0ee2f..3941d3c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ChainingCredentialsProvider.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ChainingCredentialsProvider.java
@@ -113,19 +113,18 @@ public boolean get(URIish uri, CredentialItem... items)
 			throws UnsupportedCredentialItem {
 		for (CredentialsProvider p : credentialProviders) {
 			if (p.supports(items)) {
-				p.get(uri, items);
-				if (isAnyNull(items))
+				if (!p.get(uri, items)) {
+					if (p.isInteractive()) {
+						return false; // user cancelled the request
+					}
 					continue;
+				}
+				if (isAnyNull(items)) {
+					continue;
+				}
 				return true;
 			}
 		}
 		return false;
 	}
-
-	private boolean isAnyNull(CredentialItem... items) {
-		for (CredentialItem i : items)
-			if (i == null)
-				return true;
-		return false;
-	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Connection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Connection.java
index 0ff9fce..da288ec 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Connection.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Connection.java
@@ -59,8 +59,7 @@
  *
  * @see Transport
  */
-public interface Connection {
-
+public interface Connection extends AutoCloseable {
 	/**
 	 * Get the complete map of refs advertised as available for fetching or
 	 * pushing.
@@ -108,6 +107,10 @@ public interface Connection {
 	 * <p>
 	 * If additional messages were produced by the remote peer, these should
 	 * still be retained in the connection instance for {@link #getMessages()}.
+	 * <p>
+	 * {@code AutoClosable.close()} declares that it throws {@link Exception}.
+	 * Implementers shouldn't throw checked exceptions. This override narrows
+	 * the signature to prevent them from doing so.
 	 */
 	public void close();
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/CredentialsProvider.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/CredentialsProvider.java
index 464d0f9..4800f68 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/CredentialsProvider.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/CredentialsProvider.java
@@ -81,6 +81,20 @@ public static void setDefault(CredentialsProvider p) {
 	}
 
 	/**
+	 * @param items
+	 *            credential items to check
+	 * @return {@code true} if any of the passed items is null, {@code false}
+	 *         otherwise
+	 * @since 4.2
+	 */
+	protected static boolean isAnyNull(CredentialItem... items) {
+		for (CredentialItem i : items)
+			if (i == null)
+				return true;
+		return false;
+	}
+
+	/**
 	 * Check if the provider is interactive with the end-user.
 	 *
 	 * An interactive provider may try to open a dialog box, or prompt for input
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Daemon.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Daemon.java
index d9e0b93..2593ba5 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Daemon.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Daemon.java
@@ -256,6 +256,16 @@ public void setUploadPackFactory(UploadPackFactory<DaemonClient> factory) {
 	}
 
 	/**
+	 * Get the factory used to construct per-request ReceivePack.
+	 *
+	 * @return the factory.
+	 * @since 4.2
+	 */
+	public ReceivePackFactory<DaemonClient> getReceivePackFactory() {
+		return receivePackFactory;
+	}
+
+	/**
 	 * Set the factory to construct and configure per-request ReceivePack.
 	 *
 	 * @param factory
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java
index 9aae1c3..c4b3f83 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java
@@ -397,11 +397,17 @@ private Collection<Ref> expandAutoFollowTags() throws TransportException {
 	private void expandFetchTags() throws TransportException {
 		final Map<String, Ref> haveRefs = localRefs();
 		for (final Ref r : conn.getRefs()) {
-			if (!isTag(r))
+			if (!isTag(r)) {
 				continue;
+			}
+			ObjectId id = r.getObjectId();
+			if (id == null) {
+				continue;
+			}
 			final Ref local = haveRefs.get(r.getName());
-			if (local == null || !r.getObjectId().equals(local.getObjectId()))
+			if (local == null || !id.equals(local.getObjectId())) {
 				wantTag(r);
+			}
 		}
 	}
 
@@ -413,6 +419,11 @@ private void wantTag(final Ref r) throws TransportException {
 	private void want(final Ref src, final RefSpec spec)
 			throws TransportException {
 		final ObjectId newId = src.getObjectId();
+		if (newId == null) {
+			throw new NullPointerException(MessageFormat.format(
+					JGitText.get().transportProvidedRefWithNoObjectId,
+					src.getName()));
+		}
 		if (spec.getDestination() != null) {
 			final TrackingRefUpdate tru = createUpdate(spec, newId);
 			if (newId.equals(tru.getOldObjectId()))
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/JschSession.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/JschSession.java
index 85109a5..1dfe5d9 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/JschSession.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/JschSession.java
@@ -149,14 +149,27 @@ private class JschProcess extends Process {
 				channel.setCommand(commandName);
 				setupStreams();
 				channel.connect(timeout > 0 ? timeout * 1000 : 0);
-				if (!channel.isConnected())
+				if (!channel.isConnected()) {
+					closeOutputStream();
 					throw new TransportException(uri,
 							JGitText.get().connectionFailed);
+				}
 			} catch (JSchException e) {
+				closeOutputStream();
 				throw new TransportException(uri, e.getMessage(), e);
 			}
 		}
 
+		private void closeOutputStream() {
+			if (outputStream != null) {
+				try {
+					outputStream.close();
+				} catch (IOException ioe) {
+					// ignore
+				}
+			}
+		}
+
 		private void setupStreams() throws IOException {
 			inputStream = channel.getInputStream();
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/NetRCCredentialsProvider.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/NetRCCredentialsProvider.java
index 7490999..4037545 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/NetRCCredentialsProvider.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/NetRCCredentialsProvider.java
@@ -105,12 +105,11 @@ public boolean get(URIish uri, CredentialItem... items)
 			throw new UnsupportedCredentialItem(uri, i.getClass().getName()
 					+ ":" + i.getPromptText()); //$NON-NLS-1$
 		}
-		return true;
+		return !isAnyNull(items);
 	}
 
 	@Override
 	public boolean isInteractive() {
 		return false;
 	}
-
 }
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/ProgressSpinner.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProgressSpinner.java
new file mode 100644
index 0000000..ac048a1
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ProgressSpinner.java
@@ -0,0 +1,149 @@
+/*
+ * 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.transport;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A simple spinner connected to an {@code OutputStream}.
+ * <p>
+ * This is class is not thread-safe. The update method may only be used from a
+ * single thread. Updates are sent only as frequently as {@link #update()} is
+ * invoked by the caller, and are capped at no more than 2 times per second by
+ * requiring at least 500 milliseconds between updates.
+ *
+ * @since 4.2
+ */
+public class ProgressSpinner {
+	private static final long MIN_REFRESH_MILLIS = 500;
+	private static final char[] STATES = new char[] { '-', '\\', '|', '/' };
+
+	private final OutputStream out;
+	private String msg;
+	private int state;
+	private boolean write;
+	private boolean shown;
+	private long nextUpdateMillis;
+
+	/**
+	 * Initialize a new spinner.
+	 *
+	 * @param out
+	 *            where to send output to.
+	 */
+	public ProgressSpinner(OutputStream out) {
+		this.out = out;
+		this.write = true;
+	}
+
+	/**
+	 * Begin a time consuming task.
+	 *
+	 * @param title
+	 *            description of the task, suitable for human viewing.
+	 * @param delay
+	 *            delay to wait before displaying anything at all.
+	 * @param delayUnits
+	 *            unit for {@code delay}.
+	 */
+	public void beginTask(String title, long delay, TimeUnit delayUnits) {
+		msg = title;
+		state = 0;
+		shown = false;
+
+		long now = System.currentTimeMillis();
+		if (delay > 0) {
+			nextUpdateMillis = now + delayUnits.toMillis(delay);
+		} else {
+			send(now);
+		}
+	}
+
+	/** Update the spinner if it is showing. */
+	public void update() {
+		long now = System.currentTimeMillis();
+		if (now >= nextUpdateMillis) {
+			send(now);
+			state = (state + 1) % STATES.length;
+		}
+	}
+
+	private void send(long now) {
+		StringBuilder buf = new StringBuilder(msg.length() + 16);
+		buf.append('\r').append(msg).append("... ("); //$NON-NLS-1$
+		buf.append(STATES[state]);
+		buf.append(")  "); //$NON-NLS-1$
+		shown = true;
+		write(buf.toString());
+		nextUpdateMillis = now + MIN_REFRESH_MILLIS;
+	}
+
+	/**
+	 * Denote the current task completed.
+	 *
+	 * @param result
+	 *            text to print after the task's title
+	 *            {@code "$title ... $result"}.
+	 */
+	public void endTask(String result) {
+		if (shown) {
+			write('\r' + msg + "... " + result + "\n"); //$NON-NLS-1$ //$NON-NLS-2$
+		}
+	}
+
+	private void write(String s) {
+		if (write) {
+			try {
+				out.write(s.getBytes(UTF_8));
+				out.flush();
+			} catch (IOException e) {
+				write = false;
+			}
+		}
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java
index 4fd192d..5cea882 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java
@@ -188,8 +188,13 @@ private Map<String, RemoteRefUpdate> prepareRemoteUpdates()
 		final Map<String, RemoteRefUpdate> result = new HashMap<String, RemoteRefUpdate>();
 		for (final RemoteRefUpdate rru : toPush.values()) {
 			final Ref advertisedRef = connection.getRef(rru.getRemoteName());
-			final ObjectId advertisedOld = (advertisedRef == null ? ObjectId
-					.zeroId() : advertisedRef.getObjectId());
+			ObjectId advertisedOld = null;
+			if (advertisedRef != null) {
+				advertisedOld = advertisedRef.getObjectId();
+			}
+			if (advertisedOld == null) {
+				advertisedOld = ObjectId.zeroId();
+			}
 
 			if (rru.getNewObjectId().equals(advertisedOld)) {
 				if (rru.isDelete()) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java
index 5702b6d..2b21c4a 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java
@@ -43,6 +43,9 @@
 
 package org.eclipse.jgit.transport;
 
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
+
 import java.io.IOException;
 import java.text.MessageFormat;
 import java.util.ArrayList;
@@ -168,6 +171,25 @@ public static List<ReceiveCommand> filter(List<ReceiveCommand> commands,
 		return filter((Iterable<ReceiveCommand>) commands, want);
 	}
 
+	/**
+	 * Set unprocessed commands as failed due to transaction aborted.
+	 * <p>
+	 * If a command is still {@link Result#NOT_ATTEMPTED} it will be set to
+	 * {@link Result#REJECTED_OTHER_REASON}.
+	 *
+	 * @param commands
+	 *            commands to mark as failed.
+	 * @since 4.2
+	 */
+	public static void abort(Iterable<ReceiveCommand> commands) {
+		for (ReceiveCommand c : commands) {
+			if (c.getResult() == NOT_ATTEMPTED) {
+				c.setResult(REJECTED_OTHER_REASON,
+						JGitText.get().transactionAborted);
+			}
+		}
+	}
+
 	private final ObjectId oldId;
 
 	private final ObjectId newId;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java
index 66ffc3a..0e803bd 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefSpec.java
@@ -457,10 +457,6 @@ private static boolean isValid(final String s) {
 		if (i != -1) {
 			if (s.indexOf('*', i + 1) > i)
 				return false;
-			if (i > 0 && s.charAt(i - 1) != '/')
-				return false;
-			if (i < s.length() - 1 && s.charAt(i + 1) != '/')
-				return false;
 		}
 		return true;
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandInputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandInputStream.java
index cf388e2..fe9f2a3 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandInputStream.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SideBandInputStream.java
@@ -78,12 +78,8 @@
  * @see SideBandOutputStream
  */
 class SideBandInputStream extends InputStream {
-	private static final String PFX_REMOTE = JGitText.get().prefixRemote;
-
 	static final int CH_DATA = 1;
-
 	static final int CH_PROGRESS = 2;
-
 	static final int CH_ERROR = 3;
 
 	private static Pattern P_UNBOUNDED = Pattern
@@ -174,7 +170,7 @@ private void needDataPacket() throws IOException {
 				continue;
 			case CH_ERROR:
 				eof = true;
-				throw new TransportException(PFX_REMOTE + readString(available));
+				throw new TransportException(remote(readString(available)));
 			default:
 				throw new PackProtocolException(
 						MessageFormat.format(JGitText.get().invalidChannel,
@@ -241,7 +237,18 @@ private void doProgressLine(final String msg) throws IOException {
 	}
 
 	private void beginTask(final int totalWorkUnits) {
-		monitor.beginTask(PFX_REMOTE + currentTask, totalWorkUnits);
+		monitor.beginTask(remote(currentTask), totalWorkUnits);
+	}
+
+	private static String remote(String msg) {
+		String prefix = JGitText.get().prefixRemote;
+		StringBuilder r = new StringBuilder(prefix.length() + msg.length() + 1);
+		r.append(prefix);
+		if (prefix.length() > 0 && prefix.charAt(prefix.length() - 1) != ' ') {
+			r.append(' ');
+		}
+		r.append(msg);
+		return r.toString();
 	}
 
 	private String readString(final int len) throws IOException {
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 f0c5134..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,12 +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;
@@ -58,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) {
@@ -65,8 +75,14 @@ public TransferConfig parse(final Config cfg) {
 		}
 	};
 
-	private final boolean checkReceivedObjects;
-	private final boolean allowLeadingZeroFileMode;
+	enum FsckMode {
+		ERROR, WARN, IGNORE;
+	}
+
+	private final boolean fetchFsck;
+	private final boolean receiveFsck;
+	private final String fsckSkipList;
+	private final EnumSet<ObjectChecker.ErrorType> ignore;
 	private final boolean allowInvalidPersonIdent;
 	private final boolean safeForWindows;
 	private final boolean safeForMacOS;
@@ -79,20 +95,47 @@ public TransferConfig parse(final Config cfg) {
 	}
 
 	TransferConfig(final Config rc) {
-		checkReceivedObjects = rc.getBoolean(
-				"fetch", "fsckobjects", //$NON-NLS-1$ //$NON-NLS-2$
-				rc.getBoolean("transfer", "fsckobjects", false)); //$NON-NLS-1$ //$NON-NLS-2$
-		allowLeadingZeroFileMode = checkReceivedObjects
-				&& rc.getBoolean("fsck", "allowLeadingZeroFileMode", false); //$NON-NLS-1$ //$NON-NLS-2$
-		allowInvalidPersonIdent = checkReceivedObjects
-				&& rc.getBoolean("fsck", "allowInvalidPersonIdent", false); //$NON-NLS-1$ //$NON-NLS-2$
-		safeForWindows = checkReceivedObjects
-				&& rc.getBoolean("fsck", "safeForWindows", //$NON-NLS-1$ //$NON-NLS-2$
+		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$
+		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 = checkReceivedObjects
-				&& 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(
@@ -105,14 +148,38 @@ public TransferConfig parse(final Config cfg) {
 	 *         enabled in the repository configuration.
 	 * @since 3.6
 	 */
+	@Nullable
 	public ObjectChecker newObjectChecker() {
-		if (!checkReceivedObjects)
+		return newObjectChecker(fetchFsck);
+	}
+
+	/**
+	 * @return checker to verify objects pushed into this repository, or null if
+	 *         checking is not enabled in the repository configuration.
+	 * @since 4.2
+	 */
+	@Nullable
+	public ObjectChecker newReceiveObjectChecker() {
+		return newObjectChecker(receiveFsck);
+	}
+
+	private ObjectChecker newObjectChecker(boolean check) {
+		if (!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;
 	}
 
 	/**
@@ -161,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/Transport.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java
index 6af153c..9e6d1f6 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Transport.java
@@ -98,7 +98,7 @@
  * Transport instances and the connections they create are not thread-safe.
  * Callers must ensure a transport is accessed by only one thread at a time.
  */
-public abstract class Transport {
+public abstract class Transport implements AutoCloseable {
 	/** Type of operation a Transport is being opened for. */
 	public enum Operation {
 		/** Transport is to fetch objects locally. */
@@ -1353,6 +1353,10 @@ public abstract PushConnection openPush() throws NotSupportedException,
 	 * must close that network socket, disconnecting the two peers. If the
 	 * remote repository is actually local (same system) this method must close
 	 * any open file handles used to read the "remote" repository.
+	 * <p>
+	 * {@code AutoClosable.close()} declares that it throws {@link Exception}.
+	 * Implementers shouldn't throw checked exceptions. This override narrows
+	 * the signature to prevent them from doing so.
 	 */
 	public abstract void close();
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java
index 2f9dfa1..3ee2feb 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java
@@ -83,7 +83,7 @@ public class URIish implements Serializable {
 	 * capturing groups: the first containing the user and the second containing
 	 * the password
 	 */
-	private static final String OPT_USER_PWD_P = "(?:([^/:@]+)(?::([^\\\\/]+))?@)?"; //$NON-NLS-1$
+	private static final String OPT_USER_PWD_P = "(?:([^/:]+)(?::([^\\\\/]+))?@)?"; //$NON-NLS-1$
 
 	/**
 	 * Part of a pattern which matches the host part of URIs. Defines one
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 1c6b8b7..17edfdc 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkFetchConnection.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WalkFetchConnection.java
@@ -267,6 +267,10 @@ private void queueWants(final Collection<Ref> want)
 		final HashSet<ObjectId> inWorkQueue = new HashSet<ObjectId>();
 		for (final Ref r : want) {
 			final ObjectId id = r.getObjectId();
+			if (id == null) {
+				throw new NullPointerException(MessageFormat.format(
+						JGitText.get().transportProvidedRefWithNoObjectId, r.getName()));
+			}
 			try {
 				final RevObject obj = revWalk.parseAny(id);
 				if (obj.has(COMPLETE))
@@ -633,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()));
 			}
 		}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java
index 5e71889..5813635 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/AbstractTreeIterator.java
@@ -59,6 +59,7 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.treewalk.filter.TreeFilter;
+import org.eclipse.jgit.util.Paths;
 
 /**
  * Walks a Git tree (directory) in Git sort order.
@@ -382,20 +383,9 @@ public int pathCompare(byte[] buf, int pos, int end, int pathMode) {
 	}
 
 	private int pathCompare(byte[] b, int bPos, int bEnd, int bMode, int aPos) {
-		final byte[] a = path;
-		final int aEnd = pathLen;
-
-		for (; aPos < aEnd && bPos < bEnd; aPos++, bPos++) {
-			final int cmp = (a[aPos] & 0xff) - (b[bPos] & 0xff);
-			if (cmp != 0)
-				return cmp;
-		}
-
-		if (aPos < aEnd)
-			return (a[aPos] & 0xff) - lastPathChar(bMode);
-		if (bPos < bEnd)
-			return lastPathChar(mode) - (b[bPos] & 0xff);
-		return lastPathChar(mode) - lastPathChar(bMode);
+		return Paths.compare(
+				path, aPos, pathLen, mode,
+				b, bPos, bEnd, bMode);
 	}
 
 	private static int alreadyMatch(AbstractTreeIterator a,
@@ -412,10 +402,6 @@ private static int alreadyMatch(AbstractTreeIterator a,
 		}
 	}
 
-	private static int lastPathChar(final int mode) {
-		return FileMode.TREE.equals(mode) ? '/' : '\0';
-	}
-
 	/**
 	 * Check if the current entry of both iterators has the same id.
 	 * <p>
@@ -692,6 +678,14 @@ public void stopWalk() {
 	}
 
 	/**
+	 * @return true if the iterator implements {@link #stopWalk()}.
+	 * @since 4.2
+	 */
+	protected boolean needsStopWalk() {
+		return false;
+	}
+
+	/**
 	 * @return the length of the name component of the path for the current entry
 	 */
 	public int getNameLength() {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/EmptyTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/EmptyTreeIterator.java
index 8dbf80e..ec4a84e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/EmptyTreeIterator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/EmptyTreeIterator.java
@@ -142,4 +142,9 @@ public void stopWalk() {
 		if (parent != null)
 			parent.stopWalk();
 	}
+
+	@Override
+	protected boolean needsStopWalk() {
+		return parent != null && parent.needsStopWalk();
+	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/NameConflictTreeWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/NameConflictTreeWalk.java
index 350f563..d2195a8 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/NameConflictTreeWalk.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/NameConflictTreeWalk.java
@@ -43,6 +43,8 @@
 
 package org.eclipse.jgit.treewalk;
 
+import java.io.IOException;
+
 import org.eclipse.jgit.dircache.DirCacheBuilder;
 import org.eclipse.jgit.errors.CorruptObjectException;
 import org.eclipse.jgit.lib.FileMode;
@@ -338,6 +340,41 @@ void skipEntriesEqual() throws CorruptObjectException {
 			dfConflict = null;
 	}
 
+	void stopWalk() throws IOException {
+		if (!needsStopWalk()) {
+			return;
+		}
+
+		// Name conflicts make aborting early difficult. Multiple paths may
+		// exist between the file and directory versions of a name. To ensure
+		// the directory version is skipped over (as it was previously visited
+		// during the file version step) requires popping up the stack and
+		// finishing out each subtree that the walker dove into. Siblings in
+		// parents do not need to be recursed into, bounding the cost.
+		for (;;) {
+			AbstractTreeIterator t = min();
+			if (t.eof()) {
+				if (depth > 0) {
+					exitSubtree();
+					popEntriesEqual();
+					continue;
+				}
+				return;
+			}
+			currentHead = t;
+			skipEntriesEqual();
+		}
+	}
+
+	private boolean needsStopWalk() {
+		for (AbstractTreeIterator t : trees) {
+			if (t.needsStopWalk()) {
+				return true;
+			}
+		}
+		return false;
+	}
+
 	/**
 	 * True if the current entry is covered by a directory/file conflict.
 	 *
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java
index 06dc0bf..5cd713d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/TreeWalk.java
@@ -57,6 +57,7 @@
 import org.eclipse.jgit.attributes.AttributesNode;
 import org.eclipse.jgit.attributes.AttributesNodeProvider;
 import org.eclipse.jgit.attributes.AttributesProvider;
+import org.eclipse.jgit.dircache.DirCacheBuildIterator;
 import org.eclipse.jgit.dircache.DirCacheIterator;
 import org.eclipse.jgit.errors.CorruptObjectException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -256,7 +257,7 @@ public static TreeWalk forPath(final Repository db, final String path,
 
 	private boolean postOrderTraversal;
 
-	private int depth;
+	int depth;
 
 	private boolean advance;
 
@@ -573,18 +574,13 @@ public int addTree(final AnyObjectId id) throws MissingObjectException,
 	 * @param p
 	 *            an iterator to walk over. The iterator should be new, with no
 	 *            parent, and should still be positioned before the first entry.
-	 *            The tree which the iterator operates on must have the same root
-	 *            as other trees in the walk.
-	 *
+	 *            The tree which the iterator operates on must have the same
+	 *            root as other trees in the walk.
 	 * @return position of this tree within the walker.
-	 * @throws CorruptObjectException
-	 *             the iterator was unable to obtain its first entry, due to
-	 *             possible data corruption within the backing data store.
 	 */
-	public int addTree(final AbstractTreeIterator p)
-			throws CorruptObjectException {
-		final int n = trees.length;
-		final AbstractTreeIterator[] newTrees = new AbstractTreeIterator[n + 1];
+	public int addTree(AbstractTreeIterator p) {
+		int n = trees.length;
+		AbstractTreeIterator[] newTrees = new AbstractTreeIterator[n + 1];
 
 		System.arraycopy(trees, 0, newTrees, 0, n);
 		newTrees[n] = p;
@@ -665,13 +661,30 @@ public boolean next() throws MissingObjectException,
 				return true;
 			}
 		} catch (StopWalkException stop) {
-			for (final AbstractTreeIterator t : trees)
-				t.stopWalk();
+			stopWalk();
 			return false;
 		}
 	}
 
 	/**
+	 * Notify iterators the walk is aborting.
+	 * <p>
+	 * Primarily to notify {@link DirCacheBuildIterator} the walk is aborting so
+	 * that it can copy any remaining entries.
+	 *
+	 * @throws IOException
+	 *             if traversal of remaining entries throws an exception during
+	 *             object access. This should never occur as remaining trees
+	 *             should already be in memory, however the methods used to
+	 *             finish traversal are declared to throw IOException.
+	 */
+	void stopWalk() throws IOException {
+		for (AbstractTreeIterator t : trees) {
+			t.stopWalk();
+		}
+	}
+
+	/**
 	 * Obtain the tree iterator for the current entry.
 	 * <p>
 	 * Entering into (or exiting out of) a subtree causes the current tree
@@ -861,10 +874,13 @@ public int getPathLength() {
 	 * Test if the supplied path matches the current entry's path.
 	 * <p>
 	 * This method tests that the supplied path is exactly equal to the current
-	 * entry, or is one of its parent directories. It is faster to use this
+	 * entry or is one of its parent directories. It is faster to use this
 	 * method then to use {@link #getPathString()} to first create a String
 	 * object, then test <code>startsWith</code> or some other type of string
 	 * match function.
+	 * <p>
+	 * If the current entry is a subtree, then all paths within the subtree
+	 * are considered to match it.
 	 *
 	 * @param p
 	 *            path buffer to test. Callers should ensure the path does not
@@ -900,7 +916,7 @@ public int isPathPrefix(final byte[] p, final int pLen) {
 			// If p[ci] == '/' then pattern matches this subtree,
 			// otherwise we cannot be certain so we return -1.
 			//
-			return p[ci] == '/' ? 0 : -1;
+			return p[ci] == '/' && FileMode.TREE.equals(t.mode) ? 0 : -1;
 		}
 
 		// Both strings are identical.
@@ -1062,7 +1078,7 @@ void skipEntriesEqual() throws CorruptObjectException {
 		}
 	}
 
-	private void exitSubtree() {
+	void exitSubtree() {
 		depth--;
 		for (int i = 0; i < trees.length; i++)
 			trees[i] = trees[i].parent;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java
index 94beeeb..0d617ee 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java
@@ -89,6 +89,7 @@
 import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.FS.ExecutionResult;
 import org.eclipse.jgit.util.IO;
+import org.eclipse.jgit.util.Paths;
 import org.eclipse.jgit.util.RawParseUtils;
 import org.eclipse.jgit.util.io.EolCanonicalizingInputStream;
 
@@ -692,31 +693,13 @@ public AttributesNode getEntryAttributesNode() throws IOException {
 	}
 
 	private static final Comparator<Entry> ENTRY_CMP = new Comparator<Entry>() {
-		public int compare(final Entry o1, final Entry o2) {
-			final byte[] a = o1.encodedName;
-			final byte[] b = o2.encodedName;
-			final int aLen = o1.encodedNameLen;
-			final int bLen = o2.encodedNameLen;
-			int cPos;
-
-			for (cPos = 0; cPos < aLen && cPos < bLen; cPos++) {
-				final int cmp = (a[cPos] & 0xff) - (b[cPos] & 0xff);
-				if (cmp != 0)
-					return cmp;
-			}
-
-			if (cPos < aLen)
-				return (a[cPos] & 0xff) - lastPathChar(o2);
-			if (cPos < bLen)
-				return lastPathChar(o1) - (b[cPos] & 0xff);
-			return lastPathChar(o1) - lastPathChar(o2);
+		public int compare(Entry a, Entry b) {
+			return Paths.compare(
+					a.encodedName, 0, a.encodedNameLen, a.getMode().getBits(),
+					b.encodedName, 0, b.encodedNameLen, b.getMode().getBits());
 		}
 	};
 
-	static int lastPathChar(final Entry e) {
-		return e.getMode() == FileMode.TREE ? '/' : '\0';
-	}
-
 	/**
 	 * Constructor helper.
 	 *
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/PathFilterGroup.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/PathFilterGroup.java
index bdfde0b..7601956 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/PathFilterGroup.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/PathFilterGroup.java
@@ -245,9 +245,9 @@ public boolean include(final TreeWalk walker) {
 				int hash = hasher.nextHash();
 				if (fullpaths.contains(rp, hasher.length(), hash))
 					return true;
-				if (!hasher.hasNext())
-					if (prefixes.contains(rp, hasher.length(), hash))
-						return true;
+				if (!hasher.hasNext() && walker.isSubtree()
+						&& prefixes.contains(rp, hasher.length(), hash))
+					return true;
 			}
 
 			final int cmp = walker.isPathPrefix(max, max.length);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/ChangeIdUtil.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/ChangeIdUtil.java
index 35fc99e..e14096e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/ChangeIdUtil.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/ChangeIdUtil.java
@@ -42,7 +42,6 @@
  */
 package org.eclipse.jgit.util;
 
-import java.io.IOException;
 import java.util.regex.Pattern;
 
 import org.eclipse.jgit.lib.Constants;
@@ -90,12 +89,10 @@ static String clean(String msg) {
 	 *            The commit message
 	 * @return the change id SHA1 string (without the 'I') or null if the
 	 *         message is not complete enough
-	 * @throws IOException
 	 */
 	public static ObjectId computeChangeId(final ObjectId treeId,
 			final ObjectId firstParentId, final PersonIdent author,
-			final PersonIdent committer, final String message)
-			throws IOException {
+			final PersonIdent committer, final String message) {
 		String cleanMessage = clean(message);
 		if (cleanMessage.length() == 0)
 			return null;
@@ -116,8 +113,7 @@ public static ObjectId computeChangeId(final ObjectId treeId,
 		b.append("\n\n"); //$NON-NLS-1$
 		b.append(cleanMessage);
 		try (ObjectInserter f = new ObjectInserter.Formatter()) {
-			return f.idFor(Constants.OBJ_COMMIT, //
-					b.toString().getBytes(Constants.CHARACTER_ENCODING));
+			return f.idFor(Constants.OBJ_COMMIT, Constants.encode(b.toString()));
 		}
 	}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java
index 727ea79..aa101f7 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java
@@ -409,7 +409,9 @@ public static Path createSymLink(File path, String target)
 			throws IOException {
 		Path nioPath = path.toPath();
 		if (Files.exists(nioPath, LinkOption.NOFOLLOW_LINKS)) {
-			if (Files.isRegularFile(nioPath)) {
+			BasicFileAttributes attrs = Files.readAttributes(nioPath,
+					BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
+			if (attrs.isRegularFile() || attrs.isSymbolicLink()) {
 				delete(path);
 			} else {
 				delete(path, EMPTY_DIRECTORIES_ONLY | RECURSIVE);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/Paths.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/Paths.java
new file mode 100644
index 0000000..6be7ddb
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/Paths.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2016, 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.util;
+
+import static org.eclipse.jgit.lib.FileMode.TYPE_MASK;
+import static org.eclipse.jgit.lib.FileMode.TYPE_TREE;
+
+/**
+ * Utility functions for paths inside of a Git repository.
+ *
+ * @since 4.2
+ */
+public class Paths {
+	/**
+	 * Remove trailing {@code '/'} if present.
+	 *
+	 * @param path
+	 *            input path to potentially remove trailing {@code '/'} from.
+	 * @return null if {@code path == null}; {@code path} after removing a
+	 *         trailing {@code '/'}.
+	 */
+	public static String stripTrailingSeparator(String path) {
+		if (path == null || path.isEmpty()) {
+			return path;
+		}
+
+		int i = path.length();
+		if (path.charAt(path.length() - 1) != '/') {
+			return path;
+		}
+		do {
+			i--;
+		} while (path.charAt(i - 1) == '/');
+		return path.substring(0, i);
+	}
+
+	/**
+	 * Compare two paths according to Git path sort ordering rules.
+	 *
+	 * @param aPath
+	 *            first path buffer. The range {@code [aPos, aEnd)} is used.
+	 * @param aPos
+	 *            index into {@code aPath} where the first path starts.
+	 * @param aEnd
+	 *            1 past last index of {@code aPath}.
+	 * @param aMode
+	 *            mode of the first file. Trees are sorted as though
+	 *            {@code aPath[aEnd] == '/'}, even if aEnd does not exist.
+	 * @param bPath
+	 *            second path buffer. The range {@code [bPos, bEnd)} is used.
+	 * @param bPos
+	 *            index into {@code bPath} where the second path starts.
+	 * @param bEnd
+	 *            1 past last index of {@code bPath}.
+	 * @param bMode
+	 *            mode of the second file. Trees are sorted as though
+	 *            {@code bPath[bEnd] == '/'}, even if bEnd does not exist.
+	 * @return &lt;0 if {@code aPath} sorts before {@code bPath};
+	 *         0 if the paths are the same;
+	 *         &gt;0 if {@code aPath} sorts after {@code bPath}.
+	 */
+	public static int compare(byte[] aPath, int aPos, int aEnd, int aMode,
+			byte[] bPath, int bPos, int bEnd, int bMode) {
+		int cmp = coreCompare(
+				aPath, aPos, aEnd, aMode,
+				bPath, bPos, bEnd, bMode);
+		if (cmp == 0) {
+			cmp = lastPathChar(aMode) - lastPathChar(bMode);
+		}
+		return cmp;
+	}
+
+	/**
+	 * Compare two paths, checking for identical name.
+	 * <p>
+	 * Unlike {@code compare} this method returns {@code 0} when the paths have
+	 * the same characters in their names, even if the mode differs. It is
+	 * intended for use in validation routines detecting duplicate entries.
+	 * <p>
+	 * Returns {@code 0} if the names are identical and a conflict exists
+	 * between {@code aPath} and {@code bPath}, as they share the same name.
+	 * <p>
+	 * Returns {@code <0} if all possibles occurrences of {@code aPath} sort
+	 * before {@code bPath} and no conflict can happen. In a properly sorted
+	 * tree there are no other occurrences of {@code aPath} and therefore there
+	 * are no duplicate names.
+	 * <p>
+	 * Returns {@code >0} when it is possible for a duplicate occurrence of
+	 * {@code aPath} to appear later, after {@code bPath}. Callers should
+	 * continue to examine candidates for {@code bPath} until the method returns
+	 * one of the other return values.
+	 *
+	 * @param aPath
+	 *            first path buffer. The range {@code [aPos, aEnd)} is used.
+	 * @param aPos
+	 *            index into {@code aPath} where the first path starts.
+	 * @param aEnd
+	 *            1 past last index of {@code aPath}.
+	 * @param bPath
+	 *            second path buffer. The range {@code [bPos, bEnd)} is used.
+	 * @param bPos
+	 *            index into {@code bPath} where the second path starts.
+	 * @param bEnd
+	 *            1 past last index of {@code bPath}.
+	 * @param bMode
+	 *            mode of the second file. Trees are sorted as though
+	 *            {@code bPath[bEnd] == '/'}, even if bEnd does not exist.
+	 * @return &lt;0 if no duplicate name could exist;
+	 *         0 if the paths have the same name;
+	 *         &gt;0 other {@code bPath} should still be checked by caller.
+	 */
+	public static int compareSameName(
+			byte[] aPath, int aPos, int aEnd,
+			byte[] bPath, int bPos, int bEnd, int bMode) {
+		return coreCompare(
+				aPath, aPos, aEnd, TYPE_TREE,
+				bPath, bPos, bEnd, bMode);
+	}
+
+	private static int coreCompare(
+			byte[] aPath, int aPos, int aEnd, int aMode,
+			byte[] bPath, int bPos, int bEnd, int bMode) {
+		while (aPos < aEnd && bPos < bEnd) {
+			int cmp = (aPath[aPos++] & 0xff) - (bPath[bPos++] & 0xff);
+			if (cmp != 0) {
+				return cmp;
+			}
+		}
+		if (aPos < aEnd) {
+			return (aPath[aPos] & 0xff) - lastPathChar(bMode);
+		}
+		if (bPos < bEnd) {
+			return lastPathChar(aMode) - (bPath[bPos] & 0xff);
+		}
+		return 0;
+	}
+
+	private static int lastPathChar(int mode) {
+		if ((mode & TYPE_MASK) == TYPE_TREE) {
+			return '/';
+		}
+		return 0;
+	}
+
+	private Paths() {
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/RawParseUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/RawParseUtils.java
index 45c339f..f2955f7 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/RawParseUtils.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/RawParseUtils.java
@@ -44,6 +44,8 @@
 
 package org.eclipse.jgit.util;
 
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.eclipse.jgit.lib.ObjectChecker.author;
 import static org.eclipse.jgit.lib.ObjectChecker.committer;
 import static org.eclipse.jgit.lib.ObjectChecker.encoding;
@@ -60,6 +62,7 @@
 import java.util.HashMap;
 import java.util.Map;
 
+import org.eclipse.jgit.annotations.Nullable;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.PersonIdent;
 
@@ -70,7 +73,7 @@ public final class RawParseUtils {
 	 *
 	 * @since 2.2
 	 */
-	public static final Charset UTF8_CHARSET = Charset.forName("UTF-8"); //$NON-NLS-1$
+	public static final Charset UTF8_CHARSET = UTF_8;
 
 	private static final byte[] digits10;
 
@@ -81,8 +84,9 @@ public final class RawParseUtils {
 	private static final Map<String, Charset> encodingAliases;
 
 	static {
-		encodingAliases = new HashMap<String, Charset>();
-		encodingAliases.put("latin-1", Charset.forName("ISO-8859-1")); //$NON-NLS-1$ //$NON-NLS-2$
+		encodingAliases = new HashMap<>();
+		encodingAliases.put("latin-1", ISO_8859_1); //$NON-NLS-1$
+		encodingAliases.put("iso-latin-1", ISO_8859_1); //$NON-NLS-1$
 
 		digits10 = new byte['9' + 1];
 		Arrays.fill(digits10, (byte) -1);
@@ -671,35 +675,60 @@ public static final int encoding(final byte[] b, int ptr) {
 	}
 
 	/**
+	 * Parse the "encoding " header as a string.
+	 * <p>
+	 * Locates the "encoding " header (if present) and returns its value.
+	 *
+	 * @param b
+	 *            buffer to scan.
+	 * @return the encoding header as specified in the commit; null if the
+	 *         header was not present and should be assumed.
+	 * @since 4.2
+	 */
+	@Nullable
+	public static String parseEncodingName(final byte[] b) {
+		int enc = encoding(b, 0);
+		if (enc < 0) {
+			return null;
+		}
+		int lf = nextLF(b, enc);
+		return decode(UTF_8, b, enc, lf - 1);
+	}
+
+	/**
 	 * Parse the "encoding " header into a character set reference.
 	 * <p>
 	 * Locates the "encoding " header (if present) by first calling
 	 * {@link #encoding(byte[], int)} and then returns the proper character set
 	 * to apply to this buffer to evaluate its contents as character data.
 	 * <p>
-	 * If no encoding header is present, {@link Constants#CHARSET} is assumed.
+	 * If no encoding header is present {@code UTF-8} is assumed.
 	 *
 	 * @param b
 	 *            buffer to scan.
 	 * @return the Java character set representation. Never null.
+	 * @throws IllegalCharsetNameException
+	 *             if the character set requested by the encoding header is
+	 *             malformed and unsupportable.
+	 * @throws UnsupportedCharsetException
+	 *             if the JRE does not support the character set requested by
+	 *             the encoding header.
 	 */
 	public static Charset parseEncoding(final byte[] b) {
-		final int enc = encoding(b, 0);
-		if (enc < 0)
-			return Constants.CHARSET;
-		final int lf = nextLF(b, enc);
-		String decoded = decode(Constants.CHARSET, b, enc, lf - 1);
+		String enc = parseEncodingName(b);
+		if (enc == null) {
+			return UTF_8;
+		}
+
+		String name = enc.trim();
 		try {
-			return Charset.forName(decoded);
-		} catch (IllegalCharsetNameException badName) {
-			Charset aliased = charsetForAlias(decoded);
-			if (aliased != null)
+			return Charset.forName(name);
+		} catch (IllegalCharsetNameException
+				| UnsupportedCharsetException badName) {
+			Charset aliased = charsetForAlias(name);
+			if (aliased != null) {
 				return aliased;
-			throw badName;
-		} catch (UnsupportedCharsetException badName) {
-			Charset aliased = charsetForAlias(decoded);
-			if (aliased != null)
-				return aliased;
+			}
 			throw badName;
 		}
 	}
@@ -738,7 +767,15 @@ public static PersonIdent parsePersonIdent(final String in) {
 	 *         parsed.
 	 */
 	public static PersonIdent parsePersonIdent(final byte[] raw, final int nameB) {
-		final Charset cs = parseEncoding(raw);
+		Charset cs;
+		try {
+			cs = parseEncoding(raw);
+		} catch (IllegalCharsetNameException | UnsupportedCharsetException e) {
+			// Assume UTF-8 for person identities, usually this is correct.
+			// If not decode() will fall back to the ISO-8859-1 encoding.
+			cs = UTF_8;
+		}
+
 		final int emailB = nextLF(raw, nameB, '<');
 		final int emailE = nextLF(raw, emailB, '>');
 		if (emailB >= raw.length || raw[emailB] == '\n' ||
@@ -886,7 +923,7 @@ public static String decode(final byte[] buffer) {
 	 */
 	public static String decode(final byte[] buffer, final int start,
 			final int end) {
-		return decode(Constants.CHARSET, buffer, start, end);
+		return decode(UTF_8, buffer, start, end);
 	}
 
 	/**
@@ -960,23 +997,21 @@ public static String decode(final Charset cs, final byte[] buffer,
 	public static String decodeNoFallback(final Charset cs,
 			final byte[] buffer, final int start, final int end)
 			throws CharacterCodingException {
-		final ByteBuffer b = ByteBuffer.wrap(buffer, start, end - start);
+		ByteBuffer b = ByteBuffer.wrap(buffer, start, end - start);
 		b.mark();
 
 		// Try our built-in favorite. The assumption here is that
 		// decoding will fail if the data is not actually encoded
 		// using that encoder.
-		//
 		try {
-			return decode(b, Constants.CHARSET);
+			return decode(b, UTF_8);
 		} catch (CharacterCodingException e) {
 			b.reset();
 		}
 
-		if (!cs.equals(Constants.CHARSET)) {
+		if (!cs.equals(UTF_8)) {
 			// Try the suggested encoding, it might be right since it was
 			// provided by the caller.
-			//
 			try {
 				return decode(b, cs);
 			} catch (CharacterCodingException e) {
@@ -986,9 +1021,8 @@ public static String decodeNoFallback(final Charset cs,
 
 		// Try the default character set. A small group of people
 		// might actually use the same (or very similar) locale.
-		//
-		final Charset defcs = Charset.defaultCharset();
-		if (!defcs.equals(cs) && !defcs.equals(Constants.CHARSET)) {
+		Charset defcs = Charset.defaultCharset();
+		if (!defcs.equals(cs) && !defcs.equals(UTF_8)) {
 			try {
 				return decode(b, defcs);
 			} catch (CharacterCodingException e) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/StreamCopyThread.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/StreamCopyThread.java
index 24b8b53..8d39a22 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/io/StreamCopyThread.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/io/StreamCopyThread.java
@@ -47,6 +47,7 @@
 import java.io.InputStream;
 import java.io.InterruptedIOException;
 import java.io.OutputStream;
+import java.util.concurrent.atomic.AtomicInteger;
 
 /** Thread to copy from an input stream to an output stream. */
 public class StreamCopyThread extends Thread {
@@ -58,6 +59,8 @@ public class StreamCopyThread extends Thread {
 
 	private volatile boolean done;
 
+	private final AtomicInteger flushCount = new AtomicInteger(0);
+
 	/**
 	 * Create a thread to copy data from an input stream to an output stream.
 	 *
@@ -82,6 +85,7 @@ public StreamCopyThread(final InputStream i, final OutputStream o) {
 	 * the request.
 	 */
 	public void flush() {
+		flushCount.incrementAndGet();
 		interrupt();
 	}
 
@@ -109,22 +113,30 @@ public void halt() throws InterruptedException {
 	public void run() {
 		try {
 			final byte[] buf = new byte[BUFFER_SIZE];
-			int interruptCounter = 0;
+			int flushCountBeforeRead = 0;
+			boolean readInterrupted = false;
 			for (;;) {
 				try {
-					if (interruptCounter > 0) {
+					if (readInterrupted) {
 						dst.flush();
-						interruptCounter--;
+						readInterrupted = false;
+						if (!flushCount.compareAndSet(flushCountBeforeRead, 0)) {
+							// There was a flush() call since last blocked read.
+							// Set interrupt status, so next blocked read will throw
+							// an InterruptedIOException and we will flush again.
+							interrupt();
+						}
 					}
 
 					if (done)
 						break;
 
+					flushCountBeforeRead = flushCount.get();
 					final int n;
 					try {
 						n = src.read(buf);
 					} catch (InterruptedIOException wakey) {
-						interruptCounter++;
+						readInterrupted = true;
 						continue;
 					}
 					if (n < 0)
@@ -141,7 +153,7 @@ public void run() {
 
 						// set interrupt status, which will be checked
 						// when we block in src.read
-						if (writeInterrupted)
+						if (writeInterrupted || flushCount.get() > 0)
 							interrupt();
 						break;
 					}
diff --git a/tools/default.defs b/tools/default.defs
new file mode 100644
index 0000000..3481fa1
--- /dev/null
+++ b/tools/default.defs
@@ -0,0 +1,42 @@
+def java_sources(
+    name,
+    srcs,
+    visibility = ['PUBLIC']
+  ):
+  java_library(
+    name = name,
+    resources = srcs,
+    visibility = visibility,
+  )
+
+def maven_jar(
+    name,
+    group,
+    artifact,
+    version,
+    bin_sha1,
+    src_sha1,
+    visibility = ['PUBLIC']):
+  jar_name = '%s__jar' % name
+  src_name = '%s__src' % name
+
+  remote_file(
+    name = jar_name,
+    sha1 = bin_sha1,
+    url = 'mvn:%s:%s:jar:%s' % (group, artifact, version),
+    out = '%s.jar' % jar_name,
+  )
+
+  remote_file(
+    name = src_name,
+    sha1 = src_sha1,
+    url = 'mvn:%s:%s:src:%s' % (group, artifact, version),
+    out = '%s.jar' % src_name,
+  )
+
+  prebuilt_jar(
+    name = name,
+    binary_jar = ':' + jar_name,
+    source_jar = ':' + src_name,
+    visibility = visibility)
+
diff --git a/tools/git.defs b/tools/git.defs
new file mode 100644
index 0000000..557dff2
--- /dev/null
+++ b/tools/git.defs
@@ -0,0 +1,9 @@
+def git_version():
+  import subprocess
+  cmd = ['git', 'describe', '--always', '--match', 'v[0-9].*', '--dirty']
+  p = subprocess.Popen(cmd, stdout = subprocess.PIPE)
+  v = p.communicate()[0].strip()
+  r = p.returncode
+  if r != 0:
+    raise subprocess.CalledProcessError(r, ' '.join(cmd))
+  return v