Merge changes I424295df,Ib003f7c8

* changes:
  Treat RawText of binary data as file with one single line.
  Trim boilerplate in RawParseUtils_LineMapTest.
diff --git a/WORKSPACE b/WORKSPACE
index efa161b..8251578 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -105,46 +105,46 @@
     sha1 = "a60a5e993c98c864010053cb901b7eab25306568",
 )
 
-JETTY_VER = "9.3.17.v20170317"
+JETTY_VER = "9.4.5.v20170502"
 
 maven_jar(
     name = "jetty_servlet",
     artifact = "org.eclipse.jetty:jetty-servlet:" + JETTY_VER,
-    sha1 = "ed6986b0d0ca7b9b0f9015c9efb80442e3043a8e",
-    src_sha1 = "ee6b4784a00a92e5c1b6111033b7ae41ac6052a3",
+    sha1 = "394a535b76ca7399b25be3266f06f614e020517e",
+    src_sha1 = "4e85803c8d539aa0a8389e113095ef86032ac425",
 )
 
 maven_jar(
     name = "jetty_security",
     artifact = "org.eclipse.jetty:jetty-security:" + JETTY_VER,
-    sha1 = "ca52535569445682d42aaa97c7039442719a0507",
-    src_sha1 = "2ff9f4fb18b320fd5a0272a427bacc4d5fe7bc86",
+    sha1 = "4f4fc4cbe3504b6c91143ee37b38a1f3de2dcc72",
+    src_sha1 = "2124a757c87eacea7ad6507be6a415b5b51139b5",
 )
 
 maven_jar(
     name = "jetty_server",
     artifact = "org.eclipse.jetty:jetty-server:" + JETTY_VER,
-    sha1 = "194e9a02e6ba249ef4a3f4bd56b4993087992299",
-    src_sha1 = "0c9bd572f530c411592aefb71781ecca0b3719a9",
+    sha1 = "b4d30340213c3d2a5f908860ba170c5a697829be",
+    src_sha1 = "295d873f609a0e2863f33b5dbc8906ca348f1107",
 )
 
 maven_jar(
     name = "jetty_http",
     artifact = "org.eclipse.jetty:jetty-http:" + JETTY_VER,
-    sha1 = "6c02d728e15d4868486254039c867a1ac3e4a52e",
-    src_sha1 = "3c0a2a82792f268631b4fefd77be9f126ec974b1",
+    sha1 = "c51b8a6a67d64672889249dd958edd77bff8fc0c",
+    src_sha1 = "c1bee39aeb565a4f26852b1851192d98ab611dbc",
 )
 
 maven_jar(
     name = "jetty_io",
     artifact = "org.eclipse.jetty:jetty-io:" + JETTY_VER,
-    sha1 = "756a8cd2a1cbfb84a94973b6332dd3eccd47c0cd",
-    src_sha1 = "a9afa99cccb19b441364fa805d027f457cbbb136",
+    sha1 = "76086f955d4e943396b8f340fd5bae3ce4da19d9",
+    src_sha1 = "8d41e410b2f0dd284a6e199ed08f45ef7ab2acf1",
 )
 
 maven_jar(
     name = "jetty_util",
     artifact = "org.eclipse.jetty:jetty-util:" + JETTY_VER,
-    sha1 = "b8512ab02819de01f0f5a5c6026163041f579beb",
-    src_sha1 = "96f8e3dcdc3660a5c91f19c46695daa70ac95625",
+    sha1 = "5fd36dfcf39110b809bd9b20cec62706ab694711",
+    src_sha1 = "629fcda1e4eecfd795e24cc07715ab9797970980",
 )
diff --git a/org.eclipse.jgit.ant.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.ant.test/META-INF/MANIFEST.MF
index 83361e5..b1360ea 100644
--- a/org.eclipse.jgit.ant.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.ant.test/META-INF/MANIFEST.MF
@@ -3,13 +3,13 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %plugin_name
 Bundle-SymbolicName: org.eclipse.jgit.ant.test
-Bundle-Version: 4.8.0.qualifier
+Bundle-Version: 4.9.0.qualifier
 Bundle-ActivationPolicy: lazy
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
 Import-Package: org.apache.tools.ant,
- org.eclipse.jgit.ant.tasks;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.junit;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.lib;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.util;version="[4.8.0,4.9.0)",
+ org.eclipse.jgit.ant.tasks;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.junit;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.lib;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.util;version="[4.9.0,4.10.0)",
  org.hamcrest.core;version="[1.1.0,2.0.0)",
  org.junit;version="[4.0.0,5.0.0)"
diff --git a/org.eclipse.jgit.ant.test/pom.xml b/org.eclipse.jgit.ant.test/pom.xml
index 63021ae..2de42d4 100644
--- a/org.eclipse.jgit.ant.test/pom.xml
+++ b/org.eclipse.jgit.ant.test/pom.xml
@@ -50,7 +50,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>4.8.0-SNAPSHOT</version>
+    <version>4.9.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.ant.test</artifactId>
diff --git a/org.eclipse.jgit.ant/META-INF/MANIFEST.MF b/org.eclipse.jgit.ant/META-INF/MANIFEST.MF
index 6f59d0b..7e18d16 100644
--- a/org.eclipse.jgit.ant/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.ant/META-INF/MANIFEST.MF
@@ -2,11 +2,11 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %Bundle-Name
 Bundle-SymbolicName: org.eclipse.jgit.ant
-Bundle-Version: 4.8.0.qualifier
+Bundle-Version: 4.9.0.qualifier
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
 Import-Package: org.apache.tools.ant,
-  org.eclipse.jgit.storage.file;version="[4.8.0,4.9.0)"
+  org.eclipse.jgit.storage.file;version="[4.9.0,4.10.0)"
 Bundle-Localization: plugin
 Bundle-Vendor: %Provider-Name
-Export-Package: org.eclipse.jgit.ant.tasks;version="4.8.0";
+Export-Package: org.eclipse.jgit.ant.tasks;version="4.9.0";
  uses:="org.apache.tools.ant.types,org.apache.tools.ant"
diff --git a/org.eclipse.jgit.ant/pom.xml b/org.eclipse.jgit.ant/pom.xml
index 9965cde..419dc19 100644
--- a/org.eclipse.jgit.ant/pom.xml
+++ b/org.eclipse.jgit.ant/pom.xml
@@ -48,7 +48,7 @@
 	<parent>
 		<groupId>org.eclipse.jgit</groupId>
 		<artifactId>org.eclipse.jgit-parent</artifactId>
-		<version>4.8.0-SNAPSHOT</version>
+		<version>4.9.0-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>org.eclipse.jgit.ant</artifactId>
diff --git a/org.eclipse.jgit.archive/META-INF/MANIFEST.MF b/org.eclipse.jgit.archive/META-INF/MANIFEST.MF
index ccbbb71..466fea4 100644
--- a/org.eclipse.jgit.archive/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.archive/META-INF/MANIFEST.MF
@@ -2,7 +2,7 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %plugin_name
 Bundle-SymbolicName: org.eclipse.jgit.archive
-Bundle-Version: 4.8.0.qualifier
+Bundle-Version: 4.9.0.qualifier
 Bundle-Vendor: %provider_name
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
@@ -12,15 +12,15 @@
  org.apache.commons.compress.compressors.bzip2;version="[1.4,2.0)",
  org.apache.commons.compress.compressors.gzip;version="[1.4,2.0)",
  org.apache.commons.compress.compressors.xz;version="[1.4,2.0)",
- org.eclipse.jgit.api;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.lib;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.nls;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.revwalk;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.util;version="[4.8.0,4.9.0)",
+ org.eclipse.jgit.api;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.lib;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.nls;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.revwalk;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.util;version="[4.9.0,4.10.0)",
  org.osgi.framework;version="[1.3.0,2.0.0)"
 Bundle-ActivationPolicy: lazy
 Bundle-Activator: org.eclipse.jgit.archive.FormatActivator
-Export-Package: org.eclipse.jgit.archive;version="4.8.0";
+Export-Package: org.eclipse.jgit.archive;version="4.9.0";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.api,
    org.apache.commons.compress.archivers,
diff --git a/org.eclipse.jgit.archive/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.archive/META-INF/SOURCE-MANIFEST.MF
index 6f25f24..29fafd6 100644
--- a/org.eclipse.jgit.archive/META-INF/SOURCE-MANIFEST.MF
+++ b/org.eclipse.jgit.archive/META-INF/SOURCE-MANIFEST.MF
@@ -3,5 +3,5 @@
 Bundle-Name: org.eclipse.jgit.archive - Sources
 Bundle-SymbolicName: org.eclipse.jgit.archive.source
 Bundle-Vendor: Eclipse.org - JGit
-Bundle-Version: 4.8.0.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.archive;version="4.8.0.qualifier";roots="."
+Bundle-Version: 4.9.0.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.archive;version="4.9.0.qualifier";roots="."
diff --git a/org.eclipse.jgit.archive/pom.xml b/org.eclipse.jgit.archive/pom.xml
index 56a5b96..947e59c 100644
--- a/org.eclipse.jgit.archive/pom.xml
+++ b/org.eclipse.jgit.archive/pom.xml
@@ -50,7 +50,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>4.8.0-SNAPSHOT</version>
+    <version>4.9.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.archive</artifactId>
diff --git a/org.eclipse.jgit.http.apache/META-INF/MANIFEST.MF b/org.eclipse.jgit.http.apache/META-INF/MANIFEST.MF
index 01ade87..33ee095 100644
--- a/org.eclipse.jgit.http.apache/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.http.apache/META-INF/MANIFEST.MF
@@ -2,7 +2,7 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %Bundle-Name
 Bundle-SymbolicName: org.eclipse.jgit.http.apache
-Bundle-Version: 4.8.0.qualifier
+Bundle-Version: 4.9.0.qualifier
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
 Bundle-Localization: plugin
 Bundle-Vendor: %Provider-Name
@@ -22,10 +22,10 @@
  org.apache.http.impl.client;version="[4.3.0,5.0.0)",
  org.apache.http.impl.conn;version="[4.3.0,5.0.0)",
  org.apache.http.params;version="[4.3.0,5.0.0)",
- org.eclipse.jgit.nls;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.transport.http;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.util;version="[4.8.0,4.9.0)"
-Export-Package: org.eclipse.jgit.transport.http.apache;version="4.8.0";
+ org.eclipse.jgit.nls;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.transport.http;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.util;version="[4.9.0,4.10.0)"
+Export-Package: org.eclipse.jgit.transport.http.apache;version="4.9.0";
   uses:="org.apache.http.client,
    org.eclipse.jgit.transport.http,
    org.apache.http.entity,
diff --git a/org.eclipse.jgit.http.apache/pom.xml b/org.eclipse.jgit.http.apache/pom.xml
index 214f4ac..3a1434e 100644
--- a/org.eclipse.jgit.http.apache/pom.xml
+++ b/org.eclipse.jgit.http.apache/pom.xml
@@ -48,7 +48,7 @@
 	<parent>
 		<groupId>org.eclipse.jgit</groupId>
 		<artifactId>org.eclipse.jgit-parent</artifactId>
-		<version>4.8.0-SNAPSHOT</version>
+		<version>4.9.0-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>org.eclipse.jgit.http.apache</artifactId>
diff --git a/org.eclipse.jgit.http.server/META-INF/MANIFEST.MF b/org.eclipse.jgit.http.server/META-INF/MANIFEST.MF
index 3e6b667..ae05d78 100644
--- a/org.eclipse.jgit.http.server/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.http.server/META-INF/MANIFEST.MF
@@ -2,13 +2,13 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %plugin_name
 Bundle-SymbolicName: org.eclipse.jgit.http.server
-Bundle-Version: 4.8.0.qualifier
+Bundle-Version: 4.9.0.qualifier
 Bundle-Localization: plugin
 Bundle-Vendor: %provider_name
-Export-Package: org.eclipse.jgit.http.server;version="4.8.0",
- org.eclipse.jgit.http.server.glue;version="4.8.0";
+Export-Package: org.eclipse.jgit.http.server;version="4.9.0",
+ org.eclipse.jgit.http.server.glue;version="4.9.0";
   uses:="javax.servlet,javax.servlet.http",
- org.eclipse.jgit.http.server.resolver;version="4.8.0";
+ org.eclipse.jgit.http.server.resolver;version="4.9.0";
   uses:="org.eclipse.jgit.transport.resolver,
    org.eclipse.jgit.lib,
    org.eclipse.jgit.transport,
@@ -17,12 +17,12 @@
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
 Import-Package: javax.servlet;version="[2.5.0,3.2.0)",
  javax.servlet.http;version="[2.5.0,3.2.0)",
- org.eclipse.jgit.errors;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.internal.storage.dfs;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.internal.storage.file;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.lib;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.nls;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.revwalk;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.transport;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.transport.resolver;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.util;version="[4.8.0,4.9.0)"
+ org.eclipse.jgit.errors;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.internal.storage.dfs;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.internal.storage.file;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.lib;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.nls;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.revwalk;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.transport;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.transport.resolver;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.util;version="[4.9.0,4.10.0)"
diff --git a/org.eclipse.jgit.http.server/pom.xml b/org.eclipse.jgit.http.server/pom.xml
index 3c995b4..3dc3148 100644
--- a/org.eclipse.jgit.http.server/pom.xml
+++ b/org.eclipse.jgit.http.server/pom.xml
@@ -52,7 +52,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>4.8.0-SNAPSHOT</version>
+    <version>4.9.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.http.server</artifactId>
diff --git a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/glue/ServletBinder.java b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/glue/ServletBinder.java
index 9c3ed50..47443f5 100644
--- a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/glue/ServletBinder.java
+++ b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/glue/ServletBinder.java
@@ -60,4 +60,4 @@
 	 *            the servlet to execute on this path.
 	 */
 	public void with(HttpServlet servlet);
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit.http.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.http.test/META-INF/MANIFEST.MF
index a7f7169..421fa8a 100644
--- a/org.eclipse.jgit.http.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.http.test/META-INF/MANIFEST.MF
@@ -2,44 +2,44 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %plugin_name
 Bundle-SymbolicName: org.eclipse.jgit.http.test
-Bundle-Version: 4.8.0.qualifier
+Bundle-Version: 4.9.0.qualifier
 Bundle-Vendor: %provider_name
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
 Import-Package: javax.servlet;version="[2.5.0,3.2.0)",
  javax.servlet.http;version="[2.5.0,3.2.0)",
- org.eclipse.jetty.continuation;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.http;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.io;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.security;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.security.authentication;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.server;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.server.handler;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.server.nio;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.servlet;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.util;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.util.component;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.util.log;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.util.security;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.util.thread;version="[9.0.0,9.4.0)",
- org.eclipse.jgit.errors;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.http.server;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.http.server.glue;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.http.server.resolver;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.internal;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.internal.storage.dfs;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.internal.storage.file;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.junit;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.junit.http;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.lib;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.nls;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.revwalk;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.storage.file;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.transport;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.transport.http;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.transport.http.apache;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.transport.resolver;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.util;version="[4.8.0,4.9.0)",
+ org.eclipse.jetty.continuation;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.http;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.io;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.security;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.security.authentication;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.server;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.server.handler;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.server.nio;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.servlet;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.util;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.util.component;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.util.log;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.util.security;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.util.thread;version="[9.4.5,10.0.0)",
+ org.eclipse.jgit.errors;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.http.server;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.http.server.glue;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.http.server.resolver;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.internal;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.internal.storage.dfs;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.internal.storage.file;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.junit;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.junit.http;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.lib;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.nls;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.revwalk;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.storage.file;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.transport;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.transport.http;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.transport.http.apache;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.transport.resolver;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.util;version="[4.9.0,4.10.0)",
  org.hamcrest.core;version="[1.1.0,2.0.0)",
  org.junit;version="[4.0.0,5.0.0)",
  org.junit.runner;version="[4.0.0,5.0.0)",
diff --git a/org.eclipse.jgit.http.test/pom.xml b/org.eclipse.jgit.http.test/pom.xml
index c1edbe8..7d9c17f 100644
--- a/org.eclipse.jgit.http.test/pom.xml
+++ b/org.eclipse.jgit.http.test/pom.xml
@@ -51,7 +51,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>4.8.0-SNAPSHOT</version>
+    <version>4.9.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.http.test</artifactId>
diff --git a/org.eclipse.jgit.junit.http/META-INF/MANIFEST.MF b/org.eclipse.jgit.junit.http/META-INF/MANIFEST.MF
index c611b31..dd6cb48 100644
--- a/org.eclipse.jgit.junit.http/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.junit.http/META-INF/MANIFEST.MF
@@ -2,7 +2,7 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %plugin_name
 Bundle-SymbolicName: org.eclipse.jgit.junit.http
-Bundle-Version: 4.8.0.qualifier
+Bundle-Version: 4.9.0.qualifier
 Bundle-Localization: plugin
 Bundle-Vendor: %provider_name
 Bundle-ActivationPolicy: lazy
@@ -10,26 +10,26 @@
 Import-Package: javax.servlet;version="[2.5.0,3.2.0)",
  javax.servlet.http;version="[2.5.0,3.2.0)",
  org.apache.commons.logging;version="[1.1.1,2.0.0)",
- org.eclipse.jetty.http;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.security;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.security.authentication;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.server;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.server.handler;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.server.nio;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.servlet;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.util.component;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.util.log;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.util.security;version="[9.0.0,9.4.0)",
- org.eclipse.jgit.errors;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.http.server;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.internal.storage.file;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.junit;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.lib;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.revwalk;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.transport;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.transport.resolver;version="[4.8.0,4.9.0)",
+ org.eclipse.jetty.http;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.security;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.security.authentication;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.server;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.server.handler;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.server.nio;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.servlet;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.util.component;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.util.log;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.util.security;version="[9.4.5,10.0.0)",
+ org.eclipse.jgit.errors;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.http.server;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.internal.storage.file;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.junit;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.lib;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.revwalk;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.transport;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.transport.resolver;version="[4.9.0,4.10.0)",
  org.junit;version="[4.0.0,5.0.0)"
-Export-Package: org.eclipse.jgit.junit.http;version="4.8.0";
+Export-Package: org.eclipse.jgit.junit.http;version="4.9.0";
   uses:="org.eclipse.jgit.transport,
    org.eclipse.jgit.junit,
    javax.servlet.http,
diff --git a/org.eclipse.jgit.junit.http/pom.xml b/org.eclipse.jgit.junit.http/pom.xml
index 4ec39b1..c6ad848 100644
--- a/org.eclipse.jgit.junit.http/pom.xml
+++ b/org.eclipse.jgit.junit.http/pom.xml
@@ -50,7 +50,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>4.8.0-SNAPSHOT</version>
+    <version>4.9.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.junit.http</artifactId>
diff --git a/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/AppServer.java b/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/AppServer.java
index 9ef4068..28c0f21 100644
--- a/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/AppServer.java
+++ b/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/AppServer.java
@@ -46,25 +46,25 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
-import java.io.IOException;
 import java.net.InetAddress;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.UnknownHostException;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
 
+import org.eclipse.jetty.security.AbstractLoginService;
 import org.eclipse.jetty.security.Authenticator;
 import org.eclipse.jetty.security.ConstraintMapping;
 import org.eclipse.jetty.security.ConstraintSecurityHandler;
-import org.eclipse.jetty.security.MappedLoginService;
 import org.eclipse.jetty.security.authentication.BasicAuthenticator;
 import org.eclipse.jetty.server.Connector;
 import org.eclipse.jetty.server.HttpConfiguration;
 import org.eclipse.jetty.server.HttpConnectionFactory;
 import org.eclipse.jetty.server.Server;
 import org.eclipse.jetty.server.ServerConnector;
-import org.eclipse.jetty.server.UserIdentity;
 import org.eclipse.jetty.server.handler.ContextHandlerCollection;
 import org.eclipse.jetty.servlet.ServletContextHandler;
 import org.eclipse.jetty.util.security.Constraint;
@@ -168,38 +168,41 @@
 		return ctx;
 	}
 
-	static class TestMappedLoginService extends MappedLoginService {
+	static class TestMappedLoginService extends AbstractLoginService {
 		private String role;
 
+		protected final ConcurrentMap<String, UserPrincipal> users = new ConcurrentHashMap<>();
+
 		TestMappedLoginService(String role) {
 			this.role = role;
 		}
 
 		@Override
-		protected UserIdentity loadUser(String who) {
-			return null;
+		protected void doStart() throws Exception {
+			UserPrincipal p = new UserPrincipal(username,
+					new Password(password));
+			users.put(username, p);
+			super.doStart();
 		}
 
 		@Override
-		protected void loadUsers() throws IOException {
-			putUser(username, new Password(password), new String[] { role });
+		protected String[] loadRoleInfo(UserPrincipal user) {
+			if (users.get(user.getName()) == null)
+				return null;
+			else
+				return new String[] { role };
 		}
 
 		@Override
-		protected String[] loadRoleInfo(KnownUser user) {
-			return null;
-		}
-
-		@Override
-		protected KnownUser loadUserInfo(String usrname) {
-			return null;
+		protected UserPrincipal loadUserInfo(String user) {
+			return users.get(user);
 		}
 	}
 
 	private void auth(ServletContextHandler ctx, Authenticator authType) {
 		final String role = "can-access";
 
-		MappedLoginService users = new TestMappedLoginService(role);
+		AbstractLoginService users = new TestMappedLoginService(role);
 		ConstraintMapping cm = new ConstraintMapping();
 		cm.setConstraint(new Constraint());
 		cm.getConstraint().setAuthenticate(true);
diff --git a/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/MockServletConfig.java b/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/MockServletConfig.java
index 9defcd9..03c0816 100644
--- a/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/MockServletConfig.java
+++ b/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/MockServletConfig.java
@@ -88,4 +88,4 @@
 	public ServletContext getServletContext() {
 		return null;
 	}
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/RecordingLogger.java b/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/RecordingLogger.java
index 415398d..4e35ff6 100644
--- a/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/RecordingLogger.java
+++ b/org.eclipse.jgit.junit.http/src/org/eclipse/jgit/junit/http/RecordingLogger.java
@@ -161,6 +161,12 @@
 	@Override
 	public void warn(String msg, Object... args) {
 		synchronized (warnings) {
+			int i = 0;
+			int index = msg.indexOf("{}");
+			while (index >= 0) {
+				msg = msg.replaceFirst("\\{\\}", "{" + i++ + "}");
+				index = msg.indexOf("{}");
+			}
 			warnings.add(new Warning(MessageFormat.format(msg, args)));
 		}
 	}
diff --git a/org.eclipse.jgit.junit/META-INF/MANIFEST.MF b/org.eclipse.jgit.junit/META-INF/MANIFEST.MF
index 8910659..e077f3f 100644
--- a/org.eclipse.jgit.junit/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.junit/META-INF/MANIFEST.MF
@@ -2,31 +2,31 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %plugin_name
 Bundle-SymbolicName: org.eclipse.jgit.junit
-Bundle-Version: 4.8.0.qualifier
+Bundle-Version: 4.9.0.qualifier
 Bundle-Localization: plugin
 Bundle-Vendor: %provider_name
 Bundle-ActivationPolicy: lazy
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
-Import-Package: org.eclipse.jgit.api;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.api.errors;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.dircache;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.errors;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.internal.storage.file;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.internal.storage.pack;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.lib;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.merge;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.revwalk;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.storage.file;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.treewalk;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.treewalk.filter;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.util;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.util.io;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.util.time;version="[4.8.0,4.9.0)",
+Import-Package: org.eclipse.jgit.api;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.api.errors;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.dircache;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.errors;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.internal.storage.file;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.internal.storage.pack;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.lib;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.merge;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.revwalk;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.storage.file;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.treewalk;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.treewalk.filter;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.util;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.util.io;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.util.time;version="[4.9.0,4.10.0)",
  org.junit;version="[4.0.0,5.0.0)",
  org.junit.rules;version="[4.9.0,5.0.0)",
  org.junit.runner;version="[4.0.0,5.0.0)",
  org.junit.runners.model;version="[4.5.0,5.0.0)"
-Export-Package: org.eclipse.jgit.junit;version="4.8.0";
+Export-Package: org.eclipse.jgit.junit;version="4.9.0";
   uses:="org.eclipse.jgit.dircache,
    org.eclipse.jgit.lib,
    org.eclipse.jgit.revwalk,
@@ -35,4 +35,4 @@
    org.eclipse.jgit.util,
    org.eclipse.jgit.storage.file,
    org.eclipse.jgit.api",
- org.eclipse.jgit.junit.time;version="4.8.0"
+ org.eclipse.jgit.junit.time;version="4.9.0"
diff --git a/org.eclipse.jgit.junit/pom.xml b/org.eclipse.jgit.junit/pom.xml
index fb37640..b4cbc2e 100644
--- a/org.eclipse.jgit.junit/pom.xml
+++ b/org.eclipse.jgit.junit/pom.xml
@@ -52,7 +52,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>4.8.0-SNAPSHOT</version>
+    <version>4.9.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.junit</artifactId>
diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/JGitTestUtil.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/JGitTestUtil.java
index 2962e71..5bf61f0 100644
--- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/JGitTestUtil.java
+++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/JGitTestUtil.java
@@ -258,4 +258,27 @@
 				target);
 	}
 
+	/**
+	 * Concatenate byte arrays.
+	 *
+	 * @param b
+	 *            byte arrays to combine together.
+	 * @return a single byte array that contains all bytes copied from input
+	 *         byte arrays.
+	 * @since 4.9
+	 */
+	public 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;
+	}
 }
diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java
index 933faf9..6ace9fc 100644
--- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java
+++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/LocalDiskRepositoryTestCase.java
@@ -62,6 +62,7 @@
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.dircache.DirCacheEntry;
 import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -121,6 +122,12 @@
 		mockSystemReader = new MockSystemReader();
 		mockSystemReader.userGitConfig = new FileBasedConfig(new File(tmp,
 				"usergitconfig"), FS.DETECTED);
+		// We have to set autoDetach to false for tests, because tests expect to be able
+		// to clean up by recursively removing the repository, and background GC might be
+		// in the middle of writing or deleting files, which would disrupt this.
+		mockSystemReader.userGitConfig.setBoolean(ConfigConstants.CONFIG_GC_SECTION,
+				null, ConfigConstants.CONFIG_KEY_AUTODETACH, false);
+		mockSystemReader.userGitConfig.save();
 		ceilTestDirectories(getCeilings());
 		SystemReader.setInstance(mockSystemReader);
 
diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/Repeat.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/Repeat.java
index 22b5007..a3c869f 100644
--- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/Repeat.java
+++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/Repeat.java
@@ -50,4 +50,4 @@
 @Target({ java.lang.annotation.ElementType.METHOD })
 public @interface Repeat {
 	public abstract int n();
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepeatRule.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepeatRule.java
index 75e1a67..4230073 100644
--- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepeatRule.java
+++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepeatRule.java
@@ -128,4 +128,4 @@
 		}
 		return result;
 	}
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackKey.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/StrictWorkMonitor.java
similarity index 73%
rename from org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackKey.java
rename to org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/StrictWorkMonitor.java
index 98a2a94..22b69a3 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackKey.java
+++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/StrictWorkMonitor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2011, Google Inc.
+ * Copyright (C) 2017 Google Inc.
  * and other copyright owners as documented in the project's IP log.
  *
  * This program and the accompanying materials are made available
@@ -41,20 +41,38 @@
  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-package org.eclipse.jgit.internal.storage.dfs;
+package org.eclipse.jgit.junit;
 
-import java.util.concurrent.atomic.AtomicLong;
+import static org.junit.Assert.assertEquals;
 
-final class DfsPackKey {
-	final int hash;
+import org.eclipse.jgit.lib.ProgressMonitor;
 
-	final AtomicLong cachedSize;
+public final class StrictWorkMonitor implements ProgressMonitor {
+	private int lastWork, totalWork;
 
-	DfsPackKey() {
-		// Multiply by 31 here so we can more directly combine with another
-		// value without doing the multiply there.
-		//
-		hash = System.identityHashCode(this) * 31;
-		cachedSize = new AtomicLong();
+	@Override
+	public void start(int totalTasks) {
+		// empty
+	}
+
+	@Override
+	public void beginTask(String title, int total) {
+		this.totalWork = total;
+		lastWork = 0;
+	}
+
+	@Override
+	public void update(int completed) {
+		lastWork += completed;
+	}
+
+	@Override
+	public void endTask() {
+		assertEquals("Units of work recorded", totalWork, lastWork);
+	}
+
+	@Override
+	public boolean isCancelled() {
+		return false;
 	}
 }
diff --git a/org.eclipse.jgit.lfs.server.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.lfs.server.test/META-INF/MANIFEST.MF
index f78e812..683e337 100644
--- a/org.eclipse.jgit.lfs.server.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.lfs.server.test/META-INF/MANIFEST.MF
@@ -2,7 +2,7 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %plugin_name
 Bundle-SymbolicName: org.eclipse.jgit.lfs.server.test
-Bundle-Version: 4.8.0.qualifier
+Bundle-Version: 4.9.0.qualifier
 Bundle-Vendor: %provider_name
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
@@ -13,25 +13,25 @@
  org.apache.http.client.methods;version="[4.3.0,5.0.0)",
  org.apache.http.entity;version="[4.3.0,5.0.0)",
  org.apache.http.impl.client;version="[4.3.0,5.0.0)",
- org.eclipse.jetty.continuation;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.http;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.io;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.security;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.security.authentication;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.server;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.server.handler;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.server.nio;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.servlet;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.util;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.util.component;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.util.log;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.util.security;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.util.thread;version="[9.0.0,9.4.0)",
- org.eclipse.jgit.junit.http;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.lfs.lib;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.lfs.server.fs;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.lfs.test;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.util;version="[4.8.0,4.9.0)",
+ org.eclipse.jetty.continuation;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.http;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.io;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.security;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.security.authentication;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.server;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.server.handler;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.server.nio;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.servlet;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.util;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.util.component;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.util.log;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.util.security;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.util.thread;version="[9.4.5,10.0.0)",
+ org.eclipse.jgit.junit.http;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.lfs.lib;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.lfs.server.fs;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.lfs.test;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.util;version="[4.9.0,4.10.0)",
  org.hamcrest.core;version="[1.1.0,2.0.0)",
  org.junit;version="[4.0.0,5.0.0)",
  org.junit.runner;version="[4.0.0,5.0.0)",
diff --git a/org.eclipse.jgit.lfs.server.test/pom.xml b/org.eclipse.jgit.lfs.server.test/pom.xml
index 0d10fc7..7539940 100644
--- a/org.eclipse.jgit.lfs.server.test/pom.xml
+++ b/org.eclipse.jgit.lfs.server.test/pom.xml
@@ -50,7 +50,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>4.8.0-SNAPSHOT</version>
+    <version>4.9.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.lfs.server.test</artifactId>
diff --git a/org.eclipse.jgit.lfs.server.test/tst/org/eclipse/jgit/lfs/server/fs/LfsServerTest.java b/org.eclipse.jgit.lfs.server.test/tst/org/eclipse/jgit/lfs/server/fs/LfsServerTest.java
index e10660d..5da502e 100644
--- a/org.eclipse.jgit.lfs.server.test/tst/org/eclipse/jgit/lfs/server/fs/LfsServerTest.java
+++ b/org.eclipse.jgit.lfs.server.test/tst/org/eclipse/jgit/lfs/server/fs/LfsServerTest.java
@@ -265,4 +265,4 @@
 		}
 		return Files.size(f);
 	}
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit.lfs.server/META-INF/MANIFEST.MF b/org.eclipse.jgit.lfs.server/META-INF/MANIFEST.MF
index d13ec99..1ec0cfb 100644
--- a/org.eclipse.jgit.lfs.server/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.lfs.server/META-INF/MANIFEST.MF
@@ -2,19 +2,19 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %plugin_name
 Bundle-SymbolicName: org.eclipse.jgit.lfs.server
-Bundle-Version: 4.8.0.qualifier
+Bundle-Version: 4.9.0.qualifier
 Bundle-Localization: plugin
 Bundle-Vendor: %provider_name
-Export-Package: org.eclipse.jgit.lfs.server;version="4.8.0";
+Export-Package: org.eclipse.jgit.lfs.server;version="4.9.0";
   uses:="javax.servlet.http,
    org.eclipse.jgit.lfs.lib",
- org.eclipse.jgit.lfs.server.fs;version="4.8.0";
+ org.eclipse.jgit.lfs.server.fs;version="4.9.0";
   uses:="javax.servlet,
    javax.servlet.http,
    org.eclipse.jgit.lfs.server,
    org.eclipse.jgit.lfs.lib",
- org.eclipse.jgit.lfs.server.internal;version="4.8.0";x-internal:=true,
- org.eclipse.jgit.lfs.server.s3;version="4.8.0";
+ org.eclipse.jgit.lfs.server.internal;version="4.9.0";x-internal:=true,
+ org.eclipse.jgit.lfs.server.s3;version="4.9.0";
   uses:="org.eclipse.jgit.lfs.server,
    org.eclipse.jgit.lfs.lib"
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
@@ -24,14 +24,14 @@
  javax.servlet.http;version="[3.1.0,4.0.0)",
  org.apache.http;version="[4.3.0,5.0.0)",
  org.apache.http.client;version="[4.3.0,5.0.0)",
- org.eclipse.jgit.annotations;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.internal;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.internal.storage.file;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.lfs.errors;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.lfs.internal;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.lfs.lib;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.nls;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.transport.http;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.transport.http.apache;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.util;version="[4.8.0,4.9.0)",
+ org.eclipse.jgit.annotations;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.internal;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.internal.storage.file;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.lfs.errors;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.lfs.internal;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.lfs.lib;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.nls;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.transport.http;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.transport.http.apache;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.util;version="[4.9.0,4.10.0)",
  org.slf4j;version="[1.7.0,2.0.0)"
diff --git a/org.eclipse.jgit.lfs.server/pom.xml b/org.eclipse.jgit.lfs.server/pom.xml
index 9d487ae..1f03ae7 100644
--- a/org.eclipse.jgit.lfs.server/pom.xml
+++ b/org.eclipse.jgit.lfs.server/pom.xml
@@ -50,7 +50,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>4.8.0-SNAPSHOT</version>
+    <version>4.9.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.lfs.server</artifactId>
@@ -140,25 +140,6 @@
           </archive>
         </configuration>
       </plugin>
-
-      <plugin>
-        <groupId>org.codehaus.mojo</groupId>
-        <artifactId>clirr-maven-plugin</artifactId>
-      </plugin>
     </plugins>
   </build>
-
-  <reporting>
-    <plugins>
-      <plugin>
-        <groupId>org.codehaus.mojo</groupId>
-        <artifactId>clirr-maven-plugin</artifactId>
-        <version>${clirr-version}</version>
-        <configuration>
-          <comparisonVersion>${jgit-last-release-version}</comparisonVersion>
-          <minSeverity>info</minSeverity>
-        </configuration>
-      </plugin>
-    </plugins>
-  </reporting>
 </project>
diff --git a/org.eclipse.jgit.lfs.server/src/org/eclipse/jgit/lfs/server/TransferHandler.java b/org.eclipse.jgit.lfs.server/src/org/eclipse/jgit/lfs/server/TransferHandler.java
index 86ca2d3..4fea92e 100644
--- a/org.eclipse.jgit.lfs.server/src/org/eclipse/jgit/lfs/server/TransferHandler.java
+++ b/org.eclipse.jgit.lfs.server/src/org/eclipse/jgit/lfs/server/TransferHandler.java
@@ -164,4 +164,4 @@
 	}
 
 	abstract Response.Body process() throws IOException;
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit.lfs.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.lfs.test/META-INF/MANIFEST.MF
index 78dc263..b9ecf90 100644
--- a/org.eclipse.jgit.lfs.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.lfs.test/META-INF/MANIFEST.MF
@@ -2,23 +2,23 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %plugin_name
 Bundle-SymbolicName: org.eclipse.jgit.lfs.test
-Bundle-Version: 4.8.0.qualifier
+Bundle-Version: 4.9.0.qualifier
 Bundle-Vendor: %provider_name
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
-Import-Package: org.eclipse.jgit.internal.storage.dfs;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.junit;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.lfs;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.lfs.errors;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.lfs.lib;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.lib;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.revwalk;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.treewalk;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.treewalk.filter;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.util;version="[4.8.0,4.9.0)",
+Import-Package: org.eclipse.jgit.internal.storage.dfs;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.junit;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.lfs;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.lfs.errors;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.lfs.lib;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.lib;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.revwalk;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.treewalk;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.treewalk.filter;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.util;version="[4.9.0,4.10.0)",
  org.hamcrest.core;version="[1.1.0,2.0.0)",
  org.junit;version="[4.0.0,5.0.0)",
  org.junit.runner;version="[4.0.0,5.0.0)",
  org.junit.runners;version="[4.0.0,5.0.0)"
-Export-Package: org.eclipse.jgit.lfs.test;version="4.8.0";x-friends:="org.eclipse.jgit.lfs.server.test"
+Export-Package: org.eclipse.jgit.lfs.test;version="4.9.0";x-friends:="org.eclipse.jgit.lfs.server.test"
 
diff --git a/org.eclipse.jgit.lfs.test/pom.xml b/org.eclipse.jgit.lfs.test/pom.xml
index 8268c85..bf0d5d6 100644
--- a/org.eclipse.jgit.lfs.test/pom.xml
+++ b/org.eclipse.jgit.lfs.test/pom.xml
@@ -50,7 +50,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>4.8.0-SNAPSHOT</version>
+    <version>4.9.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.lfs.test</artifactId>
diff --git a/org.eclipse.jgit.lfs/META-INF/MANIFEST.MF b/org.eclipse.jgit.lfs/META-INF/MANIFEST.MF
index daf4a56..bf94686 100644
--- a/org.eclipse.jgit.lfs/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.lfs/META-INF/MANIFEST.MF
@@ -2,20 +2,20 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %plugin_name
 Bundle-SymbolicName: org.eclipse.jgit.lfs
-Bundle-Version: 4.8.0.qualifier
+Bundle-Version: 4.9.0.qualifier
 Bundle-Localization: plugin
 Bundle-Vendor: %provider_name
-Export-Package: org.eclipse.jgit.lfs;version="4.8.0",
- org.eclipse.jgit.lfs.errors;version="4.8.0",
- org.eclipse.jgit.lfs.internal;version="4.8.0";x-friends:="org.eclipse.jgit.lfs.test,org.eclipse.jgit.lfs.server.fs,org.eclipse.jgit.lfs.server",
- org.eclipse.jgit.lfs.lib;version="4.8.0"
+Export-Package: org.eclipse.jgit.lfs;version="4.9.0",
+ org.eclipse.jgit.lfs.errors;version="4.9.0",
+ org.eclipse.jgit.lfs.internal;version="4.9.0";x-friends:="org.eclipse.jgit.lfs.test,org.eclipse.jgit.lfs.server.fs,org.eclipse.jgit.lfs.server",
+ org.eclipse.jgit.lfs.lib;version="4.9.0"
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
-Import-Package: org.eclipse.jgit.annotations;version="[4.8.0,4.9.0)";resolution:=optional,
- org.eclipse.jgit.attributes;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.errors;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.internal.storage.file;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.lib;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.nls;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.treewalk;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.treewalk.filter;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.util;version="[4.8.0,4.9.0)"
+Import-Package: org.eclipse.jgit.annotations;version="[4.9.0,4.10.0)";resolution:=optional,
+ org.eclipse.jgit.attributes;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.errors;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.internal.storage.file;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.lib;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.nls;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.treewalk;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.treewalk.filter;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.util;version="[4.9.0,4.10.0)"
diff --git a/org.eclipse.jgit.lfs/pom.xml b/org.eclipse.jgit.lfs/pom.xml
index f2ec75c..f3616c3 100644
--- a/org.eclipse.jgit.lfs/pom.xml
+++ b/org.eclipse.jgit.lfs/pom.xml
@@ -50,7 +50,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>4.8.0-SNAPSHOT</version>
+    <version>4.9.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.lfs</artifactId>
@@ -116,25 +116,6 @@
           </archive>
         </configuration>
       </plugin>
-
-      <plugin>
-        <groupId>org.codehaus.mojo</groupId>
-        <artifactId>clirr-maven-plugin</artifactId>
-      </plugin>
     </plugins>
   </build>
-
-  <reporting>
-    <plugins>
-      <plugin>
-        <groupId>org.codehaus.mojo</groupId>
-        <artifactId>clirr-maven-plugin</artifactId>
-        <version>${clirr-version}</version>
-        <configuration>
-          <comparisonVersion>${jgit-last-release-version}</comparisonVersion>
-          <minSeverity>info</minSeverity>
-        </configuration>
-      </plugin>
-    </plugins>
-  </reporting>
 </project>
diff --git a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/AtomicObjectOutputStream.java b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/AtomicObjectOutputStream.java
index 867cca5..1598b9e 100644
--- a/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/AtomicObjectOutputStream.java
+++ b/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/internal/AtomicObjectOutputStream.java
@@ -146,4 +146,4 @@
 		locked.unlock();
 		aborted = true;
 	}
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.feature/feature.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.feature/feature.xml
index 4ea616f..46b9895 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.feature/feature.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.feature/feature.xml
@@ -2,7 +2,7 @@
 <feature
       id="org.eclipse.jgit"
       label="%featureName"
-      version="4.8.0.qualifier"
+      version="4.9.0.qualifier"
       provider-name="%providerName">
 
    <description url="http://www.eclipse.org/jgit/">
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.feature/pom.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.feature/pom.xml
index 92b9a5f..356b407 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.feature/pom.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.feature/pom.xml
@@ -50,7 +50,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>jgit.tycho.parent</artifactId>
-    <version>4.8.0-SNAPSHOT</version>
+    <version>4.9.0-SNAPSHOT</version>
   </parent>
 
   <groupId>org.eclipse.jgit.feature</groupId>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.http.apache.feature/feature.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.http.apache.feature/feature.xml
index b7dc997..2a82d35 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.http.apache.feature/feature.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.http.apache.feature/feature.xml
@@ -2,7 +2,7 @@
 <feature
       id="org.eclipse.jgit.http.apache"
       label="%featureName"
-      version="4.8.0.qualifier"
+      version="4.9.0.qualifier"
       provider-name="%providerName">
 
    <description url="http://www.eclipse.org/jgit/">
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.http.apache.feature/pom.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.http.apache.feature/pom.xml
index dfbc5ce..4318e2b 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.http.apache.feature/pom.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.http.apache.feature/pom.xml
@@ -50,7 +50,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>jgit.tycho.parent</artifactId>
-    <version>4.8.0-SNAPSHOT</version>
+    <version>4.9.0-SNAPSHOT</version>
   </parent>
 
   <groupId>org.eclipse.jgit.feature</groupId>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.junit.feature/feature.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.junit.feature/feature.xml
index 429877c..7ff5a24 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.junit.feature/feature.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.junit.feature/feature.xml
@@ -2,7 +2,7 @@
 <feature
       id="org.eclipse.jgit.junit"
       label="%featureName"
-      version="4.8.0.qualifier"
+      version="4.9.0.qualifier"
       provider-name="%providerName">
 
    <description url="http://www.eclipse.org/jgit/">
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.junit.feature/pom.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.junit.feature/pom.xml
index e3ebb83..cdbc48b 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.junit.feature/pom.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.junit.feature/pom.xml
@@ -50,7 +50,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>jgit.tycho.parent</artifactId>
-    <version>4.8.0-SNAPSHOT</version>
+    <version>4.9.0-SNAPSHOT</version>
   </parent>
 
   <groupId>org.eclipse.jgit.feature</groupId>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.lfs.feature/feature.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.lfs.feature/feature.xml
index 2453965..d064c9a 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.lfs.feature/feature.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.lfs.feature/feature.xml
@@ -2,7 +2,7 @@
 <feature
       id="org.eclipse.jgit.lfs"
       label="%featureName"
-      version="4.8.0.qualifier"
+      version="4.9.0.qualifier"
       provider-name="%providerName">
 
    <description url="http://www.eclipse.org/jgit/">
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.lfs.feature/pom.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.lfs.feature/pom.xml
index eacab7b..3ce89d5 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.lfs.feature/pom.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.lfs.feature/pom.xml
@@ -50,7 +50,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>jgit.tycho.parent</artifactId>
-    <version>4.8.0-SNAPSHOT</version>
+    <version>4.9.0-SNAPSHOT</version>
   </parent>
 
   <groupId>org.eclipse.jgit.feature</groupId>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.pgm.feature/feature.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.pgm.feature/feature.xml
index a7af646..d28f0c6 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.pgm.feature/feature.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.pgm.feature/feature.xml
@@ -2,7 +2,7 @@
 <feature
       id="org.eclipse.jgit.pgm"
       label="%featureName"
-      version="4.8.0.qualifier"
+      version="4.9.0.qualifier"
       provider-name="%providerName">
 
    <description url="http://www.eclipse.org/jgit/">
@@ -31,8 +31,8 @@
          version="0.0.0"/>
 
    <requires>
-      <import feature="org.eclipse.jgit" version="4.8.0" match="equivalent"/>
-      <import feature="org.eclipse.jgit.lfs" version="4.8.0" match="equivalent"/>
+      <import feature="org.eclipse.jgit" version="4.9.0" match="equivalent"/>
+      <import feature="org.eclipse.jgit.lfs" version="4.9.0" match="equivalent"/>
    </requires>
 
    <plugin
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.pgm.feature/pom.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.pgm.feature/pom.xml
index d1498e8..8feceb0 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.pgm.feature/pom.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.pgm.feature/pom.xml
@@ -50,7 +50,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>jgit.tycho.parent</artifactId>
-    <version>4.8.0-SNAPSHOT</version>
+    <version>4.9.0-SNAPSHOT</version>
   </parent>
 
   <groupId>org.eclipse.jgit.feature</groupId>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.pgm.source.feature/feature.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.pgm.source.feature/feature.xml
index a1f9098..e59e738 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.pgm.source.feature/feature.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.pgm.source.feature/feature.xml
@@ -2,7 +2,7 @@
 <feature
       id="org.eclipse.jgit.pgm.source"
       label="%featureName"
-      version="4.8.0.qualifier"
+      version="4.9.0.qualifier"
       provider-name="%providerName">
 
    <description url="http://www.eclipse.org/jgit/">
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.pgm.source.feature/pom.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.pgm.source.feature/pom.xml
index ac6f987..4ba3edc 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.pgm.source.feature/pom.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.pgm.source.feature/pom.xml
@@ -50,7 +50,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>jgit.tycho.parent</artifactId>
-    <version>4.8.0-SNAPSHOT</version>
+    <version>4.9.0-SNAPSHOT</version>
   </parent>
 
   <groupId>org.eclipse.jgit.feature</groupId>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.repository/pom.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.repository/pom.xml
index ed9446a..7784c0b 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.repository/pom.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.repository/pom.xml
@@ -50,7 +50,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>jgit.tycho.parent</artifactId>
-    <version>4.8.0-SNAPSHOT</version>
+    <version>4.9.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.repository</artifactId>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.source.feature/feature.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.source.feature/feature.xml
index 65301d6..6b6a18d 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.source.feature/feature.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.source.feature/feature.xml
@@ -2,7 +2,7 @@
 <feature
       id="org.eclipse.jgit.source"
       label="%featureName"
-      version="4.8.0.qualifier"
+      version="4.9.0.qualifier"
       provider-name="%providerName">
 
    <description url="http://www.eclipse.org/jgit/">
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.source.feature/pom.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.source.feature/pom.xml
index 857d9e5..a4fb063 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.source.feature/pom.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.source.feature/pom.xml
@@ -50,7 +50,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>jgit.tycho.parent</artifactId>
-    <version>4.8.0-SNAPSHOT</version>
+    <version>4.9.0-SNAPSHOT</version>
   </parent>
 
   <groupId>org.eclipse.jgit.feature</groupId>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/META-INF/MANIFEST.MF b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/META-INF/MANIFEST.MF
index ceaf8b8..38d7805 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/META-INF/MANIFEST.MF
@@ -2,4 +2,4 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: JGit Target Platform Bundle
 Bundle-SymbolicName: org.eclipse.jgit.target
-Bundle-Version: 4.8.0.qualifier
+Bundle-Version: 4.9.0.qualifier
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.5.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.5.target
index 1d537e6..b2099ae 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.5.target
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.5.target
@@ -1,26 +1,26 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <?pde?>
 <!-- generated with https://github.com/mbarbero/fr.obeo.releng.targetplatform -->
-<target name="jgit-4.5" sequenceNumber="1491140572">
+<target name="jgit-4.5" sequenceNumber="1496008880">
   <locations>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="org.eclipse.jetty.client" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.client.source" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.continuation" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.continuation.source" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.http" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.http.source" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.io" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.io.source" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.security" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.security.source" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.server" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.server.source" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.servlet" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.servlet.source" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.util" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.util.source" version="9.3.17.v20170317"/>
-      <repository id="jetty-9.3.17" location="http://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.3.17.v20170317/"/>
+      <unit id="org.eclipse.jetty.client" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.client.source" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.continuation" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.continuation.source" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.http" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.http.source" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.io" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.io.source" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.security" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.security.source" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.server" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.server.source" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.servlet" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.servlet.source" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.util" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.util.source" version="9.4.5.v20170502"/>
+      <repository id="jetty-9.4.5" location="http://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.4.5.v20170502/"/>
     </location>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
       <unit id="org.apache.ant" version="1.9.4.v201504302020"/>
@@ -98,7 +98,7 @@
       <unit id="org.slf4j.api.source" version="1.7.2.v20121108-1250"/>
       <unit id="org.slf4j.impl.log4j12" version="1.7.2.v20131105-2200"/>
       <unit id="org.slf4j.impl.log4j12.source" version="1.7.2.v20131105-2200"/>
-      <repository location="http://download.eclipse.org/tools/orbit/downloads/drops/S20170306214312/repository"/>
+      <repository location="http://download.eclipse.org/tools/orbit/downloads/drops/R20170516192513/repository"/>
     </location>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
       <unit id="org.eclipse.osgi" version="0.0.0"/>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.5.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.5.tpd
index ea4f5f6..f9653b2 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.5.tpd
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.5.tpd
@@ -1,8 +1,8 @@
 target "jgit-4.5" with source configurePhase
 
-include "projects/jetty-9.3.17.tpd"
+include "projects/jetty-9.4.5.tpd"
 include "orbit/R20160221192158-Mars.tpd"
-include "orbit/S20170306214312-Oxygen.tpd"
+include "orbit/R20170516192513-Oxygen.tpd"
 
 location "http://download.eclipse.org/releases/mars/" {
 	org.eclipse.osgi lazy
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.6.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.6.target
index babcf09..e4baa5d 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.6.target
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.6.target
@@ -1,26 +1,26 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <?pde?>
 <!-- generated with https://github.com/mbarbero/fr.obeo.releng.targetplatform -->
-<target name="jgit-4.6" sequenceNumber="1491140561">
+<target name="jgit-4.6" sequenceNumber="1496008884">
   <locations>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="org.eclipse.jetty.client" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.client.source" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.continuation" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.continuation.source" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.http" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.http.source" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.io" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.io.source" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.security" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.security.source" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.server" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.server.source" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.servlet" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.servlet.source" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.util" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.util.source" version="9.3.17.v20170317"/>
-      <repository id="jetty-9.3.17" location="http://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.3.17.v20170317/"/>
+      <unit id="org.eclipse.jetty.client" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.client.source" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.continuation" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.continuation.source" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.http" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.http.source" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.io" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.io.source" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.security" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.security.source" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.server" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.server.source" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.servlet" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.servlet.source" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.util" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.util.source" version="9.4.5.v20170502"/>
+      <repository id="jetty-9.4.5" location="http://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.4.5.v20170502/"/>
     </location>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
       <unit id="org.apache.ant" version="1.9.6.v201510161327"/>
@@ -60,7 +60,7 @@
       <unit id="org.slf4j.api.source" version="1.7.2.v20121108-1250"/>
       <unit id="org.slf4j.impl.log4j12" version="1.7.2.v20131105-2200"/>
       <unit id="org.slf4j.impl.log4j12.source" version="1.7.2.v20131105-2200"/>
-      <repository location="http://download.eclipse.org/tools/orbit/downloads/drops/S20170306214312/repository"/>
+      <repository location="http://download.eclipse.org/tools/orbit/downloads/drops/R20170516192513/repository"/>
     </location>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
       <unit id="org.eclipse.osgi" version="0.0.0"/>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.6.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.6.tpd
index b699452..9ddba2d 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.6.tpd
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.6.tpd
@@ -1,7 +1,7 @@
 target "jgit-4.6" with source configurePhase
 
-include "projects/jetty-9.3.17.tpd"
-include "orbit/S20170306214312-Oxygen.tpd"
+include "projects/jetty-9.4.5.tpd"
+include "orbit/R20170516192513-Oxygen.tpd"
 
 location "http://download.eclipse.org/releases/neon/" {
 	org.eclipse.osgi lazy
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.7.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.7.target
index 5e4f555..9a15741 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.7.target
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.7.target
@@ -1,26 +1,26 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <?pde?>
 <!-- generated with https://github.com/mbarbero/fr.obeo.releng.targetplatform -->
-<target name="jgit-4.7" sequenceNumber="1491140544">
+<target name="jgit-4.7" sequenceNumber="1496008862">
   <locations>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="org.eclipse.jetty.client" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.client.source" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.continuation" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.continuation.source" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.http" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.http.source" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.io" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.io.source" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.security" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.security.source" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.server" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.server.source" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.servlet" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.servlet.source" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.util" version="9.3.17.v20170317"/>
-      <unit id="org.eclipse.jetty.util.source" version="9.3.17.v20170317"/>
-      <repository id="jetty-9.3.17" location="http://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.3.17.v20170317/"/>
+      <unit id="org.eclipse.jetty.client" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.client.source" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.continuation" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.continuation.source" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.http" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.http.source" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.io" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.io.source" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.security" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.security.source" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.server" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.server.source" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.servlet" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.servlet.source" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.util" version="9.4.5.v20170502"/>
+      <unit id="org.eclipse.jetty.util.source" version="9.4.5.v20170502"/>
+      <repository id="jetty-9.4.5" location="http://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.4.5.v20170502/"/>
     </location>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
       <unit id="org.apache.ant" version="1.9.6.v201510161327"/>
@@ -60,7 +60,7 @@
       <unit id="org.slf4j.api.source" version="1.7.2.v20121108-1250"/>
       <unit id="org.slf4j.impl.log4j12" version="1.7.2.v20131105-2200"/>
       <unit id="org.slf4j.impl.log4j12.source" version="1.7.2.v20131105-2200"/>
-      <repository location="http://download.eclipse.org/tools/orbit/downloads/drops/S20170306214312/repository"/>
+      <repository location="http://download.eclipse.org/tools/orbit/downloads/drops/R20170516192513/repository"/>
     </location>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
       <unit id="org.eclipse.osgi" version="0.0.0"/>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.7.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.7.tpd
index 624b119..4185079 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.7.tpd
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.7.tpd
@@ -1,8 +1,8 @@
 target "jgit-4.7" with source configurePhase
 
-include "projects/jetty-9.3.17.tpd"
-include "orbit/S20170306214312-Oxygen.tpd"
+include "projects/jetty-9.4.5.tpd"
+include "orbit/R20170516192513-Oxygen.tpd"
 
 location "http://download.eclipse.org/releases/oxygen/" {
 	org.eclipse.osgi lazy
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/S20170306214312-Oxygen.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20170516192513-Oxygen.tpd
similarity index 96%
rename from org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/S20170306214312-Oxygen.tpd
rename to org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20170516192513-Oxygen.tpd
index 25722bf..ef19fa6 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/S20170306214312-Oxygen.tpd
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/orbit/R20170516192513-Oxygen.tpd
@@ -1,7 +1,7 @@
-target "S20170306214312-Oxygen" with source configurePhase
+target "R20170516192513-Oxygen" with source configurePhase
 // see http://download.eclipse.org/tools/orbit/downloads/
 
-location "http://download.eclipse.org/tools/orbit/downloads/drops/S20170306214312/repository" {
+location "http://download.eclipse.org/tools/orbit/downloads/drops/R20170516192513/repository" {
 	org.apache.ant [1.9.6.v201510161327,1.9.6.v201510161327]
 	org.apache.ant.source [1.9.6.v201510161327,1.9.6.v201510161327]
 	org.apache.commons.compress [1.6.0.v201310281400,1.6.0.v201310281400]
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/pom.xml b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/pom.xml
index 2ef4d49..721fe9b 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/pom.xml
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/pom.xml
@@ -49,7 +49,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>jgit.tycho.parent</artifactId>
-    <version>4.8.0-SNAPSHOT</version>
+    <version>4.9.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.target</artifactId>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/projects/jetty-9.3.17.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/projects/jetty-9.3.17.tpd
deleted file mode 100644
index 662df09..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/projects/jetty-9.3.17.tpd
+++ /dev/null
@@ -1,20 +0,0 @@
-target "jetty-9.3.17" with source configurePhase
-
-location jetty-9.3.17 "http://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.3.17.v20170317/" {
-	org.eclipse.jetty.client [9.3.17.v20170317,9.3.17.v20170317]
-	org.eclipse.jetty.client.source [9.3.17.v20170317,9.3.17.v20170317]
-	org.eclipse.jetty.continuation [9.3.17.v20170317,9.3.17.v20170317]
-	org.eclipse.jetty.continuation.source [9.3.17.v20170317,9.3.17.v20170317]
-	org.eclipse.jetty.http [9.3.17.v20170317,9.3.17.v20170317]
-	org.eclipse.jetty.http.source [9.3.17.v20170317,9.3.17.v20170317]
-	org.eclipse.jetty.io [9.3.17.v20170317,9.3.17.v20170317]
-	org.eclipse.jetty.io.source [9.3.17.v20170317,9.3.17.v20170317]
-	org.eclipse.jetty.security [9.3.17.v20170317,9.3.17.v20170317]
-	org.eclipse.jetty.security.source [9.3.17.v20170317,9.3.17.v20170317]
-	org.eclipse.jetty.server [9.3.17.v20170317,9.3.17.v20170317]
-	org.eclipse.jetty.server.source [9.3.17.v20170317,9.3.17.v20170317]
-	org.eclipse.jetty.servlet [9.3.17.v20170317,9.3.17.v20170317]
-	org.eclipse.jetty.servlet.source [9.3.17.v20170317,9.3.17.v20170317]
-	org.eclipse.jetty.util [9.3.17.v20170317,9.3.17.v20170317]
-	org.eclipse.jetty.util.source [9.3.17.v20170317,9.3.17.v20170317]
-}
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/projects/jetty-9.3.9.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/projects/jetty-9.3.9.tpd
deleted file mode 100644
index d5621a0..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/projects/jetty-9.3.9.tpd
+++ /dev/null
@@ -1,20 +0,0 @@
-target "jetty-9.4.3" with source configurePhase
-
-location jetty-9.4.3 "http://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.3.9.v20160517/" {
-	org.eclipse.jetty.client [9.3.9.v20160517,9.3.9.v20160517]
-	org.eclipse.jetty.client.source [9.3.9.v20160517,9.3.9.v20160517]
-	org.eclipse.jetty.continuation [9.3.9.v20160517,9.3.9.v20160517]
-	org.eclipse.jetty.continuation.source [9.3.9.v20160517,9.3.9.v20160517]
-	org.eclipse.jetty.http [9.3.9.v20160517,9.3.9.v20160517]
-	org.eclipse.jetty.http.source [9.3.9.v20160517,9.3.9.v20160517]
-	org.eclipse.jetty.io [9.3.9.v20160517,9.3.9.v20160517]
-	org.eclipse.jetty.io.source [9.3.9.v20160517,9.3.9.v20160517]
-	org.eclipse.jetty.security [9.3.9.v20160517,9.3.9.v20160517]
-	org.eclipse.jetty.security.source [9.3.9.v20160517,9.3.9.v20160517]
-	org.eclipse.jetty.server [9.3.9.v20160517,9.3.9.v20160517]
-	org.eclipse.jetty.server.source [9.3.9.v20160517,9.3.9.v20160517]
-	org.eclipse.jetty.servlet [9.3.9.v20160517,9.3.9.v20160517]
-	org.eclipse.jetty.servlet.source [9.3.9.v20160517,9.3.9.v20160517]
-	org.eclipse.jetty.util [9.3.9.v20160517,9.3.9.v20160517]
-	org.eclipse.jetty.util.source [9.3.9.v20160517,9.3.9.v20160517]
-}
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/projects/jetty-9.4.5.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/projects/jetty-9.4.5.tpd
new file mode 100644
index 0000000..363c600
--- /dev/null
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/projects/jetty-9.4.5.tpd
@@ -0,0 +1,20 @@
+target "jetty-9.4.5" with source configurePhase
+
+location jetty-9.4.5 "http://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.4.5.v20170502/" {
+	org.eclipse.jetty.client [9.4.5.v20170502,9.4.5.v20170502]
+	org.eclipse.jetty.client.source [9.4.5.v20170502,9.4.5.v20170502]
+	org.eclipse.jetty.continuation [9.4.5.v20170502,9.4.5.v20170502]
+	org.eclipse.jetty.continuation.source [9.4.5.v20170502,9.4.5.v20170502]
+	org.eclipse.jetty.http [9.4.5.v20170502,9.4.5.v20170502]
+	org.eclipse.jetty.http.source [9.4.5.v20170502,9.4.5.v20170502]
+	org.eclipse.jetty.io [9.4.5.v20170502,9.4.5.v20170502]
+	org.eclipse.jetty.io.source [9.4.5.v20170502,9.4.5.v20170502]
+	org.eclipse.jetty.security [9.4.5.v20170502,9.4.5.v20170502]
+	org.eclipse.jetty.security.source [9.4.5.v20170502,9.4.5.v20170502]
+	org.eclipse.jetty.server [9.4.5.v20170502,9.4.5.v20170502]
+	org.eclipse.jetty.server.source [9.4.5.v20170502,9.4.5.v20170502]
+	org.eclipse.jetty.servlet [9.4.5.v20170502,9.4.5.v20170502]
+	org.eclipse.jetty.servlet.source [9.4.5.v20170502,9.4.5.v20170502]
+	org.eclipse.jetty.util [9.4.5.v20170502,9.4.5.v20170502]
+	org.eclipse.jetty.util.source [9.4.5.v20170502,9.4.5.v20170502]
+}
diff --git a/org.eclipse.jgit.packaging/pom.xml b/org.eclipse.jgit.packaging/pom.xml
index 000bcd5..deda4d0 100644
--- a/org.eclipse.jgit.packaging/pom.xml
+++ b/org.eclipse.jgit.packaging/pom.xml
@@ -53,7 +53,7 @@
 
   <groupId>org.eclipse.jgit</groupId>
   <artifactId>jgit.tycho.parent</artifactId>
-  <version>4.8.0-SNAPSHOT</version>
+  <version>4.9.0-SNAPSHOT</version>
   <packaging>pom</packaging>
 
   <name>JGit Tycho Parent</name>
diff --git a/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF
index 381e272..44864c3 100644
--- a/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF
@@ -2,28 +2,28 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %plugin_name
 Bundle-SymbolicName: org.eclipse.jgit.pgm.test
-Bundle-Version: 4.8.0.qualifier
+Bundle-Version: 4.9.0.qualifier
 Bundle-Vendor: %provider_name
 Bundle-Localization: plugin
 Bundle-ActivationPolicy: lazy
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
-Import-Package: org.eclipse.jgit.api;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.api.errors;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.diff;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.dircache;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.internal.storage.file;version="4.8.0",
- org.eclipse.jgit.junit;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.lib;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.merge;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.pgm;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.pgm.internal;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.pgm.opt;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.revwalk;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.storage.file;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.transport;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.treewalk;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.util;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.util.io;version="[4.8.0,4.9.0)",
+Import-Package: org.eclipse.jgit.api;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.api.errors;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.diff;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.dircache;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.internal.storage.file;version="4.9.0",
+ org.eclipse.jgit.junit;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.lib;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.merge;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.pgm;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.pgm.internal;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.pgm.opt;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.revwalk;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.storage.file;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.transport;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.treewalk;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.util;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.util.io;version="[4.9.0,4.10.0)",
  org.hamcrest.core;bundle-version="[1.1.0,2.0.0)",
  org.junit;version="[4.11.0,5.0.0)",
  org.junit.rules;version="[4.11.0,5.0.0)",
diff --git a/org.eclipse.jgit.pgm.test/pom.xml b/org.eclipse.jgit.pgm.test/pom.xml
index d9c7d1d..68b4c92 100644
--- a/org.eclipse.jgit.pgm.test/pom.xml
+++ b/org.eclipse.jgit.pgm.test/pom.xml
@@ -50,7 +50,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>4.8.0-SNAPSHOT</version>
+    <version>4.9.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.pgm.test</artifactId>
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 18c49ea..6f32bfa 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
@@ -348,7 +348,7 @@
 		commitBazAndFooSlashBar();
 		byte[] result = CLIGitCommand.executeRaw(
 				"git archive --prefix=x/ --format=zip master", db).outBytes();
-		String[] expect = { "x/baz", "x/foo/", "x/foo/bar" };
+		String[] expect = { "x/", "x/baz", "x/foo/", "x/foo/bar" };
 		String[] actual = listZipEntries(result);
 
 		Arrays.sort(expect);
@@ -361,7 +361,7 @@
 		commitBazAndFooSlashBar();
 		byte[] result = CLIGitCommand.executeRaw(
 				"git archive --prefix=x/ --format=tar master", db).outBytes();
-		String[] expect = { "x/baz", "x/foo/", "x/foo/bar" };
+		String[] expect = { "x/", "x/baz", "x/foo/", "x/foo/bar" };
 		String[] actual = listTarEntries(result);
 
 		Arrays.sort(expect);
@@ -380,7 +380,7 @@
 		commitFoo();
 		byte[] result = CLIGitCommand.executeRaw(
 				"git archive --prefix=x// --format=zip master", db).outBytes();
-		String[] expect = { "x//foo" };
+		String[] expect = { "x/", "x//foo" };
 		assertArrayEquals(expect, listZipEntries(result));
 	}
 
@@ -389,7 +389,7 @@
 		commitFoo();
 		byte[] result = CLIGitCommand.executeRaw(
 				"git archive --prefix=x// --format=tar master", db).outBytes();
-		String[] expect = { "x//foo" };
+		String[] expect = { "x/", "x//foo" };
 		assertArrayEquals(expect, listTarEntries(result));
 	}
 
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ConfigTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ConfigTest.java
index c43accd..0ce6451 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ConfigTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ConfigTest.java
@@ -74,6 +74,7 @@
 
 		String[] output = execute("git config --list");
 		List<String> expect = new ArrayList<>();
+		expect.add("gc.autoDetach=false");
 		expect.add("core.filemode=" + !isWindows);
 		expect.add("core.logallrefupdates=true");
 		if (isMac)
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 086e72e..e97762d 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
@@ -46,6 +46,7 @@
 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.util.Arrays;
 
@@ -71,6 +72,12 @@
 		git.tag().setName("v1.0").call();
 	}
 
+	private void secondCommit() throws Exception {
+		writeTrashFile("greeting", "Hello, world!");
+		git.add().addFilepattern("greeting").call();
+		git.commit().setMessage("2nd commit").call();
+	}
+
 	@Test
 	public void testNoHead() throws Exception {
 		assertEquals(CLIText.fatalError(CLIText.get().noNamesFound),
@@ -94,9 +101,7 @@
 	@Test
 	public void testDescribeCommit() throws Exception {
 		initialCommitAndTag();
-		writeTrashFile("greeting", "Hello, world!");
-		git.add().addFilepattern("greeting").call();
-		git.commit().setMessage("2nd commit").call();
+		secondCommit();
 		assertArrayEquals(new String[] { "v1.0-1-g56f6ceb", "" },
 				execute("git describe"));
 	}
@@ -109,6 +114,47 @@
 	}
 
 	@Test
+	public void testDescribeCommitMatch() throws Exception {
+		initialCommitAndTag();
+		secondCommit();
+		assertArrayEquals(new String[] { "v1.0-1-g56f6ceb", "" },
+				execute("git describe --match v1.*"));
+	}
+
+	@Test
+	public void testDescribeCommitMatch2() throws Exception {
+		initialCommitAndTag();
+		secondCommit();
+		git.tag().setName("v2.0").call();
+		assertArrayEquals(new String[] { "v1.0-1-g56f6ceb", "" },
+				execute("git describe --match v1.*"));
+	}
+
+	@Test
+	public void testDescribeCommitMultiMatch() throws Exception {
+		initialCommitAndTag();
+		secondCommit();
+		git.tag().setName("v2.0.0").call();
+		git.tag().setName("v2.1.1").call();
+		assertArrayEquals("git yields v2.0.0", new String[] { "v2.0.0", "" },
+				execute("git describe --match v2.0* --match v2.1.*"));
+	}
+
+	@Test
+	public void testDescribeCommitNoMatch() throws Exception {
+		initialCommitAndTag();
+		writeTrashFile("greeting", "Hello, world!");
+		secondCommit();
+		try {
+			execute("git describe --match 1.*");
+			fail("git describe should not find any tag matching 1.*");
+		} catch (Die e) {
+			assertEquals("No names found, cannot describe anything.",
+					e.getMessage());
+		}
+	}
+
+	@Test
 	public void testHelpArgumentBeforeUnknown() throws Exception {
 		String[] output = execute("git describe -h -XYZ");
 		String all = Arrays.toString(output);
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ReflogTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ReflogTest.java
index 7330ee9..bf6bacb 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ReflogTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/ReflogTest.java
@@ -80,4 +80,4 @@
 					"" }, execute("git reflog refs/heads/side"));
 		}
 	}
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF b/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF
index df3406c..e73b352 100644
--- a/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF
@@ -2,7 +2,7 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %plugin_name
 Bundle-SymbolicName: org.eclipse.jgit.pgm
-Bundle-Version: 4.8.0.qualifier
+Bundle-Version: 4.9.0.qualifier
 Bundle-Vendor: %provider_name
 Bundle-ActivationPolicy: lazy
 Bundle-Localization: plugin
@@ -13,60 +13,60 @@
  org.apache.commons.compress.archivers.tar;version="[1.3,2.0)",
  org.apache.commons.compress.archivers.zip;version="[1.3,2.0)",
  org.apache.commons.logging;version="1.1.1",
- org.eclipse.jetty.continuation;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.http;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.io;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.security;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.security.authentication;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.server;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.server.handler;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.server.nio;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.servlet;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.util;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.util.component;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.util.log;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.util.security;version="[9.0.0,9.4.0)",
- org.eclipse.jetty.util.thread;version="[9.0.0,9.4.0)",
- org.eclipse.jgit.api;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.api.errors;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.archive;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.awtui;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.blame;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.diff;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.dircache;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.errors;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.gitrepo;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.internal.ketch;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.internal.storage.file;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.internal.storage.pack;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.internal.storage.reftree;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.lfs;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.lfs.lib;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.lfs.server;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.lfs.server.fs;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.lfs.server.s3;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.lib;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.merge;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.nls;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.notes;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.revplot;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.revwalk;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.revwalk.filter;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.storage.file;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.storage.pack;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.transport;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.transport.http.apache;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.transport.resolver;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.treewalk;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.treewalk.filter;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.util;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.util.io;version="[4.8.0,4.9.0)",
+ org.eclipse.jetty.continuation;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.http;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.io;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.security;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.security.authentication;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.server;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.server.handler;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.server.nio;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.servlet;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.util;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.util.component;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.util.log;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.util.security;version="[9.4.5,10.0.0)",
+ org.eclipse.jetty.util.thread;version="[9.4.5,10.0.0)",
+ org.eclipse.jgit.api;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.api.errors;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.archive;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.awtui;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.blame;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.diff;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.dircache;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.errors;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.gitrepo;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.internal.ketch;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.internal.storage.file;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.internal.storage.pack;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.internal.storage.reftree;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.lfs;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.lfs.lib;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.lfs.server;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.lfs.server.fs;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.lfs.server.s3;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.lib;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.merge;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.nls;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.notes;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.revplot;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.revwalk;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.revwalk.filter;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.storage.file;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.storage.pack;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.transport;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.transport.http.apache;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.transport.resolver;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.treewalk;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.treewalk.filter;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.util;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.util.io;version="[4.9.0,4.10.0)",
  org.kohsuke.args4j;version="[2.0.12,2.1.0)",
  org.kohsuke.args4j.spi;version="[2.0.15,2.1.0)"
-Export-Package: org.eclipse.jgit.console;version="4.8.0";
+Export-Package: org.eclipse.jgit.console;version="4.9.0";
   uses:="org.eclipse.jgit.transport,
    org.eclipse.jgit.util",
- org.eclipse.jgit.pgm;version="4.8.0";
+ org.eclipse.jgit.pgm;version="4.9.0";
   uses:="org.eclipse.jgit.revwalk,
    org.eclipse.jgit.treewalk.filter,
    org.eclipse.jgit.pgm.opt,
@@ -77,11 +77,11 @@
    org.eclipse.jgit.treewalk,
    javax.swing,
    org.eclipse.jgit.transport",
- org.eclipse.jgit.pgm.debug;version="4.8.0";
+ org.eclipse.jgit.pgm.debug;version="4.9.0";
   uses:="org.eclipse.jgit.util.io,
    org.eclipse.jgit.pgm",
- org.eclipse.jgit.pgm.internal;version="4.8.0";x-friends:="org.eclipse.jgit.pgm.test,org.eclipse.jgit.test",
- org.eclipse.jgit.pgm.opt;version="4.8.0";
+ org.eclipse.jgit.pgm.internal;version="4.9.0";x-friends:="org.eclipse.jgit.pgm.test,org.eclipse.jgit.test",
+ org.eclipse.jgit.pgm.opt;version="4.9.0";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.revwalk,
    org.kohsuke.args4j.spi,
diff --git a/org.eclipse.jgit.pgm/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.pgm/META-INF/SOURCE-MANIFEST.MF
index 5b80357..a4099b4 100644
--- a/org.eclipse.jgit.pgm/META-INF/SOURCE-MANIFEST.MF
+++ b/org.eclipse.jgit.pgm/META-INF/SOURCE-MANIFEST.MF
@@ -3,5 +3,5 @@
 Bundle-Name: org.eclipse.jgit.pgm - Sources
 Bundle-SymbolicName: org.eclipse.jgit.pgm.source
 Bundle-Vendor: Eclipse.org - JGit
-Bundle-Version: 4.8.0.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit.pgm;version="4.8.0.qualifier";roots="."
+Bundle-Version: 4.9.0.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit.pgm;version="4.9.0.qualifier";roots="."
diff --git a/org.eclipse.jgit.pgm/pom.xml b/org.eclipse.jgit.pgm/pom.xml
index 9bb8bfd..d6b78c5 100644
--- a/org.eclipse.jgit.pgm/pom.xml
+++ b/org.eclipse.jgit.pgm/pom.xml
@@ -50,7 +50,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>4.8.0-SNAPSHOT</version>
+    <version>4.9.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.pgm</artifactId>
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 f630cee..8666c34 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
@@ -67,12 +67,14 @@
 failedToLockTag=Failed to lock tag {0}: {1}
 fatalError=fatal: {0}
 fatalThisProgramWillDestroyTheRepository=fatal: This program will destroy the repository\nfatal:\nfatal:\nfatal:    {0}\nfatal:\nfatal: To continue, add {1} to the command line\nfatal:
+fetchingSubmodule=Fetching submodule {0}
 fileIsRequired=argument file is required
 ffNotPossibleAborting=Not possible to fast-forward, aborting.
 forcedUpdate=forced update
 fromURI=From {0}
 initializedEmptyGitRepositoryIn=Initialized empty Git repository in {0}
 invalidHttpProxyOnlyHttpSupported=Invalid http_proxy: {0}: Only http supported.
+invalidRecurseSubmodulesMode=Invalid recurse submodules mode: {0}
 jgitVersion=jgit version {0}
 lineFormat={0}
 listeningOn=Listening on {0}
@@ -127,6 +129,7 @@
 metaVar_pass=PASS
 metaVar_path=path
 metaVar_paths=path ...
+metaVar_pattern=pattern
 metaVar_port=PORT
 metaVar_ref=REF
 metaVar_refs=REFS
@@ -246,6 +249,7 @@
 usage_lsRemoteTags=Show only refs starting with refs/tags
 usage_LsTree=List the contents of a tree object
 usage_MakeCacheTree=Show the current cache tree structure
+usage_Match=Only consider tags matching the given glob(7) pattern or patterns, excluding the "refs/tags/" prefix.
 usage_MergeBase=Find as good common ancestors as possible for a merge
 usage_MergesTwoDevelopmentHistories=Merges two development histories
 usage_PreserveOldPacks=Preserve old pack files by moving them into the preserved subdirectory instead of deleting them after repacking
@@ -285,7 +289,7 @@
 usage_addFileContentsToTheIndex=Add file contents to the index
 usage_alterTheDetailShown=alter the detail shown
 usage_approveDestructionOfRepository=approve destruction of repository
-usage_archive=zip up files from the named tree
+usage_archive=Zip up files from the named tree
 usage_archiveFormat=archive format. Currently supported formats: 'tar', 'zip', 'tgz', 'tbz2', 'txz'
 usage_archiveOutput=output file to write the archive to
 usage_archivePrefix=string to prepend to each pathname in the archive
@@ -359,6 +363,7 @@
 usage_noCommit=Don't commit after a successful merge
 usage_noPrefix=do not show any source or destination prefix
 usage_noRenames=disable rename detection
+usage_noRecurseSubmodules=Disable recursive fetching of submodules (this has the same effect as using the --recurse-submodules=no option)
 usage_noShowStandardNotes=Disable showing notes from the standard /refs/notes/commits branch
 usage_onlyMatchAgainstAlreadyTrackedFiles=Only match <filepattern> against already tracked files in the index rather than the working tree
 usage_outputFile=Output file
@@ -384,7 +389,7 @@
 usage_runLfsStore=Run LFS Store in a given directory
 usage_S3NoSslVerify=Skip verification of Amazon server certificate and hostname
 usage_setTheGitRepositoryToOperateOn=set the git repository to operate on
-usage_show=display one commit
+usage_show=Display one commit
 usage_showRefNamesMatchingCommits=Show ref names matching commits
 usage_showPatch=display patch
 usage_showNotes=Add this ref to the list of note branches from which notes are displayed
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/AbstractFetchCommand.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/AbstractFetchCommand.java
index 2cee2cb..08ec096 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/AbstractFetchCommand.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/AbstractFetchCommand.java
@@ -90,6 +90,9 @@
 			}
 		}
 		showRemoteMessages(errw, r.getMessages());
+		for (FetchResult submoduleResult : r.submoduleResults().values()) {
+			showFetchResult(submoduleResult);
+		}
 	}
 
 	static void showRemoteMessages(ThrowingPrintWriter writer, String pkt) throws IOException {
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Config.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Config.java
index faae13a..f5c3f9a 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Config.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Config.java
@@ -122,4 +122,4 @@
 			}
 		}
 	}
-}
\ No newline at end of file
+}
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 1008593..ceabe93 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
@@ -204,4 +204,4 @@
 			}
 		});
 	}
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Describe.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Describe.java
index ec000f3..eba5a43 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Describe.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Describe.java
@@ -50,6 +50,9 @@
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
 
+import java.util.ArrayList;
+import java.util.List;
+
 @Command(common = true, usage = "usage_Describe")
 class Describe extends TextBuiltin {
 
@@ -59,6 +62,9 @@
 	@Option(name = "--long", usage = "usage_LongFormat")
 	private boolean longDesc;
 
+	@Option(name = "--match", multiValued = true, usage = "usage_Match", metaVar = "metaVar_pattern")
+	private List<String> patterns = new ArrayList<>();
+
 	@Override
 	protected void run() throws Exception {
 		try (Git git = new Git(db)) {
@@ -66,6 +72,7 @@
 			if (tree != null)
 				cmd.setTarget(tree);
 			cmd.setLong(longDesc);
+			cmd.setMatch(patterns.toArray(new String[patterns.size()]));
 			String result = null;
 			try {
 				result = cmd.call();
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Fetch.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Fetch.java
index ed06733..5ed23b9 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Fetch.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Fetch.java
@@ -45,11 +45,15 @@
 
 package org.eclipse.jgit.pgm;
 
+import java.io.IOException;
+import java.text.MessageFormat;
 import java.util.List;
 
 import org.eclipse.jgit.api.FetchCommand;
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.SubmoduleConfig.FetchRecurseSubmodulesMode;
+import org.eclipse.jgit.pgm.internal.CLIText;
 import org.eclipse.jgit.lib.TextProgressMonitor;
 import org.eclipse.jgit.transport.FetchResult;
 import org.eclipse.jgit.transport.RefSpec;
@@ -58,7 +62,7 @@
 import org.kohsuke.args4j.Option;
 
 @Command(common = true, usage = "usage_updateRemoteRefsFromAnotherRepository")
-class Fetch extends AbstractFetchCommand {
+class Fetch extends AbstractFetchCommand implements FetchCommand.Callback {
 	@Option(name = "--timeout", metaVar = "metaVar_seconds", usage = "usage_abortConnectionIfNoActivity")
 	int timeout = -1;
 
@@ -96,6 +100,31 @@
 		tags = Boolean.FALSE;
 	}
 
+	private FetchRecurseSubmodulesMode recurseSubmodules;
+
+	@Option(name = "--recurse-submodules", usage = "usage_recurseSubmodules")
+	void recurseSubmodules(String mode) {
+		if (mode == null || mode.isEmpty()) {
+			recurseSubmodules = FetchRecurseSubmodulesMode.YES;
+		} else {
+			for (FetchRecurseSubmodulesMode m : FetchRecurseSubmodulesMode
+					.values()) {
+				if (m.matchConfigValue(mode)) {
+					recurseSubmodules = m;
+					return;
+				}
+			}
+			throw die(MessageFormat
+					.format(CLIText.get().invalidRecurseSubmodulesMode, mode));
+		}
+	}
+
+	@Option(name = "--no-recurse-submodules", usage = "usage_noRecurseSubmodules")
+	void noRecurseSubmodules(@SuppressWarnings("unused")
+	final boolean ignored) {
+		recurseSubmodules = FetchRecurseSubmodulesMode.NO;
+	}
+
 	@Argument(index = 0, metaVar = "metaVar_uriish")
 	private String remote = Constants.DEFAULT_REMOTE_NAME;
 
@@ -124,12 +153,25 @@
 				fetch.setThin(thin.booleanValue());
 			if (quiet == null || !quiet.booleanValue())
 				fetch.setProgressMonitor(new TextProgressMonitor(errw));
+			fetch.setRecurseSubmodules(recurseSubmodules).setCallback(this);
 
 			FetchResult result = fetch.call();
-			if (result.getTrackingRefUpdates().isEmpty())
+			if (result.getTrackingRefUpdates().isEmpty()
+					&& result.submoduleResults().isEmpty())
 				return;
 
 			showFetchResult(result);
 		}
 	}
+
+	@Override
+	public void fetchingSubmodule(String name) {
+		try {
+			outw.println(MessageFormat.format(CLIText.get().fetchingSubmodule,
+					name));
+			outw.flush();
+		} catch (IOException e) {
+			// ignore
+		}
+	}
 }
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 3addecb..c94ba0b 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
@@ -44,8 +44,11 @@
 
 package org.eclipse.jgit.pgm;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import java.io.File;
 import java.io.IOException;
+import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
 import java.lang.reflect.InvocationTargetException;
 import java.net.MalformedURLException;
@@ -54,6 +57,10 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
 
 import org.eclipse.jgit.awtui.AwtAuthenticator;
 import org.eclipse.jgit.awtui.AwtCredentialsProvider;
@@ -95,6 +102,8 @@
 
 	PrintWriter writer;
 
+	private ExecutorService gcExecutor;
+
 	/**
 	 *
 	 */
@@ -102,6 +111,17 @@
 		HttpTransport.setConnectionFactory(new HttpClientConnectionFactory());
 		CleanFilter.register();
 		SmudgeFilter.register();
+		gcExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() {
+			private final ThreadFactory baseFactory = Executors
+					.defaultThreadFactory();
+
+			@Override
+			public Thread newThread(Runnable taskBody) {
+				Thread thr = baseFactory.newThread(taskBody);
+				thr.setName("JGit-autoGc"); //$NON-NLS-1$
+				return thr;
+			}
+		});
 	}
 
 	/**
@@ -189,10 +209,12 @@
 			// broken pipe
 			exit(1, null);
 		}
+		gcExecutor.shutdown();
+		gcExecutor.awaitTermination(10, TimeUnit.MINUTES);
 	}
 
 	PrintWriter createErrorWriter() {
-		return new PrintWriter(System.err);
+		return new PrintWriter(new OutputStreamWriter(System.err, UTF_8));
 	}
 
 	private void execute(final String[] argv) throws Exception {
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 e10ecbe..1424dab 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
@@ -143,12 +143,14 @@
 	/***/ public String failedToLockTag;
 	/***/ public String fatalError;
 	/***/ public String fatalThisProgramWillDestroyTheRepository;
+	/***/ public String fetchingSubmodule;
 	/***/ public String fileIsRequired;
 	/***/ public String ffNotPossibleAborting;
 	/***/ public String forcedUpdate;
 	/***/ public String fromURI;
 	/***/ public String initializedEmptyGitRepositoryIn;
 	/***/ public String invalidHttpProxyOnlyHttpSupported;
+	/***/ public String invalidRecurseSubmodulesMode;
 	/***/ public String jgitVersion;
 	/***/ public String lfsNoAccessKey;
 	/***/ public String lfsNoSecretKey;
@@ -194,6 +196,7 @@
 	/***/ public String metaVar_pass;
 	/***/ public String metaVar_path;
 	/***/ public String metaVar_paths;
+	/***/ public String metaVar_pattern;
 	/***/ public String metaVar_port;
 	/***/ public String metaVar_ref;
 	/***/ public String metaVar_refs;
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/opt/UntrackedFilesHandler.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/opt/UntrackedFilesHandler.java
index c4e8b05..d6ff5f0 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/opt/UntrackedFilesHandler.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/opt/UntrackedFilesHandler.java
@@ -111,4 +111,4 @@
 		}
 	}
 
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.test/META-INF/MANIFEST.MF
index 4b2e535..670b42b 100644
--- a/org.eclipse.jgit.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.test/META-INF/MANIFEST.MF
@@ -2,54 +2,55 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %plugin_name
 Bundle-SymbolicName: org.eclipse.jgit.test
-Bundle-Version: 4.8.0.qualifier
+Bundle-Version: 4.9.0.qualifier
 Bundle-Localization: plugin
 Bundle-Vendor: %provider_name
 Bundle-ActivationPolicy: lazy
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
 Import-Package: com.googlecode.javaewah;version="[1.1.6,2.0.0)",
- org.eclipse.jgit.api;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.api.errors;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.attributes;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.awtui;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.blame;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.diff;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.dircache;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.errors;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.events;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.fnmatch;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.gitrepo;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.hooks;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.ignore;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.ignore.internal;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.internal;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.internal.storage.dfs;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.internal.storage.file;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.internal.storage.pack;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.internal.storage.reftree;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.junit;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.lfs;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.lib;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.merge;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.nls;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.notes;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.patch;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.pgm;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.pgm.internal;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.revplot;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.revwalk;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.revwalk.filter;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.storage.file;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.storage.pack;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.submodule;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.transport;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.transport.http;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.transport.resolver;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.treewalk;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.treewalk.filter;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.util;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.util.io;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.util.sha1;version="[4.8.0,4.9.0)",
+ org.eclipse.jgit.api;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.api.errors;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.attributes;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.awtui;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.blame;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.diff;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.dircache;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.errors;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.events;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.fnmatch;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.gitrepo;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.hooks;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.ignore;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.ignore.internal;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.internal;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.internal.fsck;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.internal.storage.dfs;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.internal.storage.file;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.internal.storage.pack;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.internal.storage.reftree;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.junit;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.lfs;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.lib;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.merge;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.nls;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.notes;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.patch;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.pgm;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.pgm.internal;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.revplot;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.revwalk;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.revwalk.filter;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.storage.file;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.storage.pack;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.submodule;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.transport;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.transport.http;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.transport.resolver;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.treewalk;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.treewalk.filter;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.util;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.util.io;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.util.sha1;version="[4.9.0,4.10.0)",
  org.junit;version="[4.4.0,5.0.0)",
  org.junit.experimental.theories;version="[4.4.0,5.0.0)",
  org.junit.rules;version="[4.11.0,5.0.0)",
diff --git a/org.eclipse.jgit.test/pom.xml b/org.eclipse.jgit.test/pom.xml
index e3b0dcd..dad1e3c 100644
--- a/org.eclipse.jgit.test/pom.xml
+++ b/org.eclipse.jgit.test/pom.xml
@@ -52,7 +52,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>4.8.0-SNAPSHOT</version>
+    <version>4.9.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.test</artifactId>
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/attributes/merge/disabled_checked.gif b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/attributes/merge/disabled_checked.gif
new file mode 100644
index 0000000..47b9e32
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/attributes/merge/disabled_checked.gif
Binary files differ
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/attributes/merge/enabled_checked.gif b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/attributes/merge/enabled_checked.gif
new file mode 100644
index 0000000..252f762
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/attributes/merge/enabled_checked.gif
Binary files differ
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 7e657e6..a0834e7 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
@@ -76,6 +76,7 @@
 import org.eclipse.jgit.treewalk.TreeWalk;
 import org.eclipse.jgit.treewalk.filter.TreeFilter;
 import org.eclipse.jgit.util.FS;
+import org.junit.Ignore;
 import org.junit.Test;
 
 /**
@@ -305,6 +306,7 @@
 		}
 	}
 
+	@Ignore("very flaky when run with Hudson")
 	@Test
 	public void commitUpdatesSmudgedEntries() throws Exception {
 		try (Git git = new Git(db)) {
@@ -361,6 +363,7 @@
 		}
 	}
 
+	@Ignore("very flaky when run with Hudson")
 	@Test
 	public void commitIgnoresSmudgedEntryWithDifferentId() throws Exception {
 		try (Git git = new Git(db)) {
@@ -554,6 +557,11 @@
 			} catch (EmtpyCommitException e) {
 				// expect this exception
 			}
+
+			// Allow empty commits also when setOnly was set
+			git.commit().setAuthor("New Author", "newauthor@example.org")
+					.setMessage("again no change").setOnly("file1")
+					.setAllowEmpty(true).call();
 		}
 	}
 
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DescribeCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DescribeCommandTest.java
index 1e5d3bc..6a66783 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DescribeCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/DescribeCommandTest.java
@@ -54,6 +54,7 @@
 
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.api.errors.RefNotFoundException;
+import org.eclipse.jgit.errors.InvalidPatternException;
 import org.eclipse.jgit.junit.RepositoryTestCase;
 import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Test;
@@ -92,26 +93,65 @@
 		ObjectId c1 = modify("aaa");
 
 		ObjectId c2 = modify("bbb");
-		tag("t1");
+		tag("alice-t1");
 
 		ObjectId c3 = modify("ccc");
-		tag("t2");
+		tag("bob-t2");
 
 		ObjectId c4 = modify("ddd");
 
 		assertNull(describe(c1));
 		assertNull(describe(c1, true));
-		assertEquals("t1", describe(c2));
-		assertEquals("t2", describe(c3));
-		assertEquals("t2-0-g44579eb", describe(c3, true));
+		assertNull(describe(c1, "a*", "b*", "c*"));
+
+		assertEquals("alice-t1", describe(c2));
+		assertEquals("alice-t1", describe(c2, "alice*"));
+		assertNull(describe(c2, "bob*"));
+		assertNull(describe(c2, "?ob*"));
+		assertEquals("alice-t1", describe(c2, "a*", "b*", "c*"));
+
+		assertEquals("bob-t2", describe(c3));
+		assertEquals("bob-t2-0-g44579eb", describe(c3, true));
+		assertEquals("alice-t1-1-g44579eb", describe(c3, "alice*"));
+		assertEquals("alice-t1-1-g44579eb", describe(c3, "a??c?-t*"));
+		assertEquals("bob-t2", describe(c3, "bob*"));
+		assertEquals("bob-t2", describe(c3, "?ob*"));
+		assertEquals("bob-t2", describe(c3, "a*", "b*", "c*"));
 
 		assertNameStartsWith(c4, "3e563c5");
 		// the value verified with git-describe(1)
-		assertEquals("t2-1-g3e563c5", describe(c4));
-		assertEquals("t2-1-g3e563c5", describe(c4, true));
+		assertEquals("bob-t2-1-g3e563c5", describe(c4));
+		assertEquals("bob-t2-1-g3e563c5", describe(c4, true));
+		assertEquals("alice-t1-2-g3e563c5", describe(c4, "alice*"));
+		assertEquals("bob-t2-1-g3e563c5", describe(c4, "bob*"));
+		assertEquals("bob-t2-1-g3e563c5", describe(c4, "a*", "b*", "c*"));
 
 		// test default target
-		assertEquals("t2-1-g3e563c5", git.describe().call());
+		assertEquals("bob-t2-1-g3e563c5", git.describe().call());
+	}
+
+	@Test
+	public void testDescribeMultiMatch() throws Exception {
+		ObjectId c1 = modify("aaa");
+		tag("v1.0.0");
+		tag("v1.1.1");
+		ObjectId c2 = modify("bbb");
+
+		// Ensure that if we're interested in any tags, we get the first match as per Git behaviour
+		assertEquals("v1.0.0", describe(c1));
+		assertEquals("v1.0.0-1-g3747db3", describe(c2));
+
+		// Ensure that if we're only interested in one of multiple tags, we get the right match
+		assertEquals("v1.0.0", describe(c1, "v1.0*"));
+		assertEquals("v1.1.1", describe(c1, "v1.1*"));
+		assertEquals("v1.0.0-1-g3747db3", describe(c2, "v1.0*"));
+		assertEquals("v1.1.1-1-g3747db3", describe(c2, "v1.1*"));
+
+		// Ensure that ordering of match precedence is preserved as per Git behaviour
+		assertEquals("v1.0.0", describe(c1, "v1.0*", "v1.1*"));
+		assertEquals("v1.1.1", describe(c1, "v1.1*", "v1.0*"));
+		assertEquals("v1.0.0-1-g3747db3", describe(c2, "v1.0*", "v1.1*"));
+		assertEquals("v1.1.1-1-g3747db3", describe(c2, "v1.1*", "v1.0*"));
 	}
 
 	/**
@@ -271,6 +311,10 @@
 		return describe(c1, false);
 	}
 
+	private String describe(ObjectId c1, String... patterns) throws GitAPIException, IOException, InvalidPatternException {
+		return git.describe().setTarget(c1).setMatch(patterns).call();
+	}
+
 	private static void assertNameStartsWith(ObjectId c4, String prefix) {
 		assertTrue(c4.name(), c4.name().startsWith(prefix));
 	}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/LogCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/LogCommandTest.java
index 38178bf..bd0efad 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/LogCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/LogCommandTest.java
@@ -289,4 +289,4 @@
 				.setMessage("merge s0 with m1").call();
 	}
 
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java
index 823516b..a341284 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PullCommandTest.java
@@ -620,4 +620,4 @@
 				fis.close();
 		}
 	}
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PushCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PushCommandTest.java
index 8c613ec..e0c1499 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PushCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/PushCommandTest.java
@@ -83,6 +83,11 @@
 
 		// create other repository
 		Repository db2 = createWorkRepository();
+		final StoredConfig config2 = db2.getConfig();
+
+		// this tests that this config can be parsed properly
+		config2.setString("fsck", "", "missingEmail", "ignore");
+		config2.save();
 
 		// setup the first repository
 		final StoredConfig config = db.getConfig();
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeTest.java
index ec2370e..f0d3c36 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/AttributesNodeTest.java
@@ -166,6 +166,25 @@
 		assertAttribute("file.type3", node, asSet(A_UNSET_ATTR, B_SET_ATTR));
 	}
 
+	@Test
+	public void testDoubleAsteriskAtEnd() throws IOException {
+		String attributeFileContent = "dir/** \tA -B\tC=value";
+
+		is = new ByteArrayInputStream(attributeFileContent.getBytes());
+		AttributesNode node = new AttributesNode();
+		node.parse(is);
+		assertAttribute("dir", node,
+				asSet(new Attribute[]{}));
+		assertAttribute("dir/", node,
+				asSet(new Attribute[]{}));
+		assertAttribute("dir/file.type1", node,
+				asSet(A_SET_ATTR, B_UNSET_ATTR, C_VALUE_ATTR));
+		assertAttribute("dir/sub/", node,
+				asSet(A_SET_ATTR, B_UNSET_ATTR, C_VALUE_ATTR));
+		assertAttribute("dir/sub/file.type1", node,
+				asSet(A_SET_ATTR, B_UNSET_ATTR, C_VALUE_ATTR));
+	}
+
 	private void assertAttribute(String path, AttributesNode node,
 			Attributes attrs) throws IOException {
 		Attributes attributes = new Attributes();
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/merge/MergeGitAttributeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/merge/MergeGitAttributeTest.java
new file mode 100644
index 0000000..665f47b
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/attributes/merge/MergeGitAttributeTest.java
@@ -0,0 +1,585 @@
+/*
+ * Copyright (C) 2017, Obeo (mathieu.cartaud@obeo.fr)
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.attributes.merge;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.util.function.Consumer;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.MergeResult;
+import org.eclipse.jgit.api.MergeResult.MergeStatus;
+import org.eclipse.jgit.api.errors.CheckoutConflictException;
+import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.api.errors.InvalidMergeHeadsException;
+import org.eclipse.jgit.api.errors.NoFilepatternException;
+import org.eclipse.jgit.api.errors.NoHeadException;
+import org.eclipse.jgit.api.errors.NoMessageException;
+import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
+import org.eclipse.jgit.attributes.Attribute;
+import org.eclipse.jgit.attributes.Attributes;
+import org.eclipse.jgit.errors.NoWorkTreeException;
+import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.treewalk.FileTreeIterator;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.filter.PathFilter;
+import org.junit.Ignore;
+import org.junit.Test;
+
+public class MergeGitAttributeTest extends RepositoryTestCase {
+
+	private static final String REFS_HEADS_RIGHT = "refs/heads/right";
+
+	private static final String REFS_HEADS_MASTER = "refs/heads/master";
+
+	private static final String REFS_HEADS_LEFT = "refs/heads/left";
+
+	private static final String DISABLE_CHECK_BRANCH = "refs/heads/disabled_checked";
+
+	private static final String ENABLE_CHECKED_BRANCH = "refs/heads/enabled_checked";
+
+	private static final String ENABLED_CHECKED_GIF = "enabled_checked.gif";
+
+	public Git createRepositoryBinaryConflict(Consumer<Git> initialCommit,
+			Consumer<Git> leftCommit, Consumer<Git> rightCommit)
+			throws NoFilepatternException, GitAPIException, NoWorkTreeException,
+			IOException {
+		// Set up a git whith conflict commits on images
+		Git git = new Git(db);
+
+		// First commit
+		initialCommit.accept(git);
+		git.add().addFilepattern(".").call();
+		RevCommit firstCommit = git.commit().setAll(true)
+				.setMessage("initial commit adding git attribute file").call();
+
+		// Create branch and add an icon Checked_Boxe (enabled_checked)
+		createBranch(firstCommit, REFS_HEADS_LEFT);
+		checkoutBranch(REFS_HEADS_LEFT);
+		leftCommit.accept(git);
+		git.add().addFilepattern(".").call();
+		git.commit().setMessage("Left").call();
+
+		// Create a second branch from master Unchecked_Boxe
+		checkoutBranch(REFS_HEADS_MASTER);
+		createBranch(firstCommit, REFS_HEADS_RIGHT);
+		checkoutBranch(REFS_HEADS_RIGHT);
+		rightCommit.accept(git);
+		git.add().addFilepattern(".").call();
+		git.commit().setMessage("Right").call();
+
+		checkoutBranch(REFS_HEADS_LEFT);
+		return git;
+
+	}
+
+	@Test
+	public void mergeTextualFile_NoAttr() throws NoWorkTreeException,
+			NoFilepatternException, GitAPIException, IOException {
+		try (Git git = createRepositoryBinaryConflict(g -> {
+			try {
+				writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "D\n");
+			} catch (IOException e) {
+				e.printStackTrace();
+			}
+		}, g -> {
+			try {
+				writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "F\n");
+			} catch (IOException e) {
+				e.printStackTrace();
+			}
+		}, g -> {
+			try {
+				writeTrashFile("main.cat", "A\n" + "E\n" + "C\n" + "D\n");
+			} catch (IOException e) {
+				e.printStackTrace();
+			}
+		})) {
+			checkoutBranch(REFS_HEADS_LEFT);
+			// Merge refs/heads/enabled_checked -> refs/heads/disabled_checked
+
+			MergeResult mergeResult = git.merge()
+					.include(git.getRepository().resolve(REFS_HEADS_RIGHT))
+					.call();
+			assertEquals(MergeStatus.MERGED, mergeResult.getMergeStatus());
+
+			assertNull(mergeResult.getConflicts());
+
+			// Check that the image was not modified (not conflict marker added)
+			String result = read(
+					writeTrashFile("res.cat", "A\n" + "E\n" + "C\n" + "F\n"));
+			assertEquals(result, read(git.getRepository().getWorkTree().toPath()
+					.resolve("main.cat").toFile()));
+		}
+	}
+
+	@Test
+	public void mergeTextualFile_UnsetMerge_Conflict()
+			throws NoWorkTreeException, NoFilepatternException, GitAPIException,
+			IOException {
+		try (Git git = createRepositoryBinaryConflict(g -> {
+			try {
+				writeTrashFile(".gitattributes", "*.cat -merge");
+				writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "D\n");
+			} catch (IOException e) {
+				e.printStackTrace();
+			}
+		}, g -> {
+			try {
+				writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "F\n");
+			} catch (IOException e) {
+				e.printStackTrace();
+			}
+		}, g -> {
+			try {
+				writeTrashFile("main.cat", "A\n" + "E\n" + "C\n" + "D\n");
+			} catch (IOException e) {
+				e.printStackTrace();
+			}
+		})) {
+			// Check that the merge attribute is unset
+			assertAddMergeAttributeUnset(REFS_HEADS_LEFT, "main.cat");
+			assertAddMergeAttributeUnset(REFS_HEADS_RIGHT, "main.cat");
+
+			checkoutBranch(REFS_HEADS_LEFT);
+			// Merge refs/heads/enabled_checked -> refs/heads/disabled_checked
+
+			String catContent = read(git.getRepository().getWorkTree().toPath()
+					.resolve("main.cat").toFile());
+
+			MergeResult mergeResult = git.merge()
+					.include(git.getRepository().resolve(REFS_HEADS_RIGHT))
+					.call();
+			assertEquals(MergeStatus.CONFLICTING, mergeResult.getMergeStatus());
+
+			// Check that the image was not modified (not conflict marker added)
+			assertEquals(catContent, read(git.getRepository().getWorkTree()
+					.toPath().resolve("main.cat").toFile()));
+		}
+	}
+
+	@Test
+	public void mergeTextualFile_UnsetMerge_NoConflict()
+			throws NoWorkTreeException, NoFilepatternException, GitAPIException,
+			IOException {
+		try (Git git = createRepositoryBinaryConflict(g -> {
+			try {
+				writeTrashFile(".gitattributes", "*.txt -merge");
+				writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "D\n");
+			} catch (IOException e) {
+				e.printStackTrace();
+			}
+		}, g -> {
+			try {
+				writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "F\n");
+			} catch (IOException e) {
+				e.printStackTrace();
+			}
+		}, g -> {
+			try {
+				writeTrashFile("main.cat", "A\n" + "E\n" + "C\n" + "D\n");
+			} catch (IOException e) {
+				e.printStackTrace();
+			}
+		})) {
+			// Check that the merge attribute is unset
+			assertAddMergeAttributeUndefined(REFS_HEADS_LEFT, "main.cat");
+			assertAddMergeAttributeUndefined(REFS_HEADS_RIGHT, "main.cat");
+
+			checkoutBranch(REFS_HEADS_LEFT);
+			// Merge refs/heads/enabled_checked -> refs/heads/disabled_checked
+
+			MergeResult mergeResult = git.merge()
+					.include(git.getRepository().resolve(REFS_HEADS_RIGHT))
+					.call();
+			assertEquals(MergeStatus.MERGED, mergeResult.getMergeStatus());
+
+			// Check that the image was not modified (not conflict marker added)
+			String result = read(
+					writeTrashFile("res.cat", "A\n" + "E\n" + "C\n" + "F\n"));
+			assertEquals(result, read(git.getRepository().getWorkTree()
+					.toPath().resolve("main.cat").toFile()));
+		}
+	}
+
+	@Test
+	public void mergeTextualFile_SetBinaryMerge_Conflict()
+			throws NoWorkTreeException, NoFilepatternException, GitAPIException,
+			IOException {
+		try (Git git = createRepositoryBinaryConflict(g -> {
+			try {
+				writeTrashFile(".gitattributes", "*.cat merge=binary");
+				writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "D\n");
+			} catch (IOException e) {
+				e.printStackTrace();
+			}
+		}, g -> {
+			try {
+				writeTrashFile("main.cat", "A\n" + "B\n" + "C\n" + "F\n");
+			} catch (IOException e) {
+				e.printStackTrace();
+			}
+		}, g -> {
+			try {
+				writeTrashFile("main.cat", "A\n" + "E\n" + "C\n" + "D\n");
+			} catch (IOException e) {
+				e.printStackTrace();
+			}
+		})) {
+			// Check that the merge attribute is set to binary
+			assertAddMergeAttributeCustom(REFS_HEADS_LEFT, "main.cat",
+					"binary");
+			assertAddMergeAttributeCustom(REFS_HEADS_RIGHT, "main.cat",
+					"binary");
+
+			checkoutBranch(REFS_HEADS_LEFT);
+			// Merge refs/heads/enabled_checked -> refs/heads/disabled_checked
+
+			String catContent = read(git.getRepository().getWorkTree().toPath()
+					.resolve("main.cat").toFile());
+
+			MergeResult mergeResult = git.merge()
+					.include(git.getRepository().resolve(REFS_HEADS_RIGHT))
+					.call();
+			assertEquals(MergeStatus.CONFLICTING, mergeResult.getMergeStatus());
+
+			// Check that the image was not modified (not conflict marker added)
+			assertEquals(catContent, read(git.getRepository().getWorkTree()
+					.toPath().resolve("main.cat").toFile()));
+		}
+	}
+
+	/*
+	 * This test is commented because JGit add conflict markers in binary files.
+	 * cf. https://www.eclipse.org/forums/index.php/t/1086511/
+	 */
+	@Test
+	@Ignore
+	public void mergeBinaryFile_NoAttr_Conflict() throws IllegalStateException,
+			IOException, NoHeadException, ConcurrentRefUpdateException,
+			CheckoutConflictException, InvalidMergeHeadsException,
+			WrongRepositoryStateException, NoMessageException, GitAPIException {
+
+		RevCommit disableCheckedCommit;
+		FileInputStream mergeResultFile = null;
+		// Set up a git with conflict commits on images
+		try (Git git = new Git(db)) {
+			// First commit
+			write(new File(db.getWorkTree(), ".gitattributes"), "");
+			git.add().addFilepattern(".gitattributes").call();
+			RevCommit firstCommit = git.commit()
+					.setMessage("initial commit adding git attribute file")
+					.call();
+
+			// Create branch and add an icon Checked_Boxe (enabled_checked)
+			createBranch(firstCommit, ENABLE_CHECKED_BRANCH);
+			checkoutBranch(ENABLE_CHECKED_BRANCH);
+			copy(ENABLED_CHECKED_GIF, ENABLED_CHECKED_GIF, "");
+			git.add().addFilepattern(ENABLED_CHECKED_GIF).call();
+			git.commit().setMessage("enabled_checked commit").call();
+
+			// Create a second branch from master Unchecked_Boxe
+			checkoutBranch(REFS_HEADS_MASTER);
+			createBranch(firstCommit, DISABLE_CHECK_BRANCH);
+			checkoutBranch(DISABLE_CHECK_BRANCH);
+			copy("disabled_checked.gif", ENABLED_CHECKED_GIF, "");
+			git.add().addFilepattern(ENABLED_CHECKED_GIF).call();
+			disableCheckedCommit = git.commit()
+					.setMessage("disabled_checked commit").call();
+
+			// Check that the merge attribute is unset
+			assertAddMergeAttributeUndefined(ENABLE_CHECKED_BRANCH,
+					ENABLED_CHECKED_GIF);
+			assertAddMergeAttributeUndefined(DISABLE_CHECK_BRANCH,
+					ENABLED_CHECKED_GIF);
+
+			checkoutBranch(ENABLE_CHECKED_BRANCH);
+			// Merge refs/heads/enabled_checked -> refs/heads/disabled_checked
+			MergeResult mergeResult = git.merge().include(disableCheckedCommit)
+					.call();
+			assertEquals(MergeStatus.CONFLICTING, mergeResult.getMergeStatus());
+
+			// Check that the image was not modified (no conflict marker added)
+			mergeResultFile = new FileInputStream(
+					db.getWorkTree().toPath().resolve(ENABLED_CHECKED_GIF)
+							.toFile());
+			assertTrue(contentEquals(
+					getClass().getResourceAsStream(ENABLED_CHECKED_GIF),
+					mergeResultFile));
+		} finally {
+			if (mergeResultFile != null) {
+				mergeResultFile.close();
+			}
+		}
+	}
+
+	@Test
+	public void mergeBinaryFile_UnsetMerge_Conflict()
+			throws IllegalStateException,
+			IOException, NoHeadException, ConcurrentRefUpdateException,
+			CheckoutConflictException, InvalidMergeHeadsException,
+			WrongRepositoryStateException, NoMessageException, GitAPIException {
+
+		RevCommit disableCheckedCommit;
+		FileInputStream mergeResultFile = null;
+		// Set up a git whith conflict commits on images
+		try (Git git = new Git(db)) {
+			// First commit
+			write(new File(db.getWorkTree(), ".gitattributes"), "*.gif -merge");
+			git.add().addFilepattern(".gitattributes").call();
+			RevCommit firstCommit = git.commit()
+					.setMessage("initial commit adding git attribute file")
+					.call();
+
+			// Create branch and add an icon Checked_Boxe (enabled_checked)
+			createBranch(firstCommit, ENABLE_CHECKED_BRANCH);
+			checkoutBranch(ENABLE_CHECKED_BRANCH);
+			copy(ENABLED_CHECKED_GIF, ENABLED_CHECKED_GIF, "");
+			git.add().addFilepattern(ENABLED_CHECKED_GIF).call();
+			git.commit().setMessage("enabled_checked commit").call();
+
+			// Create a second branch from master Unchecked_Boxe
+			checkoutBranch(REFS_HEADS_MASTER);
+			createBranch(firstCommit, DISABLE_CHECK_BRANCH);
+			checkoutBranch(DISABLE_CHECK_BRANCH);
+			copy("disabled_checked.gif", ENABLED_CHECKED_GIF, "");
+			git.add().addFilepattern(ENABLED_CHECKED_GIF).call();
+			disableCheckedCommit = git.commit()
+					.setMessage("disabled_checked commit").call();
+
+			// Check that the merge attribute is unset
+			assertAddMergeAttributeUnset(ENABLE_CHECKED_BRANCH,
+					ENABLED_CHECKED_GIF);
+			assertAddMergeAttributeUnset(DISABLE_CHECK_BRANCH,
+					ENABLED_CHECKED_GIF);
+
+			checkoutBranch(ENABLE_CHECKED_BRANCH);
+			// Merge refs/heads/enabled_checked -> refs/heads/disabled_checked
+			MergeResult mergeResult = git.merge().include(disableCheckedCommit)
+					.call();
+			assertEquals(MergeStatus.CONFLICTING, mergeResult.getMergeStatus());
+
+			// Check that the image was not modified (not conflict marker added)
+			mergeResultFile = new FileInputStream(db.getWorkTree().toPath()
+					.resolve(ENABLED_CHECKED_GIF).toFile());
+			assertTrue(contentEquals(
+					getClass().getResourceAsStream(ENABLED_CHECKED_GIF),
+					mergeResultFile));
+		} finally {
+			if (mergeResultFile != null) {
+				mergeResultFile.close();
+			}
+		}
+	}
+
+	@Test
+	public void mergeBinaryFile_SetMerge_Conflict()
+			throws IllegalStateException, IOException, NoHeadException,
+			ConcurrentRefUpdateException, CheckoutConflictException,
+			InvalidMergeHeadsException, WrongRepositoryStateException,
+			NoMessageException, GitAPIException {
+
+		RevCommit disableCheckedCommit;
+		FileInputStream mergeResultFile = null;
+		// Set up a git whith conflict commits on images
+		try (Git git = new Git(db)) {
+			// First commit
+			write(new File(db.getWorkTree(), ".gitattributes"), "*.gif merge");
+			git.add().addFilepattern(".gitattributes").call();
+			RevCommit firstCommit = git.commit()
+					.setMessage("initial commit adding git attribute file")
+					.call();
+
+			// Create branch and add an icon Checked_Boxe (enabled_checked)
+			createBranch(firstCommit, ENABLE_CHECKED_BRANCH);
+			checkoutBranch(ENABLE_CHECKED_BRANCH);
+			copy(ENABLED_CHECKED_GIF, ENABLED_CHECKED_GIF, "");
+			git.add().addFilepattern(ENABLED_CHECKED_GIF).call();
+			git.commit().setMessage("enabled_checked commit").call();
+
+			// Create a second branch from master Unchecked_Boxe
+			checkoutBranch(REFS_HEADS_MASTER);
+			createBranch(firstCommit, DISABLE_CHECK_BRANCH);
+			checkoutBranch(DISABLE_CHECK_BRANCH);
+			copy("disabled_checked.gif", ENABLED_CHECKED_GIF, "");
+			git.add().addFilepattern(ENABLED_CHECKED_GIF).call();
+			disableCheckedCommit = git.commit()
+					.setMessage("disabled_checked commit").call();
+
+			// Check that the merge attribute is set
+			assertAddMergeAttributeSet(ENABLE_CHECKED_BRANCH,
+					ENABLED_CHECKED_GIF);
+			assertAddMergeAttributeSet(DISABLE_CHECK_BRANCH,
+					ENABLED_CHECKED_GIF);
+
+			checkoutBranch(ENABLE_CHECKED_BRANCH);
+			// Merge refs/heads/enabled_checked -> refs/heads/disabled_checked
+			MergeResult mergeResult = git.merge().include(disableCheckedCommit)
+					.call();
+			assertEquals(MergeStatus.CONFLICTING, mergeResult.getMergeStatus());
+
+			// Check that the image was not modified (not conflict marker added)
+			mergeResultFile = new FileInputStream(db.getWorkTree().toPath()
+					.resolve(ENABLED_CHECKED_GIF).toFile());
+			assertFalse(contentEquals(
+					getClass().getResourceAsStream(ENABLED_CHECKED_GIF),
+					mergeResultFile));
+		} finally {
+			if (mergeResultFile != null) {
+				mergeResultFile.close();
+			}
+		}
+	}
+
+	/*
+	 * Copied from org.apache.commons.io.IOUtils
+	 */
+	private boolean contentEquals(InputStream input1, InputStream input2)
+			throws IOException {
+		if (input1 == input2) {
+			return true;
+		}
+		if (!(input1 instanceof BufferedInputStream)) {
+			input1 = new BufferedInputStream(input1);
+		}
+		if (!(input2 instanceof BufferedInputStream)) {
+			input2 = new BufferedInputStream(input2);
+		}
+
+		int ch = input1.read();
+		while (-1 != ch) {
+			final int ch2 = input2.read();
+			if (ch != ch2) {
+				return false;
+			}
+			ch = input1.read();
+		}
+
+		final int ch2 = input2.read();
+		return ch2 == -1;
+	}
+
+	private void assertAddMergeAttributeUnset(String branch, String fileName)
+			throws IllegalStateException, IOException {
+		checkoutBranch(branch);
+
+		try (TreeWalk treeWaklEnableChecked = new TreeWalk(db)) {
+			treeWaklEnableChecked.addTree(new FileTreeIterator(db));
+			treeWaklEnableChecked.setFilter(PathFilter.create(fileName));
+
+			assertTrue(treeWaklEnableChecked.next());
+			Attributes attributes = treeWaklEnableChecked.getAttributes();
+			Attribute mergeAttribute = attributes.get("merge");
+			assertNotNull(mergeAttribute);
+			assertEquals(Attribute.State.UNSET, mergeAttribute.getState());
+		}
+	}
+
+	private void assertAddMergeAttributeSet(String branch, String fileName)
+			throws IllegalStateException, IOException {
+		checkoutBranch(branch);
+
+		try (TreeWalk treeWaklEnableChecked = new TreeWalk(db)) {
+			treeWaklEnableChecked.addTree(new FileTreeIterator(db));
+			treeWaklEnableChecked.setFilter(PathFilter.create(fileName));
+
+			assertTrue(treeWaklEnableChecked.next());
+			Attributes attributes = treeWaklEnableChecked.getAttributes();
+			Attribute mergeAttribute = attributes.get("merge");
+			assertNotNull(mergeAttribute);
+			assertEquals(Attribute.State.SET, mergeAttribute.getState());
+		}
+	}
+
+	private void assertAddMergeAttributeUndefined(String branch,
+			String fileName) throws IllegalStateException, IOException {
+		checkoutBranch(branch);
+
+		try (TreeWalk treeWaklEnableChecked = new TreeWalk(db)) {
+			treeWaklEnableChecked.addTree(new FileTreeIterator(db));
+			treeWaklEnableChecked.setFilter(PathFilter.create(fileName));
+
+			assertTrue(treeWaklEnableChecked.next());
+			Attributes attributes = treeWaklEnableChecked.getAttributes();
+			Attribute mergeAttribute = attributes.get("merge");
+			assertNull(mergeAttribute);
+		}
+	}
+
+	private void assertAddMergeAttributeCustom(String branch, String fileName,
+			String value) throws IllegalStateException, IOException {
+		checkoutBranch(branch);
+
+		try (TreeWalk treeWaklEnableChecked = new TreeWalk(db)) {
+			treeWaklEnableChecked.addTree(new FileTreeIterator(db));
+			treeWaklEnableChecked.setFilter(PathFilter.create(fileName));
+
+			assertTrue(treeWaklEnableChecked.next());
+			Attributes attributes = treeWaklEnableChecked.getAttributes();
+			Attribute mergeAttribute = attributes.get("merge");
+			assertNotNull(mergeAttribute);
+			assertEquals(Attribute.State.CUSTOM, mergeAttribute.getState());
+			assertEquals(value, mergeAttribute.getValue());
+		}
+	}
+
+	private void copy(String resourcePath, String resourceNewName,
+			String pathInRepo) throws IOException {
+		InputStream input = getClass().getResourceAsStream(resourcePath);
+		Files.copy(input, db.getWorkTree().toPath().resolve(pathInRepo)
+				.resolve(resourceNewName));
+	}
+
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java
index 24b5ad7..6ed2c21 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
@@ -1120,6 +1120,7 @@
 		testRelative("a/", "a/b", "b");
 		testRelative("/a/b/c", "/b/c", "../../b/c");
 		testRelative("/abc", "bcd", "bcd");
+		testRelative("abc", "def", "def");
 		testRelative("abc", "/bcd", "/bcd");
 		testRelative("http://a", "a/b", "a/b");
 		testRelative("http://base.com/a/", "http://child.com/a/b", "http://child.com/a/b");
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/FastIgnoreRuleTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/FastIgnoreRuleTest.java
index 1863b80..bcc8f7e 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/FastIgnoreRuleTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/FastIgnoreRuleTest.java
@@ -391,7 +391,6 @@
 		assertMatched("/**/a/b", "c/d/a/b");
 		assertMatched("/**/**/a/b", "c/d/a/b");
 
-		assertMatched("a/b/**", "a/b");
 		assertMatched("a/b/**", "a/b/c");
 		assertMatched("a/b/**", "a/b/c/d/");
 		assertMatched("a/b/**/**", "a/b/c/d");
@@ -415,6 +414,12 @@
 
 	@Test
 	public void testWildmatchDoNotMatch() {
+		assertNotMatched("a/**", "a/");
+		assertNotMatched("a/b/**", "a/b/");
+		assertNotMatched("a/**", "a");
+		assertNotMatched("a/b/**", "a/b");
+		assertNotMatched("a/b/**/", "a/b");
+		assertNotMatched("a/b/**/**", "a/b");
 		assertNotMatched("**/a/b", "a/c/b");
 		assertNotMatched("!/**/*.zip", "c/a/b.zip");
 		assertNotMatched("!**/*.zip", "c/a/b.zip");
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DeltaBaseCacheTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DeltaBaseCacheTest.java
index 5bef9fa..32d711f 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DeltaBaseCacheTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DeltaBaseCacheTest.java
@@ -57,13 +57,14 @@
 public class DeltaBaseCacheTest {
 	private static final int SZ = 512;
 
-	private DfsPackKey key;
+	private DfsStreamKey key;
 	private DeltaBaseCache cache;
 	private TestRng rng;
 
 	@Before
 	public void setUp() {
-		key = new DfsPackKey();
+		DfsRepositoryDescription repo = new DfsRepositoryDescription("test");
+		key = DfsStreamKey.of(repo, "test.key");
 		cache = new DeltaBaseCache(SZ);
 		rng = new TestRng(getClass().getSimpleName());
 	}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTest.java
new file mode 100644
index 0000000..2e3ee45
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheTest.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2017, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.storage.dfs;
+
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.jgit.junit.TestRng;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+
+public class DfsBlockCacheTest {
+	@Rule
+	public TestName testName = new TestName();
+	private TestRng rng;
+	private DfsBlockCache cache;
+
+	@Before
+	public void setUp() {
+		rng = new TestRng(testName.getMethodName());
+		resetCache();
+	}
+
+	@SuppressWarnings("resource")
+	@Test
+	public void streamKeyReusesBlocks() throws Exception {
+		DfsRepositoryDescription repo = new DfsRepositoryDescription("test");
+		InMemoryRepository r1 = new InMemoryRepository(repo);
+		byte[] content = rng.nextBytes(424242);
+		ObjectId id;
+		try (ObjectInserter ins = r1.newObjectInserter()) {
+			id = ins.insert(OBJ_BLOB, content);
+			ins.flush();
+		}
+
+		long oldSize = cache.getCurrentSize();
+		assertTrue(oldSize > 2000);
+		assertEquals(0, cache.getHitCount());
+
+		List<DfsPackDescription> packs = r1.getObjectDatabase().listPacks();
+		InMemoryRepository r2 = new InMemoryRepository(repo);
+		r2.getObjectDatabase().commitPack(packs, Collections.emptyList());
+		try (ObjectReader rdr = r2.newObjectReader()) {
+			byte[] actual = rdr.open(id, OBJ_BLOB).getBytes();
+			assertTrue(Arrays.equals(content, actual));
+		}
+		assertEquals(0, cache.getMissCount());
+		assertEquals(oldSize, cache.getCurrentSize());
+	}
+
+	@SuppressWarnings("resource")
+	@Test
+	public void weirdBlockSize() throws Exception {
+		DfsRepositoryDescription repo = new DfsRepositoryDescription("test");
+		InMemoryRepository r1 = new InMemoryRepository(repo);
+
+		byte[] content1 = rng.nextBytes(4);
+		byte[] content2 = rng.nextBytes(424242);
+		ObjectId id1;
+		ObjectId id2;
+		try (ObjectInserter ins = r1.newObjectInserter()) {
+			id1 = ins.insert(OBJ_BLOB, content1);
+			id2 = ins.insert(OBJ_BLOB, content2);
+			ins.flush();
+		}
+
+		resetCache();
+		List<DfsPackDescription> packs = r1.getObjectDatabase().listPacks();
+
+		InMemoryRepository r2 = new InMemoryRepository(repo);
+		r2.getObjectDatabase().setReadableChannelBlockSizeForTest(500);
+		r2.getObjectDatabase().commitPack(packs, Collections.emptyList());
+		try (ObjectReader rdr = r2.newObjectReader()) {
+			byte[] actual = rdr.open(id1, OBJ_BLOB).getBytes();
+			assertTrue(Arrays.equals(content1, actual));
+		}
+
+		InMemoryRepository r3 = new InMemoryRepository(repo);
+		r3.getObjectDatabase().setReadableChannelBlockSizeForTest(500);
+		r3.getObjectDatabase().commitPack(packs, Collections.emptyList());
+		try (ObjectReader rdr = r3.newObjectReader()) {
+			byte[] actual = rdr.open(id2, OBJ_BLOB).getBytes();
+			assertTrue(Arrays.equals(content2, actual));
+		}
+	}
+
+	private void resetCache() {
+		DfsBlockCache.reconfigure(new DfsBlockCacheConfig()
+				.setBlockSize(512)
+				.setBlockLimit(1 << 20));
+		cache = DfsBlockCache.getInstance();
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsFsckTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsFsckTest.java
new file mode 100644
index 0000000..804d744
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsFsckTest.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2017, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.storage.dfs;
+
+import static org.eclipse.jgit.junit.JGitTestUtil.concat;
+import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH;
+import static org.eclipse.jgit.lib.Constants.encodeASCII;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+
+import org.eclipse.jgit.internal.fsck.FsckError;
+import org.eclipse.jgit.internal.fsck.FsckError.CorruptObject;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectChecker.ErrorType;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Before;
+import org.junit.Test;
+
+public class DfsFsckTest {
+	private TestRepository<InMemoryRepository> git;
+
+	private InMemoryRepository repo;
+
+	private ObjectInserter ins;
+
+	@Before
+	public void setUp() throws IOException {
+		DfsRepositoryDescription desc = new DfsRepositoryDescription("test");
+		git = new TestRepository<>(new InMemoryRepository(desc));
+		repo = git.getRepository();
+		ins = repo.newObjectInserter();
+	}
+
+	@Test
+	public void testHealthyRepo() throws Exception {
+		RevCommit commit0 = git.commit().message("0").create();
+		RevCommit commit1 = git.commit().message("1").parent(commit0).create();
+		git.update("master", commit1);
+
+		DfsFsck fsck = new DfsFsck(repo);
+		FsckError errors = fsck.check(null);
+
+		assertEquals(errors.getCorruptObjects().size(), 0);
+		assertEquals(errors.getMissingObjects().size(), 0);
+		assertEquals(errors.getCorruptIndices().size(), 0);
+	}
+
+	@Test
+	public void testCommitWithCorruptAuthor() throws Exception {
+		StringBuilder b = new StringBuilder();
+		b.append("tree be9bfa841874ccc9f2ef7c48d0c76226f89b7189\n");
+		b.append("author b <b@c> <b@c> 0 +0000\n");
+		b.append("committer <> 0 +0000\n");
+		byte[] data = encodeASCII(b.toString());
+		ObjectId id = ins.insert(Constants.OBJ_COMMIT, data);
+		ins.flush();
+
+		DfsFsck fsck = new DfsFsck(repo);
+		FsckError errors = fsck.check(null);
+
+		assertEquals(errors.getCorruptObjects().size(), 1);
+		CorruptObject o = errors.getCorruptObjects().iterator().next();
+		assertTrue(o.getId().equals(id));
+		assertEquals(o.getErrorType(), ErrorType.BAD_DATE);
+	}
+
+	@Test
+	public void testCommitWithoutTree() throws Exception {
+		StringBuilder b = new StringBuilder();
+		b.append("parent ");
+		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
+		b.append('\n');
+		byte[] data = encodeASCII(b.toString());
+		ObjectId id = ins.insert(Constants.OBJ_COMMIT, data);
+		ins.flush();
+
+		DfsFsck fsck = new DfsFsck(repo);
+		FsckError errors = fsck.check(null);
+
+		assertEquals(errors.getCorruptObjects().size(), 1);
+		CorruptObject o = errors.getCorruptObjects().iterator().next();
+		assertTrue(o.getId().equals(id));
+		assertEquals(o.getErrorType(), ErrorType.MISSING_TREE);
+	}
+
+	@Test
+	public void testTagWithoutObject() throws Exception {
+		StringBuilder b = new StringBuilder();
+		b.append("type commit\n");
+		b.append("tag test-tag\n");
+		b.append("tagger A. U. Thor <author@localhost> 1 +0000\n");
+		byte[] data = encodeASCII(b.toString());
+		ObjectId id = ins.insert(Constants.OBJ_TAG, data);
+		ins.flush();
+
+		DfsFsck fsck = new DfsFsck(repo);
+		FsckError errors = fsck.check(null);
+
+		assertEquals(errors.getCorruptObjects().size(), 1);
+		CorruptObject o = errors.getCorruptObjects().iterator().next();
+		assertTrue(o.getId().equals(id));
+		assertEquals(o.getErrorType(), ErrorType.MISSING_OBJECT);
+	}
+
+	@Test
+	public void testTreeWithNullSha() throws Exception {
+		byte[] data = concat(encodeASCII("100644 A"), new byte[] { '\0' },
+				new byte[OBJECT_ID_LENGTH]);
+		ObjectId id = ins.insert(Constants.OBJ_TREE, data);
+		ins.flush();
+
+		DfsFsck fsck = new DfsFsck(repo);
+		FsckError errors = fsck.check(null);
+
+		assertEquals(errors.getCorruptObjects().size(), 1);
+		CorruptObject o = errors.getCorruptObjects().iterator().next();
+		assertTrue(o.getId().equals(id));
+		assertEquals(o.getErrorType(), ErrorType.NULL_SHA1);
+	}
+
+	@Test
+	public void testMultipleInvalidObjects() throws Exception {
+		StringBuilder b = new StringBuilder();
+		b.append("tree ");
+		b.append("be9bfa841874ccc9f2ef7c48d0c76226f89b7189");
+		b.append('\n');
+		b.append("parent ");
+		b.append("\n");
+		byte[] data = encodeASCII(b.toString());
+		ObjectId id1 = ins.insert(Constants.OBJ_COMMIT, data);
+
+		b = new StringBuilder();
+		b.append("100644");
+		data = encodeASCII(b.toString());
+		ObjectId id2 = ins.insert(Constants.OBJ_TREE, data);
+
+		ins.flush();
+
+		DfsFsck fsck = new DfsFsck(repo);
+		FsckError errors = fsck.check(null);
+
+		assertEquals(errors.getCorruptObjects().size(), 2);
+		for (CorruptObject o : errors.getCorruptObjects()) {
+			if (o.getId().equals(id1)) {
+				assertEquals(o.getErrorType(), ErrorType.BAD_PARENT_SHA1);
+			} else if (o.getId().equals(id2)) {
+				assertNull(o.getErrorType());
+			} else {
+				fail();
+			}
+		}
+	}
+
+	@Test
+	public void testValidConnectivity() throws Exception {
+		ObjectId blobId = ins
+				.insert(Constants.OBJ_BLOB, Constants.encode("foo"));
+
+		byte[] blobIdBytes = new byte[OBJECT_ID_LENGTH];
+		blobId.copyRawTo(blobIdBytes, 0);
+		byte[] data = concat(encodeASCII("100644 regular-file\0"), blobIdBytes);
+		ObjectId treeId = ins.insert(Constants.OBJ_TREE, data);
+		ins.flush();
+
+		RevCommit commit = git.commit().message("0").setTopLevelTree(treeId)
+				.create();
+
+		git.update("master", commit);
+
+		DfsFsck fsck = new DfsFsck(repo);
+		FsckError errors = fsck.check(null);
+		assertEquals(errors.getMissingObjects().size(), 0);
+	}
+
+	@Test
+	public void testMissingObject() throws Exception {
+		ObjectId blobId = ObjectId
+				.fromString("19102815663d23f8b75a47e7a01965dcdc96468c");
+		byte[] blobIdBytes = new byte[OBJECT_ID_LENGTH];
+		blobId.copyRawTo(blobIdBytes, 0);
+		byte[] data = concat(encodeASCII("100644 regular-file\0"), blobIdBytes);
+		ObjectId treeId = ins.insert(Constants.OBJ_TREE, data);
+		ins.flush();
+
+		RevCommit commit = git.commit().message("0").setTopLevelTree(treeId)
+				.create();
+
+		git.update("master", commit);
+
+		DfsFsck fsck = new DfsFsck(repo);
+		FsckError errors = fsck.check(null);
+		assertEquals(errors.getMissingObjects().size(), 1);
+		assertEquals(errors.getMissingObjects().iterator().next(), blobId);
+	}
+
+	@Test
+	public void testNonCommitHead() throws Exception {
+		RevCommit commit0 = git.commit().message("0").create();
+		StringBuilder b = new StringBuilder();
+		b.append("object ");
+		b.append(commit0.getName());
+		b.append('\n');
+		b.append("type commit\n");
+		b.append("tag test-tag\n");
+		b.append("tagger A. U. Thor <author@localhost> 1 +0000\n");
+
+		byte[] data = encodeASCII(b.toString());
+		ObjectId tagId = ins.insert(Constants.OBJ_TAG, data);
+		ins.flush();
+
+		git.update("master", tagId);
+
+		DfsFsck fsck = new DfsFsck(repo);
+		FsckError errors = fsck.check(null);
+		assertEquals(errors.getCorruptObjects().size(), 0);
+		assertEquals(errors.getNonCommitHeads().size(), 1);
+		assertEquals(errors.getNonCommitHeads().iterator().next(),
+				"refs/heads/master");
+	}
+
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollectorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollectorTest.java
index 32002fd..e4dcc2e 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollectorTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/dfs/DfsGarbageCollectorTest.java
@@ -31,6 +31,7 @@
 import org.junit.Before;
 import org.junit.Test;
 
+/** Tests for pack creation and garbage expiration. */
 public class DfsGarbageCollectorTest {
 	private TestRepository<InMemoryRepository> git;
 	private InMemoryRepository repo;
@@ -632,6 +633,26 @@
 		}
 	}
 
+	@Test
+	public void testSinglePackForAllRefs() throws Exception {
+		RevCommit commit0 = commit().message("0").create();
+		git.update("head", commit0);
+		RevCommit commit1 = commit().message("1").parent(commit0).create();
+		git.update("refs/notes/note1", commit1);
+
+		DfsGarbageCollector gc = new DfsGarbageCollector(repo);
+		gc.setGarbageTtl(0, TimeUnit.MILLISECONDS);
+		gc.getPackConfig().setSinglePack(true);
+		run(gc);
+		assertEquals(1, odb.getPacks().length);
+
+		gc = new DfsGarbageCollector(repo);
+		gc.setGarbageTtl(0, TimeUnit.MILLISECONDS);
+		gc.getPackConfig().setSinglePack(false);
+		run(gc);
+		assertEquals(2, odb.getPacks().length);
+	}
+
 	private TestRepository<InMemoryRepository>.CommitBuilder commit() {
 		return git.commit();
 	}
@@ -674,7 +695,7 @@
 
 	private boolean isObjectInPack(AnyObjectId id, DfsPackFile pack)
 			throws IOException {
-		try (DfsReader reader = new DfsReader(odb)) {
+		try (DfsReader reader = odb.newReader()) {
 			return pack.hasObject(reader, id);
 		}
 	}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BatchRefUpdateTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BatchRefUpdateTest.java
new file mode 100644
index 0000000..34f6c71
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/BatchRefUpdateTest.java
@@ -0,0 +1,915 @@
+/*
+ * Copyright (C) 2017 Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.storage.file;
+
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.eclipse.jgit.internal.storage.file.BatchRefUpdateTest.Result.LOCK_FAILURE;
+import static org.eclipse.jgit.internal.storage.file.BatchRefUpdateTest.Result.OK;
+import static org.eclipse.jgit.internal.storage.file.BatchRefUpdateTest.Result.REJECTED_MISSING_OBJECT;
+import static org.eclipse.jgit.internal.storage.file.BatchRefUpdateTest.Result.REJECTED_NONFASTFORWARD;
+import static org.eclipse.jgit.internal.storage.file.BatchRefUpdateTest.Result.TRANSACTION_ABORTED;
+import static org.eclipse.jgit.lib.ObjectId.zeroId;
+import static org.eclipse.jgit.transport.ReceiveCommand.Type.CREATE;
+import static org.eclipse.jgit.transport.ReceiveCommand.Type.DELETE;
+import static org.eclipse.jgit.transport.ReceiveCommand.Type.UPDATE;
+import static org.eclipse.jgit.transport.ReceiveCommand.Type.UPDATE_NONFASTFORWARD;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Predicate;
+
+import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
+import org.eclipse.jgit.junit.StrictWorkMonitor;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.CheckoutEntry;
+import org.eclipse.jgit.lib.ConfigConstants;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.ReflogEntry;
+import org.eclipse.jgit.lib.ReflogReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@SuppressWarnings("boxing")
+@RunWith(Parameterized.class)
+public class BatchRefUpdateTest extends LocalDiskRepositoryTestCase {
+	@Parameter
+	public boolean atomic;
+
+	@Parameters(name = "atomic={0}")
+	public static Collection<Object[]> data() {
+		return Arrays.asList(new Object[][]{ {Boolean.FALSE}, {Boolean.TRUE} });
+	}
+
+	private Repository diskRepo;
+	private TestRepository<Repository> repo;
+	private RefDirectory refdir;
+	private RevCommit A;
+	private RevCommit B;
+
+	@Override
+	@Before
+	public void setUp() throws Exception {
+		super.setUp();
+
+		diskRepo = createBareRepository();
+		StoredConfig cfg = diskRepo.getConfig();
+		cfg.load();
+		cfg.setBoolean(ConfigConstants.CONFIG_CORE_SECTION, null,
+				ConfigConstants.CONFIG_KEY_LOGALLREFUPDATES, true);
+		cfg.save();
+
+		refdir = (RefDirectory) diskRepo.getRefDatabase();
+		refdir.setRetrySleepMs(Arrays.asList(0, 0));
+
+		repo = new TestRepository<>(diskRepo);
+		A = repo.commit().create();
+		B = repo.commit(repo.getRevWalk().parseCommit(A));
+	}
+
+	@Test
+	public void simpleNoForce() throws IOException {
+		writeLooseRef("refs/heads/master", A);
+		writeLooseRef("refs/heads/masters", B);
+
+		List<ReceiveCommand> cmds = Arrays.asList(
+				new ReceiveCommand(A, B, "refs/heads/master", UPDATE),
+				new ReceiveCommand(B, A, "refs/heads/masters", UPDATE_NONFASTFORWARD));
+		execute(newBatchUpdate(cmds));
+
+		if (atomic) {
+			assertResults(cmds, TRANSACTION_ABORTED, REJECTED_NONFASTFORWARD);
+			assertRefs(
+					"refs/heads/master", A,
+					"refs/heads/masters", B);
+		} else {
+			assertResults(cmds, OK, REJECTED_NONFASTFORWARD);
+			assertRefs(
+					"refs/heads/master", B,
+					"refs/heads/masters", B);
+		}
+	}
+
+	@Test
+	public void simpleForce() throws IOException {
+		writeLooseRef("refs/heads/master", A);
+		writeLooseRef("refs/heads/masters", B);
+
+		List<ReceiveCommand> cmds = Arrays.asList(
+				new ReceiveCommand(A, B, "refs/heads/master", UPDATE),
+				new ReceiveCommand(B, A, "refs/heads/masters", UPDATE_NONFASTFORWARD));
+		execute(newBatchUpdate(cmds).setAllowNonFastForwards(true));
+
+		assertResults(cmds, OK, OK);
+		assertRefs(
+				"refs/heads/master", B,
+				"refs/heads/masters", A);
+	}
+
+	@Test
+	public void nonFastForwardDoesNotDoExpensiveMergeCheck() throws IOException {
+		writeLooseRef("refs/heads/master", B);
+
+		List<ReceiveCommand> cmds = Arrays.asList(
+				new ReceiveCommand(B, A, "refs/heads/master", UPDATE_NONFASTFORWARD));
+		try (RevWalk rw = new RevWalk(diskRepo) {
+					@Override
+					public boolean isMergedInto(RevCommit base, RevCommit tip) {
+						throw new AssertionError("isMergedInto() should not be called");
+					}
+				}) {
+			newBatchUpdate(cmds)
+					.setAllowNonFastForwards(true)
+					.execute(rw, new StrictWorkMonitor());
+		}
+
+		assertResults(cmds, OK);
+		assertRefs("refs/heads/master", A);
+	}
+
+	@Test
+	public void fileDirectoryConflict() throws IOException {
+		writeLooseRef("refs/heads/master", A);
+		writeLooseRef("refs/heads/masters", B);
+
+		List<ReceiveCommand> cmds = Arrays.asList(
+				new ReceiveCommand(A, B, "refs/heads/master", UPDATE),
+				new ReceiveCommand(zeroId(), A, "refs/heads/master/x", CREATE),
+				new ReceiveCommand(zeroId(), A, "refs/heads", CREATE));
+		execute(newBatchUpdate(cmds).setAllowNonFastForwards(true), false);
+
+		if (atomic) {
+			// Atomic update sees that master and master/x are conflicting, then marks
+			// the first one in the list as LOCK_FAILURE and aborts the rest.
+			assertResults(cmds,
+					LOCK_FAILURE, TRANSACTION_ABORTED, TRANSACTION_ABORTED);
+			assertRefs(
+					"refs/heads/master", A,
+					"refs/heads/masters", B);
+		} else {
+			// Non-atomic updates are applied in order: master succeeds, then master/x
+			// fails due to conflict.
+			assertResults(cmds, OK, LOCK_FAILURE, LOCK_FAILURE);
+			assertRefs(
+					"refs/heads/master", B,
+					"refs/heads/masters", B);
+		}
+	}
+
+	@Test
+	public void conflictThanksToDelete() throws IOException {
+		writeLooseRef("refs/heads/master", A);
+		writeLooseRef("refs/heads/masters", B);
+
+		List<ReceiveCommand> cmds = Arrays.asList(
+				new ReceiveCommand(A, B, "refs/heads/master", UPDATE),
+				new ReceiveCommand(zeroId(), A, "refs/heads/masters/x", CREATE),
+				new ReceiveCommand(B, zeroId(), "refs/heads/masters", DELETE));
+		execute(newBatchUpdate(cmds).setAllowNonFastForwards(true));
+
+		assertResults(cmds, OK, OK, OK);
+		assertRefs(
+				"refs/heads/master", B,
+				"refs/heads/masters/x", A);
+	}
+
+	@Test
+	public void updateToMissingObject() throws IOException {
+		writeLooseRef("refs/heads/master", A);
+
+		ObjectId bad =
+				ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+		List<ReceiveCommand> cmds = Arrays.asList(
+				new ReceiveCommand(A, bad, "refs/heads/master", UPDATE),
+				new ReceiveCommand(zeroId(), B, "refs/heads/foo2", CREATE));
+		execute(newBatchUpdate(cmds).setAllowNonFastForwards(true), false);
+
+		if (atomic) {
+			assertResults(cmds, REJECTED_MISSING_OBJECT, TRANSACTION_ABORTED);
+			assertRefs("refs/heads/master", A);
+		} else {
+			assertResults(cmds, REJECTED_MISSING_OBJECT, OK);
+			assertRefs(
+					"refs/heads/master", A,
+					"refs/heads/foo2", B);
+		}
+	}
+
+	@Test
+	public void addMissingObject() throws IOException {
+		writeLooseRef("refs/heads/master", A);
+
+		ObjectId bad =
+				ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+		List<ReceiveCommand> cmds = Arrays.asList(
+				new ReceiveCommand(A, B, "refs/heads/master", UPDATE),
+				new ReceiveCommand(zeroId(), bad, "refs/heads/foo2", CREATE));
+		execute(newBatchUpdate(cmds).setAllowNonFastForwards(true), false);
+
+		if (atomic) {
+			assertResults(cmds, TRANSACTION_ABORTED, REJECTED_MISSING_OBJECT);
+			assertRefs("refs/heads/master", A);
+		} else {
+			assertResults(cmds, OK, REJECTED_MISSING_OBJECT);
+			assertRefs("refs/heads/master", B);
+		}
+	}
+
+	@Test
+	public void oneNonExistentRef() throws IOException {
+		List<ReceiveCommand> cmds = Arrays.asList(
+				new ReceiveCommand(A, B, "refs/heads/foo1", UPDATE),
+				new ReceiveCommand(zeroId(), B, "refs/heads/foo2", CREATE));
+		execute(newBatchUpdate(cmds).setAllowNonFastForwards(true));
+
+		if (atomic) {
+			assertResults(cmds, LOCK_FAILURE, TRANSACTION_ABORTED);
+			assertRefs();
+		} else {
+			assertResults(cmds, LOCK_FAILURE, OK);
+			assertRefs("refs/heads/foo2", B);
+		}
+	}
+
+	@Test
+	public void oneRefWrongOldValue() throws IOException {
+		writeLooseRef("refs/heads/master", A);
+
+		List<ReceiveCommand> cmds = Arrays.asList(
+				new ReceiveCommand(B, B, "refs/heads/master", UPDATE),
+				new ReceiveCommand(zeroId(), B, "refs/heads/foo2", CREATE));
+		execute(newBatchUpdate(cmds).setAllowNonFastForwards(true));
+
+		if (atomic) {
+			assertResults(cmds, LOCK_FAILURE, TRANSACTION_ABORTED);
+			assertRefs("refs/heads/master", A);
+		} else {
+			assertResults(cmds, LOCK_FAILURE, OK);
+			assertRefs(
+					"refs/heads/master", A,
+					"refs/heads/foo2", B);
+		}
+	}
+
+	@Test
+	public void nonExistentRef() throws IOException {
+		writeLooseRef("refs/heads/master", A);
+
+		List<ReceiveCommand> cmds = Arrays.asList(
+				new ReceiveCommand(A, B, "refs/heads/master", UPDATE),
+				new ReceiveCommand(A, zeroId(), "refs/heads/foo2", DELETE));
+		execute(newBatchUpdate(cmds).setAllowNonFastForwards(true));
+
+		if (atomic) {
+			assertResults(cmds, TRANSACTION_ABORTED, LOCK_FAILURE);
+			assertRefs("refs/heads/master", A);
+		} else {
+			assertResults(cmds, OK, LOCK_FAILURE);
+			assertRefs("refs/heads/master", B);
+		}
+	}
+
+	@Test
+	public void noRefLog() throws IOException {
+		writeRef("refs/heads/master", A);
+
+		Map<String, ReflogEntry> oldLogs =
+				getLastReflogs("refs/heads/master", "refs/heads/branch");
+		assertEquals(Collections.singleton("refs/heads/master"), oldLogs.keySet());
+
+		List<ReceiveCommand> cmds = Arrays.asList(
+				new ReceiveCommand(A, B, "refs/heads/master", UPDATE),
+				new ReceiveCommand(zeroId(), B, "refs/heads/branch", CREATE));
+		execute(newBatchUpdate(cmds).setAllowNonFastForwards(true));
+
+		assertResults(cmds, OK, OK);
+		assertRefs(
+				"refs/heads/master", B,
+				"refs/heads/branch", B);
+		assertReflogUnchanged(oldLogs, "refs/heads/master");
+		assertReflogUnchanged(oldLogs, "refs/heads/branch");
+	}
+
+	@Test
+	public void reflogDefaultIdent() throws IOException {
+		writeRef("refs/heads/master", A);
+		writeRef("refs/heads/branch2", A);
+
+		Map<String, ReflogEntry> oldLogs = getLastReflogs(
+				"refs/heads/master", "refs/heads/branch1", "refs/heads/branch2");
+		List<ReceiveCommand> cmds = Arrays.asList(
+				new ReceiveCommand(A, B, "refs/heads/master", UPDATE),
+				new ReceiveCommand(zeroId(), B, "refs/heads/branch1", CREATE));
+		execute(
+				newBatchUpdate(cmds)
+						.setAllowNonFastForwards(true)
+						.setRefLogMessage("a reflog", false));
+
+		assertResults(cmds, OK, OK);
+		assertRefs(
+				"refs/heads/master", B,
+				"refs/heads/branch1", B,
+				"refs/heads/branch2", A);
+		assertReflogEquals(
+				reflog(A, B, new PersonIdent(diskRepo), "a reflog"),
+				getLastReflog("refs/heads/master"));
+		assertReflogEquals(
+				reflog(zeroId(), B, new PersonIdent(diskRepo), "a reflog"),
+				getLastReflog("refs/heads/branch1"));
+		assertReflogUnchanged(oldLogs, "refs/heads/branch2");
+	}
+
+	@Test
+	public void reflogAppendStatusNoMessage() throws IOException {
+		writeRef("refs/heads/master", A);
+		writeRef("refs/heads/branch1", B);
+
+		List<ReceiveCommand> cmds = Arrays.asList(
+				new ReceiveCommand(A, B, "refs/heads/master", UPDATE),
+				new ReceiveCommand(B, A, "refs/heads/branch1", UPDATE_NONFASTFORWARD),
+				new ReceiveCommand(zeroId(), A, "refs/heads/branch2", CREATE));
+		execute(
+				newBatchUpdate(cmds)
+						.setAllowNonFastForwards(true)
+						.setRefLogMessage(null, true));
+
+		assertResults(cmds, OK, OK, OK);
+		assertRefs(
+				"refs/heads/master", B,
+				"refs/heads/branch1", A,
+				"refs/heads/branch2", A);
+		assertReflogEquals(
+				// Always forced; setAllowNonFastForwards(true) bypasses the check.
+				reflog(A, B, new PersonIdent(diskRepo), "forced-update"),
+				getLastReflog("refs/heads/master"));
+		assertReflogEquals(
+				reflog(B, A, new PersonIdent(diskRepo), "forced-update"),
+				getLastReflog("refs/heads/branch1"));
+		assertReflogEquals(
+				reflog(zeroId(), A, new PersonIdent(diskRepo), "created"),
+				getLastReflog("refs/heads/branch2"));
+	}
+
+	@Test
+	public void reflogAppendStatusFastForward() throws IOException {
+		writeRef("refs/heads/master", A);
+
+		List<ReceiveCommand> cmds = Arrays.asList(
+				new ReceiveCommand(A, B, "refs/heads/master", UPDATE));
+		execute(newBatchUpdate(cmds).setRefLogMessage(null, true));
+
+		assertResults(cmds, OK);
+		assertRefs("refs/heads/master", B);
+		assertReflogEquals(
+				reflog(A, B, new PersonIdent(diskRepo), "fast-forward"),
+				getLastReflog("refs/heads/master"));
+	}
+
+	@Test
+	public void reflogAppendStatusWithMessage() throws IOException {
+		writeRef("refs/heads/master", A);
+
+		List<ReceiveCommand> cmds = Arrays.asList(
+				new ReceiveCommand(A, B, "refs/heads/master", UPDATE),
+				new ReceiveCommand(zeroId(), A, "refs/heads/branch", CREATE));
+		execute(newBatchUpdate(cmds).setRefLogMessage("a reflog", true));
+
+		assertResults(cmds, OK, OK);
+		assertRefs(
+				"refs/heads/master", B,
+				"refs/heads/branch", A);
+		assertReflogEquals(
+				reflog(A, B, new PersonIdent(diskRepo), "a reflog: fast-forward"),
+				getLastReflog("refs/heads/master"));
+		assertReflogEquals(
+				reflog(zeroId(), A, new PersonIdent(diskRepo), "a reflog: created"),
+				getLastReflog("refs/heads/branch"));
+	}
+
+	@Test
+	public void reflogCustomIdent() throws IOException {
+		writeRef("refs/heads/master", A);
+
+		List<ReceiveCommand> cmds = Arrays.asList(
+				new ReceiveCommand(A, B, "refs/heads/master", UPDATE),
+				new ReceiveCommand(zeroId(), B, "refs/heads/branch", CREATE));
+		PersonIdent ident = new PersonIdent("A Reflog User", "reflog@example.com");
+		execute(
+				newBatchUpdate(cmds)
+						.setRefLogMessage("a reflog", false)
+						.setRefLogIdent(ident));
+
+		assertResults(cmds, OK, OK);
+		assertRefs(
+				"refs/heads/master", B,
+				"refs/heads/branch", B);
+		assertReflogEquals(
+				reflog(A, B, ident, "a reflog"),
+				getLastReflog("refs/heads/master"),
+				true);
+		assertReflogEquals(
+				reflog(zeroId(), B, ident, "a reflog"),
+				getLastReflog("refs/heads/branch"),
+				true);
+	}
+
+	@Test
+	public void reflogDelete() throws IOException {
+		writeRef("refs/heads/master", A);
+		writeRef("refs/heads/branch", A);
+		assertEquals(
+				2, getLastReflogs("refs/heads/master", "refs/heads/branch").size());
+
+		List<ReceiveCommand> cmds = Arrays.asList(
+				new ReceiveCommand(A, zeroId(), "refs/heads/master", DELETE),
+				new ReceiveCommand(A, B, "refs/heads/branch", UPDATE));
+		execute(newBatchUpdate(cmds).setRefLogMessage("a reflog", false));
+
+		assertResults(cmds, OK, OK);
+		assertRefs("refs/heads/branch", B);
+		assertNull(getLastReflog("refs/heads/master"));
+		assertReflogEquals(
+				reflog(A, B, new PersonIdent(diskRepo), "a reflog"),
+				getLastReflog("refs/heads/branch"));
+	}
+
+	@Test
+	public void reflogFileDirectoryConflict() throws IOException {
+		writeRef("refs/heads/master", A);
+
+		List<ReceiveCommand> cmds = Arrays.asList(
+				new ReceiveCommand(A, zeroId(), "refs/heads/master", DELETE),
+				new ReceiveCommand(zeroId(), A, "refs/heads/master/x", CREATE));
+		execute(newBatchUpdate(cmds).setRefLogMessage("a reflog", false));
+
+		assertResults(cmds, OK, OK);
+		assertRefs("refs/heads/master/x", A);
+		assertNull(getLastReflog("refs/heads/master"));
+		assertReflogEquals(
+				reflog(zeroId(), A, new PersonIdent(diskRepo), "a reflog"),
+				getLastReflog("refs/heads/master/x"));
+	}
+
+	@Test
+	public void reflogOnLockFailure() throws IOException {
+		writeRef("refs/heads/master", A);
+
+		Map<String, ReflogEntry> oldLogs =
+				getLastReflogs("refs/heads/master", "refs/heads/branch");
+
+		List<ReceiveCommand> cmds = Arrays.asList(
+				new ReceiveCommand(A, B, "refs/heads/master", UPDATE),
+				new ReceiveCommand(A, B, "refs/heads/branch", UPDATE));
+		execute(newBatchUpdate(cmds).setRefLogMessage("a reflog", false));
+
+		if (atomic) {
+			assertResults(cmds, TRANSACTION_ABORTED, LOCK_FAILURE);
+			assertReflogUnchanged(oldLogs, "refs/heads/master");
+			assertReflogUnchanged(oldLogs, "refs/heads/branch");
+		} else {
+			assertResults(cmds, OK, LOCK_FAILURE);
+			assertReflogEquals(
+					reflog(A, B, new PersonIdent(diskRepo), "a reflog"),
+					getLastReflog("refs/heads/master"));
+			assertReflogUnchanged(oldLogs, "refs/heads/branch");
+		}
+	}
+
+	@Test
+	public void overrideRefLogMessage() throws Exception {
+		writeRef("refs/heads/master", A);
+
+		List<ReceiveCommand> cmds = Arrays.asList(
+				new ReceiveCommand(A, B, "refs/heads/master", UPDATE),
+				new ReceiveCommand(zeroId(), B, "refs/heads/branch", CREATE));
+		cmds.get(0).setRefLogMessage("custom log", false);
+		PersonIdent ident = new PersonIdent(diskRepo);
+		execute(
+				newBatchUpdate(cmds)
+						.setRefLogIdent(ident)
+						.setRefLogMessage("a reflog", true));
+
+		assertResults(cmds, OK, OK);
+		assertReflogEquals(
+				reflog(A, B, ident, "custom log"),
+				getLastReflog("refs/heads/master"),
+				true);
+		assertReflogEquals(
+				reflog(zeroId(), B, ident, "a reflog: created"),
+				getLastReflog("refs/heads/branch"),
+				true);
+	}
+
+	@Test
+	public void overrideDisableRefLog() throws Exception {
+		writeRef("refs/heads/master", A);
+
+		Map<String, ReflogEntry> oldLogs =
+				getLastReflogs("refs/heads/master", "refs/heads/branch");
+
+		List<ReceiveCommand> cmds = Arrays.asList(
+				new ReceiveCommand(A, B, "refs/heads/master", UPDATE),
+				new ReceiveCommand(zeroId(), B, "refs/heads/branch", CREATE));
+		cmds.get(0).disableRefLog();
+		execute(newBatchUpdate(cmds).setRefLogMessage("a reflog", true));
+
+		assertResults(cmds, OK, OK);
+		assertReflogUnchanged(oldLogs, "refs/heads/master");
+		assertReflogEquals(
+				reflog(zeroId(), B, new PersonIdent(diskRepo), "a reflog: created"),
+				getLastReflog("refs/heads/branch"));
+	}
+
+	@Test
+	public void packedRefsLockFailure() throws Exception {
+		writeLooseRef("refs/heads/master", A);
+
+		List<ReceiveCommand> cmds = Arrays.asList(
+				new ReceiveCommand(A, B, "refs/heads/master", UPDATE),
+				new ReceiveCommand(zeroId(), B, "refs/heads/branch", CREATE));
+
+		LockFile myLock = refdir.lockPackedRefs();
+		try {
+			execute(newBatchUpdate(cmds).setAllowNonFastForwards(true));
+
+			assertFalse(getLockFile("refs/heads/master").exists());
+			assertFalse(getLockFile("refs/heads/branch").exists());
+
+			if (atomic) {
+				assertResults(cmds, LOCK_FAILURE, TRANSACTION_ABORTED);
+				assertRefs("refs/heads/master", A);
+			} else {
+				// Only operates on loose refs, doesn't care that packed-refs is locked.
+				assertResults(cmds, OK, OK);
+				assertRefs(
+						"refs/heads/master", B,
+						"refs/heads/branch", B);
+			}
+		} finally {
+			myLock.unlock();
+		}
+	}
+
+	@Test
+	public void oneRefLockFailure() throws Exception {
+		writeLooseRef("refs/heads/master", A);
+
+		List<ReceiveCommand> cmds = Arrays.asList(
+				new ReceiveCommand(zeroId(), B, "refs/heads/branch", CREATE),
+				new ReceiveCommand(A, B, "refs/heads/master", UPDATE));
+
+		LockFile myLock = new LockFile(refdir.fileFor("refs/heads/master"));
+		assertTrue(myLock.lock());
+		try {
+			execute(newBatchUpdate(cmds).setAllowNonFastForwards(true));
+
+			assertFalse(LockFile.getLockFile(refdir.packedRefsFile).exists());
+			assertFalse(getLockFile("refs/heads/branch").exists());
+
+			if (atomic) {
+				assertResults(cmds, TRANSACTION_ABORTED, LOCK_FAILURE);
+				assertRefs("refs/heads/master", A);
+			} else {
+				assertResults(cmds, OK, LOCK_FAILURE);
+				assertRefs(
+						"refs/heads/branch", B,
+						"refs/heads/master", A);
+			}
+		} finally {
+			myLock.unlock();
+		}
+	}
+
+	@Test
+	public void singleRefUpdateDoesNotRequirePackedRefsLock() throws Exception {
+		writeLooseRef("refs/heads/master", A);
+
+		List<ReceiveCommand> cmds = Arrays.asList(
+				new ReceiveCommand(A, B, "refs/heads/master", UPDATE));
+
+		LockFile myLock = refdir.lockPackedRefs();
+		try {
+			execute(newBatchUpdate(cmds).setAllowNonFastForwards(true));
+
+			assertFalse(getLockFile("refs/heads/master").exists());
+			assertResults(cmds, OK);
+			assertRefs("refs/heads/master", B);
+		} finally {
+			myLock.unlock();
+		}
+	}
+
+	@Test
+	public void atomicUpdateRespectsInProcessLock() throws Exception {
+		assumeTrue(atomic);
+
+		writeLooseRef("refs/heads/master", A);
+
+		List<ReceiveCommand> cmds = Arrays.asList(
+				new ReceiveCommand(A, B, "refs/heads/master", UPDATE),
+				new ReceiveCommand(zeroId(), B, "refs/heads/branch", CREATE));
+
+		Thread t = new Thread(() -> {
+			try {
+				execute(newBatchUpdate(cmds).setAllowNonFastForwards(true));
+			} catch (Exception e) {
+				throw new RuntimeException(e);
+			}
+		});
+
+		ReentrantLock l = refdir.inProcessPackedRefsLock;
+		l.lock();
+		try {
+			t.start();
+			long timeoutSecs = 10;
+			long startNanos = System.nanoTime();
+
+			// Hold onto the lock until we observe the worker thread has attempted to
+			// acquire it.
+			while (l.getQueueLength() == 0) {
+				long elapsedNanos = System.nanoTime() - startNanos;
+				assertTrue(
+						"timed out waiting for work thread to attempt to acquire lock",
+						NANOSECONDS.toSeconds(elapsedNanos) < timeoutSecs);
+				Thread.sleep(3);
+			}
+
+			// Once we unlock, the worker thread should finish the update promptly.
+			l.unlock();
+			t.join(SECONDS.toMillis(timeoutSecs));
+			assertFalse(t.isAlive());
+		} finally {
+			if (l.isHeldByCurrentThread()) {
+				l.unlock();
+			}
+		}
+
+		assertResults(cmds, OK, OK);
+		assertRefs(
+				"refs/heads/master", B,
+				"refs/heads/branch", B);
+	}
+
+	private void writeLooseRef(String name, AnyObjectId id) throws IOException {
+		write(new File(diskRepo.getDirectory(), name), id.name() + "\n");
+	}
+
+	private void writeRef(String name, AnyObjectId id) throws IOException {
+		RefUpdate u = diskRepo.updateRef(name);
+		u.setRefLogMessage(getClass().getSimpleName(), false);
+		u.setForceUpdate(true);
+		u.setNewObjectId(id);
+		RefUpdate.Result r = u.update();
+		switch (r) {
+			case NEW:
+			case FORCED:
+				return;
+			default:
+				throw new IOException("Got " + r + " while updating " + name);
+		}
+	}
+
+	private BatchRefUpdate newBatchUpdate(List<ReceiveCommand> cmds) {
+		BatchRefUpdate u = refdir.newBatchUpdate();
+		if (atomic) {
+			assertTrue(u.isAtomic());
+		} else {
+			u.setAtomic(false);
+		}
+		u.addCommand(cmds);
+		return u;
+	}
+
+	private void execute(BatchRefUpdate u) throws IOException {
+		execute(u, false);
+	}
+
+	private void execute(BatchRefUpdate u, boolean strictWork) throws IOException {
+		try (RevWalk rw = new RevWalk(diskRepo)) {
+			u.execute(rw,
+					strictWork ? new StrictWorkMonitor() : NullProgressMonitor.INSTANCE);
+		}
+	}
+
+	private void assertRefs(Object... args) throws IOException {
+		if (args.length % 2 != 0) {
+			throw new IllegalArgumentException(
+					"expected even number of args: " + Arrays.toString(args));
+		}
+
+		Map<String, AnyObjectId> expected = new LinkedHashMap<>();
+		for (int i = 0; i < args.length; i += 2) {
+			expected.put((String) args[i], (AnyObjectId) args[i + 1]);
+		}
+
+		Map<String, Ref> refs = refdir.getRefs(RefDatabase.ALL);
+		Ref actualHead = refs.remove(Constants.HEAD);
+		if (actualHead != null) {
+			String actualLeafName = actualHead.getLeaf().getName();
+			assertEquals(
+					"expected HEAD to point to refs/heads/master, got: " + actualLeafName,
+					"refs/heads/master", actualLeafName);
+			AnyObjectId expectedMaster = expected.get("refs/heads/master");
+			assertNotNull("expected master ref since HEAD exists", expectedMaster);
+			assertEquals(expectedMaster, actualHead.getObjectId());
+		}
+
+		Map<String, AnyObjectId> actual = new LinkedHashMap<>();
+		refs.forEach((n, r) -> actual.put(n, r.getObjectId()));
+
+		assertEquals(expected.keySet(), actual.keySet());
+		actual.forEach((n, a) -> assertEquals(n, expected.get(n), a));
+	}
+
+	enum Result {
+		OK(ReceiveCommand.Result.OK),
+		LOCK_FAILURE(ReceiveCommand.Result.LOCK_FAILURE),
+		REJECTED_NONFASTFORWARD(ReceiveCommand.Result.REJECTED_NONFASTFORWARD),
+		REJECTED_MISSING_OBJECT(ReceiveCommand.Result.REJECTED_MISSING_OBJECT),
+		TRANSACTION_ABORTED(ReceiveCommand::isTransactionAborted);
+
+		final Predicate<? super ReceiveCommand> p;
+
+		private Result(Predicate<? super ReceiveCommand> p) {
+			this.p = p;
+		}
+
+		private Result(ReceiveCommand.Result result) {
+			this(c -> c.getResult() == result);
+		}
+	}
+
+	private void assertResults(
+			List<ReceiveCommand> cmds, Result... expected) {
+		if (expected.length != cmds.size()) {
+			throw new IllegalArgumentException(
+					"expected " + cmds.size() + " result args");
+		}
+		for (int i = 0; i < cmds.size(); i++) {
+			ReceiveCommand c = cmds.get(i);
+			Result r = expected[i];
+			assertTrue(
+					String.format(
+							"result of command (%d) should be %s: %s %s%s",
+							Integer.valueOf(i), r, c,
+							c.getResult(),
+							c.getMessage() != null ? " (" + c.getMessage() + ")" : ""),
+					r.p.test(c));
+		}
+	}
+
+	private Map<String, ReflogEntry> getLastReflogs(String... names)
+			throws IOException {
+		Map<String, ReflogEntry> result = new LinkedHashMap<>();
+		for (String name : names) {
+			ReflogEntry e = getLastReflog(name);
+			if (e != null) {
+				result.put(name, e);
+			}
+		}
+		return result;
+	}
+
+	private ReflogEntry getLastReflog(String name) throws IOException {
+		ReflogReader r = diskRepo.getReflogReader(name);
+		if (r == null) {
+			return null;
+		}
+		return r.getLastEntry();
+	}
+
+	private File getLockFile(String refName) {
+		return LockFile.getLockFile(refdir.fileFor(refName));
+	}
+
+	private void assertReflogUnchanged(
+			Map<String, ReflogEntry> old, String name) throws IOException {
+		assertReflogEquals(old.get(name), getLastReflog(name), true);
+	}
+
+	private static void assertReflogEquals(
+			ReflogEntry expected, ReflogEntry actual) {
+		assertReflogEquals(expected, actual, false);
+	}
+
+	private static void assertReflogEquals(
+			ReflogEntry expected, ReflogEntry actual, boolean strictTime) {
+		if (expected == null) {
+			assertNull(actual);
+			return;
+		}
+		assertNotNull(actual);
+		assertEquals(expected.getOldId(), actual.getOldId());
+		assertEquals(expected.getNewId(), actual.getNewId());
+		if (strictTime) {
+			assertEquals(expected.getWho(), actual.getWho());
+		} else {
+			assertEquals(expected.getWho().getName(), actual.getWho().getName());
+			assertEquals(
+					expected.getWho().getEmailAddress(),
+					actual.getWho().getEmailAddress());
+		}
+		assertEquals(expected.getComment(), actual.getComment());
+	}
+
+	private static ReflogEntry reflog(ObjectId oldId, ObjectId newId,
+			PersonIdent who, String comment) {
+		return new ReflogEntry() {
+			@Override
+			public ObjectId getOldId() {
+				return oldId;
+			}
+
+			@Override
+			public ObjectId getNewId() {
+				return newId;
+			}
+
+			@Override
+			public PersonIdent getWho() {
+				return who;
+			}
+
+			@Override
+			public String getComment() {
+				return comment;
+			}
+
+			@Override
+			public CheckoutEntry parseCheckout() {
+				throw new UnsupportedOperationException();
+			}
+		};
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPackRefsTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPackRefsTest.java
index 11a2a22..c43bdbd 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPackRefsTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/GcPackRefsTest.java
@@ -43,12 +43,13 @@
 
 package org.eclipse.jgit.internal.storage.file;
 
-import static java.lang.Integer.valueOf;
+import static org.hamcrest.Matchers.lessThanOrEqualTo;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThat;
 
 import java.io.File;
 import java.io.IOException;
@@ -74,6 +75,7 @@
 import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.junit.Test;
 
+@SuppressWarnings("boxing")
 public class GcPackRefsTest extends GcTestCase {
 	@Test
 	public void looseRefPacked() throws Exception {
@@ -100,27 +102,23 @@
 		RevBlob a = tr.blob("a");
 		tr.lightweightTag("t", a);
 
-		final CyclicBarrier syncPoint = new CyclicBarrier(2);
+		CyclicBarrier syncPoint = new CyclicBarrier(2);
 
-		Callable<Integer> packRefs = new Callable<Integer>() {
-
-			/** @return 0 for success, 1 in case of error when writing pack */
-			@Override
-			public Integer call() throws Exception {
-				syncPoint.await();
-				try {
-					gc.packRefs();
-					return valueOf(0);
-				} catch (IOException e) {
-					return valueOf(1);
-				}
+		// Returns 0 for success, 1 in case of error when writing pack.
+		Callable<Integer> packRefs = () -> {
+			syncPoint.await();
+			try {
+				gc.packRefs();
+				return 0;
+			} catch (IOException e) {
+				return 1;
 			}
 		};
 		ExecutorService pool = Executors.newFixedThreadPool(2);
 		try {
 			Future<Integer> p1 = pool.submit(packRefs);
 			Future<Integer> p2 = pool.submit(packRefs);
-			assertEquals(1, p1.get().intValue() + p2.get().intValue());
+			assertThat(p1.get() + p2.get(), lessThanOrEqualTo(1));
 		} finally {
 			pool.shutdown();
 			pool.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/PackWriterTest.java
index c817dc3..9b97eb4 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
@@ -711,7 +711,7 @@
 			}
 			ObjectWalk ow = walk.toObjectWalkWithSameObjects();
 
-			pw.preparePack(NullProgressMonitor.INSTANCE, ow, want, have);
+			pw.preparePack(NullProgressMonitor.INSTANCE, ow, want, have, NONE);
 			String id = pw.computeName().getName();
 			File packdir = new File(repo.getObjectsDirectory(), "pack");
 			File packFile = new File(packdir, "pack-" + id + ".pack");
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java
index 53db123..fefccf3 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefDirectoryTest.java
@@ -61,32 +61,27 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.List;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 
+import org.eclipse.jgit.errors.LockFailedException;
 import org.eclipse.jgit.events.ListenerHandle;
 import org.eclipse.jgit.events.RefsChangedEvent;
 import org.eclipse.jgit.events.RefsChangedListener;
 import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.AnyObjectId;
-import org.eclipse.jgit.lib.BatchRefUpdate;
-import org.eclipse.jgit.lib.NullProgressMonitor;
-import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Ref.Storage;
 import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevTag;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.ReceiveCommand;
-import org.eclipse.jgit.transport.ReceiveCommand.Type;
 import org.junit.Before;
 import org.junit.Test;
 
+@SuppressWarnings("boxing")
 public class RefDirectoryTest extends LocalDiskRepositoryTestCase {
 	private Repository diskRepo;
 
@@ -1293,125 +1288,20 @@
 	}
 
 	@Test
-	public void testBatchRefUpdateSimpleNoForce() throws IOException {
+	public void testPackedRefsLockFailure() throws Exception {
 		writeLooseRef("refs/heads/master", A);
-		writeLooseRef("refs/heads/masters", B);
-		List<ReceiveCommand> commands = Arrays.asList(
-				newCommand(A, B, "refs/heads/master",
-						ReceiveCommand.Type.UPDATE),
-				newCommand(B, A, "refs/heads/masters",
-						ReceiveCommand.Type.UPDATE_NONFASTFORWARD));
-		BatchRefUpdate batchUpdate = refdir.newBatchUpdate();
-		batchUpdate.addCommand(commands);
-		batchUpdate.execute(new RevWalk(diskRepo), new StrictWorkMonitor());
-		Map<String, Ref> refs = refdir.getRefs(RefDatabase.ALL);
-		assertEquals(ReceiveCommand.Result.OK, commands.get(0).getResult());
-		assertEquals(ReceiveCommand.Result.REJECTED_NONFASTFORWARD, commands
-				.get(1).getResult());
-		assertEquals("[HEAD, refs/heads/master, refs/heads/masters]", refs
-				.keySet().toString());
-		assertEquals(B.getId(), refs.get("refs/heads/master").getObjectId());
-		assertEquals(B.getId(), refs.get("refs/heads/masters").getObjectId());
-	}
-
-	@Test
-	public void testBatchRefUpdateSimpleForce() throws IOException {
-		writeLooseRef("refs/heads/master", A);
-		writeLooseRef("refs/heads/masters", B);
-		List<ReceiveCommand> commands = Arrays.asList(
-				newCommand(A, B, "refs/heads/master",
-						ReceiveCommand.Type.UPDATE),
-				newCommand(B, A, "refs/heads/masters",
-						ReceiveCommand.Type.UPDATE_NONFASTFORWARD));
-		BatchRefUpdate batchUpdate = refdir.newBatchUpdate();
-		batchUpdate.setAllowNonFastForwards(true);
-		batchUpdate.addCommand(commands);
-		batchUpdate.execute(new RevWalk(diskRepo), new StrictWorkMonitor());
-		Map<String, Ref> refs = refdir.getRefs(RefDatabase.ALL);
-		assertEquals(ReceiveCommand.Result.OK, commands.get(0).getResult());
-		assertEquals(ReceiveCommand.Result.OK, commands.get(1).getResult());
-		assertEquals("[HEAD, refs/heads/master, refs/heads/masters]", refs
-				.keySet().toString());
-		assertEquals(B.getId(), refs.get("refs/heads/master").getObjectId());
-		assertEquals(A.getId(), refs.get("refs/heads/masters").getObjectId());
-	}
-
-	@Test
-	public void testBatchRefUpdateNonFastForwardDoesNotDoExpensiveMergeCheck()
-			throws IOException {
-		writeLooseRef("refs/heads/master", B);
-		List<ReceiveCommand> commands = Arrays.asList(
-				newCommand(B, A, "refs/heads/master",
-						ReceiveCommand.Type.UPDATE_NONFASTFORWARD));
-		BatchRefUpdate batchUpdate = refdir.newBatchUpdate();
-		batchUpdate.setAllowNonFastForwards(true);
-		batchUpdate.addCommand(commands);
-		batchUpdate.execute(new RevWalk(diskRepo) {
-			@Override
-			public boolean isMergedInto(RevCommit base, RevCommit tip) {
-				throw new AssertionError("isMergedInto() should not be called");
-			}
-		}, new StrictWorkMonitor());
-		Map<String, Ref> refs = refdir.getRefs(RefDatabase.ALL);
-		assertEquals(ReceiveCommand.Result.OK, commands.get(0).getResult());
-		assertEquals(A.getId(), refs.get("refs/heads/master").getObjectId());
-	}
-
-	@Test
-	public void testBatchRefUpdateConflict() throws IOException {
-		writeLooseRef("refs/heads/master", A);
-		writeLooseRef("refs/heads/masters", B);
-		List<ReceiveCommand> commands = Arrays.asList(
-				newCommand(A, B, "refs/heads/master",
-						ReceiveCommand.Type.UPDATE),
-				newCommand(null, A, "refs/heads/master/x",
-						ReceiveCommand.Type.CREATE),
-				newCommand(null, A, "refs/heads", ReceiveCommand.Type.CREATE));
-		BatchRefUpdate batchUpdate = refdir.newBatchUpdate();
-		batchUpdate.setAllowNonFastForwards(true);
-		batchUpdate.addCommand(commands);
-		batchUpdate
-				.execute(new RevWalk(diskRepo), NullProgressMonitor.INSTANCE);
-		Map<String, Ref> refs = refdir.getRefs(RefDatabase.ALL);
-		assertEquals(ReceiveCommand.Result.OK, commands.get(0).getResult());
-		assertEquals(ReceiveCommand.Result.LOCK_FAILURE, commands.get(1)
-				.getResult());
-		assertEquals(ReceiveCommand.Result.LOCK_FAILURE, commands.get(2)
-				.getResult());
-		assertEquals("[HEAD, refs/heads/master, refs/heads/masters]", refs
-				.keySet().toString());
-		assertEquals(B.getId(), refs.get("refs/heads/master").getObjectId());
-		assertEquals(B.getId(), refs.get("refs/heads/masters").getObjectId());
-	}
-
-	@Test
-	public void testBatchRefUpdateConflictThanksToDelete() throws IOException {
-		writeLooseRef("refs/heads/master", A);
-		writeLooseRef("refs/heads/masters", B);
-		List<ReceiveCommand> commands = Arrays.asList(
-				newCommand(A, B, "refs/heads/master",
-						ReceiveCommand.Type.UPDATE),
-				newCommand(null, A, "refs/heads/masters/x",
-						ReceiveCommand.Type.CREATE),
-				newCommand(B, null, "refs/heads/masters",
-						ReceiveCommand.Type.DELETE));
-		BatchRefUpdate batchUpdate = refdir.newBatchUpdate();
-		batchUpdate.setAllowNonFastForwards(true);
-		batchUpdate.addCommand(commands);
-		batchUpdate.execute(new RevWalk(diskRepo), new StrictWorkMonitor());
-		Map<String, Ref> refs = refdir.getRefs(RefDatabase.ALL);
-		assertEquals(ReceiveCommand.Result.OK, commands.get(0).getResult());
-		assertEquals(ReceiveCommand.Result.OK, commands.get(1).getResult());
-		assertEquals(ReceiveCommand.Result.OK, commands.get(2).getResult());
-		assertEquals("[HEAD, refs/heads/master, refs/heads/masters/x]", refs
-				.keySet().toString());
-		assertEquals(A.getId(), refs.get("refs/heads/masters/x").getObjectId());
-	}
-
-	private static ReceiveCommand newCommand(RevCommit a, RevCommit b,
-			String string, Type update) {
-		return new ReceiveCommand(a != null ? a.getId() : null,
-				b != null ? b.getId() : null, string, update);
+		refdir.setRetrySleepMs(Arrays.asList(0, 0));
+		LockFile myLock = refdir.lockPackedRefs();
+		try {
+			refdir.pack(Arrays.asList("refs/heads/master"));
+			fail("expected LockFailedException");
+		} catch (LockFailedException e) {
+			assertEquals(refdir.packedRefsFile.getPath(), e.getFile().getPath());
+		} finally {
+			myLock.unlock();
+		}
+		Ref ref = refdir.getRef("refs/heads/master");
+		assertEquals(Storage.LOOSE, ref.getStorage());
 	}
 
 	private void writeLooseRef(String name, AnyObjectId id) throws IOException {
@@ -1439,34 +1329,4 @@
 		File path = new File(diskRepo.getDirectory(), name);
 		assertTrue("deleted " + name, path.delete());
 	}
-
-	private static final class StrictWorkMonitor implements ProgressMonitor {
-		private int lastWork, totalWork;
-
-		@Override
-		public void start(int totalTasks) {
-			// empty
-		}
-
-		@Override
-		public void beginTask(String title, int total) {
-			this.totalWork = total;
-			lastWork = 0;
-		}
-
-		@Override
-		public void update(int completed) {
-			lastWork += completed;
-		}
-
-		@Override
-		public void endTask() {
-			assertEquals("Units of work recorded", totalWork, lastWork);
-		}
-
-		@Override
-		public boolean isCancelled() {
-			return false;
-		}
-	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefUpdateTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefUpdateTest.java
index daef91d..34f9eb9 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefUpdateTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/RefUpdateTest.java
@@ -45,6 +45,7 @@
 
 package org.eclipse.jgit.internal.storage.file;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.eclipse.jgit.junit.Assert.assertEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -64,6 +65,7 @@
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefRename;
@@ -240,14 +242,73 @@
 	@Test
 	public void testDeleteHeadInBareRepo() throws IOException {
 		Repository bareRepo = createBareRepository();
+		String master = "refs/heads/master";
+		Ref head = bareRepo.exactRef(Constants.HEAD);
+		assertNotNull(head);
+		assertTrue(head.isSymbolic());
+		assertEquals(master, head.getLeaf().getName());
+		assertNull(head.getObjectId());
+		assertNull(bareRepo.exactRef(master));
+
+		ObjectId blobId;
+		try (ObjectInserter ins = bareRepo.newObjectInserter()) {
+			blobId = ins.insert(Constants.OBJ_BLOB, "contents".getBytes(UTF_8));
+			ins.flush();
+		}
+
+		// Create master via HEAD, so we delete it.
 		RefUpdate ref = bareRepo.updateRef(Constants.HEAD);
-		ref.setNewObjectId(ObjectId
-				.fromString("0123456789012345678901234567890123456789"));
-		// Create the HEAD ref so we can delete it.
+		ref.setNewObjectId(blobId);
 		assertEquals(Result.NEW, ref.update());
+
+		head = bareRepo.exactRef(Constants.HEAD);
+		assertTrue(head.isSymbolic());
+		assertEquals(master, head.getLeaf().getName());
+		assertEquals(blobId, head.getLeaf().getObjectId());
+		assertEquals(blobId, bareRepo.exactRef(master).getObjectId());
+
+		// Unlike in a non-bare repo, deleting the HEAD is allowed, and leaves HEAD
+		// back in a dangling state.
 		ref = bareRepo.updateRef(Constants.HEAD);
-		delete(bareRepo, ref, Result.NO_CHANGE, true, true);
+		ref.setExpectedOldObjectId(blobId);
+		ref.setForceUpdate(true);
+		delete(bareRepo, ref, Result.FORCED, true, true);
+
+		head = bareRepo.exactRef(Constants.HEAD);
+		assertNotNull(head);
+		assertTrue(head.isSymbolic());
+		assertEquals(master, head.getLeaf().getName());
+		assertNull(head.getObjectId());
+		assertNull(bareRepo.exactRef(master));
 	}
+
+	@Test
+	public void testDeleteSymref() throws IOException {
+		RefUpdate dst = updateRef("refs/heads/abc");
+		assertEquals(Result.NEW, dst.update());
+		ObjectId id = dst.getNewObjectId();
+
+		RefUpdate u = db.updateRef("refs/symref");
+		assertEquals(Result.NEW, u.link(dst.getName()));
+
+		Ref ref = db.exactRef(u.getName());
+		assertNotNull(ref);
+		assertTrue(ref.isSymbolic());
+		assertEquals(dst.getName(), ref.getLeaf().getName());
+		assertEquals(id, ref.getLeaf().getObjectId());
+
+		u = db.updateRef(u.getName());
+		u.setDetachingSymbolicRef();
+		u.setForceUpdate(true);
+		assertEquals(Result.FORCED, u.delete());
+
+		assertNull(db.exactRef(u.getName()));
+		ref = db.exactRef(dst.getName());
+		assertNotNull(ref);
+		assertFalse(ref.isSymbolic());
+		assertEquals(id, ref.getObjectId());
+	}
+
 	/**
 	 * Delete a loose ref and make sure the directory in refs is deleted too,
 	 * and the reflog dir too
@@ -898,6 +959,60 @@
 				"HEAD").getReverseEntries().get(0).getComment());
 	}
 
+	@Test
+	public void testCreateMissingObject() throws IOException {
+		String name = "refs/heads/abc";
+		ObjectId bad =
+				ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+		RefUpdate ru = db.updateRef(name);
+		ru.setNewObjectId(bad);
+		Result update = ru.update();
+		assertEquals(Result.REJECTED_MISSING_OBJECT, update);
+
+		Ref ref = db.exactRef(name);
+		assertNull(ref);
+	}
+
+	@Test
+	public void testUpdateMissingObject() throws IOException {
+		String name = "refs/heads/abc";
+		RefUpdate ru = updateRef(name);
+		Result update = ru.update();
+		assertEquals(Result.NEW, update);
+		ObjectId oldId = ru.getNewObjectId();
+
+		ObjectId bad =
+				ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+		ru = db.updateRef(name);
+		ru.setNewObjectId(bad);
+		update = ru.update();
+		assertEquals(Result.REJECTED_MISSING_OBJECT, update);
+
+		Ref ref = db.exactRef(name);
+		assertNotNull(ref);
+		assertEquals(oldId, ref.getObjectId());
+	}
+
+	@Test
+	public void testForceUpdateMissingObject() throws IOException {
+		String name = "refs/heads/abc";
+		RefUpdate ru = updateRef(name);
+		Result update = ru.update();
+		assertEquals(Result.NEW, update);
+		ObjectId oldId = ru.getNewObjectId();
+
+		ObjectId bad =
+				ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+		ru = db.updateRef(name);
+		ru.setNewObjectId(bad);
+		update = ru.forceUpdate();
+		assertEquals(Result.REJECTED_MISSING_OBJECT, update);
+
+		Ref ref = db.exactRef(name);
+		assertNotNull(ref);
+		assertEquals(oldId, ref.getObjectId());
+	}
+
 	private static void writeReflog(Repository db, ObjectId newId, String msg,
 			String refName) throws IOException {
 		RefDirectory refs = (RefDirectory) db.getRefDatabase();
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 ae1e531..9d23d83 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/file/T0003_BasicTest.java
@@ -661,33 +661,39 @@
 
 	@Test
 	public void test028_LockPackedRef() throws IOException {
+		ObjectId id1;
+		ObjectId id2;
+		try (ObjectInserter ins = db.newObjectInserter()) {
+			id1 = ins.insert(
+					Constants.OBJ_BLOB, "contents1".getBytes(Constants.CHARSET));
+			id2 = ins.insert(
+					Constants.OBJ_BLOB, "contents2".getBytes(Constants.CHARSET));
+			ins.flush();
+		}
+
 		writeTrashFile(".git/packed-refs",
-				"7f822839a2fe9760f386cbbbcb3f92c5fe81def7 refs/heads/foobar");
+				id1.name() + " refs/heads/foobar");
 		writeTrashFile(".git/HEAD", "ref: refs/heads/foobar\n");
 		BUG_WorkAroundRacyGitIssues("packed-refs");
 		BUG_WorkAroundRacyGitIssues("HEAD");
 
 		ObjectId resolve = db.resolve("HEAD");
-		assertEquals("7f822839a2fe9760f386cbbbcb3f92c5fe81def7", resolve.name());
+		assertEquals(id1, resolve);
 
 		RefUpdate lockRef = db.updateRef("HEAD");
-		ObjectId newId = ObjectId
-				.fromString("07f822839a2fe9760f386cbbbcb3f92c5fe81def");
-		lockRef.setNewObjectId(newId);
+		lockRef.setNewObjectId(id2);
 		assertEquals(RefUpdate.Result.FORCED, lockRef.forceUpdate());
 
 		assertTrue(new File(db.getDirectory(), "refs/heads/foobar").exists());
-		assertEquals(newId, db.resolve("refs/heads/foobar"));
+		assertEquals(id2, db.resolve("refs/heads/foobar"));
 
 		// Again. The ref already exists
 		RefUpdate lockRef2 = db.updateRef("HEAD");
-		ObjectId newId2 = ObjectId
-				.fromString("7f822839a2fe9760f386cbbbcb3f92c5fe81def7");
-		lockRef2.setNewObjectId(newId2);
+		lockRef2.setNewObjectId(id1);
 		assertEquals(RefUpdate.Result.FORCED, lockRef2.forceUpdate());
 
 		assertTrue(new File(db.getDirectory(), "refs/heads/foobar").exists());
-		assertEquals(newId2, db.resolve("refs/heads/foobar"));
+		assertEquals(id1, db.resolve("refs/heads/foobar"));
 	}
 
 	@Test
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/pack/GcCommitSelectionTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/pack/GcCommitSelectionTest.java
index 20b8c51..d9b58e2 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/pack/GcCommitSelectionTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/storage/pack/GcCommitSelectionTest.java
@@ -69,6 +69,15 @@
 
 	@Test
 	public void testBitmapSpansNoMerges() throws Exception {
+		testBitmapSpansNoMerges(false);
+	}
+
+	@Test
+	public void testBitmapSpansNoMergesWithTags() throws Exception {
+		testBitmapSpansNoMerges(true);
+	}
+
+	private void testBitmapSpansNoMerges(boolean withTags) throws Exception {
 		/*
 		 * Commit counts -> expected bitmap counts for history without merges.
 		 * The top 100 contiguous commits should always have bitmaps, and the
@@ -89,7 +98,10 @@
 			assertTrue(nextCommitCount > currentCommits); // programming error
 			for (int i = currentCommits; i < nextCommitCount; i++) {
 				String str = "A" + i;
-				bb.commit().message(str).add(str, str).create();
+				RevCommit rc = bb.commit().message(str).add(str, str).create();
+				if (withTags) {
+					tr.lightweightTag(str, rc);
+				}
 			}
 			currentCommits = nextCommitCount;
 
@@ -233,7 +245,7 @@
 				m8, m9);
 		PackWriterBitmapPreparer preparer = newPeparer(m9, commits);
 		List<BitmapCommit> selection = new ArrayList<>(
-				preparer.selectCommits(commits.size()));
+				preparer.selectCommits(commits.size(), PackWriter.NONE));
 
 		// Verify that the output is ordered by the separate "chains"
 		String[] expected = { m0.name(), m1.name(), m2.name(), m4.name(),
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/AbbreviatedObjectIdTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/AbbreviatedObjectIdTest.java
index 6529d9e..30a9626 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/AbbreviatedObjectIdTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/AbbreviatedObjectIdTest.java
@@ -86,7 +86,6 @@
 		final ObjectId f = i.toObjectId();
 		assertNotNull(f);
 		assertEquals(ObjectId.fromString(s), f);
-		assertEquals(f.hashCode(), i.hashCode());
 	}
 
 	@Test
@@ -101,7 +100,6 @@
 		final ObjectId f = i.toObjectId();
 		assertNotNull(f);
 		assertEquals(ObjectId.fromString(s), f);
-		assertEquals(f.hashCode(), i.hashCode());
 	}
 
 	@Test
@@ -215,7 +213,7 @@
 	}
 
 	@Test
-	public void testEquals_Short() {
+	public void testEquals_Short8() {
 		final String s = "7b6e8067";
 		final AbbreviatedObjectId a = AbbreviatedObjectId.fromString(s);
 		final AbbreviatedObjectId b = AbbreviatedObjectId.fromString(s);
@@ -226,6 +224,18 @@
 	}
 
 	@Test
+	public void testEquals_Short4() {
+		final String s = "7b6e";
+		final AbbreviatedObjectId a = AbbreviatedObjectId.fromString(s);
+		final AbbreviatedObjectId b = AbbreviatedObjectId.fromString(s);
+		assertNotSame(a, b);
+		assertTrue(a.hashCode() != 0);
+		assertTrue(a.hashCode() == b.hashCode());
+		assertEquals(b, a);
+		assertEquals(a, b);
+	}
+
+	@Test
 	public void testEquals_Full() {
 		final String s = "7b6e8067ec96acef9a4184b43210d583b6d2f99a";
 		final AbbreviatedObjectId a = AbbreviatedObjectId.fromString(s);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/DirCacheCheckoutTest.java
index 75b574e..f8c2d45 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
@@ -1672,6 +1672,20 @@
 		}
 	}
 
+	@Test
+	public void testLongFilename() throws Exception {
+		char[] bytes = new char[253];
+		Arrays.fill(bytes, 'f');
+		String longFileName = new String(bytes);
+		// 1
+		doit(mkmap(longFileName, "a"), mkmap(longFileName, "b"),
+				mkmap(longFileName, "a"));
+		writeTrashFile(longFileName, "a");
+		checkout();
+		assertNoConflicts();
+		assertUpdated(longFileName);
+	}
+
 	public void assertWorkDir(Map<String, String> i)
 			throws CorruptObjectException,
 			IOException {
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 43160fb..c8729d9 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectCheckerTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectCheckerTest.java
@@ -45,6 +45,7 @@
 package org.eclipse.jgit.lib;
 
 import static java.lang.Integer.valueOf;
+import static org.eclipse.jgit.junit.JGitTestUtil.concat;
 import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH;
 import static org.eclipse.jgit.lib.Constants.OBJ_BAD;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
@@ -1054,20 +1055,7 @@
 		checker.checkTree(data);
 	}
 
-	private static byte[] concat(byte[]... b) {
-		int n = 0;
-		for (byte[] a : b) {
-			n += a.length;
-		}
 
-		byte[] data = new byte[n];
-		n = 0;
-		for (byte[] a : b) {
-			System.arraycopy(a, 0, data, n, a.length);
-			n += a.length;
-		}
-		return data;
-	}
 
 	@Test
 	public void testInvalidTreeNameIsMacHFSGitCorruptUTF8AtEnd()
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ReflogResolveTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ReflogResolveTest.java
index 7db9f60..15f28af 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ReflogResolveTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ReflogResolveTest.java
@@ -179,4 +179,4 @@
 			}
 		}
 	}
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/SubmoduleConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/SubmoduleConfigTest.java
new file mode 100644
index 0000000..f5b0c5b
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/SubmoduleConfigTest.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2017, David Pursehouse <david.pursehouse@gmail.com>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.lib;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.eclipse.jgit.lib.SubmoduleConfig.FetchRecurseSubmodulesMode;
+import org.junit.Test;
+
+public class SubmoduleConfigTest {
+	@Test
+	public void fetchRecurseMatch() throws Exception {
+		assertTrue(FetchRecurseSubmodulesMode.YES.matchConfigValue("yes"));
+		assertTrue(FetchRecurseSubmodulesMode.YES.matchConfigValue("YES"));
+		assertTrue(FetchRecurseSubmodulesMode.YES.matchConfigValue("true"));
+		assertTrue(FetchRecurseSubmodulesMode.YES.matchConfigValue("TRUE"));
+
+		assertTrue(FetchRecurseSubmodulesMode.ON_DEMAND
+				.matchConfigValue("on-demand"));
+		assertTrue(FetchRecurseSubmodulesMode.ON_DEMAND
+				.matchConfigValue("ON-DEMAND"));
+		assertTrue(FetchRecurseSubmodulesMode.ON_DEMAND
+				.matchConfigValue("on_demand"));
+		assertTrue(FetchRecurseSubmodulesMode.ON_DEMAND
+				.matchConfigValue("ON_DEMAND"));
+
+		assertTrue(FetchRecurseSubmodulesMode.NO.matchConfigValue("no"));
+		assertTrue(FetchRecurseSubmodulesMode.NO.matchConfigValue("NO"));
+		assertTrue(FetchRecurseSubmodulesMode.NO.matchConfigValue("false"));
+		assertTrue(FetchRecurseSubmodulesMode.NO.matchConfigValue("FALSE"));
+	}
+
+	@Test
+	public void fetchRecurseNoMatch() throws Exception {
+		assertFalse(FetchRecurseSubmodulesMode.YES.matchConfigValue("Y"));
+		assertFalse(FetchRecurseSubmodulesMode.NO.matchConfigValue("N"));
+		assertFalse(FetchRecurseSubmodulesMode.ON_DEMAND
+				.matchConfigValue("ONDEMAND"));
+		assertFalse(FetchRecurseSubmodulesMode.YES.matchConfigValue(""));
+		assertFalse(FetchRecurseSubmodulesMode.YES.matchConfigValue(null));
+	}
+
+	@Test
+	public void fetchRecurseToConfigValue() {
+		assertEquals("on-demand",
+				FetchRecurseSubmodulesMode.ON_DEMAND.toConfigValue());
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkMergeBaseTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkMergeBaseTest.java
index 2451c50..077645e 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkMergeBaseTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevWalkMergeBaseTest.java
@@ -171,4 +171,4 @@
 		assertNull(rw.next());
 	}
 
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/SkipRevFilterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/SkipRevFilterTest.java
index 353a487..cf02aa8 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/SkipRevFilterTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/SkipRevFilterTest.java
@@ -81,4 +81,4 @@
 	public void testSkipRevFilterNegative() throws Exception {
 		SkipRevFilter.create(-1);
 	}
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleAddTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleAddTest.java
index 5c46659..f42dd02 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleAddTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleAddTest.java
@@ -269,4 +269,4 @@
 					ConfigConstants.CONFIG_KEY_URL));
 		}
 	}
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleStatusTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleStatusTest.java
index 61df9d9..5832518 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleStatusTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleStatusTest.java
@@ -59,11 +59,11 @@
 import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
 import org.eclipse.jgit.dircache.DirCacheEntry;
 import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.StoredConfig;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
@@ -256,11 +256,16 @@
 	}
 
 	@Test
-	public void repositoryWithInitializedSubmodule() throws IOException,
-			GitAPIException {
-		final ObjectId id = ObjectId
-				.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
-		final String path = "sub";
+	public void repositoryWithInitializedSubmodule() throws Exception {
+		String path = "sub";
+		Repository subRepo = Git.init().setBare(false)
+				.setDirectory(new File(db.getWorkTree(), path)).call()
+				.getRepository();
+		assertNotNull(subRepo);
+
+		TestRepository<?> subTr = new TestRepository<>(subRepo);
+		ObjectId id = subTr.branch(Constants.HEAD).commit().create().copy();
+
 		DirCache cache = db.lockDirCache();
 		DirCacheEditor editor = cache.editor();
 		editor.add(new PathEdit(path) {
@@ -287,15 +292,6 @@
 				ConfigConstants.CONFIG_KEY_URL, url);
 		modulesConfig.save();
 
-		Repository subRepo = Git.init().setBare(false)
-				.setDirectory(new File(db.getWorkTree(), path)).call()
-				.getRepository();
-		assertNotNull(subRepo);
-
-		RefUpdate update = subRepo.updateRef(Constants.HEAD, true);
-		update.setNewObjectId(id);
-		update.forceUpdate();
-
 		SubmoduleStatusCommand command = new SubmoduleStatusCommand(db);
 		Map<String, SubmoduleStatus> statuses = command.call();
 		assertNotNull(statuses);
@@ -312,11 +308,16 @@
 	}
 
 	@Test
-	public void repositoryWithDifferentRevCheckedOutSubmodule()
-			throws IOException, GitAPIException {
-		final ObjectId id = ObjectId
-				.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
-		final String path = "sub";
+	public void repositoryWithDifferentRevCheckedOutSubmodule() throws Exception {
+		String path = "sub";
+		Repository subRepo = Git.init().setBare(false)
+				.setDirectory(new File(db.getWorkTree(), path)).call()
+				.getRepository();
+		assertNotNull(subRepo);
+
+		TestRepository<?> subTr = new TestRepository<>(subRepo);
+		ObjectId id = subTr.branch(Constants.HEAD).commit().create().copy();
+
 		DirCache cache = db.lockDirCache();
 		DirCacheEditor editor = cache.editor();
 		editor.add(new PathEdit(path) {
@@ -343,15 +344,7 @@
 				ConfigConstants.CONFIG_KEY_URL, url);
 		modulesConfig.save();
 
-		Repository subRepo = Git.init().setBare(false)
-				.setDirectory(new File(db.getWorkTree(), path)).call()
-				.getRepository();
-		assertNotNull(subRepo);
-
-		RefUpdate update = subRepo.updateRef(Constants.HEAD, true);
-		update.setNewObjectId(ObjectId
-				.fromString("aaaa0000aaaa0000aaaa0000aaaa0000aaaa0000"));
-		update.forceUpdate();
+		ObjectId newId = subTr.branch(Constants.HEAD).commit().create().copy();
 
 		SubmoduleStatusCommand command = new SubmoduleStatusCommand(db);
 		Map<String, SubmoduleStatus> statuses = command.call();
@@ -365,7 +358,7 @@
 		assertNotNull(status);
 		assertEquals(path, status.getPath());
 		assertEquals(id, status.getIndexId());
-		assertEquals(update.getNewObjectId(), status.getHeadId());
+		assertEquals(newId, status.getHeadId());
 		assertEquals(SubmoduleStatusType.REV_CHECKED_OUT, status.getType());
 	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FSTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FSTest.java
index 4061b56..2c8273d 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FSTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FSTest.java
@@ -172,9 +172,18 @@
 		FS fs = FS.DETECTED.newInstance();
 		assumeTrue(fs instanceof FS_POSIX);
 
-		String r = FS.readPipe(fs.userHome(),
-				new String[] { "bash", "--login", "-c", "foobar" },
+		FS.readPipe(fs.userHome(),
+				new String[] { "/bin/sh", "-c", "exit 1" },
 				Charset.defaultCharset().name());
-		System.out.println(r);
+	}
+
+	@Test(expected = CommandFailedException.class)
+	public void testReadPipeCommandStartFailure()
+			throws CommandFailedException {
+		FS fs = FS.DETECTED.newInstance();
+
+		FS.readPipe(fs.userHome(),
+				  new String[] { "this-command-does-not-exist" },
+				  Charset.defaultCharset().name());
 	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/IntListTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/IntListTest.java
index c6eca9d..d6ea8c6 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/IntListTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/IntListTest.java
@@ -44,6 +44,7 @@
 package org.eclipse.jgit.util;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
@@ -186,6 +187,16 @@
 	}
 
 	@Test
+	public void testContains() {
+		IntList i = new IntList();
+		i.add(1);
+		i.add(4);
+		assertTrue(i.contains(1));
+		assertTrue(i.contains(4));
+		assertFalse(i.contains(2));
+	}
+
+	@Test
 	public void testToString() {
 		final IntList i = new IntList();
 		i.add(1);
diff --git a/org.eclipse.jgit.ui/META-INF/MANIFEST.MF b/org.eclipse.jgit.ui/META-INF/MANIFEST.MF
index 8ab9024..2a6babd 100644
--- a/org.eclipse.jgit.ui/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.ui/META-INF/MANIFEST.MF
@@ -3,14 +3,14 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %plugin_name
 Bundle-SymbolicName: org.eclipse.jgit.ui
-Bundle-Version: 4.8.0.qualifier
+Bundle-Version: 4.9.0.qualifier
 Bundle-Vendor: %provider_name
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
-Export-Package: org.eclipse.jgit.awtui;version="4.8.0"
-Import-Package: org.eclipse.jgit.errors;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.lib;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.nls;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.revplot;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.revwalk;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.transport;version="[4.8.0,4.9.0)",
- org.eclipse.jgit.util;version="[4.8.0,4.9.0)"
+Export-Package: org.eclipse.jgit.awtui;version="4.9.0"
+Import-Package: org.eclipse.jgit.errors;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.lib;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.nls;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.revplot;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.revwalk;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.transport;version="[4.9.0,4.10.0)",
+ org.eclipse.jgit.util;version="[4.9.0,4.10.0)"
diff --git a/org.eclipse.jgit.ui/pom.xml b/org.eclipse.jgit.ui/pom.xml
index 10b9374..244bd6f 100644
--- a/org.eclipse.jgit.ui/pom.xml
+++ b/org.eclipse.jgit.ui/pom.xml
@@ -52,7 +52,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>4.8.0-SNAPSHOT</version>
+    <version>4.9.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.ui</artifactId>
diff --git a/org.eclipse.jgit/.settings/.api_filters b/org.eclipse.jgit/.settings/.api_filters
index d359bff..da7c122 100644
--- a/org.eclipse.jgit/.settings/.api_filters
+++ b/org.eclipse.jgit/.settings/.api_filters
@@ -1,5 +1,21 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <component id="org.eclipse.jgit" version="2">
+    <resource path="src/org/eclipse/jgit/lib/ConfigConstants.java" type="org.eclipse.jgit.lib.ConfigConstants">
+        <filter id="1141899266">
+            <message_arguments>
+                <message_argument value="4.7"/>
+                <message_argument value="4.8"/>
+                <message_argument value="CONFIG_KEY_AUTODETACH"/>
+            </message_arguments>
+        </filter>
+        <filter id="1141899266">
+            <message_arguments>
+                <message_argument value="4.7"/>
+                <message_argument value="4.8"/>
+                <message_argument value="CONFIG_KEY_LOGEXPIRY"/>
+            </message_arguments>
+        </filter>
+    </resource>
     <resource path="src/org/eclipse/jgit/merge/MergeStrategy.java" type="org.eclipse.jgit.merge.MergeStrategy">
         <filter comment="OSGi semantic versioning allows breaking implementors of an API in a minor version" id="336695337">
             <message_arguments>
@@ -8,4 +24,12 @@
             </message_arguments>
         </filter>
     </resource>
+    <resource path="src/org/eclipse/jgit/merge/ResolveMerger.java" type="org.eclipse.jgit.merge.ResolveMerger">
+        <filter comment="OSGi semantic versioning allows breaking implementors of an API in a minor version" id="338792546">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.merge.ResolveMerger"/>
+                <message_argument value="processEntry(CanonicalTreeParser, CanonicalTreeParser, CanonicalTreeParser, DirCacheBuildIterator, WorkingTreeIterator, boolean)"/>
+            </message_arguments>
+        </filter>
+    </resource>
 </component>
diff --git a/org.eclipse.jgit/META-INF/MANIFEST.MF b/org.eclipse.jgit/META-INF/MANIFEST.MF
index e4791dc..4719ceb 100644
--- a/org.eclipse.jgit/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit/META-INF/MANIFEST.MF
@@ -2,12 +2,12 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %plugin_name
 Bundle-SymbolicName: org.eclipse.jgit
-Bundle-Version: 4.8.0.qualifier
+Bundle-Version: 4.9.0.qualifier
 Bundle-Localization: plugin
 Bundle-Vendor: %provider_name
 Bundle-ActivationPolicy: lazy
-Export-Package: org.eclipse.jgit.annotations;version="4.8.0",
- org.eclipse.jgit.api;version="4.8.0";
+Export-Package: org.eclipse.jgit.annotations;version="4.9.0",
+ org.eclipse.jgit.api;version="4.9.0";
   uses:="org.eclipse.jgit.revwalk,
    org.eclipse.jgit.treewalk.filter,
    org.eclipse.jgit.diff,
@@ -21,51 +21,52 @@
    org.eclipse.jgit.submodule,
    org.eclipse.jgit.transport,
    org.eclipse.jgit.merge",
- org.eclipse.jgit.api.errors;version="4.8.0";uses:="org.eclipse.jgit.lib,org.eclipse.jgit.errors",
- org.eclipse.jgit.attributes;version="4.8.0",
- org.eclipse.jgit.blame;version="4.8.0";
+ org.eclipse.jgit.api.errors;version="4.9.0";uses:="org.eclipse.jgit.lib,org.eclipse.jgit.errors",
+ org.eclipse.jgit.attributes;version="4.9.0",
+ org.eclipse.jgit.blame;version="4.9.0";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.revwalk,
    org.eclipse.jgit.treewalk.filter,
    org.eclipse.jgit.diff",
- org.eclipse.jgit.diff;version="4.8.0";
+ org.eclipse.jgit.diff;version="4.9.0";
   uses:="org.eclipse.jgit.patch,
    org.eclipse.jgit.lib,
    org.eclipse.jgit.treewalk,
    org.eclipse.jgit.revwalk,
    org.eclipse.jgit.treewalk.filter,
    org.eclipse.jgit.util",
- org.eclipse.jgit.dircache;version="4.8.0";
+ org.eclipse.jgit.dircache;version="4.9.0";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.treewalk,
    org.eclipse.jgit.util,
    org.eclipse.jgit.events,
    org.eclipse.jgit.attributes",
- org.eclipse.jgit.errors;version="4.8.0";
+ org.eclipse.jgit.errors;version="4.9.0";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.internal.storage.pack,
    org.eclipse.jgit.transport,
    org.eclipse.jgit.dircache",
- org.eclipse.jgit.events;version="4.8.0";uses:="org.eclipse.jgit.lib",
- org.eclipse.jgit.fnmatch;version="4.8.0",
- org.eclipse.jgit.gitrepo;version="4.8.0";
+ org.eclipse.jgit.events;version="4.9.0";uses:="org.eclipse.jgit.lib",
+ org.eclipse.jgit.fnmatch;version="4.9.0",
+ org.eclipse.jgit.gitrepo;version="4.9.0";
   uses:="org.eclipse.jgit.api,
    org.eclipse.jgit.lib,
    org.eclipse.jgit.revwalk,
    org.xml.sax.helpers,
    org.xml.sax",
- org.eclipse.jgit.gitrepo.internal;version="4.8.0";x-internal:=true,
- org.eclipse.jgit.hooks;version="4.8.0";uses:="org.eclipse.jgit.lib",
- org.eclipse.jgit.ignore;version="4.8.0",
- org.eclipse.jgit.ignore.internal;version="4.8.0";x-friends:="org.eclipse.jgit.test",
- org.eclipse.jgit.internal;version="4.8.0";x-friends:="org.eclipse.jgit.test,org.eclipse.jgit.http.test",
- org.eclipse.jgit.internal.ketch;version="4.8.0";x-friends:="org.eclipse.jgit.junit,org.eclipse.jgit.test,org.eclipse.jgit.pgm",
- org.eclipse.jgit.internal.storage.dfs;version="4.8.0";
+ org.eclipse.jgit.gitrepo.internal;version="4.9.0";x-internal:=true,
+ org.eclipse.jgit.hooks;version="4.9.0";uses:="org.eclipse.jgit.lib",
+ org.eclipse.jgit.ignore;version="4.9.0",
+ org.eclipse.jgit.ignore.internal;version="4.9.0";x-friends:="org.eclipse.jgit.test",
+ org.eclipse.jgit.internal;version="4.9.0";x-friends:="org.eclipse.jgit.test,org.eclipse.jgit.http.test",
+ org.eclipse.jgit.internal.fsck;version="4.9.0";x-friends:="org.eclipse.jgit.test",
+ org.eclipse.jgit.internal.ketch;version="4.9.0";x-friends:="org.eclipse.jgit.junit,org.eclipse.jgit.test,org.eclipse.jgit.pgm",
+ org.eclipse.jgit.internal.storage.dfs;version="4.9.0";
   x-friends:="org.eclipse.jgit.test,
    org.eclipse.jgit.http.server,
    org.eclipse.jgit.http.test,
    org.eclipse.jgit.lfs.test",
- org.eclipse.jgit.internal.storage.file;version="4.8.0";
+ org.eclipse.jgit.internal.storage.file;version="4.9.0";
   x-friends:="org.eclipse.jgit.test,
    org.eclipse.jgit.junit,
    org.eclipse.jgit.junit.http,
@@ -73,9 +74,9 @@
    org.eclipse.jgit.lfs,
    org.eclipse.jgit.pgm,
    org.eclipse.jgit.pgm.test",
- org.eclipse.jgit.internal.storage.pack;version="4.8.0";x-friends:="org.eclipse.jgit.junit,org.eclipse.jgit.test,org.eclipse.jgit.pgm",
- org.eclipse.jgit.internal.storage.reftree;version="4.8.0";x-friends:="org.eclipse.jgit.junit,org.eclipse.jgit.test,org.eclipse.jgit.pgm",
- org.eclipse.jgit.lib;version="4.8.0";
+ org.eclipse.jgit.internal.storage.pack;version="4.9.0";x-friends:="org.eclipse.jgit.junit,org.eclipse.jgit.test,org.eclipse.jgit.pgm",
+ org.eclipse.jgit.internal.storage.reftree;version="4.9.0";x-friends:="org.eclipse.jgit.junit,org.eclipse.jgit.test,org.eclipse.jgit.pgm",
+ org.eclipse.jgit.lib;version="4.9.0";
   uses:="org.eclipse.jgit.revwalk,
    org.eclipse.jgit.treewalk.filter,
    org.eclipse.jgit.util,
@@ -85,32 +86,33 @@
    org.eclipse.jgit.treewalk,
    org.eclipse.jgit.transport,
    org.eclipse.jgit.submodule",
- org.eclipse.jgit.merge;version="4.8.0";
+ org.eclipse.jgit.lib.internal;version="4.9.0";x-internal:=true,
+ org.eclipse.jgit.merge;version="4.9.0";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.treewalk,
    org.eclipse.jgit.revwalk,
    org.eclipse.jgit.diff,
    org.eclipse.jgit.dircache,
    org.eclipse.jgit.api",
- org.eclipse.jgit.nls;version="4.8.0",
- org.eclipse.jgit.notes;version="4.8.0";
+ org.eclipse.jgit.nls;version="4.9.0",
+ org.eclipse.jgit.notes;version="4.9.0";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.treewalk,
    org.eclipse.jgit.revwalk,
    org.eclipse.jgit.merge",
- org.eclipse.jgit.patch;version="4.8.0";uses:="org.eclipse.jgit.lib,org.eclipse.jgit.diff",
- org.eclipse.jgit.revplot;version="4.8.0";uses:="org.eclipse.jgit.lib,org.eclipse.jgit.revwalk",
- org.eclipse.jgit.revwalk;version="4.8.0";
+ org.eclipse.jgit.patch;version="4.9.0";uses:="org.eclipse.jgit.lib,org.eclipse.jgit.diff",
+ org.eclipse.jgit.revplot;version="4.9.0";uses:="org.eclipse.jgit.lib,org.eclipse.jgit.revwalk",
+ org.eclipse.jgit.revwalk;version="4.9.0";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.treewalk,
    org.eclipse.jgit.treewalk.filter,
    org.eclipse.jgit.diff,
    org.eclipse.jgit.revwalk.filter",
- org.eclipse.jgit.revwalk.filter;version="4.8.0";uses:="org.eclipse.jgit.revwalk,org.eclipse.jgit.lib,org.eclipse.jgit.util",
- org.eclipse.jgit.storage.file;version="4.8.0";uses:="org.eclipse.jgit.lib,org.eclipse.jgit.util",
- org.eclipse.jgit.storage.pack;version="4.8.0";uses:="org.eclipse.jgit.lib",
- org.eclipse.jgit.submodule;version="4.8.0";uses:="org.eclipse.jgit.lib,org.eclipse.jgit.treewalk.filter,org.eclipse.jgit.treewalk",
- org.eclipse.jgit.transport;version="4.8.0";
+ org.eclipse.jgit.revwalk.filter;version="4.9.0";uses:="org.eclipse.jgit.revwalk,org.eclipse.jgit.lib,org.eclipse.jgit.util",
+ org.eclipse.jgit.storage.file;version="4.9.0";uses:="org.eclipse.jgit.lib,org.eclipse.jgit.util",
+ org.eclipse.jgit.storage.pack;version="4.9.0";uses:="org.eclipse.jgit.lib",
+ org.eclipse.jgit.submodule;version="4.9.0";uses:="org.eclipse.jgit.lib,org.eclipse.jgit.treewalk.filter,org.eclipse.jgit.treewalk",
+ org.eclipse.jgit.transport;version="4.9.0";
   uses:="org.eclipse.jgit.transport.resolver,
    org.eclipse.jgit.revwalk,
    org.eclipse.jgit.internal.storage.pack,
@@ -122,24 +124,24 @@
    org.eclipse.jgit.transport.http,
    org.eclipse.jgit.errors,
    org.eclipse.jgit.storage.pack",
- org.eclipse.jgit.transport.http;version="4.8.0";uses:="javax.net.ssl",
- org.eclipse.jgit.transport.resolver;version="4.8.0";uses:="org.eclipse.jgit.lib,org.eclipse.jgit.transport",
- org.eclipse.jgit.treewalk;version="4.8.0";
+ org.eclipse.jgit.transport.http;version="4.9.0";uses:="javax.net.ssl",
+ org.eclipse.jgit.transport.resolver;version="4.9.0";uses:="org.eclipse.jgit.lib,org.eclipse.jgit.transport",
+ org.eclipse.jgit.treewalk;version="4.9.0";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.revwalk,
    org.eclipse.jgit.attributes,
    org.eclipse.jgit.treewalk.filter,
    org.eclipse.jgit.util,
    org.eclipse.jgit.dircache",
- org.eclipse.jgit.treewalk.filter;version="4.8.0";uses:="org.eclipse.jgit.treewalk",
- org.eclipse.jgit.util;version="4.8.0";
+ org.eclipse.jgit.treewalk.filter;version="4.9.0";uses:="org.eclipse.jgit.treewalk",
+ org.eclipse.jgit.util;version="4.9.0";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.transport.http,
    org.eclipse.jgit.storage.file,
    org.ietf.jgss",
- org.eclipse.jgit.util.io;version="4.8.0",
- org.eclipse.jgit.util.sha1;version="4.8.0",
- org.eclipse.jgit.util.time;version="4.8.0"
+ org.eclipse.jgit.util.io;version="4.9.0",
+ org.eclipse.jgit.util.sha1;version="4.9.0",
+ org.eclipse.jgit.util.time;version="4.9.0"
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
 Import-Package: com.googlecode.javaewah;version="[1.1.6,2.0.0)",
  com.jcraft.jsch;version="[0.1.37,0.2.0)",
diff --git a/org.eclipse.jgit/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit/META-INF/SOURCE-MANIFEST.MF
index bee6802..151278b 100644
--- a/org.eclipse.jgit/META-INF/SOURCE-MANIFEST.MF
+++ b/org.eclipse.jgit/META-INF/SOURCE-MANIFEST.MF
@@ -3,5 +3,5 @@
 Bundle-Name: org.eclipse.jgit - Sources
 Bundle-SymbolicName: org.eclipse.jgit.source
 Bundle-Vendor: Eclipse.org - JGit
-Bundle-Version: 4.8.0.qualifier
-Eclipse-SourceBundle: org.eclipse.jgit;version="4.8.0.qualifier";roots="."
+Bundle-Version: 4.9.0.qualifier
+Eclipse-SourceBundle: org.eclipse.jgit;version="4.9.0.qualifier";roots="."
diff --git a/org.eclipse.jgit/pom.xml b/org.eclipse.jgit/pom.xml
index 849b567..8306eb5 100644
--- a/org.eclipse.jgit/pom.xml
+++ b/org.eclipse.jgit/pom.xml
@@ -53,7 +53,7 @@
   <parent>
     <groupId>org.eclipse.jgit</groupId>
     <artifactId>org.eclipse.jgit-parent</artifactId>
-    <version>4.8.0-SNAPSHOT</version>
+    <version>4.9.0-SNAPSHOT</version>
   </parent>
 
   <artifactId>org.eclipse.jgit</artifactId>
@@ -206,8 +206,8 @@
     <pluginManagement>
       <plugins>
         <plugin>
-          <groupId>org.codehaus.mojo</groupId>
-          <artifactId>findbugs-maven-plugin</artifactId>
+          <groupId>com.github.hazendaz.spotbugs</groupId>
+          <artifactId>spotbugs-maven-plugin</artifactId>
           <configuration>
             <excludeFilterFile>findBugs/FindBugsExcludeFilter.xml</excludeFilterFile>
           </configuration>
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 225cb53..fc29f48 100644
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
@@ -208,12 +208,14 @@
 createBranchFailedUnknownReason=Create branch failed for unknown reason
 createBranchUnexpectedResult=Create branch returned unexpected result {0}
 createNewFileFailed=Could not create new file {0}
+createRequiresZeroOldId=Create requires old ID to be zero
 credentialPassword=Password
 credentialUsername=Username
 daemonAlreadyRunning=Daemon already running
 daysAgo={0} days ago
 deleteBranchUnexpectedResult=Delete branch returned unexpected result {0}
 deleteFileFailed=Could not delete file {0}
+deleteRequiresZeroNewId=Delete requires new ID to be zero
 deleteTagUnexpectedResult=Delete tag returned unexpected result {0}
 deletingNotSupported=Deleting {0} not supported.
 destinationIsNotAWildcard=Destination is not a wildcard.
@@ -242,6 +244,7 @@
 encryptionOnlyPBE=Encryption error: only password-based encryption (PBE) algorithms are supported.
 endOfFileInEscape=End of file in escape
 entryNotFoundByPath=Entry not found by path: {0}
+enumValueNotSupported0=Invalid value: {0}
 enumValueNotSupported2=Invalid value: {0}.{1}={2}
 enumValueNotSupported3=Invalid value: {0}.{1}.{2}={3}
 enumValuesNotAvailable=Enumerated values of type {0} not available
@@ -299,6 +302,8 @@
 flagsAlreadyCreated={0} flags already created.
 funnyRefname=funny refname
 gcFailed=Garbage collection failed.
+gcLogExists=A previous GC run reported an error: ''{0}''.  Automatic gc will fail until ''{1}'' is removed.
+gcTooManyUnpruned=Too many loose, unpruneable objects after garbage collection.  Consider adjusting gc.auto or gc.pruneExpire.
 gitmodulesNotFound=.gitmodules not found in tree.
 headRequiredToStash=HEAD required to stash local changes
 hoursAgo={0} hours ago
@@ -404,8 +409,11 @@
 mergeRecursiveTooManyMergeBasesFor = "More than {0} merge bases for:\n a {1}\n b {2} found:\n  count {3}"
 messageAndTaggerNotAllowedInUnannotatedTags = Unannotated tags cannot have a message or tagger
 minutesAgo={0} minutes ago
+mismatchOffset=mismatch offset for object {0}
+mismatchCRC=mismatch CRC for object {0}
 missingAccesskey=Missing accesskey.
 missingConfigurationForKey=No value for key {0} found in configuration
+missingCRC=missing CRC for object {0}
 missingDeltaBase=delta base
 missingForwardImageInGITBinaryPatch=Missing forward-image in GIT binary patch
 missingObject=Missing {0} {1}
@@ -423,6 +431,7 @@
 needPackOut=need packOut
 needsAtLeastOneEntry=Needs at least one entry
 needsWorkdir=Needs workdir
+newIdMustNotBeNull=New ID must not be null
 newlineInQuotesNotAllowed=Newline in quotes not allowed
 noApplyInDelete=No apply in delete
 noClosingBracket=No closing {0} found for {1} at index {2}.
@@ -456,6 +465,7 @@
 objectNotFoundIn=Object {0} not found in {1}.
 obtainingCommitsForCherryPick=Obtaining commits that need to be cherry-picked
 offsetWrittenDeltaBaseForObjectNotFoundInAPack=Offset-written delta base for object not found in a pack
+oldIdMustNotBeNull=Expected old ID must not be null
 onlyAlreadyUpToDateAndFastForwardMergesAreAvailable=only already-up-to-date and fast forward merges are available
 onlyOneFetchSupported=Only one fetch supported
 onlyOneOperationCallPerConnectionIsSupported=Only one operation call per connection is supported.
@@ -660,6 +670,7 @@
 unknownHost=unknown host
 unknownIndexVersionOrCorruptIndex=Unknown index version (or corrupt index): {0}
 unknownObject=unknown object
+unknownObjectInIndex=unknown object {0} found in index but not in pack file
 unknownObjectType=Unknown object type {0}.
 unknownObjectType2=unknown
 unknownRepositoryFormat=Unknown repository format
@@ -682,6 +693,7 @@
 unsupportedPackIndexVersion=Unsupported pack index version {0}
 unsupportedPackVersion=Unsupported pack version {0}.
 unsupportedRepositoryDescription=Repository description not supported
+updateRequiresOldIdAndNewId=Update requires both old ID and new ID to be nonzero
 updatingHeadFailed=Updating HEAD failed
 updatingReferences=Updating references
 updatingRefFailed=Updating the ref {0} to {1} failed. ReturnCode from RefUpdate.update() was {2}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/ArchiveCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/ArchiveCommand.java
index 0d18eb3..7ea8e73 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/ArchiveCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/ArchiveCommand.java
@@ -403,6 +403,12 @@
 				if (!paths.isEmpty())
 					walk.setFilter(PathFilterGroup.createFromStrings(paths));
 
+				// Put base directory into archive
+				if (pfx.endsWith("/")) { //$NON-NLS-1$
+					fmt.putEntry(outa, tree, pfx.replaceAll("[/]+$", "/"), //$NON-NLS-1$ //$NON-NLS-2$
+							FileMode.TREE, null);
+				}
+
 				while (walk.next()) {
 					String name = pfx + walk.getPathString();
 					FileMode mode = walk.getFileMode(0);
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 2deff07..d450c64 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2011, 2013 Chris Aniszczyk <caniszczyk@gmail.com>
+ * Copyright (C) 2011, 2017 Chris Aniszczyk <caniszczyk@gmail.com>
  * and other copyright owners as documented in the project's IP log.
  *
  * This program and the accompanying materials are made available
@@ -76,6 +76,8 @@
 import org.eclipse.jgit.transport.RemoteConfig;
 import org.eclipse.jgit.transport.TagOpt;
 import org.eclipse.jgit.transport.URIish;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.FileUtils;
 
 /**
  * Clone a repository into a new working directory
@@ -109,6 +111,10 @@
 
 	private Callback callback;
 
+	private boolean directoryExistsInitially;
+
+	private boolean gitDirExistsInitially;
+
 	/**
 	 * Callback for status of clone operation.
 	 *
@@ -167,26 +173,55 @@
 	@Override
 	public Git call() throws GitAPIException, InvalidRemoteException,
 			org.eclipse.jgit.api.errors.TransportException {
-		Repository repository = null;
+		URIish u = null;
 		try {
-			URIish u = new URIish(uri);
-			repository = init(u);
-			FetchResult result = fetch(repository, u);
-			if (!noCheckout)
-				checkout(repository, result);
-			return new Git(repository, true);
+			u = new URIish(uri);
+			verifyDirectories(u);
+		} catch (URISyntaxException e) {
+			throw new InvalidRemoteException(
+					MessageFormat.format(JGitText.get().invalidURL, uri));
+		}
+		Repository repository = null;
+		FetchResult fetchResult = null;
+		Thread cleanupHook = new Thread(() -> cleanup());
+		Runtime.getRuntime().addShutdownHook(cleanupHook);
+		try {
+			repository = init();
+			fetchResult = fetch(repository, u);
 		} catch (IOException ioe) {
 			if (repository != null) {
 				repository.close();
 			}
+			cleanup();
 			throw new JGitInternalException(ioe.getMessage(), ioe);
 		} catch (URISyntaxException e) {
 			if (repository != null) {
 				repository.close();
 			}
+			cleanup();
 			throw new InvalidRemoteException(MessageFormat.format(
 					JGitText.get().invalidRemote, remote));
+		} catch (GitAPIException | RuntimeException e) {
+			if (repository != null) {
+				repository.close();
+			}
+			cleanup();
+			throw e;
+		} finally {
+			Runtime.getRuntime().removeShutdownHook(cleanupHook);
 		}
+		if (!noCheckout) {
+			try {
+				checkout(repository, fetchResult);
+			} catch (IOException ioe) {
+				repository.close();
+				throw new JGitInternalException(ioe.getMessage(), ioe);
+			} catch (GitAPIException | RuntimeException e) {
+				repository.close();
+				throw e;
+			}
+		}
+		return new Git(repository, true);
 	}
 
 	private static boolean isNonEmptyDirectory(File dir) {
@@ -197,12 +232,12 @@
 		return false;
 	}
 
-	private Repository init(URIish u) throws GitAPIException {
-		InitCommand command = Git.init();
-		command.setBare(bare);
+	private void verifyDirectories(URIish u) {
 		if (directory == null && gitDir == null) {
 			directory = new File(u.getHumanishName(), Constants.DOT_GIT);
 		}
+		directoryExistsInitially = directory != null && directory.exists();
+		gitDirExistsInitially = gitDir != null && gitDir.exists();
 		validateDirs(directory, gitDir, bare);
 		if (isNonEmptyDirectory(directory)) {
 			throw new JGitInternalException(MessageFormat.format(
@@ -212,6 +247,11 @@
 			throw new JGitInternalException(MessageFormat.format(
 					JGitText.get().cloneNonEmptyDirectory, gitDir.getName()));
 		}
+	}
+
+	private Repository init() throws GitAPIException {
+		InitCommand command = Git.init();
+		command.setBare(bare);
 		if (directory != null) {
 			command.setDirectory(directory);
 		}
@@ -571,10 +611,12 @@
 	 *
 	 * @param callback
 	 *            the callback
+	 * @return {@code this}
 	 * @since 4.8
 	 */
-	public void setCallback(Callback callback) {
+	public CloneCommand setCallback(Callback callback) {
 		this.callback = callback;
+		return this;
 	}
 
 	private static void validateDirs(File directory, File gitDir, boolean bare)
@@ -602,4 +644,38 @@
 			}
 		}
 	}
+
+	private void cleanup() {
+		try {
+			if (directory != null) {
+				if (!directoryExistsInitially) {
+					FileUtils.delete(directory, FileUtils.RECURSIVE
+							| FileUtils.SKIP_MISSING | FileUtils.IGNORE_ERRORS);
+				} else {
+					deleteChildren(directory);
+				}
+			}
+			if (gitDir != null) {
+				if (!gitDirExistsInitially) {
+					FileUtils.delete(gitDir, FileUtils.RECURSIVE
+							| FileUtils.SKIP_MISSING | FileUtils.IGNORE_ERRORS);
+				} else {
+					deleteChildren(directory);
+				}
+			}
+		} catch (IOException e) {
+			// Ignore; this is a best-effort cleanup in error cases, and
+			// IOException should not be raised anyway
+		}
+	}
+
+	private void deleteChildren(File file) throws IOException {
+		if (!FS.DETECTED.isDirectory(file)) {
+			return;
+		}
+		for (File child : file.listFiles()) {
+			FileUtils.delete(child, FileUtils.RECURSIVE | FileUtils.SKIP_MISSING
+					| FileUtils.IGNORE_ERRORS);
+		}
+	}
 }
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 274ece6..e29fc05 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java
@@ -482,7 +482,7 @@
 						JGitText.get().entryNotFoundByPath, only.get(i)));
 
 		// there must be at least one change
-		if (emptyCommit)
+		if (emptyCommit && !allowEmpty.booleanValue())
 			// 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);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/DescribeCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/DescribeCommand.java
index 389c511..d365171 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/DescribeCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/DescribeCommand.java
@@ -47,17 +47,22 @@
 import java.io.IOException;
 import java.text.MessageFormat;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
 
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.api.errors.JGitInternalException;
 import org.eclipse.jgit.api.errors.RefNotFoundException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.InvalidPatternException;
 import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.ignore.internal.IMatcher;
+import org.eclipse.jgit.ignore.internal.PathMatcher;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
@@ -94,6 +99,11 @@
 	private boolean longDesc;
 
 	/**
+	 * Pattern matchers to be applied to tags under consideration
+	 */
+	private List<IMatcher> matchers = new ArrayList<>();
+
+	/**
 	 *
 	 * @param repo
 	 */
@@ -170,6 +180,54 @@
 	}
 
 	/**
+	 * Sets one or more {@code glob(7)} patterns that tags must match to be considered.
+	 * If multiple patterns are provided, tags only need match one of them.
+	 *
+	 * @param patterns the {@code glob(7)} pattern or patterns
+	 * @return {@code this}
+	 * @throws InvalidPatternException if the pattern passed in was invalid.
+	 *
+	 * @see <a
+	 *      href="https://www.kernel.org/pub/software/scm/git/docs/git-describe.html"
+	 *      >Git documentation about describe</a>
+	 * @since 4.9
+	 */
+	public DescribeCommand setMatch(String... patterns) throws InvalidPatternException {
+		for (String p : patterns) {
+			matchers.add(PathMatcher.createPathMatcher(p, null, false));
+		}
+		return this;
+	}
+
+	private Optional<Ref> getBestMatch(List<Ref> tags) {
+		if (tags == null || tags.size() == 0) {
+			return Optional.empty();
+		} else if (matchers.size() == 0) {
+			// No matchers, simply return the first tag entry
+			return Optional.of(tags.get(0));
+		} else {
+			// Find the first tag that matches one of the matchers; precedence according to matcher definition order
+			for (IMatcher matcher : matchers) {
+				Optional<Ref> match = tags.stream()
+						.filter(tag -> matcher.matches(tag.getName(), false))
+						.findFirst();
+				if (match.isPresent()) {
+					return match;
+				}
+			}
+			return Optional.empty();
+		}
+	}
+
+	private ObjectId getObjectIdFromRef(Ref r) {
+		ObjectId key = repo.peel(r).getPeeledObjectId();
+		if (key == null) {
+			key = r.getObjectId();
+		}
+		return key;
+	}
+
+	/**
 	 * Describes the specified commit. Target defaults to HEAD if no commit was
 	 * set explicitly.
 	 *
@@ -189,14 +247,9 @@
 			if (target == null)
 				setTarget(Constants.HEAD);
 
-			Map<ObjectId, Ref> tags = new HashMap<>();
-
-			for (Ref r : repo.getRefDatabase().getRefs(R_TAGS).values()) {
-				ObjectId key = repo.peel(r).getPeeledObjectId();
-				if (key == null)
-					key = r.getObjectId();
-				tags.put(key, r);
-			}
+			Collection<Ref> tagList = repo.getRefDatabase().getRefs(R_TAGS).values();
+			Map<ObjectId, List<Ref>> tags = tagList.stream()
+					.collect(Collectors.groupingBy(this::getObjectIdFromRef));
 
 			// combined flags of all the candidate instances
 			final RevFlagSet allFlags = new RevFlagSet();
@@ -242,11 +295,11 @@
 			}
 			List<Candidate> candidates = new ArrayList<>();    // all the candidates we find
 
-			// is the target already pointing to a tag? if so, we are done!
-			Ref lucky = tags.get(target);
-			if (lucky != null) {
-				return longDesc ? longDescription(lucky, 0, target) : lucky
-						.getName().substring(R_TAGS.length());
+			// is the target already pointing to a suitable tag? if so, we are done!
+			Optional<Ref> bestMatch = getBestMatch(tags.get(target));
+			if (bestMatch.isPresent()) {
+				return longDesc ? longDescription(bestMatch.get(), 0, target) :
+						bestMatch.get().getName().substring(R_TAGS.length());
 			}
 
 			w.markStart(target);
@@ -258,9 +311,9 @@
 					// if a tag already dominates this commit,
 					// then there's no point in picking a tag on this commit
 					// since the one that dominates it is always more preferable
-					Ref t = tags.get(c);
-					if (t != null) {
-						Candidate cd = new Candidate(c, t);
+					bestMatch = getBestMatch(tags.get(c));
+					if (bestMatch.isPresent()) {
+						Candidate cd = new Candidate(c, bestMatch.get());
 						candidates.add(cd);
 						cd.depth = seen;
 					}
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 cc3302b..4853159 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java
@@ -48,6 +48,7 @@
 import java.util.ArrayList;
 import java.util.List;
 
+import org.eclipse.jgit.annotations.Nullable;
 import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.api.errors.InvalidConfigurationException;
 import org.eclipse.jgit.api.errors.InvalidRemoteException;
@@ -99,6 +100,24 @@
 
 	private FetchRecurseSubmodulesMode submoduleRecurseMode = null;
 
+	private Callback callback;
+
+	/**
+	 * Callback for status of fetch operation.
+	 *
+	 * @since 4.8
+	 *
+	 */
+	public interface Callback {
+		/**
+		 * Notify fetching a submodule.
+		 *
+		 * @param name
+		 *            the submodule name.
+		 */
+		void fetchingSubmodule(String name);
+	}
+
 	/**
 	 * @param repo
 	 */
@@ -173,6 +192,9 @@
 							.setThin(thin).setRefSpecs(refSpecs)
 							.setDryRun(dryRun)
 							.setRecurseSubmodules(recurseMode);
+					if (callback != null) {
+						callback.fetchingSubmodule(walk.getPath());
+					}
 					results.addSubmodule(walk.getPath(), f.call());
 				}
 			}
@@ -237,11 +259,19 @@
 	 * Set the mode to be used for recursing into submodules.
 	 *
 	 * @param recurse
+	 *            corresponds to the
+	 *            --recurse-submodules/--no-recurse-submodules options. If
+	 *            {@code null} use the value of the
+	 *            {@code submodule.name.fetchRecurseSubmodules} option
+	 *            configured per submodule. If not specified there, use the
+	 *            value of the {@code fetch.recurseSubmodules} option configured
+	 *            in git config. If not configured in either, "on-demand" is the
+	 *            built-in default.
 	 * @return {@code this}
 	 * @since 4.7
 	 */
 	public FetchCommand setRecurseSubmodules(
-			FetchRecurseSubmodulesMode recurse) {
+			@Nullable FetchRecurseSubmodulesMode recurse) {
 		checkCallable();
 		submoduleRecurseMode = recurse;
 		return this;
@@ -434,4 +464,17 @@
 		this.tagOption = tagOpt;
 		return this;
 	}
+
+	/**
+	 * Register a progress callback.
+	 *
+	 * @param callback
+	 *            the callback
+	 * @return {@code this}
+	 * @since 4.8
+	 */
+	public FetchCommand setCallback(Callback callback) {
+		this.callback = callback;
+		return this;
+	}
 }
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 b5d9e8a..bae54ce 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/MergeCommand.java
@@ -53,6 +53,7 @@
 import java.util.Locale;
 import java.util.Map;
 
+import org.eclipse.jgit.annotations.Nullable;
 import org.eclipse.jgit.api.MergeResult.MergeStatus;
 import org.eclipse.jgit.api.errors.CheckoutConflictException;
 import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
@@ -554,12 +555,15 @@
 	 * Sets the fast forward mode.
 	 *
 	 * @param fastForwardMode
-	 *            corresponds to the --ff/--no-ff/--ff-only options. --ff is the
-	 *            default option.
+	 *            corresponds to the --ff/--no-ff/--ff-only options. If
+	 *            {@code null} use the value of the {@code merge.ff} option
+	 *            configured in git config. If this option is not configured
+	 *            --ff is the built-in default.
 	 * @return {@code this}
 	 * @since 2.2
 	 */
-	public MergeCommand setFastForward(FastForwardMode fastForwardMode) {
+	public MergeCommand setFastForward(
+			@Nullable FastForwardMode fastForwardMode) {
 		checkCallable();
 		this.fastForwardMode = fastForwardMode;
 		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 9c5ae43..aa97996 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java
@@ -47,6 +47,9 @@
 import java.io.IOException;
 import java.text.MessageFormat;
 
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.api.MergeCommand.FastForwardMode;
+import org.eclipse.jgit.api.MergeCommand.FastForwardMode.Merge;
 import org.eclipse.jgit.api.RebaseCommand.Operation;
 import org.eclipse.jgit.api.errors.CanceledException;
 import org.eclipse.jgit.api.errors.DetachedHeadException;
@@ -96,6 +99,8 @@
 
 	private TagOpt tagOption;
 
+	private FastForwardMode fastForwardMode;
+
 	private FetchRecurseSubmodulesMode submoduleRecurseMode = null;
 
 	/**
@@ -347,10 +352,9 @@
 			result = new PullResult(fetchRes, remote, rebaseRes);
 		} else {
 			MergeCommand merge = new MergeCommand(repo);
-			merge.include(upstreamName, commitToMerge);
-			merge.setStrategy(strategy);
-			merge.setProgressMonitor(monitor);
-			MergeResult mergeRes = merge.call();
+			MergeResult mergeRes = merge.include(upstreamName, commitToMerge)
+					.setStrategy(strategy).setProgressMonitor(monitor)
+					.setFastForward(getFastForwardMode()).call();
 			monitor.update(1);
 			result = new PullResult(fetchRes, remote, mergeRes);
 		}
@@ -433,14 +437,36 @@
 	}
 
 	/**
+	 * Sets the fast forward mode. It is used if pull is configured to do a
+	 * merge as opposed to rebase. If non-{@code null} takes precedence over the
+	 * fast-forward mode configured in git config.
+	 *
+	 * @param fastForwardMode
+	 *            corresponds to the --ff/--no-ff/--ff-only options. If
+	 *            {@code null} use the value of {@code pull.ff} configured in
+	 *            git config. If {@code pull.ff} is not configured fall back to
+	 *            the value of {@code merge.ff}. If {@code merge.ff} is not
+	 *            configured --ff is the built-in default.
+	 * @return {@code this}
+	 * @since 4.9
+	 */
+	public PullCommand setFastForward(
+			@Nullable FastForwardMode fastForwardMode) {
+		checkCallable();
+		this.fastForwardMode = fastForwardMode;
+		return this;
+	}
+
+	/**
 	 * Set the mode to be used for recursing into submodules.
 	 *
 	 * @param recurse
 	 * @return {@code this}
 	 * @since 4.7
+	 * @see FetchCommand#setRecurseSubmodules(FetchRecurseSubmodulesMode)
 	 */
 	public PullCommand setRecurseSubmodules(
-			FetchRecurseSubmodulesMode recurse) {
+			@Nullable FetchRecurseSubmodulesMode recurse) {
 		this.submoduleRecurseMode = recurse;
 		return this;
 	}
@@ -470,4 +496,15 @@
 		}
 		return mode;
 	}
+
+	private FastForwardMode getFastForwardMode() {
+		if (fastForwardMode != null) {
+			return fastForwardMode;
+		}
+		Config config = repo.getConfig();
+		Merge ffMode = config.getEnum(Merge.values(),
+				ConfigConstants.CONFIG_PULL_SECTION, null,
+				ConfigConstants.CONFIG_KEY_FF, null);
+		return ffMode != null ? FastForwardMode.valueOf(ffMode) : null;
+	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/ReflogCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/ReflogCommand.java
index 04caa0f..394bea5 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/ReflogCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/ReflogCommand.java
@@ -109,4 +109,4 @@
 		}
 	}
 
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleSyncCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleSyncCommand.java
index f97dce9..b5c0b15 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleSyncCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleSyncCommand.java
@@ -162,4 +162,4 @@
 			throw new JGitInternalException(e.getMessage(), e);
 		}
 	}
-}
\ No newline at end of file
+}
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 34bfbe7..4d3dff0 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleUpdateCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleUpdateCommand.java
@@ -240,9 +240,11 @@
 	 *
 	 * @param callback
 	 *            the callback
+	 * @return {@code this}
 	 * @since 4.8
 	 */
-	public void setCallback(CloneCommand.Callback callback) {
+	public SubmoduleUpdateCommand setCallback(CloneCommand.Callback callback) {
 		this.callback = callback;
+		return this;
 	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/TooLargeObjectInPackException.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/TooLargeObjectInPackException.java
index 08c25c2..b841f57 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/TooLargeObjectInPackException.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/TooLargeObjectInPackException.java
@@ -38,7 +38,8 @@
 package org.eclipse.jgit.api.errors;
 
 /**
- * Exception thrown when the server rejected a too large pack
+ * Exception thrown when PackParser finds an object larger than a predefined
+ * limit
  *
  * @since 4.4
  */
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/Attribute.java b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/Attribute.java
index 905ad76..c256b73 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/Attribute.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/Attribute.java
@@ -193,4 +193,4 @@
 			return key + "=" + value; //$NON-NLS-1$
 		}
 	}
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/Attributes.java b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/Attributes.java
index 0810e31..d3826b3 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/Attributes.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/Attributes.java
@@ -1,5 +1,6 @@
 /*
- * Copyright (C) 2015, Ivan Motsch <ivan.motsch@bsiag.com>
+ * Copyright (C) 2015, Ivan Motsch <ivan.motsch@bsiag.com>,
+ * Copyright (C) 2017, Obeo (mathieu.cartaud@obeo.fr)
  *
  * This program and the accompanying materials are made available
  * under the terms of the Eclipse Distribution License v1.0 which
@@ -48,6 +49,7 @@
 import java.util.Map;
 
 import org.eclipse.jgit.attributes.Attribute.State;
+import org.eclipse.jgit.lib.Constants;
 
 /**
  * Represents a set of attributes for a path
@@ -170,6 +172,26 @@
 		return a != null ? a.getValue() : null;
 	}
 
+	/**
+	 * Test if the given attributes implies to handle the related entry as a
+	 * binary file (i.e. if the entry has an -merge or a merge=binary attribute)
+	 * or if it can be content merged.
+	 *
+	 * @return <code>true</code> if the entry can be content merged,
+	 *         <code>false</code> otherwise
+	 * @since 4.9
+	 */
+	public boolean canBeContentMerged() {
+		if (isUnset(Constants.ATTR_MERGE)) {
+			return false;
+		} else if (isCustom(Constants.ATTR_MERGE)
+				&& getValue(Constants.ATTR_MERGE)
+						.equals(Constants.ATTR_BUILTIN_BINARY_MERGER)) {
+			return false;
+		}
+		return true;
+	}
+
 	@Override
 	public String toString() {
 		StringBuilder buf = new StringBuilder();
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesRule.java b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesRule.java
index c9c69db..b88a16e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesRule.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/attributes/AttributesRule.java
@@ -225,4 +225,4 @@
 		return sb.toString();
 
 	}
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffEntry.java
index e1dfcff..5eb1942 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffEntry.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffEntry.java
@@ -525,4 +525,4 @@
 		buf.append("]");
 		return buf.toString();
 	}
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/RenameDetector.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/RenameDetector.java
index bc52473..d899429 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/RenameDetector.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/RenameDetector.java
@@ -220,7 +220,9 @@
 	 * must be allocated, and 1,000,000 file compares may need to be performed.
 	 *
 	 * @param limit
-	 *            new file limit.
+	 *            new file limit. 0 means no limit; a negative number means no
+	 *            inexact rename detection will be performed, only exact rename
+	 *            detection.
 	 */
 	public void setRenameLimit(int limit) {
 		renameLimit = limit;
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 84f0da9..aed76ac 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheCheckout.java
@@ -1299,8 +1299,13 @@
 			return;
 		}
 
+		String name = f.getName();
+		if (name.length() > 200) {
+			name = name.substring(0, 200);
+		}
 		File tmpFile = File.createTempFile(
-				"._" + f.getName(), null, parentDir); //$NON-NLS-1$
+				"._" + name, null, parentDir); //$NON-NLS-1$
+
 		EolStreamType nonNullEolStreamType;
 		if (checkoutMetadata.eolStreamType != null) {
 			nonNullEolStreamType = checkoutMetadata.eolStreamType;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/CorruptPackIndexException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/CorruptPackIndexException.java
new file mode 100644
index 0000000..65d83b3
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/CorruptPackIndexException.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2017, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.errors;
+
+import org.eclipse.jgit.annotations.Nullable;
+
+/**
+ * Exception thrown when encounters a corrupt pack index file.
+ *
+ * @since 4.9
+ */
+public class CorruptPackIndexException extends Exception {
+	private static final long serialVersionUID = 1L;
+
+	/** The error type of a corrupt index file. */
+	public enum ErrorType {
+		/** Offset does not match index in pack file. */
+		MISMATCH_OFFSET,
+		/** CRC does not match CRC of the object data in pack file. */
+		MISMATCH_CRC,
+		/** CRC is not present in index file. */
+		MISSING_CRC,
+		/** Object in pack is not present in index file. */
+		MISSING_OBJ,
+		/** Object in index file is not present in pack file. */
+		UNKNOWN_OBJ,
+	}
+
+	private ErrorType errorType;
+
+	/**
+	 * Report a specific error condition discovered in an index file.
+	 *
+	 * @param message
+	 *            the error message.
+	 * @param errorType
+	 *            the error type of corruption.
+	 */
+	public CorruptPackIndexException(String message, ErrorType errorType) {
+		super(message);
+		this.errorType = errorType;
+	}
+
+	/**
+	 * Specific the reason of the corrupt index file.
+	 *
+	 * @return error condition or null.
+	 */
+	@Nullable
+	public ErrorType getErrorType() {
+		return errorType;
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/TooLargeObjectInPackException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/TooLargeObjectInPackException.java
index b5b1af5..ece76ed 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/errors/TooLargeObjectInPackException.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/TooLargeObjectInPackException.java
@@ -92,4 +92,4 @@
 	public TooLargeObjectInPackException(URIish uri, String s) {
 		super(uri.setPass(null) + ": " + s); //$NON-NLS-1$
 	}
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/TranslationBundleLoadingException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/TranslationBundleLoadingException.java
index 4f297b9..6cb332d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/errors/TranslationBundleLoadingException.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/TranslationBundleLoadingException.java
@@ -69,4 +69,4 @@
 				+ bundleClass.getName() + ", " + locale.toString() + "]", //$NON-NLS-1$ //$NON-NLS-2$
 				bundleClass, locale, cause);
 	}
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java
index 6669c9c..1de8a0b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java
@@ -731,7 +731,9 @@
 	 * Returns the child if either base or child is not a bare path. This provides a missing feature in
 	 * java.net.URI (see http://bugs.java.com/view_bug.do?bug_id=6226081).
 	 */
+	private static final String SLASH = "/"; //$NON-NLS-1$
 	static URI relativize(URI current, URI target) {
+
 		// We only handle bare paths for now.
 		if (!target.toString().equals(target.getPath())) {
 			return target;
@@ -744,37 +746,46 @@
 		String dest = target.normalize().getPath();
 
 		// TODO(hanwen): maybe (absolute, relative) should throw an exception.
-		if (cur.startsWith("/") != dest.startsWith("/")) { //$NON-NLS-1$//$NON-NLS-2$
+		if (cur.startsWith(SLASH) != dest.startsWith(SLASH)) {
 			return target;
 		}
 
-		while (cur.startsWith("/")) { //$NON-NLS-1$
+		while (cur.startsWith(SLASH)) {
 			cur = cur.substring(1);
 		}
-		while (dest.startsWith("/")) { //$NON-NLS-1$
+		while (dest.startsWith(SLASH)) {
 			dest = dest.substring(1);
 		}
 
-		if (!cur.endsWith("/")) { //$NON-NLS-1$
+		if (cur.indexOf('/') == -1 || dest.indexOf('/') == -1) {
+			// Avoid having to special-casing in the next two ifs.
+			String prefix = "prefix/"; //$NON-NLS-1$
+			cur = prefix + cur;
+			dest = prefix + dest;
+		}
+
+		if (!cur.endsWith(SLASH)) {
 			// The current file doesn't matter.
-			cur = cur.substring(0, cur.lastIndexOf('/'));
+			int lastSlash = cur.lastIndexOf('/');
+			cur = cur.substring(0, lastSlash);
 		}
 		String destFile = ""; //$NON-NLS-1$
-		if (!dest.endsWith("/")) { //$NON-NLS-1$
+		if (!dest.endsWith(SLASH)) {
 			// We always have to provide the destination file.
-			destFile = dest.substring(dest.lastIndexOf('/') + 1, dest.length());
+			int lastSlash = dest.lastIndexOf('/');
+			destFile = dest.substring(lastSlash + 1, dest.length());
 			dest = dest.substring(0, dest.lastIndexOf('/'));
 		}
 
-		String[] cs = cur.split("/"); //$NON-NLS-1$
-		String[] ds = dest.split("/"); //$NON-NLS-1$
+		String[] cs = cur.split(SLASH);
+		String[] ds = dest.split(SLASH);
 
 		int common = 0;
 		while (common < cs.length && common < ds.length && cs[common].equals(ds[common])) {
 			common++;
 		}
 
-		StringJoiner j = new StringJoiner("/"); //$NON-NLS-1$
+		StringJoiner j = new StringJoiner(SLASH);
 		for (int i = common; i < cs.length; i++) {
 			j.add(".."); //$NON-NLS-1$
 		}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/GitHook.java b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/GitHook.java
index c1aca6a..b684dd6 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/hooks/GitHook.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/hooks/GitHook.java
@@ -42,9 +42,12 @@
  */
 package org.eclipse.jgit.hooks;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.PrintStream;
+import java.io.UnsupportedEncodingException;
 import java.util.concurrent.Callable;
 
 import org.eclipse.jgit.api.errors.AbortedByHookException;
@@ -147,14 +150,21 @@
 	 */
 	protected void doRun() throws AbortedByHookException {
 		final ByteArrayOutputStream errorByteArray = new ByteArrayOutputStream();
-		final PrintStream hookErrRedirect = new PrintStream(errorByteArray);
+		PrintStream hookErrRedirect = null;
+		try {
+			hookErrRedirect = new PrintStream(errorByteArray, false,
+					UTF_8.name());
+		} catch (UnsupportedEncodingException e) {
+			// UTF-8 is guaranteed to be available
+		}
 		ProcessResult result = FS.DETECTED.runHookIfPresent(getRepository(),
 				getHookName(), getParameters(), getOutputStream(),
 				hookErrRedirect, getStdinArgs());
 		if (result.isExecutedWithError()) {
-			throw new AbortedByHookException(errorByteArray.toString(),
+			throw new AbortedByHookException(
+					new String(errorByteArray.toByteArray(), UTF_8),
 					getHookName(), result.getExitCode());
 		}
 	}
 
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/PathMatcher.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/PathMatcher.java
index 65224ea..ce9ad80 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/PathMatcher.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/PathMatcher.java
@@ -227,29 +227,30 @@
 			int left = right;
 			right = path.indexOf(slash, right);
 			if (right == -1) {
-				if (left < endExcl)
+				if (left < endExcl) {
 					match = matches(matcher, path, left, endExcl,
 							assumeDirectory);
+				} else {
+					// a/** should not match a/ or a
+					match = match && matchers.get(matcher) != WILD;
+				}
 				if (match) {
-					if (matcher == matchers.size() - 2
-							&& matchers.get(matcher + 1) == WILD)
-						// ** can match *nothing*: a/b/** match also a/b
-						return true;
 					if (matcher < matchers.size() - 1
 							&& matchers.get(matcher) == WILD) {
 						// ** can match *nothing*: a/**/b match also a/b
 						matcher++;
 						match = matches(matcher, path, left, endExcl,
 								assumeDirectory);
-					} else if (dirOnly && !assumeDirectory)
+					} else if (dirOnly && !assumeDirectory) {
 						// Directory expectations not met
 						return false;
+					}
 				}
 				return match && matcher + 1 == matchers.size();
 			}
-			if (right - left > 0)
+			if (right - left > 0) {
 				match = matches(matcher, path, left, right, assumeDirectory);
-			else {
+			} else {
 				// path starts with slash???
 				right++;
 				continue;
@@ -261,12 +262,14 @@
 					right = left - 1;
 				}
 				matcher++;
-				if (matcher == matchers.size())
+				if (matcher == matchers.size()) {
 					return true;
-			} else if (lastWildmatch != -1)
+				}
+			} else if (lastWildmatch != -1) {
 				matcher = lastWildmatch + 1;
-			else
+			} else {
 				return false;
+			}
 			right++;
 		}
 	}
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 b2c59a3..a68f839 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
@@ -267,12 +267,14 @@
 	/***/ public String createBranchFailedUnknownReason;
 	/***/ public String createBranchUnexpectedResult;
 	/***/ public String createNewFileFailed;
+	/***/ public String createRequiresZeroOldId;
 	/***/ public String credentialPassword;
 	/***/ public String credentialUsername;
 	/***/ public String daemonAlreadyRunning;
 	/***/ public String daysAgo;
 	/***/ public String deleteBranchUnexpectedResult;
 	/***/ public String deleteFileFailed;
+	/***/ public String deleteRequiresZeroNewId;
 	/***/ public String deleteTagUnexpectedResult;
 	/***/ public String deletingNotSupported;
 	/***/ public String destinationIsNotAWildcard;
@@ -301,6 +303,7 @@
 	/***/ public String encryptionOnlyPBE;
 	/***/ public String endOfFileInEscape;
 	/***/ public String entryNotFoundByPath;
+	/***/ public String enumValueNotSupported0;
 	/***/ public String enumValueNotSupported2;
 	/***/ public String enumValueNotSupported3;
 	/***/ public String enumValuesNotAvailable;
@@ -358,6 +361,8 @@
 	/***/ public String flagsAlreadyCreated;
 	/***/ public String funnyRefname;
 	/***/ public String gcFailed;
+	/***/ public String gcLogExists;
+	/***/ public String gcTooManyUnpruned;
 	/***/ public String gitmodulesNotFound;
 	/***/ public String headRequiredToStash;
 	/***/ public String hoursAgo;
@@ -463,8 +468,11 @@
 	/***/ public String mergeRecursiveTooManyMergeBasesFor;
 	/***/ public String messageAndTaggerNotAllowedInUnannotatedTags;
 	/***/ public String minutesAgo;
+	/***/ public String mismatchOffset;
+	/***/ public String mismatchCRC;
 	/***/ public String missingAccesskey;
 	/***/ public String missingConfigurationForKey;
+	/***/ public String missingCRC;
 	/***/ public String missingDeltaBase;
 	/***/ public String missingForwardImageInGITBinaryPatch;
 	/***/ public String missingObject;
@@ -482,6 +490,7 @@
 	/***/ public String needPackOut;
 	/***/ public String needsAtLeastOneEntry;
 	/***/ public String needsWorkdir;
+	/***/ public String newIdMustNotBeNull;
 	/***/ public String newlineInQuotesNotAllowed;
 	/***/ public String noApplyInDelete;
 	/***/ public String noClosingBracket;
@@ -515,6 +524,7 @@
 	/***/ public String objectNotFoundIn;
 	/***/ public String obtainingCommitsForCherryPick;
 	/***/ public String offsetWrittenDeltaBaseForObjectNotFoundInAPack;
+	/***/ public String oldIdMustNotBeNull;
 	/***/ public String onlyAlreadyUpToDateAndFastForwardMergesAreAvailable;
 	/***/ public String onlyOneFetchSupported;
 	/***/ public String onlyOneOperationCallPerConnectionIsSupported;
@@ -719,6 +729,7 @@
 	/***/ public String unknownHost;
 	/***/ public String unknownIndexVersionOrCorruptIndex;
 	/***/ public String unknownObject;
+	/***/ public String unknownObjectInIndex;
 	/***/ public String unknownObjectType;
 	/***/ public String unknownObjectType2;
 	/***/ public String unknownRepositoryFormat;
@@ -741,6 +752,7 @@
 	/***/ public String unsupportedPackIndexVersion;
 	/***/ public String unsupportedPackVersion;
 	/***/ public String unsupportedRepositoryDescription;
+	/***/ public String updateRequiresOldIdAndNewId;
 	/***/ public String updatingHeadFailed;
 	/***/ public String updatingReferences;
 	/***/ public String updatingRefFailed;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/fsck/FsckError.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/fsck/FsckError.java
new file mode 100644
index 0000000..588ed9b
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/fsck/FsckError.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2017, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.internal.fsck;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.errors.CorruptPackIndexException;
+import org.eclipse.jgit.errors.CorruptPackIndexException.ErrorType;
+import org.eclipse.jgit.lib.ObjectChecker;
+import org.eclipse.jgit.lib.ObjectId;
+
+/** Holds all fsck errors of a git repository. */
+public class FsckError {
+	/** Represents a corrupt object. */
+	public static class CorruptObject {
+		final ObjectId id;
+
+		final int type;
+
+		ObjectChecker.ErrorType errorType;
+
+		/**
+		 * @param id
+		 *            the object identifier.
+		 * @param type
+		 *            type of the object.
+		 */
+		public CorruptObject(ObjectId id, int type) {
+			this.id = id;
+			this.type = type;
+		}
+
+		void setErrorType(ObjectChecker.ErrorType errorType) {
+			this.errorType = errorType;
+		}
+
+		/** @return identifier of the object. */
+		public ObjectId getId() {
+			return id;
+		}
+
+		/** @return type of the object. */
+		public int getType() {
+			return type;
+		}
+
+		/** @return error type of the corruption. */
+		@Nullable
+		public ObjectChecker.ErrorType getErrorType() {
+			return errorType;
+		}
+	}
+
+	/** Represents a corrupt pack index file. */
+	public static class CorruptIndex {
+		String fileName;
+
+		CorruptPackIndexException.ErrorType errorType;
+
+		/**
+		 * @param fileName
+		 *            the file name of the pack index.
+		 * @param errorType
+		 *            the type of error as reported in
+		 *            {@link CorruptPackIndexException}.
+		 */
+		public CorruptIndex(String fileName, ErrorType errorType) {
+			this.fileName = fileName;
+			this.errorType = errorType;
+		}
+
+		/** @return the file name of the index file. */
+		public String getFileName() {
+			return fileName;
+		}
+
+		/** @return the error type of the corruption. */
+		public ErrorType getErrorType() {
+			return errorType;
+		}
+	}
+
+	private final Set<CorruptObject> corruptObjects = new HashSet<>();
+
+	private final Set<ObjectId> missingObjects = new HashSet<>();
+
+	private final Set<CorruptIndex> corruptIndices = new HashSet<>();
+
+	private final Set<String> nonCommitHeads = new HashSet<>();
+
+	/** @return corrupt objects from all pack files. */
+	public Set<CorruptObject> getCorruptObjects() {
+		return corruptObjects;
+	}
+
+	/** @return missing objects that should present in pack files. */
+	public Set<ObjectId> getMissingObjects() {
+		return missingObjects;
+	}
+
+	/** @return corrupt index files associated with the packs. */
+	public Set<CorruptIndex> getCorruptIndices() {
+		return corruptIndices;
+	}
+
+	/** @return refs/heads/* point to non-commit object. */
+	public Set<String> getNonCommitHeads() {
+		return nonCommitHeads;
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/fsck/FsckPackParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/fsck/FsckPackParser.java
new file mode 100644
index 0000000..e6ec681
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/fsck/FsckPackParser.java
@@ -0,0 +1,326 @@
+/*
+ * Copyright (C) 2017, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.fsck;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.Channels;
+import java.text.MessageFormat;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.zip.CRC32;
+
+import org.eclipse.jgit.errors.CorruptObjectException;
+import org.eclipse.jgit.errors.CorruptPackIndexException;
+import org.eclipse.jgit.errors.CorruptPackIndexException.ErrorType;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.fsck.FsckError.CorruptObject;
+import org.eclipse.jgit.internal.storage.dfs.ReadableChannel;
+import org.eclipse.jgit.internal.storage.file.PackIndex;
+import org.eclipse.jgit.internal.storage.file.PackIndex.MutableEntry;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.ObjectChecker;
+import org.eclipse.jgit.lib.ObjectDatabase;
+import org.eclipse.jgit.transport.PackParser;
+import org.eclipse.jgit.transport.PackedObjectInfo;
+
+/** A read-only pack parser for object validity checking. */
+public class FsckPackParser extends PackParser {
+	private final CRC32 crc;
+
+	private final ReadableChannel channel;
+
+	private final Set<CorruptObject> corruptObjects = new HashSet<>();
+
+	private long expectedObjectCount = -1L;
+
+	private long offset;
+
+	private int blockSize;
+
+	/**
+	 * @param db
+	 *            the object database which stores repository's data.
+	 * @param channel
+	 *            readable channel of the pack file.
+	 */
+	public FsckPackParser(ObjectDatabase db, ReadableChannel channel) {
+		super(db, Channels.newInputStream(channel));
+		this.channel = channel;
+		setCheckObjectCollisions(false);
+		this.crc = new CRC32();
+		this.blockSize = channel.blockSize() > 0 ? channel.blockSize() : 65536;
+	}
+
+	@Override
+	protected void onPackHeader(long objCnt) throws IOException {
+		if (expectedObjectCount >= 0) {
+			// Some DFS pack files don't contain the correct object count, e.g.
+			// INSERT/RECEIVE packs don't always contain the correct object
+			// count in their headers. Overwrite the expected object count
+			// after parsing the pack header.
+			setExpectedObjectCount(expectedObjectCount);
+		}
+	}
+
+	@Override
+	protected void onBeginWholeObject(long streamPosition, int type,
+			long inflatedSize) throws IOException {
+		crc.reset();
+	}
+
+	@Override
+	protected void onObjectHeader(Source src, byte[] raw, int pos, int len)
+			throws IOException {
+		crc.update(raw, pos, len);
+	}
+
+	@Override
+	protected void onObjectData(Source src, byte[] raw, int pos, int len)
+			throws IOException {
+		crc.update(raw, pos, len);
+	}
+
+	@Override
+	protected void onEndWholeObject(PackedObjectInfo info) throws IOException {
+		info.setCRC((int) crc.getValue());
+	}
+
+	@Override
+	protected void onBeginOfsDelta(long deltaStreamPosition,
+			long baseStreamPosition, long inflatedSize) throws IOException {
+		crc.reset();
+	}
+
+	@Override
+	protected void onBeginRefDelta(long deltaStreamPosition, AnyObjectId baseId,
+			long inflatedSize) throws IOException {
+		crc.reset();
+	}
+
+	@Override
+	protected UnresolvedDelta onEndDelta() throws IOException {
+		UnresolvedDelta delta = new UnresolvedDelta();
+		delta.setCRC((int) crc.getValue());
+		return delta;
+	}
+
+	@Override
+	protected void onInflatedObjectData(PackedObjectInfo obj, int typeCode,
+			byte[] data) throws IOException {
+		// FsckPackParser ignores this event.
+	}
+
+	@Override
+	protected void verifySafeObject(final AnyObjectId id, final int type,
+			final byte[] data) {
+		try {
+			super.verifySafeObject(id, type, data);
+		} catch (CorruptObjectException e) {
+			// catch the exception and continue parse the pack file
+			CorruptObject o = new CorruptObject(id.toObjectId(), type);
+			if (e.getErrorType() != null) {
+				o.setErrorType(e.getErrorType());
+			}
+			corruptObjects.add(o);
+		}
+	}
+
+	@Override
+	protected void onPackFooter(byte[] hash) throws IOException {
+	}
+
+	@Override
+	protected boolean onAppendBase(int typeCode, byte[] data,
+			PackedObjectInfo info) throws IOException {
+		// Do nothing.
+		return false;
+	}
+
+	@Override
+	protected void onEndThinPack() throws IOException {
+	}
+
+	@Override
+	protected ObjectTypeAndSize seekDatabase(PackedObjectInfo obj,
+			ObjectTypeAndSize info) throws IOException {
+		crc.reset();
+		offset = obj.getOffset();
+		return readObjectHeader(info);
+	}
+
+	@Override
+	protected ObjectTypeAndSize seekDatabase(UnresolvedDelta delta,
+			ObjectTypeAndSize info) throws IOException {
+		crc.reset();
+		offset = delta.getOffset();
+		return readObjectHeader(info);
+	}
+
+	@Override
+	protected int readDatabase(byte[] dst, int pos, int cnt)
+			throws IOException {
+		// read from input instead of database.
+		int n = read(offset, dst, pos, cnt);
+		if (n > 0) {
+			offset += n;
+		}
+		return n;
+	}
+
+	int read(long channelPosition, byte[] dst, int pos, int cnt)
+			throws IOException {
+		long block = channelPosition / blockSize;
+		byte[] bytes = readFromChannel(block);
+		if (bytes == null) {
+			return -1;
+		}
+		int offset = (int) (channelPosition - block * blockSize);
+		int bytesToCopy = Math.min(cnt, bytes.length - offset);
+		if (bytesToCopy < 1) {
+			return -1;
+		}
+		System.arraycopy(bytes, offset, dst, pos, bytesToCopy);
+		return bytesToCopy;
+	}
+
+	private byte[] readFromChannel(long block) throws IOException {
+		channel.position(block * blockSize);
+		ByteBuffer buf = ByteBuffer.allocate(blockSize);
+		int totalBytesRead = 0;
+		while (totalBytesRead < blockSize) {
+			int bytesRead = channel.read(buf);
+			if (bytesRead == -1) {
+				if (totalBytesRead == 0) {
+					return null;
+				}
+				return Arrays.copyOf(buf.array(), totalBytesRead);
+			}
+			totalBytesRead += bytesRead;
+		}
+		return buf.array();
+	}
+
+	@Override
+	protected boolean checkCRC(int oldCRC) {
+		return oldCRC == (int) crc.getValue();
+	}
+
+	@Override
+	protected void onStoreStream(byte[] raw, int pos, int len)
+			throws IOException {
+	}
+
+	/**
+	 * @return corrupt objects that reported by {@link ObjectChecker}.
+	 */
+	public Set<CorruptObject> getCorruptObjects() {
+		return corruptObjects;
+	}
+
+	/**
+	 * Verify the existing index file with all objects from the pack.
+	 *
+	 * @param entries
+	 *            all the entries that are expected in the index file
+	 * @param idx
+	 *            index file associate with the pack
+	 * @throws CorruptPackIndexException
+	 *             when the index file is corrupt.
+	 */
+	public void verifyIndex(List<PackedObjectInfo> entries, PackIndex idx)
+			throws CorruptPackIndexException {
+		Set<String> all = new HashSet<>();
+		for (PackedObjectInfo entry : entries) {
+			all.add(entry.getName());
+			long offset = idx.findOffset(entry);
+			if (offset == -1) {
+				throw new CorruptPackIndexException(
+						MessageFormat.format(JGitText.get().missingObject,
+								entry.getType(), entry.getName()),
+						ErrorType.MISSING_OBJ);
+			} else if (offset != entry.getOffset()) {
+				throw new CorruptPackIndexException(MessageFormat
+						.format(JGitText.get().mismatchOffset, entry.getName()),
+						ErrorType.MISMATCH_OFFSET);
+			}
+
+			try {
+				if (idx.hasCRC32Support()
+						&& (int) idx.findCRC32(entry) != entry.getCRC()) {
+					throw new CorruptPackIndexException(
+							MessageFormat.format(JGitText.get().mismatchCRC,
+									entry.getName()),
+							ErrorType.MISMATCH_CRC);
+				}
+			} catch (MissingObjectException e) {
+				throw new CorruptPackIndexException(MessageFormat
+						.format(JGitText.get().missingCRC, entry.getName()),
+						ErrorType.MISSING_CRC);
+			}
+		}
+
+		for (MutableEntry entry : idx) {
+			if (!all.contains(entry.name())) {
+				throw new CorruptPackIndexException(MessageFormat.format(
+						JGitText.get().unknownObjectInIndex, entry.name()),
+						ErrorType.UNKNOWN_OBJ);
+			}
+		}
+	}
+
+	/**
+	 * Set the object count for overwriting the expected object count from pack
+	 * header.
+	 *
+	 * @param expectedObjectCount
+	 *            the actual expected object count.
+	 */
+	public void overwriteObjectCount(long expectedObjectCount) {
+		this.expectedObjectCount = expectedObjectCount;
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/fsck/package-info.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/fsck/package-info.java
new file mode 100644
index 0000000..361b61f
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/fsck/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Git fsck support.
+ */
+package org.eclipse.jgit.internal.fsck;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/BlockBasedFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/BlockBasedFile.java
new file mode 100644
index 0000000..813e7f4
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/BlockBasedFile.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2017, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.storage.dfs;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.text.MessageFormat;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.errors.PackInvalidException;
+import org.eclipse.jgit.internal.storage.pack.PackExt;
+
+/** Block based file stored in {@link DfsBlockCache}. */
+public abstract class BlockBasedFile {
+	/** Cache that owns this file and its data. */
+	final DfsBlockCache cache;
+
+	/** Unique identity of this file while in-memory. */
+	final DfsStreamKey key;
+
+	/** Description of the associated pack file's storage. */
+	final DfsPackDescription desc;
+	final PackExt ext;
+
+	/**
+	 * Preferred alignment for loading blocks from the backing file.
+	 * <p>
+	 * It is initialized to 0 and filled in on the first read made from the
+	 * file. Block sizes may be odd, e.g. 4091, caused by the underling DFS
+	 * storing 4091 user bytes and 5 bytes block metadata into a lower level
+	 * 4096 byte block on disk.
+	 */
+	volatile int blockSize;
+
+	/**
+	 * Total number of bytes in this pack file.
+	 * <p>
+	 * This field initializes to -1 and gets populated when a block is loaded.
+	 */
+	volatile long length;
+
+	/** True once corruption has been detected that cannot be worked around. */
+	volatile boolean invalid;
+
+	BlockBasedFile(DfsBlockCache cache, DfsPackDescription desc, PackExt ext) {
+		this.cache = cache;
+		this.key = desc.getStreamKey(ext);
+		this.desc = desc;
+		this.ext = ext;
+	}
+
+	String getFileName() {
+		return desc.getFileName(ext);
+	}
+
+	boolean invalid() {
+		return invalid;
+	}
+
+	void setInvalid() {
+		invalid = true;
+	}
+
+	void setBlockSize(int newSize) {
+		blockSize = newSize;
+	}
+
+	long alignToBlock(long pos) {
+		int size = blockSize;
+		if (size == 0)
+			size = cache.getBlockSize();
+		return (pos / size) * size;
+	}
+
+	int blockSize(ReadableChannel rc) {
+		// If the block alignment is not yet known, discover it. Prefer the
+		// larger size from either the cache or the file itself.
+		int size = blockSize;
+		if (size == 0) {
+			size = rc.blockSize();
+			if (size <= 0)
+				size = cache.getBlockSize();
+			else if (size < cache.getBlockSize())
+				size = (cache.getBlockSize() / size) * size;
+			blockSize = size;
+		}
+		return size;
+	}
+
+	DfsBlock readOneBlock(long pos, DfsReader ctx,
+			@Nullable ReadableChannel fileChannel) throws IOException {
+		if (invalid)
+			throw new PackInvalidException(getFileName());
+
+		ctx.stats.readBlock++;
+		long start = System.nanoTime();
+		ReadableChannel rc = fileChannel != null ? fileChannel
+				: ctx.db.openFile(desc, ext);
+		try {
+			int size = blockSize(rc);
+			pos = (pos / size) * size;
+
+			// If the size of the file is not yet known, try to discover it.
+			// Channels may choose to return -1 to indicate they don't
+			// know the length yet, in this case read up to the size unit
+			// given by the caller, then recheck the length.
+			long len = length;
+			if (len < 0) {
+				len = rc.size();
+				if (0 <= len)
+					length = len;
+			}
+
+			if (0 <= len && len < pos + size)
+				size = (int) (len - pos);
+			if (size <= 0)
+				throw new EOFException(MessageFormat.format(
+						DfsText.get().shortReadOfBlock, Long.valueOf(pos),
+						getFileName(), Long.valueOf(0), Long.valueOf(0)));
+
+			byte[] buf = new byte[size];
+			rc.position(pos);
+			int cnt = read(rc, ByteBuffer.wrap(buf, 0, size));
+			ctx.stats.readBlockBytes += cnt;
+			if (cnt != size) {
+				if (0 <= len) {
+					throw new EOFException(MessageFormat.format(
+							DfsText.get().shortReadOfBlock, Long.valueOf(pos),
+							getFileName(), Integer.valueOf(size),
+							Integer.valueOf(cnt)));
+				}
+
+				// Assume the entire thing was read in a single shot, compact
+				// the buffer to only the space required.
+				byte[] n = new byte[cnt];
+				System.arraycopy(buf, 0, n, 0, n.length);
+				buf = n;
+			} else if (len < 0) {
+				// With no length at the start of the read, the channel should
+				// have the length available at the end.
+				length = len = rc.size();
+			}
+
+			return new DfsBlock(key, pos, buf);
+		} finally {
+			if (rc != fileChannel) {
+				rc.close();
+			}
+			ctx.stats.readBlockMicros += elapsedMicros(start);
+		}
+	}
+
+	static int read(ReadableChannel rc, ByteBuffer buf) throws IOException {
+		int n;
+		do {
+			n = rc.read(buf);
+		} while (0 < n && buf.hasRemaining());
+		return buf.position();
+	}
+
+	static long elapsedMicros(long start) {
+		return (System.nanoTime() - start) / 1000L;
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DeltaBaseCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DeltaBaseCache.java
index 64a63d7..bd4b4d2 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DeltaBaseCache.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DeltaBaseCache.java
@@ -75,7 +75,7 @@
 		table = new Entry[1 << TABLE_BITS];
 	}
 
-	Entry get(DfsPackKey key, long position) {
+	Entry get(DfsStreamKey key, long position) {
 		Entry e = table[hash(position)];
 		for (; e != null; e = e.tableNext) {
 			if (e.offset == position && key.equals(e.pack)) {
@@ -86,7 +86,7 @@
 		return null;
 	}
 
-	void put(DfsPackKey key, long offset, int objectType, byte[] data) {
+	void put(DfsStreamKey key, long offset, int objectType, byte[] data) {
 		if (data.length > maxByteCount)
 			return; // Too large to cache.
 
@@ -189,7 +189,7 @@
 	}
 
 	static class Entry {
-		final DfsPackKey pack;
+		final DfsStreamKey pack;
 		final long offset;
 		final int type;
 		final byte[] data;
@@ -198,7 +198,7 @@
 		Entry lruPrev;
 		Entry lruNext;
 
-		Entry(DfsPackKey key, long offset, int type, byte[] data) {
+		Entry(DfsStreamKey key, long offset, int type, byte[] data) {
 			this.pack = key;
 			this.offset = offset;
 			this.type = type;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlock.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlock.java
index 4a33fb8..dae922e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlock.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlock.java
@@ -52,9 +52,9 @@
 
 import org.eclipse.jgit.internal.storage.pack.PackOutputStream;
 
-/** A cached slice of a {@link DfsPackFile}. */
+/** A cached slice of a {@link BlockBasedFile}. */
 final class DfsBlock {
-	final DfsPackKey pack;
+	final DfsStreamKey stream;
 
 	final long start;
 
@@ -62,8 +62,8 @@
 
 	private final byte[] block;
 
-	DfsBlock(DfsPackKey p, long pos, byte[] buf) {
-		pack = p;
+	DfsBlock(DfsStreamKey p, long pos, byte[] buf) {
+		stream = p;
 		start = pos;
 		end = pos + buf.length;
 		block = buf;
@@ -73,8 +73,8 @@
 		return block.length;
 	}
 
-	boolean contains(DfsPackKey want, long pos) {
-		return pack == want && start <= pos && pos < end;
+	boolean contains(DfsStreamKey want, long pos) {
+		return stream.equals(want) && start <= pos && pos < end;
 	}
 
 	int copy(long pos, byte[] dstbuf, int dstoff, int cnt) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCache.java
index ef0b80c..45202b5 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCache.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCache.java
@@ -45,23 +45,20 @@
 package org.eclipse.jgit.internal.storage.dfs;
 
 import java.io.IOException;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.atomic.AtomicReferenceArray;
 import java.util.concurrent.locks.ReentrantLock;
 
+import org.eclipse.jgit.annotations.Nullable;
 import org.eclipse.jgit.internal.JGitText;
 
 /**
- * Caches slices of a {@link DfsPackFile} in memory for faster read access.
+ * Caches slices of a {@link BlockBasedFile} in memory for faster read access.
  * <p>
  * The DfsBlockCache serves as a Java based "buffer cache", loading segments of
- * a DfsPackFile into the JVM heap prior to use. As JGit often wants to do reads
- * of only tiny slices of a file, the DfsBlockCache tries to smooth out these
- * tiny reads into larger block-sized IO operations.
+ * a BlockBasedFile into the JVM heap prior to use. As JGit often wants to do
+ * reads of only tiny slices of a file, the DfsBlockCache tries to smooth out
+ * these tiny reads into larger block-sized IO operations.
  * <p>
  * Whenever a cache miss occurs, loading is invoked by exactly one thread for
  * the given <code>(DfsPackKey,position)</code> key tuple. This is ensured by an
@@ -108,14 +105,7 @@
 	 *             settings, usually too low of a limit.
 	 */
 	public static void reconfigure(DfsBlockCacheConfig cfg) {
-		DfsBlockCache nc = new DfsBlockCache(cfg);
-		DfsBlockCache oc = cache;
-		cache = nc;
-
-		if (oc != null) {
-			for (DfsPackFile pack : oc.getPackFiles())
-				pack.key.cachedSize.set(0);
-		}
+		cache = new DfsBlockCache(cfg);
 	}
 
 	/** @return the currently active DfsBlockCache. */
@@ -153,12 +143,6 @@
 	/** As {@link #blockSize} is a power of 2, bits to shift for a / blockSize. */
 	private final int blockSizeShift;
 
-	/** Cache of pack files, indexed by description. */
-	private final Map<DfsPackDescription, DfsPackFile> packCache;
-
-	/** View of pack files in the pack cache. */
-	private final Collection<DfsPackFile> packFiles;
-
 	/** Number of times a block was found in the cache. */
 	private final AtomicLong statHit;
 
@@ -194,13 +178,12 @@
 		blockSizeShift = Integer.numberOfTrailingZeros(blockSize);
 
 		clockLock = new ReentrantLock(true /* fair */);
-		clockHand = new Ref<>(new DfsPackKey(), -1, 0, null);
+		String none = ""; //$NON-NLS-1$
+		clockHand = new Ref<>(
+				DfsStreamKey.of(new DfsRepositoryDescription(none), none),
+				-1, 0, null);
 		clockHand.next = clockHand;
 
-		packCache = new ConcurrentHashMap<>(
-				16, 0.75f, 1);
-		packFiles = Collections.unmodifiableCollection(packCache.values());
-
 		statHit = new AtomicLong();
 		statMiss = new AtomicLong();
 	}
@@ -249,38 +232,6 @@
 		return statEvict;
 	}
 
-	/**
-	 * Get the pack files stored in this cache.
-	 *
-	 * @return a collection of pack files, some of which may not actually be
-	 *             present; the caller should check the pack's cached size.
-	 */
-	public Collection<DfsPackFile> getPackFiles() {
-		return packFiles;
-	}
-
-	DfsPackFile getOrCreate(DfsPackDescription dsc, DfsPackKey key) {
-		// TODO This table grows without bound. It needs to clean up
-		// entries that aren't in cache anymore, and aren't being used
-		// by a live DfsObjDatabase reference.
-
-		DfsPackFile pack = packCache.get(dsc);
-		if (pack != null && !pack.invalid()) {
-			return pack;
-		}
-
-		// 'pack' either didn't exist or was invalid. Compute a new
-		// entry atomically (guaranteed by ConcurrentHashMap).
-		return packCache.compute(dsc, (k, v) -> {
-			if (v != null && !v.invalid()) { // valid value added by
-				return v;                    // another thread
-			} else {
-				return new DfsPackFile(
-						this, dsc, key != null ? key : new DfsPackKey());
-			}
-		});
-	}
-
 	private int hash(int packHash, long off) {
 		return packHash + (int) (off >>> blockSizeShift);
 	}
@@ -302,26 +253,29 @@
 	/**
 	 * Lookup a cached object, creating and loading it if it doesn't exist.
 	 *
-	 * @param pack
+	 * @param file
 	 *            the pack that "contains" the cached object.
 	 * @param position
 	 *            offset within <code>pack</code> of the object.
 	 * @param ctx
 	 *            current thread's reader.
+	 * @param fileChannel
+	 *            optional channel to read {@code pack}.
 	 * @return the object reference.
 	 * @throws IOException
 	 *             the reference was not in the cache and could not be loaded.
 	 */
-	DfsBlock getOrLoad(DfsPackFile pack, long position, DfsReader ctx)
-			throws IOException {
+	DfsBlock getOrLoad(BlockBasedFile file, long position, DfsReader ctx,
+			@Nullable ReadableChannel fileChannel) throws IOException {
 		final long requestedPosition = position;
-		position = pack.alignToBlock(position);
+		position = file.alignToBlock(position);
 
-		DfsPackKey key = pack.key;
+		DfsStreamKey key = file.key;
 		int slot = slot(key, position);
 		HashEntry e1 = table.get(slot);
 		DfsBlock v = scan(e1, key, position);
-		if (v != null) {
+		if (v != null && v.contains(key, requestedPosition)) {
+			ctx.stats.blockCacheHit++;
 			statHit.incrementAndGet();
 			return v;
 		}
@@ -334,6 +288,7 @@
 			if (e2 != e1) {
 				v = scan(e2, key, position);
 				if (v != null) {
+					ctx.stats.blockCacheHit++;
 					statHit.incrementAndGet();
 					creditSpace(blockSize);
 					return v;
@@ -343,7 +298,7 @@
 			statMiss.incrementAndGet();
 			boolean credit = true;
 			try {
-				v = pack.readOneBlock(position, ctx);
+				v = file.readOneBlock(requestedPosition, ctx, fileChannel);
 				credit = false;
 			} finally {
 				if (credit)
@@ -356,7 +311,6 @@
 				e2 = table.get(slot);
 			}
 
-			key.cachedSize.addAndGet(v.size());
 			Ref<DfsBlock> ref = new Ref<>(key, position, v.size(), v);
 			ref.hot = true;
 			for (;;) {
@@ -372,9 +326,9 @@
 
 		// If the block size changed from the default, it is possible the block
 		// that was loaded is the wrong block for the requested position.
-		if (v.contains(pack.key, requestedPosition))
+		if (v.contains(file.key, requestedPosition))
 			return v;
-		return getOrLoad(pack, requestedPosition, ctx);
+		return getOrLoad(file, requestedPosition, ctx, fileChannel);
 	}
 
 	@SuppressWarnings("unchecked")
@@ -404,7 +358,6 @@
 					dead.next = null;
 					dead.value = null;
 					live -= dead.size;
-					dead.pack.cachedSize.addAndGet(-dead.size);
 					statEvict++;
 				} while (maxBytes < live);
 				clockHand = prev;
@@ -437,10 +390,14 @@
 	}
 
 	void put(DfsBlock v) {
-		put(v.pack, v.start, v.size(), v);
+		put(v.stream, v.start, v.size(), v);
 	}
 
-	<T> Ref<T> put(DfsPackKey key, long pos, int size, T v) {
+	<T> Ref<T> putRef(DfsStreamKey key, long size, T v) {
+		return put(key, 0, (int) Math.min(size, Integer.MAX_VALUE), v);
+	}
+
+	<T> Ref<T> put(DfsStreamKey key, long pos, int size, T v) {
 		int slot = slot(key, pos);
 		HashEntry e1 = table.get(slot);
 		Ref<T> ref = scanRef(e1, key, pos);
@@ -460,7 +417,6 @@
 				}
 			}
 
-			key.cachedSize.addAndGet(size);
 			ref = new Ref<>(key, pos, size, v);
 			ref.hot = true;
 			for (;;) {
@@ -476,12 +432,12 @@
 		return ref;
 	}
 
-	boolean contains(DfsPackKey key, long position) {
+	boolean contains(DfsStreamKey key, long position) {
 		return scan(table.get(slot(key, position)), key, position) != null;
 	}
 
 	@SuppressWarnings("unchecked")
-	<T> T get(DfsPackKey key, long position) {
+	<T> T get(DfsStreamKey key, long position) {
 		T val = (T) scan(table.get(slot(key, position)), key, position);
 		if (val == null)
 			statMiss.incrementAndGet();
@@ -490,31 +446,36 @@
 		return val;
 	}
 
-	private <T> T scan(HashEntry n, DfsPackKey pack, long position) {
-		Ref<T> r = scanRef(n, pack, position);
+	private <T> T scan(HashEntry n, DfsStreamKey key, long position) {
+		Ref<T> r = scanRef(n, key, position);
 		return r != null ? r.get() : null;
 	}
 
+	<T> Ref<T> getRef(DfsStreamKey key) {
+		Ref<T> r = scanRef(table.get(slot(key, 0)), key, 0);
+		if (r != null)
+			statHit.incrementAndGet();
+		else
+			statMiss.incrementAndGet();
+		return r;
+	}
+
 	@SuppressWarnings("unchecked")
-	private <T> Ref<T> scanRef(HashEntry n, DfsPackKey pack, long position) {
+	private <T> Ref<T> scanRef(HashEntry n, DfsStreamKey key, long position) {
 		for (; n != null; n = n.next) {
 			Ref<T> r = n.ref;
-			if (r.pack == pack && r.position == position)
+			if (r.position == position && r.key.equals(key))
 				return r.get() != null ? r : null;
 		}
 		return null;
 	}
 
-	void remove(DfsPackFile pack) {
-		packCache.remove(pack.getPackDescription());
+	private int slot(DfsStreamKey key, long position) {
+		return (hash(key.hash, position) >>> 1) % tableSize;
 	}
 
-	private int slot(DfsPackKey pack, long position) {
-		return (hash(pack.hash, position) >>> 1) % tableSize;
-	}
-
-	private ReentrantLock lockFor(DfsPackKey pack, long position) {
-		return loadLocks[(hash(pack.hash, position) >>> 1) % loadLocks.length];
+	private ReentrantLock lockFor(DfsStreamKey key, long position) {
+		return loadLocks[(hash(key.hash, position) >>> 1) % loadLocks.length];
 	}
 
 	private static HashEntry clean(HashEntry top) {
@@ -540,15 +501,15 @@
 	}
 
 	static final class Ref<T> {
-		final DfsPackKey pack;
+		final DfsStreamKey key;
 		final long position;
 		final int size;
 		volatile T value;
 		Ref next;
 		volatile boolean hot;
 
-		Ref(DfsPackKey pack, long position, int size, T v) {
-			this.pack = pack;
+		Ref(DfsStreamKey key, long position, int size, T v) {
+			this.key = key;
 			this.position = position;
 			this.size = size;
 			this.value = v;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsFsck.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsFsck.java
new file mode 100644
index 0000000..f90ba7d
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsFsck.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2017, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.storage.dfs;
+
+import java.io.IOException;
+import java.util.List;
+
+import org.eclipse.jgit.errors.CorruptPackIndexException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.internal.fsck.FsckError;
+import org.eclipse.jgit.internal.fsck.FsckError.CorruptIndex;
+import org.eclipse.jgit.internal.fsck.FsckPackParser;
+import org.eclipse.jgit.internal.storage.pack.PackExt;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectChecker;
+import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.revwalk.ObjectWalk;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.transport.PackedObjectInfo;
+
+/** Verify the validity and connectivity of a DFS repository. */
+public class DfsFsck {
+	private final DfsRepository repo;
+
+	private final DfsObjDatabase objdb;
+
+	private final DfsReader ctx;
+
+	private ObjectChecker objChecker = new ObjectChecker();
+
+	/**
+	 * Initialize DFS fsck.
+	 *
+	 * @param repository
+	 *            the dfs repository to check.
+	 */
+	public DfsFsck(DfsRepository repository) {
+		repo = repository;
+		objdb = repo.getObjectDatabase();
+		ctx = objdb.newReader();
+	}
+
+
+	/**
+	 * Verify the integrity and connectivity of all objects in the object
+	 * database.
+	 *
+	 * @param pm
+	 *            callback to provide progress feedback during the check.
+	 * @return all errors about the repository.
+	 * @throws IOException
+	 *             if encounters IO errors during the process.
+	 */
+	public FsckError check(ProgressMonitor pm) throws IOException {
+		FsckError errors = new FsckError();
+		try {
+			for (DfsPackFile pack : objdb.getPacks()) {
+				DfsPackDescription packDesc = pack.getPackDescription();
+				try (ReadableChannel channel = repo.getObjectDatabase()
+						.openFile(packDesc, PackExt.PACK)) {
+					List<PackedObjectInfo> objectsInPack;
+					FsckPackParser parser = new FsckPackParser(
+							repo.getObjectDatabase(), channel);
+					parser.setObjectChecker(objChecker);
+					parser.overwriteObjectCount(packDesc.getObjectCount());
+					parser.parse(pm);
+					errors.getCorruptObjects()
+							.addAll(parser.getCorruptObjects());
+					objectsInPack = parser.getSortedObjectList(null);
+					parser.verifyIndex(objectsInPack, pack.getPackIndex(ctx));
+				} catch (MissingObjectException e) {
+					errors.getMissingObjects().add(e.getObjectId());
+				} catch (CorruptPackIndexException e) {
+					errors.getCorruptIndices().add(new CorruptIndex(
+							pack.getPackDescription()
+									.getFileName(PackExt.INDEX),
+							e.getErrorType()));
+				}
+			}
+
+			try (ObjectWalk ow = new ObjectWalk(ctx)) {
+				for (Ref r : repo.getAllRefs().values()) {
+					try {
+						RevObject tip = ow.parseAny(r.getObjectId());
+						if (r.getLeaf().getName().startsWith(Constants.R_HEADS)) {
+							// check if heads point to a commit object
+							if (tip.getType() != Constants.OBJ_COMMIT) {
+								errors.getNonCommitHeads()
+										.add(r.getLeaf().getName());
+							}
+						}
+						ow.markStart(tip);
+						ow.checkConnectivity();
+						ow.markUninteresting(tip);
+					} catch (MissingObjectException e) {
+						errors.getMissingObjects().add(e.getObjectId());
+					}
+				}
+			}
+		} finally {
+			ctx.close();
+		}
+		return errors;
+	}
+
+	/**
+	 * Use a customized object checker instead of the default one. Caller can
+	 * specify a skip list to ignore some errors.
+	 *
+	 * @param objChecker
+	 *            A customized object checker.
+	 */
+	public void setObjectChecker(ObjectChecker objChecker) {
+		this.objChecker = objChecker;
+	}
+}
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 de447de..ce2b053 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
@@ -53,6 +53,7 @@
 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.internal.storage.pack.PackWriter.NONE;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -111,7 +112,8 @@
 	private List<DfsPackFile> packsBefore;
 	private List<DfsPackFile> expiredGarbagePacks;
 
-	private Set<ObjectId> allHeads;
+	private Set<ObjectId> allHeadsAndTags;
+	private Set<ObjectId> allTags;
 	private Set<ObjectId> nonHeads;
 	private Set<ObjectId> txnHeads;
 	private Set<ObjectId> tagTargets;
@@ -233,7 +235,7 @@
 					JGitText.get().supportOnlyPackIndexVersion2);
 
 		startTimeMillis = SystemReader.getInstance().getCurrentTime();
-		ctx = (DfsReader) objdb.newReader();
+		ctx = objdb.newReader();
 		try {
 			refdb.refresh();
 			objdb.clearCache();
@@ -241,23 +243,42 @@
 			Collection<Ref> refsBefore = getAllRefs();
 			readPacksBefore();
 
-			allHeads = new HashSet<>();
+			Set<ObjectId> allHeads = new HashSet<>();
+			allHeadsAndTags = new HashSet<>();
+			allTags = new HashSet<>();
 			nonHeads = new HashSet<>();
 			txnHeads = new HashSet<>();
 			tagTargets = new HashSet<>();
 			for (Ref ref : refsBefore) {
-				if (ref.isSymbolic() || ref.getObjectId() == null)
+				if (ref.isSymbolic() || ref.getObjectId() == null) {
 					continue;
-				if (isHead(ref) || isTag(ref))
+				}
+				if (isHead(ref)) {
 					allHeads.add(ref.getObjectId());
-				else if (RefTreeNames.isRefTree(refdb, ref.getName()))
+				} else if (isTag(ref)) {
+					allTags.add(ref.getObjectId());
+				} else if (RefTreeNames.isRefTree(refdb, ref.getName())) {
 					txnHeads.add(ref.getObjectId());
-				else
+				} else {
 					nonHeads.add(ref.getObjectId());
-				if (ref.getPeeledObjectId() != null)
+				}
+				if (ref.getPeeledObjectId() != null) {
 					tagTargets.add(ref.getPeeledObjectId());
+				}
 			}
-			tagTargets.addAll(allHeads);
+			// Don't exclude tags that are also branch tips.
+			allTags.removeAll(allHeads);
+			allHeadsAndTags.addAll(allHeads);
+			allHeadsAndTags.addAll(allTags);
+
+			// Hoist all branch tips and tags earlier in the pack file
+			tagTargets.addAll(allHeadsAndTags);
+
+			// Combine the GC_REST objects into the GC pack if requested
+			if (packConfig.getSinglePack()) {
+				allHeadsAndTags.addAll(nonHeads);
+				nonHeads.clear();
+			}
 
 			boolean rollback = true;
 			try {
@@ -413,12 +434,12 @@
 	}
 
 	private void packHeads(ProgressMonitor pm) throws IOException {
-		if (allHeads.isEmpty())
+		if (allHeadsAndTags.isEmpty())
 			return;
 
 		try (PackWriter pw = newPackWriter()) {
 			pw.setTagTargets(tagTargets);
-			pw.preparePack(pm, allHeads, PackWriter.NONE);
+			pw.preparePack(pm, allHeadsAndTags, NONE, NONE, allTags);
 			if (0 < pw.getObjectCount())
 				writePack(GC, pw, pm,
 						estimateGcPackSize(INSERT, RECEIVE, COMPACT, GC));
@@ -432,7 +453,7 @@
 		try (PackWriter pw = newPackWriter()) {
 			for (ObjectIdSet packedObjs : newPackObj)
 				pw.excludeObjects(packedObjs);
-			pw.preparePack(pm, nonHeads, allHeads);
+			pw.preparePack(pm, nonHeads, allHeadsAndTags);
 			if (0 < pw.getObjectCount())
 				writePack(GC_REST, pw, pm,
 						estimateGcPackSize(INSERT, RECEIVE, COMPACT, GC_REST));
@@ -446,7 +467,7 @@
 		try (PackWriter pw = newPackWriter()) {
 			for (ObjectIdSet packedObjs : newPackObj)
 				pw.excludeObjects(packedObjs);
-			pw.preparePack(pm, txnHeads, PackWriter.NONE);
+			pw.preparePack(pm, txnHeads, NONE);
 			if (0 < pw.getObjectCount())
 				writePack(GC_TXN, pw, pm, 0 /* unknown pack size */);
 		}
@@ -542,22 +563,25 @@
 		try (DfsOutputStream out = objdb.writeFile(pack, PACK)) {
 			pw.writePack(pm, pm, out);
 			pack.addFileExt(PACK);
+			pack.setBlockSize(PACK, out.blockSize());
 		}
 
-		try (CountingOutputStream cnt =
-				new CountingOutputStream(objdb.writeFile(pack, INDEX))) {
+		try (DfsOutputStream out = objdb.writeFile(pack, INDEX)) {
+			CountingOutputStream cnt = new CountingOutputStream(out);
 			pw.writeIndex(cnt);
 			pack.addFileExt(INDEX);
 			pack.setFileSize(INDEX, cnt.getCount());
+			pack.setBlockSize(INDEX, out.blockSize());
 			pack.setIndexVersion(pw.getIndexVersion());
 		}
 
 		if (pw.prepareBitmapIndex(pm)) {
-			try (CountingOutputStream cnt = new CountingOutputStream(
-					objdb.writeFile(pack, BITMAP_INDEX))) {
+			try (DfsOutputStream out = objdb.writeFile(pack, BITMAP_INDEX)) {
+				CountingOutputStream cnt = new CountingOutputStream(out);
 				pw.writeBitmapIndex(cnt);
 				pack.addFileExt(BITMAP_INDEX);
 				pack.setFileSize(BITMAP_INDEX, cnt.getCount());
+				pack.setBlockSize(BITMAP_INDEX, out.blockSize());
 			}
 		}
 
@@ -566,8 +590,6 @@
 		pack.setLastModified(startTimeMillis);
 		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/DfsInserter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsInserter.java
index fd72756..01654d4 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsInserter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsInserter.java
@@ -104,7 +104,7 @@
 	ObjectIdOwnerMap<PackedObjectInfo> objectMap;
 
 	DfsBlockCache cache;
-	DfsPackKey packKey;
+	DfsStreamKey packKey;
 	DfsPackDescription packDsc;
 	PackStream packOut;
 	private boolean rollback;
@@ -221,7 +221,7 @@
 		db.commitPack(Collections.singletonList(packDsc), null);
 		rollback = false;
 
-		DfsPackFile p = cache.getOrCreate(packDsc, packKey);
+		DfsPackFile p = new DfsPackFile(cache, packDsc);
 		if (index != null)
 			p.setPackIndex(index);
 		db.addPack(p);
@@ -281,8 +281,10 @@
 
 		rollback = true;
 		packDsc = db.newPack(DfsObjDatabase.PackSource.INSERT);
-		packOut = new PackStream(db.writeFile(packDsc, PACK));
-		packKey = new DfsPackKey();
+		DfsOutputStream dfsOut = db.writeFile(packDsc, PACK);
+		packDsc.setBlockSize(PACK, dfsOut.blockSize());
+		packOut = new PackStream(dfsOut);
+		packKey = packDsc.getStreamKey(PACK);
 
 		// Write the header as though it were a single object pack.
 		byte[] buf = packOut.hdrBuf;
@@ -312,13 +314,14 @@
 			packIndex = PackIndex.read(buf.openInputStream());
 		}
 
-		DfsOutputStream os = db.writeFile(pack, INDEX);
-		try (CountingOutputStream cnt = new CountingOutputStream(os)) {
+		try (DfsOutputStream os = db.writeFile(pack, INDEX)) {
+			CountingOutputStream cnt = new CountingOutputStream(os);
 			if (buf != null)
 				buf.writeTo(cnt, null);
 			else
 				index(cnt, packHash, list);
 			pack.addFileExt(INDEX);
+			pack.setBlockSize(INDEX, os.blockSize());
 			pack.setFileSize(INDEX, cnt.getCount());
 		} finally {
 			if (buf != null) {
@@ -530,7 +533,7 @@
 	}
 
 	private class Reader extends ObjectReader {
-		private final DfsReader ctx = new DfsReader(db);
+		private final DfsReader ctx = db.newReader();
 
 		@Override
 		public ObjectReader newReader() {
@@ -633,11 +636,11 @@
 		private final int type;
 		private final long size;
 
-		private final DfsPackKey srcPack;
+		private final DfsStreamKey srcPack;
 		private final long pos;
 
 		StreamLoader(ObjectId id, int type, long sz,
-				DfsPackKey key, long pos) {
+				DfsStreamKey key, long pos) {
 			this.id = id;
 			this.type = type;
 			this.size = sz;
@@ -647,7 +650,7 @@
 
 		@Override
 		public ObjectStream openStream() throws IOException {
-			final DfsReader ctx = new DfsReader(db);
+			final DfsReader ctx = db.newReader();
 			if (srcPack != packKey) {
 				try {
 					// Post DfsInserter.flush() use the normal code path.
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 b1cb72d..76189c1 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
@@ -170,7 +170,7 @@
 	}
 
 	@Override
-	public ObjectReader newReader() {
+	public DfsReader newReader() {
 		return new DfsReader(this);
 	}
 
@@ -464,8 +464,8 @@
 			DfsPackFile oldPack = forReuse.remove(dsc);
 			if (oldPack != null) {
 				list.add(oldPack);
-			} else {
-				list.add(cache.getOrCreate(dsc, null));
+			} else if (dsc.hasFileExt(PackExt.PACK)) {
+				list.add(new DfsPackFile(cache, dsc));
 				foundNew = true;
 			}
 		}
@@ -482,8 +482,7 @@
 	}
 
 	private static Map<DfsPackDescription, DfsPackFile> reuseMap(PackList old) {
-		Map<DfsPackDescription, DfsPackFile> forReuse
-			= new HashMap<>();
+		Map<DfsPackDescription, DfsPackFile> forReuse = new HashMap<>();
 		for (DfsPackFile p : old.packs) {
 			if (p.invalid()) {
 				// The pack instance is corrupted, and cannot be safely used
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 0a29ac5..ac14c0b 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
@@ -201,7 +201,7 @@
 			pm = NullProgressMonitor.INSTANCE;
 
 		DfsObjDatabase objdb = repo.getObjectDatabase();
-		try (DfsReader ctx = (DfsReader) objdb.newReader()) {
+		try (DfsReader ctx = objdb.newReader()) {
 			PackConfig pc = new PackConfig(repo);
 			pc.setIndexVersion(2);
 			pc.setDeltaCompress(false);
@@ -370,27 +370,23 @@
 	private static void writePack(DfsObjDatabase objdb,
 			DfsPackDescription pack,
 			PackWriter pw, ProgressMonitor pm) throws IOException {
-		DfsOutputStream out = objdb.writeFile(pack, PACK);
-		try {
+		try (DfsOutputStream out = objdb.writeFile(pack, PACK)) {
 			pw.writePack(pm, pm, out);
 			pack.addFileExt(PACK);
-		} finally {
-			out.close();
+			pack.setBlockSize(PACK, out.blockSize());
 		}
 	}
 
 	private static void writeIndex(DfsObjDatabase objdb,
 			DfsPackDescription pack,
 			PackWriter pw) throws IOException {
-		DfsOutputStream out = objdb.writeFile(pack, INDEX);
-		try {
+		try (DfsOutputStream out = objdb.writeFile(pack, INDEX)) {
 			CountingOutputStream cnt = new CountingOutputStream(out);
 			pw.writeIndex(cnt);
 			pack.addFileExt(INDEX);
 			pack.setFileSize(INDEX, cnt.getCount());
+			pack.setBlockSize(INDEX, out.blockSize());
 			pack.setIndexVersion(pw.getIndexVersion());
-		} finally {
-			out.close();
 		}
 	}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackDescription.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackDescription.java
index e825f1a..58a006e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackDescription.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackDescription.java
@@ -45,8 +45,7 @@
 
 import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK;
 
-import java.util.HashMap;
-import java.util.Map;
+import java.util.Arrays;
 
 import org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource;
 import org.eclipse.jgit.internal.storage.pack.PackExt;
@@ -62,25 +61,16 @@
  */
 public class DfsPackDescription implements Comparable<DfsPackDescription> {
 	private final DfsRepositoryDescription repoDesc;
-
 	private final String packName;
-
 	private PackSource packSource;
-
 	private long lastModified;
-
-	private final Map<PackExt, Long> sizeMap;
-
+	private long[] sizeMap;
+	private int[] blockSizeMap;
 	private long objectCount;
-
 	private long deltaCount;
-
 	private PackStatistics stats;
-
 	private int extensions;
-
 	private int indexVersion;
-
 	private long estimatedPackSize;
 
 	/**
@@ -102,7 +92,10 @@
 		this.repoDesc = repoDesc;
 		int dot = name.lastIndexOf('.');
 		this.packName = (dot < 0) ? name : name.substring(0, dot);
-		this.sizeMap = new HashMap<>(PackExt.values().length * 2);
+
+		int extCnt = PackExt.values().length;
+		sizeMap = new long[extCnt];
+		blockSizeMap = new int[extCnt];
 	}
 
 	/** @return description of the repository. */
@@ -138,6 +131,15 @@
 		return packName + '.' + ext.getExtension();
 	}
 
+	/**
+	 * @param ext
+	 *            the file extension.
+	 * @return cache key for use by the block cache.
+	 */
+	public DfsStreamKey getStreamKey(PackExt ext) {
+		return DfsStreamKey.of(getRepositoryDescription(), getFileName(ext));
+	}
+
 	/** @return the source of the pack. */
 	public PackSource getPackSource() {
 		return packSource;
@@ -177,7 +179,11 @@
 	 * @return {@code this}
 	 */
 	public DfsPackDescription setFileSize(PackExt ext, long bytes) {
-		sizeMap.put(ext, Long.valueOf(Math.max(0, bytes)));
+		int i = ext.getPosition();
+		if (i >= sizeMap.length) {
+			sizeMap = Arrays.copyOf(sizeMap, i + 1);
+		}
+		sizeMap[i] = Math.max(0, bytes);
 		return this;
 	}
 
@@ -187,8 +193,36 @@
 	 * @return size of the file, in bytes. If 0 the file size is not yet known.
 	 */
 	public long getFileSize(PackExt ext) {
-		Long size = sizeMap.get(ext);
-		return size == null ? 0 : size.longValue();
+		int i = ext.getPosition();
+		return i < sizeMap.length ? sizeMap[i] : 0;
+	}
+
+	/**
+	 * @param ext
+	 *            the file extension.
+	 * @return blockSize of the file, in bytes. If 0 the blockSize size is not
+	 *         yet known and may be discovered when opening the file.
+	 */
+	public int getBlockSize(PackExt ext) {
+		int i = ext.getPosition();
+		return i < blockSizeMap.length ? blockSizeMap[i] : 0;
+	}
+
+	/**
+	 * @param ext
+	 *            the file extension.
+	 * @param blockSize
+	 *            blockSize of the file, in bytes. If 0 the blockSize is not
+	 *            known and will be determined on first read.
+	 * @return {@code this}
+	 */
+	public DfsPackDescription setBlockSize(PackExt ext, int blockSize) {
+		int i = ext.getPosition();
+		if (i >= blockSizeMap.length) {
+			blockSizeMap = Arrays.copyOf(blockSizeMap, i + 1);
+		}
+		blockSizeMap[i] = Math.max(0, blockSize);
+		return this;
 	}
 
 	/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackFile.java
index f15d427..2326219 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackFile.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackFile.java
@@ -72,7 +72,6 @@
 import org.eclipse.jgit.internal.storage.file.PackIndex;
 import org.eclipse.jgit.internal.storage.file.PackReverseIndex;
 import org.eclipse.jgit.internal.storage.pack.BinaryDelta;
-import org.eclipse.jgit.internal.storage.pack.PackExt;
 import org.eclipse.jgit.internal.storage.pack.PackOutputStream;
 import org.eclipse.jgit.internal.storage.pack.StoredObjectRepresentation;
 import org.eclipse.jgit.lib.AbbreviatedObjectId;
@@ -88,53 +87,7 @@
  * delta packed format yielding high compression of lots of object where some
  * objects are similar.
  */
-public final class DfsPackFile {
-	/**
-	 * File offset used to cache {@link #index} in {@link DfsBlockCache}.
-	 * <p>
-	 * To better manage memory, the forward index is stored as a single block in
-	 * the block cache under this file position. A negative value is used
-	 * because it cannot occur in a normal pack file, and it is less likely to
-	 * collide with a valid data block from the file as the high bits will all
-	 * be set when treated as an unsigned long by the cache code.
-	 */
-	private static final long POS_INDEX = -1;
-
-	/** Offset used to cache {@link #reverseIndex}. See {@link #POS_INDEX}. */
-	private static final long POS_REVERSE_INDEX = -2;
-
-	/** Offset used to cache {@link #bitmapIndex}. See {@link #POS_INDEX}. */
-	private static final long POS_BITMAP_INDEX = -3;
-
-	/** Cache that owns this pack file and its data. */
-	private final DfsBlockCache cache;
-
-	/** Description of the pack file's storage. */
-	private final DfsPackDescription packDesc;
-
-	/** Unique identity of this pack while in-memory. */
-	final DfsPackKey key;
-
-	/**
-	 * Total number of bytes in this pack file.
-	 * <p>
-	 * This field initializes to -1 and gets populated when a block is loaded.
-	 */
-	volatile long length;
-
-	/**
-	 * Preferred alignment for loading blocks from the backing file.
-	 * <p>
-	 * It is initialized to 0 and filled in on the first read made from the
-	 * file. Block sizes may be odd, e.g. 4091, caused by the underling DFS
-	 * storing 4091 user bytes and 5 bytes block metadata into a lower level
-	 * 4096 byte block on disk.
-	 */
-	private volatile int blockSize;
-
-	/** True once corruption has been detected that cannot be worked around. */
-	private volatile boolean invalid;
-
+public final class DfsPackFile extends BlockBasedFile {
 	/**
 	 * Lock for initialization of {@link #index} and {@link #corruptObjects}.
 	 * <p>
@@ -167,22 +120,22 @@
 	 *            cache that owns the pack data.
 	 * @param desc
 	 *            description of the pack within the DFS.
-	 * @param key
-	 *            interned key used to identify blocks in the block cache.
 	 */
-	DfsPackFile(DfsBlockCache cache, DfsPackDescription desc, DfsPackKey key) {
-		this.cache = cache;
-		this.packDesc = desc;
-		this.key = key;
+	DfsPackFile(DfsBlockCache cache, DfsPackDescription desc) {
+		super(cache, desc, PACK);
 
-		length = desc.getFileSize(PACK);
-		if (length <= 0)
-			length = -1;
+		int bs = desc.getBlockSize(PACK);
+		if (bs > 0) {
+			setBlockSize(bs);
+		}
+
+		long sz = desc.getFileSize(PACK);
+		length = sz > 0 ? sz : -1;
 	}
 
 	/** @return description that was originally used to configure this pack file. */
 	public DfsPackDescription getPackDescription() {
-		return packDesc;
+		return desc;
 	}
 
 	/**
@@ -193,24 +146,11 @@
 		return idxref != null && idxref.has();
 	}
 
-	/** @return bytes cached in memory for this pack, excluding the index. */
-	public long getCachedSize() {
-		return key.cachedSize.get();
-	}
-
-	String getPackName() {
-		return packDesc.getFileName(PACK);
-	}
-
-	void setBlockSize(int newSize) {
-		blockSize = newSize;
-	}
-
 	void setPackIndex(PackIndex idx) {
 		long objCnt = idx.getObjectCount();
 		int recSize = Constants.OBJECT_ID_LENGTH + 8;
-		int sz = (int) Math.min(objCnt * recSize, Integer.MAX_VALUE);
-		index = cache.put(key, POS_INDEX, sz, idx);
+		long sz = objCnt * recSize;
+		index = cache.putRef(desc.getStreamKey(INDEX), sz, idx);
 	}
 
 	/**
@@ -236,7 +176,7 @@
 		}
 
 		if (invalid)
-			throw new PackInvalidException(getPackName());
+			throw new PackInvalidException(getFileName());
 
 		Repository.getGlobalListenerList()
 				.dispatch(new BeforeDfsPackIndexLoadedEvent(this));
@@ -249,9 +189,21 @@
 					return idx;
 			}
 
+			DfsStreamKey idxKey = desc.getStreamKey(INDEX);
+			idxref = cache.getRef(idxKey);
+			if (idxref != null) {
+				PackIndex idx = idxref.get();
+				if (idx != null) {
+					index = idxref;
+					return idx;
+				}
+			}
+
 			PackIndex idx;
 			try {
-				ReadableChannel rc = ctx.db.openFile(packDesc, INDEX);
+				ctx.stats.readIdx++;
+				long start = System.nanoTime();
+				ReadableChannel rc = ctx.db.openFile(desc, INDEX);
 				try {
 					InputStream in = Channels.newInputStream(rc);
 					int wantSize = 8192;
@@ -260,25 +212,22 @@
 						bs = (wantSize / bs) * bs;
 					else if (bs <= 0)
 						bs = wantSize;
-					in = new BufferedInputStream(in, bs);
-					idx = PackIndex.read(in);
+					idx = PackIndex.read(new BufferedInputStream(in, bs));
+					ctx.stats.readIdxBytes += rc.position();
 				} finally {
 					rc.close();
+					ctx.stats.readIdxMicros += elapsedMicros(start);
 				}
 			} catch (EOFException e) {
 				invalid = true;
-				IOException e2 = new IOException(MessageFormat.format(
+				throw new IOException(MessageFormat.format(
 						DfsText.get().shortReadOfIndex,
-						packDesc.getFileName(INDEX)));
-				e2.initCause(e);
-				throw e2;
+						desc.getFileName(INDEX)), e);
 			} catch (IOException e) {
 				invalid = true;
-				IOException e2 = new IOException(MessageFormat.format(
+				throw new IOException(MessageFormat.format(
 						DfsText.get().cannotReadIndex,
-						packDesc.getFileName(INDEX)));
-				e2.initCause(e);
-				throw e2;
+						desc.getFileName(INDEX)), e);
 			}
 
 			setPackIndex(idx);
@@ -287,12 +236,13 @@
 	}
 
 	final boolean isGarbage() {
-		return packDesc.getPackSource() == UNREACHABLE_GARBAGE;
+		return desc.getPackSource() == UNREACHABLE_GARBAGE;
 	}
 
 	PackBitmapIndex getBitmapIndex(DfsReader ctx) throws IOException {
-		if (invalid || isGarbage())
+		if (invalid || isGarbage() || !desc.hasFileExt(BITMAP_INDEX))
 			return null;
+
 		DfsBlockCache.Ref<PackBitmapIndex> idxref = bitmapIndex;
 		if (idxref != null) {
 			PackBitmapIndex idx = idxref.get();
@@ -300,9 +250,6 @@
 				return idx;
 		}
 
-		if (!packDesc.hasFileExt(PackExt.BITMAP_INDEX))
-			return null;
-
 		synchronized (initLock) {
 			idxref = bitmapIndex;
 			if (idxref != null) {
@@ -311,10 +258,22 @@
 					return idx;
 			}
 
+			DfsStreamKey bitmapKey = desc.getStreamKey(BITMAP_INDEX);
+			idxref = cache.getRef(bitmapKey);
+			if (idxref != null) {
+				PackBitmapIndex idx = idxref.get();
+				if (idx != null) {
+					bitmapIndex = idxref;
+					return idx;
+				}
+			}
+
 			long size;
 			PackBitmapIndex idx;
 			try {
-				ReadableChannel rc = ctx.db.openFile(packDesc, BITMAP_INDEX);
+				ctx.stats.readBitmap++;
+				long start = System.nanoTime();
+				ReadableChannel rc = ctx.db.openFile(desc, BITMAP_INDEX);
 				try {
 					InputStream in = Channels.newInputStream(rc);
 					int wantSize = 8192;
@@ -329,23 +288,20 @@
 				} finally {
 					size = rc.position();
 					rc.close();
+					ctx.stats.readIdxBytes += size;
+					ctx.stats.readIdxMicros += elapsedMicros(start);
 				}
 			} catch (EOFException e) {
-				IOException e2 = new IOException(MessageFormat.format(
+				throw new IOException(MessageFormat.format(
 						DfsText.get().shortReadOfIndex,
-						packDesc.getFileName(BITMAP_INDEX)));
-				e2.initCause(e);
-				throw e2;
+						desc.getFileName(BITMAP_INDEX)), e);
 			} catch (IOException e) {
-				IOException e2 = new IOException(MessageFormat.format(
+				throw new IOException(MessageFormat.format(
 						DfsText.get().cannotReadIndex,
-						packDesc.getFileName(BITMAP_INDEX)));
-				e2.initCause(e);
-				throw e2;
+						desc.getFileName(BITMAP_INDEX)), e);
 			}
 
-			bitmapIndex = cache.put(key, POS_BITMAP_INDEX,
-					(int) Math.min(size, Integer.MAX_VALUE), idx);
+			bitmapIndex = cache.putRef(bitmapKey, size, idx);
 			return idx;
 		}
 	}
@@ -366,11 +322,21 @@
 					return revidx;
 			}
 
+			DfsStreamKey revKey =
+					new DfsStreamKey.ForReverseIndex(desc.getStreamKey(INDEX));
+			revref = cache.getRef(revKey);
+			if (revref != null) {
+				PackReverseIndex idx = revref.get();
+				if (idx != null) {
+					reverseIndex = revref;
+					return idx;
+				}
+			}
+
 			PackIndex idx = idx(ctx);
 			PackReverseIndex revidx = new PackReverseIndex(idx);
-			int sz = (int) Math.min(
-					idx.getObjectCount() * 8, Integer.MAX_VALUE);
-			reverseIndex = cache.put(key, POS_REVERSE_INDEX, sz, revidx);
+			long cnt = idx.getObjectCount();
+			reverseIndex = cache.putRef(revKey, cnt * 8, revidx);
 			return revidx;
 		}
 	}
@@ -421,7 +387,6 @@
 
 	/** Release all memory used by this DfsPackFile instance. */
 	public void close() {
-		cache.remove(this);
 		index = null;
 		reverseIndex = null;
 	}
@@ -478,21 +443,42 @@
 
 	private void copyPackThroughCache(PackOutputStream out, DfsReader ctx)
 			throws IOException {
-		long position = 12;
-		long remaining = length - (12 + 20);
-		while (0 < remaining) {
-			DfsBlock b = cache.getOrLoad(this, position, ctx);
-			int ptr = (int) (position - b.start);
-			int n = (int) Math.min(b.size() - ptr, remaining);
-			b.write(out, position, n);
-			position += n;
-			remaining -= n;
+		ReadableChannel rc = null;
+		try {
+			long position = 12;
+			long remaining = length - (12 + 20);
+			while (0 < remaining) {
+				DfsBlock b;
+				if (rc != null) {
+					b = cache.getOrLoad(this, position, ctx, rc);
+				} else {
+					b = cache.get(key, alignToBlock(position));
+					if (b == null) {
+						rc = ctx.db.openFile(desc, PACK);
+						int sz = ctx.getOptions().getStreamPackBufferSize();
+						if (sz > 0) {
+							rc.setReadAheadBytes(sz);
+						}
+						b = cache.getOrLoad(this, position, ctx, rc);
+					}
+				}
+
+				int ptr = (int) (position - b.start);
+				int n = (int) Math.min(b.size() - ptr, remaining);
+				b.write(out, position, n);
+				position += n;
+				remaining -= n;
+			}
+		} finally {
+			if (rc != null) {
+				rc.close();
+			}
 		}
 	}
 
 	private long copyPackBypassCache(PackOutputStream out, DfsReader ctx)
 			throws IOException {
-		try (ReadableChannel rc = ctx.db.openFile(packDesc, PACK)) {
+		try (ReadableChannel rc = ctx.db.openFile(desc, PACK)) {
 			ByteBuffer buf = newCopyBuffer(out, rc);
 			if (ctx.getOptions().getStreamPackBufferSize() > 0)
 				rc.setReadAheadBytes(ctx.getOptions().getStreamPackBufferSize());
@@ -631,7 +617,7 @@
 					setCorrupt(src.offset);
 					throw new CorruptObjectException(MessageFormat.format(
 							JGitText.get().objectAtHasBadZlibStream,
-							Long.valueOf(src.offset), getPackName()));
+							Long.valueOf(src.offset), getFileName()));
 				}
 			} else if (validate) {
 				assert(crc1 != null);
@@ -673,7 +659,7 @@
 			CorruptObjectException corruptObject = new CorruptObjectException(
 					MessageFormat.format(
 							JGitText.get().objectAtHasBadZlibStream,
-							Long.valueOf(src.offset), getPackName()));
+							Long.valueOf(src.offset), getFileName()));
 			corruptObject.initCause(dataFormat);
 
 			StoredObjectRepresentationNotAvailableException gone;
@@ -735,24 +721,16 @@
 				if (crc2.getValue() != expectedCRC) {
 					throw new CorruptObjectException(MessageFormat.format(
 							JGitText.get().objectAtHasBadZlibStream,
-							Long.valueOf(src.offset), getPackName()));
+							Long.valueOf(src.offset), getFileName()));
 				}
 			}
 		}
 	}
 
-	boolean invalid() {
-		return invalid;
-	}
-
-	void setInvalid() {
-		invalid = true;
-	}
-
 	private IOException packfileIsTruncated() {
 		invalid = true;
 		return new IOException(MessageFormat.format(
-				JGitText.get().packfileIsTruncated, getPackName()));
+				JGitText.get().packfileIsTruncated, getFileName()));
 	}
 
 	private void readFully(long position, byte[] dstbuf, int dstoff, int cnt,
@@ -761,98 +739,8 @@
 			throw new EOFException();
 	}
 
-	long alignToBlock(long pos) {
-		int size = blockSize;
-		if (size == 0)
-			size = cache.getBlockSize();
-		return (pos / size) * size;
-	}
-
 	DfsBlock getOrLoadBlock(long pos, DfsReader ctx) throws IOException {
-		return cache.getOrLoad(this, pos, ctx);
-	}
-
-	DfsBlock readOneBlock(long pos, DfsReader ctx)
-			throws IOException {
-		if (invalid)
-			throw new PackInvalidException(getPackName());
-
-		ReadableChannel rc = ctx.db.openFile(packDesc, PACK);
-		try {
-			int size = blockSize(rc);
-			pos = (pos / size) * size;
-
-			// If the size of the file is not yet known, try to discover it.
-			// Channels may choose to return -1 to indicate they don't
-			// know the length yet, in this case read up to the size unit
-			// given by the caller, then recheck the length.
-			long len = length;
-			if (len < 0) {
-				len = rc.size();
-				if (0 <= len)
-					length = len;
-			}
-
-			if (0 <= len && len < pos + size)
-				size = (int) (len - pos);
-			if (size <= 0)
-				throw new EOFException(MessageFormat.format(
-						DfsText.get().shortReadOfBlock, Long.valueOf(pos),
-						getPackName(), Long.valueOf(0), Long.valueOf(0)));
-
-			byte[] buf = new byte[size];
-			rc.position(pos);
-			int cnt = read(rc, ByteBuffer.wrap(buf, 0, size));
-			if (cnt != size) {
-				if (0 <= len) {
-					throw new EOFException(MessageFormat.format(
-						    DfsText.get().shortReadOfBlock,
-						    Long.valueOf(pos),
-						    getPackName(),
-						    Integer.valueOf(size),
-						    Integer.valueOf(cnt)));
-				}
-
-				// Assume the entire thing was read in a single shot, compact
-				// the buffer to only the space required.
-				byte[] n = new byte[cnt];
-				System.arraycopy(buf, 0, n, 0, n.length);
-				buf = n;
-			} else if (len < 0) {
-				// With no length at the start of the read, the channel should
-				// have the length available at the end.
-				length = len = rc.size();
-			}
-
-			DfsBlock v = new DfsBlock(key, pos, buf);
-			return v;
-		} finally {
-			rc.close();
-		}
-	}
-
-	private int blockSize(ReadableChannel rc) {
-		// If the block alignment is not yet known, discover it. Prefer the
-		// larger size from either the cache or the file itself.
-		int size = blockSize;
-		if (size == 0) {
-			size = rc.blockSize();
-			if (size <= 0)
-				size = cache.getBlockSize();
-			else if (size < cache.getBlockSize())
-				size = (cache.getBlockSize() / size) * size;
-			blockSize = size;
-		}
-		return size;
-	}
-
-	private static int read(ReadableChannel rc, ByteBuffer buf)
-			throws IOException {
-		int n;
-		do {
-			n = rc.read(buf);
-		} while (0 < n && buf.hasRemaining());
-		return buf.position();
+		return cache.getOrLoad(this, pos, ctx, null);
 	}
 
 	ObjectLoader load(DfsReader ctx, long pos)
@@ -991,7 +879,7 @@
 			CorruptObjectException coe = new CorruptObjectException(
 					MessageFormat.format(
 							JGitText.get().objectAtHasBadZlibStream, Long.valueOf(pos),
-							getPackName()));
+							getFileName()));
 			coe.initCause(dfe);
 			throw coe;
 		}
@@ -1139,7 +1027,7 @@
 			CorruptObjectException coe = new CorruptObjectException(
 					MessageFormat.format(
 							JGitText.get().objectAtHasBadZlibStream, Long.valueOf(pos),
-							getPackName()));
+							getFileName()));
 			coe.initCause(dfe);
 			throw coe;
 		}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackParser.java
index 6430ea9..fd99db1 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackParser.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsPackParser.java
@@ -94,7 +94,7 @@
 	private DfsPackDescription packDsc;
 
 	/** Key used during delta resolution reading delta chains. */
-	private DfsPackKey packKey;
+	private DfsStreamKey packKey;
 
 	/** If the index was small enough, the entire index after writing. */
 	private PackIndex packIndex;
@@ -150,12 +150,13 @@
 			readBlock = null;
 			packDsc.addFileExt(PACK);
 			packDsc.setFileSize(PACK, packEnd);
+			packDsc.setBlockSize(PACK, blockSize);
 
 			writePackIndex();
 			objdb.commitPack(Collections.singletonList(packDsc), null);
 			rollback = false;
 
-			DfsPackFile p = blockCache.getOrCreate(packDsc, packKey);
+			DfsPackFile p = new DfsPackFile(blockCache, packDsc);
 			p.setBlockSize(blockSize);
 			if (packIndex != null)
 				p.setPackIndex(packIndex);
@@ -206,9 +207,9 @@
 		}
 
 		packDsc = objdb.newPack(DfsObjDatabase.PackSource.RECEIVE);
-		packKey = new DfsPackKey();
-
 		out = objdb.writeFile(packDsc, PACK);
+		packKey = packDsc.getStreamKey(PACK);
+
 		int size = out.blockSize();
 		if (size <= 0)
 			size = blockCache.getBlockSize();
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReader.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReader.java
index 755b163..4b0d583 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReader.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReader.java
@@ -95,7 +95,7 @@
  * See the base {@link ObjectReader} documentation for details. Notably, a
  * reader is not thread safe.
  */
-public final class DfsReader extends ObjectReader implements ObjectReuseAsIs {
+public class DfsReader extends ObjectReader implements ObjectReuseAsIs {
 	private static final int MAX_RESOLVE_MATCHES = 256;
 
 	/** Temporary buffer large enough for at least one raw object id. */
@@ -104,17 +104,21 @@
 	/** Database this reader loads objects from. */
 	final DfsObjDatabase db;
 
+	final DfsReaderIoStats.Accumulator stats = new DfsReaderIoStats.Accumulator();
+
 	private Inflater inf;
-
 	private DfsBlock block;
-
 	private DeltaBaseCache baseCache;
-
 	private DfsPackFile last;
-
 	private boolean avoidUnreachable;
 
-	DfsReader(DfsObjDatabase db) {
+	/**
+	 * Initialize a new DfsReader
+	 *
+	 * @param db
+	 *            parent DfsObjDatabase.
+	 */
+	protected DfsReader(DfsObjDatabase db) {
 		this.db = db;
 		this.streamFileThreshold = db.getReaderOptions().getStreamFileThreshold();
 	}
@@ -131,7 +135,7 @@
 
 	@Override
 	public ObjectReader newReader() {
-		return new DfsReader(db);
+		return db.newReader();
 	}
 
 	@Override
@@ -170,6 +174,7 @@
 		PackList packList = db.getPackList();
 		resolveImpl(packList, id, matches);
 		if (matches.size() < MAX_RESOLVE_MATCHES && packList.dirty()) {
+			stats.scanPacks++;
 			resolveImpl(db.scanPacks(packList), id, matches);
 		}
 		return matches;
@@ -198,6 +203,7 @@
 		if (hasImpl(packList, objectId)) {
 			return true;
 		} else if (packList.dirty()) {
+			stats.scanPacks++;
 			return hasImpl(db.scanPacks(packList), objectId);
 		}
 		return false;
@@ -234,6 +240,7 @@
 			return checkType(ldr, objectId, typeHint);
 		}
 		if (packList.dirty()) {
+			stats.scanPacks++;
 			ldr = openImpl(db.scanPacks(packList), objectId);
 			if (ldr != null) {
 				return checkType(ldr, objectId, typeHint);
@@ -316,6 +323,7 @@
 		List<FoundObject<T>> r = new ArrayList<>();
 		findAllImpl(packList, pending, r);
 		if (!pending.isEmpty() && packList.dirty()) {
+			stats.scanPacks++;
 			findAllImpl(db.scanPacks(packList), pending, r);
 		}
 		for (T t : pending) {
@@ -452,7 +460,6 @@
 		final IOException findAllError = error;
 		return new AsyncObjectSizeQueue<T>() {
 			private FoundObject<T> cur;
-
 			private long sz;
 
 			@Override
@@ -718,9 +725,10 @@
 		for (int dstoff = 0;;) {
 			int n = inf.inflate(dstbuf, dstoff, dstbuf.length - dstoff);
 			dstoff += n;
-			if (inf.finished() || (headerOnly && dstoff == dstbuf.length))
+			if (inf.finished() || (headerOnly && dstoff == dstbuf.length)) {
+				stats.inflatedBytes += dstoff;
 				return dstoff;
-			if (inf.needsInput()) {
+			} else if (inf.needsInput()) {
 				pin(pack, position);
 				position += block.setInput(position, inf);
 			} else if (n == 0)
@@ -749,8 +757,7 @@
 	}
 
 	void pin(DfsPackFile pack, long position) throws IOException {
-		DfsBlock b = block;
-		if (b == null || !b.contains(pack.key, position)) {
+		if (block == null || !block.contains(pack.key, position)) {
 			// If memory is low, we may need what is in our window field to
 			// be cleaned up by the GC during the get for the next window.
 			// So we always clear it, even though we are just going to set
@@ -764,6 +771,11 @@
 		block = null;
 	}
 
+	/** @return IO statistics accumulated by this reader. */
+	public DfsReaderIoStats getIoStats() {
+		return new DfsReaderIoStats(stats);
+	}
+
 	/** Release the current window cursor. */
 	@Override
 	public void close() {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReaderIoStats.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReaderIoStats.java
new file mode 100644
index 0000000..9a174c8
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReaderIoStats.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2017, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.storage.dfs;
+
+/** IO statistics for a {@link DfsReader}. */
+public class DfsReaderIoStats {
+	/** POJO to accumulate IO statistics. */
+	public static class Accumulator {
+		/** Number of times the reader explicitly called scanPacks. */
+		long scanPacks;
+
+		/** Total number of complete pack indexes read into memory. */
+		long readIdx;
+
+		/** Total number of complete bitmap indexes read into memory. */
+		long readBitmap;
+
+		/** Total number of bytes read from indexes. */
+		long readIdxBytes;
+
+		/** Total microseconds spent reading pack or bitmap indexes. */
+		long readIdxMicros;
+
+		/** Total number of block cache hits. */
+		long blockCacheHit;
+
+		/** Total number of discrete blocks read from pack file(s). */
+		long readBlock;
+
+		/** Total number of compressed bytes read as block sized units. */
+		long readBlockBytes;
+
+		/** Total microseconds spent reading {@link #readBlock} blocks. */
+		long readBlockMicros;
+
+		/** Total number of bytes decompressed. */
+		long inflatedBytes;
+
+		Accumulator() {
+		}
+	}
+
+	private final Accumulator stats;
+
+	DfsReaderIoStats(Accumulator stats) {
+		this.stats = stats;
+	}
+
+	/** @return number of times the reader explicitly called scanPacks. */
+	public long getScanPacks() {
+		return stats.scanPacks;
+	}
+
+	/** @return total number of complete pack indexes read into memory. */
+	public long getReadPackIndexCount() {
+		return stats.readIdx;
+	}
+
+	/** @return total number of complete bitmap indexes read into memory. */
+	public long getReadBitmapIndexCount() {
+		return stats.readBitmap;
+	}
+
+	/** @return total number of bytes read from indexes. */
+	public long getReadIndexBytes() {
+		return stats.readIdxBytes;
+	}
+
+	/** @return total microseconds spent reading pack or bitmap indexes. */
+	public long getReadIndexMicros() {
+		return stats.readIdxMicros;
+	}
+
+	/** @return total number of block cache hits. */
+	public long getBlockCacheHits() {
+		return stats.blockCacheHit;
+	}
+
+	/** @return total number of discrete blocks read from pack file(s). */
+	public long getReadBlocksCount() {
+		return stats.readBlock;
+	}
+
+	/** @return total number of compressed bytes read as block sized units. */
+	public long getReadBlocksBytes() {
+		return stats.readBlockBytes;
+	}
+
+	/** @return total microseconds spent reading blocks. */
+	public long getReadBlocksMicros() {
+		return stats.readBlockMicros;
+	}
+
+	/** @return total number of bytes decompressed. */
+	public long getInflatedBytes() {
+		return stats.inflatedBytes;
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsStreamKey.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsStreamKey.java
new file mode 100644
index 0000000..54a7489
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsStreamKey.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2011, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.storage.dfs;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.util.Arrays;
+
+/** Key used by {@link DfsBlockCache} to disambiguate streams. */
+public abstract class DfsStreamKey {
+	/**
+	 * @param repo
+	 *            description of the containing repository.
+	 * @param name
+	 *            compute the key from a string name.
+	 * @return key for {@code name}
+	 */
+	public static DfsStreamKey of(DfsRepositoryDescription repo, String name) {
+		return new ByteArrayDfsStreamKey(repo, name.getBytes(UTF_8));
+	}
+
+	final int hash;
+
+	/**
+	 * @param hash
+	 *            hash of the other identifying components of the key.
+	 */
+	protected DfsStreamKey(int hash) {
+		// Multiply by 31 here so we can more directly combine with another
+		// value without doing the multiply there.
+		this.hash = hash * 31;
+	}
+
+	@Override
+	public int hashCode() {
+		return hash;
+	}
+
+	@Override
+	public abstract boolean equals(Object o);
+
+	@SuppressWarnings("boxing")
+	@Override
+	public String toString() {
+		return String.format("DfsStreamKey[hash=%08x]", hash); //$NON-NLS-1$
+	}
+
+	private static final class ByteArrayDfsStreamKey extends DfsStreamKey {
+		private final DfsRepositoryDescription repo;
+		private final byte[] name;
+
+		ByteArrayDfsStreamKey(DfsRepositoryDescription repo, byte[] name) {
+			super(repo.hashCode() * 31 + Arrays.hashCode(name));
+			this.repo = repo;
+			this.name = name;
+		}
+
+		@Override
+		public boolean equals(Object o) {
+			if (o instanceof ByteArrayDfsStreamKey) {
+				ByteArrayDfsStreamKey k = (ByteArrayDfsStreamKey) o;
+				return hash == k.hash
+						&& repo.equals(k.repo)
+						&& Arrays.equals(name, k.name);
+			}
+			return false;
+		}
+	}
+
+	static final class ForReverseIndex extends DfsStreamKey {
+		private final DfsStreamKey idxKey;
+
+		ForReverseIndex(DfsStreamKey idxKey) {
+			super(idxKey.hash + 1);
+			this.idxKey = idxKey;
+		}
+
+		@Override
+		public boolean equals(Object o) {
+			return o instanceof ForReverseIndex
+					&& idxKey.equals(((ForReverseIndex) o).idxKey);
+		}
+	}
+}
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 527e46b..383ed3d 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
@@ -53,7 +53,7 @@
 
 	static final AtomicInteger packId = new AtomicInteger();
 
-	private final DfsObjDatabase objdb;
+	private final MemObjDatabase objdb;
 	private final RefDatabase refdb;
 	private String gitwebDescription;
 	private boolean performsAtomicTransactions = true;
@@ -75,7 +75,7 @@
 	}
 
 	@Override
-	public DfsObjDatabase getObjectDatabase() {
+	public MemObjDatabase getObjectDatabase() {
 		return objdb;
 	}
 
@@ -106,13 +106,23 @@
 		gitwebDescription = d;
 	}
 
-	private class MemObjDatabase extends DfsObjDatabase {
+	/** DfsObjDatabase used by InMemoryRepository. */
+	public class MemObjDatabase extends DfsObjDatabase {
 		private List<DfsPackDescription> packs = new ArrayList<>();
+		private int blockSize;
 
 		MemObjDatabase(DfsRepository repo) {
 			super(repo, new DfsReaderOptions());
 		}
 
+		/**
+		 * @param blockSize
+		 *            force a different block size for testing.
+		 */
+		public void setReadableChannelBlockSizeForTest(int blockSize) {
+			this.blockSize = blockSize;
+		}
+
 		@Override
 		protected synchronized List<DfsPackDescription> listPacks() {
 			return packs;
@@ -152,7 +162,7 @@
 			byte[] file = memPack.fileMap.get(ext);
 			if (file == null)
 				throw new FileNotFoundException(desc.getFileName(ext));
-			return new ByteArrayReadableChannel(file);
+			return new ByteArrayReadableChannel(file, blockSize);
 		}
 
 		@Override
@@ -216,13 +226,13 @@
 
 	private static class ByteArrayReadableChannel implements ReadableChannel {
 		private final byte[] data;
-
+		private final int blockSize;
 		private int position;
-
 		private boolean open = true;
 
-		ByteArrayReadableChannel(byte[] buf) {
+		ByteArrayReadableChannel(byte[] buf, int blockSize) {
 			data = buf;
+			this.blockSize = blockSize;
 		}
 
 		@Override
@@ -262,7 +272,7 @@
 
 		@Override
 		public int blockSize() {
-			return 0;
+			return blockSize;
 		}
 
 		@Override
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/LargePackedWholeObject.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/LargePackedWholeObject.java
index 6d40a75..73a93e6 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/LargePackedWholeObject.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/LargePackedWholeObject.java
@@ -99,7 +99,7 @@
 
 	@Override
 	public ObjectStream openStream() throws MissingObjectException, IOException {
-		DfsReader ctx = new DfsReader(db);
+		DfsReader ctx = db.newReader();
 		InputStream in;
 		try {
 			in = new PackInputStream(pack, objectOffset + headerLength, ctx);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/CheckoutEntryImpl.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/CheckoutEntryImpl.java
index 4b4337d..2eacb7a 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/CheckoutEntryImpl.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/CheckoutEntryImpl.java
@@ -74,4 +74,4 @@
 	public String getToBranch() {
 		return to;
 	}
-}
\ No newline at end of file
+}
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 2fd4961..6a674aa 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
@@ -637,12 +637,18 @@
 
 	}
 
+	private boolean shouldAutoDetach() {
+		return getConfig().getBoolean(ConfigConstants.CONFIG_GC_SECTION,
+				ConfigConstants.CONFIG_KEY_AUTODETACH, true);
+	}
+
 	@Override
 	public void autoGC(ProgressMonitor monitor) {
 		GC gc = new GC(this);
 		gc.setPackConfig(new PackConfig(this));
 		gc.setProgressMonitor(monitor);
 		gc.setAuto(true);
+		gc.setBackground(shouldAutoDetach());
 		try {
 			gc.gc();
 		} catch (ParseException | IOException e) {
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 c68e5f7..45ecd32 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
@@ -50,6 +50,8 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
 import java.nio.channels.Channels;
 import java.nio.channels.FileChannel;
 import java.nio.file.DirectoryStream;
@@ -73,11 +75,16 @@
 import java.util.Objects;
 import java.util.Set;
 import java.util.TreeMap;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.api.errors.JGitInternalException;
 import org.eclipse.jgit.dircache.DirCacheIterator;
 import org.eclipse.jgit.errors.CancelledException;
 import org.eclipse.jgit.errors.CorruptObjectException;
@@ -99,6 +106,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.internal.WorkQueue;
 import org.eclipse.jgit.lib.RefDatabase;
 import org.eclipse.jgit.lib.ReflogEntry;
 import org.eclipse.jgit.lib.ReflogReader;
@@ -143,6 +151,20 @@
 
 	private static final int DEFAULT_AUTOLIMIT = 6700;
 
+	private static volatile ExecutorService executor;
+
+	/**
+	 * Set the executor for running auto-gc in the background. If no executor is
+	 * set JGit's own WorkQueue will be used.
+	 *
+	 * @param e
+	 *            the executor to be used for running auto-gc
+	 * @since 4.8
+	 */
+	public static void setExecutor(ExecutorService e) {
+		executor = e;
+	}
+
 	private final FileRepository repo;
 
 	private ProgressMonitor pm;
@@ -178,6 +200,11 @@
 	private boolean automatic;
 
 	/**
+	 * Whether to run gc in a background thread
+	 */
+	private boolean background;
+
+	/**
 	 * Creates a new garbage collector with default values. An expirationTime of
 	 * two weeks and <code>null</code> as progress monitor will be used.
 	 *
@@ -202,13 +229,77 @@
 	 * first check whether any housekeeping is required; if not, it exits
 	 * without performing any work.
 	 *
+	 * If {@link #setBackground(boolean)} was set to {@code true}
+	 * {@code collectGarbage} will start the gc in the background, and then
+	 * return immediately. In this case, errors will not be reported except in
+	 * gc.log.
+	 *
 	 * @return the collection of {@link PackFile}'s which are newly created
 	 * @throws IOException
 	 * @throws ParseException
 	 *             If the configuration parameter "gc.pruneexpire" couldn't be
 	 *             parsed
 	 */
+	// TODO(ms): in 5.0 change signature and return Future<Collection<PackFile>>
 	public Collection<PackFile> gc() throws IOException, ParseException {
+		final GcLog gcLog = background ? new GcLog(repo) : null;
+		if (gcLog != null && !gcLog.lock(background)) {
+			// there is already a background gc running
+			return Collections.emptyList();
+		}
+
+		Callable<Collection<PackFile>> gcTask = () -> {
+			try {
+				Collection<PackFile> newPacks = doGc();
+				if (automatic && tooManyLooseObjects() && gcLog != null) {
+					String message = JGitText.get().gcTooManyUnpruned;
+					gcLog.write(message);
+					gcLog.commit();
+				}
+				return newPacks;
+			} catch (IOException | ParseException e) {
+				if (background) {
+					if (gcLog == null) {
+						// Lacking a log, there's no way to report this.
+						return Collections.emptyList();
+					}
+					try {
+						gcLog.write(e.getMessage());
+						StringWriter sw = new StringWriter();
+						e.printStackTrace(new PrintWriter(sw));
+						gcLog.write(sw.toString());
+						gcLog.commit();
+					} catch (IOException e2) {
+						e2.addSuppressed(e);
+						LOG.error(e2.getMessage(), e2);
+					}
+				} else {
+					throw new JGitInternalException(e.getMessage(), e);
+				}
+			} finally {
+				if (gcLog != null) {
+					gcLog.unlock();
+				}
+			}
+			return Collections.emptyList();
+		};
+		Future<Collection<PackFile>> result = executor().submit(gcTask);
+		if (background) {
+			// TODO(ms): in 5.0 change signature and return the Future
+			return Collections.emptyList();
+		}
+		try {
+			return result.get();
+		} catch (InterruptedException | ExecutionException e) {
+			throw new IOException(e);
+		}
+	}
+
+	private ExecutorService executor() {
+		return (executor != null) ? executor : WorkQueue.getExecutor();
+	}
+
+	private Collection<PackFile> doGc() throws IOException, ParseException {
 		if (automatic && !needGc()) {
 			return Collections.emptyList();
 		}
@@ -729,7 +820,9 @@
 		long time = System.currentTimeMillis();
 		Collection<Ref> refsBefore = getAllRefs();
 
+		Set<ObjectId> allHeadsAndTags = new HashSet<>();
 		Set<ObjectId> allHeads = new HashSet<>();
+		Set<ObjectId> allTags = new HashSet<>();
 		Set<ObjectId> nonHeads = new HashSet<>();
 		Set<ObjectId> txnHeads = new HashSet<>();
 		Set<ObjectId> tagTargets = new HashSet<>();
@@ -739,16 +832,21 @@
 		for (Ref ref : refsBefore) {
 			checkCancelled();
 			nonHeads.addAll(listRefLogObjects(ref, 0));
-			if (ref.isSymbolic() || ref.getObjectId() == null)
+			if (ref.isSymbolic() || ref.getObjectId() == null) {
 				continue;
-			if (isHead(ref) || isTag(ref))
+			}
+			if (isHead(ref)) {
 				allHeads.add(ref.getObjectId());
-			else if (RefTreeNames.isRefTree(refdb, ref.getName()))
+			} else if (isTag(ref)) {
+				allTags.add(ref.getObjectId());
+			} else if (RefTreeNames.isRefTree(refdb, ref.getName())) {
 				txnHeads.add(ref.getObjectId());
-			else
+			} else {
 				nonHeads.add(ref.getObjectId());
-			if (ref.getPeeledObjectId() != null)
+			}
+			if (ref.getPeeledObjectId() != null) {
 				tagTargets.add(ref.getPeeledObjectId());
+			}
 		}
 
 		List<ObjectIdSet> excluded = new LinkedList<>();
@@ -758,13 +856,25 @@
 				excluded.add(f.getIndex());
 		}
 
-		tagTargets.addAll(allHeads);
+		// Don't exclude tags that are also branch tips
+		allTags.removeAll(allHeads);
+		allHeadsAndTags.addAll(allHeads);
+		allHeadsAndTags.addAll(allTags);
+
+		// Hoist all branch tips and tags earlier in the pack file
+		tagTargets.addAll(allHeadsAndTags);
 		nonHeads.addAll(indexObjects);
 
+		// Combine the GC_REST objects into the GC pack if requested
+		if (pconfig != null && pconfig.getSinglePack()) {
+			allHeadsAndTags.addAll(nonHeads);
+			nonHeads.clear();
+		}
+
 		List<PackFile> ret = new ArrayList<>(2);
 		PackFile heads = null;
-		if (!allHeads.isEmpty()) {
-			heads = writePack(allHeads, Collections.<ObjectId> emptySet(),
+		if (!allHeadsAndTags.isEmpty()) {
+			heads = writePack(allHeadsAndTags, PackWriter.NONE, allTags,
 					tagTargets, excluded);
 			if (heads != null) {
 				ret.add(heads);
@@ -772,12 +882,14 @@
 			}
 		}
 		if (!nonHeads.isEmpty()) {
-			PackFile rest = writePack(nonHeads, allHeads, tagTargets, excluded);
+			PackFile rest = writePack(nonHeads, allHeadsAndTags, PackWriter.NONE,
+					tagTargets, excluded);
 			if (rest != null)
 				ret.add(rest);
 		}
 		if (!txnHeads.isEmpty()) {
-			PackFile txn = writePack(txnHeads, PackWriter.NONE, null, excluded);
+			PackFile txn = writePack(txnHeads, PackWriter.NONE, PackWriter.NONE,
+					null, excluded);
 			if (txn != null)
 				ret.add(txn);
 		}
@@ -961,8 +1073,9 @@
 	}
 
 	private PackFile writePack(@NonNull Set<? extends ObjectId> want,
-			@NonNull Set<? extends ObjectId> have, Set<ObjectId> tagTargets,
-			List<ObjectIdSet> excludeObjects) throws IOException {
+			@NonNull Set<? extends ObjectId> have, @NonNull Set<ObjectId> tags,
+			Set<ObjectId> tagTargets, List<ObjectIdSet> excludeObjects)
+			throws IOException {
 		checkCancelled();
 		File tmpPack = null;
 		Map<PackExt, File> tmpExts = new TreeMap<>(
@@ -988,12 +1101,13 @@
 			// prepare the PackWriter
 			pw.setDeltaBaseAsOffset(true);
 			pw.setReuseDeltaCommits(false);
-			if (tagTargets != null)
+			if (tagTargets != null) {
 				pw.setTagTargets(tagTargets);
+			}
 			if (excludeObjects != null)
 				for (ObjectIdSet idx : excludeObjects)
 					pw.excludeObjects(idx);
-			pw.preparePack(pm, want, have);
+			pw.preparePack(pm, want, have, PackWriter.NONE, tags);
 			if (pw.getObjectCount() == 0)
 				return null;
 			checkCancelled();
@@ -1348,6 +1462,14 @@
 		this.automatic = auto;
 	}
 
+	/**
+	 * @param background
+	 *            whether to run the gc in a background thread.
+	 */
+	void setBackground(boolean background) {
+		this.background = background;
+	}
+
 	private boolean needGc() {
 		if (tooManyPacks()) {
 			addRepackAllOption();
@@ -1386,8 +1508,7 @@
 	 * @return {@code true} if number of loose objects > gc.auto (default 6700)
 	 */
 	boolean tooManyLooseObjects() {
-		int auto = repo.getConfig().getInt(ConfigConstants.CONFIG_GC_SECTION,
-				ConfigConstants.CONFIG_KEY_AUTO, DEFAULT_AUTOLIMIT);
+		int auto = getLooseObjectLimit();
 		if (auto <= 0) {
 			return false;
 		}
@@ -1419,4 +1540,9 @@
 		}
 		return false;
 	}
+
+	private int getLooseObjectLimit() {
+		return repo.getConfig().getInt(ConfigConstants.CONFIG_GC_SECTION,
+				ConfigConstants.CONFIG_KEY_AUTO, DEFAULT_AUTOLIMIT);
+	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GcLog.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GcLog.java
new file mode 100644
index 0000000..9ea77cc
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GcLog.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2017 Two Sigma Open Source
+ * 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 org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.ConfigConstants;
+import org.eclipse.jgit.util.GitDateParser;
+import org.eclipse.jgit.util.SystemReader;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.attribute.FileTime;
+import java.text.MessageFormat;
+import java.text.ParseException;
+import java.time.Instant;
+
+/**
+ * This class manages the gc.log file for a {@link FileRepository}.
+ */
+class GcLog {
+	private final FileRepository repo;
+
+	private final File logFile;
+
+	private final LockFile lock;
+
+	private Instant gcLogExpire;
+
+	private static final String LOG_EXPIRY_DEFAULT = "1.day.ago"; //$NON-NLS-1$
+
+	private boolean nonEmpty = false;
+
+	/**
+	 * Construct a GcLog object for a {@link FileRepository}
+	 *
+	 * @param repo
+	 *            the repository
+	 */
+	GcLog(FileRepository repo) {
+		this.repo = repo;
+		logFile = new File(repo.getDirectory(), "gc.log"); //$NON-NLS-1$
+		lock = new LockFile(logFile);
+	}
+
+	private Instant getLogExpiry() throws ParseException {
+		if (gcLogExpire == null) {
+			String logExpiryStr = repo.getConfig().getString(
+					ConfigConstants.CONFIG_GC_SECTION, null,
+					ConfigConstants.CONFIG_KEY_LOGEXPIRY);
+			if (logExpiryStr == null) {
+				logExpiryStr = LOG_EXPIRY_DEFAULT;
+			}
+			gcLogExpire = GitDateParser.parse(logExpiryStr, null,
+					SystemReader.getInstance().getLocale()).toInstant();
+		}
+		return gcLogExpire;
+	}
+
+	private boolean autoGcBlockedByOldLockFile(boolean background) {
+		try {
+			FileTime lastModified = Files.getLastModifiedTime(logFile.toPath());
+			if (lastModified.toInstant().compareTo(getLogExpiry()) > 0) {
+				// There is an existing log file, which is too recent to ignore
+				if (!background) {
+					try (BufferedReader reader = Files
+							.newBufferedReader(logFile.toPath())) {
+						char[] buf = new char[1000];
+						int len = reader.read(buf, 0, 1000);
+						String oldError = new String(buf, 0, len);
+
+						throw new JGitInternalException(MessageFormat.format(
+								JGitText.get().gcLogExists, oldError, logFile));
+					}
+				}
+				return true;
+			}
+		} catch (NoSuchFileException e) {
+			// No existing log file, OK.
+		} catch (IOException | ParseException e) {
+			throw new JGitInternalException(e.getMessage(), e);
+		}
+		return false;
+	}
+
+	/**
+	 * Lock the GC log file for updates
+	 *
+	 * @param background
+	 *            If true, and if gc.log already exists, unlock and return false
+	 * @return {@code true} if we hold the lock
+	 */
+	boolean lock(boolean background) {
+		try {
+			if (!lock.lock()) {
+				return false;
+			}
+		} catch (IOException e) {
+			throw new JGitInternalException(e.getMessage(), e);
+		}
+		if (autoGcBlockedByOldLockFile(background)) {
+			lock.unlock();
+			return false;
+		}
+		return true;
+	}
+
+	/**
+	 * Unlock (roll back) the GC log lock
+	 */
+	void unlock() {
+		lock.unlock();
+	}
+
+	/**
+	 * Commit changes to the gc log, if there have been any writes. Otherwise,
+	 * just unlock and delete the existing file (if any)
+	 *
+	 * @return true if committing (or unlocking/deleting) succeeds.
+	 */
+	boolean commit() {
+		if (nonEmpty) {
+			return lock.commit();
+		} else {
+			logFile.delete();
+			lock.unlock();
+			return true;
+		}
+	}
+
+	/**
+	 * Write to the pending gc log. Content will be committed upon a call to
+	 * commit()
+	 *
+	 * @param content
+	 *            The content to write
+	 * @throws IOException
+	 */
+	void write(String content) throws IOException {
+		if (content.length() > 0) {
+			nonEmpty = true;
+		}
+		lock.write(content.getBytes(UTF_8));
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/InfoAttributesNode.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/InfoAttributesNode.java
index bda5cbe..3f82e2a 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/InfoAttributesNode.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/InfoAttributesNode.java
@@ -78,4 +78,4 @@
 		return r.getRules().isEmpty() ? null : r;
 	}
 
-}
\ No newline at end of file
+}
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 d2fcacf..6221cfa 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
@@ -374,7 +374,7 @@
 		};
 	}
 
-	private void requireLock() {
+	void requireLock() {
 		if (os == null) {
 			unlock();
 			throw new IllegalStateException(MessageFormat.format(JGitText.get().lockOnNotHeld, ref));
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackInputStream.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackInputStream.java
index 154809b..962f765 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackInputStream.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackInputStream.java
@@ -82,4 +82,4 @@
 	public void close() {
 		wc.close();
 	}
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackedBatchRefUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackedBatchRefUpdate.java
new file mode 100644
index 0000000..b661ae7
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackedBatchRefUpdate.java
@@ -0,0 +1,522 @@
+/*
+ * Copyright (C) 2017, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.internal.storage.file;
+
+import static java.util.stream.Collectors.toList;
+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_NONFASTFORWARD;
+
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.errors.LockFailedException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.storage.file.RefDirectory.PackedRefList;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.ProgressMonitor;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.ReflogEntry;
+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.util.RefList;
+
+/**
+ * Implementation of {@link BatchRefUpdate} that uses the {@code packed-refs}
+ * file to support atomically updating multiple refs.
+ * <p>
+ * The algorithm is designed to be compatible with traditional single ref
+ * updates operating on single refs only. Regardless of success or failure, the
+ * results are atomic: from the perspective of any reader, either all updates in
+ * the batch will be visible, or none will. In the case of process failure
+ * during any of the following steps, removal of stale lock files is always
+ * safe, and will never result in an inconsistent state, although the update may
+ * or may not have been applied.
+ * <p>
+ * The algorithm is:
+ * <ol>
+ * <li>Pack loose refs involved in the transaction using the normal pack-refs
+ * operation. This ensures that creating lock files in the following step
+ * succeeds even if a batch contains both a delete of {@code refs/x} (loose) and
+ * a create of {@code refs/x/y}.</li>
+ * <li>Create locks for all loose refs involved in the transaction, even if they
+ * are not currently loose.</li>
+ * <li>Pack loose refs again, this time while holding all lock files (see {@link
+ * RefDirectory#pack(Map)}), without deleting them afterwards. This covers a
+ * potential race where new loose refs were created after the initial packing
+ * step. If no new loose refs were created during this race, this step does not
+ * modify any files on disk. Keep the merged state in memory.</li>
+ * <li>Update the in-memory packed refs with the commands in the batch, possibly
+ * failing the whole batch if any old ref values do not match.</li>
+ * <li>If the update succeeds, lock {@code packed-refs} and commit by atomically
+ * renaming the lock file.</li>
+ * <li>Delete loose ref lock files.</li>
+ * </ol>
+ *
+ * Because the packed-refs file format is a sorted list, this algorithm is
+ * linear in the total number of refs, regardless of the batch size. This can be
+ * a significant slowdown on repositories with large numbers of refs; callers
+ * that prefer speed over atomicity should use {@code setAtomic(false)}. As an
+ * optimization, an update containing a single ref update does not use the
+ * packed-refs protocol.
+ */
+class PackedBatchRefUpdate extends BatchRefUpdate {
+	private RefDirectory refdb;
+
+	PackedBatchRefUpdate(RefDirectory refdb) {
+		super(refdb);
+		this.refdb = refdb;
+	}
+
+	@Override
+	public void execute(RevWalk walk, ProgressMonitor monitor,
+			List<String> options) throws IOException {
+		if (!isAtomic()) {
+			// Use default one-by-one implementation.
+			super.execute(walk, monitor, options);
+			return;
+		}
+		List<ReceiveCommand> pending =
+				ReceiveCommand.filter(getCommands(), NOT_ATTEMPTED);
+		if (pending.isEmpty()) {
+			return;
+		}
+		if (pending.size() == 1) {
+			// Single-ref updates are always atomic, no need for packed-refs.
+			super.execute(walk, monitor, options);
+			return;
+		}
+
+		// Required implementation details copied from super.execute.
+		if (!blockUntilTimestamps(MAX_WAIT)) {
+			return;
+		}
+		if (options != null) {
+			setPushOptions(options);
+		}
+		// End required implementation details.
+
+		// Check for conflicting names before attempting to acquire locks, since
+		// lockfile creation may fail on file/directory conflicts.
+		if (!checkConflictingNames(pending)) {
+			return;
+		}
+
+		if (!checkObjectExistence(walk, pending)) {
+			return;
+		}
+
+		if (!checkNonFastForwards(walk, pending)) {
+			return;
+		}
+
+		// Pack refs normally, so we can create lock files even in the case where
+		// refs/x is deleted and refs/x/y is created in this batch.
+		try {
+			refdb.pack(
+					pending.stream().map(ReceiveCommand::getRefName).collect(toList()));
+		} catch (LockFailedException e) {
+			lockFailure(pending.get(0), pending);
+			return;
+		}
+
+		Map<String, LockFile> locks = null;
+		refdb.inProcessPackedRefsLock.lock();
+		try {
+			locks = lockLooseRefs(pending);
+			if (locks == null) {
+				return;
+			}
+			PackedRefList oldPackedList = refdb.pack(locks);
+			RefList<Ref> newRefs = applyUpdates(walk, oldPackedList, pending);
+			if (newRefs == null) {
+				return;
+			}
+			LockFile packedRefsLock = refdb.lockPackedRefs();
+			if (packedRefsLock == null) {
+				lockFailure(pending.get(0), pending);
+				return;
+			}
+			// commitPackedRefs removes lock file (by renaming over real file).
+			refdb.commitPackedRefs(packedRefsLock, newRefs, oldPackedList);
+		} finally {
+			try {
+				unlockAll(locks);
+			} finally {
+				refdb.inProcessPackedRefsLock.unlock();
+			}
+		}
+
+		refdb.fireRefsChanged();
+		pending.forEach(c -> c.setResult(ReceiveCommand.Result.OK));
+		writeReflog(pending);
+	}
+
+	private boolean checkConflictingNames(List<ReceiveCommand> commands)
+			throws IOException {
+		Set<String> takenNames = new HashSet<>();
+		Set<String> takenPrefixes = new HashSet<>();
+		Set<String> deletes = new HashSet<>();
+		for (ReceiveCommand cmd : commands) {
+			if (cmd.getType() != ReceiveCommand.Type.DELETE) {
+				takenNames.add(cmd.getRefName());
+				addPrefixesTo(cmd.getRefName(), takenPrefixes);
+			} else {
+				deletes.add(cmd.getRefName());
+			}
+		}
+		Set<String> initialRefs = refdb.getRefs(RefDatabase.ALL).keySet();
+		for (String name : initialRefs) {
+			if (!deletes.contains(name)) {
+				takenNames.add(name);
+				addPrefixesTo(name, takenPrefixes);
+			}
+		}
+
+		for (ReceiveCommand cmd : commands) {
+			if (cmd.getType() != ReceiveCommand.Type.DELETE &&
+					takenPrefixes.contains(cmd.getRefName())) {
+				// This ref is a prefix of some other ref. This check doesn't apply when
+				// this command is a delete, because if the ref is deleted nobody will
+				// ever be creating a loose ref with that name.
+				lockFailure(cmd, commands);
+				return false;
+			}
+			for (String prefix : getPrefixes(cmd.getRefName())) {
+				if (takenNames.contains(prefix)) {
+					// A prefix of this ref is already a refname. This check does apply
+					// when this command is a delete, because we would need to create the
+					// refname as a directory in order to create a lockfile for the
+					// to-be-deleted ref.
+					lockFailure(cmd, commands);
+					return false;
+				}
+			}
+		}
+		return true;
+	}
+
+	private boolean checkObjectExistence(RevWalk walk,
+			List<ReceiveCommand> commands) throws IOException {
+		for (ReceiveCommand cmd : commands) {
+			try {
+				if (!cmd.getNewId().equals(ObjectId.zeroId())) {
+					walk.parseAny(cmd.getNewId());
+				}
+			} catch (MissingObjectException e) {
+				// ReceiveCommand#setResult(Result) converts REJECTED to
+				// REJECTED_NONFASTFORWARD, even though that result is also used for a
+				// missing object. Eagerly handle this case so we can set the right
+				// result.
+				reject(cmd, ReceiveCommand.Result.REJECTED_MISSING_OBJECT, commands);
+				return false;
+			}
+		}
+		return true;
+	}
+
+	private boolean checkNonFastForwards(RevWalk walk,
+			List<ReceiveCommand> commands) throws IOException {
+		if (isAllowNonFastForwards()) {
+			return true;
+		}
+		for (ReceiveCommand cmd : commands) {
+			cmd.updateType(walk);
+			if (cmd.getType() == ReceiveCommand.Type.UPDATE_NONFASTFORWARD) {
+				reject(cmd, REJECTED_NONFASTFORWARD, commands);
+				return false;
+			}
+		}
+		return true;
+	}
+
+	/**
+	 * Lock loose refs corresponding to a list of commands.
+	 *
+	 * @param commands
+	 *            commands that we intend to execute.
+	 * @return map of ref name in the input commands to lock file. Always contains
+	 *         one entry for each ref in the input list. All locks are acquired
+	 *         before returning. If any lock was not able to be acquired: the
+	 *         return value is null; no locks are held; and all commands that were
+	 *         pending are set to fail with {@code LOCK_FAILURE}.
+	 * @throws IOException
+	 *             an error occurred other than a failure to acquire; no locks are
+	 *             held if this exception is thrown.
+	 */
+	@Nullable
+	private Map<String, LockFile> lockLooseRefs(List<ReceiveCommand> commands)
+			throws IOException {
+		ReceiveCommand failed = null;
+		Map<String, LockFile> locks = new HashMap<>();
+		try {
+			RETRY: for (int ms : refdb.getRetrySleepMs()) {
+				failed = null;
+				// Release all locks before trying again, to prevent deadlock.
+				unlockAll(locks);
+				locks.clear();
+				RefDirectory.sleep(ms);
+
+				for (ReceiveCommand c : commands) {
+					String name = c.getRefName();
+					LockFile lock = new LockFile(refdb.fileFor(name));
+					if (locks.put(name, lock) != null) {
+						throw new IOException(
+								MessageFormat.format(JGitText.get().duplicateRef, name));
+					}
+					if (!lock.lock()) {
+						failed = c;
+						continue RETRY;
+					}
+				}
+				Map<String, LockFile> result = locks;
+				locks = null;
+				return result;
+			}
+		} finally {
+			unlockAll(locks);
+		}
+		lockFailure(failed != null ? failed : commands.get(0), commands);
+		return null;
+	}
+
+	private static RefList<Ref> applyUpdates(RevWalk walk, RefList<Ref> refs,
+			List<ReceiveCommand> commands) throws IOException {
+		int nDeletes = 0;
+		List<ReceiveCommand> adds = new ArrayList<>(commands.size());
+		for (ReceiveCommand c : commands) {
+			if (c.getType() == ReceiveCommand.Type.CREATE) {
+				adds.add(c);
+			} else if (c.getType() == ReceiveCommand.Type.DELETE) {
+				nDeletes++;
+			}
+		}
+		int addIdx = 0;
+
+		// Construct a new RefList by linearly scanning the old list, and merging in
+		// any updates.
+		Map<String, ReceiveCommand> byName = byName(commands);
+		RefList.Builder<Ref> b =
+				new RefList.Builder<>(refs.size() - nDeletes + adds.size());
+		for (Ref ref : refs) {
+			String name = ref.getName();
+			ReceiveCommand cmd = byName.remove(name);
+			if (cmd == null) {
+				b.add(ref);
+				continue;
+			}
+			if (!cmd.getOldId().equals(ref.getObjectId())) {
+				lockFailure(cmd, commands);
+				return null;
+			}
+
+			// Consume any adds between the last and current ref.
+			while (addIdx < adds.size()) {
+				ReceiveCommand currAdd = adds.get(addIdx);
+				if (currAdd.getRefName().compareTo(name) < 0) {
+					b.add(peeledRef(walk, currAdd));
+					byName.remove(currAdd.getRefName());
+				} else {
+					break;
+				}
+				addIdx++;
+			}
+
+			if (cmd.getType() != ReceiveCommand.Type.DELETE) {
+				b.add(peeledRef(walk, cmd));
+			}
+		}
+
+		// All remaining adds are valid, since the refs didn't exist.
+		while (addIdx < adds.size()) {
+			ReceiveCommand cmd = adds.get(addIdx++);
+			byName.remove(cmd.getRefName());
+			b.add(peeledRef(walk, cmd));
+		}
+
+		// Any remaining updates/deletes do not correspond to any existing refs, so
+		// they are lock failures.
+		if (!byName.isEmpty()) {
+			lockFailure(byName.values().iterator().next(), commands);
+			return null;
+		}
+
+		return b.toRefList();
+	}
+
+	private void writeReflog(List<ReceiveCommand> commands) {
+		PersonIdent ident = getRefLogIdent();
+		if (ident == null) {
+			ident = new PersonIdent(refdb.getRepository());
+		}
+		ReflogWriter w = refdb.getLogWriter();
+		for (ReceiveCommand cmd : commands) {
+			// Assume any pending commands have already been executed atomically.
+			if (cmd.getResult() != ReceiveCommand.Result.OK) {
+				continue;
+			}
+			String name = cmd.getRefName();
+
+			if (cmd.getType() == ReceiveCommand.Type.DELETE) {
+				try {
+					RefDirectory.delete(w.logFor(name), RefDirectory.levelsIn(name));
+				} catch (IOException e) {
+					// Ignore failures, see below.
+				}
+				continue;
+			}
+
+			if (isRefLogDisabled(cmd)) {
+				continue;
+			}
+
+			String msg = getRefLogMessage(cmd);
+			if (isRefLogIncludingResult(cmd)) {
+				String strResult = toResultString(cmd);
+				if (strResult != null) {
+					msg = msg.isEmpty()
+							? strResult : msg + ": " + strResult; //$NON-NLS-1$
+				}
+			}
+			try {
+				w.log(name, cmd.getOldId(), cmd.getNewId(), ident, msg);
+			} catch (IOException e) {
+				// Ignore failures, but continue attempting to write more reflogs.
+				//
+				// In this storage format, it is impossible to atomically write the
+				// reflog with the ref updates, so we have to choose between:
+				// a. Propagating this exception and claiming failure, even though the
+				//    actual ref updates succeeded.
+				// b. Ignoring failures writing the reflog, so we claim success if and
+				//    only if the ref updates succeeded.
+				// We choose (b) in order to surprise callers the least.
+				//
+				// Possible future improvements:
+				// * Log a warning to a logger.
+				// * Retry a fixed number of times in case the error was transient.
+			}
+		}
+	}
+
+	private String toResultString(ReceiveCommand cmd) {
+		switch (cmd.getType()) {
+		case CREATE:
+			return ReflogEntry.PREFIX_CREATED;
+		case UPDATE:
+			// Match the behavior of a single RefUpdate. In that case, setting the
+			// force bit completely bypasses the potentially expensive isMergedInto
+			// check, by design, so the reflog message may be inaccurate.
+			//
+			// Similarly, this class bypasses the isMergedInto checks when the force
+			// bit is set, meaning we can't actually distinguish between UPDATE and
+			// UPDATE_NONFASTFORWARD when isAllowNonFastForwards() returns true.
+			return isAllowNonFastForwards()
+					? ReflogEntry.PREFIX_FORCED_UPDATE : ReflogEntry.PREFIX_FAST_FORWARD;
+		case UPDATE_NONFASTFORWARD:
+			return ReflogEntry.PREFIX_FORCED_UPDATE;
+		default:
+			return null;
+		}
+	}
+
+	private static Map<String, ReceiveCommand> byName(
+			List<ReceiveCommand> commands) {
+		Map<String, ReceiveCommand> ret = new LinkedHashMap<>();
+		for (ReceiveCommand cmd : commands) {
+			ret.put(cmd.getRefName(), cmd);
+		}
+		return ret;
+	}
+
+	private static Ref peeledRef(RevWalk walk, ReceiveCommand cmd)
+			throws IOException {
+		ObjectId newId = cmd.getNewId().copy();
+		RevObject obj = walk.parseAny(newId);
+		if (obj instanceof RevTag) {
+			return new ObjectIdRef.PeeledTag(
+					Ref.Storage.PACKED, cmd.getRefName(), newId, walk.peel(obj).copy());
+		}
+		return new ObjectIdRef.PeeledNonTag(
+				Ref.Storage.PACKED, cmd.getRefName(), newId);
+	}
+
+	private static void unlockAll(@Nullable Map<?, LockFile> locks) {
+		if (locks != null) {
+			locks.values().forEach(LockFile::unlock);
+		}
+	}
+
+	private static void lockFailure(ReceiveCommand cmd,
+			List<ReceiveCommand> commands) {
+		reject(cmd, LOCK_FAILURE, commands);
+	}
+
+	private static void reject(ReceiveCommand cmd, ReceiveCommand.Result result,
+			List<ReceiveCommand> commands) {
+		cmd.setResult(result);
+		for (ReceiveCommand c2 : commands) {
+			if (c2.getResult() == ReceiveCommand.Result.OK) {
+				// Undo OK status so ReceiveCommand#abort aborts it. Assumes this method
+				// is always called before committing any updates to disk.
+				c2.setResult(ReceiveCommand.Result.NOT_ATTEMPTED);
+			}
+		}
+		ReceiveCommand.abort(commands);
+	}
+}
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 24d51a5..ecf7ef9 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
@@ -63,17 +63,22 @@
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStreamReader;
+import java.io.InterruptedIOException;
 import java.security.DigestInputStream;
 import java.security.MessageDigest;
 import java.text.MessageFormat;
 import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.ReentrantLock;
 
 import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.annotations.Nullable;
 import org.eclipse.jgit.errors.InvalidObjectIdException;
 import org.eclipse.jgit.errors.LockFailedException;
 import org.eclipse.jgit.errors.MissingObjectException;
@@ -135,6 +140,10 @@
 			Constants.MERGE_HEAD, Constants.FETCH_HEAD, Constants.ORIG_HEAD,
 			Constants.CHERRY_PICK_HEAD };
 
+	@SuppressWarnings("boxing")
+	private static final List<Integer> RETRY_SLEEP_MS =
+			Collections.unmodifiableList(Arrays.asList(0, 100, 200, 400, 800, 1600));
+
 	private final FileRepository parent;
 
 	private final File gitDir;
@@ -143,7 +152,7 @@
 
 	private final ReflogWriter logWriter;
 
-	private final File packedRefsFile;
+	final File packedRefsFile;
 
 	/**
 	 * Immutable sorted list of loose references.
@@ -159,6 +168,22 @@
 	final AtomicReference<PackedRefList> packedRefs = new AtomicReference<>();
 
 	/**
+	 * Lock for coordinating operations within a single process that may contend
+	 * on the {@code packed-refs} file.
+	 * <p>
+	 * All operations that write {@code packed-refs} must still acquire a
+	 * {@link LockFile} on {@link #packedRefsFile}, even after they have acquired
+	 * this lock, since there may be multiple {@link RefDirectory} instances or
+	 * other processes operating on the same repo on disk.
+	 * <p>
+	 * This lock exists so multiple threads in the same process can wait in a fair
+	 * queue without trying, failing, and retrying to acquire the on-disk lock. If
+	 * {@code RepositoryCache} is used, this lock instance will be used by all
+	 * threads.
+	 */
+	final ReentrantLock inProcessPackedRefsLock = new ReentrantLock(true);
+
+	/**
 	 * Number of modifications made to this database.
 	 * <p>
 	 * This counter is incremented when a change is made, or detected from the
@@ -174,6 +199,8 @@
 	 */
 	private final AtomicInteger lastNotifiedModCnt = new AtomicInteger();
 
+	private List<Integer> retrySleepMs = RETRY_SLEEP_MS;
+
 	RefDirectory(final FileRepository db) {
 		final FS fs = db.getFS();
 		parent = db;
@@ -183,7 +210,7 @@
 		packedRefsFile = fs.resolve(gitDir, PACKED_REFS);
 
 		looseRefs.set(RefList.<LooseRef> emptyList());
-		packedRefs.set(PackedRefList.NO_PACKED_REFS);
+		packedRefs.set(NO_PACKED_REFS);
 	}
 
 	Repository getRepository() {
@@ -209,7 +236,7 @@
 
 	private void clearReferences() {
 		looseRefs.set(RefList.<LooseRef> emptyList());
-		packedRefs.set(PackedRefList.NO_PACKED_REFS);
+		packedRefs.set(NO_PACKED_REFS);
 	}
 
 	@Override
@@ -562,6 +589,16 @@
 		return new RefDirectoryRename(from, to);
 	}
 
+	@Override
+	public PackedBatchRefUpdate newBatchUpdate() {
+		return new PackedBatchRefUpdate(this);
+	}
+
+	@Override
+	public boolean performsAtomicTransactions() {
+		return true;
+	}
+
 	void stored(RefDirectoryUpdate update, FileSnapshot snapshot) {
 		final ObjectId target = update.getNewObjectId().copy();
 		final Ref leaf = update.getRef().getLeaf();
@@ -580,6 +617,9 @@
 
 	void delete(RefDirectoryUpdate update) throws IOException {
 		Ref dst = update.getRef();
+		if (!update.isDetachingSymbolicRef()) {
+			dst = dst.getLeaf();
+		}
 		String name = dst.getName();
 
 		// Write the packed-refs file using an atomic update. We might
@@ -587,16 +627,19 @@
 		// we don't miss an edit made externally.
 		final PackedRefList packed = getPackedRefs();
 		if (packed.contains(name)) {
-			LockFile lck = new LockFile(packedRefsFile);
-			if (!lck.lock())
-				throw new LockFailedException(packedRefsFile);
+			inProcessPackedRefsLock.lock();
 			try {
-				PackedRefList cur = readPackedRefs();
-				int idx = cur.find(name);
-				if (0 <= idx)
-					commitPackedRefs(lck, cur.remove(idx), packed);
+				LockFile lck = lockPackedRefsOrThrow();
+				try {
+					PackedRefList cur = readPackedRefs();
+					int idx = cur.find(name);
+					if (0 <= idx)
+						commitPackedRefs(lck, cur.remove(idx), packed);
+				} finally {
+					lck.unlock();
+				}
 			} finally {
-				lck.unlock();
+				inProcessPackedRefsLock.unlock();
 			}
 		}
 
@@ -632,75 +675,144 @@
 	 * @throws IOException
 	 */
 	public void pack(List<String> refs) throws IOException {
-		if (refs.size() == 0)
-			return;
+		pack(refs, Collections.emptyMap());
+	}
+
+	PackedRefList pack(Map<String, LockFile> heldLocks) throws IOException {
+		return pack(heldLocks.keySet(), heldLocks);
+	}
+
+	private PackedRefList pack(Collection<String> refs,
+			Map<String, LockFile> heldLocks) throws IOException {
+		for (LockFile ol : heldLocks.values()) {
+			ol.requireLock();
+		}
+		if (refs.size() == 0) {
+			return null;
+		}
 		FS fs = parent.getFS();
 
 		// Lock the packed refs file and read the content
-		LockFile lck = new LockFile(packedRefsFile);
-		if (!lck.lock())
-			throw new IOException(MessageFormat.format(
-					JGitText.get().cannotLock, packedRefsFile));
-
+		inProcessPackedRefsLock.lock();
 		try {
-			final PackedRefList packed = getPackedRefs();
-			RefList<Ref> cur = readPackedRefs();
+			LockFile lck = lockPackedRefsOrThrow();
+			try {
+				final PackedRefList packed = getPackedRefs();
+				RefList<Ref> cur = readPackedRefs();
 
-			// Iterate over all refs to be packed
-			for (String refName : refs) {
-				Ref ref = readRef(refName, cur);
-				if (ref.isSymbolic())
-					continue; // can't pack symbolic refs
-				// Add/Update it to packed-refs
-				int idx = cur.find(refName);
-				if (idx >= 0)
-					cur = cur.set(idx, peeledPackedRef(ref));
-				else
-					cur = cur.add(idx, peeledPackedRef(ref));
-			}
-
-			// The new content for packed-refs is collected. Persist it.
-			commitPackedRefs(lck, cur, packed);
-
-			// Now delete the loose refs which are now packed
-			for (String refName : refs) {
-				// Lock the loose ref
-				File refFile = fileFor(refName);
-				if (!fs.exists(refFile))
-					continue;
-				LockFile rLck = new LockFile(refFile);
-				if (!rLck.lock())
-					continue;
-				try {
-					LooseRef currentLooseRef = scanRef(null, refName);
-					if (currentLooseRef == null || currentLooseRef.isSymbolic())
-						continue;
-					Ref packedRef = cur.get(refName);
-					ObjectId clr_oid = currentLooseRef.getObjectId();
-					if (clr_oid != null
-							&& clr_oid.equals(packedRef.getObjectId())) {
-						RefList<LooseRef> curLoose, newLoose;
-						do {
-							curLoose = looseRefs.get();
-							int idx = curLoose.find(refName);
-							if (idx < 0)
-								break;
-							newLoose = curLoose.remove(idx);
-						} while (!looseRefs.compareAndSet(curLoose, newLoose));
-						int levels = levelsIn(refName) - 2;
-						delete(refFile, levels, rLck);
+				// Iterate over all refs to be packed
+				boolean dirty = false;
+				for (String refName : refs) {
+					Ref oldRef = readRef(refName, cur);
+					if (oldRef == null) {
+						continue; // A non-existent ref is already correctly packed.
 					}
-				} finally {
-					rLck.unlock();
+					if (oldRef.isSymbolic()) {
+						continue; // can't pack symbolic refs
+					}
+					// Add/Update it to packed-refs
+					Ref newRef = peeledPackedRef(oldRef);
+					if (newRef == oldRef) {
+						// No-op; peeledPackedRef returns the input ref only if it's already
+						// packed, and readRef returns a packed ref only if there is no
+						// loose ref.
+						continue;
+					}
+
+					dirty = true;
+					int idx = cur.find(refName);
+					if (idx >= 0) {
+						cur = cur.set(idx, newRef);
+					} else {
+						cur = cur.add(idx, newRef);
+					}
 				}
+				if (!dirty) {
+					// All requested refs were already packed accurately
+					return packed;
+				}
+
+				// The new content for packed-refs is collected. Persist it.
+				PackedRefList result = commitPackedRefs(lck, cur, packed);
+
+				// Now delete the loose refs which are now packed
+				for (String refName : refs) {
+					// Lock the loose ref
+					File refFile = fileFor(refName);
+					if (!fs.exists(refFile)) {
+						continue;
+					}
+
+					LockFile rLck = heldLocks.get(refName);
+					boolean shouldUnlock;
+					if (rLck == null) {
+						rLck = new LockFile(refFile);
+						if (!rLck.lock()) {
+							continue;
+						}
+						shouldUnlock = true;
+					} else {
+						shouldUnlock = false;
+					}
+
+					try {
+						LooseRef currentLooseRef = scanRef(null, refName);
+						if (currentLooseRef == null || currentLooseRef.isSymbolic()) {
+							continue;
+						}
+						Ref packedRef = cur.get(refName);
+						ObjectId clr_oid = currentLooseRef.getObjectId();
+						if (clr_oid != null
+								&& clr_oid.equals(packedRef.getObjectId())) {
+							RefList<LooseRef> curLoose, newLoose;
+							do {
+								curLoose = looseRefs.get();
+								int idx = curLoose.find(refName);
+								if (idx < 0) {
+									break;
+								}
+								newLoose = curLoose.remove(idx);
+							} while (!looseRefs.compareAndSet(curLoose, newLoose));
+							int levels = levelsIn(refName) - 2;
+							delete(refFile, levels, rLck);
+						}
+					} finally {
+						if (shouldUnlock) {
+							rLck.unlock();
+						}
+					}
+				}
+				// Don't fire refsChanged. The refs have not change, only their
+				// storage.
+				return result;
+			} finally {
+				lck.unlock();
 			}
-			// Don't fire refsChanged. The refs have not change, only their
-			// storage.
 		} finally {
-			lck.unlock();
+			inProcessPackedRefsLock.unlock();
 		}
 	}
 
+	@Nullable
+	LockFile lockPackedRefs() throws IOException {
+		LockFile lck = new LockFile(packedRefsFile);
+		for (int ms : getRetrySleepMs()) {
+			sleep(ms);
+			if (lck.lock()) {
+				return lck;
+			}
+		}
+		return null;
+	}
+
+	private LockFile lockPackedRefsOrThrow() throws IOException {
+		LockFile lck = lockPackedRefs();
+		if (lck == null) {
+			throw new LockFailedException(packedRefsFile);
+		}
+		return lck;
+	}
+
 	/**
 	 * Make sure a ref is peeled and has the Storage PACKED. If the given ref
 	 * has this attributes simply return it. Otherwise create a new peeled
@@ -794,7 +906,7 @@
 					throw noPackedRefs;
 				}
 				// Ignore it and leave the new list empty.
-				return PackedRefList.NO_PACKED_REFS;
+				return NO_PACKED_REFS;
 			}
 			try {
 				return new PackedRefList(parsePackedRefs(br), snapshot,
@@ -875,8 +987,11 @@
 		return new StringBuilder(end - off).append(src, off, end).toString();
 	}
 
-	private void commitPackedRefs(final LockFile lck, final RefList<Ref> refs,
+	PackedRefList commitPackedRefs(final LockFile lck, final RefList<Ref> refs,
 			final PackedRefList oldPackedList) throws IOException {
+		// Can't just return packedRefs.get() from this method; it might have been
+		// updated again after writePackedRefs() returns.
+		AtomicReference<PackedRefList> result = new AtomicReference<>();
 		new RefWriter(refs) {
 			@Override
 			protected void writeFile(String name, byte[] content)
@@ -898,10 +1013,28 @@
 					throw new ObjectWritingException(MessageFormat.format(JGitText.get().unableToWrite, name));
 
 				byte[] digest = Constants.newMessageDigest().digest(content);
-				packedRefs.compareAndSet(oldPackedList, new PackedRefList(refs,
-						lck.getCommitSnapshot(), ObjectId.fromRaw(digest)));
+				PackedRefList newPackedList = new PackedRefList(
+						refs, lck.getCommitSnapshot(), ObjectId.fromRaw(digest));
+
+				// This thread holds the file lock, so no other thread or process should
+				// be able to modify the packed-refs file on disk. If the list changed,
+				// it means something is very wrong, so throw an exception.
+				//
+				// However, we can't use a naive compareAndSet to check whether the
+				// update was successful, because another thread might _read_ the
+				// packed refs file that was written out by this thread while holding
+				// the lock, and update the packedRefs reference to point to that. So
+				// compare the actual contents instead.
+				PackedRefList afterUpdate = packedRefs.updateAndGet(
+						p -> p.id.equals(oldPackedList.id) ? newPackedList : p);
+				if (!afterUpdate.id.equals(newPackedList.id)) {
+					throw new ObjectWritingException(
+							MessageFormat.format(JGitText.get().unableToWrite, name));
+				}
+				result.set(newPackedList);
 			}
 		}.writePackedRefs();
+		return result.get();
 	}
 
 	private Ref readRef(String name, RefList<Ref> packed) throws IOException {
@@ -1023,7 +1156,7 @@
 	}
 
 	/** If the parent should fire listeners, fires them. */
-	private void fireRefsChanged() {
+	void fireRefsChanged() {
 		final int last = lastNotifiedModCnt.get();
 		final int curr = modCnt.get();
 		if (last != curr && lastNotifiedModCnt.compareAndSet(last, curr) && last != 0)
@@ -1090,22 +1223,80 @@
 		}
 	}
 
-	private static class PackedRefList extends RefList<Ref> {
-		static final PackedRefList NO_PACKED_REFS = new PackedRefList(
-				RefList.emptyList(), FileSnapshot.MISSING_FILE,
-				ObjectId.zeroId());
+	/**
+	 * Get times to sleep while retrying a possibly contentious operation.
+	 * <p>
+	 * For retrying an operation that might have high contention, such as locking
+	 * the {@code packed-refs} file, the caller may implement a retry loop using
+	 * the returned values:
+	 *
+	 * <pre>
+	 * for (int toSleepMs : getRetrySleepMs()) {
+	 *   sleep(toSleepMs);
+	 *   if (isSuccessful(doSomething())) {
+	 *     return success;
+	 *   }
+	 * }
+	 * return failure;
+	 * </pre>
+	 *
+	 * The first value in the returned iterable is 0, and the caller should treat
+	 * a fully-consumed iterator as a timeout.
+	 *
+	 * @return iterable of times, in milliseconds, that the caller should sleep
+	 *         before attempting an operation.
+	 */
+	Iterable<Integer> getRetrySleepMs() {
+		return retrySleepMs;
+	}
 
-		final FileSnapshot snapshot;
+	void setRetrySleepMs(List<Integer> retrySleepMs) {
+		if (retrySleepMs == null || retrySleepMs.isEmpty()
+				|| retrySleepMs.get(0).intValue() != 0) {
+			throw new IllegalArgumentException();
+		}
+		this.retrySleepMs = retrySleepMs;
+	}
 
-		final ObjectId id;
+	/**
+	 * Sleep with {@link Thread#sleep(long)}, converting {@link
+	 * InterruptedException} to {@link InterruptedIOException}.
+	 *
+	 * @param ms
+	 *            time to sleep, in milliseconds; zero or negative is a no-op.
+	 * @throws InterruptedIOException
+	 *             if sleeping was interrupted.
+	 */
+	static void sleep(long ms) throws InterruptedIOException {
+		if (ms <= 0) {
+			return;
+		}
+		try {
+			Thread.sleep(ms);
+		} catch (InterruptedException e) {
+			InterruptedIOException ie = new InterruptedIOException();
+			ie.initCause(e);
+			throw ie;
+		}
+	}
 
-		PackedRefList(RefList<Ref> src, FileSnapshot s, ObjectId i) {
+	static class PackedRefList extends RefList<Ref> {
+
+		private final FileSnapshot snapshot;
+
+		private final ObjectId id;
+
+		private PackedRefList(RefList<Ref> src, FileSnapshot s, ObjectId i) {
 			super(src);
 			snapshot = s;
 			id = i;
 		}
 	}
 
+	private static final PackedRefList NO_PACKED_REFS = new PackedRefList(
+			RefList.emptyList(), FileSnapshot.MISSING_FILE,
+			ObjectId.zeroId());
+
 	private static LooseSymbolicRef newSymbolicRef(FileSnapshot snapshot,
 			String name, String target) {
 		Ref dst = new ObjectIdRef.Unpeeled(NEW, target, null);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectoryUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectoryUpdate.java
index 3c1916b..1105352 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectoryUpdate.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/RefDirectoryUpdate.java
@@ -50,6 +50,7 @@
 
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.ReflogEntry;
 import org.eclipse.jgit.lib.Repository;
 
 /** Updates any reference stored by {@link RefDirectory}. */
@@ -127,14 +128,14 @@
 		return status;
 	}
 
-	private String toResultString(final Result status) {
+	private String toResultString(Result status) {
 		switch (status) {
 		case FORCED:
-			return "forced-update"; //$NON-NLS-1$
+			return ReflogEntry.PREFIX_FORCED_UPDATE;
 		case FAST_FORWARD:
-			return "fast forward"; //$NON-NLS-1$
+			return ReflogEntry.PREFIX_FAST_FORWARD;
 		case NEW:
-			return "created"; //$NON-NLS-1$
+			return ReflogEntry.PREFIX_CREATED;
 		default:
 			return null;
 		}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ReflogEntryImpl.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ReflogEntryImpl.java
index 16b2a46..8723a8b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ReflogEntryImpl.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ReflogEntryImpl.java
@@ -139,4 +139,4 @@
 		else
 			return null;
 	}
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ReflogWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ReflogWriter.java
index 24d2c79..9873798 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ReflogWriter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ReflogWriter.java
@@ -287,4 +287,4 @@
 				|| refName.startsWith(R_REMOTES) //
 				|| refName.equals(R_STASH);
 	}
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/SimpleDataOutput.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/SimpleDataOutput.java
index 373a494..5fe0429 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/SimpleDataOutput.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/SimpleDataOutput.java
@@ -136,4 +136,4 @@
 	public void writeUTF(String s) throws IOException {
 		throw new UnsupportedOperationException();
 	}
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/DeltaIndexScanner.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/DeltaIndexScanner.java
index 7e10878..969d02b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/DeltaIndexScanner.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/DeltaIndexScanner.java
@@ -127,4 +127,4 @@
 			sz <<= 1;
 		return sz;
 	}
-}
\ No newline at end of file
+}
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 8810a9f..7271560 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
@@ -233,7 +233,9 @@
 
 	private List<CachedPack> cachedPacks = new ArrayList<>(2);
 
-	private Set<ObjectId> tagTargets = Collections.emptySet();
+	private Set<ObjectId> tagTargets = NONE;
+
+	private Set<? extends ObjectId> excludeFromBitmapSelection = NONE;
 
 	private ObjectIdSet[] excludeInPacks;
 
@@ -712,8 +714,7 @@
 	public void preparePack(ProgressMonitor countingMonitor,
 			@NonNull Set<? extends ObjectId> want,
 			@NonNull Set<? extends ObjectId> have) throws IOException {
-		preparePack(countingMonitor,
-				want, have, Collections.<ObjectId> emptySet());
+		preparePack(countingMonitor, want, have, NONE, NONE);
 	}
 
 	/**
@@ -721,9 +722,9 @@
 	 * <p>
 	 * Like {@link #preparePack(ProgressMonitor, Set, Set)} but also allows
 	 * specifying commits that should not be walked past ("shallow" commits).
-	 * The caller is responsible for filtering out commits that should not
-	 * be shallow any more ("unshallow" commits as in {@link #setShallowPack})
-	 * from the shallow set.
+	 * The caller is responsible for filtering out commits that should not be
+	 * shallow any more ("unshallow" commits as in {@link #setShallowPack}) from
+	 * the shallow set.
 	 *
 	 * @param countingMonitor
 	 *            progress during object enumeration.
@@ -731,27 +732,67 @@
 	 *            objects of interest, ancestors of which will be included in
 	 *            the pack. Must not be {@code null}.
 	 * @param have
-	 *            objects whose ancestors (up to and including
-	 *            {@code shallow} commits) do not need to be included in the
-	 *            pack because they are already available from elsewhere.
-	 *            Must not be {@code null}.
+	 *            objects whose ancestors (up to and including {@code shallow}
+	 *            commits) do not need to be included in the pack because they
+	 *            are already available from elsewhere. Must not be
+	 *            {@code null}.
 	 * @param shallow
 	 *            commits indicating the boundary of the history marked with
-	 *            {@code have}. Shallow commits have parents but those
-	 *            parents are considered not to be already available.
-	 *            Parents of {@code shallow} commits and earlier generations
-	 *            will be included in the pack if requested by {@code want}.
-	 *            Must not be {@code null}.
+	 *            {@code have}. Shallow commits have parents but those parents
+	 *            are considered not to be already available. Parents of
+	 *            {@code shallow} commits and earlier generations will be
+	 *            included in the pack if requested by {@code want}. Must not be
+	 *            {@code null}.
 	 * @throws IOException
-	 *            an I/O problem occured while reading objects.
+	 *             an I/O problem occurred while reading objects.
 	 */
 	public void preparePack(ProgressMonitor countingMonitor,
 			@NonNull Set<? extends ObjectId> want,
 			@NonNull Set<? extends ObjectId> have,
 			@NonNull Set<? extends ObjectId> shallow) throws IOException {
+		preparePack(countingMonitor, want, have, shallow, NONE);
+	}
+
+	/**
+	 * Prepare the list of objects to be written to the pack stream.
+	 * <p>
+	 * Like {@link #preparePack(ProgressMonitor, Set, Set)} but also allows
+	 * specifying commits that should not be walked past ("shallow" commits).
+	 * The caller is responsible for filtering out commits that should not be
+	 * shallow any more ("unshallow" commits as in {@link #setShallowPack}) from
+	 * the shallow set.
+	 *
+	 * @param countingMonitor
+	 *            progress during object enumeration.
+	 * @param want
+	 *            objects of interest, ancestors of which will be included in
+	 *            the pack. Must not be {@code null}.
+	 * @param have
+	 *            objects whose ancestors (up to and including {@code shallow}
+	 *            commits) do not need to be included in the pack because they
+	 *            are already available from elsewhere. Must not be
+	 *            {@code null}.
+	 * @param shallow
+	 *            commits indicating the boundary of the history marked with
+	 *            {@code have}. Shallow commits have parents but those parents
+	 *            are considered not to be already available. Parents of
+	 *            {@code shallow} commits and earlier generations will be
+	 *            included in the pack if requested by {@code want}. Must not be
+	 *            {@code null}.
+	 * @param noBitmaps
+	 *            collection of objects to be excluded from bitmap commit
+	 *            selection.
+	 * @throws IOException
+	 *             an I/O problem occurred while reading objects.
+	 */
+	public void preparePack(ProgressMonitor countingMonitor,
+			@NonNull Set<? extends ObjectId> want,
+			@NonNull Set<? extends ObjectId> have,
+			@NonNull Set<? extends ObjectId> shallow,
+			@NonNull Set<? extends ObjectId> noBitmaps) throws IOException {
 		try (ObjectWalk ow = getObjectWalk()) {
 			ow.assumeShallow(shallow);
-			preparePack(countingMonitor, ow, want, have);
+			preparePack(countingMonitor, ow, want, have, noBitmaps);
 		}
 	}
 
@@ -784,13 +825,17 @@
 	 *            points of graph traversal). Pass {@link #NONE} if all objects
 	 *            reachable from {@code want} are desired, such as when serving
 	 *            a clone.
+	 * @param noBitmaps
+	 *            collection of objects to be excluded from bitmap commit
+	 *            selection.
 	 * @throws IOException
 	 *             when some I/O problem occur during reading objects.
 	 */
 	public void preparePack(ProgressMonitor countingMonitor,
 			@NonNull ObjectWalk walk,
 			@NonNull Set<? extends ObjectId> interestingObjects,
-			@NonNull Set<? extends ObjectId> uninterestingObjects)
+			@NonNull Set<? extends ObjectId> uninterestingObjects,
+			@NonNull Set<? extends ObjectId> noBitmaps)
 			throws IOException {
 		if (countingMonitor == null)
 			countingMonitor = NullProgressMonitor.INSTANCE;
@@ -798,7 +843,7 @@
 			throw new IllegalArgumentException(
 					JGitText.get().shallowPacksRequireDepthWalk);
 		findObjectsToPack(countingMonitor, walk, interestingObjects,
-				uninterestingObjects);
+				uninterestingObjects, noBitmaps);
 	}
 
 	/**
@@ -965,8 +1010,9 @@
 	/**
 	 * Write the prepared pack to the supplied stream.
 	 * <p>
-	 * Called after {@link #preparePack(ProgressMonitor, ObjectWalk, Set, Set)}
-	 * or {@link #preparePack(ProgressMonitor, Set, Set)}.
+	 * Called after
+	 * {@link #preparePack(ProgressMonitor, ObjectWalk, Set, Set, Set)} or
+	 * {@link #preparePack(ProgressMonitor, Set, Set)}.
 	 * <p>
 	 * Performs delta search if enabled and writes the pack stream.
 	 * <p>
@@ -1652,12 +1698,14 @@
 
 	private void findObjectsToPack(@NonNull ProgressMonitor countingMonitor,
 			@NonNull ObjectWalk walker, @NonNull Set<? extends ObjectId> want,
-			@NonNull Set<? extends ObjectId> have) throws IOException {
+			@NonNull Set<? extends ObjectId> have,
+			@NonNull Set<? extends ObjectId> noBitmaps) throws IOException {
 		final long countingStart = System.currentTimeMillis();
 		beginPhase(PackingPhase.COUNTING, countingMonitor, ProgressMonitor.UNKNOWN);
 
 		stats.interestingObjects = Collections.unmodifiableSet(new HashSet<ObjectId>(want));
 		stats.uninterestingObjects = Collections.unmodifiableSet(new HashSet<ObjectId>(have));
+		excludeFromBitmapSelection = noBitmaps;
 
 		canBuildBitmaps = config.isBuildBitmaps()
 				&& !shallowPack
@@ -2070,8 +2118,8 @@
 		PackWriterBitmapPreparer bitmapPreparer = new PackWriterBitmapPreparer(
 				reader, writeBitmaps, pm, stats.interestingObjects, config);
 
-		Collection<PackWriterBitmapPreparer.BitmapCommit> selectedCommits =
-				bitmapPreparer.selectCommits(numCommits);
+		Collection<PackWriterBitmapPreparer.BitmapCommit> selectedCommits = bitmapPreparer
+				.selectCommits(numCommits, excludeFromBitmapSelection);
 
 		beginPhase(PackingPhase.BUILDING_BITMAPS, pm, selectedCommits.size());
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriterBitmapPreparer.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriterBitmapPreparer.java
index 07a03b4..8bedddb 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriterBitmapPreparer.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/PackWriterBitmapPreparer.java
@@ -141,6 +141,8 @@
 	 *
 	 * @param expectedCommitCount
 	 *            count of commits in the pack
+	 * @param excludeFromBitmapSelection
+	 *            commits that should be excluded from bitmap selection
 	 * @return commit objects for which bitmap indices should be built
 	 * @throws IncorrectObjectTypeException
 	 *             if any of the processed objects is not a commit
@@ -149,7 +151,8 @@
 	 * @throws MissingObjectException
 	 *             if an expected object is missing
 	 */
-	Collection<BitmapCommit> selectCommits(int expectedCommitCount)
+	Collection<BitmapCommit> selectCommits(int expectedCommitCount,
+			Set<? extends ObjectId> excludeFromBitmapSelection)
 			throws IncorrectObjectTypeException, IOException,
 			MissingObjectException {
 		/*
@@ -164,7 +167,7 @@
 		RevWalk rw = new RevWalk(reader);
 		rw.setRetainBody(false);
 		CommitSelectionHelper selectionHelper = setupTipCommitBitmaps(rw,
-				expectedCommitCount);
+				expectedCommitCount, excludeFromBitmapSelection);
 		pm.endTask();
 
 		int totCommits = selectionHelper.getCommitCount();
@@ -363,6 +366,8 @@
 	 * @param expectedCommitCount
 	 *            expected count of commits. The actual count may be less due to
 	 *            unreachable garbage.
+	 * @param excludeFromBitmapSelection
+	 *            commits that should be excluded from bitmap selection
 	 * @return a {@link CommitSelectionHelper} containing bitmaps for the tip
 	 *         commits
 	 * @throws IncorrectObjectTypeException
@@ -373,8 +378,10 @@
 	 *             if an expected object is missing
 	 */
 	private CommitSelectionHelper setupTipCommitBitmaps(RevWalk rw,
-			int expectedCommitCount) throws IncorrectObjectTypeException,
-					IOException, MissingObjectException {
+			int expectedCommitCount,
+			Set<? extends ObjectId> excludeFromBitmapSelection)
+			throws IncorrectObjectTypeException, IOException,
+			MissingObjectException {
 		BitmapBuilder reuse = commitBitmapIndex.newBitmapBuilder();
 		List<BitmapCommit> reuseCommits = new ArrayList<>();
 		for (PackBitmapIndexRemapper.Entry entry : bitmapRemapper) {
@@ -403,7 +410,8 @@
 		Set<RevCommit> peeledWant = new HashSet<>(want.size());
 		for (AnyObjectId objectId : want) {
 			RevObject ro = rw.peel(rw.parseAny(objectId));
-			if (!(ro instanceof RevCommit) || reuse.contains(ro)) {
+			if (!(ro instanceof RevCommit) || reuse.contains(ro)
+					|| excludeFromBitmapSelection.contains(ro)) {
 				continue;
 			}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/AbbreviatedObjectId.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AbbreviatedObjectId.java
index 29a379e..0567051 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/AbbreviatedObjectId.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/AbbreviatedObjectId.java
@@ -336,7 +336,7 @@
 
 	@Override
 	public int hashCode() {
-		return w2;
+		return w1;
 	}
 
 	@Override
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 de1003b..825c1f7 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BaseRepositoryBuilder.java
@@ -738,4 +738,4 @@
 	protected final B self() {
 		return (B) this;
 	}
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchRefUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchRefUpdate.java
index 3f6995d..956607c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchRefUpdate.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchRefUpdate.java
@@ -58,6 +58,8 @@
 import java.util.List;
 import java.util.concurrent.TimeoutException;
 
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.RefUpdate.Result;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -80,8 +82,10 @@
 	 * clock skew between machines on the same LAN using an NTP server also on
 	 * the same LAN should be under 5 seconds. 5 seconds is also not that long
 	 * for a large `git push` operation to complete.
+	 *
+	 * @since 4.9
 	 */
-	private static final Duration MAX_WAIT = Duration.ofSeconds(5);
+	protected static final Duration MAX_WAIT = Duration.ofSeconds(5);
 
 	private final RefDatabase refdb;
 
@@ -173,25 +177,36 @@
 	 * @return message the caller wants to include in the reflog; null if the
 	 *         update should not be logged.
 	 */
+	@Nullable
 	public String getRefLogMessage() {
 		return refLogMessage;
 	}
 
-	/** @return {@code true} if the ref log message should show the result. */
+	/**
+	 * Check whether the reflog message should include the result of the update,
+	 * such as fast-forward or force-update.
+	 * <p>
+	 * Describes the default for commands in this batch that do not override it
+	 * with {@link ReceiveCommand#setRefLogMessage(String, boolean)}.
+	 *
+	 * @return true if the message should include the result.
+	 */
 	public boolean isRefLogIncludingResult() {
 		return refLogIncludeResult;
 	}
 
 	/**
 	 * Set the message to include in the reflog.
+	 * <p>
+	 * Describes the default for commands in this batch that do not override it
+	 * with {@link ReceiveCommand#setRefLogMessage(String, boolean)}.
 	 *
 	 * @param msg
-	 *            the message to describe this change. It may be null if
-	 *            appendStatus is null in order not to append to the reflog
+	 *            the message to describe this change. If null and appendStatus is
+	 *            false, the reflog will not be updated.
 	 * @param appendStatus
 	 *            true if the status of the ref change (fast-forward or
-	 *            forced-update) should be appended to the user supplied
-	 *            message.
+	 *            forced-update) should be appended to the user supplied message.
 	 * @return {@code this}.
 	 */
 	public BatchRefUpdate setRefLogMessage(String msg, boolean appendStatus) {
@@ -209,6 +224,8 @@
 
 	/**
 	 * Don't record this update in the ref's associated reflog.
+	 * <p>
+	 * Equivalent to {@code setRefLogMessage(null, false)}.
 	 *
 	 * @return {@code this}.
 	 */
@@ -218,7 +235,11 @@
 		return this;
 	}
 
-	/** @return true if log has been disabled by {@link #disableRefLog()}. */
+	/**
+	 * Check whether log has been disabled by {@link #disableRefLog()}.
+	 *
+	 * @return true if disabled.
+	 */
 	public boolean isRefLogDisabled() {
 		return refLogMessage == null;
 	}
@@ -323,14 +344,29 @@
 	/**
 	 * Gets the list of option strings associated with this update.
 	 *
-	 * @return pushOptions
+	 * @return push options that were passed to {@link #execute}; prior to calling
+	 *         {@link #execute}, always returns null.
 	 * @since 4.5
 	 */
+	@Nullable
 	public List<String> getPushOptions() {
 		return pushOptions;
 	}
 
 	/**
+	 * Set push options associated with this update.
+	 * <p>
+	 * Implementations must call this at the top of {@link #execute(RevWalk,
+	 * ProgressMonitor, List)}.
+	 *
+	 * @param options options passed to {@code execute}.
+	 * @since 4.9
+	 */
+	protected void setPushOptions(List<String> options) {
+		pushOptions = options;
+	}
+
+	/**
 	 * @return list of timestamps the batch must wait for.
 	 * @since 4.6
 	 */
@@ -396,7 +432,7 @@
 		}
 
 		if (options != null) {
-			pushOptions = options;
+			setPushOptions(options);
 		}
 
 		monitor.beginTask(JGitText.get().updatingReferences, commands.size());
@@ -407,6 +443,11 @@
 		for (ReceiveCommand cmd : commands) {
 			try {
 				if (cmd.getResult() == NOT_ATTEMPTED) {
+					if (isMissing(walk, cmd.getOldId())
+							|| isMissing(walk, cmd.getNewId())) {
+						cmd.setResult(ReceiveCommand.Result.REJECTED_MISSING_OBJECT);
+						continue;
+					}
 					cmd.updateType(walk);
 					switch (cmd.getType()) {
 					case CREATE:
@@ -462,7 +503,7 @@
 								break SWITCH;
 							}
 							ru.setCheckConflicting(false);
-							addRefToPrefixes(takenPrefixes, cmd.getRefName());
+							takenPrefixes.addAll(getPrefixes(cmd.getRefName()));
 							takenNames.add(cmd.getRefName());
 							cmd.setResult(ru.update(walk));
 						}
@@ -478,6 +519,19 @@
 		monitor.endTask();
 	}
 
+	private static boolean isMissing(RevWalk walk, ObjectId id)
+			throws IOException {
+		if (id.equals(ObjectId.zeroId())) {
+			return false; // Explicit add or delete is not missing.
+		}
+		try {
+			walk.parseAny(id);
+			return false;
+		} catch (MissingObjectException e) {
+			return true;
+		}
+	}
+
 	/**
 	 * Wait for timestamps to be in the past, aborting commands on timeout.
 	 *
@@ -523,29 +577,45 @@
 		execute(walk, monitor, null);
 	}
 
-	private static Collection<String> getTakenPrefixes(
-			final Collection<String> names) {
+	private static Collection<String> getTakenPrefixes(Collection<String> names) {
 		Collection<String> ref = new HashSet<>();
-		for (String name : names)
-			ref.addAll(getPrefixes(name));
+		for (String name : names) {
+			addPrefixesTo(name, ref);
+		}
 		return ref;
 	}
 
-	private static void addRefToPrefixes(Collection<String> prefixes,
-			String name) {
-		for (String prefix : getPrefixes(name)) {
-			prefixes.add(prefix);
-		}
+	/**
+	 * Get all path prefixes of a ref name.
+	 *
+	 * @param name
+	 *            ref name.
+	 * @return path prefixes of the ref name. For {@code refs/heads/foo}, returns
+	 *         {@code refs} and {@code refs/heads}.
+	 * @since 4.9
+	 */
+	protected static Collection<String> getPrefixes(String name) {
+		Collection<String> ret = new HashSet<>();
+		addPrefixesTo(name, ret);
+		return ret;
 	}
 
-	static Collection<String> getPrefixes(String s) {
-		Collection<String> ret = new HashSet<>();
-		int p1 = s.indexOf('/');
+	/**
+	 * Add prefixes of a ref name to an existing collection.
+	 *
+	 * @param name
+	 *            ref name.
+	 * @param out
+	 *            path prefixes of the ref name. For {@code refs/heads/foo},
+	 *            returns {@code refs} and {@code refs/heads}.
+	 * @since 4.9
+	 */
+	protected static void addPrefixesTo(String name, Collection<String> out) {
+		int p1 = name.indexOf('/');
 		while (p1 > 0) {
-			ret.add(s.substring(0, p1));
-			p1 = s.indexOf('/', p1 + 1);
+			out.add(name.substring(0, p1));
+			p1 = name.indexOf('/', p1 + 1);
 		}
-		return ret;
 	}
 
 	/**
@@ -560,11 +630,11 @@
 	 */
 	protected RefUpdate newUpdate(ReceiveCommand cmd) throws IOException {
 		RefUpdate ru = refdb.newUpdate(cmd.getRefName(), false);
-		if (isRefLogDisabled())
+		if (isRefLogDisabled(cmd)) {
 			ru.disableRefLog();
-		else {
+		} else {
 			ru.setRefLogIdent(refLogIdent);
-			ru.setRefLogMessage(refLogMessage, refLogIncludeResult);
+			ru.setRefLogMessage(getRefLogMessage(cmd), isRefLogIncludingResult(cmd));
 		}
 		ru.setPushCertificate(pushCert);
 		switch (cmd.getType()) {
@@ -585,6 +655,47 @@
 		}
 	}
 
+	/**
+	 * Check whether reflog is disabled for a command.
+	 *
+	 * @param cmd
+	 *            specific command.
+	 * @return whether the reflog is disabled, taking into account the state from
+	 *         this instance as well as overrides in the given command.
+	 * @since 4.9
+	 */
+	protected boolean isRefLogDisabled(ReceiveCommand cmd) {
+		return cmd.hasCustomRefLog() ? cmd.isRefLogDisabled() : isRefLogDisabled();
+	}
+
+	/**
+	 * Get reflog message for a command.
+	 *
+	 * @param cmd
+	 *            specific command.
+	 * @return reflog message, taking into account the state from this instance as
+	 *         well as overrides in the given command.
+	 * @since 4.9
+	 */
+	protected String getRefLogMessage(ReceiveCommand cmd) {
+		return cmd.hasCustomRefLog() ? cmd.getRefLogMessage() : getRefLogMessage();
+	}
+
+	/**
+	 * Check whether the reflog message for a command should include the result.
+	 *
+	 * @param cmd
+	 *            specific command.
+	 * @return whether the reflog message should show the result, taking into
+	 *         account the state from this instance as well as overrides in the
+	 *         given command.
+	 * @since 4.9
+	 */
+	protected boolean isRefLogIncludingResult(ReceiveCommand cmd) {
+		return cmd.hasCustomRefLog()
+				? cmd.isRefLogIncludingResult() : isRefLogIncludingResult();
+	}
+
 	@Override
 	public String toString() {
 		StringBuilder r = new StringBuilder();
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchingProgressMonitor.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchingProgressMonitor.java
index 54c8052..a75293d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchingProgressMonitor.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchingProgressMonitor.java
@@ -46,6 +46,8 @@
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
 
+import org.eclipse.jgit.lib.internal.WorkQueue;
+
 /** ProgressMonitor that batches update events. */
 public abstract class BatchingProgressMonitor implements ProgressMonitor {
 	private long delayStartTime;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BitmapObject.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BitmapObject.java
index 345016c..4e0dc2c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BitmapObject.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BitmapObject.java
@@ -62,4 +62,4 @@
 	 * @return unique hash of this object.
 	 */
 	public abstract ObjectId getObjectId();
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CheckoutEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CheckoutEntry.java
index d6608cd..34d0b14 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/CheckoutEntry.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/CheckoutEntry.java
@@ -17,4 +17,4 @@
 	 */
 	public abstract String getToBranch();
 
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
index 74fc706..ad5b106 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
@@ -108,6 +108,12 @@
 	public static final String CONFIG_PULL_SECTION = "pull";
 
 	/**
+	 * The "merge" section
+	 * @since 4.9
+	 */
+	public static final String CONFIG_MERGE_SECTION = "merge";
+
+	/**
 	 * The "filter" section
 	 * @since 4.6
 	 */
@@ -291,6 +297,20 @@
 	public static final String CONFIG_KEY_PRUNEPACKEXPIRE = "prunepackexpire";
 
 	/**
+	 * The "logexpiry" key
+	 *
+	 * @since 4.7
+	 */
+	public static final String CONFIG_KEY_LOGEXPIRY = "logExpiry";
+
+	/**
+	 * The "autodetach" key
+	 *
+	 * @since 4.7
+	 */
+	public static final String CONFIG_KEY_AUTODETACH = "autoDetach";
+
+	/**
 	 * The "aggressiveDepth" key
 	 * @since 3.6
 	 */
@@ -351,6 +371,13 @@
 	public static final String CONFIG_KEY_RENAMES = "renames";
 
 	/**
+	 * The "inCoreLimit" key in the "merge section". It's a size limit (bytes) used to
+	 * control a file to be stored in {@code Heap} or {@code LocalFile} during the merge.
+	 * @since 4.9
+	 */
+	public static final String CONFIG_KEY_IN_CORE_LIMIT = "inCoreLimit";
+
+	/**
 	 * The "prune" key
 	 * @since 3.3
 	 */
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java
index ff80672..bb7316d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java
@@ -1,7 +1,7 @@
 /*
  * Copyright (C) 2008, Google Inc.
  * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2006-2012, Shawn O. Pearce <spearce@spearce.org>
+ * Copyright (C) 2006-2017, 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
@@ -429,6 +429,20 @@
 	public static final String HOOKS = "hooks";
 
 	/**
+	 * Merge attribute.
+	 *
+	 * @since 4.9
+	 */
+	public static final String ATTR_MERGE = "merge"; //$NON-NLS-1$
+
+	/**
+	 * Binary value for custom merger.
+	 *
+	 * @since 4.9
+	 */
+	public static final String ATTR_BUILTIN_BINARY_MERGER = "binary"; //$NON-NLS-1$
+
+	/**
 	 * Create a new digest function for objects.
 	 *
 	 * @return a new digest object.
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RebaseTodoFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RebaseTodoFile.java
index 1047a6d..c4923a3 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RebaseTodoFile.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RebaseTodoFile.java
@@ -43,6 +43,8 @@
 
 package org.eclipse.jgit.lib;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import java.io.BufferedOutputStream;
 import java.io.File;
 import java.io.FileOutputStream;
@@ -177,8 +179,8 @@
 		while (tokenCount < 3 && nextSpace < lineEnd) {
 			switch (tokenCount) {
 			case 0:
-				String actionToken = new String(buf, tokenBegin, nextSpace
-						- tokenBegin - 1);
+				String actionToken = new String(buf, tokenBegin,
+						nextSpace - tokenBegin - 1, UTF_8);
 				tokenBegin = nextSpace;
 				action = RebaseTodoLine.Action.parse(actionToken);
 				if (action == null)
@@ -186,14 +188,14 @@
 				break;
 			case 1:
 				nextSpace = RawParseUtils.next(buf, tokenBegin, ' ');
-				String commitToken = new String(buf, tokenBegin, nextSpace
-						- tokenBegin - 1);
+				String commitToken = new String(buf, tokenBegin,
+						nextSpace - tokenBegin - 1, UTF_8);
 				tokenBegin = nextSpace;
 				commit = AbbreviatedObjectId.fromString(commitToken);
 				break;
 			case 2:
-				return new RebaseTodoLine(action, commit, RawParseUtils.decode(
-						buf, tokenBegin, 1 + lineEnd));
+				return new RebaseTodoLine(action, commit,
+						RawParseUtils.decode(buf, tokenBegin, 1 + lineEnd));
 			}
 			tokenCount++;
 		}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefUpdate.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefUpdate.java
index fc334f0..0778645 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefUpdate.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefUpdate.java
@@ -58,7 +58,13 @@
  * Creates, updates or deletes any reference.
  */
 public abstract class RefUpdate {
-	/** Status of an update request. */
+	/**
+	 * Status of an update request.
+	 * <p>
+	 * New values may be added to this enum in the future. Callers may assume that
+	 * unknown values are failures, and may generally treat them the same as
+	 * {@link #REJECTED_OTHER_REASON}.
+	 */
 	public static enum Result {
 		/** The ref update/delete has not been attempted by the caller. */
 		NOT_ATTEMPTED,
@@ -114,6 +120,10 @@
 		 * merged into the new value. The configuration did not allow a forced
 		 * update/delete to take place, so ref still contains the old value. No
 		 * previous history was lost.
+		 * <p>
+		 * <em>Note:</em> Despite the general name, this result only refers to the
+		 * non-fast-forward case. For more general errors, see {@link
+		 * #REJECTED_OTHER_REASON}.
 		 */
 		REJECTED,
 
@@ -139,7 +149,25 @@
 		 * The ref was renamed from another name
 		 * <p>
 		 */
-		RENAMED
+		RENAMED,
+
+		/**
+		 * One or more objects aren't in the repository.
+		 * <p>
+		 * This is severe indication of either repository corruption on the
+		 * server side, or a bug in the client wherein the client did not supply
+		 * all required objects during the pack transfer.
+		 *
+		 * @since 4.9
+		 */
+		REJECTED_MISSING_OBJECT,
+
+		/**
+		 * Rejected for some other reason not covered by another enum value.
+		 *
+		 * @since 4.9
+		 */
+		REJECTED_OTHER_REASON;
 	}
 
 	/** New value the caller wants this ref to have. */
@@ -278,6 +306,16 @@
 	}
 
 	/**
+	 * Return whether this update is actually detaching a symbolic ref.
+	 *
+	 * @return true if detaching a symref.
+	 * @since 4.9
+	 */
+	public boolean isDetachingSymbolicRef() {
+		return detachingSymbolicRef;
+	}
+
+	/**
 	 * Set the new value the ref will update to.
 	 *
 	 * @param id
@@ -627,34 +665,47 @@
 		RevObject oldObj;
 
 		// don't make expensive conflict check if this is an existing Ref
-		if (oldValue == null && checkConflicting && getRefDatabase().isNameConflicting(getName()))
+		if (oldValue == null && checkConflicting
+				&& getRefDatabase().isNameConflicting(getName())) {
 			return Result.LOCK_FAILURE;
+		}
 		try {
 			// If we're detaching a symbolic reference, we should update the reference
 			// itself. Otherwise, we will update the leaf reference, which should be
 			// an ObjectIdRef.
-			if (!tryLock(!detachingSymbolicRef))
+			if (!tryLock(!detachingSymbolicRef)) {
 				return Result.LOCK_FAILURE;
+			}
 			if (expValue != null) {
 				final ObjectId o;
 				o = oldValue != null ? oldValue : ObjectId.zeroId();
-				if (!AnyObjectId.equals(expValue, o))
+				if (!AnyObjectId.equals(expValue, o)) {
 					return Result.LOCK_FAILURE;
+				}
 			}
-			if (oldValue == null)
+			try {
+				newObj = safeParseNew(walk, newValue);
+			} catch (MissingObjectException e) {
+				return Result.REJECTED_MISSING_OBJECT;
+			}
+
+			if (oldValue == null) {
 				return store.execute(Result.NEW);
+			}
 
-			newObj = safeParse(walk, newValue);
-			oldObj = safeParse(walk, oldValue);
-			if (newObj == oldObj && !detachingSymbolicRef)
+			oldObj = safeParseOld(walk, oldValue);
+			if (newObj == oldObj && !detachingSymbolicRef) {
 				return store.execute(Result.NO_CHANGE);
+			}
 
-			if (isForceUpdate())
+			if (isForceUpdate()) {
 				return store.execute(Result.FORCED);
+			}
 
 			if (newObj instanceof RevCommit && oldObj instanceof RevCommit) {
-				if (walk.isMergedInto((RevCommit) oldObj, (RevCommit) newObj))
+				if (walk.isMergedInto((RevCommit) oldObj, (RevCommit) newObj)) {
 					return store.execute(Result.FAST_FORWARD);
+				}
 			}
 
 			return Result.REJECTED;
@@ -674,16 +725,23 @@
 		checkConflicting = check;
 	}
 
-	private static RevObject safeParse(final RevWalk rw, final AnyObjectId id)
+	private static RevObject safeParseNew(RevWalk rw, AnyObjectId newId)
+			throws IOException {
+		if (newId == null || ObjectId.zeroId().equals(newId)) {
+			return null;
+		}
+		return rw.parseAny(newId);
+	}
+
+	private static RevObject safeParseOld(RevWalk rw, AnyObjectId oldId)
 			throws IOException {
 		try {
-			return id != null ? rw.parseAny(id) : null;
+			return oldId != null ? rw.parseAny(oldId) : null;
 		} catch (MissingObjectException e) {
-			// We can expect some objects to be missing, like if we are
-			// trying to force a deletion of a branch and the object it
-			// points to has been pruned from the database due to freak
-			// corruption accidents (it happens with 'git new-work-dir').
-			//
+			// We can expect some old objects to be missing, like if we are trying to
+			// force a deletion of a branch and the object it points to has been
+			// pruned from the database due to freak corruption accidents (it happens
+			// with 'git new-work-dir').
 			return null;
 		}
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ReflogEntry.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ReflogEntry.java
index 0504646..afa6521 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ReflogEntry.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ReflogEntry.java
@@ -50,6 +50,39 @@
 public interface ReflogEntry {
 
 	/**
+	 * Prefix used in reflog messages when the ref was first created.
+	 * <p>
+	 * Does not have a corresponding constant in C git, but is untranslated like
+	 * the other constants.
+	 *
+	 * @since 4.9
+	 */
+	public static final String PREFIX_CREATED = "created"; //$NON-NLS-1$
+
+	/**
+	 * Prefix used in reflog messages when the ref was updated with a fast
+	 * forward.
+	 * <p>
+	 * Untranslated, and exactly matches the
+	 * <a href="https://git.kernel.org/pub/scm/git/git.git/tree/builtin/fetch.c?id=f3da2b79be9565779e4f76dc5812c68e156afdf0#n680">
+	 * untranslated string in C git</a>.
+	 *
+	 * @since 4.9
+	 */
+	public static final String PREFIX_FAST_FORWARD = "fast-forward"; //$NON-NLS-1$
+
+	/**
+	 * Prefix used in reflog messages when the ref was force updated.
+	 * <p>
+	 * Untranslated, and exactly matches the
+	 * <a href="https://git.kernel.org/pub/scm/git/git.git/tree/builtin/fetch.c?id=f3da2b79be9565779e4f76dc5812c68e156afdf0#n695">
+	 * untranslated string in C git</a>.
+	 *
+	 * @since 4.9
+	 */
+	public static final String PREFIX_FORCED_UPDATE = "forced-update"; //$NON-NLS-1$
+
+	/**
 	 * @return the commit id before the change
 	 */
 	public abstract ObjectId getOldId();
@@ -75,4 +108,4 @@
 	 */
 	public abstract CheckoutEntry parseCheckout();
 
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ReflogReader.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ReflogReader.java
index fdab883..d3f2536 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ReflogReader.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ReflogReader.java
@@ -86,4 +86,4 @@
 	public abstract List<ReflogEntry> getReverseEntries(int max)
 			throws IOException;
 
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java
index baa5286..53e9fe3 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java
@@ -55,6 +55,7 @@
 import org.eclipse.jgit.annotations.NonNull;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.lib.internal.WorkQueue;
 import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.IO;
 import org.eclipse.jgit.util.RawParseUtils;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/SubmoduleConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/SubmoduleConfig.java
index 3126160..12f7b82 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/SubmoduleConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/SubmoduleConfig.java
@@ -43,6 +43,10 @@
 
 package org.eclipse.jgit.lib;
 
+import java.util.Locale;
+
+import org.eclipse.jgit.util.StringUtils;
+
 /**
  * Submodule section of a Git configuration file.
  *
@@ -75,12 +79,17 @@
 
 		@Override
 		public String toConfigValue() {
-			return configValue;
+			return name().toLowerCase(Locale.ROOT).replace('_', '-');
 		}
 
 		@Override
 		public boolean matchConfigValue(String s) {
-			return configValue.equals(s);
+			if (StringUtils.isEmptyOrNull(s)) {
+				return false;
+			}
+			s = s.replace('-', '_');
+			return name().equalsIgnoreCase(s)
+					|| configValue.equalsIgnoreCase(s);
 		}
 	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TextProgressMonitor.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TextProgressMonitor.java
index 7675fcc..c31c3c6 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/TextProgressMonitor.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/TextProgressMonitor.java
@@ -44,7 +44,10 @@
 
 package org.eclipse.jgit.lib;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import java.io.IOException;
+import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
 import java.io.Writer;
 
@@ -56,7 +59,7 @@
 
 	/** Initialize a new progress monitor. */
 	public TextProgressMonitor() {
-		this(new PrintWriter(System.err));
+		this(new PrintWriter(new OutputStreamWriter(System.err, UTF_8)));
 	}
 
 	/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/WorkQueue.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/WorkQueue.java
similarity index 95%
rename from org.eclipse.jgit/src/org/eclipse/jgit/lib/WorkQueue.java
rename to org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/WorkQueue.java
index 07b87f5..3303f47 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/WorkQueue.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/internal/WorkQueue.java
@@ -41,7 +41,7 @@
  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
-package org.eclipse.jgit.lib;
+package org.eclipse.jgit.lib.internal;
 
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
@@ -50,7 +50,7 @@
 /**
  * Simple work queue to run tasks in the background
  */
-class WorkQueue {
+public class WorkQueue {
 	private static final ScheduledThreadPoolExecutor executor;
 
 	static final Object executorKiller;
@@ -94,7 +94,10 @@
 		};
 	}
 
-	static ScheduledThreadPoolExecutor getExecutor() {
+	/**
+	 * @return the WorkQueue's executor
+	 */
+	public static ScheduledThreadPoolExecutor getExecutor() {
 		return executor;
 	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatterPass.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatterPass.java
index 0345921..060f068 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatterPass.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/MergeFormatterPass.java
@@ -143,4 +143,4 @@
 		if (out.isBeginln())
 			out.write('\n');
 	}
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
index 1aac352..92c2bb3 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
@@ -2,6 +2,7 @@
  * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com>,
  * Copyright (C) 2010-2012, Matthias Sohn <matthias.sohn@sap.com>
  * Copyright (C) 2012, Research In Motion Limited
+ * Copyright (C) 2017, Obeo (mathieu.cartaud@obeo.fr)
  * and other copyright owners as documented in the project's IP log.
  *
  * This program and the accompanying materials are made available
@@ -67,6 +68,7 @@
 import java.util.List;
 import java.util.Map;
 
+import org.eclipse.jgit.attributes.Attributes;
 import org.eclipse.jgit.diff.DiffAlgorithm;
 import org.eclipse.jgit.diff.DiffAlgorithm.SupportedAlgorithm;
 import org.eclipse.jgit.diff.RawText;
@@ -83,6 +85,7 @@
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.errors.NoWorkTreeException;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
@@ -270,6 +273,12 @@
 	 */
 	protected MergeAlgorithm mergeAlgorithm;
 
+	/**
+	 * The size limit (bytes) which controls a file to be stored in {@code Heap} or
+	 * {@code LocalFile} during the merge.
+	 */
+	private int inCoreLimit;
+
 	private static MergeAlgorithm getMergeAlgorithm(Config config) {
 		SupportedAlgorithm diffAlg = config.getEnum(
 				CONFIG_DIFF_SECTION, null, CONFIG_KEY_ALGORITHM,
@@ -277,6 +286,11 @@
 		return new MergeAlgorithm(DiffAlgorithm.getAlgorithm(diffAlg));
 	}
 
+	private static int getInCoreLimit(Config config) {
+		return config.getInt(
+				ConfigConstants.CONFIG_MERGE_SECTION, ConfigConstants.CONFIG_KEY_IN_CORE_LIMIT, 10 << 20);
+	}
+
 	private static String[] defaultCommitNames() {
 		return new String[] { "BASE", "OURS", "THEIRS" }; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
 	}
@@ -287,7 +301,9 @@
 	 */
 	protected ResolveMerger(Repository local, boolean inCore) {
 		super(local);
-		mergeAlgorithm = getMergeAlgorithm(local.getConfig());
+		Config config = local.getConfig();
+		mergeAlgorithm = getMergeAlgorithm(config);
+		inCoreLimit = getInCoreLimit(config);
 		commitNames = defaultCommitNames();
 		this.inCore = inCore;
 
@@ -429,9 +445,10 @@
 	}
 
 	/**
-	 * Processes one path and tries to merge. This method will do all do all
-	 * trivial (not content) merges and will also detect if a merge will fail.
-	 * The merge will fail when one of the following is true
+	 * Processes one path and tries to merge taking git attributes in account.
+	 * This method will do all trivial (not content) merges and will also detect
+	 * if a merge will fail. The merge will fail when one of the following is
+	 * true
 	 * <ul>
 	 * <li>the index entry does not match the entry in ours. When merging one
 	 * branch into the current HEAD, ours will point to HEAD and theirs will
@@ -463,6 +480,8 @@
 	 * @param ignoreConflicts
 	 *            see
 	 *            {@link ResolveMerger#mergeTrees(AbstractTreeIterator, RevTree, RevTree, boolean)}
+	 * @param attributes
+	 *            the attributes defined for this entry
 	 * @return <code>false</code> if the merge will fail because the index entry
 	 *         didn't match ours or the working-dir file was dirty and a
 	 *         conflict occurred
@@ -470,12 +489,12 @@
 	 * @throws IncorrectObjectTypeException
 	 * @throws CorruptObjectException
 	 * @throws IOException
-	 * @since 3.5
+	 * @since 4.9
 	 */
 	protected boolean processEntry(CanonicalTreeParser base,
 			CanonicalTreeParser ours, CanonicalTreeParser theirs,
 			DirCacheBuildIterator index, WorkingTreeIterator work,
-			boolean ignoreConflicts)
+			boolean ignoreConflicts, Attributes attributes)
 			throws MissingObjectException, IncorrectObjectTypeException,
 			CorruptObjectException, IOException {
 		enterSubtree = true;
@@ -627,7 +646,8 @@
 				return false;
 
 			// Don't attempt to resolve submodule link conflicts
-			if (isGitLink(modeO) || isGitLink(modeT)) {
+			if (isGitLink(modeO) || isGitLink(modeT)
+					|| !attributes.canBeContentMerged()) {
 				add(tw.getRawPath(), base, DirCacheEntry.STAGE_1, 0, 0);
 				add(tw.getRawPath(), ours, DirCacheEntry.STAGE_2, 0, 0);
 				add(tw.getRawPath(), theirs, DirCacheEntry.STAGE_3, 0, 0);
@@ -636,8 +656,9 @@
 			}
 
 			MergeResult<RawText> result = contentMerge(base, ours, theirs);
-			if (ignoreConflicts)
+			if (ignoreConflicts) {
 				result.setContainsConflicts(false);
+			}
 			updateIndex(base, ours, theirs, result);
 			if (result.containsConflicts() && !ignoreConflicts)
 				unmergedPaths.add(tw.getPathString());
@@ -760,6 +781,7 @@
 			MergeResult<RawText> result) throws FileNotFoundException,
 			IOException {
 		File mergedFile = !inCore ? writeMergedFile(result) : null;
+
 		if (result.containsConflicts()) {
 			// A conflict occurred, the file will contain conflict markers
 			// the index will be populated with the three stages and the
@@ -827,7 +849,7 @@
 	private ObjectId insertMergeResult(MergeResult<RawText> result)
 			throws IOException {
 		TemporaryBuffer.LocalFile buf = new TemporaryBuffer.LocalFile(
-				db != null ? nonNullRepo().getDirectory() : null, 10 << 20);
+				db != null ? nonNullRepo().getDirectory() : null, inCoreLimit);
 		try {
 			new MergeFormatter().formatMerge(buf, result,
 					Arrays.asList(commitNames), CHARACTER_ENCODING);
@@ -1091,6 +1113,8 @@
 	protected boolean mergeTreeWalk(TreeWalk treeWalk, boolean ignoreConflicts)
 			throws IOException {
 		boolean hasWorkingTreeIterator = tw.getTreeCount() > T_FILE;
+		boolean hasAttributeNodeProvider = treeWalk
+				.getAttributesNodeProvider() != null;
 		while (treeWalk.next()) {
 			if (!processEntry(
 					treeWalk.getTree(T_BASE, CanonicalTreeParser.class),
@@ -1098,7 +1122,9 @@
 					treeWalk.getTree(T_THEIRS, CanonicalTreeParser.class),
 					treeWalk.getTree(T_INDEX, DirCacheBuildIterator.class),
 					hasWorkingTreeIterator ? treeWalk.getTree(T_FILE,
-							WorkingTreeIterator.class) : null, ignoreConflicts)) {
+							WorkingTreeIterator.class) : null,
+					ignoreConflicts, hasAttributeNodeProvider
+							? treeWalk.getAttributes() : new Attributes())) {
 				cleanUp();
 				return false;
 			}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/nls/TranslationBundle.java b/org.eclipse.jgit/src/org/eclipse/jgit/nls/TranslationBundle.java
index c85c179..bde69c0 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/nls/TranslationBundle.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/nls/TranslationBundle.java
@@ -184,4 +184,4 @@
 			}
 		}
 	}
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/SkipRevFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/SkipRevFilter.java
index e230c9b..51dd2ed 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/SkipRevFilter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/filter/SkipRevFilter.java
@@ -91,4 +91,4 @@
 	public RevFilter clone() {
 		return new SkipRevFilter(skip);
 	}
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/storage/pack/PackConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/storage/pack/PackConfig.java
index c64aa2d..4f2374f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/storage/pack/PackConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/storage/pack/PackConfig.java
@@ -260,6 +260,8 @@
 
 	private boolean cutDeltaChains;
 
+	private boolean singlePack;
+
 	/** Create a default configuration. */
 	public PackConfig() {
 		// Fields are initialized to defaults.
@@ -320,6 +322,7 @@
 		this.bitmapExcessiveBranchCount = cfg.bitmapExcessiveBranchCount;
 		this.bitmapInactiveBranchAgeInDays = cfg.bitmapInactiveBranchAgeInDays;
 		this.cutDeltaChains = cfg.cutDeltaChains;
+		this.singlePack = cfg.singlePack;
 	}
 
 	/**
@@ -555,6 +558,30 @@
 	}
 
 	/**
+	 * @return true if all of refs/* should be packed in a single pack. Default
+	 *        is false, packing a separate GC_REST pack for references outside
+	 *        of refs/heads/* and refs/tags/*.
+	 * @since 4.9
+	 */
+	public boolean getSinglePack() {
+		return singlePack;
+	}
+
+	/**
+	 * If {@code true}, packs a single GC pack for all objects reachable from
+	 * refs/*. Otherwise packs the GC pack with objects reachable from
+	 * refs/heads/* and refs/tags/*, and a GC_REST pack with the remaining
+	 * reachable objects. Disabled by default, packing GC and GC_REST.
+	 *
+	 * @param single
+	 *            true to pack a single GC pack rather than GC and GC_REST packs
+	 * @since 4.9
+	 */
+	public void setSinglePack(boolean single) {
+		singlePack = single;
+	}
+
+	/**
 	 * Get the number of objects to try when looking for a delta base.
 	 *
 	 * This limit is per thread, if 4 threads are used the actual memory used
@@ -1026,6 +1053,8 @@
 				rc.getBoolean("pack", "deltacompression", isDeltaCompress())); //$NON-NLS-1$ //$NON-NLS-2$
 		setCutDeltaChains(
 				rc.getBoolean("pack", "cutdeltachains", getCutDeltaChains())); //$NON-NLS-1$ //$NON-NLS-2$
+		setSinglePack(
+				rc.getBoolean("pack", "singlepack", getSinglePack())); //$NON-NLS-1$ //$NON-NLS-2$
 		setBuildBitmaps(
 				rc.getBoolean("pack", "buildbitmaps", isBuildBitmaps())); //$NON-NLS-1$ //$NON-NLS-2$
 		setBitmapContiguousCommitCount(
@@ -1073,6 +1102,7 @@
 				.append(getBitmapExcessiveBranchCount());
 		b.append(", bitmapInactiveBranchAge=") //$NON-NLS-1$
 				.append(getBitmapInactiveBranchAgeInDays());
+		b.append(", singlePack=").append(getSinglePack()); //$NON-NLS-1$
 		return b.toString();
 	}
 }
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 280e6d4..dd26fe5 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java
@@ -74,6 +74,7 @@
 import org.eclipse.jgit.lib.BatchingProgressMonitor;
 import org.eclipse.jgit.lib.Constants;
 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.RefDatabase;
@@ -360,12 +361,19 @@
 
 	private void expandSingle(final RefSpec spec, final Set<Ref> matched)
 			throws TransportException {
-		final Ref src = conn.getRef(spec.getSource());
-		if (src == null) {
-			throw new TransportException(MessageFormat.format(JGitText.get().remoteDoesNotHaveSpec, spec.getSource()));
+		String want = spec.getSource();
+		if (ObjectId.isId(want)) {
+			want(ObjectId.fromString(want));
+			return;
 		}
-		if (matched.add(src))
+
+		Ref src = conn.getRef(want);
+		if (src == null) {
+			throw new TransportException(MessageFormat.format(JGitText.get().remoteDoesNotHaveSpec, want));
+		}
+		if (matched.add(src)) {
 			want(src, spec);
+		}
 	}
 
 	private Collection<Ref> expandAutoFollowTags() throws TransportException {
@@ -440,6 +448,11 @@
 		fetchHeadUpdates.add(fhr);
 	}
 
+	private void want(ObjectId id) {
+		askFor.put(id,
+				new ObjectIdRef.Unpeeled(Ref.Storage.NETWORK, id.name(), id));
+	}
+
 	private TrackingRefUpdate createUpdate(RefSpec spec, ObjectId newId)
 			throws TransportException {
 		Ref ref = localRefs().get(spec.getDestination());
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 f445bcb..82d6ed4 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/JschSession.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/JschSession.java
@@ -229,4 +229,4 @@
 			return exitValue();
 		}
 	}
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/NetRC.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/NetRC.java
index bab5bf0..5727b03 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/NetRC.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/NetRC.java
@@ -317,4 +317,4 @@
 			}
 		}
 	}
-}
\ No newline at end of file
+}
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 c82b389..db3578b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackParser.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackParser.java
@@ -143,7 +143,7 @@
 
 	private boolean expectDataAfterPackFooter;
 
-	private long objectCount;
+	private long expectedObjectCount;
 
 	private PackedObjectInfo[] entries;
 
@@ -173,8 +173,8 @@
 
 	private LongMap<UnresolvedDelta> baseByPos;
 
-	/** Blobs whose contents need to be double-checked after indexing. */
-	private BlockList<PackedObjectInfo> deferredCheckBlobs;
+	/** Objects need to be double-checked for collision after indexing. */
+	private BlockList<PackedObjectInfo> collisionCheckObjs;
 
 	private MessageDigest packDigest;
 
@@ -525,15 +525,15 @@
 		try {
 			readPackHeader();
 
-			entries = new PackedObjectInfo[(int) objectCount];
+			entries = new PackedObjectInfo[(int) expectedObjectCount];
 			baseById = new ObjectIdOwnerMap<>();
 			baseByPos = new LongMap<>();
-			deferredCheckBlobs = new BlockList<>();
+			collisionCheckObjs = new BlockList<>();
 
 			receiving.beginTask(JGitText.get().receivingObjects,
-					(int) objectCount);
+					(int) expectedObjectCount);
 			try {
-				for (int done = 0; done < objectCount; done++) {
+				for (int done = 0; done < expectedObjectCount; done++) {
 					indexOneObject();
 					receiving.update(1);
 					if (receiving.isCancelled())
@@ -545,32 +545,12 @@
 				receiving.endTask();
 			}
 
-			if (!deferredCheckBlobs.isEmpty())
-				doDeferredCheckBlobs();
+			if (!collisionCheckObjs.isEmpty()) {
+				checkObjectCollision();
+			}
+
 			if (deltaCount > 0) {
-				if (resolving instanceof BatchingProgressMonitor) {
-					((BatchingProgressMonitor) resolving).setDelayStart(
-							1000,
-							TimeUnit.MILLISECONDS);
-				}
-				resolving.beginTask(JGitText.get().resolvingDeltas, deltaCount);
-				resolveDeltas(resolving);
-				if (entryCount < objectCount) {
-					if (!isAllowThin()) {
-						throw new IOException(MessageFormat.format(
-								JGitText.get().packHasUnresolvedDeltas,
-								Long.valueOf(objectCount - entryCount)));
-					}
-
-					resolveDeltasWithExternalBases(resolving);
-
-					if (entryCount < objectCount) {
-						throw new IOException(MessageFormat.format(
-								JGitText.get().packHasUnresolvedDeltas,
-								Long.valueOf(objectCount - entryCount)));
-					}
-				}
-				resolving.endTask();
+				processDeltas(resolving);
 			}
 
 			packDigest = null;
@@ -593,6 +573,31 @@
 		return null; // By default there is no locking.
 	}
 
+	private void processDeltas(ProgressMonitor resolving) throws IOException {
+		if (resolving instanceof BatchingProgressMonitor) {
+			((BatchingProgressMonitor) resolving).setDelayStart(1000,
+					TimeUnit.MILLISECONDS);
+		}
+		resolving.beginTask(JGitText.get().resolvingDeltas, deltaCount);
+		resolveDeltas(resolving);
+		if (entryCount < expectedObjectCount) {
+			if (!isAllowThin()) {
+				throw new IOException(MessageFormat.format(
+						JGitText.get().packHasUnresolvedDeltas,
+						Long.valueOf(expectedObjectCount - entryCount)));
+			}
+
+			resolveDeltasWithExternalBases(resolving);
+
+			if (entryCount < expectedObjectCount) {
+				throw new IOException(MessageFormat.format(
+						JGitText.get().packHasUnresolvedDeltas,
+						Long.valueOf(expectedObjectCount - entryCount)));
+			}
+		}
+		resolving.endTask();
+	}
+
 	private void resolveDeltas(final ProgressMonitor progress)
 			throws IOException {
 		final int last = entryCount;
@@ -675,10 +680,14 @@
 			objectDigest.digest(tempObjectId);
 
 			verifySafeObject(tempObjectId, type, visit.data);
+			if (isCheckObjectCollisions() && readCurs.has(tempObjectId)) {
+				checkObjectCollision(tempObjectId, type, visit.data);
+			}
 
 			PackedObjectInfo oe;
 			oe = newInfo(tempObjectId, visit.delta, visit.parent.id);
 			oe.setOffset(visit.delta.position);
+			oe.setType(type);
 			onInflatedObjectData(oe, type, visit.data);
 			addObjectAndTrack(oe);
 			visit.id = oe;
@@ -849,10 +858,9 @@
 			visit.id = baseId;
 			final int typeCode = ldr.getType();
 			final PackedObjectInfo oe = newInfo(baseId, null, null);
-
+			oe.setType(typeCode);
 			if (onAppendBase(typeCode, visit.data, oe))
 				entries[entryCount++] = oe;
-
 			visit.nextChild = firstChildOf(oe);
 			resolveDeltas(visit.next(), typeCode,
 					new ObjectTypeAndSize(), progress);
@@ -873,7 +881,7 @@
 	private void growEntries(int extraObjects) {
 		final PackedObjectInfo[] ne;
 
-		ne = new PackedObjectInfo[(int) objectCount + extraObjects];
+		ne = new PackedObjectInfo[(int) expectedObjectCount + extraObjects];
 		System.arraycopy(entries, 0, ne, 0, entryCount);
 		entries = ne;
 	}
@@ -896,9 +904,9 @@
 		if (vers != 2 && vers != 3)
 			throw new IOException(MessageFormat.format(
 					JGitText.get().unsupportedPackVersion, Long.valueOf(vers)));
-		objectCount = NB.decodeUInt32(buf, p + 8);
+		final long objectCount = NB.decodeUInt32(buf, p + 8);
 		use(hdrln);
-
+		setExpectedObjectCount(objectCount);
 		onPackHeader(objectCount);
 	}
 
@@ -1031,7 +1039,6 @@
 		objectDigest.update((byte) 0);
 
 		final byte[] data;
-		boolean checkContentLater = false;
 		if (type == Constants.OBJ_BLOB) {
 			byte[] readBuffer = buffer();
 			InputStream inf = inflate(Source.INPUT, sz);
@@ -1045,10 +1052,7 @@
 			}
 			inf.close();
 			objectDigest.digest(tempObjectId);
-			checkContentLater = isCheckObjectCollisions()
-					&& readCurs.has(tempObjectId);
 			data = null;
-
 		} else {
 			data = inflateAndReturn(Source.INPUT, sz);
 			objectDigest.update(data);
@@ -1058,16 +1062,32 @@
 
 		PackedObjectInfo obj = newInfo(tempObjectId, null, null);
 		obj.setOffset(pos);
+		obj.setType(type);
 		onEndWholeObject(obj);
 		if (data != null)
 			onInflatedObjectData(obj, type, data);
 		addObjectAndTrack(obj);
-		if (checkContentLater)
-			deferredCheckBlobs.add(obj);
+
+		if (isCheckObjectCollisions()) {
+			collisionCheckObjs.add(obj);
+		}
 	}
 
-	private void verifySafeObject(final AnyObjectId id, final int type,
-			final byte[] data) throws IOException {
+	/**
+	 * Verify the integrity of the object.
+	 *
+	 * @param id
+	 *            identity of the object to be checked.
+	 * @param type
+	 *            the type of the object.
+	 * @param data
+	 *            raw content of the object.
+	 * @throws CorruptObjectException
+	 * @since 4.9
+	 *
+	 */
+	protected void verifySafeObject(final AnyObjectId id, final int type,
+			final byte[] data) throws CorruptObjectException {
 		if (objCheck != null) {
 			try {
 				objCheck.check(id, type, data);
@@ -1075,65 +1095,73 @@
 				if (e.getErrorType() != null) {
 					throw e;
 				}
-				throw new CorruptObjectException(MessageFormat.format(
-						JGitText.get().invalidObject,
-						Constants.typeString(type),
-						readCurs.abbreviate(id, 10).name(),
-						e.getMessage()), e);
-			}
-		}
-
-		if (isCheckObjectCollisions()) {
-			try {
-				final ObjectLoader ldr = readCurs.open(id, type);
-				final byte[] existingData = ldr.getCachedBytes(data.length);
-				if (!Arrays.equals(data, existingData)) {
-					throw new IOException(MessageFormat.format(
-							JGitText.get().collisionOn, id.name()));
-				}
-			} catch (MissingObjectException notLocal) {
-				// This is OK, we don't have a copy of the object locally
-				// but the API throws when we try to read it as usually its
-				// an error to read something that doesn't exist.
+				throw new CorruptObjectException(
+						MessageFormat.format(JGitText.get().invalidObject,
+								Constants.typeString(type), id.name(),
+								e.getMessage()),
+						e);
 			}
 		}
 	}
 
-	private void doDeferredCheckBlobs() throws IOException {
+	private void checkObjectCollision() throws IOException {
+		for (PackedObjectInfo obj : collisionCheckObjs) {
+			if (!readCurs.has(obj)) {
+				continue;
+			}
+			checkObjectCollision(obj);
+		}
+	}
+
+	private void checkObjectCollision(PackedObjectInfo obj)
+			throws IOException {
+		ObjectTypeAndSize info = openDatabase(obj, new ObjectTypeAndSize());
 		final byte[] readBuffer = buffer();
 		final byte[] curBuffer = new byte[readBuffer.length];
-		ObjectTypeAndSize info = new ObjectTypeAndSize();
-
-		for (PackedObjectInfo obj : deferredCheckBlobs) {
-			info = openDatabase(obj, info);
-
-			if (info.type != Constants.OBJ_BLOB)
+		long sz = info.size;
+		InputStream pck = null;
+		try (ObjectStream cur = readCurs.open(obj, info.type).openStream()) {
+			if (cur.getSize() != sz) {
 				throw new IOException(MessageFormat.format(
-						JGitText.get().unknownObjectType,
-						Integer.valueOf(info.type)));
-
-			ObjectStream cur = readCurs.open(obj, info.type).openStream();
-			try {
-				long sz = info.size;
-				if (cur.getSize() != sz)
-					throw new IOException(MessageFormat.format(
-							JGitText.get().collisionOn, obj.name()));
-				InputStream pck = inflate(Source.DATABASE, sz);
-				while (0 < sz) {
-					int n = (int) Math.min(readBuffer.length, sz);
-					IO.readFully(cur, curBuffer, 0, n);
-					IO.readFully(pck, readBuffer, 0, n);
-					for (int i = 0; i < n; i++) {
-						if (curBuffer[i] != readBuffer[i])
-							throw new IOException(MessageFormat.format(JGitText
-									.get().collisionOn, obj.name()));
-					}
-					sz -= n;
-				}
-				pck.close();
-			} finally {
-				cur.close();
+						JGitText.get().collisionOn, obj.name()));
 			}
+			pck = inflate(Source.DATABASE, sz);
+			while (0 < sz) {
+				int n = (int) Math.min(readBuffer.length, sz);
+				IO.readFully(cur, curBuffer, 0, n);
+				IO.readFully(pck, readBuffer, 0, n);
+				for (int i = 0; i < n; i++) {
+					if (curBuffer[i] != readBuffer[i]) {
+						throw new IOException(MessageFormat.format(JGitText
+								.get().collisionOn, obj.name()));
+					}
+				}
+				sz -= n;
+			}
+		} catch (MissingObjectException notLocal) {
+			// This is OK, we don't have a copy of the object locally
+			// but the API throws when we try to read it as usually its
+			// an error to read something that doesn't exist.
+		} finally {
+			if (pck != null) {
+				pck.close();
+			}
+		}
+	}
+
+	private void checkObjectCollision(AnyObjectId obj, int type, byte[] data)
+			throws IOException {
+		try {
+			final ObjectLoader ldr = readCurs.open(obj, type);
+			final byte[] existingData = ldr.getCachedBytes(data.length);
+			if (!Arrays.equals(data, existingData)) {
+				throw new IOException(MessageFormat.format(
+						JGitText.get().collisionOn, obj.name()));
+			}
+		} catch (MissingObjectException notLocal) {
+			// This is OK, we don't have a copy of the object locally
+			// but the API throws when we try to read it as usually its
+			// an error to read something that doesn't exist.
 		}
 	}
 
@@ -1250,6 +1278,23 @@
 	}
 
 	/**
+	 * Set the expected number of objects in the pack stream.
+	 * <p>
+	 * The object count in the pack header is not always correct for some Dfs
+	 * pack files. e.g. INSERT pack always assume 1 object in the header since
+	 * the actual object count is unknown when the pack is written.
+	 * <p>
+	 * If external implementation wants to overwrite the expectedObjectCount,
+	 * they should call this method during {@link #onPackHeader(long)}.
+	 *
+	 * @param expectedObjectCount
+	 * @since 4.9
+	 */
+	protected void setExpectedObjectCount(long expectedObjectCount) {
+		this.expectedObjectCount = expectedObjectCount;
+	}
+
+	/**
 	 * Store bytes received from the raw stream.
 	 * <p>
 	 * This method is invoked during {@link #parse(ProgressMonitor)} as data is
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackedObjectInfo.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackedObjectInfo.java
index 6da1c57..381c228 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackedObjectInfo.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackedObjectInfo.java
@@ -45,6 +45,7 @@
 package org.eclipse.jgit.transport;
 
 import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectIdOwnerMap;
 
 /**
@@ -59,6 +60,8 @@
 
 	private int crc;
 
+	private int type = Constants.OBJ_BAD;
+
 	PackedObjectInfo(final long headerOffset, final int packedCRC,
 			final AnyObjectId id) {
 		super(id);
@@ -112,4 +115,24 @@
 	public void setCRC(final int crc) {
 		this.crc = crc;
 	}
+
+	/**
+	 * @return the object type. The default type is OBJ_BAD, which is considered
+	 *         as unknown or invalid type.
+	 * @since 4.9
+	 */
+	public int getType() {
+		return type;
+	}
+
+	/**
+	 * Record the object type if applicable.
+	 *
+	 * @param type
+	 *            the object type.
+	 * @since 4.9
+	 */
+	public void setType(int type) {
+		this.type = type;
+	}
 }
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 2b21c4a..14b35c9 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceiveCommand.java
@@ -52,6 +52,7 @@
 import java.util.Collection;
 import java.util.List;
 
+import org.eclipse.jgit.annotations.Nullable;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.ObjectId;
@@ -190,6 +191,20 @@
 		}
 	}
 
+	/**
+	 * Check whether a command failed due to transaction aborted.
+	 *
+	 * @param cmd
+	 *            command.
+	 * @return whether the command failed due to transaction aborted, as in {@link
+	 *         #abort(Iterable)}.
+	 * @since 4.9
+	 */
+	public static boolean isTransactionAborted(ReceiveCommand cmd) {
+		return cmd.getResult() == REJECTED_OTHER_REASON
+				&& cmd.getMessage().equals(JGitText.get().transactionAborted);
+	}
+
 	private final ObjectId oldId;
 
 	private final ObjectId newId;
@@ -204,13 +219,19 @@
 
 	private String message;
 
+	private boolean customRefLog;
+
+	private String refLogMessage;
+
+	private boolean refLogIncludeResult;
+
 	private boolean typeIsCorrect;
 
 	/**
 	 * Create a new command for {@link BaseReceivePack}.
 	 *
 	 * @param oldId
-	 *            the old object id; must not be null. Use
+	 *            the expected old object id; must not be null. Use
 	 *            {@link ObjectId#zeroId()} to indicate a ref creation.
 	 * @param newId
 	 *            the new object id; must not be null. Use
@@ -220,15 +241,23 @@
 	 */
 	public ReceiveCommand(final ObjectId oldId, final ObjectId newId,
 			final String name) {
+		if (oldId == null) {
+			throw new IllegalArgumentException(JGitText.get().oldIdMustNotBeNull);
+		}
+		if (newId == null) {
+			throw new IllegalArgumentException(JGitText.get().newIdMustNotBeNull);
+		}
 		this.oldId = oldId;
 		this.newId = newId;
 		this.name = name;
 
 		type = Type.UPDATE;
-		if (ObjectId.zeroId().equals(oldId))
+		if (ObjectId.zeroId().equals(oldId)) {
 			type = Type.CREATE;
-		if (ObjectId.zeroId().equals(newId))
+		}
+		if (ObjectId.zeroId().equals(newId)) {
 			type = Type.DELETE;
+		}
 	}
 
 	/**
@@ -243,14 +272,45 @@
 	 * @param name
 	 *            name of the ref being affected.
 	 * @param type
-	 *            type of the command.
+	 *            type of the command. Must be {@link Type#CREATE} if {@code
+	 *            oldId} is zero, or {@link Type#DELETE} if {@code newId} is zero.
 	 * @since 2.0
 	 */
 	public ReceiveCommand(final ObjectId oldId, final ObjectId newId,
 			final String name, final Type type) {
+		if (oldId == null) {
+			throw new IllegalArgumentException(JGitText.get().oldIdMustNotBeNull);
+		}
+		if (newId == null) {
+			throw new IllegalArgumentException(JGitText.get().newIdMustNotBeNull);
+		}
 		this.oldId = oldId;
 		this.newId = newId;
 		this.name = name;
+		switch (type) {
+		case CREATE:
+			if (!ObjectId.zeroId().equals(oldId)) {
+				throw new IllegalArgumentException(
+						JGitText.get().createRequiresZeroOldId);
+			}
+			break;
+		case DELETE:
+			if (!ObjectId.zeroId().equals(newId)) {
+				throw new IllegalArgumentException(
+						JGitText.get().deleteRequiresZeroNewId);
+			}
+			break;
+		case UPDATE:
+		case UPDATE_NONFASTFORWARD:
+			if (ObjectId.zeroId().equals(newId)
+					|| ObjectId.zeroId().equals(oldId)) {
+				throw new IllegalArgumentException(
+						JGitText.get().updateRequiresOldIdAndNewId);
+			}
+			break;
+		default:
+			throw new IllegalStateException(JGitText.get().enumValueNotSupported0);
+		}
 		this.type = type;
 	}
 
@@ -290,6 +350,90 @@
 	}
 
 	/**
+	 * Set the message to include in the reflog.
+	 * <p>
+	 * Overrides the default set by {@code setRefLogMessage} on any containing
+	 * {@link org.eclipse.jgit.lib.BatchRefUpdate}.
+	 *
+	 * @param msg
+	 *            the message to describe this change. If null and appendStatus is
+	 *            false, the reflog will not be updated.
+	 * @param appendStatus
+	 *            true if the status of the ref change (fast-forward or
+	 *            forced-update) should be appended to the user supplied message.
+	 * @since 4.9
+	 */
+	public void setRefLogMessage(String msg, boolean appendStatus) {
+		customRefLog = true;
+		if (msg == null && !appendStatus) {
+			disableRefLog();
+		} else if (msg == null && appendStatus) {
+			refLogMessage = ""; //$NON-NLS-1$
+			refLogIncludeResult = true;
+		} else {
+			refLogMessage = msg;
+			refLogIncludeResult = appendStatus;
+		}
+	}
+
+	/**
+	 * Don't record this update in the ref's associated reflog.
+	 * <p>
+	 * Equivalent to {@code setRefLogMessage(null, false)}.
+	 *
+	 * @since 4.9
+	 */
+	public void disableRefLog() {
+		customRefLog = true;
+		refLogMessage = null;
+		refLogIncludeResult = false;
+	}
+
+	/**
+	 * Check whether this command has a custom reflog setting that should override
+	 * defaults in any containing {@link org.eclipse.jgit.lib.BatchRefUpdate}.
+	 *
+	 * @return whether a custom reflog is set.
+	 * @since 4.9
+	 */
+	public boolean hasCustomRefLog() {
+		return customRefLog;
+	}
+
+	/**
+	 * Check whether log has been disabled by {@link #disableRefLog()}.
+	 *
+	 * @return true if disabled.
+	 * @since 4.9
+	 */
+	public boolean isRefLogDisabled() {
+		return refLogMessage == null;
+	}
+
+	/**
+	 * Get the message to include in the reflog.
+	 *
+	 * @return message the caller wants to include in the reflog; null if the
+	 *         update should not be logged.
+	 * @since 4.9
+	 */
+	@Nullable
+	public String getRefLogMessage() {
+		return refLogMessage;
+	}
+
+	/**
+	 * Check whether the reflog message should include the result of the update,
+	 * such as fast-forward or force-update.
+	 *
+	 * @return true if the message should include the result.
+	 * @since 4.9
+	 */
+	public boolean isRefLogIncludingResult() {
+		return refLogIncludeResult;
+	}
+
+	/**
 	 * Set the status of this command.
 	 *
 	 * @param s
@@ -355,6 +499,7 @@
 		try {
 			final RefUpdate ru = rp.getRepository().updateRef(getRefName());
 			ru.setRefLogIdent(rp.getRefLogIdent());
+			ru.setRefLogMessage(refLogMessage, refLogIncludeResult);
 			switch (getType()) {
 			case DELETE:
 				if (!ObjectId.zeroId().equals(getOldId())) {
@@ -428,6 +573,14 @@
 			setResult(Result.REJECTED_CURRENT_BRANCH);
 			break;
 
+		case REJECTED_MISSING_OBJECT:
+			setResult(Result.REJECTED_MISSING_OBJECT);
+			break;
+
+		case REJECTED_OTHER_REASON:
+			setResult(Result.REJECTED_OTHER_REASON);
+			break;
+
 		default:
 			setResult(Result.REJECTED_OTHER_REASON, r.name());
 			break;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteSession.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteSession.java
index 5a73cf5..d6a2fe6 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteSession.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RemoteSession.java
@@ -83,4 +83,4 @@
 	 * Disconnect the remote session
 	 */
 	public void disconnect();
-}
\ No newline at end of file
+}
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 2198b50..d4cd1c3 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java
@@ -76,8 +76,24 @@
 		}
 	};
 
-	enum FsckMode {
-		ERROR, WARN, IGNORE;
+	/**
+	 * A git configuration value for how to handle a fsck failure of a particular kind.
+	 * Used in e.g. fsck.missingEmail.
+	 * @since 4.9
+	 */
+	public enum FsckMode {
+		/**
+		 * Treat it as an error (the default).
+		 */
+		ERROR,
+		/**
+		 * Issue a warning (in fact, jgit treats this like IGNORE, but git itself does warn).
+		 */
+		WARN,
+		/**
+		 * Ignore the error.
+		 */
+		IGNORE;
 	}
 
 	private final boolean fetchFsck;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java
index ddb2fbf..17af0b9 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java
@@ -1523,7 +1523,7 @@
 				walk.reset();
 
 				ObjectWalk ow = rw.toObjectWalkWithSameObjects();
-				pw.preparePack(pm, ow, wantAll, commonBase);
+				pw.preparePack(pm, ow, wantAll, commonBase, PackWriter.NONE);
 				rw = ow;
 			}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/InterIndexDiffFilter.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/InterIndexDiffFilter.java
index 1719416..2ea8228 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/InterIndexDiffFilter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/filter/InterIndexDiffFilter.java
@@ -102,4 +102,4 @@
 	public String toString() {
 		return "INTERINDEX_DIFF"; //$NON-NLS-1$
 	}
-}
\ No newline at end of file
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java
index 229355c..1cc39bd 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java
@@ -497,7 +497,13 @@
 			if (env != null) {
 				pb.environment().putAll(env);
 			}
-			Process p = pb.start();
+			Process p;
+			try {
+				p = pb.start();
+			} catch (IOException e) {
+				// Process failed to start
+				throw new CommandFailedException(-1, e.getMessage(), e);
+			}
 			p.getOutputStream().close();
 			GobblerThread gobbler = new GobblerThread(p, command, dir);
 			gobbler.start();
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/IntList.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/IntList.java
index 658dd06..0a3c846 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/IntList.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/IntList.java
@@ -71,6 +71,21 @@
 	}
 
 	/**
+	 * Check if an entry appears in this collection.
+	 *
+	 * @param value
+	 *            the value to search for.
+	 * @return true of {@code value} appears in this list.
+	 * @since 4.9
+	 */
+	public boolean contains(int value) {
+		for (int i = 0; i < count; i++)
+			if (entries[i] == value)
+				return true;
+		return false;
+	}
+
+	/**
 	 * @param i
 	 *            index to read, must be in the range [0, {@link #size()}).
 	 * @return the number at the specified index
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/RefList.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/RefList.java
index 1597817..ce4b7c7 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/RefList.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/RefList.java
@@ -338,10 +338,11 @@
 		 * Create an empty list with at least the specified capacity.
 		 *
 		 * @param capacity
-		 *            the new capacity.
+		 *            the new capacity; if zero or negative, behavior is the same as
+		 *            {@link #Builder()}.
 		 */
 		public Builder(int capacity) {
-			list = new Ref[capacity];
+			list = new Ref[Math.max(capacity, 16)];
 		}
 
 		/** @return number of items in this builder's internal collection. */
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/RelativeDateFormatter.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/RelativeDateFormatter.java
index 3cb3749..21a55a6 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/RelativeDateFormatter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/RelativeDateFormatter.java
@@ -140,4 +140,4 @@
 		long rounded = (n + unit / 2) / unit;
 		return rounded;
 	}
-}
\ No newline at end of file
+}
diff --git a/pom.xml b/pom.xml
index b26af96..3e6c55b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -51,7 +51,7 @@
   <groupId>org.eclipse.jgit</groupId>
   <artifactId>org.eclipse.jgit-parent</artifactId>
   <packaging>pom</packaging>
-  <version>4.8.0-SNAPSHOT</version>
+  <version>4.9.0-SNAPSHOT</version>
 
   <name>JGit - Parent</name>
   <url>${jgit-url}</url>
@@ -70,6 +70,10 @@
     <connection>scm:git:https://git.eclipse.org/r/jgit/jgit</connection>
   </scm>
 
+  <prerequisites>
+    <maven>3.3.1</maven>
+  </prerequisites>
+
   <ciManagement>
     <system>hudson</system>
     <url>https://hudson.eclipse.org/jgit/</url>
@@ -200,15 +204,15 @@
     <commons-compress-version>1.6</commons-compress-version>
     <osgi-core-version>4.3.1</osgi-core-version>
     <servlet-api-version>3.1.0</servlet-api-version>
-    <jetty-version>9.3.17.v20170317</jetty-version>
-    <japicmp-version>0.8.1</japicmp-version>
+    <jetty-version>9.4.5.v20170502</jetty-version>
+    <japicmp-version>0.10.0</japicmp-version>
     <httpclient-version>4.3.6</httpclient-version>
     <slf4j-version>1.7.2</slf4j-version>
     <log4j-version>1.2.15</log4j-version>
     <maven-javadoc-plugin-version>2.10.4</maven-javadoc-plugin-version>
     <tycho-extras-version>1.0.0</tycho-extras-version>
     <gson-version>2.2.4</gson-version>
-    <findbugs-maven-plugin-version>3.0.4</findbugs-maven-plugin-version>
+    <spotbugs-maven-plugin-version>3.0.6</spotbugs-maven-plugin-version>
     <maven-surefire-report-plugin-version>2.20</maven-surefire-report-plugin-version>
 
     <!-- Properties to enable jacoco code coverage analysis -->
@@ -367,9 +371,9 @@
         </plugin>
 
         <plugin>
-          <groupId>org.codehaus.mojo</groupId>
-          <artifactId>findbugs-maven-plugin</artifactId>
-          <version>${findbugs-maven-plugin-version}</version>
+          <groupId>com.github.hazendaz.spotbugs</groupId>
+          <artifactId>spotbugs-maven-plugin</artifactId>
+          <version>${spotbugs-maven-plugin-version}</version>
           <configuration>
             <findbugsXmlOutput>true</findbugsXmlOutput>
             <failOnError>false</failOnError>
@@ -386,7 +390,7 @@
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-pmd-plugin</artifactId>
-          <version>3.7</version>
+          <version>3.8</version>
           <configuration>
             <sourceEncoding>utf-8</sourceEncoding>
             <minimumTokens>100</minimumTokens>
@@ -575,9 +579,9 @@
         <version>2.5</version>
       </plugin>
       <plugin>
-        <groupId>org.codehaus.mojo</groupId>
-        <artifactId>findbugs-maven-plugin</artifactId>
-        <version>${findbugs-maven-plugin-version}</version>
+        <groupId>com.github.hazendaz.spotbugs</groupId>
+        <artifactId>spotbugs-maven-plugin</artifactId>
+        <version>${spotbugs-maven-plugin-version}</version>
       </plugin>
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
@@ -747,8 +751,8 @@
       <build>
         <plugins>
           <plugin>
-            <groupId>org.codehaus.mojo</groupId>
-            <artifactId>findbugs-maven-plugin</artifactId>
+            <groupId>com.github.hazendaz.spotbugs</groupId>
+            <artifactId>spotbugs-maven-plugin</artifactId>
           </plugin>
           <plugin>
             <groupId>org.apache.maven.plugins</groupId>