Merge "Revert "ResolveMerge only needs to visit differing TreeEntries""
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 d1c2580..a8e312d 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 org.eclipse.jgit.errors.UnpackException;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.InternalHttpServerGlue;
 import org.eclipse.jgit.transport.ReceivePack;
 import org.eclipse.jgit.transport.RefAdvertiser.PacketLineOutRefAdvertiser;
 import org.eclipse.jgit.transport.resolver.ReceivePackFactory;
@@ -100,6 +101,9 @@ protected void begin(HttpServletRequest req, Repository db)
 				throws IOException, ServiceNotEnabledException,
 				ServiceNotAuthorizedException {
 			ReceivePack rp = receivePackFactory.create(req, db);
+			InternalHttpServerGlue.setPeerUserAgent(
+					rp,
+					req.getHeader(HDR_USER_AGENT));
 			req.setAttribute(ATTRIBUTE_HANDLER, rp);
 		}
 
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 c5272b5..7aefcbd 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
@@ -74,6 +74,7 @@
 import javax.servlet.http.HttpServletResponse;
 
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.InternalHttpServerGlue;
 import org.eclipse.jgit.transport.RefAdvertiser.PacketLineOutRefAdvertiser;
 import org.eclipse.jgit.transport.UploadPack;
 import org.eclipse.jgit.transport.UploadPackInternalServerErrorException;
@@ -100,6 +101,9 @@ protected void begin(HttpServletRequest req, Repository db)
 				throws IOException, ServiceNotEnabledException,
 				ServiceNotAuthorizedException {
 			UploadPack up = uploadPackFactory.create(req, db);
+			InternalHttpServerGlue.setPeerUserAgent(
+					up,
+					req.getHeader(HDR_USER_AGENT));
 			req.setAttribute(ATTRIBUTE_HANDLER, up);
 		}
 
diff --git a/org.eclipse.jgit.packaging/pom.xml b/org.eclipse.jgit.packaging/pom.xml
index 3f552b6..5c6d51a 100644
--- a/org.eclipse.jgit.packaging/pom.xml
+++ b/org.eclipse.jgit.packaging/pom.xml
@@ -68,6 +68,10 @@
       <id>repo.eclipse.org.cbi-releases</id>
       <url>https://repo.eclipse.org/content/repositories/cbi-releases/</url>
     </pluginRepository>
+    <pluginRepository>
+      <id>repo.eclipse.org.cbi-snapshots</id>
+      <url>https://repo.eclipse.org/content/repositories/cbi-snapshots/</url>
+    </pluginRepository>
   </pluginRepositories>
 
   <modules>
@@ -210,7 +214,7 @@
         <plugin>
           <groupId>org.eclipse.cbi.maven.plugins</groupId>
           <artifactId>eclipse-jarsigner-plugin</artifactId>
-          <version>1.1.1</version>
+          <version>1.1.2-SNAPSHOT</version>
         </plugin>
         <plugin>
           <groupId>org.codehaus.mojo</groupId>
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 38c3a2f..7151de7 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
@@ -47,14 +47,15 @@
 import java.io.File;
 import java.io.IOException;
 import java.io.PrintWriter;
+import java.lang.reflect.InvocationTargetException;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.List;
 
-import org.eclipse.jgit.console.ConsoleAuthenticator;
-import org.eclipse.jgit.console.ConsoleCredentialsProvider;
+import org.eclipse.jgit.awtui.AwtAuthenticator;
+import org.eclipse.jgit.awtui.AwtCredentialsProvider;
 import org.eclipse.jgit.errors.TransportException;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.RepositoryBuilder;
@@ -115,8 +116,10 @@ public static void main(final String[] argv) {
 	 */
 	protected void run(final String[] argv) {
 		try {
-			ConsoleAuthenticator.install();
-			ConsoleCredentialsProvider.install();
+			if (!installConsole()) {
+				AwtAuthenticator.install();
+				AwtCredentialsProvider.install();
+			}
 			configureHttpProxy();
 			execute(argv);
 		} catch (Die err) {
@@ -246,6 +249,45 @@ protected Repository openGitDir(String aGitdir) throws IOException {
 		return rb.build();
 	}
 
+	private static boolean installConsole() {
+		try {
+			install("org.eclipse.jgit.console.ConsoleAuthenticator"); //$NON-NLS-1$
+			install("org.eclipse.jgit.console.ConsoleCredentialsProvider"); //$NON-NLS-1$
+			return true;
+		} catch (ClassNotFoundException e) {
+			return false;
+		} catch (NoClassDefFoundError e) {
+			return false;
+		} catch (UnsupportedClassVersionError e) {
+			return false;
+
+		} catch (IllegalArgumentException e) {
+			throw new RuntimeException(CLIText.get().cannotSetupConsole, e);
+		} catch (SecurityException e) {
+			throw new RuntimeException(CLIText.get().cannotSetupConsole, e);
+		} catch (IllegalAccessException e) {
+			throw new RuntimeException(CLIText.get().cannotSetupConsole, e);
+		} catch (InvocationTargetException e) {
+			throw new RuntimeException(CLIText.get().cannotSetupConsole, e);
+		} catch (NoSuchMethodException e) {
+			throw new RuntimeException(CLIText.get().cannotSetupConsole, e);
+		}
+	}
+
+	private static void install(final String name)
+			throws IllegalAccessException, InvocationTargetException,
+			NoSuchMethodException, ClassNotFoundException {
+		try {
+		Class.forName(name).getMethod("install").invoke(null); //$NON-NLS-1$
+		} catch (InvocationTargetException e) {
+			if (e.getCause() instanceof RuntimeException)
+				throw (RuntimeException) e.getCause();
+			if (e.getCause() instanceof Error)
+				throw (Error) e.getCause();
+			throw e;
+		}
+	}
+
 	/**
 	 * Configure the JRE's standard HTTP based on <code>http_proxy</code>.
 	 * <p>
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 c6578cc..274757d 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
@@ -121,6 +121,42 @@ public void testValidCommitBlankAuthor() throws CorruptObjectException {
 	}
 
 	@Test
+	public void testCommitCorruptAuthor() throws CorruptObjectException {
+		StringBuilder b = new StringBuilder();
+		b.append("tree be9bfa841874ccc9f2ef7c48d0c76226f89b7189\n");
+		b.append("author b <b@c> <b@c> 0 +0000\n");
+		b.append("committer <> 0 +0000\n");
+
+		byte[] data = Constants.encodeASCII(b.toString());
+		try {
+			checker.checkCommit(data);
+			fail("Did not catch corrupt object");
+		} catch (CorruptObjectException e) {
+			assertEquals("invalid author", e.getMessage());
+		}
+		checker.setAllowInvalidPersonIdent(true);
+		checker.checkCommit(data);
+	}
+
+	@Test
+	public void testCommitCorruptCommitter() throws CorruptObjectException {
+		StringBuilder b = new StringBuilder();
+		b.append("tree be9bfa841874ccc9f2ef7c48d0c76226f89b7189\n");
+		b.append("author <> 0 +0000\n");
+		b.append("committer b <b@c> <b@c> 0 +0000\n");
+
+		byte[] data = Constants.encodeASCII(b.toString());
+		try {
+			checker.checkCommit(data);
+			fail("Did not catch corrupt object");
+		} catch (CorruptObjectException e) {
+			assertEquals("invalid committer", e.getMessage());
+		}
+		checker.setAllowInvalidPersonIdent(true);
+		checker.checkCommit(data);
+	}
+
+	@Test
 	public void testValidCommit1Parent() throws CorruptObjectException {
 		final StringBuilder b = new StringBuilder();
 
@@ -940,7 +976,8 @@ public void testValidTagHasNoTaggerHeader() throws CorruptObjectException {
 	}
 
 	@Test
-	public void testInvalidTagInvalidTaggerHeader1() {
+	public void testInvalidTagInvalidTaggerHeader1()
+			throws CorruptObjectException {
 		final StringBuilder b = new StringBuilder();
 
 		b.append("object ");
@@ -958,6 +995,8 @@ public void testInvalidTagInvalidTaggerHeader1() {
 		} catch (CorruptObjectException e) {
 			assertEquals("invalid tagger", e.getMessage());
 		}
+		checker.setAllowInvalidPersonIdent(true);
+		checker.checkTag(data);
 	}
 
 	@Test
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/ObjectWalkTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/ObjectWalkTest.java
index 9cbb1c8..dfde7fc 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/ObjectWalkTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/ObjectWalkTest.java
@@ -215,21 +215,6 @@ public void testCull() throws Exception {
 	}
 
 	@Test
-	public void testMarkUninterestingPropagation() throws Exception {
-		final RevBlob f = blob("1");
-		final RevTree t = tree(file("f", f));
-		final RevCommit c1 = commit(t);
-		final RevCommit c2 = commit(t);
-
-		markUninteresting(c1);
-		markStart(c2);
-
-		assertSame(c2, objw.next());
-		assertNull(objw.next());
-		assertNull(objw.nextObject());
-	}
-
-	@Test
 	public void testEmptyTreeCorruption() throws Exception {
 		ObjectId bId = ObjectId
 				.fromString("abbbfafe3129f85747aba7bfac992af77134c607");
diff --git a/org.eclipse.jgit.ui/src/org/eclipse/jgit/awtui/AwtCredentialsProvider.java b/org.eclipse.jgit.ui/src/org/eclipse/jgit/awtui/AwtCredentialsProvider.java
new file mode 100644
index 0000000..fd26bfa
--- /dev/null
+++ b/org.eclipse.jgit.ui/src/org/eclipse/jgit/awtui/AwtCredentialsProvider.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2010, Google Inc.
+ * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
+ * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.awtui;
+
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JPasswordField;
+import javax.swing.JTextField;
+
+import org.eclipse.jgit.errors.UnsupportedCredentialItem;
+import org.eclipse.jgit.transport.CredentialItem;
+import org.eclipse.jgit.transport.CredentialsProvider;
+import org.eclipse.jgit.transport.URIish;
+
+/** Interacts with the user during authentication by using AWT/Swing dialogs. */
+public class AwtCredentialsProvider extends CredentialsProvider {
+	/** Install this implementation as the default. */
+	public static void install() {
+		CredentialsProvider.setDefault(new AwtCredentialsProvider());
+	}
+
+	@Override
+	public boolean isInteractive() {
+		return true;
+	}
+
+	@Override
+	public boolean supports(CredentialItem... items) {
+		for (CredentialItem i : items) {
+			if (i instanceof CredentialItem.StringType)
+				continue;
+
+			else if (i instanceof CredentialItem.CharArrayType)
+				continue;
+
+			else if (i instanceof CredentialItem.YesNoType)
+				continue;
+
+			else if (i instanceof CredentialItem.InformationalMessage)
+				continue;
+
+			else
+				return false;
+		}
+		return true;
+	}
+
+	@Override
+	public boolean get(URIish uri, CredentialItem... items)
+			throws UnsupportedCredentialItem {
+		if (items.length == 0) {
+			return true;
+
+		} else if (items.length == 1) {
+			final CredentialItem item = items[0];
+
+			if (item instanceof CredentialItem.InformationalMessage) {
+				JOptionPane.showMessageDialog(null, item.getPromptText(),
+						UIText.get().warning, JOptionPane.INFORMATION_MESSAGE);
+				return true;
+
+			} else if (item instanceof CredentialItem.YesNoType) {
+				CredentialItem.YesNoType v = (CredentialItem.YesNoType) item;
+				int r = JOptionPane.showConfirmDialog(null, v.getPromptText(),
+						UIText.get().warning, JOptionPane.YES_NO_OPTION);
+				switch (r) {
+				case JOptionPane.YES_OPTION:
+					v.setValue(true);
+					return true;
+
+				case JOptionPane.NO_OPTION:
+					v.setValue(false);
+					return true;
+
+				case JOptionPane.CANCEL_OPTION:
+				case JOptionPane.CLOSED_OPTION:
+				default:
+					return false;
+				}
+
+			} else {
+				return interactive(uri, items);
+			}
+
+		} else {
+			return interactive(uri, items);
+		}
+	}
+
+	private static boolean interactive(URIish uri, CredentialItem[] items) {
+		final GridBagConstraints gbc = new GridBagConstraints(0, 0, 1, 1, 1, 1,
+				GridBagConstraints.NORTHWEST, GridBagConstraints.NONE,
+				new Insets(0, 0, 0, 0), 0, 0);
+		final JPanel panel = new JPanel();
+		panel.setLayout(new GridBagLayout());
+
+		final JTextField[] texts = new JTextField[items.length];
+		for (int i = 0; i < items.length; i++) {
+			CredentialItem item = items[i];
+
+			if (item instanceof CredentialItem.StringType
+					|| item instanceof CredentialItem.CharArrayType) {
+				gbc.fill = GridBagConstraints.NONE;
+				gbc.gridwidth = GridBagConstraints.RELATIVE;
+				gbc.gridx = 0;
+				panel.add(new JLabel(item.getPromptText()), gbc);
+
+				gbc.fill = GridBagConstraints.HORIZONTAL;
+				gbc.gridwidth = GridBagConstraints.RELATIVE;
+				gbc.gridx = 1;
+				if (item.isValueSecure())
+					texts[i] = new JPasswordField(20);
+				else
+					texts[i] = new JTextField(20);
+				panel.add(texts[i], gbc);
+				gbc.gridy++;
+
+			} else if (item instanceof CredentialItem.InformationalMessage) {
+				gbc.fill = GridBagConstraints.NONE;
+				gbc.gridwidth = GridBagConstraints.REMAINDER;
+				gbc.gridx = 0;
+				panel.add(new JLabel(item.getPromptText()), gbc);
+				gbc.gridy++;
+
+			} else {
+				throw new UnsupportedCredentialItem(uri, item.getPromptText());
+			}
+		}
+
+		if (JOptionPane.showConfirmDialog(null, panel,
+				UIText.get().authenticationRequired,
+				JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE) != JOptionPane.OK_OPTION)
+			return false; // cancel
+
+		for (int i = 0; i < items.length; i++) {
+			CredentialItem item = items[i];
+			JTextField f = texts[i];
+
+			if (item instanceof CredentialItem.StringType) {
+				CredentialItem.StringType v = (CredentialItem.StringType) item;
+				if (f instanceof JPasswordField)
+					v.setValue(new String(((JPasswordField) f).getPassword()));
+				else
+					v.setValue(f.getText());
+
+			} else if (item instanceof CredentialItem.CharArrayType) {
+				CredentialItem.CharArrayType v = (CredentialItem.CharArrayType) item;
+				if (f instanceof JPasswordField)
+					v.setValueNoCopy(((JPasswordField) f).getPassword());
+				else
+					v.setValueNoCopy(f.getText().toCharArray());
+			}
+		}
+		return true;
+	}
+}
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 10f5339..6028618 100644
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
@@ -394,6 +394,7 @@
 packFileInvalid=Pack file invalid: {0}
 packfileIsTruncated=Packfile {0} is truncated.
 packfileIsTruncatedNoParam=Packfile is truncated.
+packHandleIsStale=Pack file {0} handle is stale, removing it from pack list
 packHasUnresolvedDeltas=pack has unresolved deltas
 packingCancelledDuringObjectsWriting=Packing cancelled during objects writing
 packObjectCountMismatch=Pack object count mismatch: pack {0} index {1}: {2}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java
index 17b1242..ac67037 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/ResetCommand.java
@@ -227,9 +227,9 @@ else if (repo.readSquashCommitMsg() != null)
 			setCallable(false);
 			return result;
 		} catch (IOException e) {
-			throw new JGitInternalException(
+			throw new JGitInternalException(MessageFormat.format(
 					JGitText.get().exceptionCaughtDuringExecutionOfResetCommand,
-					e);
+					e.getMessage()), e);
 		}
 	}
 
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 2448bf8..3077e18 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
@@ -453,6 +453,7 @@ public static JGitText get() {
 	/***/ public String packFileInvalid;
 	/***/ public String packfileIsTruncated;
 	/***/ public String packfileIsTruncatedNoParam;
+	/***/ public String packHandleIsStale;
 	/***/ public String packHasUnresolvedDeltas;
 	/***/ public String packingCancelledDuringObjectsWriting;
 	/***/ public String packObjectCountMismatch;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlock.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlock.java
index c185332..7926536 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlock.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlock.java
@@ -46,7 +46,6 @@
 package org.eclipse.jgit.internal.storage.dfs;
 
 import java.io.IOException;
-import java.security.MessageDigest;
 import java.util.zip.CRC32;
 import java.util.zip.DataFormatException;
 import java.util.zip.Inflater;
@@ -101,12 +100,9 @@ void crc32(CRC32 out, long pos, int cnt) {
 		out.update(block, ptr, cnt);
 	}
 
-	void write(PackOutputStream out, long pos, int cnt, MessageDigest digest)
+	void write(PackOutputStream out, long pos, int cnt)
 			throws IOException {
-		int ptr = (int) (pos - start);
-		out.write(block, ptr, cnt);
-		if (digest != null)
-			digest.update(block, ptr, cnt);
+		out.write(block, (int) (pos - start), cnt);
 	}
 
 	void check(Inflater inf, byte[] tmp, long pos, int cnt)
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCache.java
index 748a4a3..2e170a5 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCache.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCache.java
@@ -135,6 +135,9 @@ public static DfsBlockCache getInstance() {
 	/** Maximum number of bytes the cache should hold. */
 	private final long maxBytes;
 
+	/** Pack files smaller than this size can be copied through the cache. */
+	private final long maxStreamThroughCache;
+
 	/**
 	 * Suggested block size to read from pack files in.
 	 * <p>
@@ -191,6 +194,7 @@ else if (eb < 4)
 			eb = tableSize;
 
 		maxBytes = cfg.getBlockLimit();
+		maxStreamThroughCache = (long) (maxBytes * cfg.getStreamRatio());
 		blockSize = cfg.getBlockSize();
 		blockSizeShift = Integer.numberOfTrailingZeros(blockSize);
 
@@ -206,6 +210,10 @@ else if (eb < 4)
 		statMiss = new AtomicLong();
 	}
 
+	boolean shouldCopyThroughCache(long length) {
+		return length <= maxStreamThroughCache;
+	}
+
 	/** @return total number of bytes in the cache. */
 	public long getCurrentSize() {
 		return liveBytes;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheConfig.java
index ca1451a..a7d13de 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsBlockCacheConfig.java
@@ -47,7 +47,11 @@
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DFS_SECTION;
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_BLOCK_LIMIT;
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_BLOCK_SIZE;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_STREAM_RATIO;
 
+import java.text.MessageFormat;
+
+import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.Config;
 
 /** Configuration parameters for {@link DfsBlockCache}. */
@@ -59,13 +63,14 @@ public class DfsBlockCacheConfig {
 	public static final int MB = 1024 * KB;
 
 	private long blockLimit;
-
 	private int blockSize;
+	private double streamRatio;
 
 	/** Create a default configuration. */
 	public DfsBlockCacheConfig() {
 		setBlockLimit(32 * MB);
 		setBlockSize(64 * KB);
+		setStreamRatio(0.30);
 	}
 
 	/**
@@ -106,6 +111,27 @@ public DfsBlockCacheConfig setBlockSize(final int newSize) {
 	}
 
 	/**
+	 * @return highest percentage of {@link #getBlockLimit()} a single pack can
+	 *         occupy while being copied by the pack reuse strategy. <b>Default
+	 *         is 0.30, or 30%</b>.
+	 * @since 4.0
+	 */
+	public double getStreamRatio() {
+		return streamRatio;
+	}
+
+	/**
+	 * @param ratio
+	 *            percentage of cache to occupy with a copied pack.
+	 * @return {@code this}
+	 * @since 4.0
+	 */
+	public DfsBlockCacheConfig setStreamRatio(double ratio) {
+		streamRatio = Math.max(0, Math.min(ratio, 1.0));
+		return this;
+	}
+
+	/**
 	 * Update properties by setting fields from the configuration.
 	 * <p>
 	 * If a property is not defined in the configuration, then it is left
@@ -127,6 +153,22 @@ public DfsBlockCacheConfig fromConfig(final Config rc) {
 				CONFIG_DFS_SECTION,
 				CONFIG_KEY_BLOCK_SIZE,
 				getBlockSize()));
+
+		String v = rc.getString(
+				CONFIG_CORE_SECTION,
+				CONFIG_DFS_SECTION,
+				CONFIG_KEY_STREAM_RATIO);
+		if (v != null) {
+			try {
+				setStreamRatio(Double.parseDouble(v));
+			} catch (NumberFormatException e) {
+				throw new IllegalArgumentException(MessageFormat.format(
+						JGitText.get().enumValueNotSupported3,
+						CONFIG_CORE_SECTION,
+						CONFIG_DFS_SECTION,
+						CONFIG_KEY_STREAM_RATIO, v));
+			}
+		}
 		return this;
 	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsCachedPack.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsCachedPack.java
index 3da5184..a5308f6 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsCachedPack.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsCachedPack.java
@@ -78,8 +78,7 @@ public boolean hasObject(ObjectToPack obj, StoredObjectRepresentation rep) {
 		return ((DfsObjectRepresentation) rep).pack == pack;
 	}
 
-	void copyAsIs(PackOutputStream out, boolean validate, DfsReader ctx)
-			throws IOException {
-		pack.copyPackAsIs(out, validate, ctx);
+	void copyAsIs(PackOutputStream out, DfsReader ctx) throws IOException {
+		pack.copyPackAsIs(out, ctx);
 	}
 }
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 58df895..e03488b 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
@@ -54,6 +54,7 @@
 import java.io.EOFException;
 import java.io.IOException;
 import java.io.InputStream;
+import java.nio.ByteBuffer;
 import java.nio.channels.Channels;
 import java.text.MessageFormat;
 import java.util.Set;
@@ -80,7 +81,6 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectLoader;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.util.IO;
 import org.eclipse.jgit.util.LongList;
 
 /**
@@ -464,11 +464,73 @@ long getObjectCount(DfsReader ctx) throws IOException {
 		return dstbuf;
 	}
 
-	void copyPackAsIs(PackOutputStream out, boolean validate, DfsReader ctx)
+	void copyPackAsIs(PackOutputStream out, DfsReader ctx)
 			throws IOException {
-		// Pin the first window, this ensures the length is accurate.
-		ctx.pin(this, 0);
-		ctx.copyPackAsIs(this, length, validate, out);
+		// If the length hasn't been determined yet, pin to set it.
+		if (length == -1) {
+			ctx.pin(this, 0);
+			ctx.unpin();
+		}
+		if (cache.shouldCopyThroughCache(length))
+			copyPackThroughCache(out, ctx);
+		else
+			copyPackBypassCache(out, ctx);
+	}
+
+	private void copyPackThroughCache(PackOutputStream out, DfsReader ctx)
+			throws IOException {
+		long position = 12;
+		long remaining = length - (12 + 20);
+		while (0 < remaining) {
+			DfsBlock b = cache.getOrLoad(this, position, ctx);
+			int ptr = (int) (position - b.start);
+			int n = (int) Math.min(b.size() - ptr, remaining);
+			b.write(out, position, n);
+			position += n;
+			remaining -= n;
+		}
+	}
+
+	private long copyPackBypassCache(PackOutputStream out, DfsReader ctx)
+			throws IOException {
+		try (ReadableChannel rc = ctx.db.openFile(packDesc, PACK)) {
+			ByteBuffer buf = newCopyBuffer(out, rc);
+			if (ctx.getOptions().getStreamPackBufferSize() > 0)
+				rc.setReadAheadBytes(ctx.getOptions().getStreamPackBufferSize());
+			long position = 12;
+			long remaining = length - (12 + 20);
+			while (0 < remaining) {
+				DfsBlock b = cache.get(key, alignToBlock(position));
+				if (b != null) {
+					int ptr = (int) (position - b.start);
+					int n = (int) Math.min(b.size() - ptr, remaining);
+					b.write(out, position, n);
+					position += n;
+					remaining -= n;
+					rc.position(position);
+					continue;
+				}
+
+				buf.position(0);
+				int n = read(rc, buf);
+				if (n <= 0)
+					throw packfileIsTruncated();
+				else if (n > remaining)
+					n = (int) remaining;
+				out.write(buf.array(), 0, n);
+				position += n;
+				remaining -= n;
+			}
+			return position;
+		}
+	}
+
+	private ByteBuffer newCopyBuffer(PackOutputStream out, ReadableChannel rc) {
+		int bs = blockSize(rc);
+		byte[] copyBuf = out.getCopyBuffer();
+		if (bs > copyBuf.length)
+			copyBuf = new byte[bs];
+		return ByteBuffer.wrap(copyBuf, 0, bs);
 	}
 
 	void copyAsIs(PackOutputStream out, DfsObjectToPack src,
@@ -617,7 +679,7 @@ void copyAsIs(PackOutputStream out, DfsObjectToPack src,
 			// and we have it pinned.  Write this out without copying.
 			//
 			out.writeHeader(src, inflatedLength);
-			quickCopy.write(out, dataOffset, (int) dataLength, null);
+			quickCopy.write(out, dataOffset, (int) dataLength);
 
 		} else if (dataLength <= buf.length) {
 			// Tiny optimization: Lots of objects are very small deltas or
@@ -668,6 +730,12 @@ void setInvalid() {
 		invalid = true;
 	}
 
+	private IOException packfileIsTruncated() {
+		invalid = true;
+		return new IOException(MessageFormat.format(
+				JGitText.get().packfileIsTruncated, getPackName()));
+	}
+
 	private void readFully(long position, byte[] dstbuf, int dstoff, int cnt,
 			DfsReader ctx) throws IOException {
 		if (ctx.copy(this, position, dstbuf, dstoff, cnt) != cnt)
@@ -692,18 +760,8 @@ DfsBlock readOneBlock(long pos, DfsReader ctx)
 
 		ReadableChannel rc = ctx.db.openFile(packDesc, PACK);
 		try {
-			// If the block alignment is not yet known, discover it. Prefer the
-			// larger size from either the cache or the file itself.
-			int size = blockSize;
-			if (size == 0) {
-				size = rc.blockSize();
-				if (size <= 0)
-					size = cache.getBlockSize();
-				else if (size < cache.getBlockSize())
-					size = (cache.getBlockSize() / size) * size;
-				blockSize = size;
-				pos = (pos / size) * size;
-			}
+			int size = blockSize(rc);
+			pos = (pos / size) * size;
 
 			// If the size of the file is not yet known, try to discover it.
 			// Channels may choose to return -1 to indicate they don't
@@ -725,7 +783,7 @@ else if (size < cache.getBlockSize())
 
 			byte[] buf = new byte[size];
 			rc.position(pos);
-			int cnt = IO.read(rc, buf, 0, size);
+			int cnt = read(rc, ByteBuffer.wrap(buf, 0, size));
 			if (cnt != size) {
 				if (0 <= len) {
 					throw new EOFException(MessageFormat.format(
@@ -754,6 +812,30 @@ else if (size < cache.getBlockSize())
 		}
 	}
 
+	private int blockSize(ReadableChannel rc) {
+		// If the block alignment is not yet known, discover it. Prefer the
+		// larger size from either the cache or the file itself.
+		int size = blockSize;
+		if (size == 0) {
+			size = rc.blockSize();
+			if (size <= 0)
+				size = cache.getBlockSize();
+			else if (size < cache.getBlockSize())
+				size = (cache.getBlockSize() / size) * size;
+			blockSize = size;
+		}
+		return size;
+	}
+
+	private static int read(ReadableChannel rc, ByteBuffer buf)
+			throws IOException {
+		int n;
+		do {
+			n = rc.read(buf);
+		} while (0 < n && buf.hasRemaining());
+		return buf.position();
+	}
+
 	ObjectLoader load(DfsReader ctx, long pos)
 			throws IOException {
 		try {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReader.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReader.java
index 4cf7cbe..f5f3375 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReader.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReader.java
@@ -44,14 +44,10 @@
 
 package org.eclipse.jgit.internal.storage.dfs;
 
-import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK;
 import static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH;
 
 import java.io.IOException;
-import java.security.MessageDigest;
-import java.text.MessageFormat;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
@@ -65,7 +61,6 @@
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.errors.StoredObjectRepresentationNotAvailableException;
-import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.internal.storage.file.BitmapIndexImpl;
 import org.eclipse.jgit.internal.storage.file.PackBitmapIndex;
 import org.eclipse.jgit.internal.storage.file.PackIndex;
@@ -81,7 +76,6 @@
 import org.eclipse.jgit.lib.AsyncObjectSizeQueue;
 import org.eclipse.jgit.lib.BitmapIndex;
 import org.eclipse.jgit.lib.BitmapIndex.BitmapBuilder;
-import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.InflaterCache;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectLoader;
@@ -498,9 +492,9 @@ public void writeObjects(PackOutputStream out, List<ObjectToPack> list)
 			out.writeObject(otp);
 	}
 
-	public void copyPackAsIs(PackOutputStream out, CachedPack pack,
-			boolean validate) throws IOException {
-		((DfsCachedPack) pack).copyAsIs(out, validate, this);
+	public void copyPackAsIs(PackOutputStream out, CachedPack pack)
+			throws IOException {
+		((DfsCachedPack) pack).copyAsIs(out, this);
 	}
 
 	/**
@@ -547,52 +541,6 @@ int copy(DfsPackFile pack, long position, byte[] dstbuf, int dstoff, int cnt)
 		return cnt - need;
 	}
 
-	void copyPackAsIs(DfsPackFile pack, long length, boolean validate,
-			PackOutputStream out) throws IOException {
-		MessageDigest md = null;
-		if (validate) {
-			md = Constants.newMessageDigest();
-			byte[] buf = out.getCopyBuffer();
-			pin(pack, 0);
-			if (block.copy(0, buf, 0, 12) != 12) {
-				pack.setInvalid();
-				throw new IOException(MessageFormat.format(
-						JGitText.get().packfileIsTruncated, pack.getPackName()));
-			}
-			md.update(buf, 0, 12);
-		}
-
-		long position = 12;
-		long remaining = length - (12 + 20);
-		while (0 < remaining) {
-			pin(pack, position);
-
-			int ptr = (int) (position - block.start);
-			int n = (int) Math.min(block.size() - ptr, remaining);
-			block.write(out, position, n, md);
-			position += n;
-			remaining -= n;
-		}
-
-		if (md != null) {
-			byte[] buf = new byte[20];
-			byte[] actHash = md.digest();
-
-			pin(pack, position);
-			if (block.copy(position, buf, 0, 20) != 20) {
-				pack.setInvalid();
-				throw new IOException(MessageFormat.format(
-						JGitText.get().packfileIsTruncated, pack.getPackName()));
-			}
-			if (!Arrays.equals(actHash, buf)) {
-				pack.setInvalid();
-				throw new IOException(MessageFormat.format(
-						JGitText.get().packfileCorruptionDetected,
-						pack.getPackDescription().getFileName(PACK)));
-			}
-		}
-	}
-
 	/**
 	 * Inflate a region of the pack starting at {@code position}.
 	 *
@@ -664,6 +612,10 @@ void pin(DfsPackFile pack, long position) throws IOException {
 		}
 	}
 
+	void unpin() {
+		block = null;
+	}
+
 	/** Release the current window cursor. */
 	@Override
 	public void release() {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReaderOptions.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReaderOptions.java
index 2a62547..8419807 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReaderOptions.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/DfsReaderOptions.java
@@ -46,6 +46,7 @@
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_CORE_SECTION;
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_DFS_SECTION;
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_DELTA_BASE_CACHE_LIMIT;
+import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_STREAM_BUFFER;
 import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_STREAM_FILE_TRESHOLD;
 
 import org.eclipse.jgit.lib.Config;
@@ -60,9 +61,10 @@ public class DfsReaderOptions {
 	public static final int MiB = 1024 * KiB;
 
 	private int deltaBaseCacheLimit;
-
 	private int streamFileThreshold;
 
+	private int streamPackBufferSize;
+
 	/** Create a default reader configuration. */
 	public DfsReaderOptions() {
 		setDeltaBaseCacheLimit(10 * MiB);
@@ -105,6 +107,27 @@ public DfsReaderOptions setStreamFileThreshold(final int newLimit) {
 	}
 
 	/**
+	 * @return number of bytes to use for buffering when streaming a pack file
+	 *         during copying. If 0 the block size of the pack is used.
+	 * @since 4.0
+	 */
+	public int getStreamPackBufferSize() {
+		return streamPackBufferSize;
+	}
+
+	/**
+	 * @param bufsz
+	 *            new buffer size in bytes for buffers used when streaming pack
+	 *            files during copying.
+	 * @return {@code this}
+	 * @since 4.0
+	 */
+	public DfsReaderOptions setStreamPackBufferSize(int bufsz) {
+		streamPackBufferSize = Math.max(0, bufsz);
+		return this;
+	}
+
+	/**
 	 * Update properties by setting fields from the configuration.
 	 * <p>
 	 * If a property is not defined in the configuration, then it is left
@@ -130,6 +153,12 @@ public DfsReaderOptions fromConfig(Config rc) {
 		sft = Math.min(sft, maxMem / 4); // don't use more than 1/4 of the heap
 		sft = Math.min(sft, Integer.MAX_VALUE); // cannot exceed array length
 		setStreamFileThreshold((int) sft);
+
+		setStreamPackBufferSize(rc.getInt(
+				CONFIG_CORE_SECTION,
+				CONFIG_DFS_SECTION,
+				CONFIG_KEY_STREAM_BUFFER,
+				getStreamPackBufferSize()));
 		return this;
 	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/InMemoryRepository.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/InMemoryRepository.java
index ae05536..8e7af0d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/InMemoryRepository.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/InMemoryRepository.java
@@ -227,6 +227,10 @@ public long size() {
 		public int blockSize() {
 			return 0;
 		}
+
+		public void setReadAheadBytes(int b) {
+			// Unnecessary on a byte array.
+		}
 	}
 
 	private class MemRefDatabase extends DfsRefDatabase {
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/ReadableChannel.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/ReadableChannel.java
index 5ec7079..240d552 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/ReadableChannel.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/ReadableChannel.java
@@ -100,4 +100,33 @@ public interface ReadableChannel extends ReadableByteChannel {
 	 *         not need to be a power of 2.
 	 */
 	public int blockSize();
+
+	/**
+	 * Recommend the channel maintain a read-ahead buffer.
+	 * <p>
+	 * A read-ahead buffer of approximately {@code bufferSize} in bytes may be
+	 * allocated and used by the channel to smooth out latency for read.
+	 * <p>
+	 * Callers can continue to read in smaller than {@code bufferSize} chunks.
+	 * With read-ahead buffering enabled read latency may fluctuate in a pattern
+	 * of one slower read followed by {@code (bufferSize / readSize) - 1} fast
+	 * reads satisfied by the read-ahead buffer. When summed up overall time to
+	 * read the same contiguous range should be lower than if read-ahead was not
+	 * enabled, as the implementation can combine reads to increase throughput.
+	 * <p>
+	 * To avoid unnecessary IO callers should only enable read-ahead if the
+	 * majority of the channel will be accessed in order.
+	 * <p>
+	 * Implementations may chose to read-ahead using asynchronous APIs or
+	 * background threads, or may simply aggregate reads using a buffer.
+	 * <p>
+	 * This read ahead stays in effect until the channel is closed or the buffer
+	 * size is set to 0.
+	 *
+	 * @param bufferSize
+	 *            requested size of the read ahead buffer, in bytes.
+	 * @throws IOException
+	 *             if the read ahead cannot be adjusted.
+	 */
+	public void setReadAheadBytes(int bufferSize) throws IOException;
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ByteArrayWindow.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ByteArrayWindow.java
index 863c553..dc720bc 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ByteArrayWindow.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ByteArrayWindow.java
@@ -46,7 +46,6 @@
 package org.eclipse.jgit.internal.storage.file;
 
 import java.io.IOException;
-import java.security.MessageDigest;
 import java.util.zip.CRC32;
 import java.util.zip.DataFormatException;
 import java.util.zip.Inflater;
@@ -84,12 +83,10 @@ void crc32(CRC32 out, long pos, int cnt) {
 	}
 
 	@Override
-	void write(PackOutputStream out, long pos, int cnt, MessageDigest digest)
+	void write(PackOutputStream out, long pos, int cnt)
 			throws IOException {
 		int ptr = (int) (pos - start);
 		out.write(array, ptr, cnt);
-		if (digest != null)
-			digest.update(array, ptr, cnt);
 	}
 
 	void check(Inflater inf, byte[] tmp, long pos, int cnt)
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ByteBufferWindow.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ByteBufferWindow.java
index 31925d2..05ddd69 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ByteBufferWindow.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ByteBufferWindow.java
@@ -47,7 +47,6 @@
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
-import java.security.MessageDigest;
 import java.util.zip.DataFormatException;
 import java.util.zip.Inflater;
 
@@ -76,7 +75,7 @@ protected int copy(final int p, final byte[] b, final int o, int n) {
 	}
 
 	@Override
-	void write(PackOutputStream out, long pos, int cnt, MessageDigest digest)
+	void write(PackOutputStream out, long pos, int cnt)
 			throws IOException {
 		final ByteBuffer s = buffer.slice();
 		s.position((int) (pos - start));
@@ -86,8 +85,6 @@ void write(PackOutputStream out, long pos, int cnt, MessageDigest digest)
 			int n = Math.min(cnt, buf.length);
 			s.get(buf, 0, n);
 			out.write(buf, 0, n);
-			if (digest != null)
-				digest.update(buf, 0, n);
 			cnt -= n;
 		}
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ByteWindow.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ByteWindow.java
index ab5eb7c..e774a14 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ByteWindow.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/ByteWindow.java
@@ -45,7 +45,6 @@
 package org.eclipse.jgit.internal.storage.file;
 
 import java.io.IOException;
-import java.security.MessageDigest;
 import java.util.zip.DataFormatException;
 import java.util.zip.Inflater;
 
@@ -121,8 +120,8 @@ final int copy(long pos, byte[] dstbuf, int dstoff, int cnt) {
 	 */
 	protected abstract int copy(int pos, byte[] dstbuf, int dstoff, int cnt);
 
-	abstract void write(PackOutputStream out, long pos, int cnt,
-			MessageDigest md) throws IOException;
+	abstract void write(PackOutputStream out, long pos, int cnt)
+			throws IOException;
 
 	final int setInput(long pos, Inflater inf) throws DataFormatException {
 		return setInput((int) (pos - start), inf);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LocalCachedPack.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LocalCachedPack.java
index b70ebcf..fd9dcda 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LocalCachedPack.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LocalCachedPack.java
@@ -79,10 +79,10 @@ public long getObjectCount() throws IOException {
 		return cnt;
 	}
 
-	void copyAsIs(PackOutputStream out, boolean validate, WindowCursor wc)
+	void copyAsIs(PackOutputStream out, WindowCursor wc)
 			throws IOException {
 		for (PackFile pack : getPacks())
-			pack.copyPackAsIs(out, validate, wc);
+			pack.copyPackAsIs(out, wc);
 	}
 
 	@Override
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 687408e..796109a 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,6 +114,8 @@ public class ObjectDirectory extends FileObjectDatabase {
 	/** 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;
@@ -554,22 +556,35 @@ void selectObjectRepresentation(PackWriter packer, ObjectToPack otp,
 	}
 
 	private void handlePackError(IOException e, PackFile p) {
-		String tmpl;
+		String warnTmpl = null;
 		if ((e instanceof CorruptObjectException)
 				|| (e instanceof PackInvalidException)) {
-			tmpl = JGitText.get().corruptPack;
+			warnTmpl = JGitText.get().corruptPack;
 			// Assume the pack is corrupted, and remove it from the list.
 			removePack(p);
 		} else if (e instanceof FileNotFoundException) {
-			tmpl = JGitText.get().packWasDeleted;
+			warnTmpl = JGitText.get().packWasDeleted;
 			removePack(p);
+		} else if (e.getMessage() != null
+				&& e.getMessage().toLowerCase().contains(STALE_FILE_HANDLE_MSG)) {
+			warnTmpl = JGitText.get().packHandleIsStale;
+			removePack(p);
+		}
+		if (warnTmpl != null) {
+			if (LOG.isDebugEnabled()) {
+				LOG.debug(MessageFormat.format(warnTmpl,
+						p.getPackFile().getAbsolutePath()), e);
+			} else {
+				LOG.warn(MessageFormat.format(warnTmpl,
+						p.getPackFile().getAbsolutePath()));
+			}
 		} else {
-			tmpl = JGitText.get().exceptionWhileReadingPack;
 			// Don't remove the pack from the list, as the error may be
 			// transient.
+			LOG.error(MessageFormat.format(
+					JGitText.get().exceptionWhileReadingPack, p.getPackFile()
+							.getAbsolutePath()), e);
 		}
-		LOG.error(MessageFormat.format(tmpl,
-				p.getPackFile().getAbsolutePath()), e);
 	}
 
 	@Override
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 ad3322e..75c361e 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
@@ -344,11 +344,11 @@ ObjectId findObjectForOffset(final long offset) throws IOException {
 		return dstbuf;
 	}
 
-	void copyPackAsIs(PackOutputStream out, boolean validate, WindowCursor curs)
+	void copyPackAsIs(PackOutputStream out, WindowCursor curs)
 			throws IOException {
 		// Pin the first window, this ensures the length is accurate.
 		curs.pin(this, 0);
-		curs.copyPackAsIs(this, length, validate, out);
+		curs.copyPackAsIs(this, length, out);
 	}
 
 	final void copyAsIs(PackOutputStream out, LocalObjectToPack src,
@@ -502,7 +502,7 @@ private void copyAsIs2(PackOutputStream out, LocalObjectToPack src,
 			// and we have it pinned.  Write this out without copying.
 			//
 			out.writeHeader(src, inflatedLength);
-			quickCopy.write(out, dataOffset, (int) dataLength, null);
+			quickCopy.write(out, dataOffset, (int) dataLength);
 
 		} else if (dataLength <= buf.length) {
 			// Tiny optimization: Lots of objects are very small deltas or
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/WindowCursor.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/WindowCursor.java
index 85c3c74..21d6cd2 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/WindowCursor.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/WindowCursor.java
@@ -45,9 +45,6 @@
 package org.eclipse.jgit.internal.storage.file;
 
 import java.io.IOException;
-import java.security.MessageDigest;
-import java.text.MessageFormat;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
@@ -59,7 +56,6 @@
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.errors.StoredObjectRepresentationNotAvailableException;
-import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.internal.storage.pack.CachedPack;
 import org.eclipse.jgit.internal.storage.pack.ObjectReuseAsIs;
 import org.eclipse.jgit.internal.storage.pack.ObjectToPack;
@@ -232,27 +228,13 @@ int copy(final PackFile pack, long position, final byte[] dstbuf,
 		return cnt - need;
 	}
 
-	public void copyPackAsIs(PackOutputStream out, CachedPack pack,
-			boolean validate) throws IOException {
-		((LocalCachedPack) pack).copyAsIs(out, validate, this);
+	public void copyPackAsIs(PackOutputStream out, CachedPack pack)
+			throws IOException {
+		((LocalCachedPack) pack).copyAsIs(out, this);
 	}
 
-	void copyPackAsIs(final PackFile pack, final long length, boolean validate,
+	void copyPackAsIs(final PackFile pack, final long length,
 			final PackOutputStream out) throws IOException {
-		MessageDigest md = null;
-		if (validate) {
-			md = Constants.newMessageDigest();
-			byte[] buf = out.getCopyBuffer();
-			pin(pack, 0);
-			if (window.copy(0, buf, 0, 12) != 12) {
-				pack.setInvalid();
-				throw new IOException(MessageFormat.format(
-						JGitText.get().packfileIsTruncated, pack.getPackFile()
-								.getPath()));
-			}
-			md.update(buf, 0, 12);
-		}
-
 		long position = 12;
 		long remaining = length - (12 + 20);
 		while (0 < remaining) {
@@ -260,29 +242,10 @@ void copyPackAsIs(final PackFile pack, final long length, boolean validate,
 
 			int ptr = (int) (position - window.start);
 			int n = (int) Math.min(window.size() - ptr, remaining);
-			window.write(out, position, n, md);
+			window.write(out, position, n);
 			position += n;
 			remaining -= n;
 		}
-
-		if (md != null) {
-			byte[] buf = new byte[20];
-			byte[] actHash = md.digest();
-
-			pin(pack, position);
-			if (window.copy(position, buf, 0, 20) != 20) {
-				pack.setInvalid();
-				throw new IOException(MessageFormat.format(
-						JGitText.get().packfileIsTruncated, pack.getPackFile()
-								.getPath()));
-			}
-			if (!Arrays.equals(actHash, buf)) {
-				pack.setInvalid();
-				throw new IOException(MessageFormat.format(
-						JGitText.get().packfileCorruptionDetected, pack
-								.getPackFile().getPath()));
-			}
-		}
 	}
 
 	/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/ObjectReuseAsIs.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/ObjectReuseAsIs.java
index 00b6b65..2e5d599 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/ObjectReuseAsIs.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/pack/ObjectReuseAsIs.java
@@ -209,16 +209,11 @@ public void copyObjectAsIs(PackOutputStream out, ObjectToPack otp,
 	 *            stream to append the pack onto.
 	 * @param pack
 	 *            the cached pack to send.
-	 * @param validate
-	 *            if true the representation must be validated and not be
-	 *            corrupt before being reused. If false, validation may be
-	 *            skipped as it will be performed elsewhere in the processing
-	 *            pipeline.
 	 * @throws IOException
 	 *             the pack cannot be read, or stream did not accept a write.
 	 */
-	public abstract void copyPackAsIs(PackOutputStream out, CachedPack pack,
-			boolean validate) throws IOException;
+	public abstract void copyPackAsIs(PackOutputStream out, CachedPack pack)
+			throws IOException;
 
 	/**
 	 * Obtain the available cached packs that match the bitmap and update
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 510538d..6d0c8e6 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
@@ -1048,7 +1048,7 @@ public void writePack(ProgressMonitor compressMonitor,
 				stats.reusedObjects += pack.getObjectCount();
 				stats.reusedDeltas += deltaCnt;
 				stats.totalDeltas += deltaCnt;
-				reuseSupport.copyPackAsIs(out, pack, reuseValidate);
+				reuseSupport.copyPackAsIs(out, pack);
 			}
 			writeChecksum(out);
 			out.flush();
@@ -1866,7 +1866,7 @@ private void findObjectsToPackUsingBitmaps(
 				false);
 		BitmapBuilder needBitmap = wantBitmap.andNot(haveBitmap);
 
-		if (useCachedPacks && reuseSupport != null
+		if (useCachedPacks && reuseSupport != null && !reuseValidate
 				&& (excludeInPacks == null || excludeInPacks.length == 0))
 			cachedPacks.addAll(
 					reuseSupport.getCachedPacksAndUpdate(needBitmap));
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
index 8a2080b..a89bcee 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
@@ -299,4 +299,16 @@ public class ConfigConstants {
 	 * @since 3.3
 	 */
 	public static final String CONFIG_KEY_PRUNE = "prune";
+
+	/**
+	 * The "streamBuffer" key
+	 * @since 4.0
+	 */
+	public static final String CONFIG_KEY_STREAM_BUFFER = "streamBuffer";
+
+	/**
+	 * The "streamRatio" key
+	 * @since 4.0
+	 */
+	public static final String CONFIG_KEY_STREAM_RATIO = "streamRatio";
 }
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 8435c9a..359b592 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectChecker.java
@@ -104,6 +104,8 @@ public class ObjectChecker {
 	private final MutableInteger ptrout = new MutableInteger();
 
 	private boolean allowZeroMode;
+
+	private boolean allowInvalidPersonIdent;
 	private boolean windows;
 	private boolean macosx;
 
@@ -125,6 +127,22 @@ public ObjectChecker setAllowLeadingZeroFileMode(boolean allow) {
 	}
 
 	/**
+	 * Enable accepting invalid author, committer and tagger identities.
+	 * <p>
+	 * Some broken Git versions/libraries allowed users to create commits and
+	 * tags with invalid formatting between the name, email and timestamp.
+	 *
+	 * @param allow
+	 *            if true accept invalid person identity strings.
+	 * @return {@code this}.
+	 * @since 4.0
+	 */
+	public ObjectChecker setAllowInvalidPersonIdent(boolean allow) {
+		allowInvalidPersonIdent = allow;
+		return this;
+	}
+
+	/**
 	 * Restrict trees to only names legal on Windows platforms.
 	 * <p>
 	 * Also rejects any mixed case forms of reserved names ({@code .git}).
@@ -198,6 +216,9 @@ private int id(final byte[] raw, final int ptr) {
 	}
 
 	private int personIdent(final byte[] raw, int ptr) {
+		if (allowInvalidPersonIdent)
+			return nextLF(raw, ptr) - 1;
+
 		final int emailB = nextLF(raw, ptr, '<');
 		if (emailB == ptr || raw[emailB - 1] != '<')
 			return -1;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ObjectWalk.java b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ObjectWalk.java
index b73ccb1..a0af067 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ObjectWalk.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/revwalk/ObjectWalk.java
@@ -232,7 +232,7 @@ public void markUninteresting(RevObject o) throws MissingObjectException,
 		}
 
 		if (o instanceof RevCommit)
-			markUninteresting((RevCommit) o);
+			super.markUninteresting((RevCommit) o);
 		else if (o instanceof RevTree)
 			markTreeUninteresting((RevTree) o);
 		else
@@ -242,18 +242,6 @@ else if (o instanceof RevTree)
 			addObject(o);
 	}
 
-	@Override
-	public void markUninteresting(RevCommit c) throws MissingObjectException,
-			IncorrectObjectTypeException, IOException {
-		super.markUninteresting(c);
-		try {
-			markTreeUninteresting(c.getTree());
-		} catch (MissingObjectException e) {
-			// we don't care if the tree of the commit does not exist locally
-		}
-	}
-
-	@Override
 	public void sort(RevSort s) {
 		super.sort(s);
 		boundary = hasRevSort(RevSort.BOUNDARY);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseConnection.java
index 1072d58..59ff1bd 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseConnection.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseConnection.java
@@ -65,6 +65,8 @@
 public abstract class BaseConnection implements Connection {
 	private Map<String, Ref> advertisedRefs = Collections.emptyMap();
 
+	private String peerUserAgent;
+
 	private boolean startedOperation;
 
 	private Writer messageWriter;
@@ -85,6 +87,28 @@ public String getMessages() {
 		return messageWriter != null ? messageWriter.toString() : ""; //$NON-NLS-1$
 	}
 
+	/**
+	 * User agent advertised by the remote server.
+	 *
+	 * @return agent (version of Git) running on the remote server. Null if the
+	 *         server does not advertise this version.
+	 * @since 4.0
+	 */
+	public String getPeerUserAgent() {
+		return peerUserAgent;
+	}
+
+	/**
+	 * Remember the remote peer's agent.
+	 *
+	 * @param agent
+	 *            remote peer agent string.
+	 * @since 4.0
+	 */
+	protected void setPeerUserAgent(String agent) {
+		peerUserAgent = agent;
+	}
+
 	public abstract void close();
 
 	/**
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java
index 8f825ea..7f9cec7 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackConnection.java
@@ -46,6 +46,8 @@
 
 package org.eclipse.jgit.transport;
 
+import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_AGENT;
+
 import java.io.EOFException;
 import java.io.IOException;
 import java.io.InputStream;
@@ -275,6 +277,18 @@ protected boolean wantCapability(final StringBuilder b, final String option) {
 		return true;
 	}
 
+	protected void addUserAgentCapability(StringBuilder b) {
+		String a = UserAgent.get();
+		if (a != null && UserAgent.hasAgent(remoteCapablities)) {
+			b.append(' ').append(OPTION_AGENT).append('=').append(a);
+		}
+	}
+
+	@Override
+	public String getPeerUserAgent() {
+		return UserAgent.getAgent(remoteCapablities, super.getPeerUserAgent());
+	}
+
 	private PackProtocolException duplicateAdvertisement(final String name) {
 		return new PackProtocolException(uri, MessageFormat.format(JGitText.get().duplicateAdvertisementsOf, name));
 	}
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 4036c00..a6fc633 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackFetchConnection.java
@@ -521,6 +521,7 @@ else if (wantCapability(line, OPTION_SIDE_BAND))
 					OPTION_MULTI_ACK_DETAILED));
 		}
 
+		addUserAgentCapability(line);
 		return line.toString();
 	}
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java
index 863934d..1e5b8e8 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BasePackPushConnection.java
@@ -268,6 +268,7 @@ private String enableCapabilities(final ProgressMonitor monitor,
 					outputStream);
 			pckIn = new PacketLineIn(in);
 		}
+		addUserAgentCapability(line);
 
 		if (line.length() > 0)
 			line.setCharAt(0, '\0');
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 b7a599b..cf6b2fd 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/BaseReceivePack.java
@@ -48,6 +48,7 @@
 import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_OFS_DELTA;
 import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_REPORT_STATUS;
 import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_SIDE_BAND_64K;
+import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_AGENT;
 import static org.eclipse.jgit.transport.SideBandOutputStream.CH_DATA;
 import static org.eclipse.jgit.transport.SideBandOutputStream.CH_PROGRESS;
 import static org.eclipse.jgit.transport.SideBandOutputStream.MAX_BUF;
@@ -224,6 +225,7 @@ public Set<String> getCapabilities() {
 
 	/** Capabilities requested by the client. */
 	private Set<String> enabledCapabilities;
+	String userAgent;
 	private Set<ObjectId> clientShallowCommits;
 	private List<ReceiveCommand> commands;
 
@@ -289,6 +291,7 @@ public ReceiveConfig parse(final Config cfg) {
 
 		final boolean checkReceivedObjects;
 		final boolean allowLeadingZeroFileMode;
+		final boolean allowInvalidPersonIdent;
 		final boolean safeForWindows;
 		final boolean safeForMacOS;
 
@@ -306,6 +309,8 @@ public ReceiveConfig parse(final Config cfg) {
 					config.getBoolean("transfer", "fsckobjects", false)); //$NON-NLS-1$ //$NON-NLS-2$
 			allowLeadingZeroFileMode = checkReceivedObjects
 					&& config.getBoolean("fsck", "allowLeadingZeroFileMode", false); //$NON-NLS-1$ //$NON-NLS-2$
+			allowInvalidPersonIdent = checkReceivedObjects
+					&& config.getBoolean("fsck", "allowInvalidPersonIdent", false); //$NON-NLS-1$ //$NON-NLS-2$
 			safeForWindows = checkReceivedObjects
 					&& config.getBoolean("fsck", "safeForWindows", false); //$NON-NLS-1$ //$NON-NLS-2$
 			safeForMacOS = checkReceivedObjects
@@ -326,6 +331,7 @@ ObjectChecker newObjectChecker() {
 				return null;
 			return new ObjectChecker()
 				.setAllowLeadingZeroFileMode(allowLeadingZeroFileMode)
+				.setAllowInvalidPersonIdent(allowInvalidPersonIdent)
 				.setSafeForWindows(safeForWindows)
 				.setSafeForMacOS(safeForMacOS);
 		}
@@ -734,6 +740,25 @@ public boolean isSideBand() throws RequestNotYetReadException {
 		return enabledCapabilities.contains(CAPABILITY_SIDE_BAND_64K);
 	}
 
+	/**
+	 * Get the user agent of the client.
+	 * <p>
+	 * If the client is new enough to use {@code agent=} capability that value
+	 * will be returned. Older HTTP clients may also supply their version using
+	 * the HTTP {@code User-Agent} header. The capability overrides the HTTP
+	 * header if both are available.
+	 * <p>
+	 * When an HTTP request has been received this method returns the HTTP
+	 * {@code User-Agent} header value until capabilities have been parsed.
+	 *
+	 * @return user agent supplied by the client. Available only if the client
+	 *         is new enough to advertise its user agent.
+	 * @since 4.0
+	 */
+	public String getPeerUserAgent() {
+		return UserAgent.getAgent(enabledCapabilities, userAgent);
+	}
+
 	/** @return all of the command received by the current request. */
 	public List<ReceiveCommand> getAllCommands() {
 		return Collections.unmodifiableList(commands);
@@ -951,6 +976,7 @@ public void sendAdvertisedRefs(final RefAdvertiser adv)
 			adv.advertiseCapability(CAPABILITY_ATOMIC);
 		if (allowOfsDelta)
 			adv.advertiseCapability(CAPABILITY_OFS_DELTA);
+		adv.advertiseCapability(OPTION_AGENT, UserAgent.get());
 		adv.send(getAdvertisedOrDefaultRefs());
 		for (ObjectId obj : advertisedHaves)
 			adv.advertiseHave(obj);
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Connection.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Connection.java
index e386c26..0ff9fce 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/Connection.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/Connection.java
@@ -127,4 +127,13 @@ public interface Connection {
 	 *         remote produced no additional messages.
 	 */
 	public String getMessages();
+
+	/**
+	 * User agent advertised by the remote server.
+	 *
+	 * @return agent (version of Git) running on the remote server. Null if the
+	 *         server does not advertise this version.
+	 * @since 4.0
+	 */
+	public String getPeerUserAgent();
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java
index d2902a3..9aae1c3 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/FetchProcess.java
@@ -136,6 +136,7 @@ private void executeImp(final ProgressMonitor monitor,
 		conn = transport.openFetch();
 		try {
 			result.setAdvertisedRefs(transport.getURI(), conn.getRefsMap());
+			result.peerUserAgent = conn.getPeerUserAgent();
 			final Set<Ref> matched = new HashSet<Ref>();
 			for (final RefSpec spec : toFetch) {
 				if (spec.getSource() == null)
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 27052db..8d9d2b7 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/GitProtocolConstants.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/GitProtocolConstants.java
@@ -186,6 +186,13 @@ public class GitProtocolConstants {
 	 */
 	public static final String CAPABILITY_PUSH_CERT = "push-cert"; //$NON-NLS-1$
 
+	/**
+	 * Implementation name and version of the client or server.
+	 *
+	 * @since 4.0
+	 */
+	public static final String OPTION_AGENT = "agent"; //$NON-NLS-1$
+
 	static enum MultiAck {
 		OFF, CONTINUE, DETAILED;
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/InternalHttpServerGlue.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/InternalHttpServerGlue.java
new file mode 100644
index 0000000..fe7aaf7
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/InternalHttpServerGlue.java
@@ -0,0 +1,80 @@
+/*
+ * 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;
+
+/**
+ * Internal API to to assist {@code org.eclipse.jgit.http.server}.
+ * <p>
+ * <b>Do not call.</b>
+ *
+ * @since 4.0
+ */
+public class InternalHttpServerGlue {
+	/**
+	 * Apply a default user agent for a request.
+	 *
+	 * @param up
+	 *            current UploadPack instance.
+	 * @param agent
+	 *            user agent string from the HTTP headers.
+	 */
+	public static void setPeerUserAgent(UploadPack up, String agent) {
+		up.userAgent = agent;
+	}
+
+	/**
+	 * Apply a default user agent for a request.
+	 *
+	 * @param rp
+	 *            current ReceivePack instance.
+	 * @param agent
+	 *            user agent string from the HTTP headers.
+	 */
+	public static void setPeerUserAgent(ReceivePack rp, String agent) {
+		rp.userAgent = agent;
+	}
+
+	private InternalHttpServerGlue() {
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/OperationResult.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/OperationResult.java
index b4a48b0..ad51f3e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/OperationResult.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/OperationResult.java
@@ -68,6 +68,8 @@ public abstract class OperationResult {
 
 	StringBuilder messageBuffer;
 
+	String peerUserAgent;
+
 	/**
 	 * Get the URI this result came from.
 	 * <p>
@@ -165,4 +167,15 @@ void addMessages(final String msg) {
 				messageBuffer.append('\n');
 		}
 	}
+
+	/**
+	 * Get the user agent advertised by the peer server, if available.
+	 *
+	 * @return advertised user agent, e.g. {@code "JGit/4.0"}. Null if the peer
+	 *         did not advertise version information.
+	 * @since 4.0
+	 */
+	public String getPeerUserAgent() {
+		return peerUserAgent;
+	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java
index 53fba55..00f84f7 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/PushProcess.java
@@ -155,6 +155,7 @@ PushResult execute(final ProgressMonitor monitor)
 			try {
 				res.setAdvertisedRefs(transport.getURI(), connection
 						.getRefsMap());
+				res.peerUserAgent = connection.getPeerUserAgent();
 				res.setRemoteUpdates(toPush);
 				monitor.endTask();
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefAdvertiser.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefAdvertiser.java
index 76547a6..f72a4b2 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefAdvertiser.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/RefAdvertiser.java
@@ -148,6 +148,21 @@ public void advertiseCapability(String name) {
 	}
 
 	/**
+	 * Add one protocol capability with a value ({@code "name=value"}).
+	 *
+	 * @param name
+	 *            name of the capability.
+	 * @param value
+	 *            value. If null the capability will not be added.
+	 * @since 4.0
+	 */
+	public void advertiseCapability(String name, String value) {
+		if (value != null) {
+			capablities.add(name + '=' + value);
+		}
+	}
+
+	/**
 	 * Add a symbolic ref to capabilities.
 	 * <p>
 	 * This method must be invoked prior to any of the following:
@@ -164,8 +179,7 @@ public void advertiseCapability(String name) {
 	 * @since 3.6
 	 */
 	public void addSymref(String from, String to) {
-		String symref = String.format("%s=%s:%s", OPTION_SYMREF, from, to); //$NON-NLS-1$
-		advertiseCapability(symref);
+		advertiseCapability(OPTION_SYMREF, from + ':' + to);
 	}
 
 	/**
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 1de91a5..60043ff 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransferConfig.java
@@ -67,6 +67,7 @@ public TransferConfig parse(final Config cfg) {
 
 	private final boolean checkReceivedObjects;
 	private final boolean allowLeadingZeroFileMode;
+	private final boolean allowInvalidPersonIdent;
 	private final boolean safeForWindows;
 	private final boolean safeForMacOS;
 	private final boolean allowTipSha1InWant;
@@ -82,6 +83,8 @@ private TransferConfig(final Config rc) {
 				rc.getBoolean("transfer", "fsckobjects", false)); //$NON-NLS-1$ //$NON-NLS-2$
 		allowLeadingZeroFileMode = checkReceivedObjects
 				&& rc.getBoolean("fsck", "allowLeadingZeroFileMode", false); //$NON-NLS-1$ //$NON-NLS-2$
+		allowInvalidPersonIdent = checkReceivedObjects
+				&& rc.getBoolean("fsck", "allowInvalidPersonIdent", false); //$NON-NLS-1$ //$NON-NLS-2$
 		safeForWindows = checkReceivedObjects
 				&& rc.getBoolean("fsck", "safeForWindows", //$NON-NLS-1$ //$NON-NLS-2$
 						SystemReader.getInstance().isWindows());
@@ -113,6 +116,7 @@ public ObjectChecker newObjectChecker() {
 			return null;
 		return new ObjectChecker()
 			.setAllowLeadingZeroFileMode(allowLeadingZeroFileMode)
+			.setAllowInvalidPersonIdent(allowInvalidPersonIdent)
 			.setSafeForWindows(safeForWindows)
 			.setSafeForMacOS(safeForMacOS);
 	}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java
index 82d1737..b23771e 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java
@@ -134,8 +134,6 @@ public class TransportHttp extends HttpTransport implements WalkTransport,
 
 	private static final String SVC_RECEIVE_PACK = "git-receive-pack"; //$NON-NLS-1$
 
-	private static final String userAgent = computeUserAgent();
-
 	static final TransportProtocol PROTO_HTTP = new TransportProtocol() {
 		private final String[] schemeNames = { "http", "https" }; //$NON-NLS-1$ //$NON-NLS-2$
 
@@ -204,17 +202,6 @@ public Transport open(URIish uri, Repository local, String remoteName)
 		}
 	};
 
-	private static String computeUserAgent() {
-		String version;
-		final Package pkg = TransportHttp.class.getPackage();
-		if (pkg != null && pkg.getImplementationVersion() != null) {
-			version = pkg.getImplementationVersion();
-		} else {
-			version = "unknown"; //$NON-NLS-1$
-		}
-		return "JGit/" + version; //$NON-NLS-1$
-	}
-
 	private static final Config.SectionParser<HttpConfig> HTTP_KEY = new SectionParser<HttpConfig>() {
 		public HttpConfig parse(final Config cfg) {
 			return new HttpConfig(cfg);
@@ -309,16 +296,17 @@ public FetchConnection openFetch() throws TransportException,
 			final HttpConnection c = connect(service);
 			final InputStream in = openInputStream(c);
 			try {
+				BaseConnection f;
 				if (isSmartHttp(c, service)) {
 					readSmartHeaders(in, service);
-					return new SmartHttpFetchConnection(in);
-
+					f = new SmartHttpFetchConnection(in);
 				} else {
 					// Assume this server doesn't support smart HTTP fetch
 					// and fall back on dumb object walking.
-					//
-					return newDumbConnection(in);
+					f = newDumbConnection(in);
 				}
+				f.setPeerUserAgent(c.getHeaderField(HttpSupport.HDR_SERVER));
+				return (FetchConnection) f;
 			} finally {
 				in.close();
 			}
@@ -331,7 +319,7 @@ public FetchConnection openFetch() throws TransportException,
 		}
 	}
 
-	private FetchConnection newDumbConnection(InputStream in)
+	private WalkFetchConnection newDumbConnection(InputStream in)
 			throws IOException, PackProtocolException {
 		HttpObjectDB d = new HttpObjectDB(objectsUrl);
 		BufferedReader br = toBufferedReader(in);
@@ -400,9 +388,7 @@ public PushConnection openPush() throws NotSupportedException,
 			final InputStream in = openInputStream(c);
 			try {
 				if (isSmartHttp(c, service)) {
-					readSmartHeaders(in, service);
-					return new SmartHttpPushConnection(in);
-
+					return smartPush(service, c, in);
 				} else if (!useSmartHttp) {
 					final String msg = JGitText.get().smartHTTPPushDisabled;
 					throw new NotSupportedException(msg);
@@ -423,6 +409,14 @@ public PushConnection openPush() throws NotSupportedException,
 		}
 	}
 
+	private PushConnection smartPush(String service, HttpConnection c,
+			InputStream in) throws IOException, TransportException {
+		readSmartHeaders(in, service);
+		SmartHttpPushConnection p = new SmartHttpPushConnection(in);
+		p.setPeerUserAgent(c.getHeaderField(HttpSupport.HDR_SERVER));
+		return p;
+	}
+
 	@Override
 	public void close() {
 		// No explicit connections are maintained.
@@ -551,7 +545,9 @@ protected HttpConnection httpOpen(String method, URL u)
 		conn.setUseCaches(false);
 		conn.setRequestProperty(HDR_ACCEPT_ENCODING, ENCODING_GZIP);
 		conn.setRequestProperty(HDR_PRAGMA, "no-cache"); //$NON-NLS-1$
-		conn.setRequestProperty(HDR_USER_AGENT, userAgent);
+		if (UserAgent.get() != null) {
+			conn.setRequestProperty(HDR_USER_AGENT, UserAgent.get());
+		}
 		int timeOut = getTimeout();
 		if (timeOut != -1) {
 			int effTimeOut = timeOut * 1000;
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 1a653bd..3afdb61 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UploadPack.java
@@ -44,6 +44,7 @@
 package org.eclipse.jgit.transport;
 
 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_INCLUDE_TAG;
 import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_MULTI_ACK;
@@ -253,6 +254,7 @@ public Set<String> getOptions() {
 
 	/** Capabilities requested by the client. */
 	private Set<String> options;
+	String userAgent;
 
 	/** Raw ObjectIds the client has asked for, before validating them. */
 	private final Set<ObjectId> wantIds = new HashSet<ObjectId>();
@@ -806,6 +808,7 @@ public void sendAdvertisedRefs(final RefAdvertiser adv) throws IOException,
 				|| policy == RequestPolicy.REACHABLE_COMMIT_TIP
 				|| policy == null)
 			adv.advertiseCapability(OPTION_ALLOW_TIP_SHA1_IN_WANT);
+		adv.advertiseCapability(OPTION_AGENT, UserAgent.get());
 		adv.setDerefTags(true);
 		Map<String, Ref> refs = getAdvertisedOrDefaultRefs();
 		findSymrefs(adv, refs);
@@ -884,6 +887,37 @@ private void recvWants() throws IOException {
 		}
 	}
 
+	/**
+	 * Returns the clone/fetch depth. Valid only after calling recvWants().
+	 *
+	 * @return the depth requested by the client, or 0 if unbounded.
+	 * @since 4.0
+	 */
+	public int getDepth() {
+		if (options == null)
+			throw new RequestNotYetReadException();
+		return depth;
+	}
+
+	/**
+	 * Get the user agent of the client.
+	 * <p>
+	 * If the client is new enough to use {@code agent=} capability that value
+	 * will be returned. Older HTTP clients may also supply their version using
+	 * the HTTP {@code User-Agent} header. The capability overrides the HTTP
+	 * header if both are available.
+	 * <p>
+	 * When an HTTP request has been received this method returns the HTTP
+	 * {@code User-Agent} header value until capabilities have been parsed.
+	 *
+	 * @return user agent supplied by the client. Available only if the client
+	 *         is new enough to advertise its user agent.
+	 * @since 4.0
+	 */
+	public String getPeerUserAgent() {
+		return UserAgent.getAgent(options, userAgent);
+	}
+
 	private boolean negotiate() throws IOException {
 		okToGiveUp = Boolean.FALSE;
 
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/UserAgent.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UserAgent.java
new file mode 100644
index 0000000..eadb92d
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/UserAgent.java
@@ -0,0 +1,147 @@
+/*
+ * 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.GitProtocolConstants.OPTION_AGENT;
+
+import java.util.Set;
+
+import org.eclipse.jgit.util.StringUtils;
+
+/**
+ * User agent to be reported by this JGit client and server on the network.
+ * <p>
+ * On HTTP transports this user agent string is always supplied by the JGit
+ * client in the {@code User-Agent} HTTP header.
+ * <p>
+ * On native transports this user agent string is always sent when JGit is a
+ * server. When JGit is a client the user agent string will be supplied to the
+ * remote server only if the remote server advertises its own agent identity.
+ *
+ * @since 4.0
+ */
+public class UserAgent {
+	private static volatile String userAgent = computeUserAgent();
+
+	private static String computeUserAgent() {
+		return clean("JGit/" + computeVersion()); //$NON-NLS-1$
+	}
+
+	private static String computeVersion() {
+		Package pkg = UserAgent.class.getPackage();
+		if (pkg != null) {
+			String ver = pkg.getImplementationVersion();
+			if (!StringUtils.isEmptyOrNull(ver)) {
+				return ver;
+			}
+		}
+		return "unknown"; //$NON-NLS-1$
+	}
+
+	private static String clean(String s) {
+		s = s.trim();
+		StringBuilder b = new StringBuilder(s.length());
+		for (int i = 0; i < s.length(); i++) {
+			char c = s.charAt(i);
+			if (c <= 32 || c >= 127) {
+				if (b.length() > 0 && b.charAt(b.length() - 1) == '.')
+					continue;
+				c = '.';
+			}
+			b.append(c);
+		}
+		return b.length() > 0 ? b.toString() : null;
+	}
+
+	/**
+	 * Get the user agent string advertised by JGit.
+	 *
+	 * @return a string similar to {@code "JGit/4.0"}; null if the agent has
+	 *         been cleared and should not be shared with a peer.
+	 */
+	public static String get() {
+		return userAgent;
+	}
+
+	/**
+	 * Change the user agent string advertised by JGit.
+	 * <p>
+	 * The new string should start with {@code "JGit/"} (for example
+	 * {@code "JGit/4.0"}) to advertise the implementation as JGit based.
+	 * <p>
+	 * Spaces and other whitespace should be avoided as these will be
+	 * automatically converted to {@code "."}.
+	 * <p>
+	 * User agent strings are restricted to printable ASCII.
+	 *
+	 * @param agent
+	 *            new user agent string for this running JGit library. Setting
+	 *            to null or empty string will avoid sending any identification
+	 *            to the peer.
+	 */
+	public static void set(String agent) {
+		userAgent = StringUtils.isEmptyOrNull(agent) ? null : clean(agent);
+	}
+
+	static String getAgent(Set<String> options, String transportAgent) {
+		if (options == null || options.isEmpty()) {
+			return transportAgent;
+		}
+		for (String o : options) {
+			if (o.startsWith(OPTION_AGENT)
+					&& o.length() > OPTION_AGENT.length()
+					&& o.charAt(OPTION_AGENT.length()) == '=') {
+				return o.substring(OPTION_AGENT.length() + 1);
+			}
+		}
+		return transportAgent;
+	}
+
+	static boolean hasAgent(Set<String> options) {
+		return getAgent(options, null) != null;
+	}
+
+	private UserAgent() {
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java
index 37c9f7b..8b4ad0a 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java
@@ -74,6 +74,12 @@ public class HttpSupport {
 	/** The {@code User-Agent} header. */
 	public static final String HDR_USER_AGENT = "User-Agent"; //$NON-NLS-1$
 
+	/**
+	 * The {@code Server} header.
+	 * @since 4.0
+	 */
+	public static final String HDR_SERVER = "Server"; //$NON-NLS-1$
+
 	/** The {@code Date} header. */
 	public static final String HDR_DATE = "Date"; //$NON-NLS-1$
 
diff --git a/pom.xml b/pom.xml
index 4b8ac43..1e00f24 100644
--- a/pom.xml
+++ b/pom.xml
@@ -210,6 +210,10 @@
       <id>repo.eclipse.org.cbi-releases</id>
       <url>https://repo.eclipse.org/content/repositories/cbi-releases/</url>
     </pluginRepository>
+    <pluginRepository>
+      <id>repo.eclipse.org.cbi-snapshots</id>
+      <url>https://repo.eclipse.org/content/repositories/cbi-snapshots/</url>
+    </pluginRepository>
   </pluginRepositories>
 
   <build>
@@ -346,7 +350,7 @@
         <plugin>
           <groupId>org.eclipse.cbi.maven.plugins</groupId>
           <artifactId>eclipse-jarsigner-plugin</artifactId>
-          <version>1.1.1</version>
+          <version>1.1.2-SNAPSHOT</version>
         </plugin>
         <plugin>
           <groupId>org.eclipse.tycho.extras</groupId>