Merge branch 'stable-4.0' into stable-4.1

* stable-4.0:
  JGit v4.0.3.201509231615-r

Change-Id: Ie74b0392ef145ffd27dc903c45f7fec2d4492a17
Signed-off-by: David Pursehouse <david.pursehouse@gmail.com>
diff --git a/org.eclipse.jgit.ant.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.ant.test/META-INF/MANIFEST.MF
index 84e0927..83051f9 100644
--- a/org.eclipse.jgit.ant.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.ant.test/META-INF/MANIFEST.MF
@@ -3,14 +3,14 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %plugin_name
 Bundle-SymbolicName: org.eclipse.jgit.ant.test
-Bundle-Version: 4.0.3.201509231615-r
+Bundle-Version: 4.1.2.201602141800-r
 Bundle-ActivationPolicy: lazy
 Bundle-RequiredExecutionEnvironment: JavaSE-1.7
 Import-Package: org.apache.tools.ant,
- org.eclipse.jgit.ant.tasks;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.internal.storage.file;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.junit;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.lib;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.util;version="[4.0.3,4.1.0)",
+ org.eclipse.jgit.ant.tasks;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.internal.storage.file;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.junit;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.lib;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.util;version="[4.1.2,4.2.0)",
  org.hamcrest;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 b213b59..67e6c38 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.0.3.201509231615-r</version>
+    <version>4.1.2.201602141800-r</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 adea04e..5ff9fd5 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.0.3.201509231615-r
+Bundle-Version: 4.1.2.201602141800-r
 Bundle-RequiredExecutionEnvironment: JavaSE-1.7
 Import-Package: org.apache.tools.ant,
-  org.eclipse.jgit.storage.file;version="[4.0.3,4.1.0)"
+  org.eclipse.jgit.storage.file;version="[4.1.2,4.2.0)"
 Bundle-Localization: plugin
 Bundle-Vendor: %Provider-Name
-Export-Package: org.eclipse.jgit.ant.tasks;version="4.0.3";
+Export-Package: org.eclipse.jgit.ant.tasks;version="4.1.2";
  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 580bead..f40e0f7 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.0.3.201509231615-r</version>
+		<version>4.1.2.201602141800-r</version>
 	</parent>
 
 	<artifactId>org.eclipse.jgit.ant</artifactId>
diff --git a/org.eclipse.jgit.ant/src/org/eclipse/jgit/ant/tasks/GitAddTask.java b/org.eclipse.jgit.ant/src/org/eclipse/jgit/ant/tasks/GitAddTask.java
index c76ae2a..b9a8688 100644
--- a/org.eclipse.jgit.ant/src/org/eclipse/jgit/ant/tasks/GitAddTask.java
+++ b/org.eclipse.jgit.ant/src/org/eclipse/jgit/ant/tasks/GitAddTask.java
@@ -117,10 +117,10 @@
 		}
 
 		AddCommand gitAdd;
-		try {
-			Repository repo = new FileRepositoryBuilder().readEnvironment()
-					.findGitDir(src).build();
-			gitAdd = new Git(repo).add();
+		try (Repository repo = new FileRepositoryBuilder().readEnvironment()
+				.findGitDir(src).build();
+			Git git = new Git(repo);) {
+			gitAdd = git.add();
 		} catch (IOException e) {
 			throw new BuildException("Could not access repository " + src, e);
 		}
diff --git a/org.eclipse.jgit.ant/src/org/eclipse/jgit/ant/tasks/GitCheckoutTask.java b/org.eclipse.jgit.ant/src/org/eclipse/jgit/ant/tasks/GitCheckoutTask.java
index 14c4bc5..9962472 100644
--- a/org.eclipse.jgit.ant/src/org/eclipse/jgit/ant/tasks/GitCheckoutTask.java
+++ b/org.eclipse.jgit.ant/src/org/eclipse/jgit/ant/tasks/GitCheckoutTask.java
@@ -105,10 +105,10 @@
 	@Override
 	public void execute() throws BuildException {
 		CheckoutCommand checkout;
-		try {
-			Repository repo = new FileRepositoryBuilder().readEnvironment()
-					.findGitDir(src).build();
-			checkout = new Git(repo).checkout();
+		try (Repository repo = new FileRepositoryBuilder().readEnvironment()
+				.findGitDir(src).build();
+			Git git = new Git(repo)) {
+			checkout = git.checkout();
 		} catch (IOException e) {
 			throw new BuildException("Could not access repository " + src, e);
 		}
diff --git a/org.eclipse.jgit.archive/.settings/org.eclipse.jdt.core.prefs b/org.eclipse.jgit.archive/.settings/org.eclipse.jdt.core.prefs
index ff39d16..4e28e0b 100644
--- a/org.eclipse.jgit.archive/.settings/org.eclipse.jdt.core.prefs
+++ b/org.eclipse.jgit.archive/.settings/org.eclipse.jdt.core.prefs
@@ -1,10 +1,10 @@
 eclipse.preferences.version=1
-org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled
+org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=enabled
 org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore
 org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull
 org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault
 org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable
-org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled
+org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled
 org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
 org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate
 org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
@@ -67,11 +67,11 @@
 org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error
 org.eclipse.jdt.core.compiler.problem.nullReference=error
 org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error
-org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning
+org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore
 org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning
 org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore
 org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=error
-org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning
+org.eclipse.jdt.core.compiler.problem.potentialNullReference=error
 org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore
 org.eclipse.jdt.core.compiler.problem.rawTypeReference=ignore
 org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning
diff --git a/org.eclipse.jgit.archive/META-INF/MANIFEST.MF b/org.eclipse.jgit.archive/META-INF/MANIFEST.MF
index 7db8615..3787f86 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.0.3.201509231615-r
+Bundle-Version: 4.1.2.201602141800-r
 Bundle-Vendor: %provider_name
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-1.7
@@ -12,15 +12,16 @@
  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.0.3,4.1.0)",
- org.eclipse.jgit.lib;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.nls;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.util;version="[4.0.3,4.1.0)",
+ org.eclipse.jgit.api;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.lib;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.nls;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.util;version="[4.1.2,4.2.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.0.3";
+Export-Package: org.eclipse.jgit.archive;version="4.1.2";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.api,
    org.apache.commons.compress.archivers,
    org.osgi.framework"
+Require-Bundle: org.eclipse.jdt.annotation;bundle-version="[1.1.0,2.0.0)";resolution:=optional
diff --git a/org.eclipse.jgit.archive/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.archive/META-INF/SOURCE-MANIFEST.MF
index 06084e2..d1f674d 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.0.3.201509231615-r
-Eclipse-SourceBundle: org.eclipse.jgit.archive;version="4.0.3.201509231615-r";roots="."
+Bundle-Version: 4.1.2.201602141800-r
+Eclipse-SourceBundle: org.eclipse.jgit.archive;version="4.1.2.201602141800-r";roots="."
diff --git a/org.eclipse.jgit.archive/pom.xml b/org.eclipse.jgit.archive/pom.xml
index 5bb3eda..377d812 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.0.3.201509231615-r</version>
+    <version>4.1.2.201602141800-r</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 d30a17b..182262f 100644
--- a/org.eclipse.jgit.http.apache/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.http.apache/META-INF/MANIFEST.MF
@@ -2,10 +2,11 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %Bundle-Name
 Bundle-SymbolicName: org.eclipse.jgit.http.apache
-Bundle-Version: 4.0.3.201509231615-r
+Bundle-Version: 4.1.2.201602141800-r
 Bundle-RequiredExecutionEnvironment: JavaSE-1.7
 Bundle-Localization: plugin
 Bundle-Vendor: %Provider-Name
+Bundle-ActivationPolicy: lazy
 Import-Package: org.apache.http;version="[4.1.0,5.0.0)",
  org.apache.http.client;version="[4.1.0,5.0.0)",
  org.apache.http.client.methods;version="[4.1.0,5.0.0)",
@@ -18,12 +19,13 @@
  org.apache.http.impl.client;version="[4.1.0,5.0.0)",
  org.apache.http.impl.client.cache;version="[4.1.0,5.0.0)",
  org.apache.http.params;version="[4.1.0,5.0.0)",
- org.eclipse.jgit.nls;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.transport.http;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.util;version="[4.0.3,4.1.0)"
-Export-Package: org.eclipse.jgit.transport.http.apache;version="4.0.3";
+ org.eclipse.jgit.nls;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.transport.http;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.util;version="[4.1.2,4.2.0)"
+Export-Package: org.eclipse.jgit.transport.http.apache;version="4.1.2";
   uses:="org.eclipse.jgit.transport.http,
    javax.net.ssl,
    org.apache.http.client,
    org.apache.http.client.methods,
-   org.apache.http"
+   org.apache.http",
+ org.eclipse.jgit.transport.http.apache.internal;x-internal:=true
diff --git a/org.eclipse.jgit.http.apache/pom.xml b/org.eclipse.jgit.http.apache/pom.xml
index 37f3462..7510088 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.0.3.201509231615-r</version>
+		<version>4.1.2.201602141800-r</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 135525f..b66606c 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.0.3.201509231615-r
+Bundle-Version: 4.1.2.201602141800-r
 Bundle-Localization: plugin
 Bundle-Vendor: %provider_name
-Export-Package: org.eclipse.jgit.http.server;version="4.0.3",
- org.eclipse.jgit.http.server.glue;version="4.0.3";
+Export-Package: org.eclipse.jgit.http.server;version="4.1.2",
+ org.eclipse.jgit.http.server.glue;version="4.1.2";
   uses:="javax.servlet,javax.servlet.http",
- org.eclipse.jgit.http.server.resolver;version="4.0.3";
+ org.eclipse.jgit.http.server.resolver;version="4.1.2";
   uses:="org.eclipse.jgit.transport.resolver,
    org.eclipse.jgit.lib,
    org.eclipse.jgit.transport,
@@ -17,12 +17,12 @@
 Bundle-RequiredExecutionEnvironment: JavaSE-1.7
 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.0.3,4.1.0)",
- org.eclipse.jgit.internal.storage.dfs;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.internal.storage.file;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.lib;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.nls;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.revwalk;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.transport;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.transport.resolver;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.util;version="[4.0.3,4.1.0)"
+ org.eclipse.jgit.errors;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.internal.storage.dfs;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.internal.storage.file;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.lib;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.nls;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.revwalk;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.transport;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.transport.resolver;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.util;version="[4.1.2,4.2.0)"
diff --git a/org.eclipse.jgit.http.server/pom.xml b/org.eclipse.jgit.http.server/pom.xml
index 7e1b8ef..ea60b00 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.0.3.201509231615-r</version>
+    <version>4.1.2.201602141800-r</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.http.server</artifactId>
diff --git a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/AsIsFileFilter.java b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/AsIsFileFilter.java
index b2250e3..d33362b 100644
--- a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/AsIsFileFilter.java
+++ b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/AsIsFileFilter.java
@@ -80,14 +80,16 @@
 
 	public void doFilter(ServletRequest request, ServletResponse response,
 			FilterChain chain) throws IOException, ServletException {
+		HttpServletRequest req = (HttpServletRequest) request;
+		HttpServletResponse res = (HttpServletResponse) response;
 		try {
 			final Repository db = getRepository(request);
-			asIs.access((HttpServletRequest) request, db);
+			asIs.access(req, db);
 			chain.doFilter(request, response);
 		} catch (ServiceNotAuthorizedException e) {
-			((HttpServletResponse) response).sendError(SC_UNAUTHORIZED);
+			res.sendError(SC_UNAUTHORIZED, e.getMessage());
 		} catch (ServiceNotEnabledException e) {
-			((HttpServletResponse) response).sendError(SC_FORBIDDEN);
+			res.sendError(SC_FORBIDDEN, e.getMessage());
 		}
 	}
 }
diff --git a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/ReceivePackServlet.java b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/ReceivePackServlet.java
index 030287b..c88670e 100644
--- a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/ReceivePackServlet.java
+++ b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/ReceivePackServlet.java
@@ -76,6 +76,7 @@
 import javax.servlet.http.HttpServletResponse;
 
 import org.eclipse.jgit.errors.CorruptObjectException;
+import org.eclipse.jgit.errors.PackProtocolException;
 import org.eclipse.jgit.errors.UnpackException;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.transport.InternalHttpServerGlue;
@@ -137,11 +138,10 @@
 			try {
 				rp = receivePackFactory.create(req, getRepository(req));
 			} catch (ServiceNotAuthorizedException e) {
-				rsp.sendError(SC_UNAUTHORIZED);
+				rsp.sendError(SC_UNAUTHORIZED, e.getMessage());
 				return;
-
 			} catch (ServiceNotEnabledException e) {
-				sendError(req, rsp, SC_FORBIDDEN);
+				sendError(req, rsp, SC_FORBIDDEN, e.getMessage());
 				return;
 			}
 
@@ -201,7 +201,7 @@
 			consumeRequestBody(req);
 			out.close();
 
-		} catch (UnpackException e) {
+		} catch (UnpackException | PackProtocolException e) {
 			// This should be already reported to the client.
 			log(rp.getRepository(), e.getCause());
 			consumeRequestBody(req);
diff --git a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/RepositoryFilter.java b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/RepositoryFilter.java
index 5840402..a021c1f 100644
--- a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/RepositoryFilter.java
+++ b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/RepositoryFilter.java
@@ -137,10 +137,10 @@
 			sendError(req, res, SC_NOT_FOUND);
 			return;
 		} catch (ServiceNotEnabledException e) {
-			sendError(req, res, SC_FORBIDDEN);
+			sendError(req, res, SC_FORBIDDEN, e.getMessage());
 			return;
 		} catch (ServiceNotAuthorizedException e) {
-			res.sendError(SC_UNAUTHORIZED);
+			res.sendError(SC_UNAUTHORIZED, e.getMessage());
 			return;
 		} catch (ServiceMayNotContinueException e) {
 			sendError(req, res, SC_FORBIDDEN, e.getMessage());
diff --git a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/SmartServiceInfoRefs.java b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/SmartServiceInfoRefs.java
index 5133219..7d4f21b 100644
--- a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/SmartServiceInfoRefs.java
+++ b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/SmartServiceInfoRefs.java
@@ -98,7 +98,7 @@
 			try {
 				begin(req, db);
 			} catch (ServiceNotAuthorizedException e) {
-				res.sendError(SC_UNAUTHORIZED);
+				res.sendError(SC_UNAUTHORIZED, e.getMessage());
 				return;
 			} catch (ServiceNotEnabledException e) {
 				sendError(req, res, SC_FORBIDDEN, e.getMessage());
@@ -132,11 +132,9 @@
 			advertise(req, new PacketLineOutRefAdvertiser(out));
 			buf.close();
 		} catch (ServiceNotAuthorizedException e) {
-			res.sendError(SC_UNAUTHORIZED);
-
+			res.sendError(SC_UNAUTHORIZED, e.getMessage());
 		} catch (ServiceNotEnabledException e) {
-			sendError(req, res, SC_FORBIDDEN);
-
+			sendError(req, res, SC_FORBIDDEN, e.getMessage());
 		} catch (ServiceMayNotContinueException e) {
 			if (e.isOutput())
 				buf.close();
diff --git a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/UploadPackServlet.java b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/UploadPackServlet.java
index 44d4b1b..8c27b71 100644
--- a/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/UploadPackServlet.java
+++ b/org.eclipse.jgit.http.server/src/org/eclipse/jgit/http/server/UploadPackServlet.java
@@ -137,11 +137,10 @@
 			try {
 				rp = uploadPackFactory.create(req, getRepository(req));
 			} catch (ServiceNotAuthorizedException e) {
-				rsp.sendError(SC_UNAUTHORIZED);
+				rsp.sendError(SC_UNAUTHORIZED, e.getMessage());
 				return;
-
 			} catch (ServiceNotEnabledException e) {
-				sendError(req, rsp, SC_FORBIDDEN);
+				sendError(req, rsp, SC_FORBIDDEN, e.getMessage());
 				return;
 			}
 
diff --git a/org.eclipse.jgit.http.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.http.test/META-INF/MANIFEST.MF
index ad965a9..a5bf31a 100644
--- a/org.eclipse.jgit.http.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.http.test/META-INF/MANIFEST.MF
@@ -2,7 +2,7 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %plugin_name
 Bundle-SymbolicName: org.eclipse.jgit.http.test
-Bundle-Version: 4.0.3.201509231615-r
+Bundle-Version: 4.1.2.201602141800-r
 Bundle-Vendor: %provider_name
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-1.7
@@ -22,23 +22,23 @@
  org.eclipse.jetty.util.log;version="[9.0.0,10.0.0)",
  org.eclipse.jetty.util.security;version="[9.0.0,10.0.0)",
  org.eclipse.jetty.util.thread;version="[9.0.0,10.0.0)",
- org.eclipse.jgit.errors;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.http.server;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.http.server.glue;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.http.server.resolver;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.internal;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.internal.storage.file;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.junit;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.junit.http;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.lib;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.nls;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.revwalk;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.storage.file;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.transport;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.transport.http;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.transport.http.apache;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.transport.resolver;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.util;version="[4.0.3,4.1.0)",
+ org.eclipse.jgit.errors;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.http.server;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.http.server.glue;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.http.server.resolver;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.internal;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.internal.storage.file;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.junit;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.junit.http;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.lib;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.nls;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.revwalk;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.storage.file;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.transport;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.transport.http;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.transport.http.apache;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.transport.resolver;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.util;version="[4.1.2,4.2.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 5631230..17128ed 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.0.3.201509231615-r</version>
+    <version>4.1.2.201602141800-r</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 c63e16c..8a5ee6c 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.0.3.201509231615-r
+Bundle-Version: 4.1.2.201602141800-r
 Bundle-Localization: plugin
 Bundle-Vendor: %provider_name
 Bundle-ActivationPolicy: lazy
@@ -20,13 +20,23 @@
  org.eclipse.jetty.util.component;version="[9.0.0,10.0.0)",
  org.eclipse.jetty.util.log;version="[9.0.0,10.0.0)",
  org.eclipse.jetty.util.security;version="[9.0.0,10.0.0)",
- org.eclipse.jgit.errors;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.http.server;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.internal.storage.file;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.junit;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.lib;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.revwalk;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.transport;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.transport.resolver;version="[4.0.3,4.1.0)",
+ org.eclipse.jgit.errors;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.http.server;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.internal.storage.file;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.junit;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.lib;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.revwalk;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.transport;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.transport.resolver;version="[4.1.2,4.2.0)",
  org.junit;version="[4.0.0,5.0.0)"
-Export-Package: org.eclipse.jgit.junit.http;version="4.0.3"
+Export-Package: org.eclipse.jgit.junit.http;version="4.1.2";
+  uses:="org.eclipse.jgit.transport,
+   org.eclipse.jgit.junit,
+   javax.servlet.http,
+   org.eclipse.jgit.lib,
+   org.eclipse.jgit.revwalk,
+   org.eclipse.jetty.server.handler,
+   javax.servlet,
+   org.eclipse.jetty.server,
+   org.eclipse.jetty.util.log,
+   org.eclipse.jetty.servlet"
diff --git a/org.eclipse.jgit.junit.http/pom.xml b/org.eclipse.jgit.junit.http/pom.xml
index 38e2594..d25ae00 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.0.3.201509231615-r</version>
+    <version>4.1.2.201602141800-r</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.junit.http</artifactId>
diff --git a/org.eclipse.jgit.junit/META-INF/MANIFEST.MF b/org.eclipse.jgit.junit/META-INF/MANIFEST.MF
index 2ca7e3a..ea32b1b 100644
--- a/org.eclipse.jgit.junit/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.junit/META-INF/MANIFEST.MF
@@ -2,24 +2,32 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %plugin_name
 Bundle-SymbolicName: org.eclipse.jgit.junit
-Bundle-Version: 4.0.3.201509231615-r
+Bundle-Version: 4.1.2.201602141800-r
 Bundle-Localization: plugin
 Bundle-Vendor: %provider_name
 Bundle-ActivationPolicy: lazy
 Bundle-RequiredExecutionEnvironment: JavaSE-1.7
-Import-Package: org.eclipse.jgit.api;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.api.errors;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.dircache;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.errors;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.internal.storage.file;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.internal.storage.pack;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.lib;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.merge;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.revwalk;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.storage.file;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.treewalk;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.treewalk.filter;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.util;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.util.io;version="[4.0.3,4.1.0)",
+Import-Package: org.eclipse.jgit.api;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.api.errors;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.dircache;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.errors;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.internal.storage.file;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.internal.storage.pack;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.lib;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.merge;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.revwalk;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.storage.file;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.treewalk;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.treewalk.filter;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.util;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.util.io;version="[4.1.2,4.2.0)",
  org.junit;version="[4.0.0,5.0.0)"
-Export-Package: org.eclipse.jgit.junit;version="4.0.3"
+Export-Package: org.eclipse.jgit.junit;version="4.1.2";
+  uses:="org.eclipse.jgit.dircache,
+   org.eclipse.jgit.lib,
+   org.eclipse.jgit.revwalk,
+   org.eclipse.jgit.internal.storage.file,
+   org.eclipse.jgit.treewalk,
+   org.eclipse.jgit.util,
+   org.eclipse.jgit.storage.file,
+   org.eclipse.jgit.api"
diff --git a/org.eclipse.jgit.junit/pom.xml b/org.eclipse.jgit.junit/pom.xml
index 25f88f8..d3616a7 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.0.3.201509231615-r</version>
+    <version>4.1.2.201602141800-r</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.junit</artifactId>
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 b98db7d..b5348f9 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
@@ -50,18 +50,13 @@
 
 import java.io.File;
 import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.concurrent.TimeUnit;
 
+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.Constants;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.lib.RepositoryCache;
+import org.eclipse.jgit.lib.*;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
 import org.eclipse.jgit.storage.file.WindowCacheConfig;
 import org.eclipse.jgit.util.FS;
@@ -229,6 +224,102 @@
 			System.err.println(msg);
 	}
 
+	public static final int MOD_TIME = 1;
+
+	public static final int SMUDGE = 2;
+
+	public static final int LENGTH = 4;
+
+	public static final int CONTENT_ID = 8;
+
+	public static final int CONTENT = 16;
+
+	public static final int ASSUME_UNCHANGED = 32;
+
+	/**
+	 * Represent the state of the index in one String. This representation is
+	 * useful when writing tests which do assertions on the state of the index.
+	 * By default information about path, mode, stage (if different from 0) is
+	 * included. A bitmask controls which additional info about
+	 * modificationTimes, smudge state and length is included.
+	 * <p>
+	 * The format of the returned string is described with this BNF:
+	 *
+	 * <pre>
+	 * result = ( "[" path mode stage? time? smudge? length? sha1? content? "]" )* .
+	 * mode = ", mode:" number .
+	 * stage = ", stage:" number .
+	 * time = ", time:t" timestamp-index .
+	 * smudge = "" | ", smudged" .
+	 * length = ", length:" number .
+	 * sha1 = ", sha1:" hex-sha1 .
+	 * content = ", content:" blob-data .
+	 * </pre>
+	 *
+	 * 'stage' is only presented when the stage is different from 0. All
+	 * reported time stamps are mapped to strings like "t0", "t1", ... "tn". The
+	 * smallest reported time-stamp will be called "t0". This allows to write
+	 * assertions against the string although the concrete value of the time
+	 * stamps is unknown.
+	 *
+	 * @param repo
+	 *            the repository the index state should be determined for
+	 *
+	 * @param includedOptions
+	 *            a bitmask constructed out of the constants {@link #MOD_TIME},
+	 *            {@link #SMUDGE}, {@link #LENGTH}, {@link #CONTENT_ID} and
+	 *            {@link #CONTENT} controlling which info is present in the
+	 *            resulting string.
+	 * @return a string encoding the index state
+	 * @throws IllegalStateException
+	 * @throws IOException
+	 */
+	public static String indexState(Repository repo, int includedOptions)
+			throws IllegalStateException, IOException {
+		DirCache dc = repo.readDirCache();
+		StringBuilder sb = new StringBuilder();
+		TreeSet<Long> timeStamps = null;
+
+		// iterate once over the dircache just to collect all time stamps
+		if (0 != (includedOptions & MOD_TIME)) {
+			timeStamps = new TreeSet<Long>();
+			for (int i=0; i<dc.getEntryCount(); ++i)
+				timeStamps.add(Long.valueOf(dc.getEntry(i).getLastModified()));
+		}
+
+		// iterate again, now produce the result string
+		for (int i=0; i<dc.getEntryCount(); ++i) {
+			DirCacheEntry entry = dc.getEntry(i);
+			sb.append("["+entry.getPathString()+", mode:" + entry.getFileMode());
+			int stage = entry.getStage();
+			if (stage != 0)
+				sb.append(", stage:" + stage);
+			if (0 != (includedOptions & MOD_TIME)) {
+				sb.append(", time:t"+
+						timeStamps.headSet(Long.valueOf(entry.getLastModified())).size());
+			}
+			if (0 != (includedOptions & SMUDGE))
+				if (entry.isSmudged())
+					sb.append(", smudged");
+			if (0 != (includedOptions & LENGTH))
+				sb.append(", length:"
+						+ Integer.toString(entry.getLength()));
+			if (0 != (includedOptions & CONTENT_ID))
+				sb.append(", sha1:" + ObjectId.toString(entry.getObjectId()));
+			if (0 != (includedOptions & CONTENT)) {
+				sb.append(", content:"
+						+ new String(repo.open(entry.getObjectId(),
+						Constants.OBJ_BLOB).getCachedBytes(), "UTF-8"));
+			}
+			if (0 != (includedOptions & ASSUME_UNCHANGED))
+				sb.append(", assume-unchanged:"
+						+ Boolean.toString(entry.isAssumeValid()));
+			sb.append("]");
+		}
+		return sb.toString();
+	}
+
+
 	/**
 	 * Creates a new empty bare repository.
 	 *
diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/MockSystemReader.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/MockSystemReader.java
index 65551d6..d24dd44 100644
--- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/MockSystemReader.java
+++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/MockSystemReader.java
@@ -47,6 +47,7 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.lang.reflect.Field;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
 import java.util.HashMap;
@@ -170,6 +171,7 @@
 	 * Assign some properties for the currently executing platform
 	 */
 	public void setCurrentPlatform() {
+		resetOsNames();
 		setProperty("os.name", System.getProperty("os.name"));
 		setProperty("file.separator", System.getProperty("file.separator"));
 		setProperty("path.separator", System.getProperty("path.separator"));
@@ -180,6 +182,7 @@
 	 * Emulate Windows
 	 */
 	public void setWindows() {
+		resetOsNames();
 		setProperty("os.name", "Windows");
 		setProperty("file.separator", "\\");
 		setProperty("path.separator", ";");
@@ -191,10 +194,25 @@
 	 * Emulate Unix
 	 */
 	public void setUnix() {
+		resetOsNames();
 		setProperty("os.name", "*nix"); // Essentially anything but Windows
 		setProperty("file.separator", "/");
 		setProperty("path.separator", ":");
 		setProperty("line.separator", "\n");
 		setPlatformChecker();
 	}
+
+	private void resetOsNames() {
+		Field field;
+		try {
+			field = SystemReader.class.getDeclaredField("isWindows");
+			field.setAccessible(true);
+			field.set(null, null);
+			field = SystemReader.class.getDeclaredField("isMacOS");
+			field.setAccessible(true);
+			field.set(null, null);
+		} catch (Exception e) {
+			e.printStackTrace();
+		}
+	}
 }
diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepositoryTestCase.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepositoryTestCase.java
index 83148d0..ac4539a 100644
--- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepositoryTestCase.java
+++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/RepositoryTestCase.java
@@ -56,11 +56,9 @@
 import java.io.InputStreamReader;
 import java.io.Reader;
 import java.util.Map;
-import java.util.TreeSet;
 
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.errors.GitAPIException;
-import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.dircache.DirCacheBuilder;
 import org.eclipse.jgit.dircache.DirCacheCheckout;
 import org.eclipse.jgit.dircache.DirCacheEntry;
@@ -154,101 +152,6 @@
 		trash = db.getWorkTree();
 	}
 
-	public static final int MOD_TIME = 1;
-
-	public static final int SMUDGE = 2;
-
-	public static final int LENGTH = 4;
-
-	public static final int CONTENT_ID = 8;
-
-	public static final int CONTENT = 16;
-
-	public static final int ASSUME_UNCHANGED = 32;
-
-	/**
-	 * Represent the state of the index in one String. This representation is
-	 * useful when writing tests which do assertions on the state of the index.
-	 * By default information about path, mode, stage (if different from 0) is
-	 * included. A bitmask controls which additional info about
-	 * modificationTimes, smudge state and length is included.
-	 * <p>
-	 * The format of the returned string is described with this BNF:
-	 *
-	 * <pre>
-	 * result = ( "[" path mode stage? time? smudge? length? sha1? content? "]" )* .
-	 * mode = ", mode:" number .
-	 * stage = ", stage:" number .
-	 * time = ", time:t" timestamp-index .
-	 * smudge = "" | ", smudged" .
-	 * length = ", length:" number .
-	 * sha1 = ", sha1:" hex-sha1 .
-	 * content = ", content:" blob-data .
-	 * </pre>
-	 *
-	 * 'stage' is only presented when the stage is different from 0. All
-	 * reported time stamps are mapped to strings like "t0", "t1", ... "tn". The
-	 * smallest reported time-stamp will be called "t0". This allows to write
-	 * assertions against the string although the concrete value of the time
-	 * stamps is unknown.
-	 *
-	 * @param repo
-	 *            the repository the index state should be determined for
-	 *
-	 * @param includedOptions
-	 *            a bitmask constructed out of the constants {@link #MOD_TIME},
-	 *            {@link #SMUDGE}, {@link #LENGTH}, {@link #CONTENT_ID} and
-	 *            {@link #CONTENT} controlling which info is present in the
-	 *            resulting string.
-	 * @return a string encoding the index state
-	 * @throws IllegalStateException
-	 * @throws IOException
-	 */
-	public String indexState(Repository repo, int includedOptions)
-			throws IllegalStateException, IOException {
-		DirCache dc = repo.readDirCache();
-		StringBuilder sb = new StringBuilder();
-		TreeSet<Long> timeStamps = null;
-
-		// iterate once over the dircache just to collect all time stamps
-		if (0 != (includedOptions & MOD_TIME)) {
-			timeStamps = new TreeSet<Long>();
-			for (int i=0; i<dc.getEntryCount(); ++i)
-				timeStamps.add(Long.valueOf(dc.getEntry(i).getLastModified()));
-		}
-
-		// iterate again, now produce the result string
-		for (int i=0; i<dc.getEntryCount(); ++i) {
-			DirCacheEntry entry = dc.getEntry(i);
-			sb.append("["+entry.getPathString()+", mode:" + entry.getFileMode());
-			int stage = entry.getStage();
-			if (stage != 0)
-				sb.append(", stage:" + stage);
-			if (0 != (includedOptions & MOD_TIME)) {
-				sb.append(", time:t"+
-					timeStamps.headSet(Long.valueOf(entry.getLastModified())).size());
-			}
-			if (0 != (includedOptions & SMUDGE))
-				if (entry.isSmudged())
-					sb.append(", smudged");
-			if (0 != (includedOptions & LENGTH))
-				sb.append(", length:"
-						+ Integer.toString(entry.getLength()));
-			if (0 != (includedOptions & CONTENT_ID))
-				sb.append(", sha1:" + ObjectId.toString(entry.getObjectId()));
-			if (0 != (includedOptions & CONTENT)) {
-				sb.append(", content:"
-						+ new String(db.open(entry.getObjectId(),
-								Constants.OBJ_BLOB).getCachedBytes(), "UTF-8"));
-			}
-			if (0 != (includedOptions & ASSUME_UNCHANGED))
-				sb.append(", assume-unchanged:"
-						+ Boolean.toString(entry.isAssumeValid()));
-			sb.append("]");
-		}
-		return sb.toString();
-	}
-
 	/**
 	 * Represent the state of the index in one String. This representation is
 	 * useful when writing tests which do assertions on the state of the index.
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 59c6a2e..09b59b0 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.0.3.201509231615-r"
+      version="4.1.2.201602141800-r"
       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 ec7fd40..ce0f9b9 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.0.3.201509231615-r</version>
+    <version>4.1.2.201602141800-r</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 2ec128a..31295cf 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.0.3.201509231615-r"
+      version="4.1.2.201602141800-r"
       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 313d526..350de3c 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.0.3.201509231615-r</version>
+    <version>4.1.2.201602141800-r</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 52dc5f5..cb9fbf3 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.0.3.201509231615-r"
+      version="4.1.2.201602141800-r"
       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 ac6d5ba..cb76911 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.0.3.201509231615-r</version>
+    <version>4.1.2.201602141800-r</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 f1b8cea..e1dcfcd 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.0.3.201509231615-r"
+      version="4.1.2.201602141800-r"
       provider-name="%providerName">
 
    <description url="http://www.eclipse.org/jgit/">
@@ -27,7 +27,7 @@
          version="0.0.0"/>
 
    <requires>
-      <import feature="org.eclipse.jgit" version="4.0.0" match="equivalent"/>
+      <import feature="org.eclipse.jgit" version="4.1.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 0dedec3..9076053 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.0.3.201509231615-r</version>
+    <version>4.1.2.201602141800-r</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 381178b..bbd0aa0 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.0.3.201509231615-r"
+      version="4.1.2.201602141800-r"
       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 551835a..0e64da4 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.0.3.201509231615-r</version>
+    <version>4.1.2.201602141800-r</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 135f9ec..88ad032 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.0.3.201509231615-r</version>
+    <version>4.1.2.201602141800-r</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 b10c53f..17030ff 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.0.3.201509231615-r"
+      version="4.1.2.201602141800-r"
       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 e6d565d..aa7128e 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.0.3.201509231615-r</version>
+    <version>4.1.2.201602141800-r</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 3464b30..38732b5 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.0.3.201509231615-r
+Bundle-Version: 4.1.2.201602141800-r
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.3.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.3.target
index decc554..e8670e7 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.3.target
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.3.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.3" sequenceNumber="1436830977">
+<target name="jgit-4.3" sequenceNumber="1440024094">
   <locations>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="org.eclipse.jetty.client" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.client.source" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.continuation" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.continuation.source" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.http" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.http.source" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.io" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.io.source" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.security" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.security.source" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.server" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.server.source" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.servlet" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.servlet.source" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.util" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.util.source" version="9.2.10.v20150310"/>
-      <repository id="jetty-9.2.10" location="http://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.2.10.v20150310/"/>
+      <unit id="org.eclipse.jetty.client" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.client.source" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.continuation" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.continuation.source" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.http" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.http.source" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.io" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.io.source" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.security" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.security.source" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.server" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.server.source" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.servlet" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.servlet.source" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.util" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.util.source" version="9.2.13.v20150730"/>
+      <repository id="jetty-9.2.13" location="http://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.2.13.v20150730/"/>
     </location>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
       <unit id="org.apache.ant" version="1.9.2.v201404171502"/>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.3.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.3.tpd
index f038aa0..062e930 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.3.tpd
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.3.tpd
@@ -1,6 +1,6 @@
 target "jgit-4.3" with source configurePhase
 
-include "projects/jetty-9.2.10.tpd"
+include "projects/jetty-9.2.13.tpd"
 include "orbit/R20150124073747-Luna-SR2.tpd"
 
 location "http://download.eclipse.org/releases/kepler/" {
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.4.target b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.4.target
index c7172fe..0b85d75 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.4.target
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.4.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.4" sequenceNumber="1436830965">
+<target name="jgit-4.4" sequenceNumber="1440024079">
   <locations>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="org.eclipse.jetty.client" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.client.source" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.continuation" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.continuation.source" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.http" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.http.source" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.io" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.io.source" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.security" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.security.source" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.server" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.server.source" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.servlet" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.servlet.source" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.util" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.util.source" version="9.2.10.v20150310"/>
-      <repository id="jetty-9.2.10" location="http://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.2.10.v20150310/"/>
+      <unit id="org.eclipse.jetty.client" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.client.source" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.continuation" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.continuation.source" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.http" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.http.source" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.io" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.io.source" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.security" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.security.source" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.server" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.server.source" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.servlet" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.servlet.source" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.util" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.util.source" version="9.2.13.v20150730"/>
+      <repository id="jetty-9.2.13" location="http://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.2.13.v20150730/"/>
     </location>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
       <unit id="org.apache.ant" version="1.9.2.v201404171502"/>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.4.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.4.tpd
index 0760bcd..9b9558f 100644
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.4.tpd
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/jgit-4.4.tpd
@@ -1,6 +1,6 @@
 target "jgit-4.4" with source configurePhase
 
-include "projects/jetty-9.2.10.tpd"
+include "projects/jetty-9.2.13.tpd"
 include "orbit/R20150124073747-Luna-SR2.tpd"
 
 location "http://download.eclipse.org/releases/luna/" {
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 030bbc2..2067482 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="1441320200">
+<target name="jgit-4.5" sequenceNumber="1440022750">
   <locations>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
-      <unit id="org.eclipse.jetty.client" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.client.source" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.continuation" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.continuation.source" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.http" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.http.source" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.io" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.io.source" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.security" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.security.source" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.server" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.server.source" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.servlet" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.servlet.source" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.util" version="9.2.10.v20150310"/>
-      <unit id="org.eclipse.jetty.util.source" version="9.2.10.v20150310"/>
-      <repository id="jetty-9.2.10" location="http://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.2.10.v20150310/"/>
+      <unit id="org.eclipse.jetty.client" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.client.source" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.continuation" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.continuation.source" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.http" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.http.source" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.io" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.io.source" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.security" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.security.source" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.server" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.server.source" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.servlet" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.servlet.source" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.util" version="9.2.13.v20150730"/>
+      <unit id="org.eclipse.jetty.util.source" version="9.2.13.v20150730"/>
+      <repository id="jetty-9.2.13" location="http://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.2.13.v20150730/"/>
     </location>
     <location includeMode="slicer" includeAllPlatforms="false" includeSource="true" includeConfigurePhase="true" type="InstallableUnit">
       <unit id="org.apache.ant" version="1.9.4.v201504302020"/>
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 93ffef3..cdf24b5 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,6 +1,6 @@
 target "jgit-4.5" with source configurePhase
 
-include "projects/jetty-9.2.10.tpd"
+include "projects/jetty-9.2.13.tpd"
 include "orbit/R20150821153341-Mars.tpd"
 
 location "http://download.eclipse.org/releases/mars/" {
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 278bca0..9b565ec 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.0.3.201509231615-r</version>
+    <version>4.1.2.201602141800-r</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.target</artifactId>
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/projects/jetty-9.2.10.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/projects/jetty-9.2.10.tpd
deleted file mode 100644
index 5a7ee2e..0000000
--- a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/projects/jetty-9.2.10.tpd
+++ /dev/null
@@ -1,20 +0,0 @@
-target "jetty-9.2.10" with source configurePhase
-
-location jetty-9.2.10 "http://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.2.10.v20150310/" {
-	org.eclipse.jetty.client [9.2.10.v20150310,9.2.10.v20150310]
-	org.eclipse.jetty.client.source [9.2.10.v20150310,9.2.10.v20150310]
-	org.eclipse.jetty.continuation [9.2.10.v20150310,9.2.10.v20150310]
-	org.eclipse.jetty.continuation.source [9.2.10.v20150310,9.2.10.v20150310]
-	org.eclipse.jetty.http [9.2.10.v20150310,9.2.10.v20150310]
-	org.eclipse.jetty.http.source [9.2.10.v20150310,9.2.10.v20150310]
-	org.eclipse.jetty.io [9.2.10.v20150310,9.2.10.v20150310]
-	org.eclipse.jetty.io.source [9.2.10.v20150310,9.2.10.v20150310]
-	org.eclipse.jetty.security [9.2.10.v20150310,9.2.10.v20150310]
-	org.eclipse.jetty.security.source [9.2.10.v20150310,9.2.10.v20150310]
-	org.eclipse.jetty.server [9.2.10.v20150310,9.2.10.v20150310]
-	org.eclipse.jetty.server.source [9.2.10.v20150310,9.2.10.v20150310]
-	org.eclipse.jetty.servlet [9.2.10.v20150310,9.2.10.v20150310]
-	org.eclipse.jetty.servlet.source [9.2.10.v20150310,9.2.10.v20150310]
-	org.eclipse.jetty.util [9.2.10.v20150310,9.2.10.v20150310]
-	org.eclipse.jetty.util.source [9.2.10.v20150310,9.2.10.v20150310]
-}
diff --git a/org.eclipse.jgit.packaging/org.eclipse.jgit.target/projects/jetty-9.2.13.tpd b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/projects/jetty-9.2.13.tpd
new file mode 100644
index 0000000..289a73d
--- /dev/null
+++ b/org.eclipse.jgit.packaging/org.eclipse.jgit.target/projects/jetty-9.2.13.tpd
@@ -0,0 +1,20 @@
+target "jetty-9.2.13" with source configurePhase
+
+location jetty-9.2.13 "http://download.eclipse.org/jetty/updates/jetty-bundles-9.x/9.2.13.v20150730/" {
+	org.eclipse.jetty.client [9.2.13.v20150730,9.2.13.v20150730]
+	org.eclipse.jetty.client.source [9.2.13.v20150730,9.2.13.v20150730]
+	org.eclipse.jetty.continuation [9.2.13.v20150730,9.2.13.v20150730]
+	org.eclipse.jetty.continuation.source [9.2.13.v20150730,9.2.13.v20150730]
+	org.eclipse.jetty.http [9.2.13.v20150730,9.2.13.v20150730]
+	org.eclipse.jetty.http.source [9.2.13.v20150730,9.2.13.v20150730]
+	org.eclipse.jetty.io [9.2.13.v20150730,9.2.13.v20150730]
+	org.eclipse.jetty.io.source [9.2.13.v20150730,9.2.13.v20150730]
+	org.eclipse.jetty.security [9.2.13.v20150730,9.2.13.v20150730]
+	org.eclipse.jetty.security.source [9.2.13.v20150730,9.2.13.v20150730]
+	org.eclipse.jetty.server [9.2.13.v20150730,9.2.13.v20150730]
+	org.eclipse.jetty.server.source [9.2.13.v20150730,9.2.13.v20150730]
+	org.eclipse.jetty.servlet [9.2.13.v20150730,9.2.13.v20150730]
+	org.eclipse.jetty.servlet.source [9.2.13.v20150730,9.2.13.v20150730]
+	org.eclipse.jetty.util [9.2.13.v20150730,9.2.13.v20150730]
+	org.eclipse.jetty.util.source [9.2.13.v20150730,9.2.13.v20150730]
+}
diff --git a/org.eclipse.jgit.packaging/pom.xml b/org.eclipse.jgit.packaging/pom.xml
index 2a3fcb2..3b5b7d8 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.0.3.201509231615-r</version>
+  <version>4.1.2.201602141800-r</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 8d61863..1693bee 100644
--- a/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.pgm.test/META-INF/MANIFEST.MF
@@ -2,27 +2,27 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %plugin_name
 Bundle-SymbolicName: org.eclipse.jgit.pgm.test
-Bundle-Version: 4.0.3.201509231615-r
+Bundle-Version: 4.1.2.201602141800-r
 Bundle-Vendor: %provider_name
 Bundle-Localization: plugin
 Bundle-ActivationPolicy: lazy
 Bundle-RequiredExecutionEnvironment: JavaSE-1.7
-Import-Package: org.eclipse.jgit.api;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.api.errors;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.diff;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.dircache;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.junit;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.lib;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.merge;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.pgm;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.pgm.internal;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.pgm.opt;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.revwalk;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.storage.file;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.transport;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.treewalk;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.util;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.util.io;version="[4.0.3,4.1.0)",
+Import-Package: org.eclipse.jgit.api;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.api.errors;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.diff;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.dircache;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.junit;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.lib;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.merge;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.pgm;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.pgm.internal;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.pgm.opt;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.revwalk;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.storage.file;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.transport;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.treewalk;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.util;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.util.io;version="[4.1.2,4.2.0)",
  org.hamcrest.core;bundle-version="[1.1.0,2.0.0)",
  org.junit;version="[4.4.0,5.0.0)",
  org.kohsuke.args4j;version="[2.0.12,2.1.0)"
diff --git a/org.eclipse.jgit.pgm.test/pom.xml b/org.eclipse.jgit.pgm.test/pom.xml
index c570b1b..cca2e92 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.0.3.201509231615-r</version>
+    <version>4.1.2.201602141800-r</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.pgm.test</artifactId>
diff --git a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CheckoutTest.java b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CheckoutTest.java
index cef9b9e..387eb2b 100644
--- a/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CheckoutTest.java
+++ b/org.eclipse.jgit.pgm.test/tst/org/eclipse/jgit/pgm/CheckoutTest.java
@@ -43,6 +43,7 @@
 package org.eclipse.jgit.pgm;
 
 import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 
 import java.io.File;
@@ -59,7 +60,6 @@
 import org.eclipse.jgit.treewalk.FileTreeIterator.FileEntry;
 import org.eclipse.jgit.treewalk.TreeWalk;
 import org.eclipse.jgit.util.FileUtils;
-import org.junit.Assert;
 import org.junit.Test;
 
 public class CheckoutTest extends CLIRepositoryTestCase {
@@ -68,7 +68,8 @@
 	public void testCheckoutSelf() throws Exception {
 		new Git(db).commit().setMessage("initial commit").call();
 
-		assertEquals("Already on 'master'", execute("git checkout master"));
+		assertStringArrayEquals("Already on 'master'",
+				execute("git checkout master"));
 	}
 
 	@Test
@@ -76,20 +77,21 @@
 		new Git(db).commit().setMessage("initial commit").call();
 		new Git(db).branchCreate().setName("side").call();
 
-		assertEquals("Switched to branch 'side'", execute("git checkout side"));
+		assertStringArrayEquals("Switched to branch 'side'",
+				execute("git checkout side"));
 	}
 
 	@Test
 	public void testCheckoutNewBranch() throws Exception {
 		new Git(db).commit().setMessage("initial commit").call();
 
-		assertEquals("Switched to a new branch 'side'",
+		assertStringArrayEquals("Switched to a new branch 'side'",
 				execute("git checkout -b side"));
 	}
 
 	@Test
 	public void testCheckoutNonExistingBranch() throws Exception {
-		assertEquals(
+		assertStringArrayEquals(
 				"error: pathspec 'side' did not match any file(s) known to git.",
 				execute("git checkout side"));
 	}
@@ -98,19 +100,20 @@
 	public void testCheckoutNewBranchThatAlreadyExists() throws Exception {
 		new Git(db).commit().setMessage("initial commit").call();
 
-		assertEquals("fatal: A branch named 'master' already exists.",
+		assertStringArrayEquals(
+				"fatal: A branch named 'master' already exists.",
 				execute("git checkout -b master"));
 	}
 
 	@Test
 	public void testCheckoutNewBranchOnBranchToBeBorn() throws Exception {
-		assertEquals("fatal: You are on a branch yet to be born",
+		assertStringArrayEquals("fatal: You are on a branch yet to be born",
 				execute("git checkout -b side"));
 	}
 
 	@Test
 	public void testCheckoutUnresolvedHead() throws Exception {
-		assertEquals(
+		assertStringArrayEquals(
 				"error: pathspec 'HEAD' did not match any file(s) known to git.",
 				execute("git checkout HEAD"));
 	}
@@ -119,7 +122,7 @@
 	public void testCheckoutHead() throws Exception {
 		new Git(db).commit().setMessage("initial commit").call();
 
-		assertEquals("", execute("git checkout HEAD"));
+		assertStringArrayEquals("", execute("git checkout HEAD"));
 	}
 
 	@Test
@@ -139,10 +142,10 @@
 		git.add().addFilepattern(".").call();
 
 		String[] execute = execute("git checkout branch_1");
-		Assert.assertEquals(
+		assertEquals(
 				"error: Your local changes to the following files would be overwritten by checkout:",
 				execute[0]);
-		Assert.assertEquals("\ta", execute[1]);
+		assertEquals("\ta", execute[1]);
 	}
 
 	/**
@@ -193,7 +196,7 @@
 		Git git = new Git(db);
 		git.commit().setMessage("initial commit").call();
 
-		assertEquals("Switched to a new branch 'new_branch'",
+		assertStringArrayEquals("Switched to a new branch 'new_branch'",
 				execute("git checkout --orphan new_branch"));
 		assertEquals("refs/heads/new_branch", db.getRef("HEAD").getTarget().getName());
 		RevCommit commit = git.commit().setMessage("orphan commit").call();
@@ -553,17 +556,13 @@
 		// assertEquals("a/c", exception.getConflictingPaths().get(1));
 	}
 
-	static private void assertEquals(Object expected, Object actual) {
-		Assert.assertEquals(expected, actual);
-	}
-
-	static private void assertEquals(String expected, String[] actual) {
+	static private void assertStringArrayEquals(String expected, String[] actual) {
 		// if there is more than one line, ignore last one if empty
-		Assert.assertEquals(
+		assertEquals(
 				1,
 				actual.length > 1 && actual[actual.length - 1].equals("") ? actual.length - 1
 						: actual.length);
-		Assert.assertEquals(expected, actual[0]);
+		assertEquals(expected, actual[0]);
 	}
 
 	@Test
diff --git a/org.eclipse.jgit.pgm/.settings/org.eclipse.jdt.core.prefs b/org.eclipse.jgit.pgm/.settings/org.eclipse.jdt.core.prefs
index ff39d16..4e28e0b 100644
--- a/org.eclipse.jgit.pgm/.settings/org.eclipse.jdt.core.prefs
+++ b/org.eclipse.jgit.pgm/.settings/org.eclipse.jdt.core.prefs
@@ -1,10 +1,10 @@
 eclipse.preferences.version=1
-org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled
+org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=enabled
 org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore
 org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull
 org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault
 org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable
-org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled
+org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled
 org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
 org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate
 org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
@@ -67,11 +67,11 @@
 org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error
 org.eclipse.jdt.core.compiler.problem.nullReference=error
 org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error
-org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning
+org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore
 org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning
 org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore
 org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=error
-org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning
+org.eclipse.jdt.core.compiler.problem.potentialNullReference=error
 org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore
 org.eclipse.jdt.core.compiler.problem.rawTypeReference=ignore
 org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning
diff --git a/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF b/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF
index 746fd2b..7d00a96 100644
--- a/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF
@@ -2,44 +2,46 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %plugin_name
 Bundle-SymbolicName: org.eclipse.jgit.pgm
-Bundle-Version: 4.0.3.201509231615-r
+Bundle-Version: 4.1.2.201602141800-r
 Bundle-Vendor: %provider_name
+Bundle-ActivationPolicy: lazy
 Bundle-Localization: plugin
 Bundle-RequiredExecutionEnvironment: JavaSE-1.7
 Import-Package: org.apache.commons.compress.archivers;version="[1.3,2.0)",
  org.apache.commons.compress.archivers.tar;version="[1.3,2.0)",
  org.apache.commons.compress.archivers.zip;version="[1.3,2.0)",
- org.eclipse.jgit.api;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.api.errors;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.archive;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.awtui;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.blame;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.diff;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.dircache;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.errors;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.gitrepo;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.internal.storage.file;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.internal.storage.pack;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.lib;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.merge;version="4.0.3",
- org.eclipse.jgit.nls;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.notes;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.revplot;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.revwalk;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.revwalk.filter;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.storage.file;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.storage.pack;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.transport;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.transport.resolver;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.treewalk;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.treewalk.filter;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.util;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.util.io;version="[4.0.3,4.1.0)",
+ org.eclipse.jgit.api;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.api.errors;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.archive;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.awtui;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.blame;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.diff;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.dircache;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.errors;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.gitrepo;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.internal.storage.file;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.internal.storage.pack;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.lib;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.merge;version="4.1.2",
+ org.eclipse.jgit.nls;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.notes;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.revplot;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.revwalk;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.revwalk.filter;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.storage.file;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.storage.pack;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.transport;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.transport.resolver;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.treewalk;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.treewalk.filter;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.util;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.util.io;version="[4.1.2,4.2.0)",
  org.kohsuke.args4j;version="[2.0.12,2.1.0)",
  org.kohsuke.args4j.spi;version="[2.0.15,2.1.0)"
-Bundle-ActivationPolicy: lazy
-Export-Package: org.eclipse.jgit.console;version="4.0.3",
- org.eclipse.jgit.pgm;version="4.0.3";
+Export-Package: org.eclipse.jgit.console;version="4.1.2";
+  uses:="org.eclipse.jgit.transport,
+   org.eclipse.jgit.util",
+ org.eclipse.jgit.pgm;version="4.1.2";
   uses:="org.eclipse.jgit.revwalk,
    org.eclipse.jgit.treewalk.filter,
    org.eclipse.jgit.pgm.opt,
@@ -50,12 +52,15 @@
    org.eclipse.jgit.treewalk,
    javax.swing,
    org.eclipse.jgit.transport",
- org.eclipse.jgit.pgm.debug;version="4.0.3";uses:="org.eclipse.jgit.pgm",
- org.eclipse.jgit.pgm.internal;version="4.0.3";x-friends:="org.eclipse.jgit.pgm.test,org.eclipse.jgit.test",
- org.eclipse.jgit.pgm.opt;version="4.0.3";
+ org.eclipse.jgit.pgm.debug;version="4.1.2";
+  uses:="org.eclipse.jgit.util.io,
+   org.eclipse.jgit.pgm",
+ org.eclipse.jgit.pgm.internal;version="4.1.2";x-friends:="org.eclipse.jgit.pgm.test,org.eclipse.jgit.test",
+ org.eclipse.jgit.pgm.opt;version="4.1.2";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.revwalk,
    org.kohsuke.args4j.spi,
    org.kohsuke.args4j"
 Main-Class: org.eclipse.jgit.pgm.Main
 Implementation-Title: JGit Command Line Interface
+Require-Bundle: org.eclipse.jdt.annotation;bundle-version="[1.1.0,2.0.0)";resolution:=optional
diff --git a/org.eclipse.jgit.pgm/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit.pgm/META-INF/SOURCE-MANIFEST.MF
index c68c9f4..215f8e7 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.0.3.201509231615-r
-Eclipse-SourceBundle: org.eclipse.jgit.pgm;version="4.0.3.201509231615-r";roots="."
+Bundle-Version: 4.1.2.201602141800-r
+Eclipse-SourceBundle: org.eclipse.jgit.pgm;version="4.1.2.201602141800-r";roots="."
diff --git a/org.eclipse.jgit.pgm/pom.xml b/org.eclipse.jgit.pgm/pom.xml
index 9d8485f..f84e746 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.0.3.201509231615-r</version>
+    <version>4.1.2.201602141800-r</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.pgm</artifactId>
diff --git a/org.eclipse.jgit.pgm/resources/log4j.properties b/org.eclipse.jgit.pgm/resources/log4j.properties
index 4a2e480..1496c5a 100644
--- a/org.eclipse.jgit.pgm/resources/log4j.properties
+++ b/org.eclipse.jgit.pgm/resources/log4j.properties
@@ -1,4 +1,4 @@
-log4j.rootLogger=WARNING, stderr
+log4j.rootLogger=WARN, stderr
 
 log4j.appender.stderr=org.apache.log4j.ConsoleAppender
 log4j.appender.stderr.Target=System.err
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Add.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Add.java
index 12aac77..c36c485 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Add.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Add.java
@@ -62,10 +62,12 @@
 
 	@Override
 	protected void run() throws Exception {
-		AddCommand addCmd = new Git(db).add();
-		addCmd.setUpdate(update);
-		for (String p : filepatterns)
-			addCmd.addFilepattern(p);
-		addCmd.call();
+		try (Git git = new Git(db)) {
+			AddCommand addCmd = git.add();
+			addCmd.setUpdate(update);
+			for (String p : filepatterns)
+				addCmd.addFilepattern(p);
+			addCmd.call();
+		}
 	}
 }
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Archive.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Archive.java
index 80bb9ec..fe2ba83 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Archive.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Archive.java
@@ -87,8 +87,8 @@
 			else
 				stream = outs;
 
-			try {
-				ArchiveCommand cmd = new Git(db).archive()
+			try (Git git = new Git(db)) {
+				ArchiveCommand cmd = git.archive()
 					.setTree(tree)
 					.setFormat(format)
 					.setPrefix(prefix)
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Branch.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Branch.java
index 72e3715..83a1ca7 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Branch.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Branch.java
@@ -186,29 +186,31 @@
 		// This can happen if HEAD is stillborn
 		if (head != null) {
 			String current = head.getLeaf().getName();
-			ListBranchCommand command = new Git(db).branchList();
-			if (all)
-				command.setListMode(ListMode.ALL);
-			else if (remote)
-				command.setListMode(ListMode.REMOTE);
+			try (Git git = new Git(db)) {
+				ListBranchCommand command = git.branchList();
+				if (all)
+					command.setListMode(ListMode.ALL);
+				else if (remote)
+					command.setListMode(ListMode.REMOTE);
 
-			if (containsCommitish != null)
-				command.setContains(containsCommitish);
+				if (containsCommitish != null)
+					command.setContains(containsCommitish);
 
-			List<Ref> refs = command.call();
-			for (Ref ref : refs) {
-				if (ref.getName().equals(Constants.HEAD))
-					addRef("(no branch)", head); //$NON-NLS-1$
-			}
+				List<Ref> refs = command.call();
+				for (Ref ref : refs) {
+					if (ref.getName().equals(Constants.HEAD))
+						addRef("(no branch)", head); //$NON-NLS-1$
+				}
 
-			addRefs(refs, Constants.R_HEADS);
-			addRefs(refs, Constants.R_REMOTES);
+				addRefs(refs, Constants.R_HEADS);
+				addRefs(refs, Constants.R_REMOTES);
 
-			try (ObjectReader reader = db.newObjectReader()) {
-				for (final Entry<String, Ref> e : printRefs.entrySet()) {
-					final Ref ref = e.getValue();
-					printHead(reader, e.getKey(),
-							current.equals(ref.getName()), ref);
+				try (ObjectReader reader = db.newObjectReader()) {
+					for (final Entry<String, Ref> e : printRefs.entrySet()) {
+						final Ref ref = e.getValue();
+						printHead(reader, e.getKey(),
+								current.equals(ref.getName()), ref);
+					}
 				}
 			}
 		}
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Checkout.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Checkout.java
index 56d4fcf..4579462 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Checkout.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Checkout.java
@@ -89,47 +89,49 @@
 				throw die(CLIText.get().onBranchToBeBorn);
 		}
 
-		CheckoutCommand command = new Git(db).checkout();
-		if (paths.size() > 0) {
-			command.setStartPoint(name);
-			for (String path : paths)
-				command.addPath(path);
-		} else {
-			command.setCreateBranch(createBranch);
-			command.setName(name);
-			command.setForce(force);
-			command.setOrphan(orphan);
-		}
-		try {
-			String oldBranch = db.getBranch();
-			Ref ref = command.call();
-			if (ref == null)
-				return;
-			if (Repository.shortenRefName(ref.getName()).equals(oldBranch)) {
-				outw.println(MessageFormat.format(
-						CLIText.get().alreadyOnBranch,
-						name));
-				return;
+		try (Git git = new Git(db)) {
+			CheckoutCommand command = git.checkout();
+			if (paths.size() > 0) {
+				command.setStartPoint(name);
+				for (String path : paths)
+					command.addPath(path);
+			} else {
+				command.setCreateBranch(createBranch);
+				command.setName(name);
+				command.setForce(force);
+				command.setOrphan(orphan);
 			}
-			if (createBranch || orphan)
+			try {
+				String oldBranch = db.getBranch();
+				Ref ref = command.call();
+				if (ref == null)
+					return;
+				if (Repository.shortenRefName(ref.getName()).equals(oldBranch)) {
+					outw.println(MessageFormat.format(
+							CLIText.get().alreadyOnBranch,
+							name));
+					return;
+				}
+				if (createBranch || orphan)
+					outw.println(MessageFormat.format(
+							CLIText.get().switchedToNewBranch, name));
+				else
+					outw.println(MessageFormat.format(
+							CLIText.get().switchedToBranch,
+							Repository.shortenRefName(ref.getName())));
+			} catch (RefNotFoundException e) {
 				outw.println(MessageFormat.format(
-						CLIText.get().switchedToNewBranch, name));
-			else
-				outw.println(MessageFormat.format(
-						CLIText.get().switchedToBranch,
-						Repository.shortenRefName(ref.getName())));
-		} catch (RefNotFoundException e) {
-			outw.println(MessageFormat.format(
-					CLIText.get().pathspecDidNotMatch,
-					name));
-		} catch (RefAlreadyExistsException e) {
-			throw die(MessageFormat.format(CLIText.get().branchAlreadyExists,
-					name));
-		} catch (CheckoutConflictException e) {
-			outw.println(CLIText.get().checkoutConflict);
-			for (String path : e.getConflictingPaths())
-				outw.println(MessageFormat.format(
-						CLIText.get().checkoutConflictPathLine, path));
+						CLIText.get().pathspecDidNotMatch,
+						name));
+			} catch (RefAlreadyExistsException e) {
+				throw die(MessageFormat.format(CLIText.get().branchAlreadyExists,
+						name));
+			} catch (CheckoutConflictException e) {
+				outw.println(CLIText.get().checkoutConflict);
+				for (String path : e.getConflictingPaths())
+					outw.println(MessageFormat.format(
+							CLIText.get().checkoutConflictPathLine, path));
+			}
 		}
 	}
 }
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Commit.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Commit.java
index 14c449a..f18242d 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Commit.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Commit.java
@@ -80,37 +80,39 @@
 	@Override
 	protected void run() throws NoHeadException, NoMessageException,
 			ConcurrentRefUpdateException, JGitInternalException, Exception {
-		CommitCommand commitCmd = new Git(db).commit();
-		if (author != null)
-			commitCmd.setAuthor(RawParseUtils.parsePersonIdent(author));
-		if (message != null)
-			commitCmd.setMessage(message);
-		if (only && paths.isEmpty())
-			throw die(CLIText.get().pathsRequired);
-		if (only && all)
-			throw die(CLIText.get().onlyOneOfIncludeOnlyAllInteractiveCanBeUsed);
-		if (!paths.isEmpty())
-			for (String p : paths)
-				commitCmd.setOnly(p);
-		commitCmd.setAmend(amend);
-		commitCmd.setAll(all);
-		Ref head = db.getRef(Constants.HEAD);
-		RevCommit commit;
-		try {
-			commit = commitCmd.call();
-		} catch (JGitInternalException e) {
-			throw die(e.getMessage());
-		}
+		try (Git git = new Git(db)) {
+			CommitCommand commitCmd = git.commit();
+			if (author != null)
+				commitCmd.setAuthor(RawParseUtils.parsePersonIdent(author));
+			if (message != null)
+				commitCmd.setMessage(message);
+			if (only && paths.isEmpty())
+				throw die(CLIText.get().pathsRequired);
+			if (only && all)
+				throw die(CLIText.get().onlyOneOfIncludeOnlyAllInteractiveCanBeUsed);
+			if (!paths.isEmpty())
+				for (String p : paths)
+					commitCmd.setOnly(p);
+			commitCmd.setAmend(amend);
+			commitCmd.setAll(all);
+			Ref head = db.getRef(Constants.HEAD);
+			RevCommit commit;
+			try {
+				commit = commitCmd.call();
+			} catch (JGitInternalException e) {
+				throw die(e.getMessage());
+			}
 
-		String branchName;
-		if (!head.isSymbolic())
-			branchName = CLIText.get().branchDetachedHEAD;
-		else {
-			branchName = head.getTarget().getName();
-			if (branchName.startsWith(Constants.R_HEADS))
-				branchName = branchName.substring(Constants.R_HEADS.length());
+			String branchName;
+			if (!head.isSymbolic())
+				branchName = CLIText.get().branchDetachedHEAD;
+			else {
+				branchName = head.getTarget().getName();
+				if (branchName.startsWith(Constants.R_HEADS))
+					branchName = branchName.substring(Constants.R_HEADS.length());
+			}
+			outw.println("[" + branchName + " " + commit.name() + "] " //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+					+ commit.getShortMessage());
 		}
-		outw.println("[" + branchName + " " + commit.name() + "] " //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
-				+ commit.getShortMessage());
 	}
 }
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 901e560..ec000f3 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
@@ -61,20 +61,21 @@
 
 	@Override
 	protected void run() throws Exception {
-		DescribeCommand cmd = new Git(db).describe();
-		if (tree != null)
-			cmd.setTarget(tree);
-		cmd.setLong(longDesc);
-		String result = null;
-		try {
-			result = cmd.call();
-		} catch (RefNotFoundException e) {
-			throw die(CLIText.get().noNamesFound, e);
+		try (Git git = new Git(db)) {
+			DescribeCommand cmd = git.describe();
+			if (tree != null)
+				cmd.setTarget(tree);
+			cmd.setLong(longDesc);
+			String result = null;
+			try {
+				result = cmd.call();
+			} catch (RefNotFoundException e) {
+				throw die(CLIText.get().noNamesFound, e);
+			}
+			if (result == null)
+				throw die(CLIText.get().noNamesFound);
+
+			outw.println(result);
 		}
-		if (result == null)
-			throw die(CLIText.get().noNamesFound);
-
-		outw.println(result);
 	}
-
 }
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTree.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTree.java
index d89053c..32adf6d 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTree.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/DiffTree.java
@@ -74,46 +74,47 @@
 
 	@Override
 	protected void run() throws Exception {
-		final TreeWalk walk = new TreeWalk(db);
-		walk.setRecursive(recursive);
-		for (final AbstractTreeIterator i : trees)
-			walk.addTree(i);
-		walk.setFilter(AndTreeFilter.create(TreeFilter.ANY_DIFF, pathFilter));
+		try (final TreeWalk walk = new TreeWalk(db)) {
+			walk.setRecursive(recursive);
+			for (final AbstractTreeIterator i : trees)
+				walk.addTree(i);
+			walk.setFilter(AndTreeFilter.create(TreeFilter.ANY_DIFF, pathFilter));
 
-		final int nTree = walk.getTreeCount();
-		while (walk.next()) {
-			for (int i = 1; i < nTree; i++)
-				outw.print(':');
-			for (int i = 0; i < nTree; i++) {
-				final FileMode m = walk.getFileMode(i);
-				final String s = m.toString();
-				for (int pad = 6 - s.length(); pad > 0; pad--)
-					outw.print('0');
-				outw.print(s);
-				outw.print(' ');
+			final int nTree = walk.getTreeCount();
+			while (walk.next()) {
+				for (int i = 1; i < nTree; i++)
+					outw.print(':');
+				for (int i = 0; i < nTree; i++) {
+					final FileMode m = walk.getFileMode(i);
+					final String s = m.toString();
+					for (int pad = 6 - s.length(); pad > 0; pad--)
+						outw.print('0');
+					outw.print(s);
+					outw.print(' ');
+				}
+
+				for (int i = 0; i < nTree; i++) {
+					outw.print(walk.getObjectId(i).name());
+					outw.print(' ');
+				}
+
+				char chg = 'M';
+				if (nTree == 2) {
+					final int m0 = walk.getRawMode(0);
+					final int m1 = walk.getRawMode(1);
+					if (m0 == 0 && m1 != 0)
+						chg = 'A';
+					else if (m0 != 0 && m1 == 0)
+						chg = 'D';
+					else if (m0 != m1 && walk.idEqual(0, 1))
+						chg = 'T';
+				}
+				outw.print(chg);
+
+				outw.print('\t');
+				outw.print(walk.getPathString());
+				outw.println();
 			}
-
-			for (int i = 0; i < nTree; i++) {
-				outw.print(walk.getObjectId(i).name());
-				outw.print(' ');
-			}
-
-			char chg = 'M';
-			if (nTree == 2) {
-				final int m0 = walk.getRawMode(0);
-				final int m1 = walk.getRawMode(1);
-				if (m0 == 0 && m1 != 0)
-					chg = 'A';
-				else if (m0 != 0 && m1 == 0)
-					chg = 'D';
-				else if (m0 != m1 && walk.idEqual(0, 1))
-					chg = 'T';
-			}
-			outw.print(chg);
-
-			outw.print('\t');
-			outw.print(walk.getPathString());
-			outw.println();
 		}
 	}
 }
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 186fdd8..ed06733 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
@@ -104,31 +104,32 @@
 
 	@Override
 	protected void run() throws Exception {
-		Git git = new Git(db);
-		FetchCommand fetch = git.fetch();
-		if (fsck != null)
-			fetch.setCheckFetchedObjects(fsck.booleanValue());
-		if (prune != null)
-			fetch.setRemoveDeletedRefs(prune.booleanValue());
-		if (toget != null)
-			fetch.setRefSpecs(toget);
-		if (tags != null) {
-			fetch.setTagOpt(tags.booleanValue() ? TagOpt.FETCH_TAGS
-					: TagOpt.NO_TAGS);
+		try (Git git = new Git(db)) {
+			FetchCommand fetch = git.fetch();
+			if (fsck != null)
+				fetch.setCheckFetchedObjects(fsck.booleanValue());
+			if (prune != null)
+				fetch.setRemoveDeletedRefs(prune.booleanValue());
+			if (toget != null)
+				fetch.setRefSpecs(toget);
+			if (tags != null) {
+				fetch.setTagOpt(tags.booleanValue() ? TagOpt.FETCH_TAGS
+						: TagOpt.NO_TAGS);
+			}
+			if (0 <= timeout)
+				fetch.setTimeout(timeout);
+			fetch.setDryRun(dryRun);
+			fetch.setRemote(remote);
+			if (thin != null)
+				fetch.setThin(thin.booleanValue());
+			if (quiet == null || !quiet.booleanValue())
+				fetch.setProgressMonitor(new TextProgressMonitor(errw));
+
+			FetchResult result = fetch.call();
+			if (result.getTrackingRefUpdates().isEmpty())
+				return;
+
+			showFetchResult(result);
 		}
-		if (0 <= timeout)
-			fetch.setTimeout(timeout);
-		fetch.setDryRun(dryRun);
-		fetch.setRemote(remote);
-		if (thin != null)
-			fetch.setThin(thin.booleanValue());
-		if (quiet == null || !quiet.booleanValue())
-			fetch.setProgressMonitor(new TextProgressMonitor(errw));
-
-		FetchResult result = fetch.call();
-		if (result.getTrackingRefUpdates().isEmpty())
-			return;
-
-		showFetchResult(result);
 	}
 }
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/LsTree.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/LsTree.java
index 4b16ed8..872ea67 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/LsTree.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/LsTree.java
@@ -72,27 +72,28 @@
 
 	@Override
 	protected void run() throws Exception {
-		final TreeWalk walk = new TreeWalk(db);
-		walk.reset(); // drop the first empty tree, which we do not need here
-		if (paths.size() > 0)
-			walk.setFilter(PathFilterGroup.createFromStrings(paths));
-		walk.setRecursive(recursive);
-		walk.addTree(tree);
+		try (final TreeWalk walk = new TreeWalk(db)) {
+			walk.reset(); // drop the first empty tree, which we do not need here
+			if (paths.size() > 0)
+				walk.setFilter(PathFilterGroup.createFromStrings(paths));
+			walk.setRecursive(recursive);
+			walk.addTree(tree);
 
-		while (walk.next()) {
-			final FileMode mode = walk.getFileMode(0);
-			if (mode == FileMode.TREE)
-				outw.print('0');
-			outw.print(mode);
-			outw.print(' ');
-			outw.print(Constants.typeString(mode.getObjectType()));
+			while (walk.next()) {
+				final FileMode mode = walk.getFileMode(0);
+				if (mode == FileMode.TREE)
+					outw.print('0');
+				outw.print(mode);
+				outw.print(' ');
+				outw.print(Constants.typeString(mode.getObjectType()));
 
-			outw.print(' ');
-			outw.print(walk.getObjectId(0).name());
+				outw.print(' ');
+				outw.print(walk.getObjectId(0).name());
 
-			outw.print('\t');
-			outw.print(QuotedString.GIT_PATH.quote(walk.getPathString()));
-			outw.println();
+				outw.print('\t');
+				outw.print(QuotedString.GIT_PATH.quote(walk.getPathString()));
+				outw.println();
+			}
 		}
 	}
 }
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 7151de7..ceb0d6b 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
@@ -181,7 +181,7 @@
 
 		if (argv.length == 0 || help) {
 			final String ex = clp.printExample(ExampleMode.ALL, CLIText.get().resourceBundle());
-			writer.println("jgit" + ex + " command [ARG ...]"); //$NON-NLS-1$
+			writer.println("jgit" + ex + " command [ARG ...]"); //$NON-NLS-1$ //$NON-NLS-2$
 			if (help) {
 				writer.println();
 				clp.printUsage(writer, CLIText.get().resourceBundle());
@@ -291,39 +291,45 @@
 	/**
 	 * Configure the JRE's standard HTTP based on <code>http_proxy</code>.
 	 * <p>
-	 * The popular libcurl library honors the <code>http_proxy</code>
-	 * environment variable as a means of specifying an HTTP proxy for requests
-	 * made behind a firewall. This is not natively recognized by the JRE, so
-	 * this method can be used by command line utilities to configure the JRE
-	 * before the first request is sent.
+	 * The popular libcurl library honors the <code>http_proxy</code>,
+	 * <code>https_proxy</code> environment variables as a means of specifying
+	 * an HTTP/S proxy for requests made behind a firewall. This is not natively
+	 * recognized by the JRE, so this method can be used by command line
+	 * utilities to configure the JRE before the first request is sent.
 	 *
 	 * @throws MalformedURLException
-	 *             the value in <code>http_proxy</code> is unsupportable.
+	 *             the value in <code>http_proxy</code> or
+	 *             <code>https_proxy</code> is unsupportable.
 	 */
 	private static void configureHttpProxy() throws MalformedURLException {
-		final String s = System.getenv("http_proxy"); //$NON-NLS-1$
-		if (s == null || s.equals("")) //$NON-NLS-1$
-			return;
+		for (String protocol : new String[] { "http", "https" }) { //$NON-NLS-1$ //$NON-NLS-2$
+			final String s = System.getenv(protocol + "_proxy"); //$NON-NLS-1$
+			if (s == null || s.equals("")) //$NON-NLS-1$
+				return;
 
-		final URL u = new URL((s.indexOf("://") == -1) ? "http://" + s : s); //$NON-NLS-1$ //$NON-NLS-2$
-		if (!"http".equals(u.getProtocol())) //$NON-NLS-1$
-			throw new MalformedURLException(MessageFormat.format(CLIText.get().invalidHttpProxyOnlyHttpSupported, s));
+			final URL u = new URL(
+					(s.indexOf("://") == -1) ? protocol + "://" + s : s); //$NON-NLS-1$ //$NON-NLS-2$
+			if (!u.getProtocol().startsWith("http")) //$NON-NLS-1$
+				throw new MalformedURLException(MessageFormat.format(
+						CLIText.get().invalidHttpProxyOnlyHttpSupported, s));
 
-		final String proxyHost = u.getHost();
-		final int proxyPort = u.getPort();
+			final String proxyHost = u.getHost();
+			final int proxyPort = u.getPort();
 
-		System.setProperty("http.proxyHost", proxyHost); //$NON-NLS-1$
-		if (proxyPort > 0)
-			System.setProperty("http.proxyPort", String.valueOf(proxyPort)); //$NON-NLS-1$
+			System.setProperty(protocol + ".proxyHost", proxyHost); //$NON-NLS-1$
+			if (proxyPort > 0)
+				System.setProperty(protocol + ".proxyPort", //$NON-NLS-1$
+						String.valueOf(proxyPort));
 
-		final String userpass = u.getUserInfo();
-		if (userpass != null && userpass.contains(":")) { //$NON-NLS-1$
-			final int c = userpass.indexOf(':');
-			final String user = userpass.substring(0, c);
-			final String pass = userpass.substring(c + 1);
-			CachedAuthenticator
-					.add(new CachedAuthenticator.CachedAuthentication(
-							proxyHost, proxyPort, user, pass));
+			final String userpass = u.getUserInfo();
+			if (userpass != null && userpass.contains(":")) { //$NON-NLS-1$
+				final int c = userpass.indexOf(':');
+				final String user = userpass.substring(0, c);
+				final String pass = userpass.substring(c + 1);
+				CachedAuthenticator.add(
+						new CachedAuthenticator.CachedAuthentication(proxyHost,
+								proxyPort, user, pass));
+			}
 		}
 	}
 }
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Merge.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Merge.java
index 93c4388..e0ff058 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Merge.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Merge.java
@@ -121,22 +121,23 @@
 					CLIText.get().refDoesNotExistOrNoCommit, ref));
 
 		Ref oldHead = db.getRef(Constants.HEAD);
-		Git git = new Git(db);
-		MergeCommand mergeCmd = git.merge().setStrategy(mergeStrategy)
-				.setSquash(squash).setFastForward(ff).setCommit(!noCommit);
-		if (srcRef != null)
-			mergeCmd.include(srcRef);
-		else
-			mergeCmd.include(src);
-
-		if (message != null)
-			mergeCmd.setMessage(message);
-
 		MergeResult result;
-		try {
-			result = mergeCmd.call();
-		} catch (CheckoutConflictException e) {
-			result = new MergeResult(e.getConflictingPaths()); // CHECKOUT_CONFLICT
+		try (Git git = new Git(db)) {
+			MergeCommand mergeCmd = git.merge().setStrategy(mergeStrategy)
+					.setSquash(squash).setFastForward(ff).setCommit(!noCommit);
+			if (srcRef != null)
+				mergeCmd.include(srcRef);
+			else
+				mergeCmd.include(src);
+
+			if (message != null)
+				mergeCmd.setMessage(message);
+
+			try {
+				result = mergeCmd.call();
+			} catch (CheckoutConflictException e) {
+				result = new MergeResult(e.getConflictingPaths()); // CHECKOUT_CONFLICT
+			}
 		}
 
 		switch (result.getMergeStatus()) {
@@ -206,12 +207,13 @@
 
 	private boolean isMergedInto(Ref oldHead, AnyObjectId src)
 			throws IOException {
-		RevWalk revWalk = new RevWalk(db);
-		ObjectId oldHeadObjectId = oldHead.getPeeledObjectId();
-		if (oldHeadObjectId == null)
-			oldHeadObjectId = oldHead.getObjectId();
-		RevCommit oldHeadCommit = revWalk.lookupCommit(oldHeadObjectId);
-		RevCommit srcCommit = revWalk.lookupCommit(src);
-		return revWalk.isMergedInto(oldHeadCommit, srcCommit);
+		try (RevWalk revWalk = new RevWalk(db)) {
+			ObjectId oldHeadObjectId = oldHead.getPeeledObjectId();
+			if (oldHeadObjectId == null)
+				oldHeadObjectId = oldHead.getObjectId();
+			RevCommit oldHeadCommit = revWalk.lookupCommit(oldHeadObjectId);
+			RevCommit srcCommit = revWalk.lookupCommit(src);
+			return revWalk.isMergedInto(oldHeadCommit, srcCommit);
+		}
 	}
 }
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Push.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Push.java
index 4268f21..1879ef5 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Push.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Push.java
@@ -109,24 +109,25 @@
 
 	@Override
 	protected void run() throws Exception {
-		Git git = new Git(db);
-		PushCommand push = git.push();
-		push.setDryRun(dryRun);
-		push.setForce(force);
-		push.setProgressMonitor(new TextProgressMonitor(errw));
-		push.setReceivePack(receivePack);
-		push.setRefSpecs(refSpecs);
-		if (all)
-			push.setPushAll();
-		if (tags)
-			push.setPushTags();
-		push.setRemote(remote);
-		push.setThin(thin);
-		push.setTimeout(timeout);
-		Iterable<PushResult> results = push.call();
-		for (PushResult result : results) {
-			try (ObjectReader reader = db.newObjectReader()) {
-				printPushResult(reader, result.getURI(), result);
+		try (Git git = new Git(db)) {
+			PushCommand push = git.push();
+			push.setDryRun(dryRun);
+			push.setForce(force);
+			push.setProgressMonitor(new TextProgressMonitor(errw));
+			push.setReceivePack(receivePack);
+			push.setRefSpecs(refSpecs);
+			if (all)
+				push.setPushAll();
+			if (tags)
+				push.setPushTags();
+			push.setRemote(remote);
+			push.setThin(thin);
+			push.setTimeout(timeout);
+			Iterable<PushResult> results = push.call();
+			for (PushResult result : results) {
+				try (ObjectReader reader = db.newObjectReader()) {
+					printPushResult(reader, result.getURI(), result);
+				}
 			}
 		}
 	}
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Reflog.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Reflog.java
index aa90f8d..86a021d 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Reflog.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Reflog.java
@@ -59,13 +59,15 @@
 
 	@Override
 	protected void run() throws Exception {
-		ReflogCommand cmd = new Git(db).reflog();
-		if (ref != null)
-			cmd.setRef(ref);
-		Collection<ReflogEntry> entries = cmd.call();
-		int i = 0;
-		for (ReflogEntry entry : entries) {
-			outw.println(toString(entry, i++));
+		try (Git git = new Git(db)) {
+			ReflogCommand cmd = git.reflog();
+			if (ref != null)
+				cmd.setRef(ref);
+			Collection<ReflogEntry> entries = cmd.call();
+			int i = 0;
+			for (ReflogEntry entry : entries) {
+				outw.println(toString(entry, i++));
+			}
 		}
 	}
 
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Reset.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Reset.java
index f4cbcaf..6d1b1c5 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Reset.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Reset.java
@@ -66,19 +66,21 @@
 
 	@Override
 	protected void run() throws Exception {
-		ResetCommand command = new Git(db).reset();
-		command.setRef(commit);
-		ResetType mode = null;
-		if (soft)
-			mode = selectMode(mode, ResetType.SOFT);
-		if (mixed)
-			mode = selectMode(mode, ResetType.MIXED);
-		if (hard)
-			mode = selectMode(mode, ResetType.HARD);
-		if (mode == null)
-			throw die("no reset mode set");
-		command.setMode(mode);
-		command.call();
+		try (Git git = new Git(db)) {
+			ResetCommand command = git.reset();
+			command.setRef(commit);
+			ResetType mode = null;
+			if (soft)
+				mode = selectMode(mode, ResetType.SOFT);
+			if (mixed)
+				mode = selectMode(mode, ResetType.MIXED);
+			if (hard)
+				mode = selectMode(mode, ResetType.HARD);
+			if (mode == null)
+				throw die("no reset mode set");
+			command.setMode(mode);
+			command.call();
+		}
 	}
 
 	private static ResetType selectMode(ResetType mode, ResetType want) {
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Rm.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Rm.java
index 816b310..f4f864b 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Rm.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Rm.java
@@ -63,10 +63,11 @@
 
 	@Override
 	protected void run() throws Exception {
-		RmCommand command = new Git(db).rm();
-		for (String p : paths)
-			command.addFilepattern(p);
-		command.call();
+		try (Git git = new Git(db)) {
+			RmCommand command = git.rm();
+			for (String p : paths)
+				command.addFilepattern(p);
+			command.call();
+		}
 	}
-
 }
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Show.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Show.java
index b668139..c5986b0 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Show.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Show.java
@@ -251,16 +251,17 @@
 
 	private void show(RevTree obj) throws MissingObjectException,
 			IncorrectObjectTypeException, CorruptObjectException, IOException {
-		final TreeWalk walk = new TreeWalk(db);
-		walk.reset();
-		walk.addTree(obj);
+		try (final TreeWalk walk = new TreeWalk(db)) {
+			walk.reset();
+			walk.addTree(obj);
 
-		while (walk.next()) {
-			outw.print(walk.getPathString());
-			final FileMode mode = walk.getFileMode(0);
-			if (mode == FileMode.TREE)
-				outw.print("/"); //$NON-NLS-1$
-			outw.println();
+			while (walk.next()) {
+				outw.print(walk.getPathString());
+				final FileMode mode = walk.getFileMode(0);
+				if (mode == FileMode.TREE)
+					outw.print("/"); //$NON-NLS-1$
+				outw.println();
+			}
 		}
 	}
 
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Status.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Status.java
index 12d4208..be82d07 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Status.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Status.java
@@ -88,12 +88,14 @@
 
 	@Override
 	protected void run() throws Exception {
-		StatusCommand statusCommand = new Git(db).status();
-		if (filterPaths != null && filterPaths.size() > 0)
-			for (String path : filterPaths)
-				statusCommand.addPath(path);
-		org.eclipse.jgit.api.Status status = statusCommand.call();
-		printStatus(status);
+		try (Git git = new Git(db)) {
+			StatusCommand statusCommand = git.status();
+			if (filterPaths != null && filterPaths.size() > 0)
+				for (String path : filterPaths)
+					statusCommand.addPath(path);
+			org.eclipse.jgit.api.Status status = statusCommand.call();
+			printStatus(status);
+		}
 	}
 
 	private void printStatus(org.eclipse.jgit.api.Status status)
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Tag.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Tag.java
index a90d4c4..45fceb5 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Tag.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Tag.java
@@ -79,26 +79,28 @@
 
 	@Override
 	protected void run() throws Exception {
-		Git git = new Git(db);
-		if (tagName != null) {
-			TagCommand command = git.tag().setForceUpdate(force)
-					.setMessage(message).setName(tagName);
+		try (Git git = new Git(db)) {
+			if (tagName != null) {
+				TagCommand command = git.tag().setForceUpdate(force)
+						.setMessage(message).setName(tagName);
 
-			if (object != null) {
-				RevWalk walk = new RevWalk(db);
-				command.setObjectId(walk.parseAny(object));
-			}
-			try {
-				command.call();
-			} catch (RefAlreadyExistsException e) {
-				throw die(MessageFormat.format(CLIText.get().tagAlreadyExists,
-						tagName));
-			}
-		} else {
-			ListTagCommand command = git.tagList();
-			List<Ref> list = command.call();
-			for (Ref ref : list) {
-				outw.println(Repository.shortenRefName(ref.getName()));
+				if (object != null) {
+					try (RevWalk walk = new RevWalk(db)) {
+						command.setObjectId(walk.parseAny(object));
+					}
+				}
+				try {
+					command.call();
+				} catch (RefAlreadyExistsException e) {
+					throw die(MessageFormat.format(CLIText.get().tagAlreadyExists,
+							tagName));
+				}
+			} else {
+				ListTagCommand command = git.tagList();
+				List<Ref> list = command.call();
+				for (Ref ref : list) {
+					outw.println(Repository.shortenRefName(ref.getName()));
+				}
 			}
 		}
 	}
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/DiffAlgorithms.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/DiffAlgorithms.java
index 24d717d..df7ebb7 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/DiffAlgorithms.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/DiffAlgorithms.java
@@ -173,9 +173,9 @@
 		int maxN = 0;
 
 		AbbreviatedObjectId startId;
-		try (ObjectReader or = db.newObjectReader()) {
+		try (ObjectReader or = db.newObjectReader();
+			RevWalk rw = new RevWalk(or)) {
 			final MutableObjectId id = new MutableObjectId();
-			RevWalk rw = new RevWalk(or);
 			TreeWalk tw = new TreeWalk(or);
 			tw.setFilter(TreeFilter.ANY_DIFF);
 			tw.setRecursive(true);
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/ShowPackDelta.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/ShowPackDelta.java
index 7b5cdbf..d3eb245 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/ShowPackDelta.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/ShowPackDelta.java
@@ -75,7 +75,10 @@
 	@Override
 	protected void run() throws Exception {
 		ObjectReader reader = db.newObjectReader();
-		RevObject obj = new RevWalk(reader).parseAny(objectId);
+		RevObject obj;
+		try (RevWalk rw = new RevWalk(reader)) {
+			obj = rw.parseAny(objectId);
+		}
 		byte[] delta = getDelta(reader, obj);
 
 		// We're crossing our fingers that this will be a delta. Double
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/TextHashFunctions.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/TextHashFunctions.java
index dcfa8cf..dcbc37b 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/TextHashFunctions.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/debug/TextHashFunctions.java
@@ -300,10 +300,10 @@
 
 		long fileCnt = 0;
 		long lineCnt = 0;
-		try (ObjectReader or = db.newObjectReader()) {
-			final MutableObjectId id = new MutableObjectId();
+		try (ObjectReader or = db.newObjectReader();
 			RevWalk rw = new RevWalk(or);
-			TreeWalk tw = new TreeWalk(or);
+			TreeWalk tw = new TreeWalk(or)) {
+			final MutableObjectId id = new MutableObjectId();
 			tw.reset(rw.parseTree(db.resolve(Constants.HEAD)));
 			tw.setRecursive(true);
 
diff --git a/org.eclipse.jgit.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.test/META-INF/MANIFEST.MF
index 0c3edef..f059fdb 100644
--- a/org.eclipse.jgit.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.test/META-INF/MANIFEST.MF
@@ -2,51 +2,51 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %plugin_name
 Bundle-SymbolicName: org.eclipse.jgit.test
-Bundle-Version: 4.0.3.201509231615-r
+Bundle-Version: 4.1.2.201602141800-r
 Bundle-Localization: plugin
 Bundle-Vendor: %provider_name
 Bundle-ActivationPolicy: lazy
 Bundle-RequiredExecutionEnvironment: JavaSE-1.7
 Import-Package: com.googlecode.javaewah;version="[0.7.9,0.8.0)",
- org.eclipse.jgit.api;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.api.errors;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.attributes;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.awtui;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.blame;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.diff;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.dircache;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.errors;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.events;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.fnmatch;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.gitrepo;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.hooks;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.ignore;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.ignore.internal;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.internal;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.internal.storage.dfs;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.internal.storage.file;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.internal.storage.pack;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.junit;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.lib;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.merge;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.nls;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.notes;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.patch;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.pgm;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.pgm.internal;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.revplot;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.revwalk;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.revwalk.filter;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.storage.file;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.storage.pack;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.submodule;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.transport;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.transport.http;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.transport.resolver;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.treewalk;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.treewalk.filter;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.util;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.util.io;version="[4.0.3,4.1.0)",
+ org.eclipse.jgit.api;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.api.errors;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.attributes;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.awtui;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.blame;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.diff;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.dircache;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.errors;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.events;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.fnmatch;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.gitrepo;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.hooks;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.ignore;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.ignore.internal;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.internal;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.internal.storage.dfs;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.internal.storage.file;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.internal.storage.pack;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.junit;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.lib;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.merge;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.nls;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.notes;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.patch;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.pgm;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.pgm.internal;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.revplot;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.revwalk;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.revwalk.filter;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.storage.file;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.storage.pack;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.submodule;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.transport;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.transport.http;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.transport.resolver;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.treewalk;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.treewalk.filter;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.util;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.util.io;version="[4.1.2,4.2.0)",
  org.hamcrest;version="[1.1.0,2.0.0)",
  org.junit;version="[4.4.0,5.0.0)",
  org.junit.experimental.theories;version="[4.4.0,5.0.0)",
diff --git a/org.eclipse.jgit.test/pom.xml b/org.eclipse.jgit.test/pom.xml
index ab77aec..be72211 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.0.3.201509231615-r</version>
+    <version>4.1.2.201602141800-r</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.test</artifactId>
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CloneCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CloneCommandTest.java
index 3b2fa6c..ce11e1b 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CloneCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CloneCommandTest.java
@@ -202,6 +202,66 @@
 				fetchRefSpec(git2.getRepository()));
 	}
 
+	@Test
+	public void testCloneRepositoryCustomRemote() throws Exception {
+		File directory = createTempDirectory("testCloneRemoteUpstream");
+		CloneCommand command = Git.cloneRepository();
+		command.setDirectory(directory);
+		command.setRemote("upstream");
+		command.setURI(fileUri());
+		Git git2 = command.call();
+		addRepoToClose(git2.getRepository());
+		assertEquals("+refs/heads/*:refs/remotes/upstream/*",
+				git2.getRepository()
+					.getConfig()
+					.getStringList("remote", "upstream",
+							"fetch")[0]);
+		assertEquals("upstream",
+				git2.getRepository()
+					.getConfig()
+					.getString("branch", "test", "remote"));
+		assertEquals(db.resolve("test"),
+				git2.getRepository().resolve("upstream/test"));
+	}
+
+	@Test
+	public void testBareCloneRepositoryCustomRemote() throws Exception {
+		File directory = createTempDirectory("testCloneRemoteUpstream_bare");
+		CloneCommand command = Git.cloneRepository();
+		command.setBare(true);
+		command.setDirectory(directory);
+		command.setRemote("upstream");
+		command.setURI(fileUri());
+		Git git2 = command.call();
+		addRepoToClose(git2.getRepository());
+		assertEquals("+refs/heads/*:refs/heads/*",
+				git2.getRepository()
+					.getConfig()
+					.getStringList("remote", "upstream",
+							"fetch")[0]);
+		assertEquals("upstream",
+				git2.getRepository()
+					.getConfig()
+					.getString("branch", "test", "remote"));
+		assertNull(git2.getRepository().resolve("upstream/test"));
+	}
+
+	@Test
+	public void testBareCloneRepositoryNullRemote() throws Exception {
+		File directory = createTempDirectory("testCloneRemoteNull_bare");
+		CloneCommand command = Git.cloneRepository();
+		command.setBare(true);
+		command.setDirectory(directory);
+		command.setRemote(null);
+		command.setURI(fileUri());
+		Git git2 = command.call();
+		addRepoToClose(git2.getRepository());
+		assertEquals("+refs/heads/*:refs/heads/*", git2.getRepository()
+				.getConfig().getStringList("remote", "origin", "fetch")[0]);
+		assertEquals("origin", git2.getRepository().getConfig()
+				.getString("branch", "test", "remote"));
+	}
+
 	public static RefSpec fetchRefSpec(Repository r) throws URISyntaxException {
 		RemoteConfig remoteConfig =
 				new RemoteConfig(r.getConfig(), Constants.DEFAULT_REMOTE_NAME);
@@ -391,6 +451,17 @@
 		git.add().addFilepattern(path)
 				.addFilepattern(Constants.DOT_GIT_MODULES).call();
 		git.commit().setMessage("adding submodule").call();
+		try (SubmoduleWalk walk = SubmoduleWalk.forIndex(git.getRepository())) {
+			assertTrue(walk.next());
+			Repository subRepo = walk.getRepository();
+			addRepoToClose(subRepo);
+			assertNotNull(subRepo);
+			assertEquals(
+					new File(git.getRepository().getWorkTree(), walk.getPath()),
+					subRepo.getWorkTree());
+			assertEquals(new File(new File(git.getRepository().getDirectory(),
+					"modules"), walk.getPath()), subRepo.getDirectory());
+		}
 
 		File directory = createTempDirectory("testCloneRepositoryWithSubmodules");
 		CloneCommand clone = Git.cloneRepository();
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashApplyCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashApplyCommandTest.java
index 95b1419..ce235a7 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashApplyCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashApplyCommandTest.java
@@ -609,7 +609,7 @@
 		} catch (JGitInternalException e) {
 			assertEquals(MessageFormat.format(
 					JGitText.get().stashCommitIncorrectNumberOfParents,
-					head.name(), 0),
+					head.name(), "0"),
 					e.getMessage());
 		}
 	}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/DiffFormatterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/DiffFormatterTest.java
index 02e485d..58348d5 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/DiffFormatterTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/diff/DiffFormatterTest.java
@@ -241,8 +241,7 @@
 		ObjectId bId = blob("b\n");
 
 		String diffHeader = makeDiffHeaderModeChange(PATH_A, PATH_A, aId, bId,
-				GITLINK, REGULAR_FILE)
-				+ "-Subproject commit " + aId.name() + "\n";
+				GITLINK, REGULAR_FILE);
 
 		DiffEntry ad = DiffEntry.delete(PATH_A, aId);
 		ad.oldMode = FileMode.GITLINK;
@@ -257,7 +256,69 @@
 		assertEquals(1, fh.getHunks().size());
 
 		HunkHeader hh = fh.getHunks().get(0);
-		assertEquals(0, hh.toEditList().size());
+		assertEquals(1, hh.toEditList().size());
+	}
+
+	@Test
+	public void testCreateFileHeader_AddGitLink() throws Exception {
+		ObjectId adId = blob("a\nd\n");
+		DiffEntry ent = DiffEntry.add("FOO", adId);
+		ent.newMode = FileMode.GITLINK;
+		FileHeader fh = df.toFileHeader(ent);
+
+		String diffHeader = "diff --git a/FOO b/FOO\n" //
+				+ "new file mode " + GITLINK + "\n"
+				+ "index "
+				+ ObjectId.zeroId().abbreviate(8).name()
+				+ ".."
+				+ adId.abbreviate(8).name() + "\n" //
+				+ "--- /dev/null\n"//
+				+ "+++ b/FOO\n";
+		assertEquals(diffHeader, RawParseUtils.decode(fh.getBuffer()));
+
+		assertEquals(1, fh.getHunks().size());
+		HunkHeader hh = fh.getHunks().get(0);
+
+		EditList el = hh.toEditList();
+		assertEquals(1, el.size());
+
+		Edit e = el.get(0);
+		assertEquals(0, e.getBeginA());
+		assertEquals(0, e.getEndA());
+		assertEquals(0, e.getBeginB());
+		assertEquals(1, e.getEndB());
+		assertEquals(Edit.Type.INSERT, e.getType());
+	}
+
+	@Test
+	public void testCreateFileHeader_DeleteGitLink() throws Exception {
+		ObjectId adId = blob("a\nd\n");
+		DiffEntry ent = DiffEntry.delete("FOO", adId);
+		ent.oldMode = FileMode.GITLINK;
+		FileHeader fh = df.toFileHeader(ent);
+
+		String diffHeader = "diff --git a/FOO b/FOO\n" //
+				+ "deleted file mode " + GITLINK + "\n"
+				+ "index "
+				+ adId.abbreviate(8).name()
+				+ ".."
+				+ ObjectId.zeroId().abbreviate(8).name() + "\n" //
+				+ "--- a/FOO\n"//
+				+ "+++ /dev/null\n";
+		assertEquals(diffHeader, RawParseUtils.decode(fh.getBuffer()));
+
+		assertEquals(1, fh.getHunks().size());
+		HunkHeader hh = fh.getHunks().get(0);
+
+		EditList el = hh.toEditList();
+		assertEquals(1, el.size());
+
+		Edit e = el.get(0);
+		assertEquals(0, e.getBeginA());
+		assertEquals(1, e.getEndA());
+		assertEquals(0, e.getBeginB());
+		assertEquals(0, e.getEndB());
+		assertEquals(Edit.Type.DELETE, e.getType());
 	}
 
 	@Test
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 3d86cfd..66e7256 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
@@ -241,9 +241,9 @@
 
 	@Test
 	public void testBareRepo() throws Exception {
-		Repository remoteDb = createBareRepository();
-		Repository tempDb = createWorkRepository();
-		try {
+		try (
+				Repository remoteDb = createBareRepository();
+				Repository tempDb = createWorkRepository()) {
 			StringBuilder xmlContent = new StringBuilder();
 			xmlContent
 					.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
@@ -280,9 +280,6 @@
 			String remote = defaultDb.resolve(Constants.HEAD).name();
 			assertEquals("The gitlink should be the same as remote head",
 					remote, gitlink);
-		} finally {
-			tempDb.close();
-			remoteDb.close();
 		}
 	}
 
@@ -366,9 +363,9 @@
 
 	@Test
 	public void testRevisionBare() throws Exception {
-		Repository remoteDb = createBareRepository();
-		Repository tempDb = createWorkRepository();
-		try {
+		try (
+				Repository remoteDb = createBareRepository();
+				Repository tempDb = createWorkRepository()) {
 			StringBuilder xmlContent = new StringBuilder();
 			xmlContent.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
 					.append("<manifest>")
@@ -393,17 +390,14 @@
 			localDb.close();
 			assertEquals("The gitlink is same as remote head",
 					oldCommitId.name(), gitlink);
-		} finally {
-			tempDb.close();
-			remoteDb.close();
 		}
 	}
 
 	@Test
 	public void testCopyFileBare() throws Exception {
-		Repository remoteDb = createBareRepository();
-		Repository tempDb = createWorkRepository();
-		try {
+		try (
+				Repository remoteDb = createBareRepository();
+				Repository tempDb = createWorkRepository()) {
 			StringBuilder xmlContent = new StringBuilder();
 			xmlContent
 					.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
@@ -435,17 +429,14 @@
 			reader.close();
 			assertEquals("The Hello file should have expected content",
 					"branch world", content);
-		} finally {
-			tempDb.close();
-			remoteDb.close();
 		}
 	}
 
 	@Test
 	public void testReplaceManifestBare() throws Exception {
-		Repository remoteDb = createBareRepository();
-		Repository tempDb = createWorkRepository();
-		try {
+		try (
+				Repository remoteDb = createBareRepository();
+				Repository tempDb = createWorkRepository()) {
 			StringBuilder xmlContent = new StringBuilder();
 			xmlContent
 					.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
@@ -512,17 +503,14 @@
 			reader.close();
 			assertTrue("The bar submodule should exist", bar);
 			assertFalse("The foo submodule shouldn't exist", foo);
-		} finally {
-			tempDb.close();
-			remoteDb.close();
 		}
 	}
 
 	@Test
 	public void testRemoveOverlappingBare() throws Exception {
-		Repository remoteDb = createBareRepository();
-		Repository tempDb = createWorkRepository();
-		try {
+		try (
+				Repository remoteDb = createBareRepository();
+				Repository tempDb = createWorkRepository()) {
 			StringBuilder xmlContent = new StringBuilder();
 			xmlContent
 					.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
@@ -571,9 +559,6 @@
 			assertTrue("The foo submodule should exist", foo);
 			assertFalse("The foo/bar submodule shouldn't exist", foobar);
 			assertTrue("The a submodule should exist", a);
-		} finally {
-			tempDb.close();
-			remoteDb.close();
 		}
 	}
 
@@ -671,6 +656,42 @@
 		assertTrue("We should have foo", file.exists());
 	}
 
+	@Test
+	public void testTargetBranch() throws Exception {
+		try (
+				Repository remoteDb1 = createBareRepository();
+				Repository remoteDb2 = createBareRepository();
+				Repository tempDb = createWorkRepository()) {
+			StringBuilder xmlContent = new StringBuilder();
+			xmlContent
+					.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
+					.append("<manifest>")
+					.append("<remote name=\"remote1\" fetch=\".\" />")
+					.append("<default revision=\"master\" remote=\"remote1\" />")
+					.append("<project path=\"foo\" name=\"").append(defaultUri)
+					.append("\" />").append("</manifest>");
+			JGitTestUtil.writeTrashFile(tempDb, "manifest.xml",
+					xmlContent.toString());
+			RepoCommand command = new RepoCommand(remoteDb1);
+			command
+				.setPath(tempDb.getWorkTree().getAbsolutePath() + "/manifest.xml")
+				.setURI(rootUri)
+				.setTargetBranch("test")
+				.call();
+			ObjectId branchId = remoteDb1.resolve(
+					Constants.R_HEADS + "test^{tree}");
+			command = new RepoCommand(remoteDb2);
+			command
+				.setPath(tempDb.getWorkTree().getAbsolutePath() + "/manifest.xml")
+				.setURI(rootUri)
+				.call();
+			ObjectId defaultId = remoteDb2.resolve(Constants.HEAD + "^{tree}");
+			assertEquals(
+				"The tree id of branch db and default db should be the same",
+				branchId, defaultId);
+		}
+	}
+
 	private void resolveRelativeUris() {
 		// Find the longest common prefix ends with "/" as rootUri.
 		defaultUri = defaultDb.getDirectory().toURI().toString();
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 a4b799a..2c04787 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
@@ -55,6 +55,7 @@
 	@Test
 	public void testSimpleCharClass() {
 		assertMatched("[a]", "a");
+		assertMatched("][a]", "]a");
 		assertMatched("[a]", "a/");
 		assertMatched("[a]", "a/b");
 
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreNodeTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreNodeTest.java
index 571f318..9722ac6 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreNodeTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreNodeTest.java
@@ -44,12 +44,15 @@
 
 import static org.eclipse.jgit.junit.Assert.assertEquals;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 
+import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
 import java.util.Arrays;
 
 import org.eclipse.jgit.errors.CorruptObjectException;
@@ -324,6 +327,15 @@
 	}
 
 	@Test
+	public void testEmptyIgnoreRules() throws IOException {
+		IgnoreNode node = new IgnoreNode();
+		node.parse(writeToString("", "#", "!", "[[=a=]]"));
+		assertEquals(new ArrayList<>(), node.getRules());
+		node.parse(writeToString(" ", " / "));
+		assertEquals(2, node.getRules().size());
+	}
+
+	@Test
 	public void testSlashOnlyMatchesDirectory() throws IOException {
 		writeIgnoreFile(".gitignore", "out/");
 		writeTrashFile("out", "");
@@ -343,6 +355,56 @@
 	}
 
 	@Test
+	public void testSlashMatchesDirectory() throws IOException {
+		writeIgnoreFile(".gitignore", "out2/");
+
+		writeTrashFile("out1/out1", "");
+		writeTrashFile("out1/out2", "");
+		writeTrashFile("out2/out1", "");
+		writeTrashFile("out2/out2", "");
+
+		beginWalk();
+		assertEntry(F, tracked, ".gitignore");
+		assertEntry(D, tracked, "out1");
+		assertEntry(F, tracked, "out1/out1");
+		assertEntry(F, tracked, "out1/out2");
+		assertEntry(D, ignored, "out2");
+		assertEntry(F, ignored, "out2/out1");
+		assertEntry(F, ignored, "out2/out2");
+		endWalk();
+	}
+
+	@Test
+	public void testWildcardWithSlashMatchesDirectory() throws IOException {
+		writeIgnoreFile(".gitignore", "out2*/");
+
+		writeTrashFile("out1/out1.txt", "");
+		writeTrashFile("out1/out2", "");
+		writeTrashFile("out1/out2.txt", "");
+		writeTrashFile("out1/out2x/a", "");
+		writeTrashFile("out2/out1.txt", "");
+		writeTrashFile("out2/out2.txt", "");
+		writeTrashFile("out2x/out1.txt", "");
+		writeTrashFile("out2x/out2.txt", "");
+
+		beginWalk();
+		assertEntry(F, tracked, ".gitignore");
+		assertEntry(D, tracked, "out1");
+		assertEntry(F, tracked, "out1/out1.txt");
+		assertEntry(F, tracked, "out1/out2");
+		assertEntry(F, tracked, "out1/out2.txt");
+		assertEntry(D, ignored, "out1/out2x");
+		assertEntry(F, ignored, "out1/out2x/a");
+		assertEntry(D, ignored, "out2");
+		assertEntry(F, ignored, "out2/out1.txt");
+		assertEntry(F, ignored, "out2/out2.txt");
+		assertEntry(D, ignored, "out2x");
+		assertEntry(F, ignored, "out2x/out1.txt");
+		assertEntry(F, ignored, "out2x/out2.txt");
+		endWalk();
+	}
+
+	@Test
 	public void testWithSlashDoesNotMatchInSubDirectory() throws IOException {
 		writeIgnoreFile(".gitignore", "a/b");
 		writeTrashFile("a/a", "");
@@ -375,6 +437,67 @@
 	}
 
 	@Test
+	public void testLeadingSpaces() throws IOException {
+		writeTrashFile("  a/  a", "");
+		writeTrashFile("  a/ a", "");
+		writeTrashFile("  a/a", "");
+		writeTrashFile(" a/  a", "");
+		writeTrashFile(" a/ a", "");
+		writeTrashFile(" a/a", "");
+		writeIgnoreFile(".gitignore", " a", "  a");
+		writeTrashFile("a/  a", "");
+		writeTrashFile("a/ a", "");
+		writeTrashFile("a/a", "");
+
+		beginWalk();
+		assertEntry(D, ignored, "  a");
+		assertEntry(F, ignored, "  a/  a");
+		assertEntry(F, ignored, "  a/ a");
+		assertEntry(F, ignored, "  a/a");
+		assertEntry(D, ignored, " a");
+		assertEntry(F, ignored, " a/  a");
+		assertEntry(F, ignored, " a/ a");
+		assertEntry(F, ignored, " a/a");
+		assertEntry(F, tracked, ".gitignore");
+		assertEntry(D, tracked, "a");
+		assertEntry(F, ignored, "a/  a");
+		assertEntry(F, ignored, "a/ a");
+		assertEntry(F, tracked, "a/a");
+		endWalk();
+	}
+
+	@Test
+	public void testTrailingSpaces() throws IOException {
+		writeTrashFile("a  /a", "");
+		writeTrashFile("a  /a ", "");
+		writeTrashFile("a  /a  ", "");
+		writeTrashFile("a /a", "");
+		writeTrashFile("a /a ", "");
+		writeTrashFile("a /a  ", "");
+		writeTrashFile("a/a", "");
+		writeTrashFile("a/a ", "");
+		writeTrashFile("a/a  ", "");
+
+		writeIgnoreFile(".gitignore", "a\\ ", "a \\ ");
+
+		beginWalk();
+		assertEntry(F, tracked, ".gitignore");
+		assertEntry(D, ignored, "a  ");
+		assertEntry(F, ignored, "a  /a");
+		assertEntry(F, ignored, "a  /a ");
+		assertEntry(F, ignored, "a  /a  ");
+		assertEntry(D, ignored, "a ");
+		assertEntry(F, ignored, "a /a");
+		assertEntry(F, ignored, "a /a ");
+		assertEntry(F, ignored, "a /a  ");
+		assertEntry(D, tracked, "a");
+		assertEntry(F, tracked, "a/a");
+		assertEntry(F, ignored, "a/a ");
+		assertEntry(F, ignored, "a/a  ");
+		endWalk();
+	}
+
+	@Test
 	public void testToString() throws Exception {
 		assertEquals(Arrays.asList("").toString(), new IgnoreNode().toString());
 		assertEquals(Arrays.asList("hello").toString(),
@@ -411,4 +534,12 @@
 			data.append(line + "\n");
 		writeTrashFile(name, data.toString());
 	}
+
+	private InputStream writeToString(String... rules) throws IOException {
+		StringBuilder data = new StringBuilder();
+		for (String line : rules) {
+			data.append(line + "\n");
+		}
+		return new ByteArrayInputStream(data.toString().getBytes("UTF-8"));
+	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreRuleSpecialCasesTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreRuleSpecialCasesTest.java
index a7a78f8..f8eb126 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreRuleSpecialCasesTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/ignore/IgnoreRuleSpecialCasesTest.java
@@ -830,9 +830,45 @@
 	}
 
 	@Test
+	public void testIgnoredBackslash() throws Exception {
+		// In Git CLI a\b\c is equal to abc
+		assertMatch("a\\b\\c", "abc", true);
+	}
+
+	@Test
 	public void testEscapedBackslash() throws Exception {
 		// In Git CLI a\\b matches a\b file
 		assertMatch("a\\\\b", "a\\b", true);
+		assertMatch("a\\\\b\\c", "a\\bc", true);
+
+	}
+
+	@Test
+	public void testEscapedExclamationMark() throws Exception {
+		assertMatch("\\!b!.txt", "!b!.txt", true);
+		assertMatch("a\\!b!.txt", "a!b!.txt", true);
+	}
+
+	@Test
+	public void testEscapedHash() throws Exception {
+		assertMatch("\\#b", "#b", true);
+		assertMatch("a\\#", "a#", true);
+	}
+
+	@Test
+	public void testEscapedTrailingSpaces() throws Exception {
+		assertMatch("\\ ", " ", true);
+		assertMatch("a\\ ", "a ", true);
+	}
+
+	@Test
+	public void testNotEscapingBackslash() throws Exception {
+		assertMatch("\\out", "out", true);
+		assertMatch("\\out", "a/out", true);
+		assertMatch("c:\\/", "c:/", true);
+		assertMatch("c:\\/", "a/c:/", true);
+		assertMatch("c:\\tmp", "c:tmp", true);
+		assertMatch("c:\\tmp", "a/c:tmp", true);
 	}
 
 	@Test
@@ -841,6 +877,100 @@
 	}
 
 	@Test
+	public void testBackslash() throws Exception {
+		assertMatch("a\\", "a", true);
+		assertMatch("\\a", "a", true);
+		assertMatch("a/\\", "a/", true);
+		assertMatch("a/b\\", "a/b", true);
+		assertMatch("\\a/b", "a/b", true);
+		assertMatch("/\\a", "/a", true);
+		assertMatch("\\a\\b\\c\\", "abc", true);
+		assertMatch("/\\a/\\b/\\c\\", "a/b/c", true);
+
+		// empty path segment doesn't match
+		assertMatch("\\/a", "/a", false);
+		assertMatch("\\/a", "a", false);
+	}
+
+	@Test
+	public void testDollar() throws Exception {
+		assertMatch("$", "$", true);
+		assertMatch("$x", "$x", true);
+		assertMatch("$x", "x$", false);
+		assertMatch("$x", "$", false);
+
+		assertMatch("$x.*", "$x.a", true);
+		assertMatch("*$", "x$", true);
+		assertMatch("*.$", "x.$", true);
+
+		assertMatch("$*x", "$ax", true);
+		assertMatch("x*$", "xa$", true);
+		assertMatch("x*$", "xa", false);
+		assertMatch("[a$b]", "$", true);
+	}
+
+	@Test
+	public void testCaret() throws Exception {
+		assertMatch("^", "^", true);
+		assertMatch("^x", "^x", true);
+		assertMatch("^x", "x^", false);
+		assertMatch("^x", "^", false);
+
+		assertMatch("^x.*", "^x.a", true);
+		assertMatch("*^", "x^", true);
+		assertMatch("*.^", "x.^", true);
+
+		assertMatch("x*^", "xa^", true);
+		assertMatch("^*x", "^ax", true);
+		assertMatch("^*x", "ax", false);
+		assertMatch("[a^b]", "^", true);
+	}
+
+	@Test
+	public void testPlus() throws Exception {
+		assertMatch("+", "+", true);
+		assertMatch("+x", "+x", true);
+		assertMatch("+x", "x+", false);
+		assertMatch("+x", "+", false);
+		assertMatch("x+", "xx", false);
+
+		assertMatch("+x.*", "+x.a", true);
+		assertMatch("*+", "x+", true);
+		assertMatch("*.+", "x.+", true);
+
+		assertMatch("x*+", "xa+", true);
+		assertMatch("+*x", "+ax", true);
+		assertMatch("+*x", "ax", false);
+		assertMatch("[a+b]", "+", true);
+	}
+
+	@Test
+	public void testPipe() throws Exception {
+		assertMatch("|", "|", true);
+		assertMatch("|x", "|x", true);
+		assertMatch("|x", "x|", false);
+		assertMatch("|x", "|", false);
+		assertMatch("x|x", "xx", false);
+
+		assertMatch("x|x.*", "x|x.a", true);
+		assertMatch("*|", "x|", true);
+		assertMatch("*.|", "x.|", true);
+
+		assertMatch("x*|a", "xb|a", true);
+		assertMatch("b|*x", "b|ax", true);
+		assertMatch("b|*x", "ax", false);
+		assertMatch("[a|b]", "|", true);
+	}
+
+	@Test
+	public void testBrackets() throws Exception {
+		assertMatch("{}*()", "{}x()", true);
+		assertMatch("[a{}()b][a{}()b]?[a{}()b][a{}()b]", "{}x()", true);
+		assertMatch("x*{x}3", "xa{x}3", true);
+		assertMatch("a*{x}3", "axxx", false);
+	}
+
+	@Test
 	public void testFilePathSimpleCase() throws Exception {
 		assertFileNameMatch("a/b", "a/b", true);
 	}
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 8dbe644..d66753d 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
@@ -359,6 +359,33 @@
 	}
 
 	@Test
+	public void testFirstExactRef_IgnoresGarbageRef() throws IOException {
+		writeLooseRef("refs/heads/A", A);
+		write(new File(diskRepo.getDirectory(), "refs/heads/bad"), "FAIL\n");
+
+		Ref a = refdir.firstExactRef("refs/heads/bad", "refs/heads/A");
+		assertEquals("refs/heads/A", a.getName());
+		assertEquals(A, a.getObjectId());
+	}
+
+	@Test
+	public void testExactRef_IgnoresGarbageRef() throws IOException {
+		writeLooseRef("refs/heads/A", A);
+		write(new File(diskRepo.getDirectory(), "refs/heads/bad"), "FAIL\n");
+
+		Map<String, Ref> refs =
+				refdir.exactRef("refs/heads/bad", "refs/heads/A");
+
+		assertNull("no refs/heads/bad", refs.get("refs/heads/bad"));
+
+		Ref a = refs.get("refs/heads/A");
+		assertEquals("refs/heads/A", a.getName());
+		assertEquals(A, a.getObjectId());
+
+		assertEquals(1, refs.size());
+	}
+
+	@Test
 	public void testGetRefs_InvalidName() throws IOException {
 		writeLooseRef("refs/heads/A", A);
 
@@ -464,6 +491,21 @@
 	}
 
 	@Test
+	public void testFirstExactRef_Mixed() throws IOException {
+		writeLooseRef("refs/heads/A", A);
+		writePackedRef("refs/tags/v1.0", v1_0);
+
+		Ref a = refdir.firstExactRef("refs/heads/A", "refs/tags/v1.0");
+		Ref one = refdir.firstExactRef("refs/tags/v1.0", "refs/heads/A");
+
+		assertEquals("refs/heads/A", a.getName());
+		assertEquals("refs/tags/v1.0", one.getName());
+
+		assertEquals(A, a.getObjectId());
+		assertEquals(v1_0, one.getObjectId());
+	}
+
+	@Test
 	public void testGetRefs_TagsOnly_AllLoose() throws IOException {
 		Map<String, Ref> tags;
 		Ref a;
@@ -983,6 +1025,25 @@
 	}
 
 	@Test
+	public void testExactRef_EmptyDatabase() throws IOException {
+		Ref r;
+
+		r = refdir.exactRef(HEAD);
+		assertTrue(r.isSymbolic());
+		assertSame(LOOSE, r.getStorage());
+		assertEquals("refs/heads/master", r.getTarget().getName());
+		assertSame(NEW, r.getTarget().getStorage());
+		assertNull(r.getTarget().getObjectId());
+
+		assertNull(refdir.exactRef("refs/heads/master"));
+		assertNull(refdir.exactRef("refs/tags/v1.0"));
+		assertNull(refdir.exactRef("FETCH_HEAD"));
+		assertNull(refdir.exactRef("NOT.A.REF.NAME"));
+		assertNull(refdir.exactRef("master"));
+		assertNull(refdir.exactRef("v1.0"));
+	}
+
+	@Test
 	public void testGetRef_FetchHead() throws IOException {
 		// This is an odd special case where we need to make sure we read
 		// exactly the first 40 bytes of the file and nothing further on
@@ -1000,6 +1061,23 @@
 	}
 
 	@Test
+	public void testExactRef_FetchHead() throws IOException {
+		// This is an odd special case where we need to make sure we read
+		// exactly the first 40 bytes of the file and nothing further on
+		// that line, or the remainder of the file.
+		write(new File(diskRepo.getDirectory(), "FETCH_HEAD"), A.name()
+				+ "\tnot-for-merge"
+				+ "\tbranch 'master' of git://egit.eclipse.org/jgit\n");
+
+		Ref r = refdir.exactRef("FETCH_HEAD");
+		assertFalse(r.isSymbolic());
+		assertEquals(A, r.getObjectId());
+		assertEquals("FETCH_HEAD", r.getName());
+		assertFalse(r.isPeeled());
+		assertNull(r.getPeeledObjectId());
+	}
+
+	@Test
 	public void testGetRef_AnyHeadWithGarbage() throws IOException {
 		write(new File(diskRepo.getDirectory(), "refs/heads/A"), A.name()
 				+ "012345 . this is not a standard reference\n"
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java
index db31fd3..6238a35 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java
@@ -51,7 +51,6 @@
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
@@ -503,27 +502,6 @@
 	}
 
 	@Test
-	public void testEmptyString() throws ConfigInvalidException {
-		Config c = parse("[my]\n\tempty =\n");
-		assertNull(c.getString("my", null, "empty"));
-
-		String[] values = c.getStringList("my", null, "empty");
-		assertNotNull(values);
-		assertEquals(1, values.length);
-		assertNull(values[0]);
-
-		// always matches the default, because its non-boolean
-		assertTrue(c.getBoolean("my", "empty", true));
-		assertFalse(c.getBoolean("my", "empty", false));
-
-		assertEquals("[my]\n\tempty =\n", c.toText());
-
-		c = new Config();
-		c.setStringList("my", null, "empty", Arrays.asList(values));
-		assertEquals("[my]\n\tempty =\n", c.toText());
-	}
-
-	@Test
 	public void testUnsetBranchSection() throws ConfigInvalidException {
 		Config c = parse("" //
 				+ "[branch \"keep\"]\n"
@@ -699,6 +677,68 @@
 		assertEquals("1", c.getString("a", null, "y"));
 	}
 
+	@Test
+	public void testExplicitlySetEmptyString() throws Exception {
+		Config c = new Config();
+		c.setString("a", null, "x", "0");
+		c.setString("a", null, "y", "");
+
+		assertEquals("0", c.getString("a", null, "x"));
+		assertEquals(0, c.getInt("a", null, "x", 1));
+
+		assertEquals("", c.getString("a", null, "y"));
+		assertArrayEquals(new String[]{""}, c.getStringList("a", null, "y"));
+		try {
+			c.getInt("a", null, "y", 1);
+		} catch (IllegalArgumentException e) {
+			assertEquals("Invalid integer value: a.y=", e.getMessage());
+		}
+
+		assertNull(c.getString("a", null, "z"));
+		assertArrayEquals(new String[]{}, c.getStringList("a", null, "z"));
+	}
+
+	@Test
+	public void testParsedEmptyString() throws Exception {
+		Config c = parse("[a]\n"
+				+ "x = 0\n"
+				+ "y =\n");
+
+		assertEquals("0", c.getString("a", null, "x"));
+		assertEquals(0, c.getInt("a", null, "x", 1));
+
+		assertNull(c.getString("a", null, "y"));
+		assertArrayEquals(new String[]{null}, c.getStringList("a", null, "y"));
+		try {
+			c.getInt("a", null, "y", 1);
+		} catch (IllegalArgumentException e) {
+			assertEquals("Invalid integer value: a.y=", e.getMessage());
+		}
+
+		assertNull(c.getString("a", null, "z"));
+		assertArrayEquals(new String[]{}, c.getStringList("a", null, "z"));
+	}
+
+	@Test
+	public void testSetStringListWithEmptyValue() throws Exception {
+		Config c = new Config();
+		c.setStringList("a", null, "x", Arrays.asList(""));
+		assertArrayEquals(new String[]{""}, c.getStringList("a", null, "x"));
+	}
+
+	@Test
+	public void testEmptyValueAtEof() throws Exception {
+		String text = "[a]\nx =";
+		Config c = parse(text);
+		assertNull(c.getString("a", null, "x"));
+		assertArrayEquals(new String[]{null},
+				c.getStringList("a", null, "x"));
+		c = parse(text + "\n");
+		assertNull(c.getString("a", null, "x"));
+		assertArrayEquals(new String[]{null},
+				c.getStringList("a", null, "x"));
+	}
+
 	private static void assertReadLong(long exp) throws ConfigInvalidException {
 		assertReadLong(exp, String.valueOf(exp));
 	}
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 274757d..3abe81c 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
@@ -1810,6 +1810,17 @@
 	}
 
 	@Test
+	public void testBug477090() throws CorruptObjectException {
+		checker.setSafeForMacOS(true);
+		final byte[] bytes = {
+				// U+221E 0xe2889e INFINITY ∞
+				(byte) 0xe2, (byte) 0x88, (byte) 0x9e,
+				// .html
+				0x2e, 0x68, 0x74, 0x6d, 0x6c };
+		checker.checkPathSegment(bytes, 0, bytes.length);
+	}
+
+	@Test
 	public void testRejectDotAtEndOnWindows() {
 		checker.setSafeForWindows(true);
 		try {
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectIdTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectIdTest.java
index abf57d6..2198b87 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectIdTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ObjectIdTest.java
@@ -49,6 +49,8 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+import org.eclipse.jgit.errors.InvalidObjectIdException;
+
 import org.junit.Test;
 
 public class ObjectIdTest {
@@ -124,6 +126,21 @@
 		assertEquals(x.toLowerCase(), oid.name());
 	}
 
+	@Test(expected = InvalidObjectIdException.class)
+	public void testFromString_short() {
+		ObjectId.fromString("cafe1234");
+	}
+
+	@Test(expected = InvalidObjectIdException.class)
+	public void testFromString_nonHex() {
+		ObjectId.fromString("0123456789abcdefghij0123456789abcdefghij");
+	}
+
+	@Test(expected = InvalidObjectIdException.class)
+	public void testFromString_shortNonHex() {
+		ObjectId.fromString("6789ghij");
+	}
+
 	@Test
 	public void testGetByte() {
 		byte[] raw = new byte[20];
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RefTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RefTest.java
index f2ed684..109f401 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RefTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RefTest.java
@@ -84,6 +84,12 @@
 		}
 	}
 
+	private void writeNewRef(String name, ObjectId value) throws IOException {
+		RefUpdate updateRef = db.updateRef(name);
+		updateRef.setNewObjectId(value);
+		assertEquals(RefUpdate.Result.NEW, updateRef.update());
+	}
+
 	@Test
 	public void testRemoteNames() throws Exception {
 		FileBasedConfig config = db.getConfig();
@@ -192,6 +198,50 @@
 		assertEquals(Storage.LOOSE, ref.getStorage());
 	}
 
+	@Test
+	public void testGetShortRef() throws IOException {
+		Ref ref = db.getRef("master");
+		assertEquals("refs/heads/master", ref.getName());
+		assertEquals(db.resolve("refs/heads/master"), ref.getObjectId());
+	}
+
+	@Test
+	public void testGetShortExactRef() throws IOException {
+		assertNull(db.getRefDatabase().exactRef("master"));
+
+		Ref ref = db.getRefDatabase().exactRef("HEAD");
+		assertEquals("HEAD", ref.getName());
+		assertEquals("refs/heads/master", ref.getTarget().getName());
+		assertEquals(db.resolve("refs/heads/master"), ref.getObjectId());
+	}
+
+	@Test
+	public void testRefsUnderRefs() throws IOException {
+		ObjectId masterId = db.resolve("refs/heads/master");
+		writeNewRef("refs/heads/refs/foo/bar", masterId);
+
+		assertNull(db.getRefDatabase().exactRef("refs/foo/bar"));
+
+		Ref ref = db.getRef("refs/foo/bar");
+		assertEquals("refs/heads/refs/foo/bar", ref.getName());
+		assertEquals(db.resolve("refs/heads/master"), ref.getObjectId());
+	}
+
+	@Test
+	public void testAmbiguousRefsUnderRefs() throws IOException {
+		ObjectId masterId = db.resolve("refs/heads/master");
+		writeNewRef("refs/foo/bar", masterId);
+		writeNewRef("refs/heads/refs/foo/bar", masterId);
+
+		Ref exactRef = db.getRefDatabase().exactRef("refs/foo/bar");
+		assertEquals("refs/foo/bar", exactRef.getName());
+		assertEquals(masterId, exactRef.getObjectId());
+
+		Ref ref = db.getRef("refs/foo/bar");
+		assertEquals("refs/foo/bar", ref.getName());
+		assertEquals(masterId, ref.getObjectId());
+	}
+
 	/**
 	 * Let an "outsider" create a loose ref with the same name as a packed one
 	 *
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RepositoryCacheTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RepositoryCacheTest.java
index 0cab987..6c62925 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RepositoryCacheTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RepositoryCacheTest.java
@@ -43,11 +43,13 @@
 
 package org.eclipse.jgit.lib;
 
+import static org.hamcrest.CoreMatchers.hasItem;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNotSame;
 import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
@@ -147,4 +149,28 @@
 		d2.close();
 		d2.close();
 	}
+
+	@Test
+	public void testGetRegisteredWhenEmpty() {
+		assertEquals(0, RepositoryCache.getRegisteredKeys().size());
+	}
+
+	@Test
+	public void testGetRegistered() {
+		RepositoryCache.register(db);
+
+		assertThat(RepositoryCache.getRegisteredKeys(),
+				hasItem(FileKey.exact(db.getDirectory(), db.getFS())));
+		assertEquals(1, RepositoryCache.getRegisteredKeys().size());
+	}
+
+	@Test
+	public void testUnregister() {
+		RepositoryCache.register(db);
+		RepositoryCache
+				.unregister(FileKey.exact(db.getDirectory(), db.getFS()));
+
+		assertEquals(0, RepositoryCache.getRegisteredKeys().size());
+	}
+
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/RecursiveMergerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/RecursiveMergerTest.java
index 19e495b..7ef6448 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/RecursiveMergerTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/RecursiveMergerTest.java
@@ -60,6 +60,7 @@
 import org.eclipse.jgit.errors.NoMergeBaseException;
 import org.eclipse.jgit.errors.NoMergeBaseException.MergeBaseFailureReason;
 import org.eclipse.jgit.internal.storage.file.FileRepository;
+import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
 import org.eclipse.jgit.junit.RepositoryTestCase;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.junit.TestRepository.BranchBuilder;
@@ -308,7 +309,7 @@
 			if (indexState != IndexState.Bare)
 				assertEquals(
 						"[f, mode:100644, content:1-master\n2\n3-res(master)\n4\n5\n6\n7-res(side)\n8\n9-side\n]",
-						indexState(RepositoryTestCase.CONTENT));
+						indexState(LocalDiskRepositoryTestCase.CONTENT));
 			if (worktreeState != WorktreeState.Bare
 					&& worktreeState != WorktreeState.Missing)
 				assertEquals(
@@ -393,7 +394,7 @@
 			if (indexState != IndexState.Bare)
 				assertEquals(
 						"[f, mode:100644, content:1-master-r\n2\n3-side-r\n]",
-						indexState(RepositoryTestCase.CONTENT));
+						indexState(LocalDiskRepositoryTestCase.CONTENT));
 			if (worktreeState != WorktreeState.Bare
 					&& worktreeState != WorktreeState.Missing)
 				assertEquals(
@@ -478,7 +479,7 @@
 			if (indexState != IndexState.Bare)
 				assertEquals(
 						"[f, mode:100644, content:1\nx(side)\n2\n3\ny(side-again)\n]",
-						indexState(RepositoryTestCase.CONTENT));
+						indexState(LocalDiskRepositoryTestCase.CONTENT));
 			if (worktreeState != WorktreeState.Bare
 					&& worktreeState != WorktreeState.Missing)
 				assertEquals("1\nx(side)\n2\n3\ny(side-again)\n", read("f"));
@@ -561,7 +562,7 @@
 			if (indexState != IndexState.Bare)
 				assertEquals(
 						"[f, mode:100644, content:1-master-r\n2\n3-side-r\n][m.c, mode:100644, content:0][m.m, mode:100644, content:1][s.c, mode:100644, content:0][s.m, mode:100644, content:1]",
-						indexState(RepositoryTestCase.CONTENT));
+						indexState(LocalDiskRepositoryTestCase.CONTENT));
 			if (worktreeState != WorktreeState.Bare
 					&& worktreeState != WorktreeState.Missing) {
 				assertEquals(
@@ -638,7 +639,7 @@
 						"[f, mode:100644, stage:1, content:1-master\n2\n3\n4\n5\n6\n7\n8\n9-side\n]"
 								+ "[f, mode:100644, stage:2, content:1-master\n2\n3\n4\n5\n6\n7-conflict\n8\n9-side\n]"
 								+ "[f, mode:100644, stage:3, content:1-master\n2\n3\n4\n5\n6\n7-res(side)\n8\n9-side\n]",
-						indexState(RepositoryTestCase.CONTENT));
+						indexState(LocalDiskRepositoryTestCase.CONTENT));
 				assertEquals(
 						"1-master\n2\n3\n4\n5\n6\n<<<<<<< OURS\n7-conflict\n=======\n7-res(side)\n>>>>>>> THEIRS\n8\n9-side\n",
 						read("f"));
@@ -736,7 +737,7 @@
 			if (indexState != IndexState.Bare)
 				assertEquals(
 						"[f, mode:100644, content:1-master\n2\n3-res(master)\n4\n5-other\n6\n7-res(side)\n8\n9-side\n]",
-						indexState(RepositoryTestCase.CONTENT));
+						indexState(LocalDiskRepositoryTestCase.CONTENT));
 			if (worktreeState != WorktreeState.Bare
 					&& worktreeState != WorktreeState.Missing)
 				assertEquals(
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/ResolveMergerTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/ResolveMergerTest.java
index 478a93b..cd6a4be 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/ResolveMergerTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/merge/ResolveMergerTest.java
@@ -227,6 +227,80 @@
 	}
 
 	/**
+	 * A tracked file is replaced by a folder in THEIRS.
+	 *
+	 * @param strategy
+	 * @throws Exception
+	 */
+	@Theory
+	public void checkFileReplacedByFolderInTheirs(MergeStrategy strategy)
+			throws Exception {
+		Git git = Git.wrap(db);
+
+		writeTrashFile("sub", "file");
+		git.add().addFilepattern("sub").call();
+		RevCommit first = git.commit().setMessage("initial").call();
+
+		git.checkout().setCreateBranch(true).setStartPoint(first)
+				.setName("side").call();
+
+		git.rm().addFilepattern("sub").call();
+		writeTrashFile("sub/file", "subfile");
+		git.add().addFilepattern("sub/file").call();
+		RevCommit masterCommit = git.commit().setMessage("file -> folder")
+				.call();
+
+		git.checkout().setName("master").call();
+		writeTrashFile("noop", "other");
+		git.add().addFilepattern("noop").call();
+		git.commit().setAll(true).setMessage("noop").call();
+
+		MergeResult mergeRes = git.merge().setStrategy(strategy)
+				.include(masterCommit).call();
+		assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
+		assertEquals(
+				"[noop, mode:100644, content:other][sub/file, mode:100644, content:subfile]",
+				indexState(CONTENT));
+	}
+
+	/**
+	 * A tracked file is replaced by a folder in OURS.
+	 *
+	 * @param strategy
+	 * @throws Exception
+	 */
+	@Theory
+	public void checkFileReplacedByFolderInOurs(MergeStrategy strategy)
+			throws Exception {
+		Git git = Git.wrap(db);
+
+		writeTrashFile("sub", "file");
+		git.add().addFilepattern("sub").call();
+		RevCommit first = git.commit().setMessage("initial").call();
+
+		git.checkout().setCreateBranch(true).setStartPoint(first)
+				.setName("side").call();
+		writeTrashFile("noop", "other");
+		git.add().addFilepattern("noop").call();
+		RevCommit sideCommit = git.commit().setAll(true).setMessage("noop")
+				.call();
+
+		git.checkout().setName("master").call();
+		git.rm().addFilepattern("sub").call();
+		writeTrashFile("sub/file", "subfile");
+		git.add().addFilepattern("sub/file").call();
+		git.commit().setMessage("file -> folder")
+				.call();
+
+		MergeResult mergeRes = git.merge().setStrategy(strategy)
+				.include(sideCommit).call();
+		assertEquals(MergeStatus.MERGED, mergeRes.getMergeStatus());
+		assertEquals(
+				"[noop, mode:100644, content:other][sub/file, mode:100644, content:subfile]",
+				indexState(CONTENT));
+	}
+
+	/**
 	 * An existing directory without tracked content should not prevent merging
 	 * a file with that name.
 	 *
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleInitTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleInitTest.java
index 22e55fe..2b46498 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleInitTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleInitTest.java
@@ -246,9 +246,8 @@
 		if (File.separatorChar == '\\')
 			base = base.replace('\\', '/');
 		FileBasedConfig config = db.getConfig();
-		config.setString(ConfigConstants.CONFIG_REMOTE_SECTION,
-				Constants.DEFAULT_REMOTE_NAME, ConfigConstants.CONFIG_KEY_URL,
-				null);
+		config.unset(ConfigConstants.CONFIG_REMOTE_SECTION,
+				Constants.DEFAULT_REMOTE_NAME, ConfigConstants.CONFIG_KEY_URL);
 		config.save();
 
 		SubmoduleWalk generator = SubmoduleWalk.forIndex(db);
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleWalkTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleWalkTest.java
index f7acaa7..72b4611 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleWalkTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/submodule/SubmoduleWalkTest.java
@@ -65,6 +65,7 @@
 import org.eclipse.jgit.dircache.DirCacheEntry;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.NoWorkTreeException;
+import org.eclipse.jgit.internal.storage.file.FileRepository;
 import org.eclipse.jgit.junit.RepositoryTestCase;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
@@ -101,6 +102,13 @@
 	}
 
 	@Test
+	public void bareRepositoryWithNoSubmodules() throws IOException {
+		FileRepository bareRepo = createBareRepository();
+		boolean result = SubmoduleWalk.containsGitModulesFile(bareRepo);
+		assertFalse(result);
+	}
+
+	@Test
 	public void repositoryWithRootLevelSubmodule() throws IOException,
 			ConfigInvalidException, NoWorkTreeException, GitAPIException {
 		final ObjectId id = ObjectId
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BaseReceivePackTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BaseReceivePackTest.java
new file mode 100644
index 0000000..7578c6e
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BaseReceivePackTest.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2015, Google Inc.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.transport;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import org.eclipse.jgit.errors.PackProtocolException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Test;
+
+/** Tests for base receive-pack utilities. */
+public class BaseReceivePackTest {
+	@Test
+	public void parseCommand() throws Exception {
+		String o = "0000000000000000000000000000000000000000";
+		String n = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
+		String r = "refs/heads/master";
+		ReceiveCommand cmd = BaseReceivePack.parseCommand(o + " " + n + " " + r);
+		assertEquals(ObjectId.zeroId(), cmd.getOldId());
+		assertEquals("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
+				cmd.getNewId().name());
+		assertEquals("refs/heads/master", cmd.getRefName());
+
+		assertParseCommandFails(null);
+		assertParseCommandFails("");
+		assertParseCommandFails(o.substring(35) + " " + n.substring(35)
+				+ " " + r + "\n");
+		assertParseCommandFails(o + " " + n + " " + r + "\n");
+		assertParseCommandFails(o + " " + n + " " + "refs^foo");
+		assertParseCommandFails(o + " " + n.substring(10) + " " + r);
+		assertParseCommandFails(o.substring(10) + " " + n + " " + r);
+		assertParseCommandFails("X" + o.substring(1) + " " + n + " " + r);
+		assertParseCommandFails(o + " " + "X" + n.substring(1) + " " + r);
+	}
+
+	private void assertParseCommandFails(String input) {
+		try {
+			BaseReceivePack.parseCommand(input);
+			fail();
+		} catch (PackProtocolException e) {
+			// Expected.
+		}
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BundleWriterTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BundleWriterTest.java
index 24cee0a..ba89d2d 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BundleWriterTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/BundleWriterTest.java
@@ -147,6 +147,18 @@
 		}
 	}
 
+	@Test
+	public void testAbortWrite() throws Exception {
+		boolean caught = false;
+		try {
+			makeBundleWithCallback(
+					"refs/heads/aa", db.resolve("a").name(), null, false);
+		} catch (WriteAbortedException e) {
+			caught = true;
+		}
+		assertTrue(caught);
+	}
+
 	private static FetchResult fetchFromBundle(final Repository newRepo,
 			final byte[] bundle) throws URISyntaxException,
 			NotSupportedException, TransportException {
@@ -161,9 +173,17 @@
 	private byte[] makeBundle(final String name,
 			final String anObjectToInclude, final RevCommit assume)
 			throws FileNotFoundException, IOException {
+		return makeBundleWithCallback(name, anObjectToInclude, assume, true);
+	}
+
+	private byte[] makeBundleWithCallback(final String name,
+			final String anObjectToInclude, final RevCommit assume,
+			boolean value)
+			throws FileNotFoundException, IOException {
 		final BundleWriter bw;
 
 		bw = new BundleWriter(db);
+		bw.setObjectCountCallback(new NaiveObjectCountCallback(value));
 		bw.include(name, ObjectId.fromString(anObjectToInclude));
 		if (assume != null)
 			bw.assume(assume);
@@ -172,4 +192,19 @@
 		return out.toByteArray();
 	}
 
+	private static class NaiveObjectCountCallback
+			implements ObjectCountCallback {
+		private final boolean value;
+
+		NaiveObjectCountCallback(boolean value) {
+			this.value = value;
+		}
+
+		@Override
+		public void setObjectCount(long unused) throws WriteAbortedException {
+			if (!value)
+				throw new WriteAbortedException();
+		}
+	}
+
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/HMACSHA1NonceGeneratorTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/HMACSHA1NonceGeneratorTest.java
new file mode 100644
index 0000000..1e79b7a
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/HMACSHA1NonceGeneratorTest.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2015, Google Inc.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *	 notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *	 copyright notice, this list of conditions and the following
+ *	 disclaimer in the documentation and/or other materials provided
+ *	 with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *	 names of its contributors may be used to endorse or promote
+ *	 products derived from this software without specific prior
+ *	 written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.transport;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.PushCertificate.NonceStatus;
+import org.junit.Before;
+import org.junit.Test;
+
+/** Test for HMAC SHA-1 certificate verifier. */
+public class HMACSHA1NonceGeneratorTest {
+	private static final long TS = 1433954361;
+
+	private HMACSHA1NonceGenerator gen;
+	private Repository db;
+
+	@Before
+	public void setUp() {
+		gen = new HMACSHA1NonceGenerator("sekret");
+		db = new InMemoryRepository(new DfsRepositoryDescription("db"));
+	}
+
+	@Test
+	public void missing() throws Exception {
+		assertEquals(NonceStatus.MISSING, gen.verify("", "1234", db, false, 0));
+	}
+
+	@Test
+	public void unsolicited() throws Exception {
+		assertEquals(NonceStatus.UNSOLICITED, gen.verify("1234", "", db, false, 0));
+	}
+
+	@Test
+	public void invalidFormat() throws Exception {
+		String sent = gen.createNonce(db, TS);
+		int idx = sent.indexOf('-');
+		String sig = sent.substring(idx, sent.length() - idx);
+		assertEquals(NonceStatus.BAD,
+				gen.verify(Long.toString(TS), sent, db, true, 100));
+		assertEquals(NonceStatus.BAD, gen.verify(sig, sent, db, true, 100));
+		assertEquals(NonceStatus.BAD, gen.verify("xxx-" + sig, sent, db, true, 100));
+		assertEquals(NonceStatus.BAD, gen.verify(sent, "xxx-" + sig, db, true, 100));
+	}
+
+	@Test
+	public void slop() throws Exception {
+		String sent = gen.createNonce(db, TS - 10);
+		String received = gen.createNonce(db, TS);
+		assertEquals(NonceStatus.BAD,
+				gen.verify(received, sent, db, false, 0));
+		assertEquals(NonceStatus.BAD,
+				gen.verify(received, sent, db, false, 11));
+		assertEquals(NonceStatus.SLOP,
+				gen.verify(received, sent, db, true, 0));
+		assertEquals(NonceStatus.SLOP,
+				gen.verify(received, sent, db, true, 9));
+		assertEquals(NonceStatus.OK,
+				gen.verify(received, sent, db, true, 10));
+		assertEquals(NonceStatus.OK,
+				gen.verify(received, sent, db, true, 11));
+	}
+
+	@Test
+	public void ok() throws Exception {
+		String sent = gen.createNonce(db, TS);
+		assertEquals(NonceStatus.OK, gen.verify(sent, sent, db, false, 0));
+	}
+
+	@Test
+	public void signedByDifferentKey() throws Exception {
+		HMACSHA1NonceGenerator other = new HMACSHA1NonceGenerator("other");
+		String sent = gen.createNonce(db, TS);
+		String received = other.createNonce(db, TS);
+		assertNotEquals(received, sent);
+		assertEquals(NonceStatus.BAD,
+				gen.verify(received, sent, db, false, 0));
+	}
+
+	@Test
+	public void signedByDifferentKeyWithSlop() throws Exception {
+		HMACSHA1NonceGenerator other = new HMACSHA1NonceGenerator("other");
+		String sent = gen.createNonce(db, TS - 10);
+		String received = other.createNonce(db, TS);
+		assertEquals(NonceStatus.BAD, gen.verify(received, sent, db, true, 100));
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateIdentTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateIdentTest.java
new file mode 100644
index 0000000..68aff72
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateIdentTest.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2015, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.transport;
+
+import static org.eclipse.jgit.transport.PushCertificateIdent.parse;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import java.util.Date;
+import java.util.TimeZone;
+
+import org.eclipse.jgit.lib.PersonIdent;
+import org.junit.Test;
+
+public class PushCertificateIdentTest {
+	@Test
+	public void parseValid() throws Exception {
+		String raw = "A U. Thor <a_u_thor@example.com> 1218123387 +0700";
+		PushCertificateIdent ident = parse(raw);
+		assertEquals(raw, ident.getRaw());
+		assertEquals("A U. Thor <a_u_thor@example.com>", ident.getUserId());
+		assertEquals("A U. Thor", ident.getName());
+		assertEquals("a_u_thor@example.com", ident.getEmailAddress());
+		assertEquals(1218123387000L, ident.getWhen().getTime());
+		assertEquals(TimeZone.getTimeZone("GMT+0700"), ident.getTimeZone());
+		assertEquals(7 * 60, ident.getTimeZoneOffset());
+	}
+
+	@Test
+	public void trimName() throws Exception {
+		String name = "A U. Thor";
+		String email = "a_u_thor@example.com";
+		String rest = "<a_u_thor@example.com> 1218123387 +0700";
+
+		checkNameEmail(name, email, name + rest);
+		checkNameEmail(name, email, " " + name + rest);
+		checkNameEmail(name, email, "  " + name + rest);
+		checkNameEmail(name, email, name + " " + rest);
+		checkNameEmail(name, email, name + "  " + rest);
+		checkNameEmail(name, email, " " + name + " " + rest);
+	}
+
+	@Test
+	public void noEmail() throws Exception {
+		String name = "A U. Thor";
+		String rest = " 1218123387 +0700";
+
+		checkNameEmail(name, null, name + rest);
+		checkNameEmail(name, null, " " + name + rest);
+		checkNameEmail(name, null, "  " + name + rest);
+		checkNameEmail(name, null, name + " " + rest);
+		checkNameEmail(name, null, name + "  " + rest);
+		checkNameEmail(name, null, " " + name + " " + rest);
+	}
+
+	@Test
+	public void exoticUserId() throws Exception {
+		String rest = " 218123387 +0700";
+		assertEquals("", parse(rest).getUserId());
+
+		String id = "foo\n\0bar\uabcd\n ";
+		assertEquals(id, parse(id + rest).getUserId());
+	}
+
+	@Test
+	public void fuzzyCasesMatchPersonIdent() throws Exception {
+		// See RawParseUtils_ParsePersonIdentTest#testParsePersonIdent_fuzzyCases()
+		Date when = new Date(1234567890000l);
+		TimeZone tz = TimeZone.getTimeZone("GMT-7");
+
+		assertMatchesPersonIdent(
+				"A U Thor <author@example.com>,  C O. Miter <comiter@example.com> 1234567890 -0700",
+				new PersonIdent("A U Thor", "author@example.com", when, tz),
+				"A U Thor <author@example.com>,  C O. Miter <comiter@example.com>");
+		assertMatchesPersonIdent(
+				"A U Thor <author@example.com> and others 1234567890 -0700",
+				new PersonIdent("A U Thor", "author@example.com", when, tz),
+				"A U Thor <author@example.com> and others");
+	}
+
+	@Test
+	public void incompleteCasesMatchPersonIdent() throws Exception {
+		// See RawParseUtils_ParsePersonIdentTest#testParsePersonIdent_incompleteCases()
+		Date when = new Date(1234567890000l);
+		TimeZone tz = TimeZone.getTimeZone("GMT-7");
+
+		assertMatchesPersonIdent(
+				"Me <> 1234567890 -0700",
+				new PersonIdent("Me", "", when, tz),
+				"Me <>");
+		assertMatchesPersonIdent(
+				" <me@example.com> 1234567890 -0700",
+				new PersonIdent("", "me@example.com", when, tz),
+				" <me@example.com>");
+		assertMatchesPersonIdent(
+				" <> 1234567890 -0700",
+				new PersonIdent("", "", when, tz),
+				" <>");
+		assertMatchesPersonIdent(
+				"<>",
+				new PersonIdent("", "", 0, 0),
+				"<>");
+		assertMatchesPersonIdent(
+				" <>",
+				new PersonIdent("", "", 0, 0),
+				" <>");
+		assertMatchesPersonIdent(
+				"<me@example.com>",
+				new PersonIdent("", "me@example.com", 0, 0),
+				"<me@example.com>");
+		assertMatchesPersonIdent(
+				" <me@example.com>",
+				new PersonIdent("", "me@example.com", 0, 0),
+				" <me@example.com>");
+		assertMatchesPersonIdent(
+				"Me <>",
+				new PersonIdent("Me", "", 0, 0),
+				"Me <>");
+		assertMatchesPersonIdent(
+				"Me <me@example.com>",
+				new PersonIdent("Me", "me@example.com", 0, 0),
+				"Me <me@example.com>");
+		assertMatchesPersonIdent(
+				"Me <me@example.com> 1234567890",
+				new PersonIdent("Me", "me@example.com", 0, 0),
+				"Me <me@example.com>");
+		assertMatchesPersonIdent(
+				"Me <me@example.com> 1234567890 ",
+				new PersonIdent("Me", "me@example.com", 0, 0),
+				"Me <me@example.com>");
+	}
+
+	private static void assertMatchesPersonIdent(String raw,
+			PersonIdent expectedPersonIdent, String expectedUserId) {
+		PushCertificateIdent certIdent = PushCertificateIdent.parse(raw);
+		assertNotNull(raw);
+		assertEquals(raw, certIdent.getRaw());
+		assertEquals(expectedPersonIdent.getName(), certIdent.getName());
+		assertEquals(expectedPersonIdent.getEmailAddress(),
+				certIdent.getEmailAddress());
+		assertEquals(expectedPersonIdent.getWhen(), certIdent.getWhen());
+		assertEquals(expectedPersonIdent.getTimeZoneOffset(),
+				certIdent.getTimeZoneOffset());
+		assertEquals(expectedUserId, certIdent.getUserId());
+	}
+
+	private static void checkNameEmail(String expectedName, String expectedEmail,
+			String raw) {
+		PushCertificateIdent ident = parse(raw);
+		assertNotNull(ident);
+		assertEquals(raw, ident.getRaw());
+		assertEquals(expectedName, ident.getName());
+		assertEquals(expectedEmail, ident.getEmailAddress());
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateParserTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateParserTest.java
new file mode 100644
index 0000000..0647167
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateParserTest.java
@@ -0,0 +1,388 @@
+/*
+ * Copyright (C) 2015, Google Inc.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *	 notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *	 copyright notice, this list of conditions and the following
+ *	 disclaimer in the documentation and/or other materials provided
+ *	 with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *	 names of its contributors may be used to endorse or promote
+ *	 products derived from this software without specific prior
+ *	 written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.transport;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.ByteArrayInputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.StringReader;
+
+import org.eclipse.jgit.errors.PackProtocolException;
+import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.PushCertificate.NonceStatus;
+import org.junit.Before;
+import org.junit.Test;
+
+/** Test for push certificate parsing. */
+public class PushCertificateParserTest {
+	// Example push certificate generated by C git 2.2.0.
+	private static final String INPUT = "001ccertificate version 0.1\n"
+			+ "0041pusher Dave Borowitz <dborowitz@google.com> 1433954361 -0700\n"
+			+ "0024pushee git://localhost/repo.git\n"
+			+ "002anonce 1433954361-bde756572d665bba81d8\n"
+			+ "0005\n"
+			+ "00680000000000000000000000000000000000000000"
+			+ " 6c2b981a177396fb47345b7df3e4d3f854c6bea7"
+			+ " refs/heads/master\n"
+			+ "0022-----BEGIN PGP SIGNATURE-----\n"
+			+ "0016Version: GnuPG v1\n"
+			+ "0005\n"
+			+ "0045iQEcBAABAgAGBQJVeGg5AAoJEPfTicJkUdPkUggH/RKAeI9/i/LduuiqrL/SSdIa\n"
+			+ "00459tYaSqJKLbXz63M/AW4Sp+4u+dVCQvnAt/a35CVEnpZz6hN4Kn/tiswOWVJf4CO7\n"
+			+ "0045htNubGs5ZMwvD6sLYqKAnrM3WxV/2TbbjzjZW6Jkidz3jz/WRT4SmjGYiEO7aA+V\n"
+			+ "00454ZdIS9f7sW5VsHHYlNThCA7vH8Uu48bUovFXyQlPTX0pToSgrWV3JnTxDNxfn3iG\n"
+			+ "0045IL0zTY/qwVCdXgFownLcs6J050xrrBWIKqfcWr3u4D2aCLyR0v+S/KArr7ulZygY\n"
+			+ "0045+SOklImn8TAZiNxhWtA6ens66IiammUkZYFv7SSzoPLFZT4dC84SmGPWgf94NoQ=\n"
+			+ "000a=XFeC\n"
+			+ "0020-----END PGP SIGNATURE-----\n"
+			+ "0012push-cert-end\n";
+
+	// Same push certificate, with all trailing newlines stripped.
+	// (Note that the canonical signed payload is the same, so the same signature
+	// is still valid.)
+	private static final String INPUT_NO_NEWLINES = "001bcertificate version 0.1"
+			+ "0040pusher Dave Borowitz <dborowitz@google.com> 1433954361 -0700"
+			+ "0023pushee git://localhost/repo.git"
+			+ "0029nonce 1433954361-bde756572d665bba81d8"
+			+ "0004"
+			+ "00670000000000000000000000000000000000000000"
+			+ " 6c2b981a177396fb47345b7df3e4d3f854c6bea7"
+			+ " refs/heads/master"
+			+ "0021-----BEGIN PGP SIGNATURE-----"
+			+ "0015Version: GnuPG v1"
+			+ "0004"
+			+ "0044iQEcBAABAgAGBQJVeGg5AAoJEPfTicJkUdPkUggH/RKAeI9/i/LduuiqrL/SSdIa"
+			+ "00449tYaSqJKLbXz63M/AW4Sp+4u+dVCQvnAt/a35CVEnpZz6hN4Kn/tiswOWVJf4CO7"
+			+ "0044htNubGs5ZMwvD6sLYqKAnrM3WxV/2TbbjzjZW6Jkidz3jz/WRT4SmjGYiEO7aA+V"
+			+ "00444ZdIS9f7sW5VsHHYlNThCA7vH8Uu48bUovFXyQlPTX0pToSgrWV3JnTxDNxfn3iG"
+			+ "0044IL0zTY/qwVCdXgFownLcs6J050xrrBWIKqfcWr3u4D2aCLyR0v+S/KArr7ulZygY"
+			+ "0044+SOklImn8TAZiNxhWtA6ens66IiammUkZYFv7SSzoPLFZT4dC84SmGPWgf94NoQ="
+			+ "0009=XFeC"
+			+ "001f-----END PGP SIGNATURE-----"
+			+ "0011push-cert-end";
+
+	private Repository db;
+
+	@Before
+	public void setUp() {
+		db = new InMemoryRepository(new DfsRepositoryDescription("repo"));
+	}
+
+	private static SignedPushConfig newEnabledConfig() {
+		Config cfg = new Config();
+		cfg.setString("receive", null, "certnonceseed", "sekret");
+		return SignedPushConfig.KEY.parse(cfg);
+	}
+
+	private static SignedPushConfig newDisabledConfig() {
+		return SignedPushConfig.KEY.parse(new Config());
+	}
+
+	@Test
+	public void noCert() throws Exception {
+		PushCertificateParser parser =
+				new PushCertificateParser(db, newEnabledConfig());
+		assertTrue(parser.enabled());
+		assertNull(parser.build());
+
+		ObjectId oldId = ObjectId.zeroId();
+		ObjectId newId =
+				ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+		String line = oldId.name() + " " + newId.name() + " refs/heads/master";
+		ReceiveCommand cmd = BaseReceivePack.parseCommand(line);
+
+		parser.addCommand(cmd);
+		parser.addCommand(line);
+		assertNull(parser.build());
+	}
+
+	@Test
+	public void disabled() throws Exception {
+		PacketLineIn pckIn = newPacketLineIn(INPUT);
+		PushCertificateParser parser =
+				new PushCertificateParser(db, newDisabledConfig());
+		assertFalse(parser.enabled());
+		assertNull(parser.build());
+
+		parser.receiveHeader(pckIn, false);
+		parser.addCommand(pckIn.readString());
+		assertEquals(PushCertificateParser.BEGIN_SIGNATURE, pckIn.readString());
+		parser.receiveSignature(pckIn);
+		assertNull(parser.build());
+	}
+
+	@Test
+	public void disabledParserStillRequiresCorrectSyntax() throws Exception {
+		PacketLineIn pckIn = newPacketLineIn("001ccertificate version XYZ\n");
+		PushCertificateParser parser =
+				new PushCertificateParser(db, newDisabledConfig());
+		assertFalse(parser.enabled());
+		try {
+			parser.receiveHeader(pckIn, false);
+			fail("Expected PackProtocolException");
+		} catch (PackProtocolException e) {
+			assertEquals(
+					"Push certificate has missing or invalid value for certificate"
+						+ " version: XYZ",
+					e.getMessage());
+		}
+		assertNull(parser.build());
+	}
+
+	@Test
+	public void parseCertFromPktLine() throws Exception {
+		PacketLineIn pckIn = newPacketLineIn(INPUT);
+		PushCertificateParser parser =
+				new PushCertificateParser(db, newEnabledConfig());
+		parser.receiveHeader(pckIn, false);
+		parser.addCommand(pckIn.readString());
+		assertEquals(PushCertificateParser.BEGIN_SIGNATURE, pckIn.readString());
+		parser.receiveSignature(pckIn);
+
+		PushCertificate cert = parser.build();
+		assertEquals("0.1", cert.getVersion());
+		assertEquals("Dave Borowitz", cert.getPusherIdent().getName());
+		assertEquals("dborowitz@google.com",
+				cert.getPusherIdent().getEmailAddress());
+		assertEquals(1433954361000L, cert.getPusherIdent().getWhen().getTime());
+		assertEquals(-7 * 60, cert.getPusherIdent().getTimeZoneOffset());
+		assertEquals("git://localhost/repo.git", cert.getPushee());
+		assertEquals("1433954361-bde756572d665bba81d8", cert.getNonce());
+
+		assertNotEquals(cert.getNonce(), parser.getAdvertiseNonce());
+		assertEquals(PushCertificate.NonceStatus.BAD, cert.getNonceStatus());
+
+		assertEquals(1, cert.getCommands().size());
+		ReceiveCommand cmd = cert.getCommands().get(0);
+		assertEquals("refs/heads/master", cmd.getRefName());
+		assertEquals(ObjectId.zeroId(), cmd.getOldId());
+		assertEquals("6c2b981a177396fb47345b7df3e4d3f854c6bea7",
+				cmd.getNewId().name());
+
+		assertEquals(concatPacketLines(INPUT, 0, 6), cert.toText());
+		assertEquals(concatPacketLines(INPUT, 0, 17), cert.toTextWithSignature());
+
+		String signature = concatPacketLines(INPUT, 6, 17);
+		assertTrue(signature.startsWith(PushCertificateParser.BEGIN_SIGNATURE));
+		assertTrue(signature.endsWith(PushCertificateParser.END_SIGNATURE + "\n"));
+		assertEquals(signature, cert.getSignature());
+	}
+
+	@Test
+	public void parseCertFromPktLineNoNewlines() throws Exception {
+		PacketLineIn pckIn = newPacketLineIn(INPUT_NO_NEWLINES);
+		PushCertificateParser parser =
+				new PushCertificateParser(db, newEnabledConfig());
+		parser.receiveHeader(pckIn, false);
+		parser.addCommand(pckIn.readString());
+		assertEquals(PushCertificateParser.BEGIN_SIGNATURE, pckIn.readString());
+		parser.receiveSignature(pckIn);
+
+		PushCertificate cert = parser.build();
+		assertEquals("0.1", cert.getVersion());
+		assertEquals("Dave Borowitz", cert.getPusherIdent().getName());
+		assertEquals("dborowitz@google.com",
+				cert.getPusherIdent().getEmailAddress());
+		assertEquals(1433954361000L, cert.getPusherIdent().getWhen().getTime());
+		assertEquals(-7 * 60, cert.getPusherIdent().getTimeZoneOffset());
+		assertEquals("git://localhost/repo.git", cert.getPushee());
+		assertEquals("1433954361-bde756572d665bba81d8", cert.getNonce());
+
+		assertNotEquals(cert.getNonce(), parser.getAdvertiseNonce());
+		assertEquals(PushCertificate.NonceStatus.BAD, cert.getNonceStatus());
+
+		assertEquals(1, cert.getCommands().size());
+		ReceiveCommand cmd = cert.getCommands().get(0);
+		assertEquals("refs/heads/master", cmd.getRefName());
+		assertEquals(ObjectId.zeroId(), cmd.getOldId());
+		assertEquals("6c2b981a177396fb47345b7df3e4d3f854c6bea7",
+				cmd.getNewId().name());
+
+		// Canonical signed payload has reinserted newlines.
+		assertEquals(concatPacketLines(INPUT, 0, 6), cert.toText());
+
+		String signature = concatPacketLines(INPUT, 6, 17);
+		assertTrue(signature.startsWith(PushCertificateParser.BEGIN_SIGNATURE));
+		assertTrue(signature.endsWith(PushCertificateParser.END_SIGNATURE + "\n"));
+		assertEquals(signature, cert.getSignature());
+	}
+
+	@Test
+	public void testConcatPacketLines() throws Exception {
+		String input = "000bline 1\n000bline 2\n000bline 3\n";
+		assertEquals("line 1\n", concatPacketLines(input, 0, 1));
+		assertEquals("line 1\nline 2\n", concatPacketLines(input, 0, 2));
+		assertEquals("line 2\nline 3\n", concatPacketLines(input, 1, 3));
+		assertEquals("line 2\nline 3\n", concatPacketLines(input, 1, 4));
+	}
+
+	@Test
+	public void testConcatPacketLinesInsertsNewlines() throws Exception {
+		String input = "000bline 1\n000aline 2000bline 3\n";
+		assertEquals("line 1\n", concatPacketLines(input, 0, 1));
+		assertEquals("line 1\nline 2\n", concatPacketLines(input, 0, 2));
+		assertEquals("line 2\nline 3\n", concatPacketLines(input, 1, 3));
+		assertEquals("line 2\nline 3\n", concatPacketLines(input, 1, 4));
+	}
+
+	@Test
+	public void testParseReader() throws Exception {
+		Reader reader = new StringReader(concatPacketLines(INPUT, 0, 18));
+		PushCertificate streamCert = PushCertificateParser.fromReader(reader);
+
+		PacketLineIn pckIn = newPacketLineIn(INPUT);
+		PushCertificateParser pckParser =
+				new PushCertificateParser(db, newEnabledConfig());
+		pckParser.receiveHeader(pckIn, false);
+		pckParser.addCommand(pckIn.readString());
+		assertEquals(PushCertificateParser.BEGIN_SIGNATURE, pckIn.readString());
+		pckParser.receiveSignature(pckIn);
+		PushCertificate pckCert = pckParser.build();
+
+		// Nonce status is unsolicited since this was not parsed in the context of
+		// the wire protocol; as a result, certs are not actually equal.
+		assertEquals(NonceStatus.UNSOLICITED, streamCert.getNonceStatus());
+
+		assertEquals(pckCert.getVersion(), streamCert.getVersion());
+		assertEquals(pckCert.getPusherIdent().getName(),
+				streamCert.getPusherIdent().getName());
+		assertEquals(pckCert.getPusherIdent().getEmailAddress(),
+				streamCert.getPusherIdent().getEmailAddress());
+		assertEquals(pckCert.getPusherIdent().getWhen().getTime(),
+				streamCert.getPusherIdent().getWhen().getTime());
+		assertEquals(pckCert.getPusherIdent().getTimeZoneOffset(),
+				streamCert.getPusherIdent().getTimeZoneOffset());
+		assertEquals(pckCert.getPushee(), streamCert.getPushee());
+		assertEquals(pckCert.getNonce(), streamCert.getNonce());
+		assertEquals(pckCert.getSignature(), streamCert.getSignature());
+		assertEquals(pckCert.toText(), streamCert.toText());
+
+		assertEquals(pckCert.getCommands().size(), streamCert.getCommands().size());
+		ReceiveCommand pckCmd = pckCert.getCommands().get(0);
+		ReceiveCommand streamCmd = streamCert.getCommands().get(0);
+		assertEquals(pckCmd.getRefName(), streamCmd.getRefName());
+		assertEquals(pckCmd.getOldId(), streamCmd.getOldId());
+		assertEquals(pckCmd.getNewId().name(), streamCmd.getNewId().name());
+	}
+
+	@Test
+	public void testParseString() throws Exception {
+		String str = concatPacketLines(INPUT, 0, 18);
+		assertEquals(
+				PushCertificateParser.fromReader(new StringReader(str)),
+				PushCertificateParser.fromString(str));
+	}
+
+	@Test
+	public void testParseMultipleFromStream() throws Exception {
+		String input = concatPacketLines(INPUT, 0, 17);
+		assertFalse(input.contains(PushCertificateParser.END_CERT));
+		input += input;
+		Reader reader = new InputStreamReader(
+				new ByteArrayInputStream(Constants.encode(input)));
+
+		assertNotNull(PushCertificateParser.fromReader(reader));
+		assertNotNull(PushCertificateParser.fromReader(reader));
+		assertEquals(-1, reader.read());
+		assertNull(PushCertificateParser.fromReader(reader));
+	}
+
+	@Test
+	public void testMissingPusheeField() throws Exception {
+		// Omit pushee line from existing cert. (This means the signature would not
+		// match, but we're not verifying it here.)
+		String input = INPUT.replace("0024pushee git://localhost/repo.git\n", "");
+		assertFalse(input.contains(PushCertificateParser.PUSHEE));
+
+		PacketLineIn pckIn = newPacketLineIn(input);
+		PushCertificateParser parser =
+				new PushCertificateParser(db, newEnabledConfig());
+		parser.receiveHeader(pckIn, false);
+		parser.addCommand(pckIn.readString());
+		assertEquals(PushCertificateParser.BEGIN_SIGNATURE, pckIn.readString());
+		parser.receiveSignature(pckIn);
+
+		PushCertificate cert = parser.build();
+		assertEquals("0.1", cert.getVersion());
+		assertNull(cert.getPushee());
+		assertFalse(cert.toText().contains(PushCertificateParser.PUSHEE));
+	}
+
+	private static String concatPacketLines(String input, int begin, int end)
+			throws IOException {
+		StringBuilder result = new StringBuilder();
+		int i = 0;
+		PacketLineIn pckIn = newPacketLineIn(input);
+		while (i < end) {
+			String line;
+			try {
+				line = pckIn.readString();
+			} catch (EOFException e) {
+				break;
+			}
+			if (++i > begin) {
+				result.append(line).append('\n');
+			}
+		}
+		return result.toString();
+	}
+
+	private static PacketLineIn newPacketLineIn(String input) {
+		return new PacketLineIn(new ByteArrayInputStream(Constants.encode(input)));
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateStoreTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateStoreTest.java
new file mode 100644
index 0000000..68e0129
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/PushCertificateStoreTest.java
@@ -0,0 +1,380 @@
+/*
+ * Copyright (C) 2015, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.transport;
+
+import static org.eclipse.jgit.lib.ObjectId.zeroId;
+import static org.eclipse.jgit.lib.RefUpdate.Result.FAST_FORWARD;
+import static org.eclipse.jgit.lib.RefUpdate.Result.LOCK_FAILURE;
+import static org.eclipse.jgit.lib.RefUpdate.Result.NEW;
+import static org.eclipse.jgit.lib.RefUpdate.Result.NO_CHANGE;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+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.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.junit.Before;
+import org.junit.Test;
+
+public class PushCertificateStoreTest {
+	private static final ObjectId ID1 =
+		ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+
+	private static final ObjectId ID2 =
+		ObjectId.fromString("badc0ffebadc0ffebadc0ffebadc0ffebadc0ffe");
+
+	private static PushCertificate newCert(String... updateLines) {
+		StringBuilder cert = new StringBuilder(
+				"certificate version 0.1\n"
+				+ "pusher Dave Borowitz <dborowitz@google.com> 1433954361 -0700\n"
+				+ "pushee git://localhost/repo.git\n"
+				+ "nonce 1433954361-bde756572d665bba81d8\n"
+				+ "\n");
+		for (String updateLine : updateLines) {
+			cert.append(updateLine).append('\n');
+		}
+		cert.append(
+				"-----BEGIN PGP SIGNATURE-----\n"
+				+ "DUMMY/SIGNATURE\n"
+				+ "-----END PGP SIGNATURE-----\n");
+		try {
+			return PushCertificateParser.fromReader(new InputStreamReader(
+					new ByteArrayInputStream(Constants.encode(cert.toString()))));
+		} catch (IOException e) {
+			throw new IllegalArgumentException(e);
+		}
+	}
+
+	private static String command(ObjectId oldId, ObjectId newId, String ref) {
+		return oldId.name() + " " + newId.name() + " " + ref;
+	}
+
+	private AtomicInteger ts = new AtomicInteger(1433954361);
+	private InMemoryRepository repo;
+	private PushCertificateStore store;
+
+	@Before
+	public void setUp() throws Exception {
+		repo = new InMemoryRepository(new DfsRepositoryDescription("repo"));
+		store = newStore();
+	}
+
+	@Test
+	public void missingRef() throws Exception {
+		assertCerts("refs/heads/master");
+	}
+
+	@Test
+	public void saveNoChange() throws Exception {
+		assertEquals(NO_CHANGE, store.save());
+	}
+
+	@Test
+	public void saveOneCertOnOneRef() throws Exception {
+		PersonIdent ident = newIdent();
+		PushCertificate addMaster = newCert(
+				command(zeroId(), ID1, "refs/heads/master"));
+		store.put(addMaster, ident);
+		assertEquals(NEW, store.save());
+		assertCerts("refs/heads/master", addMaster);
+		assertCerts("refs/heads/branch");
+
+		try (RevWalk rw = new RevWalk(repo)) {
+			RevCommit c = rw.parseCommit(repo.resolve(PushCertificateStore.REF_NAME));
+			rw.parseBody(c);
+			assertEquals("Store push certificate for refs/heads/master\n",
+					c.getFullMessage());
+			assertEquals(ident, c.getAuthorIdent());
+			assertEquals(ident, c.getCommitterIdent());
+		}
+	}
+
+	@Test
+	public void saveTwoCertsOnSameRefInTwoUpdates() throws Exception {
+		PushCertificate addMaster = newCert(
+				command(zeroId(), ID1, "refs/heads/master"));
+		store.put(addMaster, newIdent());
+		assertEquals(NEW, store.save());
+		PushCertificate updateMaster = newCert(
+				command(ID1, ID2, "refs/heads/master"));
+		store.put(updateMaster, newIdent());
+		assertEquals(FAST_FORWARD, store.save());
+		assertCerts("refs/heads/master", updateMaster, addMaster);
+	}
+
+	@Test
+	public void saveTwoCertsOnSameRefInOneUpdate() throws Exception {
+		PersonIdent ident1 = newIdent();
+		PersonIdent ident2 = newIdent();
+		PushCertificate updateMaster = newCert(
+				command(ID1, ID2, "refs/heads/master"));
+		store.put(updateMaster, ident2);
+		PushCertificate addMaster = newCert(
+				command(zeroId(), ID1, "refs/heads/master"));
+		store.put(addMaster, ident1);
+		assertEquals(NEW, store.save());
+		assertCerts("refs/heads/master", updateMaster, addMaster);
+	}
+
+	@Test
+	public void saveTwoCertsOnDifferentRefsInOneUpdate() throws Exception {
+		PersonIdent ident1 = newIdent();
+		PersonIdent ident3 = newIdent();
+		PushCertificate addBranch = newCert(
+				command(zeroId(), ID1, "refs/heads/branch"));
+		store.put(addBranch, ident3);
+		PushCertificate addMaster = newCert(
+				command(zeroId(), ID1, "refs/heads/master"));
+		store.put(addMaster, ident1);
+		assertEquals(NEW, store.save());
+		assertCerts("refs/heads/master", addMaster);
+		assertCerts("refs/heads/branch", addBranch);
+	}
+
+	@Test
+	public void saveTwoCertsOnDifferentRefsInTwoUpdates() throws Exception {
+		PushCertificate addMaster = newCert(
+				command(zeroId(), ID1, "refs/heads/master"));
+		store.put(addMaster, newIdent());
+		assertEquals(NEW, store.save());
+		PushCertificate addBranch = newCert(
+				command(zeroId(), ID1, "refs/heads/branch"));
+		store.put(addBranch, newIdent());
+		assertEquals(FAST_FORWARD, store.save());
+		assertCerts("refs/heads/master", addMaster);
+		assertCerts("refs/heads/branch", addBranch);
+	}
+
+	@Test
+	public void saveOneCertOnMultipleRefs() throws Exception {
+		PersonIdent ident = newIdent();
+		PushCertificate addMasterAndBranch = newCert(
+				command(zeroId(), ID1, "refs/heads/branch"),
+				command(zeroId(), ID2, "refs/heads/master"));
+		store.put(addMasterAndBranch, ident);
+		assertEquals(NEW, store.save());
+		assertCerts("refs/heads/master", addMasterAndBranch);
+		assertCerts("refs/heads/branch", addMasterAndBranch);
+
+		try (RevWalk rw = new RevWalk(repo)) {
+			RevCommit c = rw.parseCommit(repo.resolve(PushCertificateStore.REF_NAME));
+			rw.parseBody(c);
+			assertEquals("Store push certificate for 2 refs\n", c.getFullMessage());
+			assertEquals(ident, c.getAuthorIdent());
+			assertEquals(ident, c.getCommitterIdent());
+		}
+	}
+
+	@Test
+	public void changeRefFileToDirectory() throws Exception {
+		PushCertificate deleteRefsHeads = newCert(
+				command(ID1, zeroId(), "refs/heads"));
+		store.put(deleteRefsHeads, newIdent());
+		PushCertificate addMaster = newCert(
+				command(zeroId(), ID1, "refs/heads/master"));
+		store.put(addMaster, newIdent());
+		assertEquals(NEW, store.save());
+		assertCerts("refs/heads", deleteRefsHeads);
+		assertCerts("refs/heads/master", addMaster);
+	}
+
+	@Test
+	public void getBeforeSaveDoesNotIncludePending() throws Exception {
+		PushCertificate addMaster = newCert(
+				command(zeroId(), ID1, "refs/heads/master"));
+		store.put(addMaster, newIdent());
+		assertEquals(NEW, store.save());
+
+		PushCertificate updateMaster = newCert(
+				command(ID1, ID2, "refs/heads/master"));
+		store.put(updateMaster, newIdent());
+
+		assertCerts("refs/heads/master", addMaster);
+		assertEquals(FAST_FORWARD, store.save());
+		assertCerts("refs/heads/master", updateMaster, addMaster);
+	}
+
+	@Test
+	public void lockFailure() throws Exception {
+		PushCertificateStore store1 = store;
+		PushCertificateStore store2 = newStore();
+		store2.get("refs/heads/master");
+
+		PushCertificate addMaster = newCert(
+				command(zeroId(), ID1, "refs/heads/master"));
+		store1.put(addMaster, newIdent());
+		assertEquals(NEW, store1.save());
+
+		PushCertificate addBranch = newCert(
+				command(zeroId(), ID2, "refs/heads/branch"));
+		store2.put(addBranch, newIdent());
+
+		assertEquals(LOCK_FAILURE, store2.save());
+		// Reread ref after lock failure.
+		assertCerts(store2, "refs/heads/master", addMaster);
+		assertCerts(store2, "refs/heads/branch");
+
+		assertEquals(FAST_FORWARD, store2.save());
+		assertCerts(store2, "refs/heads/master", addMaster);
+		assertCerts(store2, "refs/heads/branch", addBranch);
+	}
+
+	@Test
+	public void saveInBatch() throws Exception {
+		BatchRefUpdate batch = repo.getRefDatabase().newBatchUpdate();
+		assertFalse(store.save(batch));
+		assertEquals(0, batch.getCommands().size());
+		PushCertificate addMaster = newCert(
+				command(zeroId(), ID1, "refs/heads/master"));
+		store.put(addMaster, newIdent());
+		assertTrue(store.save(batch));
+
+		List<ReceiveCommand> commands = batch.getCommands();
+		assertEquals(1, commands.size());
+		ReceiveCommand cmd = commands.get(0);
+		assertEquals("refs/meta/push-certs", cmd.getRefName());
+		assertEquals(ReceiveCommand.Result.NOT_ATTEMPTED, cmd.getResult());
+
+		try (RevWalk rw = new RevWalk(repo)) {
+			batch.execute(rw, NullProgressMonitor.INSTANCE);
+			assertEquals(ReceiveCommand.Result.OK, cmd.getResult());
+		}
+	}
+
+	@Test
+	public void putMatchingWithNoMatchingRefs() throws Exception {
+		PushCertificate addMaster = newCert(
+				command(zeroId(), ID1, "refs/heads/master"),
+				command(zeroId(), ID2, "refs/heads/branch"));
+		store.put(addMaster, newIdent(), Collections.<ReceiveCommand> emptyList());
+		assertEquals(NO_CHANGE, store.save());
+	}
+
+	@Test
+	public void putMatchingWithNoMatchingRefsInBatchOnEmptyRef()
+			throws Exception {
+		PushCertificate addMaster = newCert(
+				command(zeroId(), ID1, "refs/heads/master"),
+				command(zeroId(), ID2, "refs/heads/branch"));
+		store.put(addMaster, newIdent(), Collections.<ReceiveCommand> emptyList());
+		BatchRefUpdate batch = repo.getRefDatabase().newBatchUpdate();
+		assertFalse(store.save(batch));
+		assertEquals(0, batch.getCommands().size());
+	}
+
+	@Test
+	public void putMatchingWithNoMatchingRefsInBatchOnNonEmptyRef()
+			throws Exception {
+		PushCertificate addMaster = newCert(
+				command(zeroId(), ID1, "refs/heads/master"));
+		store.put(addMaster, newIdent());
+		assertEquals(NEW, store.save());
+
+		PushCertificate addBranch = newCert(
+				command(zeroId(), ID2, "refs/heads/branch"));
+		store.put(addBranch, newIdent(), Collections.<ReceiveCommand> emptyList());
+		BatchRefUpdate batch = repo.getRefDatabase().newBatchUpdate();
+		assertFalse(store.save(batch));
+		assertEquals(0, batch.getCommands().size());
+	}
+
+	@Test
+	public void putMatchingWithSomeMatchingRefs() throws Exception {
+		PushCertificate addMasterAndBranch = newCert(
+				command(zeroId(), ID1, "refs/heads/master"),
+				command(zeroId(), ID2, "refs/heads/branch"));
+		store.put(addMasterAndBranch, newIdent(),
+				Collections.singleton(addMasterAndBranch.getCommands().get(0)));
+		assertEquals(NEW, store.save());
+		assertCerts("refs/heads/master", addMasterAndBranch);
+		assertCerts("refs/heads/branch");
+	}
+
+	private PersonIdent newIdent() {
+		return new PersonIdent(
+				"A U. Thor", "author@example.com", ts.getAndIncrement(), 0);
+	}
+
+	private PushCertificateStore newStore() {
+		return new PushCertificateStore(repo);
+	}
+
+	private void assertCerts(String refName, PushCertificate... expected)
+			throws Exception {
+		assertCerts(store, refName, expected);
+		assertCerts(newStore(), refName, expected);
+	}
+
+	private static void assertCerts(PushCertificateStore store, String refName,
+			PushCertificate... expected) throws Exception {
+		List<PushCertificate> ex = Arrays.asList(expected);
+		PushCertificate first = !ex.isEmpty() ? ex.get(0) : null;
+		assertEquals(first, store.get(refName));
+		assertEquals(ex, toList(store.getAll(refName)));
+	}
+
+	private static <T> List<T> toList(Iterable<T> it) {
+		List<T> list = new ArrayList<>();
+		for (T t : it) {
+			list.add(t);
+		}
+		return list;
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/URIishTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/URIishTest.java
index 8c7c992..745c322 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/URIishTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/URIishTest.java
@@ -3,6 +3,7 @@
  * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
  * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
  * Copyright (C) 2013, Robin Stocker <robin@nibor.org>
+ * Copyright (C) 2015, Patrick Steinhardt <ps@pks.im>
  * and other copyright owners as documented in the project's IP log.
  *
  * This program and the accompanying materials are made available
@@ -379,6 +380,56 @@
 	}
 
 	@Test
+	public void testSshProtoHostOnly() throws Exception {
+		final String str = "ssh://example.com/";
+		URIish u = new URIish(str);
+		assertEquals("ssh", u.getScheme());
+		assertTrue(u.isRemote());
+		assertEquals("/", u.getRawPath());
+		assertEquals("/", u.getPath());
+		assertEquals("example.com", u.getHost());
+		assertEquals(-1, u.getPort());
+		assertEquals("ssh://example.com/", u.toString());
+		assertEquals("ssh://example.com/", u.toASCIIString());
+		assertEquals("example.com", u.getHumanishName());
+		assertEquals(u, new URIish(str));
+	}
+
+	@Test
+	public void testSshProtoHostWithAuthentication() throws Exception {
+		final String str = "ssh://user:secret@pass@example.com/";
+		URIish u = new URIish(str);
+		assertEquals("ssh", u.getScheme());
+		assertTrue(u.isRemote());
+		assertEquals("/", u.getRawPath());
+		assertEquals("/", u.getPath());
+		assertEquals("example.com", u.getHost());
+		assertEquals(-1, u.getPort());
+		assertEquals("ssh://user@example.com/", u.toString());
+		assertEquals("ssh://user@example.com/", u.toASCIIString());
+		assertEquals("example.com", u.getHumanishName());
+		assertEquals("user", u.getUser());
+		assertEquals("secret@pass", u.getPass());
+		assertEquals(u, new URIish(str));
+	}
+
+	@Test
+	public void testSshProtoHostWithPort() throws Exception {
+		final String str = "ssh://example.com:2222/";
+		URIish u = new URIish(str);
+		assertEquals("ssh", u.getScheme());
+		assertTrue(u.isRemote());
+		assertEquals("/", u.getRawPath());
+		assertEquals("/", u.getPath());
+		assertEquals("example.com", u.getHost());
+		assertEquals(2222, u.getPort());
+		assertEquals("ssh://example.com:2222/", u.toString());
+		assertEquals("ssh://example.com:2222/", u.toASCIIString());
+		assertEquals("example.com", u.getHumanishName());
+		assertEquals(u, new URIish(str));
+	}
+
+	@Test
 	public void testSshProtoWithUserAndPort() throws Exception {
 		final String str = "ssh://user@example.com:33/some/p ath";
 		URIish u = new URIish(str);
@@ -623,6 +674,13 @@
 	}
 
 	@Test
+	public void testGetEmptyHumanishNameWithAuthorityOnly() throws IllegalArgumentException,
+			URISyntaxException {
+		String humanishName = new URIish(GIT_SCHEME + "abc").getHumanishName();
+		assertEquals("abc", humanishName);
+	}
+
+	@Test
 	public void testGetValidSlashHumanishName()
 			throws IllegalArgumentException, URISyntaxException {
 		String humanishName = new URIish(GIT_SCHEME + "host/abc/")
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FileUtils7Test.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FileUtils7Test.java
index 9dc5fac..4625f30 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FileUtils7Test.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FileUtils7Test.java
@@ -48,6 +48,8 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
 
 import org.junit.After;
 import org.junit.Before;
@@ -83,4 +85,14 @@
 		assertTrue(dir.exists());
 		assertTrue(file.exists());
 	}
+
+	@Test
+	public void testAtomicMove() throws IOException {
+		File src = new File(trash, "src");
+		Files.createFile(src.toPath());
+		File dst = new File(trash, "dst");
+		FileUtils.rename(src, dst, StandardCopyOption.ATOMIC_MOVE);
+		assertFalse(Files.exists(src.toPath()));
+		assertTrue(Files.exists(dst.toPath()));
+	}
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/IOReadLineTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/IOReadLineTest.java
new file mode 100644
index 0000000..928fb2e
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/IOReadLineTest.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2015, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.util;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.Collection;
+
+import org.eclipse.jgit.lib.Constants;
+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;
+
+@RunWith(Parameterized.class)
+public class IOReadLineTest {
+	@Parameter(0)
+	public boolean buffered;
+
+	@Parameter(1)
+	public int sizeHint;
+
+	@SuppressWarnings("boxing")
+	@Parameters(name="buffered={0}, sizeHint={1}")
+	public static Collection<Object[]> getParameters() {
+		Boolean[] bv = {false, true};
+		Integer[] sv = {-1, 0, 1, 2, 3, 4, 64};
+		Collection<Object[]> params = new ArrayList<>(bv.length * sv.length);
+		for (boolean b : bv) {
+			for (Integer s : sv) {
+				params.add(new Object[]{b, s});
+			}
+		}
+		return params;
+	}
+
+	@Test
+	public void testReadLine() throws Exception {
+		Reader r = newReader("foo\nbar\nbaz\n");
+		assertEquals("foo\n", readLine(r));
+		assertEquals("bar\n", readLine(r));
+		assertEquals("baz\n", readLine(r));
+		assertEquals("", readLine(r));
+	}
+
+	@Test
+	public void testReadLineNoTrailingNewline() throws Exception {
+		Reader r = newReader("foo\nbar\nbaz");
+		assertEquals("foo\n", readLine(r));
+		assertEquals("bar\n", readLine(r));
+		assertEquals("baz", readLine(r));
+		assertEquals("", readLine(r));
+	}
+
+	private String readLine(Reader r) throws Exception {
+		return IO.readLine(r, sizeHint);
+	}
+
+	private Reader newReader(String in) {
+		Reader r = new InputStreamReader(
+				new ByteArrayInputStream(Constants.encode(in)));
+		if (buffered) {
+			r = new BufferedReader(r);
+		}
+		assertEquals(Boolean.valueOf(buffered),
+				Boolean.valueOf(r.markSupported()));
+		return r;
+	}
+}
diff --git a/org.eclipse.jgit.ui/META-INF/MANIFEST.MF b/org.eclipse.jgit.ui/META-INF/MANIFEST.MF
index 2cd9dc6..3586cbc 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.0.3.201509231615-r
+Bundle-Version: 4.1.2.201602141800-r
 Bundle-Vendor: %provider_name
 Bundle-RequiredExecutionEnvironment: JavaSE-1.7
-Export-Package: org.eclipse.jgit.awtui;version="4.0.3"
-Import-Package: org.eclipse.jgit.errors;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.lib;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.nls;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.revplot;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.revwalk;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.transport;version="[4.0.3,4.1.0)",
- org.eclipse.jgit.util;version="[4.0.3,4.1.0)"
+Export-Package: org.eclipse.jgit.awtui;version="4.1.2"
+Import-Package: org.eclipse.jgit.errors;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.lib;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.nls;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.revplot;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.revwalk;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.transport;version="[4.1.2,4.2.0)",
+ org.eclipse.jgit.util;version="[4.1.2,4.2.0)"
diff --git a/org.eclipse.jgit.ui/pom.xml b/org.eclipse.jgit.ui/pom.xml
index 3ffda41..776cb8b 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.0.3.201509231615-r</version>
+    <version>4.1.2.201602141800-r</version>
   </parent>
 
   <artifactId>org.eclipse.jgit.ui</artifactId>
diff --git a/org.eclipse.jgit.ui/src/org/eclipse/jgit/awtui/SwingCommitList.java b/org.eclipse.jgit.ui/src/org/eclipse/jgit/awtui/SwingCommitList.java
index 4a11964..7359093 100644
--- a/org.eclipse.jgit.ui/src/org/eclipse/jgit/awtui/SwingCommitList.java
+++ b/org.eclipse.jgit.ui/src/org/eclipse/jgit/awtui/SwingCommitList.java
@@ -82,6 +82,7 @@
 	}
 
 	static class SwingLane extends PlotLane {
+		private static final long serialVersionUID = 1L;
 		Color color;
 	}
 }
diff --git a/org.eclipse.jgit/.settings/.api_filters b/org.eclipse.jgit/.settings/.api_filters
index 124435a..a1e79e2 100644
--- a/org.eclipse.jgit/.settings/.api_filters
+++ b/org.eclipse.jgit/.settings/.api_filters
@@ -1,9 +1,23 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <component id="org.eclipse.jgit" version="2">
-    <resource path="src/org/eclipse/jgit/util/FileUtil.java" type="org.eclipse.jgit.util.FileUtil">
-        <filter comment="moved into another bundle keeping original package" id="1110441988">
+    <resource path="src/org/eclipse/jgit/transport/PushCertificate.java" type="org.eclipse.jgit.transport.PushCertificate">
+        <filter comment="PushCertificate wasn't really usable in 4.0" id="338722907">
             <message_arguments>
-                <message_argument value="org.eclipse.jgit.util.FileUtil"/>
+                <message_argument value="org.eclipse.jgit.transport.PushCertificate"/>
+                <message_argument value="PushCertificate()"/>
+            </message_arguments>
+        </filter>
+        <filter comment="PushCertificate wasn't really usable in 4.0" id="338792546">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.transport.PushCertificate"/>
+                <message_argument value="getCommandList()"/>
+            </message_arguments>
+        </filter>
+    </resource>
+    <resource path="src/org/eclipse/jgit/transport/PushCertificateParser.java" type="org.eclipse.jgit.transport.PushCertificateParser">
+        <filter comment="PushCertificates haven't been really usable in 4.0" id="338849923">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.transport.PushCertificateParser"/>
             </message_arguments>
         </filter>
     </resource>
diff --git a/org.eclipse.jgit/.settings/org.eclipse.jdt.core.prefs b/org.eclipse.jgit/.settings/org.eclipse.jdt.core.prefs
index ff39d16..4e28e0b 100644
--- a/org.eclipse.jgit/.settings/org.eclipse.jdt.core.prefs
+++ b/org.eclipse.jgit/.settings/org.eclipse.jdt.core.prefs
@@ -1,10 +1,10 @@
 eclipse.preferences.version=1
-org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled
+org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=enabled
 org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore
 org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull
 org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault
 org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable
-org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled
+org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled
 org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
 org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate
 org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
@@ -67,11 +67,11 @@
 org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error
 org.eclipse.jdt.core.compiler.problem.nullReference=error
 org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error
-org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning
+org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore
 org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning
 org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore
 org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=error
-org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning
+org.eclipse.jdt.core.compiler.problem.potentialNullReference=error
 org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore
 org.eclipse.jdt.core.compiler.problem.rawTypeReference=ignore
 org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning
diff --git a/org.eclipse.jgit/META-INF/MANIFEST.MF b/org.eclipse.jgit/META-INF/MANIFEST.MF
index edf38e4..a51bee8 100644
--- a/org.eclipse.jgit/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit/META-INF/MANIFEST.MF
@@ -2,10 +2,11 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %plugin_name
 Bundle-SymbolicName: org.eclipse.jgit
-Bundle-Version: 4.0.3.201509231615-r
+Bundle-Version: 4.1.2.201602141800-r
 Bundle-Localization: plugin
 Bundle-Vendor: %provider_name
-Export-Package: org.eclipse.jgit.api;version="4.0.3";
+Bundle-ActivationPolicy: lazy
+Export-Package: org.eclipse.jgit.api;version="4.1.2";
   uses:="org.eclipse.jgit.revwalk,
    org.eclipse.jgit.treewalk.filter,
    org.eclipse.jgit.diff,
@@ -19,56 +20,60 @@
    org.eclipse.jgit.submodule,
    org.eclipse.jgit.transport,
    org.eclipse.jgit.merge",
- org.eclipse.jgit.api.errors;version="4.0.3";
-  uses:="org.eclipse.jgit.lib,org.eclipse.jgit.errors",
- org.eclipse.jgit.attributes;version="4.0.3",
- org.eclipse.jgit.blame;version="4.0.3";
+ org.eclipse.jgit.api.errors;version="4.1.2";
+  uses:="org.eclipse.jgit.lib,
+   org.eclipse.jgit.errors",
+ org.eclipse.jgit.attributes;version="4.1.2",
+ org.eclipse.jgit.blame;version="4.1.2";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.revwalk,
    org.eclipse.jgit.treewalk.filter,
    org.eclipse.jgit.diff",
- org.eclipse.jgit.diff;version="4.0.3";
+ org.eclipse.jgit.diff;version="4.1.2";
   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.0.3";
+ org.eclipse.jgit.dircache;version="4.1.2";
   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.0.3";
+ org.eclipse.jgit.errors;version="4.1.2";
   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.0.3";
+ org.eclipse.jgit.events;version="4.1.2";
   uses:="org.eclipse.jgit.lib",
- org.eclipse.jgit.fnmatch;version="4.0.3",
- org.eclipse.jgit.gitrepo;version="4.0.3";
+ org.eclipse.jgit.fnmatch;version="4.1.2",
+ org.eclipse.jgit.gitrepo;version="4.1.2";
   uses:="org.eclipse.jgit.api,
    org.eclipse.jgit.lib,
-   org.eclipse.jgit.revwalk",
- org.eclipse.jgit.gitrepo.internal;version="4.0.3";x-internal:=true,
- org.eclipse.jgit.hooks;version="4.0.3",
- org.eclipse.jgit.ignore;version="4.0.3",
- org.eclipse.jgit.ignore.internal;version="4.0.3";x-friends:="org.eclipse.jgit.test",
- org.eclipse.jgit.internal;version="4.0.3";x-friends:="org.eclipse.jgit.test,org.eclipse.jgit.http.test",
- org.eclipse.jgit.internal.storage.dfs;version="4.0.3";
+   org.eclipse.jgit.revwalk,
+   org.xml.sax.helpers,
+   org.xml.sax",
+ org.eclipse.jgit.gitrepo.internal;version="4.1.2";x-internal:=true,
+ org.eclipse.jgit.hooks;version="4.1.2";
+  uses:="org.eclipse.jgit.lib",
+ org.eclipse.jgit.ignore;version="4.1.2",
+ org.eclipse.jgit.ignore.internal;version="4.1.2";x-friends:="org.eclipse.jgit.test",
+ org.eclipse.jgit.internal;version="4.1.2";x-friends:="org.eclipse.jgit.test,org.eclipse.jgit.http.test",
+ org.eclipse.jgit.internal.storage.dfs;version="4.1.2";
   x-friends:="org.eclipse.jgit.test,
    org.eclipse.jgit.http.server",
- org.eclipse.jgit.internal.storage.file;version="4.0.3";
+ org.eclipse.jgit.internal.storage.file;version="4.1.2";
   x-friends:="org.eclipse.jgit.test,
    org.eclipse.jgit.junit,
    org.eclipse.jgit.junit.http,
    org.eclipse.jgit.http.server,
    org.eclipse.jgit.java7.test,
    org.eclipse.jgit.pgm",
- org.eclipse.jgit.internal.storage.pack;version="4.0.3";x-friends:="org.eclipse.jgit.junit,org.eclipse.jgit.test,org.eclipse.jgit.pgm",
- org.eclipse.jgit.lib;version="4.0.3";
+ org.eclipse.jgit.internal.storage.pack;version="4.1.2";x-friends:="org.eclipse.jgit.junit,org.eclipse.jgit.test,org.eclipse.jgit.pgm",
+ org.eclipse.jgit.lib;version="4.1.2";
   uses:="org.eclipse.jgit.revwalk,
    org.eclipse.jgit.treewalk.filter,
    org.eclipse.jgit.util,
@@ -78,41 +83,45 @@
    org.eclipse.jgit.treewalk,
    org.eclipse.jgit.transport,
    org.eclipse.jgit.submodule",
- org.eclipse.jgit.merge;version="4.0.3";
+ org.eclipse.jgit.merge;version="4.1.2";
   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.0.3",
- org.eclipse.jgit.notes;version="4.0.3";
+ org.eclipse.jgit.nls;version="4.1.2",
+ org.eclipse.jgit.notes;version="4.1.2";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.treewalk,
    org.eclipse.jgit.revwalk,
    org.eclipse.jgit.merge",
- org.eclipse.jgit.patch;version="4.0.3";
-  uses:="org.eclipse.jgit.lib,org.eclipse.jgit.diff",
- org.eclipse.jgit.revplot;version="4.0.3";
+ org.eclipse.jgit.patch;version="4.1.2";
+  uses:="org.eclipse.jgit.lib,
+   org.eclipse.jgit.diff",
+ org.eclipse.jgit.revplot;version="4.1.2";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.revwalk",
- org.eclipse.jgit.revwalk;version="4.0.3";
+ org.eclipse.jgit.revwalk;version="4.1.2";
   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.0.3";
-  uses:="org.eclipse.jgit.revwalk,org.eclipse.jgit.util",
- org.eclipse.jgit.storage.file;version="4.0.3";
-  uses:="org.eclipse.jgit.lib,org.eclipse.jgit.util",
- org.eclipse.jgit.storage.pack;version="4.0.3";
+ org.eclipse.jgit.revwalk.filter;version="4.1.2";
+  uses:="org.eclipse.jgit.revwalk,
+   org.eclipse.jgit.lib,
+   org.eclipse.jgit.util",
+ org.eclipse.jgit.storage.file;version="4.1.2";
+  uses:="org.eclipse.jgit.lib,
+   org.eclipse.jgit.util",
+ org.eclipse.jgit.storage.pack;version="4.1.2";
   uses:="org.eclipse.jgit.lib",
- org.eclipse.jgit.submodule;version="4.0.3";
+ org.eclipse.jgit.submodule;version="4.1.2";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.treewalk.filter,
    org.eclipse.jgit.treewalk",
- org.eclipse.jgit.transport;version="4.0.3";
+ org.eclipse.jgit.transport;version="4.1.2";
   uses:="org.eclipse.jgit.transport.resolver,
    org.eclipse.jgit.revwalk,
    org.eclipse.jgit.internal.storage.pack,
@@ -124,29 +133,29 @@
    org.eclipse.jgit.transport.http,
    org.eclipse.jgit.errors,
    org.eclipse.jgit.storage.pack",
- org.eclipse.jgit.transport.http;version="4.0.3";
+ org.eclipse.jgit.transport.http;version="4.1.2";
   uses:="javax.net.ssl",
- org.eclipse.jgit.transport.resolver;version="4.0.3";
+ org.eclipse.jgit.transport.resolver;version="4.1.2";
   uses:="org.eclipse.jgit.lib,
    org.eclipse.jgit.transport",
- org.eclipse.jgit.treewalk;version="4.0.3";
+ org.eclipse.jgit.treewalk;version="4.1.2";
   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.0.3";
+ org.eclipse.jgit.treewalk.filter;version="4.1.2";
   uses:="org.eclipse.jgit.treewalk",
- org.eclipse.jgit.util;version="4.0.3";
+ org.eclipse.jgit.util;version="4.1.2";
   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.0.3"
-Bundle-ActivationPolicy: lazy
+ org.eclipse.jgit.util.io;version="4.1.2"
 Bundle-RequiredExecutionEnvironment: JavaSE-1.7
-Require-Bundle: com.jcraft.jsch;bundle-version="[0.1.37,0.2.0)"
+Require-Bundle: com.jcraft.jsch;bundle-version="[0.1.37,0.2.0)",
+ org.eclipse.jdt.annotation;bundle-version="[1.1.0,2.0.0)";resolution:=optional
 Import-Package: com.googlecode.javaewah;version="[0.7.9,0.8.0)",
  javax.crypto,
  javax.net.ssl,
diff --git a/org.eclipse.jgit/META-INF/SOURCE-MANIFEST.MF b/org.eclipse.jgit/META-INF/SOURCE-MANIFEST.MF
index b7c2a35..c7d9753 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.0.3.201509231615-r
-Eclipse-SourceBundle: org.eclipse.jgit;version="4.0.3.201509231615-r";roots="."
+Bundle-Version: 4.1.2.201602141800-r
+Eclipse-SourceBundle: org.eclipse.jgit;version="4.1.2.201602141800-r";roots="."
diff --git a/org.eclipse.jgit/pom.xml b/org.eclipse.jgit/pom.xml
index 3dc7132..945b135 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.0.3.201509231615-r</version>
+    <version>4.1.2.201602141800-r</version>
   </parent>
 
   <artifactId>org.eclipse.jgit</artifactId>
@@ -88,6 +88,12 @@
       <groupId>org.slf4j</groupId>
       <artifactId>slf4j-api</artifactId>
     </dependency>
+
+	<dependency>
+	    <groupId>org.eclipse.jdt</groupId>
+	    <artifactId>org.eclipse.jdt.annotation</artifactId>
+	    <version>1.1.0</version>
+	</dependency>
   </dependencies>
 
   <build>
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 db9a684..34bbb41 100644
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
@@ -59,23 +59,23 @@
 cannotCreateTempDir=Cannot create a temp dir
 cannotDeleteCheckedOutBranch=Branch {0} is checked out and can not be deleted
 cannotDeleteFile=Cannot delete file: {0}
-cannotDeleteObjectsPath="Can't delete {0}/{1}: {2}
+cannotDeleteObjectsPath=Cannot delete {0}/{1}: {2}
 cannotDeleteStaleTrackingRef=Cannot delete stale tracking ref {0}
 cannotDeleteStaleTrackingRef2=Cannot delete stale tracking ref {0}: {1}
 cannotDetermineProxyFor=Cannot determine proxy for {0}
 cannotDownload=Cannot download {0}
-cannotEnterObjectsPath=Can't enter {0}/objects: {1}
-cannotEnterPathFromParent=Can't enter {0} from {1}: {2}
+cannotEnterObjectsPath=Cannot enter {0}/objects: {1}
+cannotEnterPathFromParent=Cannot enter {0} from {1}: {2}
 cannotExecute=cannot execute: {0}
 cannotGet=Cannot get {0}
-cannotGetObjectsPath=Can't get {0}/{1}: {2}
-cannotListObjectsPath=Can't ls {0}/{1}: {2}
-cannotListPackPath=Can't ls {0}/pack: {1}
+cannotGetObjectsPath=Cannot get {0}/{1}: {2}
+cannotListObjectsPath=Cannot ls {0}/{1}: {2}
+cannotListPackPath=Cannot ls {0}/pack: {1}
 cannotListRefs=cannot list refs
 cannotLock=Cannot lock {0}
 cannotLockPackIn=Cannot lock pack in {0}
 cannotMatchOnEmptyString=Cannot match on empty string.
-cannotMkdirObjectPath=Can't mkdir {0}/{1}: {2}
+cannotMkdirObjectPath=Cannot mkdir {0}/{1}: {2}
 cannotMoveIndexTo=Cannot move index to {0}
 cannotMovePackTo=Cannot move pack to {0}
 cannotOpenService=cannot open {0}
@@ -97,7 +97,7 @@
 cannotResolveUniquelyAbbrevObjectId=Could not resolve uniquely the abbreviated object ID
 cannotUnloadAModifiedTree=Cannot unload a modified tree.
 cannotWorkWithOtherStagesThanZeroRightNow=Cannot work with other stages than zero right now. Won't write corrupt index.
-cannotWriteObjectsPath="Can't write {0}/{1}: {2}
+cannotWriteObjectsPath=Cannot write {0}/{1}: {2}
 canOnlyCherryPickCommitsWithOneParent=Cannot cherry-pick commit ''{0}'' because it has {1} parents, only commits with exactly one parent are supported.
 canOnlyRevertCommitsWithOneParent=Cannot revert commit ''{0}'' because it has {1} parents, only commits with exactly one parent are supported
 commitDoesNotHaveGivenParent=The commit ''{0}'' does not have a parent number {1}.
@@ -237,12 +237,10 @@
 errorEncodingFromFile=Error encoding from file {0}
 errorInBase64CodeReadingStream=Error in Base64 code reading stream.
 errorInPackedRefs=error in packed-refs
-errorInvalidPushCert=error: invalid protocol: {0}
 errorInvalidProtocolWantedOldNewRef=error: invalid protocol: wanted 'old new ref'
 errorListing=Error listing {0}
 errorOccurredDuringUnpackingOnTheRemoteEnd=error occurred during unpacking on the remote end: {0}
 errorReadingInfoRefs=error reading info/refs
-errorSymlinksNotSupported=Symlinks are not supported with this OS/JRE
 exceptionCaughtDuringExecutionOfHook=Exception caught during execution of "{0}" hook.
 exceptionCaughtDuringExecutionOfAddCommand=Exception caught during execution of add command
 exceptionCaughtDuringExecutionOfArchiveCommand=Exception caught during execution of archive command
@@ -288,7 +286,6 @@
 gcFailed=Garbage collection failed.
 gitmodulesNotFound=.gitmodules not found in tree.
 headRequiredToStash=HEAD required to stash local changes
-hiddenFilesStartWithDot=Hiding only allowed for names that start with a period
 hoursAgo={0} hours ago
 hugeIndexesAreNotSupportedByJgitYet=Huge indexes are not supported by jgit, yet
 hunkBelongsToAnotherFile=Hunk belongs to another file
@@ -326,8 +323,10 @@
 invalidGitdirRef = Invalid .git reference in file ''{0}''
 invalidGitType=invalid git type: {0}
 invalidId=Invalid id: {0}
+invalidId0=Invalid id
 invalidIdLength=Invalid id length {0}; should be {1}
 invalidIgnoreParamSubmodule=Found invalid ignore param for submodule {0}.
+invalidIgnoreRule=Exception caught while parsing ignore rule ''{0}''.
 invalidIntegerValue=Invalid integer value: {0}.{1}={2}
 invalidKey=Invalid key: {0}
 invalidLineInConfigFile=Invalid line in config file
@@ -344,6 +343,7 @@
 invalidReflogRevision=Invalid reflog revision: {0}
 invalidRefName=Invalid ref name: {0}
 invalidRemote=Invalid remote: {0}
+invalidShallowObject=invalid shallow object {0}, expected commit
 invalidStageForPath=Invalid stage {0} for path {1}
 invalidTagOption=Invalid tag option: {0}
 invalidTimeout=Invalid timeout: {0}
@@ -440,6 +440,7 @@
 packChecksumMismatch=Pack checksum mismatch detected for pack file {0}
 packCorruptedWhileWritingToFilesystem=Pack corrupted while writing to filesystem
 packDoesNotMatchIndex=Pack {0} does not match index
+packedRefsHandleIsStale=packed-refs handle is stale, {0}. retry
 packetSizeMustBeAtLeast=packet size {0} must be >= {1}
 packetSizeMustBeAtMost=packet size {0} must be <= {1}
 packfileCorruptionDetected=Packfile corruption detected: {0}
@@ -464,7 +465,7 @@
 peerDidNotSupplyACompleteObjectGraph=peer did not supply a complete object graph
 personIdentEmailNonNull=E-mail address of PersonIdent must not be null.
 personIdentNameNonNull=Name of PersonIdent must not be null.
-prefixRemote=remote: 
+prefixRemote=remote:
 problemWithResolvingPushRefSpecsLocally=Problem with resolving push ref specs locally: {0}
 progressMonUploading=Uploading {0}
 propertyIsAlreadyNonNull=Property is already non null
@@ -473,6 +474,10 @@
 pullOnRepoWithoutHEADCurrentlyNotSupported=Pull on repository without HEAD currently not supported
 pullTaskName=Pull
 pushCancelled=push cancelled
+pushCertificateInvalidField=Push certificate has missing or invalid value for {0}
+pushCertificateInvalidFieldValue=Push certificate has missing or invalid value for {0}: {1}
+pushCertificateInvalidHeader=Push certificate has invalid header format
+pushCertificateInvalidSignature=Push certificate has invalid signature format
 pushIsNotSupportedForBundleTransport=Push is not supported for bundle transport
 pushNotPermitted=push not permitted
 rawLogMessageDoesNotParseAsLogEntry=Raw log message does not parse as log entry
@@ -531,7 +536,6 @@
 sequenceTooLargeForDiffAlgorithm=Sequence too large for difference algorithm.
 serviceNotEnabledNoName=Service not enabled
 serviceNotPermitted={0} not permitted
-serviceNotPermittedNoName=Service not permitted
 shallowCommitsAlreadyInitialized=Shallow commits have already been initialized
 shortCompressedStreamAt=Short compressed stream at {0}
 shortReadOfBlock=Short read of block.
@@ -561,6 +565,9 @@
 stashFailed=Stashing local changes did not successfully complete
 stashResolveFailed=Reference ''{0}'' does not resolve to stashed commit
 statelessRPCRequiresOptionToBeEnabled=stateless RPC requires {0} to be enabled
+storePushCertMultipleRefs=Store push certificate for {0} refs
+storePushCertOneRef=Store push certificate for {0}
+storePushCertReflog=Store push certificate
 submoduleExists=Submodule ''{0}'' already exists in the index
 submoduleParentRemoteUrlInvalid=Cannot remove segment from remote url ''{0}''
 submodulesNotSupported=Submodules are not supported
@@ -602,6 +609,7 @@
 unableToCreateNewObject=Unable to create new object: {0}
 unableToStore=Unable to store {0}.
 unableToWrite=Unable to write {0}
+unauthorized=Unauthorized
 unencodeableFile=Unencodable file: {0}
 unexpectedCompareResult=Unexpected metadata comparison result: {0}
 unexpectedEndOfConfigFile=Unexpected end of config file
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 53901f5..b3bc319 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CloneCommand.java
@@ -127,16 +127,23 @@
 	 */
 	public Git call() throws GitAPIException, InvalidRemoteException,
 			org.eclipse.jgit.api.errors.TransportException {
+		Repository repository = null;
 		try {
 			URIish u = new URIish(uri);
-			Repository repository = init(u);
+			repository = init(u);
 			FetchResult result = fetch(repository, u);
 			if (!noCheckout)
 				checkout(repository, result);
-			return new Git(repository);
+			return new Git(repository, true);
 		} catch (IOException ioe) {
+			if (repository != null) {
+				repository.close();
+			}
 			throw new JGitInternalException(ioe.getMessage(), ioe);
 		} catch (URISyntaxException e) {
+			if (repository != null) {
+				repository.close();
+			}
 			throw new InvalidRemoteException(MessageFormat.format(
 					JGitText.get().invalidRemote, remote));
 		}
@@ -331,7 +338,8 @@
 
 	/**
 	 * @param uri
-	 *            the uri to clone from
+	 *            the URI to clone from, or {@code null} to unset the URI.
+	 *            The URI must be set before {@link #call} is called.
 	 * @return this instance
 	 */
 	public CloneCommand setURI(String uri) {
@@ -346,7 +354,8 @@
 	 * @see URIish#getHumanishName()
 	 *
 	 * @param directory
-	 *            the directory to clone to
+	 *            the directory to clone to, or {@code null} if the directory
+	 *            name should be taken from the source uri
 	 * @return this instance
 	 * @throws IllegalStateException
 	 *             if the combination of directory, gitDir and bare is illegal.
@@ -362,7 +371,8 @@
 
 	/**
 	 * @param gitDir
-	 *            the repository meta directory
+	 *            the repository meta directory, or {@code null} to choose one
+	 *            automatically at clone time
 	 * @return this instance
 	 * @throws IllegalStateException
 	 *             if the combination of directory, gitDir and bare is illegal.
@@ -400,10 +410,14 @@
 	 *
 	 * @see Constants#DEFAULT_REMOTE_NAME
 	 * @param remote
-	 *            name that keeps track of the upstream repository
+	 *            name that keeps track of the upstream repository.
+	 *            {@code null} means to use DEFAULT_REMOTE_NAME.
 	 * @return this instance
 	 */
 	public CloneCommand setRemote(String remote) {
+		if (remote == null) {
+			remote = Constants.DEFAULT_REMOTE_NAME;
+		}
 		this.remote = remote;
 		return this;
 	}
@@ -413,9 +427,15 @@
 	 *            the initial branch to check out when cloning the repository.
 	 *            Can be specified as ref name (<code>refs/heads/master</code>),
 	 *            branch name (<code>master</code>) or tag name (<code>v1.2.3</code>).
+	 *            The default is to use the branch pointed to by the cloned
+	 *            repository's HEAD and can be requested by passing {@code null}
+	 *            or <code>HEAD</code>.
 	 * @return this instance
 	 */
 	public CloneCommand setBranch(String branch) {
+		if (branch == null) {
+			branch = Constants.HEAD;
+		}
 		this.branch = branch;
 		return this;
 	}
@@ -430,6 +450,9 @@
 	 * @return {@code this}
 	 */
 	public CloneCommand setProgressMonitor(ProgressMonitor monitor) {
+		if (monitor == null) {
+			monitor = NullProgressMonitor.INSTANCE;
+		}
 		this.monitor = monitor;
 		return this;
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/DiffCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/DiffCommand.java
index 527daef..3e3a7a8 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/DiffCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/DiffCommand.java
@@ -268,7 +268,10 @@
 	 * @return this instance
 	 */
 	public DiffCommand setProgressMonitor(ProgressMonitor monitor) {
+		if (monitor == null) {
+			monitor = NullProgressMonitor.INSTANCE;
+		}
 		this.monitor = monitor;
 		return this;
 	}
-}
\ No newline at end of file
+}
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 29d475a..9620089 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/FetchCommand.java
@@ -244,6 +244,9 @@
 	 */
 	public FetchCommand setProgressMonitor(ProgressMonitor monitor) {
 		checkCallable();
+		if (monitor == null) {
+			monitor = NullProgressMonitor.INSTANCE;
+		}
 		this.monitor = monitor;
 		return this;
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java
index 1e9fe5c..addca4c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java
@@ -207,7 +207,7 @@
 		this(repo, false);
 	}
 
-	private Git(Repository repo, boolean closeRepo) {
+	Git(Repository repo, boolean closeRepo) {
 		if (repo == null)
 			throw new NullPointerException();
 		this.repo = repo;
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 63de85c..2783edd 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/PullCommand.java
@@ -130,6 +130,9 @@
 	 * @return this instance
 	 */
 	public PullCommand setProgressMonitor(ProgressMonitor monitor) {
+		if (monitor == null) {
+			monitor = NullProgressMonitor.INSTANCE;
+		}
 		this.monitor = monitor;
 		return this;
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/PushCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/PushCommand.java
index 0e1ce58..227e322 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/PushCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/PushCommand.java
@@ -257,6 +257,9 @@
 	 */
 	public PushCommand setProgressMonitor(ProgressMonitor monitor) {
 		checkCallable();
+		if (monitor == null) {
+			monitor = NullProgressMonitor.INSTANCE;
+		}
 		this.monitor = monitor;
 		return this;
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
index 7196a2f..ff29008 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/RebaseCommand.java
@@ -1493,6 +1493,9 @@
 	 * @return this instance
 	 */
 	public RebaseCommand setProgressMonitor(ProgressMonitor monitor) {
+		if (monitor == null) {
+			monitor = NullProgressMonitor.INSTANCE;
+		}
 		this.monitor = monitor;
 		return this;
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleAddCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleAddCommand.java
index 06c8f41..fbb24c1 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleAddCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/SubmoduleAddCommand.java
@@ -173,6 +173,8 @@
 		CloneCommand clone = Git.cloneRepository();
 		configure(clone);
 		clone.setDirectory(moduleDirectory);
+		clone.setGitDir(new File(new File(repo.getDirectory(),
+				Constants.MODULES), path));
 		clone.setURI(resolvedUri);
 		if (monitor != null)
 			clone.setProgressMonitor(monitor);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/RefNotFoundException.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/RefNotFoundException.java
index c8d96a0..b9f2a56 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/RefNotFoundException.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/RefNotFoundException.java
@@ -45,6 +45,15 @@
 
 	/**
 	 * @param message
+	 * @param cause
+	 * @since 4.1
+	 */
+	public RefNotFoundException(String message, Throwable cause) {
+		super(message, cause);
+	}
+
+	/**
+	 * @param message
 	 */
 	public RefNotFoundException(String message) {
 		super(message);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/StashApplyFailureException.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/StashApplyFailureException.java
index 25d7e4d..1d54f77 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/StashApplyFailureException.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/StashApplyFailureException.java
@@ -10,6 +10,15 @@
 	private static final long serialVersionUID = 1L;
 
 	/**
+	 * @param message
+	 * @param cause
+	 * @since 4.1
+	 */
+	public StashApplyFailureException(String message, Throwable cause) {
+		super(message, cause);
+	}
+
+	/**
 	 * Create a StashApplyFailedException
 	 *
 	 * @param message
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/UnmergedPathsException.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/UnmergedPathsException.java
index 0990040..082f94c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/UnmergedPathsException.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/errors/UnmergedPathsException.java
@@ -61,4 +61,13 @@
 	public UnmergedPathsException(Throwable cause) {
 		super(JGitText.get().unmergedPaths, cause);
 	}
+
+	/**
+	 * @param message
+	 * @param cause
+	 * @since 4.1
+	 */
+	public UnmergedPathsException(String message, Throwable cause) {
+		super(message, cause);
+	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffFormatter.java b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffFormatter.java
index bcc30c3..fc701f3 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffFormatter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/diff/DiffFormatter.java
@@ -665,16 +665,12 @@
 		format(res.header, res.a, res.b);
 	}
 
-	private static void writeGitLinkDiffText(OutputStream o, DiffEntry ent)
-			throws IOException {
-		if (ent.getOldMode() == GITLINK) {
-			o.write(encodeASCII("-Subproject commit " + ent.getOldId().name() //$NON-NLS-1$
-					+ "\n")); //$NON-NLS-1$
+	private static byte[] writeGitLinkText(AbbreviatedObjectId id) {
+		if (id.toObjectId().equals(ObjectId.zeroId())) {
+			return EMPTY;
 		}
-		if (ent.getNewMode() == GITLINK) {
-			o.write(encodeASCII("+Subproject commit " + ent.getNewId().name() //$NON-NLS-1$
-					+ "\n")); //$NON-NLS-1$
-		}
+		return encodeASCII("Subproject commit " + id.name() //$NON-NLS-1$
+				+ "\n"); //$NON-NLS-1$
 	}
 
 	private String format(AbbreviatedObjectId id) {
@@ -938,13 +934,7 @@
 
 		formatHeader(buf, ent);
 
-		if (ent.getOldMode() == GITLINK || ent.getNewMode() == GITLINK) {
-			formatOldNewPaths(buf, ent);
-			writeGitLinkDiffText(buf, ent);
-			editList = new EditList();
-			type = PatchType.UNIFIED;
-
-		} else if (ent.getOldId() == null || ent.getNewId() == null) {
+		if (ent.getOldId() == null || ent.getNewId() == null) {
 			// Content not changed (e.g. only mode, pure rename)
 			editList = new EditList();
 			type = PatchType.UNIFIED;
@@ -952,8 +942,15 @@
 		} else {
 			assertHaveRepository();
 
-			byte[] aRaw = open(OLD, ent);
-			byte[] bRaw = open(NEW, ent);
+			byte[] aRaw, bRaw;
+
+			if (ent.getOldMode() == GITLINK || ent.getNewMode() == GITLINK) {
+				aRaw = writeGitLinkText(ent.getOldId());
+				bRaw = writeGitLinkText(ent.getNewId());
+			} else {
+				aRaw = open(OLD, ent);
+				bRaw = open(NEW, ent);
+			}
 
 			if (aRaw == BINARY || bRaw == BINARY //
 					|| RawText.isBinary(aRaw) || RawText.isBinary(bRaw)) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheTree.java b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheTree.java
index 83aa8fa..f139afc 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheTree.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/dircache/DirCacheTree.java
@@ -478,6 +478,7 @@
 
 			// The entry is contained in this subtree.
 			//
+			assert(st != null);
 			st.validate(cache, cCnt, cIdx, pathOff + st.nameLength() + 1);
 			cIdx += st.entrySpan;
 			entrySpan += st.entrySpan;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/DiffInterruptedException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/DiffInterruptedException.java
index e6ae685..5f9ce35 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/errors/DiffInterruptedException.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/DiffInterruptedException.java
@@ -51,4 +51,26 @@
  */
 public class DiffInterruptedException extends RuntimeException {
 	private static final long serialVersionUID = 1L;
+
+	/**
+	 * @param message
+	 * @param cause
+	 * @since 4.1
+	 */
+	public DiffInterruptedException(String message, Throwable cause) {
+		super(message, cause);
+	}
+
+	/**
+	 * @param message
+	 * @since 4.1
+	 */
+	public DiffInterruptedException(String message) {
+		super(message);
+	}
+
+	/** Indicates that the thread computing a diff was interrupted. */
+	public DiffInterruptedException() {
+		super();
+	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/InvalidObjectIdException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/InvalidObjectIdException.java
index b545312..390545f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/errors/InvalidObjectIdException.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/InvalidObjectIdException.java
@@ -45,7 +45,8 @@
 
 package org.eclipse.jgit.errors;
 
-import java.io.UnsupportedEncodingException;
+import static java.nio.charset.StandardCharsets.US_ASCII;
+
 import java.text.MessageFormat;
 
 import org.eclipse.jgit.internal.JGitText;
@@ -64,16 +65,25 @@
 	 * @param length of the sequence of invalid bytes.
 	 */
 	public InvalidObjectIdException(byte[] bytes, int offset, int length) {
-		super(MessageFormat.format(JGitText.get().invalidId, asAscii(bytes, offset, length)));
+		super(msg(bytes, offset, length));
 	}
 
-	private static String asAscii(byte[] bytes, int offset, int length) {
+	/**
+	 * @param id the invalid id.
+	 *
+	 * @since 4.1
+	 */
+	public InvalidObjectIdException(String id) {
+		super(MessageFormat.format(JGitText.get().invalidId, id));
+	}
+
+	private static String msg(byte[] bytes, int offset, int length) {
 		try {
-			return ": " + new String(bytes, offset, length, "US-ASCII"); //$NON-NLS-1$ //$NON-NLS-2$
-		} catch (UnsupportedEncodingException e2) {
-			return ""; //$NON-NLS-1$
-		} catch (StringIndexOutOfBoundsException e2) {
-			return ""; //$NON-NLS-1$
+			return MessageFormat.format(
+					JGitText.get().invalidId,
+					new String(bytes, offset, length, US_ASCII));
+		} catch (StringIndexOutOfBoundsException e) {
+			return JGitText.get().invalidId0;
 		}
 	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/LockFailedException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/LockFailedException.java
index 18aa9d9..0142e17 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/errors/LockFailedException.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/LockFailedException.java
@@ -57,6 +57,20 @@
 	private File file;
 
 	/**
+	 * @param file
+	 *            file that could not be locked
+	 * @param message
+	 *            exception message
+	 * @param cause
+	 *            cause, for later retrieval by {@link Throwable#getCause()}
+	 * @since 4.1
+	 */
+	public LockFailedException(File file, String message, Throwable cause) {
+		super(message, cause);
+		this.file = file;
+	}
+
+	/**
 	 * Construct a CannotLockException for the given file and message
 	 *
 	 * @param file
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/errors/PackProtocolException.java b/org.eclipse.jgit/src/org/eclipse/jgit/errors/PackProtocolException.java
index 5503bd1..44bc164 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/errors/PackProtocolException.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/errors/PackProtocolException.java
@@ -60,7 +60,7 @@
 	 * @param uri
 	 *            URI used for transport
 	 * @param s
-	 *            message
+	 *            message, which may be shown to an end-user.
 	 */
 	public PackProtocolException(final URIish uri, final String s) {
 		super(uri + ": " + s); //$NON-NLS-1$
@@ -73,7 +73,7 @@
 	 * @param uri
 	 *            URI used for transport
 	 * @param s
-	 *            message
+	 *            message, which may be shown to an end-user.
 	 * @param cause
 	 *            root cause exception
 	 */
@@ -86,7 +86,7 @@
 	 * Constructs an PackProtocolException with the specified detail message.
 	 *
 	 * @param s
-	 *            message
+	 *            message, which may be shown to an end-user.
 	 */
 	public PackProtocolException(final String s) {
 		super(s);
@@ -96,7 +96,7 @@
 	 * Constructs an PackProtocolException with the specified detail message.
 	 *
 	 * @param s
-	 *            message
+	 *            message, which may be shown to an end-user.
 	 * @param cause
 	 *            root cause exception
 	 */
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java
index fa27948..891479d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java
@@ -183,6 +183,9 @@
 			String qName,
 			Attributes attributes) throws SAXException {
 		if ("project".equals(qName)) { //$NON-NLS-1$
+			if (attributes.getValue("name") == null) { //$NON-NLS-1$
+				throw new SAXException(RepoText.get().invalidManifest);
+			}
 			currentProject = new RepoProject(
 					attributes.getValue("name"), //$NON-NLS-1$
 					attributes.getValue("path"), //$NON-NLS-1$
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 b39dd8a..790f4db 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java
@@ -105,6 +105,7 @@
 	private String uri;
 	private String groups;
 	private String branch;
+	private String targetBranch = Constants.HEAD;
 	private PersonIdent author;
 	private RemoteReader callback;
 	private InputStream inputStream;
@@ -224,27 +225,27 @@
 	/**
 	 * @param repo
 	 */
-	public RepoCommand(final Repository repo) {
+	public RepoCommand(Repository repo) {
 		super(repo);
 	}
 
 	/**
 	 * Set path to the manifest XML file.
-	 *
+	 * <p>
 	 * Calling {@link #setInputStream} will ignore the path set here.
 	 *
 	 * @param path
 	 *            (with <code>/</code> as separator)
 	 * @return this command
 	 */
-	public RepoCommand setPath(final String path) {
+	public RepoCommand setPath(String path) {
 		this.path = path;
 		return this;
 	}
 
 	/**
 	 * Set the input stream to the manifest XML.
-	 *
+	 * <p>
 	 * Setting inputStream will ignore the path set. It will be closed in
 	 * {@link #call}.
 	 *
@@ -252,7 +253,7 @@
 	 * @return this command
 	 * @since 3.5
 	 */
-	public RepoCommand setInputStream(final InputStream inputStream) {
+	public RepoCommand setInputStream(InputStream inputStream) {
 		this.inputStream = inputStream;
 		return this;
 	}
@@ -263,7 +264,7 @@
 	 * @param uri
 	 * @return this command
 	 */
-	public RepoCommand setURI(final String uri) {
+	public RepoCommand setURI(String uri) {
 		this.uri = uri;
 		return this;
 	}
@@ -274,14 +275,14 @@
 	 * @param groups groups separated by comma, examples: default|all|G1,-G2,-G3
 	 * @return this command
 	 */
-	public RepoCommand setGroups(final String groups) {
+	public RepoCommand setGroups(String groups) {
 		this.groups = groups;
 		return this;
 	}
 
 	/**
 	 * Set default branch.
-	 *
+	 * <p>
 	 * This is generally the name of the branch the manifest file was in. If
 	 * there's no default revision (branch) specified in manifest and no
 	 * revision specified in project, this branch will be used.
@@ -289,12 +290,30 @@
 	 * @param branch
 	 * @return this command
 	 */
-	public RepoCommand setBranch(final String branch) {
+	public RepoCommand setBranch(String branch) {
 		this.branch = branch;
 		return this;
 	}
 
 	/**
+	 * Set target branch.
+	 * <p>
+	 * This is the target branch of the super project to be updated. If not set,
+	 * default is HEAD.
+	 * <p>
+	 * For non-bare repositories, HEAD will always be used and this will be
+	 * ignored.
+	 *
+	 * @param branch
+	 * @return this command
+	 * @since 4.1
+	 */
+	public RepoCommand setTargetBranch(String branch) {
+		this.targetBranch = Constants.R_HEADS + branch;
+		return this;
+	}
+
+	/**
 	 * The progress monitor associated with the clone operation. By default,
 	 * this is set to <code>NullProgressMonitor</code>
 	 *
@@ -309,7 +328,7 @@
 
 	/**
 	 * Set the author/committer for the bare repository commit.
-	 *
+	 * <p>
 	 * For non-bare repositories, the current user will be used and this will be
 	 * ignored.
 	 *
@@ -445,7 +464,7 @@
 				ObjectId treeId = index.writeTree(inserter);
 
 				// Create a Commit object, populate it and write it
-				ObjectId headId = repo.resolve(Constants.HEAD + "^{commit}"); //$NON-NLS-1$
+				ObjectId headId = repo.resolve(targetBranch + "^{commit}"); //$NON-NLS-1$
 				CommitBuilder commit = new CommitBuilder();
 				commit.setTreeId(treeId);
 				if (headId != null)
@@ -457,7 +476,7 @@
 				ObjectId commitId = inserter.insert(commit);
 				inserter.flush();
 
-				RefUpdate ru = repo.updateRef(Constants.HEAD);
+				RefUpdate ru = repo.updateRef(targetBranch);
 				ru.setNewObjectId(commitId);
 				ru.setExpectedOldObjectId(headId != null ? headId : ObjectId.zeroId());
 				Result rc = ru.update(rw);
@@ -471,12 +490,14 @@
 					case REJECTED:
 					case LOCK_FAILURE:
 						throw new ConcurrentRefUpdateException(
-								JGitText.get().couldNotLockHEAD, ru.getRef(),
+								MessageFormat.format(
+										JGitText.get().cannotLock, targetBranch),
+								ru.getRef(),
 								rc);
 					default:
 						throw new JGitInternalException(MessageFormat.format(
 								JGitText.get().updatingRefFailed,
-								Constants.HEAD, commitId.name(), rc));
+								targetBranch, commitId.name(), rc));
 				}
 
 				return rw.parseCommit(commitId);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java
index 1fff1c3..9a07211 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java
@@ -138,6 +138,9 @@
 	 */
 	public RepoProject(String name, String path, String revision,
 			String remote, String groups) {
+		if (name == null) {
+			throw new NullPointerException();
+		}
 		this.name = name;
 		if (path != null)
 			this.path = path;
@@ -221,7 +224,7 @@
 	/**
 	 * Get the name of the remote definition of the sub repo.
 	 *
-	 * @return {@remote}
+	 * @return {@code remote}
 	 */
 	public String getRemote() {
 		return remote;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/FastIgnoreRule.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/FastIgnoreRule.java
index 2303ffd..e376cbb 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/FastIgnoreRule.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/FastIgnoreRule.java
@@ -47,6 +47,8 @@
 import org.eclipse.jgit.errors.InvalidPatternException;
 import org.eclipse.jgit.ignore.internal.IMatcher;
 import org.eclipse.jgit.ignore.internal.PathMatcher;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * "Fast" (compared with IgnoreRule) git ignore rule implementation supporting
@@ -57,6 +59,8 @@
  * @since 3.6
  */
 public class FastIgnoreRule {
+	private final static Logger LOG = LoggerFactory
+			.getLogger(FastIgnoreRule.class);
 
 	/**
 	 * Character used as default path separator for ignore entries
@@ -98,24 +102,32 @@
 		if (pattern.charAt(0) == '#') {
 			this.matcher = NO_MATCH;
 			dirOnly = false;
-		} else {
-			dirOnly = pattern.charAt(pattern.length() - 1) == PATH_SEPARATOR;
-			if (dirOnly) {
-				pattern = stripTrailing(pattern, PATH_SEPARATOR);
-				if (pattern.length() == 0) {
-					this.matcher = NO_MATCH;
-					return;
-				}
-			}
-			IMatcher m;
-			try {
-				m = PathMatcher.createPathMatcher(pattern,
-						Character.valueOf(PATH_SEPARATOR), dirOnly);
-			} catch (InvalidPatternException e) {
-				m = NO_MATCH;
-			}
-			this.matcher = m;
+			return;
 		}
+		if (pattern.charAt(0) == '\\' && pattern.length() > 1) {
+			char next = pattern.charAt(1);
+			if (next == '!' || next == '#') {
+				// remove backslash escaping first special characters
+				pattern = pattern.substring(1);
+			}
+		}
+		dirOnly = pattern.charAt(pattern.length() - 1) == PATH_SEPARATOR;
+		if (dirOnly) {
+			pattern = stripTrailing(pattern, PATH_SEPARATOR);
+			if (pattern.length() == 0) {
+				this.matcher = NO_MATCH;
+				return;
+			}
+		}
+		IMatcher m;
+		try {
+			m = PathMatcher.createPathMatcher(pattern,
+					Character.valueOf(PATH_SEPARATOR), dirOnly);
+		} catch (InvalidPatternException e) {
+			m = NO_MATCH;
+			LOG.error(e.getMessage(), e);
+		}
+		this.matcher = m;
 	}
 
 	/**
@@ -176,6 +188,14 @@
 		return !inverse;
 	}
 
+	/**
+	 * @return true if the rule never matches (comment line or broken pattern)
+	 * @since 4.1
+	 */
+	public boolean isEmpty() {
+		return matcher == NO_MATCH;
+	}
+
 	@Override
 	public String toString() {
 		StringBuilder sb = new StringBuilder();
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/IgnoreNode.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/IgnoreNode.java
index efaeacd..8b1244e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/IgnoreNode.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/IgnoreNode.java
@@ -109,9 +109,12 @@
 		BufferedReader br = asReader(in);
 		String txt;
 		while ((txt = br.readLine()) != null) {
-			txt = txt.trim();
-			if (txt.length() > 0 && !txt.startsWith("#") && !txt.equals("/")) //$NON-NLS-1$ //$NON-NLS-2$
-				rules.add(new FastIgnoreRule(txt));
+			if (txt.length() > 0 && !txt.startsWith("#") && !txt.equals("/")) { //$NON-NLS-1$ //$NON-NLS-2$
+				FastIgnoreRule rule = new FastIgnoreRule(txt);
+				if (!rule.isEmpty()) {
+					rules.add(rule);
+				}
+			}
 		}
 	}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/LeadingAsteriskMatcher.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/LeadingAsteriskMatcher.java
index f1153d9..3d0ad09 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/LeadingAsteriskMatcher.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/LeadingAsteriskMatcher.java
@@ -50,7 +50,7 @@
 public class LeadingAsteriskMatcher extends NameMatcher {
 
 	LeadingAsteriskMatcher(String pattern, Character pathSeparator, boolean dirOnly) {
-		super(pattern, pathSeparator, dirOnly);
+		super(pattern, pathSeparator, dirOnly, true);
 
 		if (subPattern.charAt(0) != '*')
 			throw new IllegalArgumentException(
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/NameMatcher.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/NameMatcher.java
index 6c4c809..8beae83 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/NameMatcher.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/NameMatcher.java
@@ -58,9 +58,13 @@
 
 	final String subPattern;
 
-	NameMatcher(String pattern, Character pathSeparator, boolean dirOnly) {
+	NameMatcher(String pattern, Character pathSeparator, boolean dirOnly,
+			boolean deleteBackslash) {
 		super(pattern, dirOnly);
 		slash = getPathSeparator(pathSeparator);
+		if (deleteBackslash) {
+			pattern = Strings.deleteBackslash(pattern);
+		}
 		beginning = pattern.length() == 0 ? false : pattern.charAt(0) == slash;
 		if (!beginning)
 			this.subPattern = pattern;
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 d3e5f6a..c3f6694 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
@@ -85,7 +85,8 @@
 	}
 
 	private boolean isSimplePathWithSegments(String path) {
-		return !isWildCard(path) && count(path, slash, true) > 0;
+		return !isWildCard(path) && path.indexOf('\\') < 0
+				&& count(path, slash, true) > 0;
 	}
 
 	static private List<IMatcher> createMatchers(List<String> segments,
@@ -118,7 +119,7 @@
 	public static IMatcher createPathMatcher(String pattern,
 			Character pathSeparator, boolean dirOnly)
 			throws InvalidPatternException {
-		pattern = pattern.trim();
+		pattern = trim(pattern);
 		char slash = Strings.getPathSeparator(pathSeparator);
 		// ignore possible leading and trailing slash
 		int slashIdx = pattern.indexOf(slash, 1);
@@ -127,6 +128,29 @@
 		return createNameMatcher0(pattern, pathSeparator, dirOnly);
 	}
 
+	/**
+	 * Trim trailing spaces, unless they are escaped with backslash, see
+	 * https://www.kernel.org/pub/software/scm/git/docs/gitignore.html
+	 *
+	 * @param pattern
+	 *            non null
+	 * @return trimmed pattern
+	 */
+	private static String trim(String pattern) {
+		while (pattern.length() > 0
+				&& pattern.charAt(pattern.length() - 1) == ' ') {
+			if (pattern.length() > 1
+					&& pattern.charAt(pattern.length() - 2) == '\\') {
+				// last space was escaped by backslash: remove backslash and
+				// keep space
+				pattern = pattern.substring(0, pattern.length() - 2) + " "; //$NON-NLS-1$
+				return pattern;
+			}
+			pattern = pattern.substring(0, pattern.length() - 1);
+		}
+		return pattern;
+	}
+
 	private static IMatcher createNameMatcher0(String segment,
 			Character pathSeparator, boolean dirOnly)
 			throws InvalidPatternException {
@@ -144,7 +168,7 @@
 		case COMPLEX:
 			return new WildCardMatcher(segment, pathSeparator, dirOnly);
 		default:
-			return new NameMatcher(segment, pathSeparator, dirOnly);
+			return new NameMatcher(segment, pathSeparator, dirOnly, true);
 		}
 	}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/Strings.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/Strings.java
index cd4d753..f972828 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/Strings.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/Strings.java
@@ -44,13 +44,16 @@
 
 import static java.lang.Character.isLetter;
 
+import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
 
 import org.eclipse.jgit.errors.InvalidPatternException;
 import org.eclipse.jgit.ignore.FastIgnoreRule;
+import org.eclipse.jgit.internal.JGitText;
 
 /**
  * Various {@link String} related utility methods, written mostly to avoid
@@ -138,16 +141,36 @@
 	private static boolean isComplexWildcard(String pattern) {
 		int idx1 = pattern.indexOf('[');
 		if (idx1 != -1) {
-			int idx2 = pattern.indexOf(']');
+			int idx2 = pattern.indexOf(']', idx1);
 			if (idx2 > idx1)
 				return true;
 		}
-		// required to match escaped backslashes '\\\\'
-		if (pattern.indexOf('?') != -1 || pattern.indexOf('\\') != -1)
+		if (pattern.indexOf('?') != -1) {
 			return true;
+		} else {
+			// check if the backslash escapes one of the glob special characters
+			// if not, backslash is not part of a regex and treated literally
+			int backSlash = pattern.indexOf('\\');
+			if (backSlash >= 0) {
+				int nextIdx = backSlash + 1;
+				if (pattern.length() == nextIdx) {
+					return false;
+				}
+				char nextChar = pattern.charAt(nextIdx);
+				if (escapedByBackslash(nextChar)) {
+					return true;
+				} else {
+					return false;
+				}
+			}
+		}
 		return false;
 	}
 
+	private static boolean escapedByBackslash(char nextChar) {
+		return nextChar == '?' || nextChar == '*' || nextChar == '[';
+	}
+
 	static PatternState checkWildCards(String pattern) {
 		if (isComplexWildcard(pattern))
 			return PatternState.COMPLEX;
@@ -226,7 +249,7 @@
 		char[] charClass = new char[6];
 
 		for (int i = 0; i < pattern.length(); i++) {
-			char c = pattern.charAt(i);
+			final char c = pattern.charAt(i);
 			switch (c) {
 
 			case '*':
@@ -236,6 +259,20 @@
 					sb.append('.').append(c);
 				break;
 
+			case '(': // fall-through
+			case ')': // fall-through
+			case '{': // fall-through
+			case '}': // fall-through
+			case '+': // fall-through
+			case '$': // fall-through
+			case '^': // fall-through
+			case '|':
+				if (seenEscape || in_brackets > 0)
+					sb.append(c);
+				else
+					sb.append('\\').append(c);
+				break;
+
 			case '.':
 				if (seenEscape)
 					sb.append(c);
@@ -273,6 +310,14 @@
 					char lookAhead = lookAhead(pattern, i);
 					if (lookAhead == ']' || lookAhead == '[')
 						ignoreLastBracket = true;
+				} else {
+					//
+					char lookAhead = lookAhead(pattern, i);
+					if (lookAhead != '\\' && lookAhead != '['
+							&& lookAhead != '?' && lookAhead != '*'
+							&& lookAhead != ' ' && lookBehind(sb) != '\\') {
+						break;
+					}
 				}
 				sb.append(c);
 				break;
@@ -349,7 +394,16 @@
 
 		if (in_brackets > 0)
 			throw new InvalidPatternException("Not closed bracket?", pattern); //$NON-NLS-1$
-		return Pattern.compile(sb.toString());
+		try {
+			return Pattern.compile(sb.toString());
+		} catch (PatternSyntaxException e) {
+			InvalidPatternException patternException = new InvalidPatternException(
+					MessageFormat.format(JGitText.get().invalidIgnoreRule,
+							pattern),
+					pattern);
+			patternException.initCause(e);
+			throw patternException;
+		}
 	}
 
 	/**
@@ -401,4 +455,30 @@
 		return null;
 	}
 
+	static String deleteBackslash(String s) {
+		if (s.indexOf('\\') < 0) {
+			return s;
+		}
+		StringBuilder sb = new StringBuilder(s.length());
+		for (int i = 0; i < s.length(); i++) {
+			char ch = s.charAt(i);
+			if (ch == '\\') {
+				if (i + 1 == s.length()) {
+					continue;
+				}
+				char next = s.charAt(i + 1);
+				if (next == '\\') {
+					sb.append(ch);
+					i++;
+					continue;
+				}
+				if (!escapedByBackslash(next)) {
+					continue;
+				}
+			}
+			sb.append(ch);
+		}
+		return sb.toString();
+	}
+
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/TrailingAsteriskMatcher.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/TrailingAsteriskMatcher.java
index 4a1c780..b927d27 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/TrailingAsteriskMatcher.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/TrailingAsteriskMatcher.java
@@ -50,7 +50,7 @@
 public class TrailingAsteriskMatcher extends NameMatcher {
 
 	TrailingAsteriskMatcher(String pattern, Character pathSeparator, boolean dirOnly) {
-		super(pattern, pathSeparator, dirOnly);
+		super(pattern, pathSeparator, dirOnly, true);
 
 		if (subPattern.charAt(subPattern.length() - 1) != '*')
 			throw new IllegalArgumentException(
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/WildCardMatcher.java b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/WildCardMatcher.java
index 7d12b0d..8f98152 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/WildCardMatcher.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/ignore/internal/WildCardMatcher.java
@@ -62,7 +62,7 @@
 
 	WildCardMatcher(String pattern, Character pathSeparator, boolean dirOnly)
 			throws InvalidPatternException {
-		super(pattern, pathSeparator, dirOnly);
+		super(pattern, pathSeparator, dirOnly, false);
 		p = convertGlob(subPattern);
 	}
 
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 9f6efef..9067e82 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
@@ -296,12 +296,10 @@
 	/***/ public String errorEncodingFromFile;
 	/***/ public String errorInBase64CodeReadingStream;
 	/***/ public String errorInPackedRefs;
-	/***/ public String errorInvalidPushCert;
 	/***/ public String errorInvalidProtocolWantedOldNewRef;
 	/***/ public String errorListing;
 	/***/ public String errorOccurredDuringUnpackingOnTheRemoteEnd;
 	/***/ public String errorReadingInfoRefs;
-	/***/ public String errorSymlinksNotSupported;
 	/***/ public String exceptionCaughtDuringExecutionOfHook;
 	/***/ public String exceptionCaughtDuringExecutionOfAddCommand;
 	/***/ public String exceptionCaughtDuringExecutionOfArchiveCommand;
@@ -347,7 +345,6 @@
 	/***/ public String gcFailed;
 	/***/ public String gitmodulesNotFound;
 	/***/ public String headRequiredToStash;
-	/***/ public String hiddenFilesStartWithDot;
 	/***/ public String hoursAgo;
 	/***/ public String hugeIndexesAreNotSupportedByJgitYet;
 	/***/ public String hunkBelongsToAnotherFile;
@@ -385,8 +382,10 @@
 	/***/ public String invalidGitdirRef;
 	/***/ public String invalidGitType;
 	/***/ public String invalidId;
+	/***/ public String invalidId0;
 	/***/ public String invalidIdLength;
 	/***/ public String invalidIgnoreParamSubmodule;
+	/***/ public String invalidIgnoreRule;
 	/***/ public String invalidIntegerValue;
 	/***/ public String invalidKey;
 	/***/ public String invalidLineInConfigFile;
@@ -403,6 +402,7 @@
 	/***/ public String invalidReflogRevision;
 	/***/ public String invalidRefName;
 	/***/ public String invalidRemote;
+	/***/ public String invalidShallowObject;
 	/***/ public String invalidStageForPath;
 	/***/ public String invalidTagOption;
 	/***/ public String invalidTimeout;
@@ -499,6 +499,7 @@
 	/***/ public String packChecksumMismatch;
 	/***/ public String packCorruptedWhileWritingToFilesystem;
 	/***/ public String packDoesNotMatchIndex;
+	/***/ public String packedRefsHandleIsStale;
 	/***/ public String packetSizeMustBeAtLeast;
 	/***/ public String packetSizeMustBeAtMost;
 	/***/ public String packfileCorruptionDetected;
@@ -532,6 +533,10 @@
 	/***/ public String pullOnRepoWithoutHEADCurrentlyNotSupported;
 	/***/ public String pullTaskName;
 	/***/ public String pushCancelled;
+	/***/ public String pushCertificateInvalidField;
+	/***/ public String pushCertificateInvalidFieldValue;
+	/***/ public String pushCertificateInvalidHeader;
+	/***/ public String pushCertificateInvalidSignature;
 	/***/ public String pushIsNotSupportedForBundleTransport;
 	/***/ public String pushNotPermitted;
 	/***/ public String rawLogMessageDoesNotParseAsLogEntry;
@@ -590,7 +595,6 @@
 	/***/ public String sequenceTooLargeForDiffAlgorithm;
 	/***/ public String serviceNotEnabledNoName;
 	/***/ public String serviceNotPermitted;
-	/***/ public String serviceNotPermittedNoName;
 	/***/ public String shallowCommitsAlreadyInitialized;
 	/***/ public String shortCompressedStreamAt;
 	/***/ public String shortReadOfBlock;
@@ -620,6 +624,9 @@
 	/***/ public String stashFailed;
 	/***/ public String stashResolveFailed;
 	/***/ public String statelessRPCRequiresOptionToBeEnabled;
+	/***/ public String storePushCertMultipleRefs;
+	/***/ public String storePushCertOneRef;
+	/***/ public String storePushCertReflog;
 	/***/ public String submoduleExists;
 	/***/ public String submodulesNotSupported;
 	/***/ public String submoduleParentRemoteUrlInvalid;
@@ -661,6 +668,7 @@
 	/***/ public String unableToCreateNewObject;
 	/***/ public String unableToStore;
 	/***/ public String unableToWrite;
+	/***/ public String unauthorized;
 	/***/ public String unencodeableFile;
 	/***/ public String unexpectedCompareResult;
 	/***/ public String unexpectedEndOfConfigFile;
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 de66292..faf27e3 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
@@ -72,6 +72,7 @@
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.storage.pack.PackConfig;
+import org.eclipse.jgit.storage.pack.PackStatistics;
 import org.eclipse.jgit.util.io.CountingOutputStream;
 
 /** Repack and garbage collect a repository. */
@@ -84,7 +85,7 @@
 
 	private final List<DfsPackDescription> newPackDesc;
 
-	private final List<PackWriter.Statistics> newPackStats;
+	private final List<PackStatistics> newPackStats;
 
 	private final List<PackWriter.ObjectIdSet> newPackObj;
 
@@ -115,7 +116,7 @@
 		refdb = repo.getRefDatabase();
 		objdb = repo.getObjectDatabase();
 		newPackDesc = new ArrayList<DfsPackDescription>(4);
-		newPackStats = new ArrayList<PackWriter.Statistics>(4);
+		newPackStats = new ArrayList<PackStatistics>(4);
 		newPackObj = new ArrayList<PackWriter.ObjectIdSet>(4);
 
 		packConfig = new PackConfig(repo);
@@ -258,7 +259,7 @@
 	}
 
 	/** @return statistics corresponding to the {@link #getNewPacks()}. */
-	public List<PackWriter.Statistics> getNewPackStatistics() {
+	public List<PackStatistics> getNewPackStatistics() {
 		return newPackStats;
 	}
 
@@ -396,7 +397,7 @@
 			}
 		});
 
-		PackWriter.Statistics stats = pw.getStatistics();
+		PackStatistics stats = pw.getStatistics();
 		pack.setPackStats(stats);
 		newPackStats.add(stats);
 
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 dbe72b2..7073763 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
@@ -67,6 +67,7 @@
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.storage.pack.PackConfig;
+import org.eclipse.jgit.storage.pack.PackStatistics;
 import org.eclipse.jgit.util.BlockList;
 import org.eclipse.jgit.util.io.CountingOutputStream;
 
@@ -94,7 +95,7 @@
 
 	private final List<DfsPackDescription> newPacks;
 
-	private final List<PackWriter.Statistics> newStats;
+	private final List<PackStatistics> newStats;
 
 	private int autoAddSize;
 
@@ -114,7 +115,7 @@
 		srcPacks = new ArrayList<DfsPackFile>();
 		exclude = new ArrayList<PackWriter.ObjectIdSet>(4);
 		newPacks = new ArrayList<DfsPackDescription>(1);
-		newStats = new ArrayList<PackWriter.Statistics>(1);
+		newStats = new ArrayList<PackStatistics>(1);
 	}
 
 	/**
@@ -231,7 +232,7 @@
 					writePack(objdb, pack, pw, pm);
 					writeIndex(objdb, pack, pw);
 
-					PackWriter.Statistics stats = pw.getStatistics();
+					PackStatistics stats = pw.getStatistics();
 					pw.close();
 					pw = null;
 
@@ -264,7 +265,7 @@
 	}
 
 	/** @return statistics corresponding to the {@link #getNewPacks()}. */
-	public List<PackWriter.Statistics> getNewPackStatistics() {
+	public List<PackStatistics> getNewPackStatistics() {
 		return newStats;
 	}
 
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 fba5157..2b9d0e5 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
@@ -50,7 +50,7 @@
 
 import org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource;
 import org.eclipse.jgit.internal.storage.pack.PackExt;
-import org.eclipse.jgit.internal.storage.pack.PackWriter;
+import org.eclipse.jgit.storage.pack.PackStatistics;
 
 /**
  * Description of a DFS stored pack/index file.
@@ -75,7 +75,7 @@
 
 	private long deltaCount;
 
-	private PackWriter.Statistics stats;
+	private PackStatistics stats;
 
 	private int extensions;
 
@@ -225,11 +225,11 @@
 	 *         DfsGarbageCollector or DfsPackCompactor, and only when the pack
 	 *         is being committed to the repository.
 	 */
-	public PackWriter.Statistics getPackStats() {
+	public PackStatistics getPackStats() {
 		return stats;
 	}
 
-	DfsPackDescription setPackStats(PackWriter.Statistics stats) {
+	DfsPackDescription setPackStats(PackStatistics stats) {
 		this.stats = stats;
 		setFileSize(PACK, stats.getTotalBytes());
 		setObjectCount(stats.getTotalObjects());
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 75b0646..96f1d54 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
@@ -533,7 +533,6 @@
 		return ByteBuffer.wrap(copyBuf, 0, bs);
 	}
 
-	@SuppressWarnings("null")
 	void copyAsIs(PackOutputStream out, DfsObjectToPack src,
 			boolean validate, DfsReader ctx) throws IOException,
 			StoredObjectRepresentationNotAvailableException {
@@ -567,22 +566,26 @@
 				c = buf[headerCnt++] & 0xff;
 			} while ((c & 128) != 0);
 			if (validate) {
+				assert(crc1 != null && crc2 != null);
 				crc1.update(buf, 0, headerCnt);
 				crc2.update(buf, 0, headerCnt);
 			}
 		} else if (typeCode == Constants.OBJ_REF_DELTA) {
 			if (validate) {
+				assert(crc1 != null && crc2 != null);
 				crc1.update(buf, 0, headerCnt);
 				crc2.update(buf, 0, headerCnt);
 			}
 
 			readFully(src.offset + headerCnt, buf, 0, 20, ctx);
 			if (validate) {
+				assert(crc1 != null && crc2 != null);
 				crc1.update(buf, 0, 20);
 				crc2.update(buf, 0, 20);
 			}
 			headerCnt += 20;
 		} else if (validate) {
+			assert(crc1 != null && crc2 != null);
 			crc1.update(buf, 0, headerCnt);
 			crc2.update(buf, 0, headerCnt);
 		}
@@ -599,6 +602,7 @@
 			quickCopy = ctx.quickCopy(this, dataOffset, dataLength);
 
 			if (validate && idx(ctx).hasCRC32Support()) {
+				assert(crc1 != null);
 				// Index has the CRC32 code cached, validate the object.
 				//
 				expectedCRC = idx(ctx).findCRC32(src);
@@ -622,6 +626,7 @@
 							Long.valueOf(src.offset), getPackName()));
 				}
 			} else if (validate) {
+				assert(crc1 != null);
 				// We don't have a CRC32 code in the index, so compute it
 				// now while inflating the raw data to get zlib to tell us
 				// whether or not the data is safe.
@@ -709,16 +714,21 @@
 			while (cnt > 0) {
 				final int n = (int) Math.min(cnt, buf.length);
 				readFully(pos, buf, 0, n, ctx);
-				if (validate)
+				if (validate) {
+					assert(crc2 != null);
 					crc2.update(buf, 0, n);
+				}
 				out.write(buf, 0, n);
 				pos += n;
 				cnt -= n;
 			}
-			if (validate && crc2.getValue() != expectedCRC) {
-				throw new CorruptObjectException(MessageFormat.format(
-						JGitText.get().objectAtHasBadZlibStream,
-						Long.valueOf(src.offset), getPackName()));
+			if (validate) {
+				assert(crc2 != null);
+				if (crc2.getValue() != expectedCRC) {
+					throw new CorruptObjectException(MessageFormat.format(
+							JGitText.get().objectAtHasBadZlibStream,
+							Long.valueOf(src.offset), getPackName()));
+				}
 			}
 		}
 	}
@@ -837,7 +847,6 @@
 		return buf.position();
 	}
 
-	@SuppressWarnings("null")
 	ObjectLoader load(DfsReader ctx, long pos)
 			throws IOException {
 		try {
@@ -934,6 +943,7 @@
 			if (data == null)
 				throw new LargeObjectException();
 
+			assert(delta != null);
 			do {
 				// Cache only the base immediately before desired object.
 				if (cached)
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRefDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRefDatabase.java
index ed8acd5..a1035a1 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRefDatabase.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsRefDatabase.java
@@ -91,6 +91,13 @@
 	}
 
 	@Override
+	public Ref exactRef(String name) throws IOException {
+		RefCache curr = read();
+		Ref ref = curr.ids.get(name);
+		return ref != null ? resolve(ref, 0, curr.ids) : null;
+	}
+
+	@Override
 	public Ref getRef(String needle) throws IOException {
 		RefCache curr = read();
 		for (String prefix : SEARCH_PATH) {
@@ -103,14 +110,6 @@
 		return null;
 	}
 
-	private Ref getOneRef(String refName) throws IOException {
-		RefCache curr = read();
-		Ref ref = curr.ids.get(refName);
-		if (ref != null)
-			return resolve(ref, 0, curr.ids);
-		return ref;
-	}
-
 	@Override
 	public List<Ref> getAdditionalRefs() {
 		return Collections.emptyList();
@@ -212,7 +211,7 @@
 	public RefUpdate newUpdate(String refName, boolean detach)
 			throws IOException {
 		boolean detachingSymbolicRef = false;
-		Ref ref = getOneRef(refName);
+		Ref ref = exactRef(refName);
 		if (ref == null)
 			ref = new ObjectIdRef.Unpeeled(NEW, refName, null);
 		else
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java
index 796109a..e7ef127 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ObjectDirectory.java
@@ -114,8 +114,6 @@
 	/** Maximum number of candidates offered as resolutions of abbreviation. */
 	private static final int RESOLVE_ABBREV_LIMIT = 256;
 
-	private static final String STALE_FILE_HANDLE_MSG = "stale file handle"; //$NON-NLS-1$
-
 	private final Config config;
 
 	private final File objects;
@@ -565,8 +563,7 @@
 		} else if (e instanceof FileNotFoundException) {
 			warnTmpl = JGitText.get().packWasDeleted;
 			removePack(p);
-		} else if (e.getMessage() != null
-				&& e.getMessage().toLowerCase().contains(STALE_FILE_HANDLE_MSG)) {
+		} else if (FileUtils.isStaleFileHandle(e)) {
 			warnTmpl = JGitText.get().packHandleIsStale;
 			removePack(p);
 		}
@@ -602,7 +599,7 @@
 		}
 
 		final File dst = fileFor(id);
-		if (fs.exists(dst)) {
+		if (dst.exists()) {
 			// We want to be extra careful and avoid replacing an object
 			// that already exists. We can't be sure renameTo() would
 			// fail on all platforms if dst exists, so we check first.
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackBitmapIndexBuilder.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackBitmapIndexBuilder.java
index 05cd100..93f8918 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackBitmapIndexBuilder.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackBitmapIndexBuilder.java
@@ -44,7 +44,7 @@
 package org.eclipse.jgit.internal.storage.file;
 
 import java.text.MessageFormat;
-import java.util.Arrays;
+import java.util.Collections;
 import java.util.Comparator;
 import java.util.Iterator;
 import java.util.List;
@@ -73,7 +73,7 @@
 	private final EWAHCompressedBitmap trees;
 	private final EWAHCompressedBitmap blobs;
 	private final EWAHCompressedBitmap tags;
-	private final ObjectToPack[] byOffset;
+	private final BlockList<PositionEntry> byOffset;
 	private final BlockList<StoredBitmap>
 			byAddOrder = new BlockList<StoredBitmap>();
 	private final ObjectIdOwnerMap<PositionEntry>
@@ -83,20 +83,25 @@
 	 * Creates a PackBitmapIndex used for building the contents of an index
 	 * file.
 	 *
-	 * @param byName
-	 *            objects sorted by name.
+	 * @param objects
+	 *            objects sorted by name. The list must be initially sorted by
+	 *            ObjectId (name); it will be resorted in place.
 	 */
-	public PackBitmapIndexBuilder(List<ObjectToPack> byName) {
+	public PackBitmapIndexBuilder(List<ObjectToPack> objects) {
 		super(new ObjectIdOwnerMap<StoredBitmap>());
-		byOffset = sortByOffset(byName);
+		byOffset = new BlockList<>(objects.size());
+		sortByOffsetAndIndex(byOffset, positionEntries, objects);
 
-		int sizeInWords = Math.max(byOffset.length / 64, 4);
+		// 64 objects fit in a single long word (64 bits).
+		// On average a repository is 30% commits, 30% trees, 30% blobs.
+		// Initialize bitmap capacity for worst case to minimize growing.
+		int sizeInWords = Math.max(4, byOffset.size() / 64 / 3);
 		commits = new EWAHCompressedBitmap(sizeInWords);
 		trees = new EWAHCompressedBitmap(sizeInWords);
 		blobs = new EWAHCompressedBitmap(sizeInWords);
 		tags = new EWAHCompressedBitmap(sizeInWords);
-		for (int i = 0; i < byOffset.length; i++) {
-			int type = byOffset[i].getType();
+		for (int i = 0; i < objects.size(); i++) {
+			int type = objects.get(i).getType();
 			switch (type) {
 			case Constants.OBJ_COMMIT:
 				commits.set(i);
@@ -115,22 +120,39 @@
 						JGitText.get().badObjectType, String.valueOf(type)));
 			}
 		}
+		commits.trim();
+		trees.trim();
+		blobs.trim();
+		tags.trim();
 	}
 
-	private ObjectToPack[] sortByOffset(List<ObjectToPack> entries) {
-		ObjectToPack[] result = new ObjectToPack[entries.size()];
-		for (int i = 0; i < result.length; i++) {
-			result[i] = entries.get(i);
-			positionEntries.add(new PositionEntry(result[i], i));
+	private static void sortByOffsetAndIndex(BlockList<PositionEntry> byOffset,
+			ObjectIdOwnerMap<PositionEntry> positionEntries,
+			List<ObjectToPack> entries) {
+		for (int i = 0; i < entries.size(); i++) {
+			positionEntries.add(new PositionEntry(entries.get(i), i));
 		}
-		Arrays.sort(result, new Comparator<ObjectToPack>() {
+		Collections.sort(entries, new Comparator<ObjectToPack>() {
 			public int compare(ObjectToPack a, ObjectToPack b) {
 				return Long.signum(a.getOffset() - b.getOffset());
 			}
 		});
-		for (int i = 0; i < result.length; i++)
-			positionEntries.get(result[i]).offsetPosition = i;
-		return result;
+		for (int i = 0; i < entries.size(); i++) {
+			PositionEntry e = positionEntries.get(entries.get(i));
+			e.offsetPosition = i;
+			byOffset.add(e);
+		}
+	}
+
+	/** @return set of objects included in the pack. */
+	public ObjectIdOwnerMap<ObjectIdOwnerMap.Entry> getObjectSet() {
+		ObjectIdOwnerMap<ObjectIdOwnerMap.Entry> r = new ObjectIdOwnerMap<>();
+		for (PositionEntry e : byOffset) {
+			r.add(new ObjectIdOwnerMap.Entry(e) {
+				// A new entry that copies the ObjectId
+			});
+		}
+		return r;
 	}
 
 	/**
@@ -168,6 +190,7 @@
 	 */
 	public void addBitmap(
 			AnyObjectId objectId, EWAHCompressedBitmap bitmap, int flags) {
+		bitmap.trim();
 		StoredBitmap result = new StoredBitmap(objectId, bitmap, null, flags);
 		getBitmaps().add(result);
 		byAddOrder.add(result);
@@ -199,7 +222,7 @@
 
 	@Override
 	public ObjectId getObject(int position) throws IllegalArgumentException {
-		ObjectId objectId = byOffset[position];
+		ObjectId objectId = byOffset.get(position);
 		if (objectId == null)
 			throw new IllegalArgumentException();
 		return objectId;
@@ -243,7 +266,7 @@
 
 	@Override
 	public int getObjectCount() {
-		return byOffset.length;
+		return byOffset.size();
 	}
 
 	/** @return an iterator over the xor compressed entries. */
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackBitmapIndexV1.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackBitmapIndexV1.java
index 6d944fd..a38a26d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackBitmapIndexV1.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackBitmapIndexV1.java
@@ -60,8 +60,7 @@
 import org.eclipse.jgit.util.NB;
 
 /**
- * Support for the pack bitmap index v1 format, which contains experimental
- * support for bitmaps.
+ * Support for the pack bitmap index v1 format.
  *
  * @see PackBitmapIndex
  */
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFile.java
index b29966e..589a811 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFile.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/PackFile.java
@@ -366,7 +366,6 @@
 		}
 	}
 
-	@SuppressWarnings("null")
 	private void copyAsIs2(PackOutputStream out, LocalObjectToPack src,
 			boolean validate, WindowCursor curs) throws IOException,
 			StoredObjectRepresentationNotAvailableException {
@@ -393,22 +392,26 @@
 				c = buf[headerCnt++] & 0xff;
 			} while ((c & 128) != 0);
 			if (validate) {
+				assert(crc1 != null && crc2 != null);
 				crc1.update(buf, 0, headerCnt);
 				crc2.update(buf, 0, headerCnt);
 			}
 		} else if (typeCode == Constants.OBJ_REF_DELTA) {
 			if (validate) {
+				assert(crc1 != null && crc2 != null);
 				crc1.update(buf, 0, headerCnt);
 				crc2.update(buf, 0, headerCnt);
 			}
 
 			readFully(src.offset + headerCnt, buf, 0, 20, curs);
 			if (validate) {
+				assert(crc1 != null && crc2 != null);
 				crc1.update(buf, 0, 20);
 				crc2.update(buf, 0, 20);
 			}
 			headerCnt += 20;
 		} else if (validate) {
+			assert(crc1 != null && crc2 != null);
 			crc1.update(buf, 0, headerCnt);
 			crc2.update(buf, 0, headerCnt);
 		}
@@ -425,6 +428,7 @@
 			quickCopy = curs.quickCopy(this, dataOffset, dataLength);
 
 			if (validate && idx().hasCRC32Support()) {
+				assert(crc1 != null);
 				// Index has the CRC32 code cached, validate the object.
 				//
 				expectedCRC = idx().findCRC32(src);
@@ -457,6 +461,7 @@
 				if (quickCopy != null) {
 					quickCopy.check(inf, tmp, dataOffset, (int) dataLength);
 				} else {
+					assert(crc1 != null);
 					long pos = dataOffset;
 					long cnt = dataLength;
 					while (cnt > 0) {
@@ -476,6 +481,7 @@
 							JGitText.get().shortCompressedStreamAt,
 							Long.valueOf(src.offset)));
 				}
+				assert(crc1 != null);
 				expectedCRC = crc1.getValue();
 			} else {
 				expectedCRC = -1;
@@ -535,16 +541,21 @@
 			while (cnt > 0) {
 				final int n = (int) Math.min(cnt, buf.length);
 				readFully(pos, buf, 0, n, curs);
-				if (validate)
+				if (validate) {
+					assert(crc2 != null);
 					crc2.update(buf, 0, n);
+				}
 				out.write(buf, 0, n);
 				pos += n;
 				cnt -= n;
 			}
-			if (validate && crc2.getValue() != expectedCRC) {
-				throw new CorruptObjectException(MessageFormat.format(
-						JGitText.get().objectAtHasBadZlibStream,
-						Long.valueOf(src.offset), getPackFile()));
+			if (validate) {
+				assert(crc2 != null);
+				if (crc2.getValue() != expectedCRC) {
+					throw new CorruptObjectException(MessageFormat.format(
+							JGitText.get().objectAtHasBadZlibStream,
+							Long.valueOf(src.offset), getPackFile()));
+				}
 			}
 		}
 	}
@@ -712,7 +723,6 @@
 					, getPackFile()));
 	}
 
-	@SuppressWarnings("null")
 	ObjectLoader load(final WindowCursor curs, long pos)
 			throws IOException, LargeObjectException {
 		try {
@@ -811,6 +821,7 @@
 			if (data == null)
 				throw new IOException(JGitText.get().inMemoryBufferLimitExceeded);
 
+			assert(delta != null);
 			do {
 				// Cache only the base immediately before desired object.
 				if (cached)
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 6d6d0ca..bb5b044 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
@@ -98,6 +98,8 @@
 import org.eclipse.jgit.util.RawParseUtils;
 import org.eclipse.jgit.util.RefList;
 import org.eclipse.jgit.util.RefMap;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * Traditional file system based {@link RefDatabase}.
@@ -115,6 +117,9 @@
  * overall size of a Git repository on disk.
  */
 public class RefDirectory extends RefDatabase {
+	private final static Logger LOG = LoggerFactory
+			.getLogger(RefDirectory.class);
+
 	/** Magic string denoting the start of a symbolic reference file. */
 	public static final String SYMREF = "ref: "; //$NON-NLS-1$
 
@@ -257,6 +262,30 @@
 	}
 
 	@Override
+	public Ref exactRef(String name) throws IOException {
+		RefList<Ref> packed = getPackedRefs();
+		Ref ref;
+		try {
+			ref = readRef(name, packed);
+			if (ref != null) {
+				ref = resolve(ref, 0, null, null, packed);
+			}
+		} catch (IOException e) {
+			if (name.contains("/") //$NON-NLS-1$
+					|| !(e.getCause() instanceof InvalidObjectIdException)) {
+				throw e;
+			}
+
+			// While looking for a ref outside of refs/ (e.g., 'config'), we
+			// found a non-ref file (e.g., a config file) instead.  Treat this
+			// as a ref-not-found condition.
+			ref = null;
+		}
+		fireRefsChanged();
+		return ref;
+	}
+
+	@Override
 	public Ref getRef(final String needle) throws IOException {
 		final RefList<Ref> packed = getPackedRefs();
 		Ref ref = null;
@@ -746,22 +775,37 @@
 	}
 
 	private PackedRefList readPackedRefs() throws IOException {
-		final FileSnapshot snapshot = FileSnapshot.save(packedRefsFile);
-		final BufferedReader br;
-		final MessageDigest digest = Constants.newMessageDigest();
-		try {
-			br = new BufferedReader(new InputStreamReader(
-					new DigestInputStream(new FileInputStream(packedRefsFile),
-							digest), CHARSET));
-		} catch (FileNotFoundException noPackedRefs) {
-			// Ignore it and leave the new list empty.
-			return PackedRefList.NO_PACKED_REFS;
-		}
-		try {
-			return new PackedRefList(parsePackedRefs(br), snapshot,
-					ObjectId.fromRaw(digest.digest()));
-		} finally {
-			br.close();
+		int maxStaleRetries = 5;
+		int retries = 0;
+		while (true) {
+			final FileSnapshot snapshot = FileSnapshot.save(packedRefsFile);
+			final BufferedReader br;
+			final MessageDigest digest = Constants.newMessageDigest();
+			try {
+				br = new BufferedReader(new InputStreamReader(
+						new DigestInputStream(new FileInputStream(packedRefsFile),
+								digest), CHARSET));
+			} catch (FileNotFoundException noPackedRefs) {
+				// Ignore it and leave the new list empty.
+				return PackedRefList.NO_PACKED_REFS;
+			}
+			try {
+				return new PackedRefList(parsePackedRefs(br), snapshot,
+						ObjectId.fromRaw(digest.digest()));
+			} catch (IOException e) {
+				if (FileUtils.isStaleFileHandle(e) && retries < maxStaleRetries) {
+					if (LOG.isDebugEnabled()) {
+						LOG.debug(MessageFormat.format(
+								JGitText.get().packedRefsHandleIsStale,
+								Integer.valueOf(retries)), e);
+					}
+					retries++;
+					continue;
+				}
+				throw e;
+			} finally {
+				br.close();
+			}
 		}
 	}
 
@@ -881,7 +925,6 @@
 		return n;
 	}
 
-	@SuppressWarnings("null")
 	private LooseRef scanRef(LooseRef ref, String name) throws IOException {
 		final File path = fileFor(name);
 		FileSnapshot currentSnapshot = null;
@@ -920,6 +963,7 @@
 			final String target = RawParseUtils.decode(buf, 5, n);
 			if (ref != null && ref.isSymbolic()
 					&& ref.getTarget().getName().equals(target)) {
+				assert(currentSnapshot != null);
 				currentSnapshot.setClean(otherSnapshot);
 				return ref;
 			}
@@ -934,6 +978,7 @@
 			id = ObjectId.fromString(buf, 0);
 			if (ref != null && !ref.isSymbolic()
 					&& ref.getTarget().getObjectId().equals(id)) {
+				assert(currentSnapshot != null);
 				currentSnapshot.setClean(otherSnapshot);
 				return ref;
 			}
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 adc6bf1..683d1cd 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
@@ -114,6 +114,9 @@
 import org.eclipse.jgit.revwalk.RevTag;
 import org.eclipse.jgit.revwalk.RevTree;
 import org.eclipse.jgit.storage.pack.PackConfig;
+import org.eclipse.jgit.storage.pack.PackStatistics;
+import org.eclipse.jgit.transport.ObjectCountCallback;
+import org.eclipse.jgit.transport.WriteAbortedException;
 import org.eclipse.jgit.util.BlockList;
 import org.eclipse.jgit.util.TemporaryBuffer;
 
@@ -133,12 +136,14 @@
  * order of objects in pack</li>
  * </ul>
  * <p>
- * Typical usage consists of creating instance intended for some pack,
- * configuring options, preparing the list of objects by calling
- * {@link #preparePack(Iterator)} or
- * {@link #preparePack(ProgressMonitor, Set, Set)}, and finally producing the
- * stream with
- * {@link #writePack(ProgressMonitor, ProgressMonitor, OutputStream)}.
+ * Typical usage consists of creating an instance, configuring options,
+ * preparing the list of objects by calling {@link #preparePack(Iterator)} or
+ * {@link #preparePack(ProgressMonitor, Set, Set)}, and streaming with
+ * {@link #writePack(ProgressMonitor, ProgressMonitor, OutputStream)}. If the
+ * pack is being stored as a file the matching index can be written out after
+ * writing the pack by {@link #writeIndex(OutputStream)}. An optional bitmap
+ * index can be made by calling {@link #prepareBitmapIndex(ProgressMonitor)}
+ * followed by {@link #writeBitmapIndex(OutputStream)}.
  * </p>
  * <p>
  * Class provide set of configurable options and {@link ProgressMonitor}
@@ -147,9 +152,10 @@
  * relies only on deltas and objects reuse.
  * </p>
  * <p>
- * This class is not thread safe, it is intended to be used in one thread, with
- * one instance per created pack. Subsequent calls to writePack result in
- * undefined behavior.
+ * This class is not thread safe. It is intended to be used in one thread as a
+ * single pass to produce one pack. Invoking methods multiple times or out of
+ * order is not supported as internal data structures are destroyed during
+ * certain phases to save memory when packing large repositories.
  * </p>
  */
 public class PackWriter implements AutoCloseable {
@@ -212,7 +218,7 @@
 	}
 
 	@SuppressWarnings("unchecked")
-	private final BlockList<ObjectToPack> objectsLists[] = new BlockList[OBJ_TAG + 1];
+	private BlockList<ObjectToPack> objectsLists[] = new BlockList[OBJ_TAG + 1];
 	{
 		objectsLists[OBJ_COMMIT] = new BlockList<ObjectToPack>();
 		objectsLists[OBJ_TREE] = new BlockList<ObjectToPack>();
@@ -220,7 +226,7 @@
 		objectsLists[OBJ_TAG] = new BlockList<ObjectToPack>();
 	}
 
-	private final ObjectIdOwnerMap<ObjectToPack> objectsMap = new ObjectIdOwnerMap<ObjectToPack>();
+	private ObjectIdOwnerMap<ObjectToPack> objectsMap = new ObjectIdOwnerMap<ObjectToPack>();
 
 	// edge objects for thin packs
 	private List<ObjectToPack> edgeObjects = new BlockList<ObjectToPack>();
@@ -245,13 +251,13 @@
 
 	private final PackConfig config;
 
-	private final Statistics stats;
+	private final PackStatistics.Accumulator stats;
 
 	private final MutableState state;
 
 	private final WeakReference<PackWriter> selfRef;
 
-	private Statistics.ObjectType typeStats;
+	private PackStatistics.ObjectType.Accumulator typeStats;
 
 	private List<ObjectToPack> sortedByName;
 
@@ -289,6 +295,8 @@
 
 	private CRC32 crc32;
 
+	private ObjectCountCallback callback;
+
 	/**
 	 * Create writer for specified repository.
 	 * <p>
@@ -352,13 +360,42 @@
 		deltaBaseAsOffset = config.isDeltaBaseAsOffset();
 		reuseDeltas = config.isReuseDeltas();
 		reuseValidate = true; // be paranoid by default
-		stats = new Statistics();
+		stats = new PackStatistics.Accumulator();
 		state = new MutableState();
 		selfRef = new WeakReference<PackWriter>(this);
 		instances.put(selfRef, Boolean.TRUE);
 	}
 
 	/**
+	 * Set the {@code ObjectCountCallback}.
+	 * <p>
+	 * It should be set before calling
+	 * {@link #writePack(ProgressMonitor, ProgressMonitor, OutputStream)}.
+	 *
+	 * @param callback
+	 *            the callback to set
+	 *
+	 * @return this object for chaining.
+	 * @since 4.1
+	 */
+	public PackWriter setObjectCountCallback(ObjectCountCallback callback) {
+		this.callback = callback;
+		return this;
+	}
+
+	/**
+	 * Records the set of shallow commits in the client.
+	 *
+	 * @param clientShallowCommits
+	 *            the shallow commits in the client
+	 * @since 4.1
+	 */
+	public void setClientShallowCommits(Set<ObjectId> clientShallowCommits) {
+		stats.clientShallowCommits = Collections
+				.unmodifiableSet(new HashSet<ObjectId>(clientShallowCommits));
+	}
+
+	/**
 	 * Check whether writer can store delta base as an offset (new style
 	 * reducing pack size) or should store it as an object id (legacy style,
 	 * compatible with old readers).
@@ -571,12 +608,12 @@
 
 	/**
 	 * Returns the object ids in the pack file that was created by this writer.
-	 *
+	 * <p>
 	 * This method can only be invoked after
 	 * {@link #writePack(ProgressMonitor, ProgressMonitor, OutputStream)} has
 	 * been invoked and completed successfully.
 	 *
-	 * @return number of objects in pack.
+	 * @return set of objects in pack.
 	 * @throws IOException
 	 *             a cached pack cannot supply its object ids.
 	 */
@@ -586,17 +623,20 @@
 			throw new IOException(
 					JGitText.get().cachedPacksPreventsListingObjects);
 
-		ObjectIdOwnerMap<ObjectIdOwnerMap.Entry> objs = new ObjectIdOwnerMap<
-				ObjectIdOwnerMap.Entry>();
+		if (writeBitmaps != null) {
+			return writeBitmaps.getObjectSet();
+		}
+
+		ObjectIdOwnerMap<ObjectIdOwnerMap.Entry> r = new ObjectIdOwnerMap<>();
 		for (BlockList<ObjectToPack> objList : objectsLists) {
 			if (objList != null) {
 				for (ObjectToPack otp : objList)
-					objs.add(new ObjectIdOwnerMap.Entry(otp) {
+					r.add(new ObjectIdOwnerMap.Entry(otp) {
 						// A new entry that copies the ObjectId
 					});
 			}
 		}
-		return objs;
+		return r;
 	}
 
 	/**
@@ -784,10 +824,11 @@
 	/**
 	 * Create an index file to match the pack file just written.
 	 * <p>
-	 * This method can only be invoked after
-	 * {@link #writePack(ProgressMonitor, ProgressMonitor, OutputStream)} has
-	 * been invoked and completed successfully. Writing a corresponding index is
-	 * an optional feature that not all pack users may require.
+	 * Called after
+	 * {@link #writePack(ProgressMonitor, ProgressMonitor, OutputStream)}.
+	 * <p>
+	 * Writing an index is only required for local pack storage. Packs sent on
+	 * the network do not need to create an index.
 	 *
 	 * @param indexStream
 	 *            output for the index data. Caller is responsible for closing
@@ -809,10 +850,7 @@
 	/**
 	 * Create a bitmap index file to match the pack file just written.
 	 * <p>
-	 * This method can only be invoked after
-	 * {@link #prepareBitmapIndex(ProgressMonitor)} has been invoked and
-	 * completed successfully. Writing a corresponding bitmap index is an
-	 * optional feature that not all pack users may require.
+	 * Called after {@link #prepareBitmapIndex(ProgressMonitor)}.
 	 *
 	 * @param bitmapIndexStream
 	 *            output for the bitmap index data. Caller is responsible for
@@ -886,14 +924,13 @@
 	/**
 	 * Write the prepared pack to the supplied stream.
 	 * <p>
-	 * At first, this method collects and sorts objects to pack, then deltas
-	 * search is performed if set up accordingly, finally pack stream is
-	 * written.
-	 * </p>
+	 * Called after {@link #preparePack(ProgressMonitor, ObjectWalk, Set, Set)}
+	 * or {@link #preparePack(ProgressMonitor, Set, Set)}.
+	 * <p>
+	 * Performs delta search if enabled and writes the pack stream.
 	 * <p>
 	 * All reused objects data checksum (Adler32/CRC32) is computed and
 	 * validated against existing checksum.
-	 * </p>
 	 *
 	 * @param compressMonitor
 	 *            progress monitor to report object compression work.
@@ -906,6 +943,9 @@
 	 *             an error occurred reading a local object's data to include in
 	 *             the pack, or writing compressed object data to the output
 	 *             stream.
+	 * @throws WriteAbortedException
+	 *             the write operation is aborted by {@link ObjectCountCallback}
+	 *             .
 	 */
 	public void writePack(ProgressMonitor compressMonitor,
 			ProgressMonitor writeMonitor, OutputStream packStream)
@@ -947,6 +987,8 @@
 
 		long objCnt = getObjectCount();
 		stats.totalObjects = objCnt;
+		if (callback != null)
+			callback.setObjectCount(objCnt);
 		beginPhase(PackingPhase.WRITING, writeMonitor, objCnt);
 		long writeStart = System.currentTimeMillis();
 		try {
@@ -955,7 +997,7 @@
 
 			writeObjects(out);
 			if (!edgeObjects.isEmpty() || !cachedPacks.isEmpty()) {
-				for (Statistics.ObjectType typeStat : stats.objectTypes) {
+				for (PackStatistics.ObjectType.Accumulator typeStat : stats.objectTypes) {
 					if (typeStat == null)
 						continue;
 					stats.thinPackBytes += typeStat.bytes;
@@ -976,7 +1018,7 @@
 			stats.timeWriting = System.currentTimeMillis() - writeStart;
 			stats.depth = depth;
 
-			for (Statistics.ObjectType typeStat : stats.objectTypes) {
+			for (PackStatistics.ObjectType.Accumulator typeStat : stats.objectTypes) {
 				if (typeStat == null)
 					continue;
 				typeStat.cntDeltas += typeStat.reusedDeltas;
@@ -993,11 +1035,11 @@
 
 	/**
 	 * @return description of what this PackWriter did in order to create the
-	 *         final pack stream. The object is only available to callers after
-	 *         {@link #writePack(ProgressMonitor, ProgressMonitor, OutputStream)}
+	 *         final pack stream. This should only be invoked after the calls to
+	 *         create the pack/index/bitmap have completed.
 	 */
-	public Statistics getStatistics() {
-		return stats;
+	public PackStatistics getStatistics() {
+		return new PackStatistics(stats);
 	}
 
 	/** @return snapshot of the current state of this PackWriter. */
@@ -1676,6 +1718,7 @@
 		final int maxBases = config.getDeltaSearchWindowSize();
 		Set<RevTree> baseTrees = new HashSet<RevTree>();
 		BlockList<RevCommit> commits = new BlockList<RevCommit>();
+		Set<ObjectId> roots = new HashSet<>();
 		RevCommit c;
 		while ((c = walker.next()) != null) {
 			if (exclude(c))
@@ -1687,8 +1730,12 @@
 			}
 
 			commits.add(c);
+			if (c.getParentCount() == 0) {
+				roots.add(c.copy());
+			}
 			countingMonitor.update(1);
 		}
+		stats.rootCommits = Collections.unmodifiableSet(roots);
 
 		if (shallowPack) {
 			for (RevCommit cmit : commits) {
@@ -1764,6 +1811,7 @@
 			countingMonitor.update((int) pack.getObjectCount());
 		endPhase(countingMonitor);
 		stats.timeCounting = System.currentTimeMillis() - countingStart;
+		stats.bitmapIndexMisses = -1;
 	}
 
 	private void findObjectsToPackUsingBitmaps(
@@ -1932,14 +1980,17 @@
 	}
 
 	/**
-	 * Prepares the bitmaps to be written to the pack index. Bitmaps can be used
-	 * to speed up fetches and clones by storing the entire object graph at
-	 * selected commits.
-	 *
-	 * This method can only be invoked after
-	 * {@link #writePack(ProgressMonitor, ProgressMonitor, OutputStream)} has
-	 * been invoked and completed successfully. Writing a corresponding bitmap
-	 * index is an optional feature that not all pack users may require.
+	 * Prepares the bitmaps to be written to the bitmap index file.
+	 * <p>
+	 * Bitmaps can be used to speed up fetches and clones by storing the entire
+	 * object graph at selected commits. Writing a bitmap index is an optional
+	 * feature that not all pack users may require.
+	 * <p>
+	 * Called after {@link #writeIndex(OutputStream)}.
+	 * <p>
+	 * To reduce memory internal state is cleared during this method, rendering
+	 * the PackWriter instance useless for anything further than a call to write
+	 * out the new bitmaps with {@link #writeBitmapIndex(OutputStream)}.
 	 *
 	 * @param pm
 	 *            progress monitor to report bitmap building work.
@@ -1955,11 +2006,17 @@
 		if (pm == null)
 			pm = NullProgressMonitor.INSTANCE;
 
-		writeBitmaps = new PackBitmapIndexBuilder(sortByName());
+		int numCommits = objectsLists[OBJ_COMMIT].size();
+		List<ObjectToPack> byName = sortByName();
+		sortedByName = null;
+		objectsLists = null;
+		objectsMap = null;
+		writeBitmaps = new PackBitmapIndexBuilder(byName);
+		byName = null;
+
 		PackWriterBitmapPreparer bitmapPreparer = new PackWriterBitmapPreparer(
 				reader, writeBitmaps, pm, stats.interestingObjects);
 
-		int numCommits = objectsLists[OBJ_COMMIT].size();
 		Collection<PackWriterBitmapPreparer.BitmapCommit> selectedCommits =
 				bitmapPreparer.doCommitSelection(numCommits);
 
@@ -2001,28 +2058,36 @@
 		return true;
 	}
 
-	/** Summary of how PackWriter created the pack. */
+	/**
+	 * Summary of how PackWriter created the pack.
+	 *
+	 * @deprecated Use {@link PackStatistics} instead.
+	 */
+	@Deprecated
 	public static class Statistics {
 		/** Statistics about a single class of object. */
 		public static class ObjectType {
-			long cntObjects;
+			// All requests are forwarded to this object.
+			private PackStatistics.ObjectType objectType;
 
-			long cntDeltas;
-
-			long reusedObjects;
-
-			long reusedDeltas;
-
-			long bytes;
-
-			long deltaBytes;
+			/**
+			 * Wraps an
+			 * {@link org.eclipse.jgit.storage.pack.PackStatistics.ObjectType}
+			 * instance to maintain backwards compatibility with existing API.
+			 *
+			 * @param type
+			 *            the wrapped instance
+			 */
+			public ObjectType(PackStatistics.ObjectType type) {
+				objectType = type;
+			}
 
 			/**
 			 * @return total number of objects output. This total includes the
 			 *         value of {@link #getDeltas()}.
 			 */
 			public long getObjects() {
-				return cntObjects;
+				return objectType.getObjects();
 			}
 
 			/**
@@ -2030,7 +2095,7 @@
 			 *         actual number of deltas if a cached pack was reused.
 			 */
 			public long getDeltas() {
-				return cntDeltas;
+				return objectType.getDeltas();
 			}
 
 			/**
@@ -2039,7 +2104,7 @@
 			 *         {@link #getReusedDeltas()}.
 			 */
 			public long getReusedObjects() {
-				return reusedObjects;
+				return objectType.getReusedObjects();
 			}
 
 			/**
@@ -2050,7 +2115,7 @@
 			 *         was reused.
 			 */
 			public long getReusedDeltas() {
-				return reusedDeltas;
+				return objectType.getReusedDeltas();
 			}
 
 			/**
@@ -2059,7 +2124,7 @@
 			 *         also includes all of {@link #getDeltaBytes()}.
 			 */
 			public long getBytes() {
-				return bytes;
+				return objectType.getBytes();
 			}
 
 			/**
@@ -2067,54 +2132,22 @@
 			 *         object headers for the delta objects.
 			 */
 			public long getDeltaBytes() {
-				return deltaBytes;
+				return objectType.getDeltaBytes();
 			}
 		}
 
-		Set<ObjectId> interestingObjects;
+		// All requests are forwarded to this object.
+		private PackStatistics statistics;
 
-		Set<ObjectId> uninterestingObjects;
-
-		Collection<CachedPack> reusedPacks;
-
-		int depth;
-
-		int deltaSearchNonEdgeObjects;
-
-		int deltasFound;
-
-		long totalObjects;
-
-		long bitmapIndexMisses;
-
-		long totalDeltas;
-
-		long reusedObjects;
-
-		long reusedDeltas;
-
-		long totalBytes;
-
-		long thinPackBytes;
-
-		long timeCounting;
-
-		long timeSearchingForReuse;
-
-		long timeSearchingForSizes;
-
-		long timeCompressing;
-
-		long timeWriting;
-
-		ObjectType[] objectTypes;
-
-		{
-			objectTypes = new ObjectType[5];
-			objectTypes[OBJ_COMMIT] = new ObjectType();
-			objectTypes[OBJ_TREE] = new ObjectType();
-			objectTypes[OBJ_BLOB] = new ObjectType();
-			objectTypes[OBJ_TAG] = new ObjectType();
+		/**
+		 * Wraps a {@link PackStatistics} object to maintain backwards
+		 * compatibility with existing API.
+		 *
+		 * @param stats
+		 *            the wrapped PackStatitics object
+		 */
+		public Statistics(PackStatistics stats) {
+			statistics = stats;
 		}
 
 		/**
@@ -2123,7 +2156,7 @@
 		 *         test.
 		 */
 		public Set<ObjectId> getInterestingObjects() {
-			return interestingObjects;
+			return statistics.getInterestingObjects();
 		}
 
 		/**
@@ -2132,7 +2165,7 @@
 		 *         has these objects.
 		 */
 		public Set<ObjectId> getUninterestingObjects() {
-			return uninterestingObjects;
+			return statistics.getUninterestingObjects();
 		}
 
 		/**
@@ -2140,7 +2173,7 @@
 		 *         in the output, if any were selected for reuse.
 		 */
 		public Collection<CachedPack> getReusedPacks() {
-			return reusedPacks;
+			return statistics.getReusedPacks();
 		}
 
 		/**
@@ -2148,7 +2181,7 @@
 		 *         delta search process in order to find a potential delta base.
 		 */
 		public int getDeltaSearchNonEdgeObjects() {
-			return deltaSearchNonEdgeObjects;
+			return statistics.getDeltaSearchNonEdgeObjects();
 		}
 
 		/**
@@ -2157,7 +2190,7 @@
 		 *         {@link #getDeltaSearchNonEdgeObjects()}.
 		 */
 		public int getDeltasFound() {
-			return deltasFound;
+			return statistics.getDeltasFound();
 		}
 
 		/**
@@ -2165,17 +2198,18 @@
 		 *         of {@link #getTotalDeltas()}.
 		 */
 		public long getTotalObjects() {
-			return totalObjects;
+			return statistics.getTotalObjects();
 		}
 
 		/**
 		 * @return the count of objects that needed to be discovered through an
 		 *         object walk because they were not found in bitmap indices.
+		 *         Returns -1 if no bitmap indices were found.
 		 *
 		 * @since 4.0
 		 */
 		public long getBitmapIndexMisses() {
-			return bitmapIndexMisses;
+			return statistics.getBitmapIndexMisses();
 		}
 
 		/**
@@ -2183,7 +2217,7 @@
 		 *         actual number of deltas if a cached pack was reused.
 		 */
 		public long getTotalDeltas() {
-			return totalDeltas;
+			return statistics.getTotalDeltas();
 		}
 
 		/**
@@ -2191,7 +2225,7 @@
 		 *         the output. This count includes {@link #getReusedDeltas()}.
 		 */
 		public long getReusedObjects() {
-			return reusedObjects;
+			return statistics.getReusedObjects();
 		}
 
 		/**
@@ -2201,7 +2235,7 @@
 		 *         actual number of reused deltas if a cached pack was reused.
 		 */
 		public long getReusedDeltas() {
-			return reusedDeltas;
+			return statistics.getReusedDeltas();
 		}
 
 		/**
@@ -2209,7 +2243,7 @@
 		 *         header, trailer, thin pack, and reused cached pack(s).
 		 */
 		public long getTotalBytes() {
-			return totalBytes;
+			return statistics.getTotalBytes();
 		}
 
 		/**
@@ -2221,7 +2255,7 @@
 		 *         pack header or trailer.
 		 */
 		public long getThinPackBytes() {
-			return thinPackBytes;
+			return statistics.getThinPackBytes();
 		}
 
 		/**
@@ -2230,17 +2264,17 @@
 		 * @return information about this type of object in the pack.
 		 */
 		public ObjectType byObjectType(int typeCode) {
-			return objectTypes[typeCode];
+			return new ObjectType(statistics.byObjectType(typeCode));
 		}
 
 		/** @return true if the resulting pack file was a shallow pack. */
 		public boolean isShallow() {
-			return depth > 0;
+			return statistics.isShallow();
 		}
 
 		/** @return depth (in commits) the pack includes if shallow. */
 		public int getDepth() {
-			return depth;
+			return statistics.getDepth();
 		}
 
 		/**
@@ -2249,7 +2283,7 @@
 		 *         that occur when a cached pack is selected for reuse.
 		 */
 		public long getTimeCounting() {
-			return timeCounting;
+			return statistics.getTimeCounting();
 		}
 
 		/**
@@ -2258,7 +2292,7 @@
 		 *         can be assumed to already have.
 		 */
 		public long getTimeSearchingForReuse() {
-			return timeSearchingForReuse;
+			return statistics.getTimeSearchingForReuse();
 		}
 
 		/**
@@ -2268,7 +2302,7 @@
 		 *         together and improve delta compression ratios.
 		 */
 		public long getTimeSearchingForSizes() {
-			return timeSearchingForSizes;
+			return statistics.getTimeSearchingForSizes();
 		}
 
 		/**
@@ -2278,7 +2312,7 @@
 		 *         delta compression.
 		 */
 		public long getTimeCompressing() {
-			return timeCompressing;
+			return statistics.getTimeCompressing();
 		}
 
 		/**
@@ -2288,16 +2322,12 @@
 		 *         value.
 		 */
 		public long getTimeWriting() {
-			return timeWriting;
+			return statistics.getTimeWriting();
 		}
 
 		/** @return total time spent processing this pack. */
 		public long getTimeTotal() {
-			return timeCounting
-				+ timeSearchingForReuse
-				+ timeSearchingForSizes
-				+ timeCompressing
-				+ timeWriting;
+			return statistics.getTimeTotal();
 		}
 
 		/**
@@ -2305,14 +2335,12 @@
 		 *         {@code getTotalBytes() / (getTimeWriting() / 1000.0)}.
 		 */
 		public double getTransferRate() {
-			return getTotalBytes() / (getTimeWriting() / 1000.0);
+			return statistics.getTransferRate();
 		}
 
 		/** @return formatted message string for display to clients. */
 		public String getMessage() {
-			return MessageFormat.format(JGitText.get().packWriterStatistics, //
-					Long.valueOf(totalObjects), Long.valueOf(totalDeltas), //
-					Long.valueOf(reusedObjects), Long.valueOf(reusedDeltas));
+			return statistics.getMessage();
 		}
 	}
 
@@ -2345,11 +2373,14 @@
 
 		State snapshot() {
 			long objCnt = 0;
-			objCnt += objectsLists[OBJ_COMMIT].size();
-			objCnt += objectsLists[OBJ_TREE].size();
-			objCnt += objectsLists[OBJ_BLOB].size();
-			objCnt += objectsLists[OBJ_TAG].size();
-			// Exclude CachedPacks.
+			BlockList<ObjectToPack>[] lists = objectsLists;
+			if (lists != null) {
+				objCnt += lists[OBJ_COMMIT].size();
+				objCnt += lists[OBJ_TREE].size();
+				objCnt += lists[OBJ_BLOB].size();
+				objCnt += lists[OBJ_TAG].size();
+				// Exclude CachedPacks.
+			}
 
 			long bytesUsed = OBJECT_TO_PACK_SIZE * objCnt;
 			PackingPhase curr = phase;
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 fca9063..756d4b0 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
@@ -114,6 +114,7 @@
 			IOException {
 		pm.beginTask(JGitText.get().selectingCommits, ProgressMonitor.UNKNOWN);
 		RevWalk rw = new RevWalk(reader);
+		rw.setRetainBody(false);
 		WalkResult result = findPaths(rw, expectedNumCommits);
 		pm.endTask();
 
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 73850b7..d7e9308 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchRefUpdate.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchRefUpdate.java
@@ -59,6 +59,7 @@
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.RefUpdate.Result;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.PushCertificate;
 import org.eclipse.jgit.transport.ReceiveCommand;
 
 /**
@@ -85,6 +86,9 @@
 	/** Should the result value be appended to {@link #refLogMessage}. */
 	private boolean refLogIncludeResult;
 
+	/** Push certificate associated with this update. */
+	private PushCertificate pushCert;
+
 	/**
 	 * Initialize a new batch update.
 	 *
@@ -195,6 +199,33 @@
 		return refLogMessage == null;
 	}
 
+	/**
+	 * Set a push certificate associated with this update.
+	 * <p>
+	 * This usually includes commands to update the refs in this batch, but is not
+	 * required to.
+	 *
+	 * @param cert
+	 *            push certificate, may be null.
+	 * @since 4.1
+	 */
+	public void setPushCertificate(PushCertificate cert) {
+		pushCert = cert;
+	}
+
+	/**
+	 * Set the push certificate associated with this update.
+	 * <p>
+	 * This usually includes commands to update the refs in this batch, but is not
+	 * required to.
+	 *
+	 * @return push certificate, may be null.
+	 * @since 4.1
+	 */
+	protected PushCertificate getPushCertificate() {
+		return pushCert;
+	}
+
 	/** @return commands this update will process. */
 	public List<ReceiveCommand> getCommands() {
 		return Collections.unmodifiableList(commands);
@@ -377,6 +408,7 @@
 			ru.setRefLogIdent(refLogIdent);
 			ru.setRefLogMessage(refLogMessage, refLogIncludeResult);
 		}
+		ru.setPushCertificate(pushCert);
 		switch (cmd.getType()) {
 		case DELETE:
 			if (!ObjectId.zeroId().equals(cmd.getOldId()))
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java
index 22337e8..e48386d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java
@@ -1200,8 +1200,6 @@
 		for (;;) {
 			int c = in.read();
 			if (c < 0) {
-				if (value.length() == 0)
-					throw new ConfigInvalidException(JGitText.get().unexpectedEndOfConfigFile);
 				break;
 			}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java
index 33c65ab..a7a67a8 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java
@@ -618,8 +618,9 @@
 					default:
 						return false;
 					}
+				default:
+					return false;
 				}
-				break;
 			case (byte) 0xef: // http://www.utf8-chartable.de/unicode-utf8-table.pl?start=65024
 				checkTruncatedIgnorableUTF8(raw, ptr, end);
 				// U+FEFF 0xefbbbf ZERO WIDTH NO-BREAK SPACE
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectId.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectId.java
index bdbffee..4edb38c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectId.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectId.java
@@ -45,7 +45,6 @@
 package org.eclipse.jgit.lib;
 
 import org.eclipse.jgit.errors.InvalidObjectIdException;
-import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.util.NB;
 import org.eclipse.jgit.util.RawParseUtils;
 
@@ -53,7 +52,6 @@
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
 import java.io.Serializable;
-import java.text.MessageFormat;
 
 /**
  * A SHA-1 abstraction.
@@ -113,16 +111,16 @@
 	}
 
 	/**
-	 * Compare to object identifier byte sequences for equality.
+	 * Compare two object identifier byte sequences for equality.
 	 *
 	 * @param firstBuffer
 	 *            the first buffer to compare against. Must have at least 20
-	 *            bytes from position ai through the end of the buffer.
+	 *            bytes from position fi through the end of the buffer.
 	 * @param fi
 	 *            first offset within firstBuffer to begin testing.
 	 * @param secondBuffer
-	 *            the second buffer to compare against. Must have at least 2
-	 *            bytes from position bi through the end of the buffer.
+	 *            the second buffer to compare against. Must have at least 20
+	 *            bytes from position si through the end of the buffer.
 	 * @param si
 	 *            first offset within secondBuffer to begin testing.
 	 * @return true if the two identifiers are the same.
@@ -230,9 +228,9 @@
 	 * @return the converted object id.
 	 */
 	public static ObjectId fromString(final String str) {
-		if (str.length() != Constants.OBJECT_ID_STRING_LENGTH)
-			throw new IllegalArgumentException(
-					MessageFormat.format(JGitText.get().invalidId, str));
+		if (str.length() != Constants.OBJECT_ID_STRING_LENGTH) {
+			throw new InvalidObjectIdException(str);
+		}
 		return fromHexString(Constants.encodeASCII(str), 0);
 	}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/PersonIdent.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PersonIdent.java
index e859119..2ecc60c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/PersonIdent.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/PersonIdent.java
@@ -63,6 +63,54 @@
 public class PersonIdent implements Serializable {
 	private static final long serialVersionUID = 1L;
 
+	/**
+	 * @param tzOffset
+	 *            timezone offset as in {@link #getTimeZoneOffset()}.
+	 * @return time zone object for the given offset.
+	 * @since 4.1
+	 */
+	public static TimeZone getTimeZone(int tzOffset) {
+		StringBuilder tzId = new StringBuilder(8);
+		tzId.append("GMT"); //$NON-NLS-1$
+		appendTimezone(tzId, tzOffset);
+		return TimeZone.getTimeZone(tzId.toString());
+	}
+
+	/**
+	 * Format a timezone offset.
+	 *
+	 * @param r
+	 *            string builder to append to.
+	 * @param offset
+	 *            timezone offset as in {@link #getTimeZoneOffset()}.
+	 * @since 4.1
+	 */
+	public static void appendTimezone(StringBuilder r, int offset) {
+		final char sign;
+		final int offsetHours;
+		final int offsetMins;
+
+		if (offset < 0) {
+			sign = '-';
+			offset = -offset;
+		} else {
+			sign = '+';
+		}
+
+		offsetHours = offset / 60;
+		offsetMins = offset % 60;
+
+		r.append(sign);
+		if (offsetHours < 10) {
+			r.append('0');
+		}
+		r.append(offsetHours);
+		if (offsetMins < 10) {
+			r.append('0');
+		}
+		r.append(offsetMins);
+	}
+
 	private final String name;
 
 	private final String emailAddress;
@@ -217,10 +265,7 @@
 	 * @return this person's declared time zone; null if time zone is unknown.
 	 */
 	public TimeZone getTimeZone() {
-		StringBuilder tzId = new StringBuilder(8);
-		tzId.append("GMT"); //$NON-NLS-1$
-		appendTimezone(tzId);
-		return TimeZone.getTimeZone(tzId.toString());
+		return getTimeZone(tzOffset);
 	}
 
 	/**
@@ -261,37 +306,10 @@
 		r.append("> "); //$NON-NLS-1$
 		r.append(when / 1000);
 		r.append(' ');
-		appendTimezone(r);
+		appendTimezone(r, tzOffset);
 		return r.toString();
 	}
 
-	private void appendTimezone(final StringBuilder r) {
-		int offset = tzOffset;
-		final char sign;
-		final int offsetHours;
-		final int offsetMins;
-
-		if (offset < 0) {
-			sign = '-';
-			offset = -offset;
-		} else {
-			sign = '+';
-		}
-
-		offsetHours = offset / 60;
-		offsetMins = offset % 60;
-
-		r.append(sign);
-		if (offsetHours < 10) {
-			r.append('0');
-		}
-		r.append(offsetHours);
-		if (offsetMins < 10) {
-			r.append('0');
-		}
-		r.append(offsetMins);
-	}
-
 	@SuppressWarnings("nls")
 	public String toString() {
 		final StringBuilder r = new StringBuilder();
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java
index 7fea880..b62033c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefDatabase.java
@@ -47,6 +47,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -211,6 +212,9 @@
 	 * Aside from taking advantage of {@link #SEARCH_PATH}, this method may be
 	 * able to more quickly resolve a single reference name than obtaining the
 	 * complete namespace by {@code getRefs(ALL).get(name)}.
+	 * <p>
+	 * To read a specific reference without using @{link #SEARCH_PATH}, see
+	 * {@link #exactRef(String)}.
 	 *
 	 * @param name
 	 *            the name of the reference. May be a short name which must be
@@ -222,6 +226,76 @@
 	public abstract Ref getRef(String name) throws IOException;
 
 	/**
+	 * Read a single reference.
+	 * <p>
+	 * Unlike {@link #getRef}, this method expects an unshortened reference
+	 * name and does not search using the standard {@link #SEARCH_PATH}.
+	 *
+	 * @param name
+	 *             the unabbreviated name of the reference.
+	 * @return the reference (if it exists); else {@code null}.
+	 * @throws IOException
+	 *             the reference space cannot be accessed.
+	 * @since 4.1
+	 */
+	public Ref exactRef(String name) throws IOException {
+		Ref ref = getRef(name);
+		if (ref == null || !name.equals(ref.getName())) {
+			return null;
+		}
+		return ref;
+	}
+
+	/**
+	 * Read the specified references.
+	 * <p>
+	 * This method expects a list of unshortened reference names and returns
+	 * a map from reference names to refs.  Any named references that do not
+	 * exist will not be included in the returned map.
+	 *
+	 * @param refs
+	 *             the unabbreviated names of references to look up.
+	 * @return modifiable map describing any refs that exist among the ref
+	 *         ref names supplied. The map can be an unsorted map.
+	 * @throws IOException
+	 *             the reference space cannot be accessed.
+	 * @since 4.1
+	 */
+	public Map<String, Ref> exactRef(String... refs) throws IOException {
+		Map<String, Ref> result = new HashMap<>(refs.length);
+		for (String name : refs) {
+			Ref ref = exactRef(name);
+			if (ref != null) {
+				result.put(name, ref);
+			}
+		}
+		return result;
+	}
+
+	/**
+	 * Find the first named reference.
+	 * <p>
+	 * This method expects a list of unshortened reference names and returns
+	 * the first that exists.
+	 *
+	 * @param refs
+	 *             the unabbreviated names of references to look up.
+	 * @return the first named reference that exists (if any); else {@code null}.
+	 * @throws IOException
+	 *             the reference space cannot be accessed.
+	 * @since 4.1
+	 */
+	public Ref firstExactRef(String... refs) throws IOException {
+		for (String name : refs) {
+			Ref ref = exactRef(name);
+			if (ref != null) {
+				return ref;
+			}
+		}
+		return null;
+	}
+
+	/**
 	 * Get a section of the reference namespace.
 	 *
 	 * @param prefix
@@ -242,6 +316,7 @@
 	 * The result list includes non-ref items such as MERGE_HEAD and
 	 * FETCH_RESULT cast to be refs. The names of these refs are not returned by
 	 * <code>getRefs(ALL)</code> but are accepted by {@link #getRef(String)}
+	 * and {@link #exactRef(String)}.
 	 *
 	 * @return a list of additional refs
 	 * @throws IOException
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 aeef9f0..4316cd0 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefUpdate.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RefUpdate.java
@@ -52,6 +52,7 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.PushCertificate;
 
 /**
  * Creates, updates or deletes any reference.
@@ -165,6 +166,9 @@
 	/** Result of the update operation. */
 	private Result result = Result.NOT_ATTEMPTED;
 
+	/** Push certificate associated with this update. */
+	private PushCertificate pushCert;
+
 	private final Ref ref;
 
 	/**
@@ -414,6 +418,31 @@
 	}
 
 	/**
+	 * Set a push certificate associated with this update.
+	 * <p>
+	 * This usually includes a command to update this ref, but is not required to.
+	 *
+	 * @param cert
+	 *            push certificate, may be null.
+	 * @since 4.1
+	 */
+	public void setPushCertificate(PushCertificate cert) {
+		pushCert = cert;
+	}
+
+	/**
+	 * Set the push certificate associated with this update.
+	 * <p>
+	 * This usually includes a command to update this ref, but is not required to.
+	 *
+	 * @return push certificate, may be null.
+	 * @since 4.1
+	 */
+	protected PushCertificate getPushCertificate() {
+		return pushCert;
+	}
+
+	/**
 	 * Get the status of this update.
 	 * <p>
 	 * The same value that was previously returned from an update method.
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java
index fc7dca2..d4c72cb 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java
@@ -849,8 +849,9 @@
 	 * Except when HEAD is detached, in which case this method returns the
 	 * current ObjectId in hexadecimal string format.
 	 *
-	 * @return name of current branch (for example {@code refs/heads/master}) or
-	 *         an ObjectId in hex format if the current branch is detached.
+	 * @return name of current branch (for example {@code refs/heads/master}),
+	 *         an ObjectId in hex format if the current branch is detached,
+	 *         or null if the repository is corrupt and has no HEAD reference.
 	 * @throws IOException
 	 */
 	public String getFullBranch() throws IOException {
@@ -871,8 +872,9 @@
 	 * leading prefix {@code refs/heads/} is removed from the reference before
 	 * it is returned to the caller.
 	 *
-	 * @return name of current branch (for example {@code master}), or an
-	 *         ObjectId in hex format if the current branch is detached.
+	 * @return name of current branch (for example {@code master}), an
+	 *         ObjectId in hex format if the current branch is detached,
+	 *         or null if the repository is corrupt and has no HEAD reference.
 	 * @throws IOException
 	 */
 	public String getBranch() throws IOException {
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 0c58a0b..23cc264 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java
@@ -47,6 +47,8 @@
 import java.io.IOException;
 import java.lang.ref.Reference;
 import java.lang.ref.SoftReference;
+import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Iterator;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
@@ -143,6 +145,28 @@
 		}
 	}
 
+	/**
+	 * Remove a repository from the cache.
+	 * <p>
+	 * Removes a repository from the cache, if it is still registered here,
+	 * permitting it to close.
+	 *
+	 * @param location
+	 *            location of the repository to remove.
+	 * @since 4.1
+	 */
+	public static void unregister(Key location) {
+		cache.unregisterRepository(location);
+	}
+
+	/**
+	 * @return the locations of all repositories registered in the cache.
+	 * @since 4.1
+	 */
+	public static Collection<Key> getRegisteredKeys() {
+		return cache.getKeys();
+	}
+
 	/** Unregister all repositories from the cache. */
 	public static void clear() {
 		cache.clearAll();
@@ -195,6 +219,10 @@
 			oldDb.close();
 	}
 
+	private Collection<Key> getKeys() {
+		return new ArrayList<Key>(cacheMap.keySet());
+	}
+
 	private void clearAll() {
 		for (int stage = 0; stage < 2; stage++) {
 			for (Iterator<Map.Entry<Key, Reference<Repository>>> i = cacheMap
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 3654ffd..8a6343c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/merge/ResolveMerger.java
@@ -525,10 +525,11 @@
 			}
 		}
 
-		if (nonTree(modeO) && modeB == modeT && tw.idEqual(T_BASE, T_THEIRS)) {
+		if (modeB == modeT && tw.idEqual(T_BASE, T_THEIRS)) {
 			// THEIRS was not changed compared to BASE. All changes must be in
 			// OURS. OURS is chosen. We can keep the existing entry.
-			keep(ourDce);
+			if (ourDce != null)
+				keep(ourDce);
 			// no checkout needed!
 			return true;
 		}
@@ -549,11 +550,12 @@
 				if (e != null)
 					toBeCheckedOut.put(tw.getPathString(), e);
 				return true;
-			} else if (modeT == 0 && modeB != 0) {
-				// we want THEIRS ... but THEIRS contains the deletion of the
-				// file. Also, do not complain if the file is already deleted
-				// locally. This complements the test in isWorktreeDirty() for
-				// the same case.
+			} else {
+				// we want THEIRS ... but THEIRS contains a folder or the
+				// deletion of the path. Delete what's in the workingtree (the
+				// workingtree is clean) but do not complain if the file is
+				// already deleted locally. This complements the test in
+				// isWorktreeDirty() for the same case.
 				if (tw.getTreeCount() > T_FILE && tw.getRawMode(T_FILE) == 0)
 					return true;
 				toBeDeleted.add(tw.getPathString());
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/notes/FanoutBucket.java b/org.eclipse.jgit/src/org/eclipse/jgit/notes/FanoutBucket.java
index ea904cd..79fbb09 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/notes/FanoutBucket.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/notes/FanoutBucket.java
@@ -260,8 +260,8 @@
 	}
 
 	ObjectId getTreeId() {
-		try {
-			return new ObjectInserter.Formatter().idFor(build(false, null));
+		try (ObjectInserter.Formatter f = new ObjectInserter.Formatter()) {
+			return f.idFor(build(false, null));
 		} catch (IOException e) {
 			// should never happen as we are not inserting
 			throw new RuntimeException(e);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java
index afb208e..1176d95 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevWalk.java
@@ -1406,6 +1406,8 @@
 
 	/**
 	 * Assume additional commits are shallow (have no parents).
+	 * <p>
+	 * This method is a No-op if the collection is empty.
 	 *
 	 * @param ids
 	 *            commits that should be treated as shallow commits, in addition
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/storage/pack/PackStatistics.java b/org.eclipse.jgit/src/org/eclipse/jgit/storage/pack/PackStatistics.java
new file mode 100644
index 0000000..a811fe3
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/storage/pack/PackStatistics.java
@@ -0,0 +1,491 @@
+/*
+ * Copyright (C) 2015, Google Inc.
+ *
+ * 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.storage.pack;
+
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT;
+import static org.eclipse.jgit.lib.Constants.OBJ_TAG;
+import static org.eclipse.jgit.lib.Constants.OBJ_TREE;
+
+import java.text.MessageFormat;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.storage.pack.CachedPack;
+import org.eclipse.jgit.lib.ObjectId;
+
+/**
+ * Statistics about {@link org.eclipse.jgit.internal.storage.pack.PackWriter}
+ * pack creation.
+ *
+ * @since 4.1
+ */
+public class PackStatistics {
+	/**
+	 * Statistics about a single type of object (commits, tags, trees and
+	 * blobs).
+	 */
+	public static class ObjectType {
+		/**
+		 * POJO for accumulating the ObjectType statistics.
+		 */
+		public static class Accumulator {
+			/** Count of objects of this type. */
+			public long cntObjects;
+
+			/** Count of deltas of this type. */
+			public long cntDeltas;
+
+			/** Count of reused objects of this type. */
+			public long reusedObjects;
+
+			/** Count of reused deltas of this type. */
+			public long reusedDeltas;
+
+			/** Count of bytes for all objects of this type. */
+			public long bytes;
+
+			/** Count of delta bytes for objects of this type. */
+			public long deltaBytes;
+		}
+
+		private ObjectType.Accumulator objectType;
+
+		/**
+		 * Creates a new {@link ObjectType} object from the accumulator.
+		 *
+		 * @param accumulator
+		 *            the accumulator of the statistics
+		 */
+		public ObjectType(ObjectType.Accumulator accumulator) {
+			/*
+			 * For efficiency this wraps and serves up the Accumulator object
+			 * rather than making a deep clone. Normal usage of PackWriter is to
+			 * create a single pack/index/bitmap and only call getStatistics()
+			 * after all work is complete.
+			 */
+			objectType = accumulator;
+		}
+
+		/**
+		 * @return total number of objects output. This total includes the value
+		 *         of {@link #getDeltas()}.
+		 */
+		public long getObjects() {
+			return objectType.cntObjects;
+		}
+
+		/**
+		 * @return total number of deltas output. This may be lower than the
+		 *         actual number of deltas if a cached pack was reused.
+		 */
+		public long getDeltas() {
+			return objectType.cntDeltas;
+		}
+
+		/**
+		 * @return number of objects whose existing representation was reused in
+		 *         the output. This count includes {@link #getReusedDeltas()}.
+		 */
+		public long getReusedObjects() {
+			return objectType.reusedObjects;
+		}
+
+		/**
+		 * @return number of deltas whose existing representation was reused in
+		 *         the output, as their base object was also output or was
+		 *         assumed present for a thin pack. This may be lower than the
+		 *         actual number of reused deltas if a cached pack was reused.
+		 */
+		public long getReusedDeltas() {
+			return objectType.reusedDeltas;
+		}
+
+		/**
+		 * @return total number of bytes written. This size includes the object
+		 *         headers as well as the compressed data. This size also
+		 *         includes all of {@link #getDeltaBytes()}.
+		 */
+		public long getBytes() {
+			return objectType.bytes;
+		}
+
+		/**
+		 * @return number of delta bytes written. This size includes the object
+		 *         headers for the delta objects.
+		 */
+		public long getDeltaBytes() {
+			return objectType.deltaBytes;
+		}
+	}
+
+	/**
+	 * POJO for accumulating the statistics.
+	 */
+	public static class Accumulator {
+		/** The set of objects to be included in the pack. */
+		public Set<ObjectId> interestingObjects;
+
+		/** The set of objects to be excluded from the pack. */
+		public Set<ObjectId> uninterestingObjects;
+
+		/** The set of shallow commits on the client. */
+		public Set<ObjectId> clientShallowCommits;
+
+		/** The collection of reused packs in the upload. */
+		public List<CachedPack> reusedPacks;
+
+		/** Commits with no parents. */
+		public Set<ObjectId> rootCommits;
+
+		/** If a shallow pack, the depth in commits. */
+		public int depth;
+
+		/**
+		 * The count of objects in the pack that went through the delta search
+		 * process in order to find a potential delta base.
+		 */
+		public int deltaSearchNonEdgeObjects;
+
+		/**
+		 * The count of objects in the pack that went through delta base search
+		 * and found a suitable base. This is a subset of
+		 * deltaSearchNonEdgeObjects.
+		 */
+		public int deltasFound;
+
+		/** The total count of objects in the pack. */
+		public long totalObjects;
+
+		/**
+		 * The count of objects that needed to be discovered through an object
+		 * walk because they were not found in bitmap indices.
+		 */
+		public long bitmapIndexMisses;
+
+		/** The total count of deltas output. */
+		public long totalDeltas;
+
+		/** The count of reused objects in the pack. */
+		public long reusedObjects;
+
+		/** The count of reused deltas in the pack. */
+		public long reusedDeltas;
+
+		/** The count of total bytes in the pack. */
+		public long totalBytes;
+
+		/** The size of the thin pack in bytes, if a thin pack was generated. */
+		public long thinPackBytes;
+
+		/** Time in ms spent counting the objects that will go into the pack. */
+		public long timeCounting;
+
+		/** Time in ms spent searching for objects to reuse. */
+		public long timeSearchingForReuse;
+
+		/** Time in ms spent searching for sizes of objects. */
+		public long timeSearchingForSizes;
+
+		/** Time in ms spent compressing the pack. */
+		public long timeCompressing;
+
+		/** Time in ms spent writing the pack. */
+		public long timeWriting;
+
+		/**
+		 * Statistics about each object type in the pack (commits, tags, trees
+		 * and blobs.)
+		 */
+		public ObjectType.Accumulator[] objectTypes;
+
+		{
+			objectTypes = new ObjectType.Accumulator[5];
+			objectTypes[OBJ_COMMIT] = new ObjectType.Accumulator();
+			objectTypes[OBJ_TREE] = new ObjectType.Accumulator();
+			objectTypes[OBJ_BLOB] = new ObjectType.Accumulator();
+			objectTypes[OBJ_TAG] = new ObjectType.Accumulator();
+		}
+	}
+
+	private Accumulator statistics;
+
+	/**
+	 * Creates a new {@link PackStatistics} object from the accumulator.
+	 *
+	 * @param accumulator
+	 *            the accumulator of the statistics
+	 */
+	public PackStatistics(Accumulator accumulator) {
+		/*
+		 * For efficiency this wraps and serves up the Accumulator object rather
+		 * than making a deep clone. Normal usage of PackWriter is to create a
+		 * single pack/index/bitmap and only call getStatistics() after all work
+		 * is complete.
+		 */
+		statistics = accumulator;
+	}
+
+	/**
+	 * @return unmodifiable collection of objects to be included in the pack.
+	 *         May be {@code null} if the pack was hand-crafted in a unit test.
+	 */
+	public Set<ObjectId> getInterestingObjects() {
+		return statistics.interestingObjects;
+	}
+
+	/**
+	 * @return unmodifiable collection of objects that should be excluded from
+	 *         the pack, as the peer that will receive the pack already has
+	 *         these objects.
+	 */
+	public Set<ObjectId> getUninterestingObjects() {
+		return statistics.uninterestingObjects;
+	}
+
+	/**
+	 * @return unmodifiable collection of objects that were shallow commits on
+	 *         the client.
+	 */
+	public Set<ObjectId> getClientShallowCommits() {
+		return statistics.clientShallowCommits;
+	}
+
+	/**
+	 * @return unmodifiable list of the cached packs that were reused in the
+	 *         output, if any were selected for reuse.
+	 */
+	public List<CachedPack> getReusedPacks() {
+		return statistics.reusedPacks;
+	}
+
+	/** @return unmodifiable collection of the root commits of the history. */
+	public Set<ObjectId> getRootCommits() {
+		return statistics.rootCommits;
+	}
+
+	/**
+	 * @return number of objects in the output pack that went through the delta
+	 *         search process in order to find a potential delta base.
+	 */
+	public int getDeltaSearchNonEdgeObjects() {
+		return statistics.deltaSearchNonEdgeObjects;
+	}
+
+	/**
+	 * @return number of objects in the output pack that went through delta base
+	 *         search and found a suitable base. This is a subset of
+	 *         {@link #getDeltaSearchNonEdgeObjects()}.
+	 */
+	public int getDeltasFound() {
+		return statistics.deltasFound;
+	}
+
+	/**
+	 * @return total number of objects output. This total includes the value of
+	 *         {@link #getTotalDeltas()}.
+	 */
+	public long getTotalObjects() {
+		return statistics.totalObjects;
+	}
+
+	/**
+	 * @return the count of objects that needed to be discovered through an
+	 *         object walk because they were not found in bitmap indices.
+	 *         Returns -1 if no bitmap indices were found.
+	 */
+	public long getBitmapIndexMisses() {
+		return statistics.bitmapIndexMisses;
+	}
+
+	/**
+	 * @return total number of deltas output. This may be lower than the actual
+	 *         number of deltas if a cached pack was reused.
+	 */
+	public long getTotalDeltas() {
+		return statistics.totalDeltas;
+	}
+
+	/**
+	 * @return number of objects whose existing representation was reused in the
+	 *         output. This count includes {@link #getReusedDeltas()}.
+	 */
+	public long getReusedObjects() {
+		return statistics.reusedObjects;
+	}
+
+	/**
+	 * @return number of deltas whose existing representation was reused in the
+	 *         output, as their base object was also output or was assumed
+	 *         present for a thin pack. This may be lower than the actual number
+	 *         of reused deltas if a cached pack was reused.
+	 */
+	public long getReusedDeltas() {
+		return statistics.reusedDeltas;
+	}
+
+	/**
+	 * @return total number of bytes written. This size includes the pack
+	 *         header, trailer, thin pack, and reused cached pack(s).
+	 */
+	public long getTotalBytes() {
+		return statistics.totalBytes;
+	}
+
+	/**
+	 * @return size of the thin pack in bytes, if a thin pack was generated. A
+	 *         thin pack is created when the client already has objects and some
+	 *         deltas are created against those objects, or if a cached pack is
+	 *         being used and some deltas will reference objects in the cached
+	 *         pack. This size does not include the pack header or trailer.
+	 */
+	public long getThinPackBytes() {
+		return statistics.thinPackBytes;
+	}
+
+	/**
+	 * @param typeCode
+	 *            object type code, e.g. OBJ_COMMIT or OBJ_TREE.
+	 * @return information about this type of object in the pack.
+	 */
+	public ObjectType byObjectType(int typeCode) {
+		return new ObjectType(statistics.objectTypes[typeCode]);
+	}
+
+	/** @return true if the resulting pack file was a shallow pack. */
+	public boolean isShallow() {
+		return statistics.depth > 0;
+	}
+
+	/** @return depth (in commits) the pack includes if shallow. */
+	public int getDepth() {
+		return statistics.depth;
+	}
+
+	/**
+	 * @return time in milliseconds spent enumerating the objects that need to
+	 *         be included in the output. This time includes any restarts that
+	 *         occur when a cached pack is selected for reuse.
+	 */
+	public long getTimeCounting() {
+		return statistics.timeCounting;
+	}
+
+	/**
+	 * @return time in milliseconds spent matching existing representations
+	 *         against objects that will be transmitted, or that the client can
+	 *         be assumed to already have.
+	 */
+	public long getTimeSearchingForReuse() {
+		return statistics.timeSearchingForReuse;
+	}
+
+	/**
+	 * @return time in milliseconds spent finding the sizes of all objects that
+	 *         will enter the delta compression search window. The sizes need to
+	 *         be known to better match similar objects together and improve
+	 *         delta compression ratios.
+	 */
+	public long getTimeSearchingForSizes() {
+		return statistics.timeSearchingForSizes;
+	}
+
+	/**
+	 * @return time in milliseconds spent on delta compression. This is observed
+	 *         wall-clock time and does not accurately track CPU time used when
+	 *         multiple threads were used to perform the delta compression.
+	 */
+	public long getTimeCompressing() {
+		return statistics.timeCompressing;
+	}
+
+	/**
+	 * @return time in milliseconds spent writing the pack output, from start of
+	 *         header until end of trailer. The transfer speed can be
+	 *         approximated by dividing {@link #getTotalBytes()} by this value.
+	 */
+	public long getTimeWriting() {
+		return statistics.timeWriting;
+	}
+
+	/** @return total time spent processing this pack. */
+	public long getTimeTotal() {
+		return statistics.timeCounting + statistics.timeSearchingForReuse
+				+ statistics.timeSearchingForSizes + statistics.timeCompressing
+				+ statistics.timeWriting;
+	}
+
+	/**
+	 * @return get the average output speed in terms of bytes-per-second.
+	 *         {@code getTotalBytes() / (getTimeWriting() / 1000.0)}.
+	 */
+	public double getTransferRate() {
+		return getTotalBytes() / (getTimeWriting() / 1000.0);
+	}
+
+	/** @return formatted message string for display to clients. */
+	public String getMessage() {
+		return MessageFormat.format(JGitText.get().packWriterStatistics,
+				Long.valueOf(statistics.totalObjects),
+				Long.valueOf(statistics.totalDeltas),
+				Long.valueOf(statistics.reusedObjects),
+				Long.valueOf(statistics.reusedDeltas));
+	}
+
+	/** @return a map containing ObjectType statistics. */
+	public Map<Integer, ObjectType> getObjectTypes() {
+		HashMap<Integer, ObjectType> map = new HashMap<>();
+		map.put(Integer.valueOf(OBJ_BLOB), new ObjectType(
+				statistics.objectTypes[OBJ_BLOB]));
+		map.put(Integer.valueOf(OBJ_COMMIT), new ObjectType(
+				statistics.objectTypes[OBJ_COMMIT]));
+		map.put(Integer.valueOf(OBJ_TAG), new ObjectType(
+				statistics.objectTypes[OBJ_TAG]));
+		map.put(Integer.valueOf(OBJ_TREE), new ObjectType(
+				statistics.objectTypes[OBJ_TREE]));
+		return map;
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/submodule/SubmoduleWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/submodule/SubmoduleWalk.java
index 7d9bca0..6263d4b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/submodule/SubmoduleWalk.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/submodule/SubmoduleWalk.java
@@ -451,13 +451,14 @@
 	}
 
 	/**
-	 * Checks whether the working tree (or the index in case of a bare repo)
-	 * contains a .gitmodules file. That's a hint that the repo contains
-	 * submodules.
+	 * Checks whether the working tree contains a .gitmodules file. That's a
+	 * hint that the repo contains submodules.
 	 *
 	 * @param repository
 	 *            the repository to check
-	 * @return <code>true</code> if the repo contains a .gitmodules file
+	 * @return <code>true</code> if the working tree contains a .gitmodules file,
+	 *         <code>false</code> otherwise. Always returns <code>false</code>
+	 *         for bare repositories.
 	 * @throws IOException
 	 * @throws CorruptObjectException
 	 * @since 3.6
@@ -465,8 +466,7 @@
 	public static boolean containsGitModulesFile(Repository repository)
 			throws IOException {
 		if (repository.isBare()) {
-			DirCache dc = repository.readDirCache();
-			return (dc.findEntry(Constants.DOT_GIT_MODULES) >= 0);
+			return false;
 		}
 		File modulesFile = new File(repository.getWorkTree(),
 				Constants.DOT_GIT_MODULES);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java
index a6fc633..cf13582 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java
@@ -193,6 +193,13 @@
 	 */
 	public static final String OPTION_ALLOW_TIP_SHA1_IN_WANT = GitProtocolConstants.OPTION_ALLOW_TIP_SHA1_IN_WANT;
 
+	/**
+	 * The client supports fetching objects that are reachable from a tip of a
+	 * ref that is allowed to fetch.
+	 * @since 4.1
+	 */
+	public static final String OPTION_ALLOW_REACHABLE_SHA1_IN_WANT = GitProtocolConstants.OPTION_ALLOW_REACHABLE_SHA1_IN_WANT;
+
 	private final RevWalk walk;
 
 	/** All commits that are immediately reachable by a local ref. */
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java
index 9112ecb..776a9f6 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java
@@ -252,13 +252,35 @@
 	/** The size of the received pack, including index size */
 	private Long packSize;
 
-	PushCertificateParser pushCertificateParser;
+	private PushCertificateParser pushCertificateParser;
+	private SignedPushConfig signedPushConfig;
+	private PushCertificate pushCert;
 
 	/**
-	 * @return the push certificate used to verify the pushers identity.
+	 * Get the push certificate used to verify the pusher's identity.
+	 * <p>
+	 * Only valid after commands are read from the wire.
+	 *
+	 * @return the parsed certificate, or null if push certificates are disabled
+	 *         or no cert was presented by the client.
+	 * @since 4.1
 	 */
-	PushCertificate getPushCertificate() {
-		return pushCertificateParser;
+	public PushCertificate getPushCertificate() {
+		return pushCert;
+	}
+
+	/**
+	 * Set the push certificate used to verify the pusher's identity.
+	 * <p>
+	 * Should only be called if reconstructing an instance without going through
+	 * the normal {@link #recvCommands()} flow.
+	 *
+	 * @param cert
+	 *            the push certificate to set.
+	 * @since 4.1
+	 */
+	public void setPushCertificate(PushCertificate cert) {
+		pushCert = cert;
 	}
 
 	/**
@@ -282,7 +304,7 @@
 		refFilter = RefFilter.DEFAULT;
 		advertisedHaves = new HashSet<ObjectId>();
 		clientShallowCommits = new HashSet<ObjectId>();
-		pushCertificateParser = new PushCertificateParser(db, cfg);
+		signedPushConfig = cfg.signedPush;
 	}
 
 	/** Configuration for receive operations. */
@@ -304,8 +326,7 @@
 		final boolean allowNonFastForwards;
 		final boolean allowOfsDelta;
 
-		final String certNonceSeed;
-		final int certNonceSlopLimit;
+		final SignedPushConfig signedPush;
 
 		ReceiveConfig(final Config config) {
 			checkReceivedObjects = config.getBoolean(
@@ -326,8 +347,7 @@
 					"denynonfastforwards", false); //$NON-NLS-1$
 			allowOfsDelta = config.getBoolean("repack", "usedeltabaseoffset", //$NON-NLS-1$ //$NON-NLS-2$
 					true);
-			certNonceSeed = config.getString("receive", null, "certnonceseed"); //$NON-NLS-1$ //$NON-NLS-2$
-			certNonceSlopLimit = config.getInt("receive", "certnonceslop", 0); //$NON-NLS-1$ //$NON-NLS-2$
+			signedPush = SignedPushConfig.KEY.parse(config);
 		}
 
 		ObjectChecker newObjectChecker() {
@@ -784,6 +804,26 @@
 	}
 
 	/**
+	 * Set the configuration for push certificate verification.
+	 *
+	 * @param cfg
+	 *            new configuration; if this object is null or its {@link
+	 *            SignedPushConfig#getCertNonceSeed()} is null, push certificate
+	 *            verification will be disabled.
+	 * @since 4.1
+	 */
+	public void setSignedPushConfig(SignedPushConfig cfg) {
+		signedPushConfig = cfg;
+	}
+
+	private PushCertificateParser getPushCertificateParser() {
+		if (pushCertificateParser == null) {
+			pushCertificateParser = new PushCertificateParser(db, signedPushConfig);
+		}
+		return pushCertificateParser;
+	}
+
+	/**
 	 * Get the user agent of the client.
 	 * <p>
 	 * If the client is new enough to use {@code agent=} capability that value
@@ -1014,9 +1054,10 @@
 		adv.advertiseCapability(CAPABILITY_REPORT_STATUS);
 		if (allowQuiet)
 			adv.advertiseCapability(CAPABILITY_QUIET);
-		if (pushCertificateParser.enabled())
-			adv.advertiseCapability(
-				pushCertificateParser.getAdvertiseNonce());
+		String nonce = getPushCertificateParser().getAdvertiseNonce();
+		if (nonce != null) {
+			adv.advertiseCapability(nonce);
+		}
 		if (db.getRefDatabase().performsAtomicTransactions())
 			adv.advertiseCapability(CAPABILITY_ATOMIC);
 		if (allowOfsDelta)
@@ -1036,58 +1077,90 @@
 	 * @throws IOException
 	 */
 	protected void recvCommands() throws IOException {
-		for (;;) {
-			String line;
-			try {
-				line = pckIn.readStringRaw();
-			} catch (EOFException eof) {
-				if (commands.isEmpty())
-					return;
-				throw eof;
+		PushCertificateParser certParser = getPushCertificateParser();
+		FirstLine firstLine = null;
+		try {
+			for (;;) {
+				String line;
+				try {
+					line = pckIn.readString();
+				} catch (EOFException eof) {
+					if (commands.isEmpty())
+						return;
+					throw eof;
+				}
+				if (line == PacketLineIn.END) {
+					break;
+				}
+
+				if (line.length() >= 48 && line.startsWith("shallow ")) { //$NON-NLS-1$
+					clientShallowCommits.add(ObjectId.fromString(line.substring(8, 48)));
+					continue;
+				}
+
+				if (firstLine == null) {
+					firstLine = new FirstLine(line);
+					enabledCapabilities = firstLine.getCapabilities();
+					line = firstLine.getLine();
+
+					if (line.equals(GitProtocolConstants.OPTION_PUSH_CERT)) {
+						certParser.receiveHeader(pckIn, !isBiDirectionalPipe());
+						continue;
+					}
+				}
+
+				if (line.equals(PushCertificateParser.BEGIN_SIGNATURE)) {
+					certParser.receiveSignature(pckIn);
+					continue;
+				}
+
+				ReceiveCommand cmd;
+				try {
+					cmd = parseCommand(line);
+				} catch (PackProtocolException e) {
+					sendError(e.getMessage());
+					throw e;
+				}
+				if (cmd.getRefName().equals(Constants.HEAD)) {
+					cmd.setResult(Result.REJECTED_CURRENT_BRANCH);
+				} else {
+					cmd.setRef(refs.get(cmd.getRefName()));
+				}
+				commands.add(cmd);
+				if (certParser.enabled()) {
+					certParser.addCommand(cmd);
+				}
 			}
-			if (line == PacketLineIn.END)
-				break;
-
-			if (line.length() >= 48 && line.startsWith("shallow ")) { //$NON-NLS-1$
-				clientShallowCommits.add(ObjectId.fromString(line.substring(8, 48)));
-				continue;
-			}
-
-			if (commands.isEmpty()) {
-				final FirstLine firstLine = new FirstLine(line);
-				enabledCapabilities = firstLine.getCapabilities();
-				line = firstLine.getLine();
-
-				if (line.equals(GitProtocolConstants.OPTION_PUSH_CERT))
-					pushCertificateParser.receiveHeader(pckIn,
-							!isBiDirectionalPipe());
-			}
-
-			if (line.equals("-----BEGIN PGP SIGNATURE-----\n")) //$NON-NLS-1$
-				pushCertificateParser.receiveSignature(pckIn);
-
-			if (pushCertificateParser.enabled())
-				pushCertificateParser.addCommand(line);
-
-			if (line.length() < 83) {
-				final String m = JGitText.get().errorInvalidProtocolWantedOldNewRef;
-				sendError(m);
-				throw new PackProtocolException(m);
-			}
-
-			final ObjectId oldId = ObjectId.fromString(line.substring(0, 40));
-			final ObjectId newId = ObjectId.fromString(line.substring(41, 81));
-			final String name = line.substring(82);
-			final ReceiveCommand cmd = new ReceiveCommand(oldId, newId, name);
-			if (name.equals(Constants.HEAD)) {
-				cmd.setResult(Result.REJECTED_CURRENT_BRANCH);
-			} else {
-				cmd.setRef(refs.get(cmd.getRefName()));
-			}
-			commands.add(cmd);
+			pushCert = certParser.build();
+		} catch (PackProtocolException e) {
+			sendError(e.getMessage());
+			throw e;
 		}
 	}
 
+	static ReceiveCommand parseCommand(String line) throws PackProtocolException {
+          if (line == null || line.length() < 83) {
+			throw new PackProtocolException(
+					JGitText.get().errorInvalidProtocolWantedOldNewRef);
+		}
+		String oldStr = line.substring(0, 40);
+		String newStr = line.substring(41, 81);
+		ObjectId oldId, newId;
+		try {
+			oldId = ObjectId.fromString(oldStr);
+			newId = ObjectId.fromString(newStr);
+		} catch (IllegalArgumentException e) {
+			throw new PackProtocolException(
+					JGitText.get().errorInvalidProtocolWantedOldNewRef, e);
+		}
+		String name = line.substring(82);
+		if (!Repository.isValidRefName(name)) {
+			throw new PackProtocolException(
+					JGitText.get().errorInvalidProtocolWantedOldNewRef);
+		}
+		return new ReceiveCommand(oldId, newId, name);
+	}
+
 	/** Enable capabilities based on a previously read capabilities line. */
 	protected void enableCapabilities() {
 		sideBand = isCapabilityEnabled(CAPABILITY_SIDE_BAND_64K);
@@ -1432,6 +1505,7 @@
 		batch.setRefLogMessage("push", true); //$NON-NLS-1$
 		batch.addCommand(toApply);
 		try {
+			batch.setPushCertificate(getPushCertificate());
 			batch.execute(walk, updating);
 		} catch (IOException err) {
 			for (ReceiveCommand cmd : toApply) {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BundleWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BundleWriter.java
index 81ad981..ca624c0 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BundleWriter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BundleWriter.java
@@ -92,6 +92,8 @@
 
 	private PackConfig packConfig;
 
+	private ObjectCountCallback callback;
+
 	/**
 	 * Create a writer for a bundle.
 	 *
@@ -188,6 +190,9 @@
 	 *             an error occurred reading a local object's data to include in
 	 *             the bundle, or writing compressed object data to the output
 	 *             stream.
+	 * @throws WriteAbortedException
+	 *             the write operation is aborted by
+	 *             {@link ObjectCountCallback}.
 	 */
 	public void writeBundle(ProgressMonitor monitor, OutputStream os)
 			throws IOException {
@@ -195,6 +200,8 @@
 		if (pc == null)
 			pc = new PackConfig(db);
 		try (PackWriter packWriter = new PackWriter(pc, db.newObjectReader())) {
+			packWriter.setObjectCountCallback(callback);
+
 			final HashSet<ObjectId> inc = new HashSet<ObjectId>();
 			final HashSet<ObjectId> exc = new HashSet<ObjectId>();
 			inc.addAll(include.values());
@@ -234,4 +241,24 @@
 			packWriter.writePack(monitor, monitor, os);
 		}
 	}
+
+	/**
+	 * Set the {@link ObjectCountCallback}.
+	 * <p>
+	 * It should be set before calling
+	 * {@link #writeBundle(ProgressMonitor, OutputStream)}.
+	 * <p>
+	 * This callback will be passed on to
+	 * {@link PackWriter#setObjectCountCallback}.
+	 *
+	 * @param callback
+	 *            the callback to set
+	 *
+	 * @return this object for chaining.
+	 * @since 4.1
+	 */
+	public BundleWriter setObjectCountCallback(ObjectCountCallback callback) {
+		this.callback = callback;
+		return this;
+	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/GitProtocolConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/GitProtocolConstants.java
index 8d2d554..efde062 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/GitProtocolConstants.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/GitProtocolConstants.java
@@ -130,6 +130,14 @@
 	public static final String OPTION_ALLOW_TIP_SHA1_IN_WANT = "allow-tip-sha1-in-want"; //$NON-NLS-1$
 
 	/**
+	 * The client supports fetching objects that are reachable from a tip of a
+	 * ref that is allowed to fetch.
+	 *
+	 * @since 4.1
+	 */
+	public static final String OPTION_ALLOW_REACHABLE_SHA1_IN_WANT = "allow-reachable-sha1-in-want"; //$NON-NLS-1$
+
+	/**
 	 * Symbolic reference support for better negotiation.
 	 *
 	 * @since 3.6
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/HMACSHA1NonceGenerator.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/HMACSHA1NonceGenerator.java
index 222ca55..7e9434a 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/HMACSHA1NonceGenerator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/HMACSHA1NonceGenerator.java
@@ -105,36 +105,42 @@
 	@Override
 	public NonceStatus verify(String received, String sent,
 			Repository db, boolean allowSlop, int slop) {
-		if (received.isEmpty())
+		if (received.isEmpty()) {
 			return NonceStatus.MISSING;
-		else if (sent.isEmpty())
+		} else if (sent.isEmpty()) {
 			return NonceStatus.UNSOLICITED;
-		else if (received.equals(sent))
+		} else if (received.equals(sent)) {
 			return NonceStatus.OK;
+		}
 
-		if (!allowSlop)
+		if (!allowSlop) {
 			return NonceStatus.BAD;
+		}
 
 		/* nonce is concat(<seconds-since-epoch>, "-", <hmac>) */
 		int idxSent = sent.indexOf('-');
 		int idxRecv = received.indexOf('-');
-		if (idxSent == -1 || idxRecv == -1)
+		if (idxSent == -1 || idxRecv == -1) {
 			return NonceStatus.BAD;
+		}
 
+		String signedStampStr = received.substring(0, idxRecv);
+		String advertisedStampStr = sent.substring(0, idxSent);
 		long signedStamp;
 		long advertisedStamp;
 		try {
-			signedStamp = Long.parseLong(received.substring(0, idxRecv));
-			advertisedStamp = Long.parseLong(sent.substring(0, idxSent));
-		} catch (Exception e) {
+			signedStamp = Long.parseLong(signedStampStr);
+			advertisedStamp = Long.parseLong(advertisedStampStr);
+		} catch (IllegalArgumentException e) {
 			return NonceStatus.BAD;
 		}
 
 		// what we would have signed earlier
 		String expect = createNonce(db, signedStamp);
 
-		if (!expect.equals(received))
+		if (!expect.equals(received)) {
 			return NonceStatus.BAD;
+		}
 
 		long nonceStampSlop = Math.abs(advertisedStamp - signedStamp);
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ObjectCountCallback.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ObjectCountCallback.java
new file mode 100644
index 0000000..cf81adb
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ObjectCountCallback.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2015, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.transport;
+
+import java.io.OutputStream;
+
+import org.eclipse.jgit.internal.storage.pack.PackWriter;
+import org.eclipse.jgit.lib.ProgressMonitor;
+
+/**
+ * A callback to tell caller the count of objects ASAP.
+ *
+ * @since 4.1
+ */
+public interface ObjectCountCallback {
+	/**
+	 * Invoked when the {@link PackWriter} has counted the objects to be
+	 * written to pack.
+	 * <p>
+	 * An {@code ObjectCountCallback} can use this information to decide
+	 * whether the
+	 * {@link PackWriter#writePack(ProgressMonitor, ProgressMonitor, OutputStream)}
+	 * operation should be aborted.
+	 * <p>
+	 * This callback will be called exactly once.
+	 *
+	 * @param objectCount
+	 *            the count of the objects.
+	 * @throws WriteAbortedException
+	 *             to indicate that the write operation should be aborted.
+	 */
+	void setObjectCount(long objectCount) throws WriteAbortedException;
+}
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 04abe22..918df94 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackParser.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PackParser.java
@@ -135,6 +135,8 @@
 
 	private boolean allowThin;
 
+	private boolean checkObjectCollisions;
+
 	private boolean needBaseObjectIds;
 
 	private boolean checkEofAfterPackFooter;
@@ -204,6 +206,7 @@
 		objectDigest = Constants.newMessageDigest();
 		tempObjectId = new MutableObjectId();
 		packDigest = Constants.newMessageDigest();
+		checkObjectCollisions = true;
 	}
 
 	/** @return true if a thin pack (missing base objects) is permitted. */
@@ -225,6 +228,39 @@
 	}
 
 	/**
+	 * @return if true received objects are verified to prevent collisions.
+	 * @since 4.1
+	 */
+	protected boolean isCheckObjectCollisions() {
+		return checkObjectCollisions;
+	}
+
+	/**
+	 * Enable checking for collisions with existing objects.
+	 * <p>
+	 * By default PackParser looks for each received object in the repository.
+	 * If the object already exists, the existing object is compared
+	 * byte-for-byte with the newly received copy to ensure they are identical.
+	 * The receive is aborted with an exception if any byte differs. This check
+	 * is necessary to prevent an evil attacker from supplying a replacement
+	 * object into this repository in the event that a discovery enabling SHA-1
+	 * collisions is made.
+	 * <p>
+	 * This check may be very costly to perform, and some repositories may have
+	 * other ways to segregate newly received object data. The check is enabled
+	 * by default, but can be explicitly disabled if the implementation can
+	 * provide the same guarantee, or is willing to accept the risks associated
+	 * with bypassing the check.
+	 *
+	 * @param check
+	 *            true to enable collision checking (strongly encouraged).
+	 * @since 4.1
+	 */
+	protected void setCheckObjectCollisions(boolean check) {
+		checkObjectCollisions = check;
+	}
+
+	/**
 	 * Configure this index pack instance to keep track of new objects.
 	 * <p>
 	 * By default an index pack doesn't save the new objects that were created
@@ -988,7 +1024,8 @@
 			}
 			inf.close();
 			tempObjectId.fromRaw(objectDigest.digest(), 0);
-			checkContentLater = readCurs.has(tempObjectId);
+			checkContentLater = isCheckObjectCollisions()
+					&& readCurs.has(tempObjectId);
 			data = null;
 
 		} else {
@@ -1022,17 +1059,19 @@
 			}
 		}
 
-		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()));
+		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.
 			}
-		} 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.
 		}
 	}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PostUploadHook.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PostUploadHook.java
new file mode 100644
index 0000000..53eeab1
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PostUploadHook.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2015, Google Inc.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.transport;
+
+import org.eclipse.jgit.internal.storage.pack.PackWriter;
+import org.eclipse.jgit.storage.pack.PackStatistics;
+
+/**
+ * Hook invoked by {@link UploadPack} after the pack has been uploaded.
+ * <p>
+ * Implementors of the interface are responsible for associating the current
+ * thread to a particular connection, if they need to also include connection
+ * information. One method is to use a {@link java.lang.ThreadLocal} to remember
+ * the connection information before invoking UploadPack.
+ *
+ * @since 4.1
+ */
+public interface PostUploadHook {
+	/** A simple no-op hook. */
+	public static final PostUploadHook NULL = new PostUploadHook() {
+		public void onPostUpload(PackStatistics stats) {
+			// Do nothing.
+		}
+	};
+
+	/**
+	 * Notifies the hook that a pack has been sent.
+	 *
+	 * @param stats
+	 *            the statistics gathered by {@link PackWriter} for the uploaded
+	 *            pack
+	 */
+	public void onPostUpload(PackStatistics stats);
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PostUploadHookChain.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PostUploadHookChain.java
new file mode 100644
index 0000000..4e2eaea
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PostUploadHookChain.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2015, Google Inc.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.transport;
+
+import java.util.List;
+
+import org.eclipse.jgit.storage.pack.PackStatistics;
+
+/**
+ * {@link PostUploadHook} that delegates to a list of other hooks.
+ * <p>
+ * Hooks are run in the order passed to the constructor.
+ *
+ * @since 4.1
+ */
+public class PostUploadHookChain implements PostUploadHook {
+	private final PostUploadHook[] hooks;
+	private final int count;
+
+	/**
+	 * Create a new hook chaining the given hooks together.
+	 *
+	 * @param hooks
+	 *            hooks to execute, in order.
+	 * @return a new chain of the given hooks.
+	 */
+	public static PostUploadHook newChain(List<? extends PostUploadHook> hooks) {
+		PostUploadHook[] newHooks = new PostUploadHook[hooks.size()];
+		int i = 0;
+		for (PostUploadHook hook : hooks)
+			if (hook != PostUploadHook.NULL)
+				newHooks[i++] = hook;
+		if (i == 0)
+			return PostUploadHook.NULL;
+		else if (i == 1)
+			return newHooks[0];
+		else
+			return new PostUploadHookChain(newHooks, i);
+	}
+
+	public void onPostUpload(PackStatistics stats) {
+		for (int i = 0; i < count; i++)
+			hooks[i].onPostUpload(stats);
+	}
+
+	private PostUploadHookChain(PostUploadHook[] hooks, int count) {
+		this.hooks = hooks;
+		this.count = count;
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificate.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificate.java
index 8ee4c17..e450345 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificate.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificate.java
@@ -43,21 +43,26 @@
 
 package org.eclipse.jgit.transport;
 
+import static org.eclipse.jgit.transport.PushCertificateParser.NONCE;
+import static org.eclipse.jgit.transport.PushCertificateParser.PUSHEE;
+import static org.eclipse.jgit.transport.PushCertificateParser.PUSHER;
+import static org.eclipse.jgit.transport.PushCertificateParser.VERSION;
+
+import java.text.MessageFormat;
+import java.util.List;
+import java.util.Objects;
+
+import org.eclipse.jgit.internal.JGitText;
+
 /**
  * The required information to verify the push.
+ * <p>
+ * A valid certificate will not return null from any getter methods; callers may
+ * assume that any null value indicates a missing or invalid certificate.
  *
  * @since 4.0
  */
 public class PushCertificate {
-	/** The tuple "name &lt;email&gt;" as presented in the push certificate. */
-	String pusher;
-
-	/** The remote URL the signed push goes to. */
-	String pushee;
-
-	/** What we think about the returned signed nonce. */
-	NonceStatus nonceStatus;
-
 	/** Verification result of the nonce returned during push. */
 	public enum NonceStatus {
 		/** Nonce was not expected, yet client sent one anyway. */
@@ -66,47 +71,206 @@
 		BAD,
 		/** Nonce is required, but was not sent by client. */
 		MISSING,
-		/** Received nonce is valid. */
+		/**
+		 * Received nonce matches sent nonce, or is valid within the accepted slop
+		 * window.
+		 */
 		OK,
-		/** Received nonce is valid and within the accepted slop window. */
+		/** Received nonce is valid, but outside the accepted slop window. */
 		SLOP
 	}
 
-	String commandList;
-	String signature;
+	private final String version;
+	private final PushCertificateIdent pusher;
+	private final String pushee;
+	private final String nonce;
+	private final NonceStatus nonceStatus;
+	private final List<ReceiveCommand> commands;
+	private final String signature;
+
+	PushCertificate(String version, PushCertificateIdent pusher, String pushee,
+			String nonce, NonceStatus nonceStatus, List<ReceiveCommand> commands,
+			String signature) {
+		if (version == null || version.isEmpty()) {
+			throw new IllegalArgumentException(MessageFormat.format(
+					JGitText.get().pushCertificateInvalidField, VERSION));
+		}
+		if (pusher == null) {
+			throw new IllegalArgumentException(MessageFormat.format(
+					JGitText.get().pushCertificateInvalidField, PUSHER));
+		}
+		if (nonce == null || nonce.isEmpty()) {
+			throw new IllegalArgumentException(MessageFormat.format(
+					JGitText.get().pushCertificateInvalidField, NONCE));
+		}
+		if (nonceStatus == null) {
+			throw new IllegalArgumentException(MessageFormat.format(
+					JGitText.get().pushCertificateInvalidField,
+					"nonce status")); //$NON-NLS-1$
+		}
+		if (commands == null || commands.isEmpty()) {
+			throw new IllegalArgumentException(MessageFormat.format(
+					JGitText.get().pushCertificateInvalidField,
+					"command")); //$NON-NLS-1$
+		}
+		if (signature == null || signature.isEmpty()) {
+			throw new IllegalArgumentException(
+					JGitText.get().pushCertificateInvalidSignature);
+		}
+		if (!signature.startsWith(PushCertificateParser.BEGIN_SIGNATURE)
+				|| !signature.endsWith(PushCertificateParser.END_SIGNATURE + '\n')) {
+			throw new IllegalArgumentException(
+					JGitText.get().pushCertificateInvalidSignature);
+		}
+		this.version = version;
+		this.pusher = pusher;
+		this.pushee = pushee;
+		this.nonce = nonce;
+		this.nonceStatus = nonceStatus;
+		this.commands = commands;
+		this.signature = signature;
+	}
 
 	/**
-	 * @return the signature, consisting of the lines received between the lines
-	 *         '----BEGIN GPG SIGNATURE-----\n' and the '----END GPG
-	 *         SIGNATURE-----\n'
+	 * @return the certificate version string.
+	 * @since 4.1
+	 */
+	public String getVersion() {
+		return version;
+	}
+
+	/**
+	 * @return the raw line that signed the cert, as a string.
+	 * @since 4.0
+	 */
+	public String getPusher() {
+		return pusher.getRaw();
+	}
+
+	/**
+	 * @return identity of the pusher who signed the cert.
+	 * @since 4.1
+	 */
+	public PushCertificateIdent getPusherIdent() {
+		return pusher;
+	}
+
+	/**
+	 * @return URL of the repository the push was originally sent to.
+	 * @since 4.0
+	 */
+	public String getPushee() {
+		return pushee;
+	}
+
+	/**
+	 * @return the raw nonce value that was presented by the pusher.
+	 * @since 4.1
+	 */
+	public String getNonce() {
+		return nonce;
+	}
+
+	/**
+	 * @return verification status of the nonce embedded in the certificate.
+	 * @since 4.0
+	 */
+	public NonceStatus getNonceStatus() {
+		return nonceStatus;
+	}
+
+	/**
+	 * @return the list of commands as one string to be feed into the signature
+	 *         verifier.
+	 * @since 4.1
+	 */
+	public List<ReceiveCommand> getCommands() {
+		return commands;
+	}
+
+	/**
+	 * @return the raw signature, consisting of the lines received between the
+	 *     lines {@code "----BEGIN GPG SIGNATURE-----\n"} and
+	 *     {@code "----END GPG SIGNATURE-----\n}", inclusive.
+	 * @since 4.0
 	 */
 	public String getSignature() {
 		return signature;
 	}
 
 	/**
-	 * @return the list of commands as one string to be feed into the signature
-	 *         verifier.
+	 * @return text payload of the certificate for the signature verifier.
+	 * @since 4.1
 	 */
-	public String getCommandList() {
-		return commandList;
+	public String toText() {
+		return toStringBuilder().toString();
 	}
 
 	/**
-	 * @return the tuple "name &lt;email&gt;" as presented by the client in the
-	 *         push certificate.
+	 * @return original text payload plus signature; the final output will be
+	 *     valid as input to {@link PushCertificateParser#fromString(String)}.
+	 * @since 4.1
 	 */
-	public String getPusher() {
-		return pusher;
+	public String toTextWithSignature() {
+		return toStringBuilder().append(signature).toString();
 	}
 
-	/** @return URL of the repository the push was originally sent to. */
-	public String getPushee() {
-		return pushee;
+	private StringBuilder toStringBuilder() {
+		StringBuilder sb = new StringBuilder()
+				.append(VERSION).append(' ').append(version).append('\n')
+				.append(PUSHER).append(' ').append(getPusher())
+				.append('\n');
+		if (pushee != null) {
+			sb.append(PUSHEE).append(' ').append(pushee).append('\n');
+		}
+		sb.append(NONCE).append(' ').append(nonce).append('\n')
+				.append('\n');
+		for (ReceiveCommand cmd : commands) {
+			sb.append(cmd.getOldId().name())
+				.append(' ').append(cmd.getNewId().name())
+				.append(' ').append(cmd.getRefName()).append('\n');
+		}
+		return sb;
 	}
 
-	/** @return verification status of the nonce embedded in the certificate. */
-	public NonceStatus getNonceStatus() {
-		return nonceStatus;
+	@Override
+	public int hashCode() {
+		return signature.hashCode();
+	}
+
+	@Override
+	public boolean equals(Object o) {
+		if (!(o instanceof PushCertificate)) {
+			return false;
+		}
+		PushCertificate p = (PushCertificate) o;
+		return version.equals(p.version)
+				&& pusher.equals(p.pusher)
+				&& Objects.equals(pushee, p.pushee)
+				&& nonceStatus == p.nonceStatus
+				&& signature.equals(p.signature)
+				&& commandsEqual(this, p);
+	}
+
+	private static boolean commandsEqual(PushCertificate c1, PushCertificate c2) {
+		if (c1.commands.size() != c2.commands.size()) {
+			return false;
+		}
+		for (int i = 0; i < c1.commands.size(); i++) {
+			ReceiveCommand cmd1 = c1.commands.get(i);
+			ReceiveCommand cmd2 = c2.commands.get(i);
+			if (!cmd1.getOldId().equals(cmd2.getOldId())
+					|| !cmd1.getNewId().equals(cmd2.getNewId())
+					|| !cmd1.getRefName().equals(cmd2.getRefName())) {
+				return false;
+			}
+		}
+		return true;
+	}
+
+	@Override
+	public String toString() {
+		return getClass().getSimpleName() + '['
+				 + toTextWithSignature() + ']';
 	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateIdent.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateIdent.java
new file mode 100644
index 0000000..871a6f7
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateIdent.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2015, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.transport;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import static org.eclipse.jgit.util.RawParseUtils.lastIndexOfTrim;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.util.MutableInteger;
+import org.eclipse.jgit.util.RawParseUtils;
+
+/**
+ * Identity in a push certificate.
+ * <p>
+ * This is similar to a {@link PersonIdent} in that it contains a name,
+ * timestamp, and timezone offset, but differs in the following ways:
+ * <ul>
+ * <li>It is always parsed from a UTF-8 string, rather than a raw commit
+ *   buffer.</li>
+ * <li>It is not guaranteed to contain a name and email portion, since any UTF-8
+ *   string is a valid OpenPGP User ID (RFC4880 5.1.1). The raw User ID is
+ *   always available as {@link #getUserId()}, but {@link #getEmailAddress()}
+ *   may return null.</li>
+ * <li>The raw text from which the identity was parsed is available with {@link
+ *   #getRaw()}. This is necessary for losslessly reconstructing the signed push
+ *   certificate payload.</li>
+ * <li>
+ * </ul>
+ *
+ * @since 4.1
+ */
+public class PushCertificateIdent {
+	/**
+	 * Parse an identity from a string.
+	 * <p>
+	 * Spaces are trimmed when parsing the timestamp and timezone offset, with one
+	 * exception. The timestamp must be preceded by a single space, and the rest
+	 * of the string prior to that space (including any additional whitespace) is
+	 * treated as the OpenPGP User ID.
+	 * <p>
+	 * If either the timestamp or timezone offsets are missing, mimics {@link
+	 * RawParseUtils#parsePersonIdent(String)} behavior and sets them both to
+	 * zero.
+	 *
+	 * @param str
+	 *            string to parse.
+	 * @return identity, never null.
+	 */
+	public static PushCertificateIdent parse(String str) {
+		MutableInteger p = new MutableInteger();
+		byte[] raw = str.getBytes(UTF_8);
+		int tzBegin = raw.length - 1;
+		tzBegin = lastIndexOfTrim(raw, ' ', tzBegin);
+		if (tzBegin < 0 || raw[tzBegin] != ' ') {
+			return new PushCertificateIdent(str, str, 0, 0);
+		}
+		int whenBegin = tzBegin++;
+		int tz = RawParseUtils.parseTimeZoneOffset(raw, tzBegin, p);
+		boolean hasTz = p.value != tzBegin;
+
+		whenBegin = lastIndexOfTrim(raw, ' ', whenBegin);
+		if (whenBegin < 0 || raw[whenBegin] != ' ') {
+			return new PushCertificateIdent(str, str, 0, 0);
+		}
+		int idEnd = whenBegin++;
+		long when = RawParseUtils.parseLongBase10(raw, whenBegin, p);
+		boolean hasWhen = p.value != whenBegin;
+
+		if (hasTz && hasWhen) {
+			idEnd = whenBegin - 1;
+		} else {
+			// If either tz or when are non-numeric, mimic parsePersonIdent behavior and
+			// set them both to zero.
+			tz = 0;
+			when = 0;
+			if (hasTz && !hasWhen) {
+				// Only one trailing numeric field; assume User ID ends before this
+				// field, but discard its value.
+				idEnd = tzBegin - 1;
+			} else {
+				// No trailing numeric fields; User ID is whole raw value.
+				idEnd = raw.length;
+			}
+		}
+		String id = new String(raw, 0, idEnd, UTF_8);
+
+		return new PushCertificateIdent(str, id, when * 1000L, tz);
+	}
+
+	private final String raw;
+	private final String userId;
+	private final long when;
+	private final int tzOffset;
+
+	/**
+	 * Construct a new identity from an OpenPGP User ID.
+	 *
+	 * @param userId
+	 *            OpenPGP User ID; any UTF-8 string.
+	 * @param when
+	 *            local time.
+	 * @param tzOffset
+	 *            timezone offset; see {@link #getTimeZoneOffset()}.
+	 */
+	public PushCertificateIdent(String userId, long when, int tzOffset) {
+		this.userId = userId;
+		this.when = when;
+		this.tzOffset = tzOffset;
+		StringBuilder sb = new StringBuilder(userId).append(' ').append(when / 1000)
+				.append(' ');
+		PersonIdent.appendTimezone(sb, tzOffset);
+		raw = sb.toString();
+	}
+
+	private PushCertificateIdent(String raw, String userId, long when,
+			int tzOffset) {
+		this.raw = raw;
+		this.userId = userId;
+		this.when = when;
+		this.tzOffset = tzOffset;
+	}
+
+	/**
+	 * Get the raw string from which this identity was parsed.
+	 * <p>
+	 * If the string was constructed manually, a suitable canonical string is
+	 * returned.
+	 * <p>
+	 * For the purposes of bytewise comparisons with other OpenPGP IDs, the string
+	 * must be encoded as UTF-8.
+	 *
+	 * @return the raw string.
+	 */
+	public String getRaw() {
+		return raw;
+	}
+
+	/** @return the OpenPGP User ID, which may be any string. */
+	public String getUserId() {
+		return userId;
+	}
+
+	/**
+	 * @return the name portion of the User ID. If no email address would be
+	 *         parsed by {@link #getEmailAddress()}, returns the full User ID with
+	 *         spaces trimmed.
+	 */
+	public String getName() {
+		int nameEnd = userId.indexOf('<');
+		if (nameEnd < 0 || userId.indexOf('>', nameEnd) < 0) {
+			nameEnd = userId.length();
+		}
+		nameEnd--;
+		while (nameEnd >= 0 && userId.charAt(nameEnd) == ' ') {
+			nameEnd--;
+		}
+		int nameBegin = 0;
+		while (nameBegin < nameEnd && userId.charAt(nameBegin) == ' ') {
+			nameBegin++;
+		}
+		return userId.substring(nameBegin, nameEnd + 1);
+	}
+
+	/**
+	 * @return the email portion of the User ID, if one was successfully parsed
+	 *         from {@link #getUserId()}, or null.
+	 */
+	public String getEmailAddress() {
+		int emailBegin = userId.indexOf('<');
+		if (emailBegin < 0) {
+			return null;
+		}
+		int emailEnd = userId.indexOf('>', emailBegin);
+		if (emailEnd < 0) {
+			return null;
+		}
+		return userId.substring(emailBegin + 1, emailEnd);
+	}
+
+	/** @return the timestamp of the identity. */
+	public Date getWhen() {
+		return new Date(when);
+	}
+
+	/**
+	 * @return this person's declared time zone; null if the timezone is unknown.
+	 */
+	public TimeZone getTimeZone() {
+		return PersonIdent.getTimeZone(tzOffset);
+	}
+
+	/**
+	 * @return this person's declared time zone as minutes east of UTC. If the
+	 *         timezone is to the west of UTC it is negative.
+	 */
+	public int getTimeZoneOffset() {
+		return tzOffset;
+	}
+
+	@Override
+	public boolean equals(Object o) {
+		return (o instanceof PushCertificateIdent)
+			&& raw.equals(((PushCertificateIdent) o).raw);
+	}
+
+	@Override
+	public int hashCode() {
+		return raw.hashCode();
+	}
+
+	@SuppressWarnings("nls")
+	@Override
+	public String toString() {
+		SimpleDateFormat fmt;
+		fmt = new SimpleDateFormat("EEE MMM d HH:mm:ss yyyy Z", Locale.US);
+		fmt.setTimeZone(getTimeZone());
+		return getClass().getSimpleName()
+			+ "[raw=\"" + raw + "\","
+			+ " userId=\"" + userId + "\","
+			+ " " + fmt.format(Long.valueOf(when)) + "]";
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateParser.java
index 4bb3d6b..5174f85 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateParser.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateParser.java
@@ -40,98 +40,305 @@
  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
+
 package org.eclipse.jgit.transport;
 
+import static org.eclipse.jgit.transport.BaseReceivePack.parseCommand;
 import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_PUSH_CERT;
 
 import java.io.EOFException;
 import java.io.IOException;
+import java.io.Reader;
 import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 import java.util.concurrent.TimeUnit;
 
+import org.eclipse.jgit.errors.PackProtocolException;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.transport.BaseReceivePack.ReceiveConfig;
+import org.eclipse.jgit.transport.PushCertificate.NonceStatus;
+import org.eclipse.jgit.util.IO;
 
 /**
- * Parser for Push certificates
+ * Parser for signed push certificates.
  *
  * @since 4.0
  */
-public class PushCertificateParser extends PushCertificate {
+public class PushCertificateParser {
+	static final String BEGIN_SIGNATURE =
+			"-----BEGIN PGP SIGNATURE-----"; //$NON-NLS-1$
+	static final String END_SIGNATURE =
+			"-----END PGP SIGNATURE-----"; //$NON-NLS-1$
 
-	private static final String VERSION = "version "; //$NON-NLS-1$
+	static final String VERSION = "certificate version"; //$NON-NLS-1$
 
-	private static final String PUSHER = "pusher"; //$NON-NLS-1$
+	static final String PUSHER = "pusher"; //$NON-NLS-1$
 
-	private static final String PUSHEE = "pushee"; //$NON-NLS-1$
+	static final String PUSHEE = "pushee"; //$NON-NLS-1$
 
-	private static final String NONCE = "nonce"; //$NON-NLS-1$
+	static final String NONCE = "nonce"; //$NON-NLS-1$
 
-	/** The individual certificate which is presented to the client */
+	static final String END_CERT = "push-cert-end"; //$NON-NLS-1$
+
+	private static final String VERSION_0_1 = "0.1"; //$NON-NLS-1$
+
+	private static interface StringReader {
+		/**
+		 * @return the next string from the input, up to an optional newline, with
+		 *         newline stripped if present
+		 *
+		 * @throws EOFException
+		 *             if EOF was reached.
+		 * @throws IOException
+		 *             if an error occurred during reading.
+		 */
+		String read() throws EOFException, IOException;
+	}
+
+	private static class PacketLineReader implements StringReader {
+		private final PacketLineIn pckIn;
+
+		private PacketLineReader(PacketLineIn pckIn) {
+			this.pckIn = pckIn;
+		}
+
+		@Override
+		public String read() throws IOException {
+			return pckIn.readString();
+		}
+	}
+
+	private static class StreamReader implements StringReader {
+		private final Reader reader;
+
+		private StreamReader(Reader reader) {
+			this.reader = reader;
+		}
+
+		@Override
+		public String read() throws IOException {
+			// Presize for a command containing 2 SHA-1s and some refname.
+			String line = IO.readLine(reader, 41 * 2 + 64);
+			if (line.isEmpty()) {
+				throw new EOFException();
+			} else if (line.charAt(line.length() - 1) == '\n') {
+				line = line.substring(0, line.length() - 1);
+			}
+			return line;
+		}
+	}
+
+	/**
+	 * Parse a push certificate from a reader.
+	 * <p>
+	 * Differences from the {@link PacketLineIn} receiver methods:
+	 * <ul>
+	 * <li>Does not use pkt-line framing.</li>
+	 * <li>Reads an entire cert in one call rather than depending on a loop in
+	 *   the caller.</li>
+	 * <li>Does not assume a {@code "push-cert-end"} line.</li>
+	 * </ul>
+	 *
+	 * @param r
+	 *            input reader; consumed only up until the end of the next
+	 *            signature in the input.
+	 * @return the parsed certificate, or null if the reader was at EOF.
+	 * @throws PackProtocolException
+	 *             if the certificate is malformed.
+	 * @throws IOException
+	 *             if there was an error reading from the input.
+	 * @since 4.1
+	 */
+	public static PushCertificate fromReader(Reader r)
+			throws PackProtocolException, IOException {
+		return new PushCertificateParser().parse(r);
+	}
+
+	/**
+	 * Parse a push certificate from a string.
+	 *
+	 * @see #fromReader(Reader)
+	 * @param str
+	 *            input string.
+	 * @return the parsed certificate.
+	 * @throws PackProtocolException
+	 *             if the certificate is malformed.
+	 * @throws IOException
+	 *             if there was an error reading from the input.
+	 * @since 4.1
+	 */
+	public static PushCertificate fromString(String str)
+			throws PackProtocolException, IOException {
+		return fromReader(new java.io.StringReader(str));
+	}
+
+	private boolean received;
+	private String version;
+	private PushCertificateIdent pusher;
+	private String pushee;
+
+	/** The nonce that was sent to the client. */
 	private String sentNonce;
 
 	/**
-	 * The nonce the pusher signed. This may vary from pushCertNonce See
-	 * git-core documentation for reasons.
+	 * The nonce the pusher signed.
+	 * <p>
+	 * This may vary from {@link #sentNonce}; see git-core documentation for
+	 * reasons.
 	 */
 	private String receivedNonce;
 
+	private NonceStatus nonceStatus;
+	private String signature;
+
+	/** Database we write the push certificate into. */
+	private final Repository db;
+
 	/**
 	 * The maximum time difference which is acceptable between advertised nonce
 	 * and received signed nonce.
 	 */
-	private int nonceSlopLimit;
+	private final int nonceSlopLimit;
 
-	NonceGenerator nonceGenerator;
+	private final boolean enabled;
+	private final NonceGenerator nonceGenerator;
+	private final List<ReceiveCommand> commands = new ArrayList<>();
 
 	/**
-	 * used to build up commandlist
+	 * @param into
+	 *            destination repository for the push.
+	 * @param cfg
+	 *            configuration for signed push.
+	 * @since 4.1
 	 */
-	StringBuilder commandlistBuilder;
-
-	/** Database we write the push certificate into. */
-	private Repository db;
-
-	PushCertificateParser(Repository into, ReceiveConfig cfg) {
-		nonceSlopLimit = cfg.certNonceSlopLimit;
-		nonceGenerator = cfg.certNonceSeed != null
-				? new HMACSHA1NonceGenerator(cfg.certNonceSeed)
-				: null;
+	public PushCertificateParser(Repository into, SignedPushConfig cfg) {
+		if (cfg != null) {
+			nonceSlopLimit = cfg.getCertNonceSlopLimit();
+			nonceGenerator = cfg.getNonceGenerator();
+		} else {
+			nonceSlopLimit = 0;
+			nonceGenerator = null;
+		}
 		db = into;
+		enabled = nonceGenerator != null;
+	}
+
+	private PushCertificateParser() {
+		db = null;
+		nonceSlopLimit = 0;
+		nonceGenerator = null;
+		enabled = true;
 	}
 
 	/**
-	 * @return if the server is configured to use signed pushes.
+	 * Parse a push certificate from a reader.
+	 *
+	 * @see #fromReader(Reader)
+	 * @param r
+	 *            input reader; consumed only up until the end of the next
+	 *            signature in the input.
+	 * @return the parsed certificate, or null if the reader was at EOF.
+	 * @throws PackProtocolException
+	 *             if the certificate is malformed.
+	 * @throws IOException
+	 *             if there was an error reading from the input.
+	 * @since 4.1
+	 */
+	public PushCertificate parse(Reader r)
+			throws PackProtocolException, IOException {
+		StreamReader reader = new StreamReader(r);
+		receiveHeader(reader, true);
+		String line;
+		try {
+			while (!(line = reader.read()).isEmpty()) {
+				if (line.equals(BEGIN_SIGNATURE)) {
+					receiveSignature(reader);
+					break;
+				}
+				addCommand(line);
+			}
+		} catch (EOFException e) {
+			// EOF reached, but might have been at a valid state. Let build call below
+			// sort it out.
+		}
+		return build();
+	}
+
+	/**
+	 * @return the parsed certificate, or null if push certificates are disabled.
+	 * @throws IOException
+	 *             if the push certificate has missing or invalid fields.
+	 * @since 4.1
+	 */
+	public PushCertificate build() throws IOException {
+		if (!received || !enabled) {
+			return null;
+		}
+		try {
+			return new PushCertificate(version, pusher, pushee, receivedNonce,
+					nonceStatus, Collections.unmodifiableList(commands), signature);
+		} catch (IllegalArgumentException e) {
+			throw new IOException(e.getMessage(), e);
+		}
+	}
+
+	/**
+	 * @return if the repository is configured to use signed pushes in this
+	 *         context.
+	 * @since 4.0
 	 */
 	public boolean enabled() {
-		return nonceGenerator != null;
+		return enabled;
 	}
 
 	/**
 	 * @return the whole string for the nonce to be included into the capability
-	 *         advertisement.
+	 *         advertisement, or null if push certificates are disabled.
+	 * @since 4.0
 	 */
 	public String getAdvertiseNonce() {
-		sentNonce = nonceGenerator.createNonce(db,
-				TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()));
-		return CAPABILITY_PUSH_CERT + "=" + sentNonce; //$NON-NLS-1$
+		String nonce = sentNonce();
+		if (nonce == null) {
+			return null;
+		}
+		return CAPABILITY_PUSH_CERT + '=' + nonce;
 	}
 
-	private String parseNextLine(PacketLineIn pckIn, String startingWith)
+	private String sentNonce() {
+		if (sentNonce == null && nonceGenerator != null) {
+			sentNonce = nonceGenerator.createNonce(db,
+					TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()));
+		}
+		return sentNonce;
+	}
+
+	private static String parseHeader(StringReader reader, String header)
 			throws IOException {
-		String s = pckIn.readString();
-		if (!s.startsWith(startingWith))
-			throw new IOException(MessageFormat.format(
-					JGitText.get().errorInvalidPushCert,
-					"expected " + startingWith)); //$NON-NLS-1$
-		return s.substring(startingWith.length());
+		return parseHeader(reader.read(), header);
+	}
+
+	private static String parseHeader(String s, String header)
+			throws IOException {
+		if (s.isEmpty()) {
+			throw new EOFException();
+		}
+		if (s.length() <= header.length()
+				|| !s.startsWith(header)
+				|| s.charAt(header.length()) != ' ') {
+			throw new PackProtocolException(MessageFormat.format(
+					JGitText.get().pushCertificateInvalidField, header));
+		}
+		return s.substring(header.length() + 1);
 	}
 
 	/**
 	 * Receive a list of commands from the input encapsulated in a push
-	 * certificate. This method doesn't parse the first line "push-cert \NUL
-	 * &lt;capabilities&gt;", but assumes the first line including the
+	 * certificate.
+	 * <p>
+	 * This method doesn't parse the first line {@code "push-cert \NUL
+	 * &lt;capabilities&gt;"}, but assumes the first line including the
 	 * capabilities has already been handled by the caller.
 	 *
 	 * @param pckIn
@@ -144,62 +351,115 @@
 	 * @throws IOException
 	 *             if the certificate from the client is badly malformed or the
 	 *             client disconnects before sending the entire certificate.
+	 * @since 4.0
 	 */
 	public void receiveHeader(PacketLineIn pckIn, boolean stateless)
 			throws IOException {
+		receiveHeader(new PacketLineReader(pckIn), stateless);
+	}
+
+	private void receiveHeader(StringReader reader, boolean stateless)
+			throws IOException {
 		try {
-			String version = parseNextLine(pckIn, VERSION);
-			if (!version.equals("0.1")) { //$NON-NLS-1$
-				throw new IOException(MessageFormat.format(
-						JGitText.get().errorInvalidPushCert,
-						"version not supported")); //$NON-NLS-1$
+			try {
+				version = parseHeader(reader, VERSION);
+			} catch (EOFException e) {
+				return;
 			}
-			pusher = parseNextLine(pckIn, PUSHER);
-			pushee = parseNextLine(pckIn, PUSHEE);
-			receivedNonce = parseNextLine(pckIn, NONCE);
-			// an empty line
-			if (!pckIn.readString().isEmpty()) {
-				throw new IOException(MessageFormat.format(
-						JGitText.get().errorInvalidPushCert,
-						"expected empty line after header")); //$NON-NLS-1$
+			received = true;
+			if (!version.equals(VERSION_0_1)) {
+				throw new PackProtocolException(MessageFormat.format(
+						JGitText.get().pushCertificateInvalidFieldValue, VERSION, version));
+			}
+			String rawPusher = parseHeader(reader, PUSHER);
+			pusher = PushCertificateIdent.parse(rawPusher);
+			if (pusher == null) {
+				throw new PackProtocolException(MessageFormat.format(
+						JGitText.get().pushCertificateInvalidFieldValue,
+						PUSHER, rawPusher));
+			}
+			String next = reader.read();
+			if (next.startsWith(PUSHEE)) {
+				pushee = parseHeader(next, PUSHEE);
+				receivedNonce = parseHeader(reader, NONCE);
+			} else {
+				receivedNonce = parseHeader(next, NONCE);
+			}
+			nonceStatus = nonceGenerator != null
+					? nonceGenerator.verify(
+						receivedNonce, sentNonce(), db, stateless, nonceSlopLimit)
+					: NonceStatus.UNSOLICITED;
+			// An empty line.
+			if (!reader.read().isEmpty()) {
+				throw new PackProtocolException(
+						JGitText.get().pushCertificateInvalidHeader);
 			}
 		} catch (EOFException eof) {
-			throw new IOException(MessageFormat.format(
-					JGitText.get().errorInvalidPushCert,
-					"broken push certificate header")); //$NON-NLS-1$
+			throw new PackProtocolException(
+					JGitText.get().pushCertificateInvalidHeader, eof);
 		}
-		nonceStatus = nonceGenerator.verify(receivedNonce, sentNonce, db,
-				stateless, nonceSlopLimit);
 	}
 
 	/**
-	 * Reads the gpg signature. This method assumes the line "-----BEGIN PGP
-	 * SIGNATURE-----\n" has already been parsed and continues parsing until an
-	 * "-----END PGP SIGNATURE-----\n" is found.
+	 * Read the PGP signature.
+	 * <p>
+	 * This method assumes the line
+	 * {@code "-----BEGIN PGP SIGNATURE-----"} has already been parsed,
+	 * and continues parsing until an {@code "-----END PGP SIGNATURE-----"} is
+	 * found, followed by {@code "push-cert-end"}.
 	 *
 	 * @param pckIn
 	 *            where we read the signature from.
 	 * @throws IOException
+	 *             if the signature is invalid.
+	 * @since 4.0
 	 */
 	public void receiveSignature(PacketLineIn pckIn) throws IOException {
+		StringReader reader = new PacketLineReader(pckIn);
+		receiveSignature(reader);
+		if (!reader.read().equals(END_CERT)) {
+			throw new PackProtocolException(
+					JGitText.get().pushCertificateInvalidSignature);
+		}
+	}
+
+	private void receiveSignature(StringReader reader) throws IOException {
+		received = true;
 		try {
-			StringBuilder sig = new StringBuilder();
-			String line = pckIn.readStringRaw();
-			while (!line.equals("-----END PGP SIGNATURE-----\n")) //$NON-NLS-1$
-				sig.append(line);
-			signature = sig.toString();
-			commandList = commandlistBuilder.toString();
+			StringBuilder sig = new StringBuilder(BEGIN_SIGNATURE).append('\n');
+			String line;
+			while (!(line = reader.read()).equals(END_SIGNATURE)) {
+				sig.append(line).append('\n');
+			}
+			signature = sig.append(END_SIGNATURE).append('\n').toString();
 		} catch (EOFException eof) {
-			throw new IOException(MessageFormat.format(
-					JGitText.get().errorInvalidPushCert,
-					"broken push certificate signature")); //$NON-NLS-1$
+			throw new PackProtocolException(
+					JGitText.get().pushCertificateInvalidSignature, eof);
 		}
 	}
 
 	/**
-	 * @param rawLine
+	 * Add a command to the signature.
+	 *
+	 * @param cmd
+	 *            the command.
+	 * @since 4.1
 	 */
-	public void addCommand(String rawLine) {
-		commandlistBuilder.append(rawLine);
+	public void addCommand(ReceiveCommand cmd) {
+		commands.add(cmd);
+	}
+
+	/**
+	 * Add a command to the signature.
+	 *
+	 * @param line
+	 *            the line read from the wire that produced this
+	 *            command, with optional trailing newline already trimmed.
+	 * @throws PackProtocolException
+	 *             if the raw line cannot be parsed to a command.
+	 * @since 4.0
+	 */
+	public void addCommand(String line) throws PackProtocolException {
+		commands.add(parseCommand(line));
 	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateStore.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateStore.java
new file mode 100644
index 0000000..d8672d5
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushCertificateStore.java
@@ -0,0 +1,552 @@
+/*
+ * Copyright (C) 2015, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.transport;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT;
+import static org.eclipse.jgit.lib.FileMode.TYPE_FILE;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+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.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+
+import org.eclipse.jgit.dircache.DirCache;
+import org.eclipse.jgit.dircache.DirCacheBuilder;
+import org.eclipse.jgit.dircache.DirCacheEditor;
+import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
+import org.eclipse.jgit.dircache.DirCacheEntry;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
+import org.eclipse.jgit.treewalk.filter.PathFilter;
+import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
+import org.eclipse.jgit.treewalk.filter.TreeFilter;
+
+/**
+ * Storage for recorded push certificates.
+ * <p>
+ * Push certificates are stored in a special ref {@code refs/meta/push-certs}.
+ * The filenames in the tree are ref names followed by the special suffix
+ * <code>@{cert}</code>, and the contents are the latest push cert affecting
+ * that ref. The special suffix allows storing certificates for both refs/foo
+ * and refs/foo/bar in case those both existed at some point.
+ *
+ * @since 4.1
+ */
+public class PushCertificateStore implements AutoCloseable {
+	/** Ref name storing push certificates. */
+	static final String REF_NAME =
+			Constants.R_REFS + "meta/push-certs"; //$NON-NLS-1$
+
+	private static class PendingCert {
+		private PushCertificate cert;
+		private PersonIdent ident;
+		private Collection<ReceiveCommand> matching;
+
+		private PendingCert(PushCertificate cert, PersonIdent ident,
+				Collection<ReceiveCommand> matching) {
+			this.cert = cert;
+			this.ident = ident;
+			this.matching = matching;
+		}
+	}
+
+	private final Repository db;
+	private final List<PendingCert> pending;
+	private ObjectReader reader;
+	private RevCommit commit;
+
+	/**
+	 * Create a new store backed by the given repository.
+	 *
+	 * @param db
+	 *            the repository.
+	 */
+	public PushCertificateStore(Repository db) {
+		this.db = db;
+		pending = new ArrayList<>();
+	}
+
+	/**
+	 * Close resources opened by this store.
+	 * <p>
+	 * If {@link #get(String)} was called, closes the cached object reader created
+	 * by that method. Does not close the underlying repository.
+	 */
+	public void close() {
+		if (reader != null) {
+			reader.close();
+			reader = null;
+			commit = null;
+		}
+	}
+
+	/**
+	 * Get latest push certificate associated with a ref.
+	 * <p>
+	 * Lazily opens {@code refs/meta/push-certs} and reads from the repository as
+	 * necessary. The state is cached between calls to {@code get}; to reread the,
+	 * call {@link #close()} first.
+	 *
+	 * @param refName
+	 *            the ref name to get the certificate for.
+	 * @return last certificate affecting the ref, or null if no cert was recorded
+	 *         for the last update to this ref.
+	 * @throws IOException
+	 *             if a problem occurred reading the repository.
+	 */
+	public PushCertificate get(String refName) throws IOException {
+		if (reader == null) {
+			load();
+		}
+		try (TreeWalk tw = newTreeWalk(refName)) {
+			return read(tw);
+		}
+	}
+
+	/**
+	 * Iterate over all push certificates affecting a ref.
+	 * <p>
+	 * Only includes push certificates actually stored in the tree; see class
+	 * Javadoc for conditions where this might not include all push certs ever
+	 * seen for this ref.
+	 * <p>
+	 * The returned iterable may be iterated multiple times, and push certs will
+	 * be re-read from the current state of the store on each call to {@link
+	 * Iterable#iterator()}. However, method calls on the returned iterator may
+	 * fail if {@code save} or {@code close} is called on the enclosing store
+	 * during iteration.
+	 *
+	 * @param refName
+	 *            the ref name to get certificates for.
+	 * @return iterable over certificates; must be fully iterated in order to
+	 *         close resources.
+	 */
+	public Iterable<PushCertificate> getAll(final String refName) {
+		return new Iterable<PushCertificate>() {
+			@Override
+			public Iterator<PushCertificate> iterator() {
+				return new Iterator<PushCertificate>() {
+					private final String path = pathName(refName);
+					private PushCertificate next;
+
+					private RevWalk rw;
+					{
+						try {
+							if (reader == null) {
+								load();
+							}
+							if (commit != null) {
+								rw = new RevWalk(reader);
+								rw.setTreeFilter(AndTreeFilter.create(
+										PathFilterGroup.create(
+											Collections.singleton(PathFilter.create(path))),
+										TreeFilter.ANY_DIFF));
+								rw.setRewriteParents(false);
+								rw.markStart(rw.parseCommit(commit));
+							} else {
+								rw = null;
+							}
+						} catch (IOException e) {
+							throw new RuntimeException(e);
+						}
+					}
+
+					@Override
+					public boolean hasNext() {
+						try {
+							if (next == null) {
+								if (rw == null) {
+									return false;
+								}
+								try {
+									RevCommit c = rw.next();
+									if (c != null) {
+										try (TreeWalk tw = TreeWalk.forPath(
+												rw.getObjectReader(), path, c.getTree())) {
+											next = read(tw);
+										}
+									} else {
+										next = null;
+									}
+								} catch (IOException e) {
+									throw new RuntimeException(e);
+								}
+							}
+							return next != null;
+						} finally {
+							if (next == null && rw != null) {
+								rw.close();
+								rw = null;
+							}
+						}
+					}
+
+					@Override
+					public PushCertificate next() {
+						hasNext();
+						PushCertificate n = next;
+						if (n == null) {
+							throw new NoSuchElementException();
+						}
+						next = null;
+						return n;
+					}
+
+					@Override
+					public void remove() {
+						throw new UnsupportedOperationException();
+					}
+				};
+			}
+		};
+	}
+
+	private void load() throws IOException {
+		close();
+		reader = db.newObjectReader();
+		Ref ref = db.getRefDatabase().exactRef(REF_NAME);
+		if (ref == null) {
+			// No ref, same as empty.
+			return;
+		}
+		try (RevWalk rw = new RevWalk(reader)) {
+			commit = rw.parseCommit(ref.getObjectId());
+		}
+	}
+
+	private static PushCertificate read(TreeWalk tw) throws IOException {
+		if (tw == null || (tw.getRawMode(0) & TYPE_FILE) != TYPE_FILE) {
+			return null;
+		}
+		ObjectLoader loader =
+				tw.getObjectReader().open(tw.getObjectId(0), OBJ_BLOB);
+		try (InputStream in = loader.openStream();
+				Reader r = new BufferedReader(new InputStreamReader(in, UTF_8))) {
+			return PushCertificateParser.fromReader(r);
+		}
+	}
+
+	/**
+	 * Put a certificate to be saved to the store.
+	 * <p>
+	 * Writes the contents of this certificate for each ref mentioned. It is up to
+	 * the caller to ensure this certificate accurately represents the state of
+	 * the ref.
+	 * <p>
+	 * Pending certificates added to this method are not returned by {@link
+	 * #get(String)} and {@link #getAll(String)} until after calling {@link
+	 * #save()}.
+	 *
+	 * @param cert
+	 *            certificate to store.
+	 * @param ident
+	 *            identity for the commit that stores this certificate. Pending
+	 *            certificates are sorted by identity timestamp during {@link
+	 *            #save()}.
+	 */
+	public void put(PushCertificate cert, PersonIdent ident) {
+		put(cert, ident, null);
+	}
+
+	/**
+	 * Put a certificate to be saved to the store, matching a set of commands.
+	 * <p>
+	 * Like {@link #put(PushCertificate, PersonIdent)}, except a value is only
+	 * stored for a push certificate if there is a corresponding command in the
+	 * list that exactly matches the old/new values mentioned in the push
+	 * certificate.
+	 * <p>
+	 * Pending certificates added to this method are not returned by {@link
+	 * #get(String)} and {@link #getAll(String)} until after calling {@link
+	 * #save()}.
+	 *
+	 * @param cert
+	 *            certificate to store.
+	 * @param ident
+	 *            identity for the commit that stores this certificate. Pending
+	 *            certificates are sorted by identity timestamp during {@link
+	 *            #save()}.
+	 * @param matching
+	 *            only store certs for the refs listed in this list whose values
+	 *            match the commands in the cert.
+	 */
+	public void put(PushCertificate cert, PersonIdent ident,
+			Collection<ReceiveCommand> matching) {
+		pending.add(new PendingCert(cert, ident, matching));
+	}
+
+	/**
+	 * Save pending certificates to the store.
+	 * <p>
+	 * One commit is created per certificate added with {@link
+	 * #put(PushCertificate, PersonIdent)}, in order of identity timestamps, and
+	 * a single ref update is performed.
+	 * <p>
+	 * The pending list is cleared if and only the ref update fails, which allows
+	 * for easy retries in case of lock failure.
+	 *
+	 * @return the result of attempting to update the ref.
+	 * @throws IOException
+	 *             if there was an error reading from or writing to the
+	 *             repository.
+	 */
+	public RefUpdate.Result save() throws IOException {
+		ObjectId newId = write();
+		if (newId == null) {
+			return RefUpdate.Result.NO_CHANGE;
+		}
+		try (ObjectInserter inserter = db.newObjectInserter()) {
+			RefUpdate.Result result = updateRef(newId);
+			switch (result) {
+				case FAST_FORWARD:
+				case NEW:
+				case NO_CHANGE:
+					pending.clear();
+					break;
+				default:
+					break;
+			}
+			return result;
+		} finally {
+			close();
+		}
+	}
+
+	/**
+	 * Save pending certificates to the store in an existing batch ref update.
+	 * <p>
+	 * One commit is created per certificate added with {@link
+	 * #put(PushCertificate, PersonIdent)}, in order of identity timestamps, all
+	 * commits are flushed, and a single command is added to the batch.
+	 * <p>
+	 * The cached ref value and pending list are <em>not</em> cleared. If the ref
+	 * update succeeds, the caller is responsible for calling {@link #close()}
+	 * and/or {@link #clear()}.
+	 *
+	 * @param batch
+	 *            update to save to.
+	 * @return whether a command was added to the batch.
+	 * @throws IOException
+	 *             if there was an error reading from or writing to the
+	 *             repository.
+	 */
+	public boolean save(BatchRefUpdate batch) throws IOException {
+		ObjectId newId = write();
+		if (newId == null || newId.equals(commit)) {
+			return false;
+		}
+		batch.addCommand(new ReceiveCommand(
+				commit != null ? commit : ObjectId.zeroId(), newId, REF_NAME));
+		return true;
+	}
+
+	/**
+	 * Clear pending certificates added with {@link #put(PushCertificate,
+	 * PersonIdent)}.
+	 */
+	public void clear() {
+		pending.clear();
+	}
+
+	private ObjectId write() throws IOException {
+		if (pending.isEmpty()) {
+			return null;
+		}
+		if (reader == null) {
+			load();
+		}
+		sortPending(pending);
+
+		ObjectId curr = commit;
+		DirCache dc = newDirCache();
+		try (ObjectInserter inserter = db.newObjectInserter()) {
+			for (PendingCert pc : pending) {
+				curr = saveCert(inserter, dc, pc, curr);
+			}
+			inserter.flush();
+			return curr;
+		}
+	}
+
+	private static void sortPending(List<PendingCert> pending) {
+		Collections.sort(pending, new Comparator<PendingCert>() {
+			@Override
+			public int compare(PendingCert a, PendingCert b) {
+				return Long.signum(
+						a.ident.getWhen().getTime() - b.ident.getWhen().getTime());
+			}
+		});
+	}
+
+	private DirCache newDirCache() throws IOException {
+		DirCache dc = DirCache.newInCore();
+		if (commit != null) {
+			DirCacheBuilder b = dc.builder();
+			b.addTree(new byte[0], DirCacheEntry.STAGE_0, reader, commit.getTree());
+			b.finish();
+		}
+		return dc;
+	}
+
+	private ObjectId saveCert(ObjectInserter inserter, DirCache dc,
+			PendingCert pc, ObjectId curr) throws IOException {
+		Map<String, ReceiveCommand> byRef;
+		if (pc.matching != null) {
+			byRef = new HashMap<>();
+			for (ReceiveCommand cmd : pc.matching) {
+				if (byRef.put(cmd.getRefName(), cmd) != null) {
+					throw new IllegalStateException();
+				}
+			}
+		} else {
+			byRef = null;
+		}
+
+		DirCacheEditor editor = dc.editor();
+		String certText = pc.cert.toText() + pc.cert.getSignature();
+		final ObjectId certId = inserter.insert(OBJ_BLOB, certText.getBytes(UTF_8));
+		boolean any = false;
+		for (ReceiveCommand cmd : pc.cert.getCommands()) {
+			if (byRef != null && !commandsEqual(cmd, byRef.get(cmd.getRefName()))) {
+				continue;
+			}
+			any = true;
+			editor.add(new PathEdit(pathName(cmd.getRefName())) {
+				@Override
+				public void apply(DirCacheEntry ent) {
+					ent.setFileMode(FileMode.REGULAR_FILE);
+					ent.setObjectId(certId);
+				}
+			});
+		}
+		if (!any) {
+			return curr;
+		}
+		editor.finish();
+		CommitBuilder cb = new CommitBuilder();
+		cb.setAuthor(pc.ident);
+		cb.setCommitter(pc.ident);
+		cb.setTreeId(dc.writeTree(inserter));
+		if (curr != null) {
+			cb.setParentId(curr);
+		} else {
+			cb.setParentIds(Collections.<ObjectId> emptyList());
+		}
+		cb.setMessage(buildMessage(pc.cert));
+		return inserter.insert(OBJ_COMMIT, cb.build());
+	}
+
+	private static boolean commandsEqual(ReceiveCommand c1, ReceiveCommand c2) {
+		if (c1 == null || c2 == null) {
+			return c1 == c2;
+		}
+		return c1.getRefName().equals(c2.getRefName())
+				&& c1.getOldId().equals(c2.getOldId())
+				&& c1.getNewId().equals(c2.getNewId());
+	}
+
+	private RefUpdate.Result updateRef(ObjectId newId) throws IOException {
+		RefUpdate ru = db.updateRef(REF_NAME);
+		ru.setExpectedOldObjectId(commit != null ? commit : ObjectId.zeroId());
+		ru.setNewObjectId(newId);
+		ru.setRefLogIdent(pending.get(pending.size() - 1).ident);
+		ru.setRefLogMessage(JGitText.get().storePushCertReflog, false);
+		try (RevWalk rw = new RevWalk(reader)) {
+			return ru.update(rw);
+		}
+	}
+
+	private TreeWalk newTreeWalk(String refName) throws IOException {
+		if (commit == null) {
+			return null;
+		}
+		return TreeWalk.forPath(reader, pathName(refName), commit.getTree());
+	}
+
+	private static String pathName(String refName) {
+		return refName + "@{cert}"; //$NON-NLS-1$
+	}
+
+	private static String buildMessage(PushCertificate cert) {
+		StringBuilder sb = new StringBuilder();
+		if (cert.getCommands().size() == 1) {
+			sb.append(MessageFormat.format(
+					JGitText.get().storePushCertOneRef,
+					cert.getCommands().get(0).getRefName()));
+		} else {
+			sb.append(MessageFormat.format(
+					JGitText.get().storePushCertMultipleRefs,
+					Integer.valueOf(cert.getCommands().size())));
+		}
+		return sb.append('\n').toString();
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java
index 44b8778..6ed9bc8 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/ReceivePack.java
@@ -246,6 +246,7 @@
 				try {
 					postReceive.onPostReceive(this, filterCommands(Result.OK));
 				} catch (Throwable e) {
+					// empty
 				}
 				throw new UnpackException(unpackError);
 			}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SignedPushConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SignedPushConfig.java
new file mode 100644
index 0000000..942e7d7
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SignedPushConfig.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2015, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.transport;
+
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Config.SectionParser;
+
+/**
+ * Configuration for server-side signed push verification.
+ *
+ * @since 4.1
+ */
+public class SignedPushConfig {
+	/** Key for {@link Config#get(SectionParser)}. */
+	public static final SectionParser<SignedPushConfig> KEY =
+			new SectionParser<SignedPushConfig>() {
+		public SignedPushConfig parse(Config cfg) {
+			return new SignedPushConfig(cfg);
+		}
+	};
+
+	private String certNonceSeed;
+	private int certNonceSlopLimit;
+	private NonceGenerator nonceGenerator;
+
+	/** Create a new config with default values disabling push verification. */
+	public SignedPushConfig() {
+	}
+
+	SignedPushConfig(Config cfg) {
+		setCertNonceSeed(cfg.getString("receive", null, "certnonceseed")); //$NON-NLS-1$ //$NON-NLS-2$
+		certNonceSlopLimit = cfg.getInt("receive", "certnonceslop", 0); //$NON-NLS-1$ //$NON-NLS-2$
+	}
+
+	/**
+	 * Set the seed used by the nonce verifier.
+	 * <p>
+	 * Setting this to a non-null value enables push certificate verification
+	 * using the default {@link HMACSHA1NonceGenerator} implementation, if a
+	 * different implementation was not set using {@link
+	 * #setNonceGenerator(NonceGenerator)}.
+	 *
+	 * @param seed
+	 *            new seed value.
+	 */
+	public void setCertNonceSeed(String seed) {
+		certNonceSeed = seed;
+	}
+
+	/** @return the configured seed. */
+	public String getCertNonceSeed() {
+		return certNonceSeed;
+	}
+
+	/**
+	 * Set the nonce slop limit.
+	 * <p>
+	 * Old but valid nonces within this limit will be accepted.
+	 *
+	 * @param limit
+	 *            new limit in seconds.
+	 */
+	public void setCertNonceSlopLimit(int limit) {
+		certNonceSlopLimit = limit;
+	}
+
+	/** @return the configured nonce slop limit. */
+	public int getCertNonceSlopLimit() {
+		return certNonceSlopLimit;
+	}
+
+	/**
+	 * Set the {@link NonceGenerator} used for signed pushes.
+	 * <p>
+	 * Setting this to a non-null value enables push certificate verification. If
+	 * this method is called, this implementation will be used instead of the
+	 * default {@link HMACSHA1NonceGenerator} even if {@link
+	 * #setCertNonceSeed(String)} was called.
+	 *
+	 * @param generator
+	 *            new nonce generator.
+	 */
+	public void setNonceGenerator(NonceGenerator generator) {
+		nonceGenerator = generator;
+	}
+
+	/**
+	 * Get the {@link NonceGenerator} used for signed pushes.
+	 * <p>
+	 * If {@link #setNonceGenerator(NonceGenerator)} was used to set a non-null
+	 * implementation, that will be returned. If no custom implementation was set
+	 * but {@link #setCertNonceSeed(String)} was called, returns a newly-created
+	 * {@link HMACSHA1NonceGenerator}.
+	 *
+	 * @return the configured nonce generator.
+	 */
+	public NonceGenerator getNonceGenerator() {
+		if (nonceGenerator != null) {
+			return nonceGenerator;
+		} else if (certNonceSeed != null) {
+			return new HMACSHA1NonceGenerator(certNonceSeed);
+		}
+		return null;
+	}
+}
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 138002e..f4de821 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java
@@ -71,6 +71,7 @@
 	private final boolean safeForWindows;
 	private final boolean safeForMacOS;
 	private final boolean allowTipSha1InWant;
+	private final boolean allowReachableSha1InWant;
 	private final String[] hideRefs;
 
 	TransferConfig(final Repository db) {
@@ -94,6 +95,8 @@
 
 		allowTipSha1InWant = rc.getBoolean(
 				"uploadpack", "allowtipsha1inwant", false); //$NON-NLS-1$ //$NON-NLS-2$
+		allowReachableSha1InWant = rc.getBoolean(
+				"uploadpack", "allowreachablesha1inwant", false); //$NON-NLS-1$ //$NON-NLS-2$
 		hideRefs = rc.getStringList("uploadpack", null, "hiderefs"); //$NON-NLS-1$ //$NON-NLS-2$
 	}
 
@@ -121,6 +124,14 @@
 	}
 
 	/**
+	 * @return allow clients to request non-tip SHA-1s?
+	 * @since 4.1
+	 */
+	public boolean isAllowReachableSha1InWant() {
+		return allowReachableSha1InWant;
+	}
+
+	/**
 	 * @return {@link RefFilter} respecting configured hidden refs.
 	 * @since 3.1
 	 */
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportAmazonS3.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportAmazonS3.java
index afaaa69..745cdb7 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportAmazonS3.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportAmazonS3.java
@@ -148,7 +148,7 @@
 		super(local, uri);
 
 		Properties props = loadProperties();
-		if (!props.contains("tmpdir") && local.getDirectory() != null) //$NON-NLS-1$
+		if (!props.containsKey("tmpdir") && local.getDirectory() != null) //$NON-NLS-1$
 			props.put("tmpdir", local.getDirectory().getPath()); //$NON-NLS-1$
 
 		s3 = new AmazonS3(props);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java
index 91e212b..3700b49 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/URIish.java
@@ -4,6 +4,7 @@
  * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
  * Copyright (C) 2010, Christian Halstrick <christian.halstrick@sap.com>
  * Copyright (C) 2013, Robin Stocker <robin@nibor.org>
+ * Copyright (C) 2015, Patrick Steinhardt <ps@pks.im>
  * and other copyright owners as documented in the project's IP log.
  *
  * This program and the accompanying materials are made available
@@ -134,11 +135,11 @@
 			+ OPT_USER_PWD_P //
 			+ HOST_P //
 			+ OPT_PORT_P //
-			+ "(" // open a catpuring group the the user-home-dir part //$NON-NLS-1$
-			+ (USER_HOME_P + "?") // //$NON-NLS-1$
-			+ "[\\\\/])" // //$NON-NLS-1$
+			+ "(" // open a group capturing the user-home-dir-part //$NON-NLS-1$
+			+ (USER_HOME_P + "?") //$NON-NLS-1$
+			+ "[\\\\/])" //$NON-NLS-1$
 			+ ")?" // close the optional group containing hostname //$NON-NLS-1$
-			+ "(.+)?" // //$NON-NLS-1$
+			+ "(.+)?" //$NON-NLS-1$
 			+ "$"); //$NON-NLS-1$
 
 	/**
@@ -690,6 +691,10 @@
 	 * <td><code>/path/to/repo/</code></td>
 	 * </tr>
 	 * <tr>
+	 * <td><code>localhost</code></td>
+	 * <td><code>ssh://localhost/</code></td>
+	 * </tr>
+	 * <tr>
 	 * <td><code>/path//to</code></td>
 	 * <td>an empty string</td>
 	 * </tr>
@@ -703,9 +708,12 @@
 	 * @see #getPath
 	 */
 	public String getHumanishName() throws IllegalArgumentException {
-		if ("".equals(getPath()) || getPath() == null) //$NON-NLS-1$
-			throw new IllegalArgumentException();
 		String s = getPath();
+		if ("/".equals(s)) //$NON-NLS-1$
+			s = getHost();
+		if ("".equals(s) || s == null) //$NON-NLS-1$
+			throw new IllegalArgumentException();
+
 		String[] elements;
 		if ("file".equals(scheme) || LOCAL_FILE.matcher(s).matches()) //$NON-NLS-1$
 			elements = s.split("[\\" + File.separatorChar + "/]"); //$NON-NLS-1$ //$NON-NLS-2$
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 753277d..101057f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java
@@ -46,6 +46,7 @@
 import static org.eclipse.jgit.lib.RefDatabase.ALL;
 import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_AGENT;
 import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_ALLOW_TIP_SHA1_IN_WANT;
+import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_ALLOW_REACHABLE_SHA1_IN_WANT;
 import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_INCLUDE_TAG;
 import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_MULTI_ACK;
 import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_MULTI_ACK_DETAILED;
@@ -93,6 +94,7 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.revwalk.filter.CommitTimeRevFilter;
 import org.eclipse.jgit.storage.pack.PackConfig;
+import org.eclipse.jgit.storage.pack.PackStatistics;
 import org.eclipse.jgit.transport.GitProtocolConstants.MultiAck;
 import org.eclipse.jgit.transport.RefAdvertiser.PacketLineOutRefAdvertiser;
 import org.eclipse.jgit.util.io.InterruptTimer;
@@ -252,6 +254,9 @@
 	/** Hook handling the various upload phases. */
 	private PreUploadHook preUploadHook = PreUploadHook.NULL;
 
+	/** Hook for taking post upload actions. */
+	private PostUploadHook postUploadHook = PostUploadHook.NULL;
+
 	/** Capabilities requested by the client. */
 	private Set<String> options;
 	String userAgent;
@@ -305,7 +310,7 @@
 
 	private boolean noDone;
 
-	private PackWriter.Statistics statistics;
+	private PackStatistics statistics;
 
 	private UploadPackLogger logger = UploadPackLogger.NULL;
 
@@ -518,7 +523,7 @@
 		this.refFilter = refFilter != null ? refFilter : RefFilter.DEFAULT;
 	}
 
-	/** @return the configured upload hook. */
+	/** @return the configured pre upload hook. */
 	public PreUploadHook getPreUploadHook() {
 		return preUploadHook;
 	}
@@ -534,6 +539,25 @@
 	}
 
 	/**
+	 * @return the configured post upload hook.
+	 * @since 4.1
+	 */
+	public PostUploadHook getPostUploadHook() {
+		return postUploadHook;
+	}
+
+	/**
+	 * Set the hook for post upload actions (logging, repacking).
+	 *
+	 * @param hook
+	 *            the hook; if null no special actions are taken.
+	 * @since 4.1
+	 */
+	public void setPostUploadHook(PostUploadHook hook) {
+		postUploadHook = hook != null ? hook : PostUploadHook.NULL;
+	}
+
+	/**
 	 * Set the configuration used by the pack generator.
 	 *
 	 * @param pc
@@ -552,11 +576,21 @@
 	 */
 	public void setTransferConfig(TransferConfig tc) {
 		this.transferConfig = tc != null ? tc : new TransferConfig(db);
-		setRequestPolicy(transferConfig.isAllowTipSha1InWant()
-				? RequestPolicy.TIP : RequestPolicy.ADVERTISED);
+		if (transferConfig.isAllowTipSha1InWant()) {
+			setRequestPolicy(transferConfig.isAllowReachableSha1InWant()
+				? RequestPolicy.REACHABLE_COMMIT_TIP : RequestPolicy.TIP);
+		} else {
+			setRequestPolicy(transferConfig.isAllowReachableSha1InWant()
+				? RequestPolicy.REACHABLE_COMMIT : RequestPolicy.ADVERTISED);
+		}
 	}
 
-	/** @return the configured logger. */
+	/**
+	 * @return the configured logger.
+	 *
+	 * @deprecated Use {@link #getPreUploadHook()}.
+	 */
+	@Deprecated
 	public UploadPackLogger getLogger() {
 		return logger;
 	}
@@ -566,7 +600,9 @@
 	 *
 	 * @param logger
 	 *            the logger instance. If null, no logging occurs.
+	 * @deprecated Use {@link #setPreUploadHook(PreUploadHook)}.
 	 */
+	@Deprecated
 	public void setLogger(UploadPackLogger logger) {
 		this.logger = logger;
 	}
@@ -648,8 +684,23 @@
 	 *         was sent, such as during the negotation phase of a smart HTTP
 	 *         connection, or if the client was already up-to-date.
 	 * @since 3.0
+	 * @deprecated Use {@link #getStatistics()}.
 	 */
+	@Deprecated
 	public PackWriter.Statistics getPackStatistics() {
+		return statistics == null ? null
+				: new PackWriter.Statistics(statistics);
+	}
+
+	/**
+	 * Get the PackWriter's statistics if a pack was sent to the client.
+	 *
+	 * @return statistics about pack output, if a pack was sent. Null if no pack
+	 *         was sent, such as during the negotation phase of a smart HTTP
+	 *         connection, or if the client was already up-to-date.
+	 * @since 4.1
+	 */
+	public PackStatistics getStatistics() {
 		return statistics;
 	}
 
@@ -684,6 +735,8 @@
 			else
 				multiAck = MultiAck.OFF;
 
+			if (!clientShallowCommits.isEmpty())
+				verifyClientShallow();
 			if (depth != 0)
 				processShallow();
 			if (!clientShallowCommits.isEmpty())
@@ -769,6 +822,35 @@
 		pckOut.end();
 	}
 
+	private void verifyClientShallow()
+			throws IOException, PackProtocolException {
+		AsyncRevObjectQueue q = walk.parseAny(clientShallowCommits, true);
+		try {
+			for (;;) {
+				try {
+					// Shallow objects named by the client must be commits.
+					RevObject o = q.next();
+					if (o == null) {
+						break;
+					}
+					if (!(o instanceof RevCommit)) {
+						throw new PackProtocolException(
+							MessageFormat.format(
+								JGitText.get().invalidShallowObject,
+								o.name()));
+					}
+				} catch (MissingObjectException notCommit) {
+					// shallow objects not known at the server are ignored
+					// by git-core upload-pack, match that behavior.
+					clientShallowCommits.remove(notCommit.getObjectId());
+					continue;
+				}
+			}
+		} finally {
+			q.release();
+		}
+	}
+
 	/**
 	 * Generate an advertisement of available refs and capabilities.
 	 *
@@ -808,6 +890,10 @@
 				|| policy == RequestPolicy.REACHABLE_COMMIT_TIP
 				|| policy == null)
 			adv.advertiseCapability(OPTION_ALLOW_TIP_SHA1_IN_WANT);
+		if (policy == RequestPolicy.REACHABLE_COMMIT
+				|| policy == RequestPolicy.REACHABLE_COMMIT_TIP
+				|| policy == null)
+			adv.advertiseCapability(OPTION_ALLOW_REACHABLE_SHA1_IN_WANT);
 		adv.advertiseCapability(OPTION_AGENT, UserAgent.get());
 		adv.setDerefTags(true);
 		Map<String, Ref> advertisedOrDefaultRefs = getAdvertisedOrDefaultRefs();
@@ -1389,6 +1475,7 @@
 			pw.setIndexDisabled(true);
 			pw.setUseCachedPacks(true);
 			pw.setUseBitmaps(depth == 0 && clientShallowCommits.isEmpty());
+			pw.setClientShallowCommits(clientShallowCommits);
 			pw.setReuseDeltaCommits(true);
 			pw.setDeltaBaseAsOffset(options.contains(OPTION_OFS_DELTA));
 			pw.setThin(options.contains(OPTION_THIN_PACK));
@@ -1458,8 +1545,10 @@
 
 		} finally {
 			statistics = pw.getStatistics();
-			if (statistics != null)
-				logger.onPackStatistics(statistics);
+			if (statistics != null) {
+				postUploadHook.onPostUpload(statistics);
+				logger.onPackStatistics(new PackWriter.Statistics(statistics));
+			}
 			pw.close();
 		}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPackLogger.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPackLogger.java
index 99fa6e0..85ebecc 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPackLogger.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPackLogger.java
@@ -52,7 +52,10 @@
  * thread to a particular connection, if they need to also include connection
  * information. One method is to use a {@link java.lang.ThreadLocal} to remember
  * the connection information before invoking UploadPack.
+ *
+ * @deprecated use {@link PostUploadHook} instead
  */
+@Deprecated
 public interface UploadPackLogger {
 	/** A simple no-op logger. */
 	public static final UploadPackLogger NULL = new UploadPackLogger() {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPackLoggerChain.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPackLoggerChain.java
index 3f14cc6..4ea0319 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPackLoggerChain.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPackLoggerChain.java
@@ -48,10 +48,13 @@
 import org.eclipse.jgit.internal.storage.pack.PackWriter;
 
 /**
- * {@link UploadPackLogger} that delegates to a list of other loggers.
+ * UploadPackLogger that delegates to a list of other loggers.
  * <p>
  * loggers are run in the order passed to the constructor.
+ *
+ * @deprecated Use {@link PostUploadHookChain} instead.
  */
+@Deprecated
 public class UploadPackLoggerChain implements UploadPackLogger {
 	private final UploadPackLogger[] loggers;
 	private final int count;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/WriteAbortedException.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WriteAbortedException.java
new file mode 100644
index 0000000..267bf7a
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/WriteAbortedException.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2015, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.transport;
+
+import java.io.IOException;
+
+/**
+ * An exception to be thrown when the write operation is aborted.
+ * <p>
+ * That can be thrown inside
+ * {@link ObjectCountCallback#setObjectCount(long)}.
+ *
+ * @since 4.1
+ */
+public class WriteAbortedException extends IOException {
+	private static final long serialVersionUID = 1L;
+
+	/**
+	 * Construct a {@code WriteAbortedException}.
+	 */
+	public WriteAbortedException() {
+	}
+
+	/**
+	 * Construct a {@code WriteAbortedException}.
+	 *
+	 * @param s message describing the issue
+	 */
+	public WriteAbortedException(String s) {
+		super(s);
+	}
+
+	/**
+	 * Construct a {@code WriteAbortedException}.
+	 *
+	 * @param s
+	 *            message describing the issue
+	 * @param why
+	 *            a lower level implementation specific issue.
+	 */
+	public WriteAbortedException(String s, Throwable why) {
+		super(s, why);
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/resolver/ServiceNotAuthorizedException.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/resolver/ServiceNotAuthorizedException.java
index a2c6edc..57a6192 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/resolver/ServiceNotAuthorizedException.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/resolver/ServiceNotAuthorizedException.java
@@ -45,12 +45,35 @@
 
 import org.eclipse.jgit.internal.JGitText;
 
-/** Indicates the request service is not authorized for current user. */
+/**
+ * Indicates that the requested service requires authentication that
+ * the current user has not provided.
+ * <p>
+ * This corresponds to response code
+ * {@code HttpServletResponse.SC_UNAUTHORIZED}.
+ */
 public class ServiceNotAuthorizedException extends Exception {
 	private static final long serialVersionUID = 1L;
 
-	/** Indicates the request service is not available. */
+	/**
+	 * @param message
+	 * @param cause
+	 * @since 4.1
+	 */
+	public ServiceNotAuthorizedException(String message, Throwable cause) {
+		super(message, cause);
+	}
+
+	/**
+	 * @param message
+	 * @since 4.1
+	 */
+	public ServiceNotAuthorizedException(String message) {
+		super(message);
+	}
+
+	/** Indicates that the requested service requires authentication. */
 	public ServiceNotAuthorizedException() {
-		super(JGitText.get().serviceNotPermittedNoName);
+		super(JGitText.get().unauthorized);
 	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/resolver/ServiceNotEnabledException.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/resolver/ServiceNotEnabledException.java
index d0fa758..78ae303 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/resolver/ServiceNotEnabledException.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/resolver/ServiceNotEnabledException.java
@@ -49,6 +49,23 @@
 public class ServiceNotEnabledException extends Exception {
 	private static final long serialVersionUID = 1L;
 
+	/**
+	 * @param message
+	 * @param cause
+	 * @since 4.1
+	 */
+	public ServiceNotEnabledException(String message, Throwable cause) {
+		super(message, cause);
+	}
+
+	/**
+	 * @param message
+	 * @since 4.1
+	 */
+	public ServiceNotEnabledException(String message) {
+		super(message);
+	}
+
 	/** Indicates the request service is not available. */
 	public ServiceNotEnabledException() {
 		super(JGitText.get().serviceNotEnabledNoName);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java
index d7c93d1..73ab04f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/treewalk/WorkingTreeIterator.java
@@ -596,7 +596,7 @@
 	 *             a relevant ignore rule file exists but cannot be read.
 	 */
 	protected boolean isEntryIgnored(final int pLen) throws IOException {
-		return isEntryIgnored(pLen, false);
+		return isEntryIgnored(pLen, mode, false);
 	}
 
 	/**
@@ -605,13 +605,16 @@
 	 *
 	 * @param pLen
 	 *            the length of the path in the path buffer.
+	 * @param fileMode
+	 *            the original iterator file mode
 	 * @param negatePrevious
 	 *            true if the previous matching iterator rule was negation
 	 * @return true if the entry is ignored by an ignore rule.
 	 * @throws IOException
 	 *             a relevant ignore rule file exists but cannot be read.
 	 */
-	private boolean isEntryIgnored(final int pLen, boolean negatePrevious)
+	private boolean isEntryIgnored(final int pLen, int fileMode,
+			boolean negatePrevious)
 			throws IOException {
 		IgnoreNode rules = getIgnoreNode();
 		if (rules != null) {
@@ -623,7 +626,7 @@
 			if (0 < pOff)
 				pOff--;
 			String p = TreeWalk.pathOf(path, pOff, pLen);
-			switch (rules.isIgnored(p, FileMode.TREE.equals(mode),
+			switch (rules.isIgnored(p, FileMode.TREE.equals(fileMode),
 					negatePrevious)) {
 			case IGNORED:
 				return true;
@@ -638,7 +641,7 @@
 			}
 		}
 		if (parent instanceof WorkingTreeIterator)
-			return ((WorkingTreeIterator) parent).isEntryIgnored(pLen,
+			return ((WorkingTreeIterator) parent).isEntryIgnored(pLen, fileMode,
 					negatePrevious);
 		return false;
 	}
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 12dfe96..e5219b2 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java
@@ -67,7 +67,6 @@
 import java.util.concurrent.atomic.AtomicBoolean;
 
 import org.eclipse.jgit.api.errors.JGitInternalException;
-import org.eclipse.jgit.errors.SymlinksNotSupportedException;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Repository;
@@ -248,7 +247,7 @@
 	 * @since 3.0
 	 */
 	public long lastModified(File f) throws IOException {
-		return f.lastModified();
+		return FileUtils.lastModified(f);
 	}
 
 	/**
@@ -261,7 +260,7 @@
 	 * @since 3.0
 	 */
 	public void setLastModified(File f, long time) throws IOException {
-		f.setLastModified(time);
+		FileUtils.setLastModified(f, time);
 	}
 
 	/**
@@ -274,7 +273,7 @@
 	 * @since 3.0
 	 */
 	public long length(File path) throws IOException {
-		return path.length();
+		return FileUtils.getLength(path);
 	}
 
 	/**
@@ -286,9 +285,7 @@
 	 * @since 3.3
 	 */
 	public void delete(File f) throws IOException {
-		if (!f.delete())
-			throw new IOException(MessageFormat.format(
-					JGitText.get().deleteFileFailed, f.getAbsolutePath()));
+		FileUtils.delete(f);
 	}
 
 	/**
@@ -623,8 +620,7 @@
 	 * @since 3.0
 	 */
 	public String readSymLink(File path) throws IOException {
-		throw new SymlinksNotSupportedException(
-				JGitText.get().errorSymlinksNotSupported);
+		return FileUtils.readSymLink(path);
 	}
 
 	/**
@@ -634,7 +630,7 @@
 	 * @since 3.0
 	 */
 	public boolean isSymLink(File path) throws IOException {
-		return false;
+		return FileUtils.isSymlink(path);
 	}
 
 	/**
@@ -646,7 +642,7 @@
 	 * @since 3.0
 	 */
 	public boolean exists(File path) {
-		return path.exists();
+		return FileUtils.exists(path);
 	}
 
 	/**
@@ -658,7 +654,7 @@
 	 * @since 3.0
 	 */
 	public boolean isDirectory(File path) {
-		return path.isDirectory();
+		return FileUtils.isDirectory(path);
 	}
 
 	/**
@@ -670,7 +666,7 @@
 	 * @since 3.0
 	 */
 	public boolean isFile(File path) {
-		return path.isFile();
+		return FileUtils.isFile(path);
 	}
 
 	/**
@@ -681,7 +677,7 @@
 	 * @since 3.0
 	 */
 	public boolean isHidden(File path) throws IOException {
-		return path.isHidden();
+		return FileUtils.isHidden(path);
 	}
 
 	/**
@@ -693,9 +689,7 @@
 	 * @since 3.0
 	 */
 	public void setHidden(File path, boolean hidden) throws IOException {
-		if (!path.getName().startsWith(".")) //$NON-NLS-1$
-			throw new IllegalArgumentException(
-					JGitText.get().hiddenFilesStartWithDot);
+		FileUtils.setHidden(path, hidden);
 	}
 
 	/**
@@ -707,8 +701,7 @@
 	 * @since 3.0
 	 */
 	public void createSymLink(File path, String target) throws IOException {
-		throw new SymlinksNotSupportedException(
-				JGitText.get().errorSymlinksNotSupported);
+		FileUtils.createSymLink(path, target);
 	}
 
 	/**
@@ -866,7 +859,10 @@
 	 * @since 4.0
 	 */
 	public File findHook(Repository repository, final String hookName) {
-		final File hookFile = new File(new File(repository.getDirectory(),
+		File gitDir = repository.getDirectory();
+		if (gitDir == null)
+			return null;
+		final File hookFile = new File(new File(gitDir,
 				Constants.HOOKS), hookName);
 		return hookFile.isFile() ? hookFile : null;
 	}
@@ -1067,28 +1063,28 @@
 			return lastModifiedTime;
 		}
 
-		private boolean isDirectory;
+		private final boolean isDirectory;
 
-		private boolean isSymbolicLink;
+		private final boolean isSymbolicLink;
 
-		private boolean isRegularFile;
+		private final boolean isRegularFile;
 
-		private long creationTime;
+		private final long creationTime;
 
-		private long lastModifiedTime;
+		private final long lastModifiedTime;
 
-		private boolean isExecutable;
+		private final boolean isExecutable;
 
-		private File file;
+		private final File file;
 
-		private boolean exists;
+		private final boolean exists;
 
 		/**
 		 * file length
 		 */
 		protected long length = -1;
 
-		FS fs;
+		final FS fs;
 
 		Attributes(FS fs, File file, boolean exists, boolean isDirectory,
 				boolean isExecutable, boolean isSymbolicLink,
@@ -1107,14 +1103,14 @@
 		}
 
 		/**
-		 * Constructor when there are issues with reading
+		 * Constructor when there are issues with reading. All attributes except
+		 * given will be set to the default values.
 		 *
 		 * @param fs
 		 * @param path
 		 */
 		public Attributes(File path, FS fs) {
-			this.file = path;
-			this.fs = fs;
+			this(fs, path, false, false, false, false, false, 0L, 0L, 0L);
 		}
 
 		/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX.java
index b07f859..779b10e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX.java
@@ -168,7 +168,7 @@
 
 	@Override
 	public boolean canExecute(File f) {
-		return FileUtil.canExecute(f);
+		return FileUtils.canExecute(f);
 	}
 
 	@Override
@@ -246,71 +246,16 @@
 	}
 
 	@Override
-	public boolean isSymLink(File path) throws IOException {
-		return FileUtil.isSymlink(path);
-	}
-
-	@Override
-	public long lastModified(File path) throws IOException {
-		return FileUtil.lastModified(path);
-	}
-
-	@Override
-	public void setLastModified(File path, long time) throws IOException {
-		FileUtil.setLastModified(path, time);
-	}
-
-	@Override
-	public void delete(File path) throws IOException {
-		FileUtil.delete(path);
-	}
-
-	@Override
-	public long length(File f) throws IOException {
-		return FileUtil.getLength(f);
-	}
-
-	@Override
-	public boolean exists(File path) {
-		return FileUtil.exists(path);
-	}
-
-	@Override
-	public boolean isDirectory(File path) {
-		return FileUtil.isDirectory(path);
-	}
-
-	@Override
-	public boolean isFile(File path) {
-		return FileUtil.isFile(path);
-	}
-
-	@Override
-	public boolean isHidden(File path) throws IOException {
-		return FileUtil.isHidden(path);
-	}
-
-	@Override
 	public void setHidden(File path, boolean hidden) throws IOException {
 		// no action on POSIX
 	}
 
-	@Override
-	public String readSymLink(File path) throws IOException {
-		return FileUtil.readSymlink(path);
-	}
-
-	@Override
-	public void createSymLink(File path, String target) throws IOException {
-		FileUtil.createSymLink(path, target);
-	}
-
 	/**
 	 * @since 3.3
 	 */
 	@Override
 	public Attributes getAttributes(File path) {
-		return FileUtil.getFileAttributesPosix(this, path);
+		return FileUtils.getFileAttributesPosix(this, path);
 	}
 
 	/**
@@ -318,7 +263,7 @@
 	 */
 	@Override
 	public File normalize(File file) {
-		return FileUtil.normalize(file);
+		return FileUtils.normalize(file);
 	}
 
 	/**
@@ -326,7 +271,7 @@
 	 */
 	@Override
 	public String normalize(String name) {
-		return FileUtil.normalize(name);
+		return FileUtils.normalize(name);
 	}
 
 	/**
@@ -335,6 +280,9 @@
 	@Override
 	public File findHook(Repository repository, String hookName) {
 		final File gitdir = repository.getDirectory();
+		if (gitdir == null) {
+			return null;
+		}
 		final Path hookPath = gitdir.toPath().resolve(Constants.HOOKS)
 				.resolve(hookName);
 		if (Files.isExecutable(hookPath))
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32.java
index 5c652be..f0a2e72 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32.java
@@ -168,7 +168,7 @@
 		try {
 			tempFile = File.createTempFile("tempsymlinktarget", ""); //$NON-NLS-1$ //$NON-NLS-2$
 			File linkName = new File(tempFile.getParentFile(), "tempsymlink"); //$NON-NLS-1$
-			FileUtil.createSymLink(linkName, tempFile.getPath());
+			createSymLink(linkName, tempFile.getPath());
 			supportSymlinks = Boolean.TRUE;
 			linkName.delete();
 		} catch (IOException | UnsupportedOperationException e) {
@@ -183,71 +183,11 @@
 		}
 	}
 
-	@Override
-	public boolean isSymLink(File path) throws IOException {
-		return FileUtil.isSymlink(path);
-	}
-
-	@Override
-	public long lastModified(File path) throws IOException {
-		return FileUtil.lastModified(path);
-	}
-
-	@Override
-	public void setLastModified(File path, long time) throws IOException {
-		FileUtil.setLastModified(path, time);
-	}
-
-	@Override
-	public void delete(File path) throws IOException {
-		FileUtil.delete(path);
-	}
-
-	@Override
-	public long length(File f) throws IOException {
-		return FileUtil.getLength(f);
-	}
-
-	@Override
-	public boolean exists(File path) {
-		return FileUtil.exists(path);
-	}
-
-	@Override
-	public boolean isDirectory(File path) {
-		return FileUtil.isDirectory(path);
-	}
-
-	@Override
-	public boolean isFile(File path) {
-		return FileUtil.isFile(path);
-	}
-
-	@Override
-	public boolean isHidden(File path) throws IOException {
-		return FileUtil.isHidden(path);
-	}
-
-	@Override
-	public void setHidden(File path, boolean hidden) throws IOException {
-		FileUtil.setHidden(path, hidden);
-	}
-
-	@Override
-	public String readSymLink(File path) throws IOException {
-		return FileUtil.readSymlink(path);
-	}
-
-	@Override
-	public void createSymLink(File path, String target) throws IOException {
-		FileUtil.createSymLink(path, target);
-	}
-
 	/**
 	 * @since 3.3
 	 */
 	@Override
 	public Attributes getAttributes(File path) {
-		return FileUtil.getFileAttributesBasic(this, path);
+		return FileUtils.getFileAttributesBasic(this, path);
 	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32_Cygwin.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32_Cygwin.java
index 3c3b2eb..2450be4 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32_Cygwin.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32_Cygwin.java
@@ -44,7 +44,6 @@
 package org.eclipse.jgit.util;
 
 import java.io.File;
-import java.io.IOException;
 import java.io.PrintStream;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -168,74 +167,6 @@
 		return true;
 	}
 
-	@Override
-	public boolean isSymLink(File path) throws IOException {
-		return FileUtil.isSymlink(path);
-	}
-
-	@Override
-	public long lastModified(File path) throws IOException {
-		return FileUtil.lastModified(path);
-	}
-
-	@Override
-	public void setLastModified(File path, long time) throws IOException {
-		FileUtil.setLastModified(path, time);
-	}
-
-	@Override
-	public void delete(File path) throws IOException {
-		FileUtil.delete(path);
-	}
-
-	@Override
-	public long length(File f) throws IOException {
-		return FileUtil.getLength(f);
-	}
-
-	@Override
-	public boolean exists(File path) {
-		return FileUtil.exists(path);
-	}
-
-	@Override
-	public boolean isDirectory(File path) {
-		return FileUtil.isDirectory(path);
-	}
-
-	@Override
-	public boolean isFile(File path) {
-		return FileUtil.isFile(path);
-	}
-
-	@Override
-	public boolean isHidden(File path) throws IOException {
-		return FileUtil.isHidden(path);
-	}
-
-	@Override
-	public void setHidden(File path, boolean hidden) throws IOException {
-		FileUtil.setHidden(path, hidden);
-	}
-
-	@Override
-	public String readSymLink(File path) throws IOException {
-		return FileUtil.readSymlink(path);
-	}
-
-	@Override
-	public void createSymLink(File path, String target) throws IOException {
-		FileUtil.createSymLink(path, target);
-	}
-
-	/**
-	 * @since 3.3
-	 */
-	@Override
-	public Attributes getAttributes(File path) {
-		return FileUtil.getFileAttributesBasic(this, path);
-	}
-
 	/**
 	 * @since 3.7
 	 */
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtil.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtil.java
index f5babed..b87b9a4 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtil.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtil.java
@@ -46,51 +46,24 @@
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Files;
-import java.nio.file.LinkOption;
-import java.nio.file.NoSuchFileException;
-import java.nio.file.Path;
-import java.nio.file.attribute.BasicFileAttributeView;
-import java.nio.file.attribute.BasicFileAttributes;
-import java.nio.file.attribute.FileTime;
-import java.nio.file.attribute.PosixFileAttributeView;
-import java.nio.file.attribute.PosixFileAttributes;
-import java.nio.file.attribute.PosixFilePermission;
-import java.text.Normalizer;
-import java.text.Normalizer.Form;
 
-import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.util.FS.Attributes;
 
 /**
  * File utilities using Java 7 NIO2
  */
+@Deprecated
 public class FileUtil {
 
-	static class Java7BasicAttributes extends Attributes {
-
-		Java7BasicAttributes(FS fs, File fPath, boolean exists,
-				boolean isDirectory, boolean isExecutable,
-				boolean isSymbolicLink, boolean isRegularFile,
-				long creationTime, long lastModifiedTime, long length) {
-			super(fs, fPath, exists, isDirectory, isExecutable, isSymbolicLink,
-					isRegularFile, creationTime, lastModifiedTime, length);
-		}
-	}
-
 	/**
 	 * @param path
 	 * @return target path of the symlink
 	 * @throws IOException
+	 * @deprecated use {@link FileUtils#readSymLink(File)} instead
 	 */
+	@Deprecated
 	public static String readSymlink(File path) throws IOException {
-		Path nioPath = path.toPath();
-		Path target = Files.readSymbolicLink(nioPath);
-		String targetString = target.toString();
-		if (SystemReader.getInstance().isWindows())
-			targetString = targetString.replace('\\', '/');
-		else if (SystemReader.getInstance().isMacOS())
-			targetString = Normalizer.normalize(targetString, Form.NFC);
-		return targetString;
+		return FileUtils.readSymLink(path);
 	}
 
 	/**
@@ -99,218 +72,168 @@
 	 * @param target
 	 *            target of the symlink to be created
 	 * @throws IOException
+	 * @deprecated use {@link FileUtils#createSymLink(File, String)} instead
 	 */
+	@Deprecated
 	public static void createSymLink(File path, String target)
 			throws IOException {
-		Path nioPath = path.toPath();
-		if (Files.exists(nioPath, LinkOption.NOFOLLOW_LINKS))
-			Files.delete(nioPath);
-		if (SystemReader.getInstance().isWindows())
-			target = target.replace('/', '\\');
-		Path nioTarget = new File(target).toPath();
-		Files.createSymbolicLink(nioPath, nioTarget);
+		FileUtils.createSymLink(path, target);
 	}
 
 	/**
 	 * @param path
 	 * @return {@code true} if the passed path is a symlink
+	 * @deprecated Use {@link Files#isSymbolicLink(java.nio.file.Path)} instead
 	 */
+	@Deprecated
 	public static boolean isSymlink(File path) {
-		Path nioPath = path.toPath();
-		return Files.isSymbolicLink(nioPath);
+		return FileUtils.isSymlink(path);
 	}
 
 	/**
 	 * @param path
 	 * @return lastModified attribute for given path
 	 * @throws IOException
+	 * @deprecated Use
+	 *             {@link Files#getLastModifiedTime(java.nio.file.Path, java.nio.file.LinkOption...)}
+	 *             instead
 	 */
+	@Deprecated
 	public static long lastModified(File path) throws IOException {
-		Path nioPath = path.toPath();
-		return Files.getLastModifiedTime(nioPath, LinkOption.NOFOLLOW_LINKS)
-				.toMillis();
+		return FileUtils.lastModified(path);
 	}
 
 	/**
 	 * @param path
 	 * @param time
 	 * @throws IOException
+	 * @deprecated Use
+	 *             {@link Files#setLastModifiedTime(java.nio.file.Path, java.nio.file.attribute.FileTime)}
+	 *             instead
 	 */
+	@Deprecated
 	public static void setLastModified(File path, long time) throws IOException {
-		Path nioPath = path.toPath();
-		Files.setLastModifiedTime(nioPath, FileTime.fromMillis(time));
+		FileUtils.setLastModified(path, time);
 	}
 
 	/**
 	 * @param path
 	 * @return {@code true} if the given path exists
+	 * @deprecated Use
+	 *             {@link Files#exists(java.nio.file.Path, java.nio.file.LinkOption...)}
+	 *             instead
 	 */
+	@Deprecated
 	public static boolean exists(File path) {
-		Path nioPath = path.toPath();
-		return Files.exists(nioPath, LinkOption.NOFOLLOW_LINKS);
+		return FileUtils.exists(path);
 	}
 
 	/**
 	 * @param path
 	 * @return {@code true} if the given path is hidden
 	 * @throws IOException
+	 * @deprecated Use {@link Files#isHidden(java.nio.file.Path)} instead
 	 */
+	@Deprecated
 	public static boolean isHidden(File path) throws IOException {
-		Path nioPath = path.toPath();
-		return Files.isHidden(nioPath);
+		return FileUtils.isHidden(path);
 	}
 
 	/**
 	 * @param path
 	 * @param hidden
 	 * @throws IOException
+	 * @deprecated Use {@link FileUtils#setHidden(File,boolean)} instead
 	 */
+	@Deprecated
 	public static void setHidden(File path, boolean hidden) throws IOException {
-		Path nioPath = path.toPath();
-		Files.setAttribute(nioPath, "dos:hidden", Boolean.valueOf(hidden), //$NON-NLS-1$
-				LinkOption.NOFOLLOW_LINKS);
+		FileUtils.setHidden(path, hidden);
 	}
 
 	/**
 	 * @param path
 	 * @return length of the given file
 	 * @throws IOException
+	 * @deprecated Use {@link FileUtils#getLength(File)} instead
 	 */
+	@Deprecated
 	public static long getLength(File path) throws IOException {
-		Path nioPath = path.toPath();
-		if (Files.isSymbolicLink(nioPath))
-			return Files.readSymbolicLink(nioPath).toString()
-					.getBytes(Constants.CHARSET).length;
-		return Files.size(nioPath);
+		return FileUtils.getLength(path);
 	}
 
 	/**
 	 * @param path
 	 * @return {@code true} if the given file a directory
+	 * @deprecated Use
+	 *             {@link Files#isDirectory(java.nio.file.Path, java.nio.file.LinkOption...)}
+	 *             instead
 	 */
+	@Deprecated
 	public static boolean isDirectory(File path) {
-		Path nioPath = path.toPath();
-		return Files.isDirectory(nioPath, LinkOption.NOFOLLOW_LINKS);
+		return FileUtils.isDirectory(path);
 	}
 
 	/**
 	 * @param path
 	 * @return {@code true} if the given file is a file
+	 * @deprecated Use
+	 *             {@link Files#isRegularFile(java.nio.file.Path, java.nio.file.LinkOption...)}
+	 *             instead
 	 */
+	@Deprecated
 	public static boolean isFile(File path) {
-		Path nioPath = path.toPath();
-		return Files.isRegularFile(nioPath, LinkOption.NOFOLLOW_LINKS);
+		return FileUtils.isFile(path);
 	}
 
 	/**
 	 * @param path
 	 * @return {@code true} if the given file can be executed
+	 * @deprecated Use {@link FileUtils#canExecute(File)} instead
 	 */
+	@Deprecated
 	public static boolean canExecute(File path) {
-		if (!isFile(path))
-			return false;
-		return path.canExecute();
+		return FileUtils.canExecute(path);
 	}
 
 	/**
 	 * @param path
 	 * @throws IOException
+	 * @deprecated use {@link FileUtils#delete(File)}
 	 */
+	@Deprecated
 	public static void delete(File path) throws IOException {
-		Path nioPath = path.toPath();
-		Files.delete(nioPath);
-	}
-
-	static Attributes getFileAttributesBasic(FS fs, File path) {
-		try {
-			Path nioPath = path.toPath();
-			BasicFileAttributes readAttributes = nioPath
-					.getFileSystem()
-					.provider()
-					.getFileAttributeView(nioPath,
-							BasicFileAttributeView.class,
-							LinkOption.NOFOLLOW_LINKS).readAttributes();
-			Attributes attributes = new FileUtil.Java7BasicAttributes(fs, path,
-					true,
-					readAttributes.isDirectory(),
-					fs.supportsExecute() ? path.canExecute() : false,
-					readAttributes.isSymbolicLink(),
-					readAttributes.isRegularFile(), //
-					readAttributes.creationTime().toMillis(), //
-					readAttributes.lastModifiedTime().toMillis(),
-					readAttributes.isSymbolicLink() ? Constants
-							.encode(FileUtils.readSymLink(path)).length
-							: readAttributes.size());
-			return attributes;
-		} catch (NoSuchFileException e) {
-			return new FileUtil.Java7BasicAttributes(fs, path, false, false,
-					false, false, false, 0L, 0L, 0L);
-		} catch (IOException e) {
-			return new Attributes(path, fs);
-		}
+		FileUtils.delete(path);
 	}
 
 	/**
 	 * @param fs
 	 * @param path
 	 * @return file system attributes for the given file
+	 * @deprecated Use {@link FileUtils#getFileAttributesPosix(FS,File)} instead
 	 */
+	@Deprecated
 	public static Attributes getFileAttributesPosix(FS fs, File path) {
-		try {
-			Path nioPath = path.toPath();
-			PosixFileAttributes readAttributes = nioPath
-					.getFileSystem()
-					.provider()
-					.getFileAttributeView(nioPath,
-							PosixFileAttributeView.class,
-							LinkOption.NOFOLLOW_LINKS).readAttributes();
-			Attributes attributes = new FileUtil.Java7BasicAttributes(
-					fs,
-					path,
-					true, //
-					readAttributes.isDirectory(), //
-					readAttributes.permissions().contains(
-							PosixFilePermission.OWNER_EXECUTE),
-					readAttributes.isSymbolicLink(),
-					readAttributes.isRegularFile(), //
-					readAttributes.creationTime().toMillis(), //
-					readAttributes.lastModifiedTime().toMillis(),
-					readAttributes.size());
-			return attributes;
-		} catch (NoSuchFileException e) {
-			return new FileUtil.Java7BasicAttributes(fs, path, false, false,
-					false, false, false, 0L, 0L, 0L);
-		} catch (IOException e) {
-			return new Attributes(path, fs);
-		}
+		return FileUtils.getFileAttributesPosix(fs, path);
 	}
 
 	/**
 	 * @param file
 	 * @return on Mac: NFC normalized {@link File}, otherwise the passed file
+	 * @deprecated Use {@link FileUtils#normalize(File)} instead
 	 */
+	@Deprecated
 	public static File normalize(File file) {
-		if (SystemReader.getInstance().isMacOS()) {
-			// TODO: Would it be faster to check with isNormalized first
-			// assuming normalized paths are much more common
-			String normalized = Normalizer.normalize(file.getPath(),
-					Normalizer.Form.NFC);
-			return new File(normalized);
-		}
-		return file;
+		return FileUtils.normalize(file);
 	}
 
 	/**
 	 * @param name
 	 * @return on Mac: NFC normalized form of given name
+	 * @deprecated Use {@link FileUtils#normalize(String)} instead
 	 */
+	@Deprecated
 	public static String normalize(String name) {
-		if (SystemReader.getInstance().isMacOS()) {
-			if (name == null)
-				return null;
-			return Normalizer.normalize(name, Normalizer.Form.NFC);
-		}
-		return name;
+		return FileUtils.normalize(name);
 	}
 
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java
index 1e58245..548d239 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java
@@ -48,12 +48,28 @@
 import java.io.File;
 import java.io.IOException;
 import java.nio.channels.FileLock;
+import java.nio.file.AtomicMoveNotSupportedException;
+import java.nio.file.CopyOption;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.attribute.BasicFileAttributeView;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileTime;
+import java.nio.file.attribute.PosixFileAttributeView;
+import java.nio.file.attribute.PosixFileAttributes;
+import java.nio.file.attribute.PosixFilePermission;
 import java.text.MessageFormat;
+import java.text.Normalizer;
+import java.text.Normalizer.Form;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.regex.Pattern;
 
 import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.util.FS.Attributes;
 
 /**
  * File Utilities
@@ -207,30 +223,68 @@
 	 */
 	public static void rename(final File src, final File dst)
 			throws IOException {
+		rename(src, dst, StandardCopyOption.REPLACE_EXISTING);
+	}
+
+	/**
+	 * Rename a file or folder using the passed {@link CopyOption}s. If the
+	 * rename fails and if we are running on a filesystem where it makes sense
+	 * to repeat a failing rename then repeat the rename operation up to 9 times
+	 * with 100ms sleep time between two calls. Furthermore if the destination
+	 * exists and is a directory hierarchy with only directories in it, the
+	 * whole directory hierarchy will be deleted. If the target represents a
+	 * non-empty directory structure, empty subdirectories within that structure
+	 * may or may not be deleted even if the method fails. Furthermore if the
+	 * destination exists and is a file then the file will be replaced if
+	 * {@link StandardCopyOption#REPLACE_EXISTING} has been set. If
+	 * {@link StandardCopyOption#ATOMIC_MOVE} has been set the rename will be
+	 * done atomically or fail with an {@link AtomicMoveNotSupportedException}
+	 *
+	 * @param src
+	 *            the old file
+	 * @param dst
+	 *            the new file
+	 * @param options
+	 *            options to pass to
+	 *            {@link Files#move(java.nio.file.Path, java.nio.file.Path, CopyOption...)}
+	 * @throws AtomicMoveNotSupportedException
+	 *             if file cannot be moved as an atomic file system operation
+	 * @throws IOException
+	 * @since 4.1
+	 */
+	public static void rename(final File src, final File dst,
+			CopyOption... options)
+					throws AtomicMoveNotSupportedException, IOException {
 		int attempts = FS.DETECTED.retryFailedLockFileCommit() ? 10 : 1;
 		while (--attempts >= 0) {
-			if (src.renameTo(dst))
-				return;
 			try {
-				if (!dst.delete())
-					delete(dst, EMPTY_DIRECTORIES_ONLY | RECURSIVE);
-				// On *nix there is no try, you do or do not
-				if (src.renameTo(dst))
-					return;
+				Files.move(src.toPath(), dst.toPath(), options);
+				return;
+			} catch (AtomicMoveNotSupportedException e) {
+				throw e;
 			} catch (IOException e) {
-				// ignore and continue retry
+				try {
+					if (!dst.delete()) {
+						delete(dst, EMPTY_DIRECTORIES_ONLY | RECURSIVE);
+					}
+					// On *nix there is no try, you do or do not
+					Files.move(src.toPath(), dst.toPath(), options);
+					return;
+				} catch (IOException e2) {
+					// ignore and continue retry
+				}
 			}
 			try {
 				Thread.sleep(100);
 			} catch (InterruptedException e) {
-				throw new IOException(MessageFormat.format(
-						JGitText.get().renameFileFailed, src.getAbsolutePath(),
-						dst.getAbsolutePath()));
+				throw new IOException(
+						MessageFormat.format(JGitText.get().renameFileFailed,
+								src.getAbsolutePath(), dst.getAbsolutePath()));
 			}
 		}
-		throw new IOException(MessageFormat.format(
-				JGitText.get().renameFileFailed, src.getAbsolutePath(),
-				dst.getAbsolutePath()));
+		throw new IOException(
+				MessageFormat.format(JGitText.get().renameFileFailed,
+						src.getAbsolutePath(), dst.getAbsolutePath()));
 	}
 
 	/**
@@ -350,18 +404,33 @@
 	 */
 	public static void createSymLink(File path, String target)
 			throws IOException {
-		FS.DETECTED.createSymLink(path, target);
+		Path nioPath = path.toPath();
+		if (Files.exists(nioPath, LinkOption.NOFOLLOW_LINKS)) {
+			Files.delete(nioPath);
+		}
+		if (SystemReader.getInstance().isWindows()) {
+			target = target.replace('/', '\\');
+		}
+		Path nioTarget = new File(target).toPath();
+		Files.createSymbolicLink(nioPath, nioTarget);
 	}
 
 	/**
 	 * @param path
-	 * @return the target of the symbolic link, or null if it is not a symbolic
-	 *         link
+	 * @return target path of the symlink, or null if it is not a symbolic link
 	 * @throws IOException
 	 * @since 3.0
 	 */
 	public static String readSymLink(File path) throws IOException {
-		return FS.DETECTED.readSymLink(path);
+		Path nioPath = path.toPath();
+		Path target = Files.readSymbolicLink(nioPath);
+		String targetString = target.toString();
+		if (SystemReader.getInstance().isWindows()) {
+			targetString = targetString.replace('\\', '/');
+		} else if (SystemReader.getInstance().isMacOS()) {
+			targetString = Normalizer.normalize(targetString, Form.NFC);
+		}
+		return targetString;
 	}
 
 	/**
@@ -453,4 +522,212 @@
 		}
 		return builder.toString();
 	}
+
+	/**
+	 * Determine if an IOException is a Stale NFS File Handle
+	 *
+	 * @param ioe
+	 * @return a boolean true if the IOException is a Stale NFS FIle Handle
+	 * @since 4.1
+	 */
+	public static boolean isStaleFileHandle(IOException ioe) {
+		String msg = ioe.getMessage();
+		return msg != null
+				&& msg.toLowerCase().matches("stale .*file .*handle"); //$NON-NLS-1$
+	}
+
+	/**
+	 * @param file
+	 * @return {@code true} if the passed file is a symbolic link
+	 */
+	static boolean isSymlink(File file) {
+		return Files.isSymbolicLink(file.toPath());
+	}
+
+	/**
+	 * @param file
+	 * @return lastModified attribute for given file, not following symbolic
+	 *         links
+	 * @throws IOException
+	 */
+	static long lastModified(File file) throws IOException {
+		return Files.getLastModifiedTime(file.toPath(), LinkOption.NOFOLLOW_LINKS)
+				.toMillis();
+	}
+
+	/**
+	 * @param file
+	 * @param time
+	 * @throws IOException
+	 */
+	static void setLastModified(File file, long time) throws IOException {
+		Files.setLastModifiedTime(file.toPath(), FileTime.fromMillis(time));
+	}
+
+	/**
+	 * @param file
+	 * @return {@code true} if the given file exists, not following symbolic
+	 *         links
+	 */
+	static boolean exists(File file) {
+		return Files.exists(file.toPath(), LinkOption.NOFOLLOW_LINKS);
+	}
+
+	/**
+	 * @param file
+	 * @return {@code true} if the given file is hidden
+	 * @throws IOException
+	 */
+	static boolean isHidden(File file) throws IOException {
+		return Files.isHidden(file.toPath());
+	}
+
+	/**
+	 * @param file
+	 * @param hidden
+	 * @throws IOException
+	 * @since 4.1
+	 */
+	public static void setHidden(File file, boolean hidden) throws IOException {
+		Files.setAttribute(file.toPath(), "dos:hidden", Boolean.valueOf(hidden), //$NON-NLS-1$
+				LinkOption.NOFOLLOW_LINKS);
+	}
+
+	/**
+	 * @param file
+	 * @return length of the given file
+	 * @throws IOException
+	 * @since 4.1
+	 */
+	public static long getLength(File file) throws IOException {
+		Path nioPath = file.toPath();
+		if (Files.isSymbolicLink(nioPath))
+			return Files.readSymbolicLink(nioPath).toString()
+					.getBytes(Constants.CHARSET).length;
+		return Files.size(nioPath);
+	}
+
+	/**
+	 * @param file
+	 * @return {@code true} if the given file is a directory, not following
+	 *         symbolic links
+	 */
+	static boolean isDirectory(File file) {
+		return Files.isDirectory(file.toPath(), LinkOption.NOFOLLOW_LINKS);
+	}
+
+	/**
+	 * @param file
+	 * @return {@code true} if the given file is a file, not following symbolic
+	 *         links
+	 */
+	static boolean isFile(File file) {
+		return Files.isRegularFile(file.toPath(), LinkOption.NOFOLLOW_LINKS);
+	}
+
+	/**
+	 * @param file
+	 * @return {@code true} if the given file can be executed
+	 * @since 4.1
+	 */
+	public static boolean canExecute(File file) {
+		if (!isFile(file)) {
+			return false;
+		}
+		return Files.isExecutable(file.toPath());
+	}
+
+	/**
+	 * @param fs
+	 * @param file
+	 * @return non null attributes object
+	 */
+	static Attributes getFileAttributesBasic(FS fs, File file) {
+		try {
+			Path nioPath = file.toPath();
+			BasicFileAttributes readAttributes = nioPath
+					.getFileSystem()
+					.provider()
+					.getFileAttributeView(nioPath,
+							BasicFileAttributeView.class,
+							LinkOption.NOFOLLOW_LINKS).readAttributes();
+			Attributes attributes = new Attributes(fs, file,
+					true,
+					readAttributes.isDirectory(),
+					fs.supportsExecute() ? file.canExecute() : false,
+					readAttributes.isSymbolicLink(),
+					readAttributes.isRegularFile(), //
+					readAttributes.creationTime().toMillis(), //
+					readAttributes.lastModifiedTime().toMillis(),
+					readAttributes.isSymbolicLink() ? Constants
+							.encode(readSymLink(file)).length
+							: readAttributes.size());
+			return attributes;
+		} catch (IOException e) {
+			return new Attributes(file, fs);
+		}
+	}
+
+	/**
+	 * @param fs
+	 * @param file
+	 * @return file system attributes for the given file
+	 * @since 4.1
+	 */
+	public static Attributes getFileAttributesPosix(FS fs, File file) {
+		try {
+			Path nioPath = file.toPath();
+			PosixFileAttributes readAttributes = nioPath
+					.getFileSystem()
+					.provider()
+					.getFileAttributeView(nioPath,
+							PosixFileAttributeView.class,
+							LinkOption.NOFOLLOW_LINKS).readAttributes();
+			Attributes attributes = new Attributes(
+					fs,
+					file,
+					true, //
+					readAttributes.isDirectory(), //
+					readAttributes.permissions().contains(
+							PosixFilePermission.OWNER_EXECUTE),
+					readAttributes.isSymbolicLink(),
+					readAttributes.isRegularFile(), //
+					readAttributes.creationTime().toMillis(), //
+					readAttributes.lastModifiedTime().toMillis(),
+					readAttributes.size());
+			return attributes;
+		} catch (IOException e) {
+			return new Attributes(file, fs);
+		}
+	}
+
+	/**
+	 * @param file
+	 * @return on Mac: NFC normalized {@link File}, otherwise the passed file
+	 * @since 4.1
+	 */
+	public static File normalize(File file) {
+		if (SystemReader.getInstance().isMacOS()) {
+			// TODO: Would it be faster to check with isNormalized first
+			// assuming normalized paths are much more common
+			String normalized = Normalizer.normalize(file.getPath(),
+					Normalizer.Form.NFC);
+			return new File(normalized);
+		}
+		return file;
+	}
+
+	/**
+	 * @param name
+	 * @return on Mac: NFC normalized form of given name
+	 * @since 4.1
+	 */
+	public static String normalize(String name) {
+		if (SystemReader.getInstance().isMacOS()) {
+			if (name == null)
+				return null;
+			return Normalizer.normalize(name, Normalizer.Form.NFC);
+		}
+		return name;
+	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/IO.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/IO.java
index c817c47..0d283fd 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/IO.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/IO.java
@@ -51,6 +51,7 @@
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.Reader;
 import java.nio.ByteBuffer;
 import java.nio.channels.ReadableByteChannel;
 import java.text.MessageFormat;
@@ -371,6 +372,75 @@
 		return l;
 	}
 
+	/**
+	 * Read the next line from a reader.
+	 * <p>
+	 * Like {@link java.io.BufferedReader#readLine()}, but only treats
+	 * {@code \n} as end-of-line, and includes the trailing newline.
+	 *
+	 * @param in
+	 *            the reader to read from.
+	 * @param sizeHint
+	 *            hint for buffer sizing; 0 or negative for default.
+	 * @return the next line from the input, always ending in {@code \n} unless
+	 *         EOF was reached.
+	 * @throws IOException
+	 *             there was an error reading from the stream.
+	 * @since 4.1
+	 */
+	public static String readLine(Reader in, int sizeHint) throws IOException {
+		if (in.markSupported()) {
+			if (sizeHint <= 0) {
+				sizeHint = 1024;
+			}
+			StringBuilder sb = new StringBuilder(sizeHint);
+			char[] buf = new char[sizeHint];
+			while (true) {
+				in.mark(sizeHint);
+				int n = in.read(buf);
+				if (n < 0) {
+					in.reset();
+					return sb.toString();
+				}
+				for (int i = 0; i < n; i++) {
+					if (buf[i] == '\n') {
+						resetAndSkipFully(in, ++i);
+						sb.append(buf, 0, i);
+						return sb.toString();
+					}
+				}
+				if (n > 0) {
+					sb.append(buf, 0, n);
+				}
+				resetAndSkipFully(in, n);
+			}
+		} else {
+			StringBuilder buf = sizeHint > 0
+					? new StringBuilder(sizeHint)
+					: new StringBuilder();
+			int i;
+			while ((i = in.read()) != -1) {
+				char c = (char) i;
+				buf.append(c);
+				if (c == '\n') {
+					break;
+				}
+			}
+			return buf.toString();
+		}
+	}
+
+	private static void resetAndSkipFully(Reader fd, long toSkip) throws IOException {
+		fd.reset();
+		while (toSkip > 0) {
+			long r = fd.skip(toSkip);
+			if (r <= 0) {
+				throw new EOFException(JGitText.get().shortSkipOfBlock);
+			}
+			toSkip -= r;
+		}
+	}
+
 	private IO() {
 		// Don't create instances of a static only utility.
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/RawParseUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/RawParseUtils.java
index 3c2460c..45c339f 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/RawParseUtils.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/RawParseUtils.java
@@ -390,7 +390,28 @@
 	 * @return the timez