diff --git a/org.eclipse.jgit.http.test/.gitignore b/org.eclipse.jgit.http.test/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..ea8c4bf7f35f6f77f75d92ad8ce8349f6e81ddba
--- /dev/null
+++ b/org.eclipse.jgit.http.test/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/org.eclipse.jgit.http.test/pom.xml b/org.eclipse.jgit.http.test/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..fe9363a3edb2c6a5573ee19a40f5de7277b224f2
--- /dev/null
+++ b/org.eclipse.jgit.http.test/pom.xml
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Copyright (C) 2009-2010, Google Inc.
+   Copyright (C) 2008, Imran M Yousuf <imyousuf@smartitengineering.com>
+   and other copyright owners as documented in the project's IP log.
+
+   This program and the accompanying materials are made available
+   under the terms of the Eclipse Distribution License v1.0 which
+   accompanies this distribution, is reproduced below, and is
+   available at http://www.eclipse.org/org/documents/edl-v10.php
+
+   All rights reserved.
+
+   Redistribution and use in source and binary forms, with or
+   without modification, are permitted provided that the following
+   conditions are met:
+
+   - Redistributions of source code must retain the above copyright
+     notice, this list of conditions and the following disclaimer.
+
+   - Redistributions in binary form must reproduce the above
+     copyright notice, this list of conditions and the following
+     disclaimer in the documentation and/or other materials provided
+     with the distribution.
+
+   - Neither the name of the Eclipse Foundation, Inc. nor the
+     names of its contributors may be used to endorse or promote
+     products derived from this software without specific prior
+     written permission.
+
+   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+   CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+   INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+   OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+   ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+   CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+   SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+   NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+   LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+   CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+   STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+   ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+   ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.eclipse.jgit</groupId>
+    <artifactId>org.eclipse.jgit-parent</artifactId>
+    <version>0.6.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.eclipse.jgit.http.test</artifactId>
+  <name>JGit - HTTP Tests</name>
+
+  <description>
+    Tests for the HTTP transport.
+  </description>
+
+  <dependencies>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.eclipse.jgit</groupId>
+      <artifactId>org.eclipse.jgit</artifactId>
+      <version>${project.version}</version>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.eclipse.jgit</groupId>
+      <artifactId>org.eclipse.jgit.junit</artifactId>
+      <version>${project.version}</version>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.eclipse.jgit</groupId>
+      <artifactId>org.eclipse.jgit.http.server</artifactId>
+      <version>${project.version}</version>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.eclipse.jetty</groupId>
+      <artifactId>jetty-servlet</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <testSourceDirectory>tst/</testSourceDirectory>
+
+    <testResources>
+      <testResource>
+        <directory>tst-rsrc/</directory>
+      </testResource>
+    </testResources>
+  </build>
+</project>
diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/AsIsServiceTest.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/AsIsServiceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..ea937481d4bea9acdf553826da00037482d2b439
--- /dev/null
+++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/AsIsServiceTest.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2009-2010, 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.http.test;
+
+import javax.servlet.http.HttpServletRequestWrapper;
+
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jgit.http.server.resolver.AsIsFileService;
+import org.eclipse.jgit.http.server.resolver.ServiceNotAuthorizedException;
+import org.eclipse.jgit.http.server.resolver.ServiceNotEnabledException;
+import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
+import org.eclipse.jgit.lib.Repository;
+
+public class AsIsServiceTest extends LocalDiskRepositoryTestCase {
+	private Repository db;
+
+	private AsIsFileService service;
+
+	protected void setUp() throws Exception {
+		super.setUp();
+
+		db = createBareRepository();
+		service = new AsIsFileService();
+	}
+
+	public void testDisabledSingleton() throws ServiceNotAuthorizedException {
+		service = AsIsFileService.DISABLED;
+		try {
+			service.access(new R(null, "1.2.3.4"), db);
+			fail("Created session for anonymous user: null");
+		} catch (ServiceNotEnabledException e) {
+			// expected not authorized
+		}
+
+		try {
+			service.access(new R("bob", "1.2.3.4"), db);
+			fail("Created session for user: \"bob\"");
+		} catch (ServiceNotEnabledException e) {
+			// expected not authorized
+		}
+	}
+
+	public void testCreate_Default() throws ServiceNotEnabledException,
+			ServiceNotAuthorizedException {
+		service.access(new R(null, "1.2.3.4"), db);
+		service.access(new R("bob", "1.2.3.4"), db);
+	}
+
+	public void testCreate_Disabled() throws ServiceNotAuthorizedException {
+		db.getConfig().setBoolean("http", null, "getanyfile", false);
+
+		try {
+			service.access(new R(null, "1.2.3.4"), db);
+			fail("Created session for anonymous user: null");
+		} catch (ServiceNotEnabledException e) {
+			// expected not authorized
+		}
+
+		try {
+			service.access(new R("bob", "1.2.3.4"), db);
+			fail("Created session for user: \"bob\"");
+		} catch (ServiceNotEnabledException e) {
+			// expected not authorized
+		}
+	}
+
+	public void testCreate_Enabled() throws ServiceNotEnabledException,
+			ServiceNotAuthorizedException {
+		db.getConfig().setBoolean("http", null, "getanyfile", true);
+		service.access(new R(null, "1.2.3.4"), db);
+		service.access(new R("bob", "1.2.3.4"), db);
+	}
+
+	private final class R extends HttpServletRequestWrapper {
+		private final String user;
+
+		private final String host;
+
+		R(final String user, final String host) {
+			super(new Request() /* can't pass null, sigh */);
+			this.user = user;
+			this.host = host;
+		}
+
+		@Override
+		public String getRemoteHost() {
+			return host;
+		}
+
+		@Override
+		public String getRemoteUser() {
+			return user;
+		}
+	}
+}
diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/DefaultReceivePackFactoryTest.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/DefaultReceivePackFactoryTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..5338caaa488b3d4edc367834836103584d8f69a8
--- /dev/null
+++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/DefaultReceivePackFactoryTest.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2009-2010, 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.http.test;
+
+import javax.servlet.http.HttpServletRequestWrapper;
+
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jgit.http.server.resolver.DefaultReceivePackFactory;
+import org.eclipse.jgit.http.server.resolver.ReceivePackFactory;
+import org.eclipse.jgit.http.server.resolver.ServiceNotAuthorizedException;
+import org.eclipse.jgit.http.server.resolver.ServiceNotEnabledException;
+import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.ReceivePack;
+
+public class DefaultReceivePackFactoryTest extends LocalDiskRepositoryTestCase {
+	private Repository db;
+
+	private ReceivePackFactory factory;
+
+	protected void setUp() throws Exception {
+		super.setUp();
+
+		db = createBareRepository();
+		factory = new DefaultReceivePackFactory();
+	}
+
+	public void testDisabledSingleton() throws ServiceNotAuthorizedException {
+		factory = ReceivePackFactory.DISABLED;
+
+		try {
+			factory.create(new R(null, "localhost"), db);
+			fail("Created session for anonymous user: null");
+		} catch (ServiceNotEnabledException e) {
+			// expected not authorized
+		}
+
+		try {
+			factory.create(new R("", "localhost"), db);
+			fail("Created session for anonymous user: \"\"");
+		} catch (ServiceNotEnabledException e) {
+			// expected not authorized
+		}
+
+		try {
+			factory.create(new R("bob", "localhost"), db);
+			fail("Created session for user: \"bob\"");
+		} catch (ServiceNotEnabledException e) {
+			// expected not authorized
+		}
+	}
+
+	public void testCreate_NullUser() throws ServiceNotEnabledException {
+		try {
+			factory.create(new R(null, "localhost"), db);
+			fail("Created session for anonymous user: null");
+		} catch (ServiceNotAuthorizedException e) {
+			// expected not authorized
+		}
+	}
+
+	public void testCreate_EmptyStringUser() throws ServiceNotEnabledException {
+		try {
+			factory.create(new R("", "localhost"), db);
+			fail("Created session for anonymous user: \"\"");
+		} catch (ServiceNotAuthorizedException e) {
+			// expected not authorized
+		}
+	}
+
+	public void testCreate_AuthUser() throws ServiceNotEnabledException,
+			ServiceNotAuthorizedException {
+		ReceivePack rp;
+		rp = factory.create(new R("bob", "1.2.3.4"), db);
+		assertNotNull("have ReceivePack", rp);
+		assertSame(db, rp.getRepository());
+
+		PersonIdent id = rp.getRefLogIdent();
+		assertNotNull(id);
+		assertEquals("bob", id.getName());
+		assertEquals("bob@1.2.3.4", id.getEmailAddress());
+
+		// Should have inherited off the current system, which is mocked
+		assertEquals(author.getTimeZoneOffset(), id.getTimeZoneOffset());
+		assertEquals(author.getWhen(), id.getWhen());
+	}
+
+	public void testCreate_Disabled() throws ServiceNotAuthorizedException {
+		db.getConfig().setBoolean("http", null, "receivepack", false);
+
+		try {
+			factory.create(new R(null, "localhost"), db);
+			fail("Created session for anonymous user: null");
+		} catch (ServiceNotEnabledException e) {
+			// expected not authorized
+		}
+
+		try {
+			factory.create(new R("", "localhost"), db);
+			fail("Created session for anonymous user: \"\"");
+		} catch (ServiceNotEnabledException e) {
+			// expected not authorized
+		}
+
+		try {
+			factory.create(new R("bob", "localhost"), db);
+			fail("Created session for user: \"bob\"");
+		} catch (ServiceNotEnabledException e) {
+			// expected not authorized
+		}
+	}
+
+	public void testCreate_Enabled() throws ServiceNotEnabledException,
+			ServiceNotAuthorizedException {
+		db.getConfig().setBoolean("http", null, "receivepack", true);
+		ReceivePack rp;
+
+		rp = factory.create(new R(null, "1.2.3.4"), db);
+		assertNotNull("have ReceivePack", rp);
+		assertSame(db, rp.getRepository());
+
+		PersonIdent id = rp.getRefLogIdent();
+		assertNotNull(id);
+		assertEquals("anonymous", id.getName());
+		assertEquals("anonymous@1.2.3.4", id.getEmailAddress());
+
+		// Should have inherited off the current system, which is mocked
+		assertEquals(author.getTimeZoneOffset(), id.getTimeZoneOffset());
+		assertEquals(author.getWhen(), id.getWhen());
+
+		rp = factory.create(new R("bob", "1.2.3.4"), db);
+		assertNotNull("have ReceivePack", rp);
+	}
+
+	private final class R extends HttpServletRequestWrapper {
+		private final String user;
+
+		private final String host;
+
+		R(final String user, final String host) {
+			super(new Request() /* can't pass null, sigh */);
+			this.user = user;
+			this.host = host;
+		}
+
+		@Override
+		public String getRemoteHost() {
+			return host;
+		}
+
+		@Override
+		public String getRemoteUser() {
+			return user;
+		}
+	}
+}
diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/DefaultUploadPackFactoryTest.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/DefaultUploadPackFactoryTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..8f57d40afe8f09a7adbb2d4e9855873ef3b223b7
--- /dev/null
+++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/DefaultUploadPackFactoryTest.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2009-2010, 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.http.test;
+
+import javax.servlet.http.HttpServletRequestWrapper;
+
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jgit.http.server.resolver.DefaultUploadPackFactory;
+import org.eclipse.jgit.http.server.resolver.ServiceNotAuthorizedException;
+import org.eclipse.jgit.http.server.resolver.ServiceNotEnabledException;
+import org.eclipse.jgit.http.server.resolver.UploadPackFactory;
+import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.UploadPack;
+
+public class DefaultUploadPackFactoryTest extends LocalDiskRepositoryTestCase {
+	private Repository db;
+
+	private UploadPackFactory factory;
+
+	protected void setUp() throws Exception {
+		super.setUp();
+
+		db = createBareRepository();
+		factory = new DefaultUploadPackFactory();
+	}
+
+	public void testDisabledSingleton() throws ServiceNotAuthorizedException {
+		factory = UploadPackFactory.DISABLED;
+
+		try {
+			factory.create(new R(null, "localhost"), db);
+			fail("Created session for anonymous user: null");
+		} catch (ServiceNotEnabledException e) {
+			// expected not authorized
+		}
+
+		try {
+			factory.create(new R("", "localhost"), db);
+			fail("Created session for anonymous user: \"\"");
+		} catch (ServiceNotEnabledException e) {
+			// expected not authorized
+		}
+
+		try {
+			factory.create(new R("bob", "localhost"), db);
+			fail("Created session for user: \"bob\"");
+		} catch (ServiceNotEnabledException e) {
+			// expected not authorized
+		}
+	}
+
+	public void testCreate_Default() throws ServiceNotEnabledException,
+			ServiceNotAuthorizedException {
+		UploadPack up;
+
+		up = factory.create(new R(null, "1.2.3.4"), db);
+		assertNotNull("have UploadPack", up);
+		assertSame(db, up.getRepository());
+
+		up = factory.create(new R("bob", "1.2.3.4"), db);
+		assertNotNull("have UploadPack", up);
+		assertSame(db, up.getRepository());
+	}
+
+	public void testCreate_Disabled() throws ServiceNotAuthorizedException {
+		db.getConfig().setBoolean("http", null, "uploadpack", false);
+
+		try {
+			factory.create(new R(null, "localhost"), db);
+			fail("Created session for anonymous user: null");
+		} catch (ServiceNotEnabledException e) {
+			// expected not authorized
+		}
+
+		try {
+			factory.create(new R("bob", "localhost"), db);
+			fail("Created session for user: \"bob\"");
+		} catch (ServiceNotEnabledException e) {
+			// expected not authorized
+		}
+	}
+
+	public void testCreate_Enabled() throws ServiceNotEnabledException,
+			ServiceNotAuthorizedException {
+		db.getConfig().setBoolean("http", null, "uploadpack", true);
+		UploadPack up;
+
+		up = factory.create(new R(null, "1.2.3.4"), db);
+		assertNotNull("have UploadPack", up);
+		assertSame(db, up.getRepository());
+
+		up = factory.create(new R("bob", "1.2.3.4"), db);
+		assertNotNull("have UploadPack", up);
+		assertSame(db, up.getRepository());
+	}
+
+	private final class R extends HttpServletRequestWrapper {
+		private final String user;
+
+		private final String host;
+
+		R(final String user, final String host) {
+			super(new Request() /* can't pass null, sigh */);
+			this.user = user;
+			this.host = host;
+		}
+
+		@Override
+		public String getRemoteHost() {
+			return host;
+		}
+
+		@Override
+		public String getRemoteUser() {
+			return user;
+		}
+	}
+}
diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/DumbClientDumbServerTest.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/DumbClientDumbServerTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..1ec324428496408ae09a57f5d32cd58e436cf360
--- /dev/null
+++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/DumbClientDumbServerTest.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2010, 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.http.test;
+
+import static org.eclipse.jgit.util.HttpSupport.HDR_ACCEPT;
+import static org.eclipse.jgit.util.HttpSupport.HDR_PRAGMA;
+import static org.eclipse.jgit.util.HttpSupport.HDR_USER_AGENT;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jetty.servlet.DefaultServlet;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jgit.errors.NotSupportedException;
+import org.eclipse.jgit.http.test.util.AccessEvent;
+import org.eclipse.jgit.http.test.util.HttpTestCase;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevBlob;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.FetchConnection;
+import org.eclipse.jgit.transport.HttpTransport;
+import org.eclipse.jgit.transport.Transport;
+import org.eclipse.jgit.transport.TransportHttp;
+import org.eclipse.jgit.transport.URIish;
+
+public class DumbClientDumbServerTest extends HttpTestCase {
+	private Repository remoteRepository;
+
+	private URIish remoteURI;
+
+	private RevBlob A_txt;
+
+	private RevCommit A, B;
+
+	protected void setUp() throws Exception {
+		super.setUp();
+
+		final TestRepository src = createTestRepository();
+		final File srcGit = src.getRepository().getDirectory();
+		final URI base = srcGit.getParentFile().toURI();
+
+		ServletContextHandler app = server.addContext("/git");
+		app.setResourceBase(base.toString());
+		app.addServlet(DefaultServlet.class, "/");
+
+		server.setUp();
+
+		remoteRepository = src.getRepository();
+		remoteURI = toURIish(app, srcGit.getName());
+
+		A_txt = src.blob("A");
+		A = src.commit().add("A_txt", A_txt).create();
+		B = src.commit().parent(A).add("A_txt", "C").add("B", "B").create();
+		src.update(master, B);
+	}
+
+	public void testListRemote() throws IOException {
+		Repository dst = createBareRepository();
+
+		assertEquals("http", remoteURI.getScheme());
+
+		Map<String, Ref> map;
+		Transport t = Transport.open(dst, remoteURI);
+		try {
+			// I didn't make up these public interface names, I just
+			// approved them for inclusion into the code base. Sorry.
+			// --spearce
+			//
+			assertTrue("isa TransportHttp", t instanceof TransportHttp);
+			assertTrue("isa HttpTransport", t instanceof HttpTransport);
+
+			FetchConnection c = t.openFetch();
+			try {
+				map = c.getRefsMap();
+			} finally {
+				c.close();
+			}
+		} finally {
+			t.close();
+		}
+
+		assertNotNull("have map of refs", map);
+		assertEquals(2, map.size());
+
+		assertNotNull("has " + master, map.get(master));
+		assertEquals(B, map.get(master).getObjectId());
+
+		assertNotNull("has " + Constants.HEAD, map.get(Constants.HEAD));
+		assertEquals(B, map.get(Constants.HEAD).getObjectId());
+
+		List<AccessEvent> requests = getRequests();
+		assertEquals(2, requests.size());
+		assertEquals(0, getRequests(remoteURI, "git-upload-pack").size());
+
+		AccessEvent info = requests.get(0);
+		assertEquals("GET", info.getMethod());
+		assertEquals(join(remoteURI, "info/refs"), info.getPath());
+		assertEquals(1, info.getParameters().size());
+		assertEquals("git-upload-pack", info.getParameter("service"));
+		assertEquals("no-cache", info.getRequestHeader(HDR_PRAGMA));
+		assertNotNull("has user-agent", info.getRequestHeader(HDR_USER_AGENT));
+		assertTrue("is jgit agent", info.getRequestHeader(HDR_USER_AGENT)
+				.startsWith("JGit/"));
+		assertEquals("application/x-git-upload-pack-advertisement, */*", info
+				.getRequestHeader(HDR_ACCEPT));
+		assertEquals(200, info.getStatus());
+
+		AccessEvent head = requests.get(1);
+		assertEquals("GET", head.getMethod());
+		assertEquals(join(remoteURI, "HEAD"), head.getPath());
+		assertEquals(0, head.getParameters().size());
+		assertEquals("no-cache", head.getRequestHeader(HDR_PRAGMA));
+		assertNotNull("has user-agent", head.getRequestHeader(HDR_USER_AGENT));
+		assertTrue("is jgit agent", head.getRequestHeader(HDR_USER_AGENT)
+				.startsWith("JGit/"));
+		assertEquals(200, head.getStatus());
+	}
+
+	public void testInitialClone_Loose() throws Exception {
+		Repository dst = createBareRepository();
+		assertFalse(dst.hasObject(A_txt));
+
+		Transport t = Transport.open(dst, remoteURI);
+		try {
+			t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
+		} finally {
+			t.close();
+		}
+
+		assertTrue(dst.hasObject(A_txt));
+		assertEquals(B, dst.getRef(master).getObjectId());
+		fsck(dst, B);
+
+		List<AccessEvent> loose = getRequests(loose(remoteURI, A_txt));
+		assertEquals(1, loose.size());
+		assertEquals("GET", loose.get(0).getMethod());
+		assertEquals(0, loose.get(0).getParameters().size());
+		assertEquals(200, loose.get(0).getStatus());
+	}
+
+	public void testInitialClone_Packed() throws Exception {
+		new TestRepository(remoteRepository).packAndPrune();
+
+		Repository dst = createBareRepository();
+		assertFalse(dst.hasObject(A_txt));
+
+		Transport t = Transport.open(dst, remoteURI);
+		try {
+			t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
+		} finally {
+			t.close();
+		}
+
+		assertTrue(dst.hasObject(A_txt));
+		assertEquals(B, dst.getRef(master).getObjectId());
+		fsck(dst, B);
+
+		List<AccessEvent> req;
+		AccessEvent event;
+
+		req = getRequests(loose(remoteURI, B));
+		assertEquals(1, req.size());
+		event = req.get(0);
+		assertEquals("GET", event.getMethod());
+		assertEquals(0, event.getParameters().size());
+		assertEquals(404, event.getStatus());
+
+		req = getRequests(join(remoteURI, "objects/info/packs"));
+		assertEquals(1, req.size());
+		event = req.get(0);
+		assertEquals("GET", event.getMethod());
+		assertEquals(0, event.getParameters().size());
+		assertEquals("no-cache", event.getRequestHeader(HDR_PRAGMA));
+		assertNotNull("has user-agent", event.getRequestHeader(HDR_USER_AGENT));
+		assertTrue("is jgit agent", event.getRequestHeader(HDR_USER_AGENT)
+				.startsWith("JGit/"));
+		assertEquals(200, event.getStatus());
+	}
+
+	public void testPushNotSupported() throws Exception {
+		final TestRepository src = createTestRepository();
+		final RevCommit Q = src.commit().create();
+		final Repository db = src.getRepository();
+
+		Transport t = Transport.open(db, remoteURI);
+		try {
+			try {
+				t.push(NullProgressMonitor.INSTANCE, push(src, Q));
+				fail("push incorrectly completed against a dumb server");
+			} catch (NotSupportedException nse) {
+				String exp = "remote does not support smart HTTP push";
+				assertEquals(exp, nse.getMessage());
+			}
+		} finally {
+			t.close();
+		}
+	}
+}
diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/DumbClientSmartServerTest.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/DumbClientSmartServerTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..0b28712f69e4126f0f723007bfe0a5b45a0b6a92
--- /dev/null
+++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/DumbClientSmartServerTest.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 2010, 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.http.test;
+
+import static org.eclipse.jgit.util.HttpSupport.HDR_ACCEPT;
+import static org.eclipse.jgit.util.HttpSupport.HDR_CONTENT_TYPE;
+import static org.eclipse.jgit.util.HttpSupport.HDR_PRAGMA;
+import static org.eclipse.jgit.util.HttpSupport.HDR_USER_AGENT;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.eclipse.jgit.errors.NotSupportedException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.http.server.GitServlet;
+import org.eclipse.jgit.http.server.resolver.RepositoryResolver;
+import org.eclipse.jgit.http.server.resolver.ServiceNotEnabledException;
+import org.eclipse.jgit.http.test.util.AccessEvent;
+import org.eclipse.jgit.http.test.util.HttpTestCase;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevBlob;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.FetchConnection;
+import org.eclipse.jgit.transport.HttpTransport;
+import org.eclipse.jgit.transport.Transport;
+import org.eclipse.jgit.transport.TransportHttp;
+import org.eclipse.jgit.transport.URIish;
+
+public class DumbClientSmartServerTest extends HttpTestCase {
+	private Repository remoteRepository;
+
+	private URIish remoteURI;
+
+	private RevBlob A_txt;
+
+	private RevCommit A, B;
+
+	protected void setUp() throws Exception {
+		super.setUp();
+
+		final TestRepository src = createTestRepository();
+		final String srcName = src.getRepository().getDirectory().getName();
+
+		ServletContextHandler app = server.addContext("/git");
+		GitServlet gs = new GitServlet();
+		gs.setRepositoryResolver(new RepositoryResolver() {
+			public Repository open(HttpServletRequest req, String name)
+					throws RepositoryNotFoundException,
+					ServiceNotEnabledException {
+				if (!name.equals(srcName))
+					throw new RepositoryNotFoundException(name);
+
+				final Repository db = src.getRepository();
+				db.incrementOpen();
+				return db;
+			}
+		});
+		app.addServlet(new ServletHolder(gs), "/*");
+
+		server.setUp();
+
+		remoteRepository = src.getRepository();
+		remoteURI = toURIish(app, srcName);
+
+		A_txt = src.blob("A");
+		A = src.commit().add("A_txt", A_txt).create();
+		B = src.commit().parent(A).add("A_txt", "C").add("B", "B").create();
+		src.update(master, B);
+	}
+
+	public void testListRemote() throws IOException {
+		Repository dst = createBareRepository();
+
+		assertEquals("http", remoteURI.getScheme());
+
+		Map<String, Ref> map;
+		Transport t = Transport.open(dst, remoteURI);
+		((TransportHttp) t).setUseSmartHttp(false);
+		try {
+			// I didn't make up these public interface names, I just
+			// approved them for inclusion into the code base. Sorry.
+			// --spearce
+			//
+			assertTrue("isa TransportHttp", t instanceof TransportHttp);
+			assertTrue("isa HttpTransport", t instanceof HttpTransport);
+
+			FetchConnection c = t.openFetch();
+			try {
+				map = c.getRefsMap();
+			} finally {
+				c.close();
+			}
+		} finally {
+			t.close();
+		}
+
+		assertNotNull("have map of refs", map);
+		assertEquals(2, map.size());
+
+		assertNotNull("has " + master, map.get(master));
+		assertEquals(B, map.get(master).getObjectId());
+
+		assertNotNull("has " + Constants.HEAD, map.get(Constants.HEAD));
+		assertEquals(B, map.get(Constants.HEAD).getObjectId());
+
+		List<AccessEvent> requests = getRequests();
+		assertEquals(2, requests.size());
+		assertEquals(0, getRequests(remoteURI, "git-upload-pack").size());
+
+		AccessEvent info = requests.get(0);
+		assertEquals("GET", info.getMethod());
+		assertEquals(join(remoteURI, "info/refs"), info.getPath());
+		assertEquals(0, info.getParameters().size());
+		assertNull("no service parameter", info.getParameter("service"));
+		assertEquals("no-cache", info.getRequestHeader(HDR_PRAGMA));
+		assertNotNull("has user-agent", info.getRequestHeader(HDR_USER_AGENT));
+		assertTrue("is jgit agent", info.getRequestHeader(HDR_USER_AGENT)
+				.startsWith("JGit/"));
+		assertEquals("*/*", info.getRequestHeader(HDR_ACCEPT));
+		assertEquals(200, info.getStatus());
+		assertEquals("text/plain;charset=UTF-8", info
+				.getResponseHeader(HDR_CONTENT_TYPE));
+
+		AccessEvent head = requests.get(1);
+		assertEquals("GET", head.getMethod());
+		assertEquals(join(remoteURI, "HEAD"), head.getPath());
+		assertEquals(0, head.getParameters().size());
+		assertEquals(200, head.getStatus());
+		assertEquals("text/plain", head.getResponseHeader(HDR_CONTENT_TYPE));
+	}
+
+	public void testInitialClone_Small() throws Exception {
+		Repository dst = createBareRepository();
+		assertFalse(dst.hasObject(A_txt));
+
+		Transport t = Transport.open(dst, remoteURI);
+		((TransportHttp) t).setUseSmartHttp(false);
+		try {
+			t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
+		} finally {
+			t.close();
+		}
+
+		assertTrue(dst.hasObject(A_txt));
+		assertEquals(B, dst.getRef(master).getObjectId());
+		fsck(dst, B);
+
+		List<AccessEvent> loose = getRequests(loose(remoteURI, A_txt));
+		assertEquals(1, loose.size());
+		assertEquals("GET", loose.get(0).getMethod());
+		assertEquals(0, loose.get(0).getParameters().size());
+		assertEquals(200, loose.get(0).getStatus());
+		assertEquals("application/x-git-loose-object", loose.get(0)
+				.getResponseHeader(HDR_CONTENT_TYPE));
+	}
+
+	public void testInitialClone_Packed() throws Exception {
+		new TestRepository(remoteRepository).packAndPrune();
+
+		Repository dst = createBareRepository();
+		assertFalse(dst.hasObject(A_txt));
+
+		Transport t = Transport.open(dst, remoteURI);
+		((TransportHttp) t).setUseSmartHttp(false);
+		try {
+			t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
+		} finally {
+			t.close();
+		}
+
+		assertTrue(dst.hasObject(A_txt));
+		assertEquals(B, dst.getRef(master).getObjectId());
+		fsck(dst, B);
+
+		List<AccessEvent> req;
+
+		req = getRequests(loose(remoteURI, B));
+		assertEquals(1, req.size());
+		assertEquals("GET", req.get(0).getMethod());
+		assertEquals(0, req.get(0).getParameters().size());
+		assertEquals(404, req.get(0).getStatus());
+
+		req = getRequests(join(remoteURI, "objects/info/packs"));
+		assertEquals(1, req.size());
+		assertEquals("GET", req.get(0).getMethod());
+		assertEquals(0, req.get(0).getParameters().size());
+		assertEquals(200, req.get(0).getStatus());
+		assertEquals("text/plain;charset=UTF-8", req.get(0).getResponseHeader(
+				HDR_CONTENT_TYPE));
+	}
+
+	public void testPushNotSupported() throws Exception {
+		final TestRepository src = createTestRepository();
+		final RevCommit Q = src.commit().create();
+		final Repository db = src.getRepository();
+
+		Transport t = Transport.open(db, remoteURI);
+		((TransportHttp) t).setUseSmartHttp(false);
+		try {
+			try {
+				t.push(NullProgressMonitor.INSTANCE, push(src, Q));
+				fail("push incorrectly completed against a smart server");
+			} catch (NotSupportedException nse) {
+				String exp = "smart HTTP push disabled";
+				assertEquals(exp, nse.getMessage());
+			}
+		} finally {
+			t.close();
+		}
+	}
+}
diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/ErrorServletTest.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/ErrorServletTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..18c1bb8f6d659294367275887226fb1e0acb03a2
--- /dev/null
+++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/ErrorServletTest.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2009-2010, 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.http.test;
+
+import java.net.HttpURLConnection;
+import java.net.URI;
+
+import junit.framework.TestCase;
+
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.eclipse.jgit.http.server.glue.ErrorServlet;
+import org.eclipse.jgit.http.test.util.AppServer;
+
+public class ErrorServletTest extends TestCase {
+	private AppServer server;
+
+	protected void setUp() throws Exception {
+		super.setUp();
+
+		server = new AppServer();
+
+		ServletContextHandler ctx = server.addContext("/");
+		ctx.addServlet(new ServletHolder(new ErrorServlet(404)), "/404");
+		ctx.addServlet(new ServletHolder(new ErrorServlet(500)), "/500");
+
+		server.setUp();
+	}
+
+	protected void tearDown() throws Exception {
+		if (server != null) {
+			server.tearDown();
+		}
+		super.tearDown();
+	}
+
+	public void testHandler() throws Exception {
+		final URI uri = server.getURI();
+		assertEquals(404, ((HttpURLConnection) uri.resolve("/404").toURL()
+				.openConnection()).getResponseCode());
+		assertEquals(500, ((HttpURLConnection) uri.resolve("/500").toURL()
+				.openConnection()).getResponseCode());
+	}
+}
diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/FileResolverTest.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/FileResolverTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..d7ca5900f57bfe920be0a31b8bccba46171db53d
--- /dev/null
+++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/FileResolverTest.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2010, 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.http.test;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.http.server.resolver.FileResolver;
+import org.eclipse.jgit.http.server.resolver.ServiceNotEnabledException;
+import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
+import org.eclipse.jgit.lib.Repository;
+
+public class FileResolverTest extends LocalDiskRepositoryTestCase {
+	public void testUnreasonableNames() throws ServiceNotEnabledException {
+		assertUnreasonable("");
+		assertUnreasonable("a\\b");
+		assertUnreasonable("../b");
+		assertUnreasonable("a/../b");
+		assertUnreasonable("a/./b");
+		assertUnreasonable("a//b");
+
+		if (new File("/foo").isAbsolute())
+			assertUnreasonable("/foo");
+
+		if (new File("//server/share").isAbsolute())
+			assertUnreasonable("//server/share");
+
+		if (new File("C:/windows").isAbsolute())
+			assertUnreasonable("C:/windows");
+	}
+
+	private void assertUnreasonable(String name)
+			throws ServiceNotEnabledException {
+		FileResolver r = new FileResolver(new File("."), false);
+		try {
+			r.open(null, name);
+			fail("Opened unreasonable name \"" + name + "\"");
+		} catch (RepositoryNotFoundException e) {
+			assertEquals("repository not found: " + name, e.getMessage());
+			assertNull("has no cause", e.getCause());
+		}
+	}
+
+	public void testExportOk() throws IOException {
+		final Repository a = createBareRepository();
+		final String name = a.getDirectory().getName();
+		final File base = a.getDirectory().getParentFile();
+		final File export = new File(a.getDirectory(), "git-daemon-export-ok");
+		FileResolver resolver;
+
+		assertFalse("no git-daemon-export-ok", export.exists());
+		resolver = new FileResolver(base, false /* require flag */);
+		try {
+			resolver.open(null, name);
+			fail("opened non-exported repository");
+		} catch (ServiceNotEnabledException e) {
+			assertEquals("Service not enabled", e.getMessage());
+		}
+
+		resolver = new FileResolver(base, true /* export all */);
+		try {
+			resolver.open(null, name).close();
+		} catch (ServiceNotEnabledException e) {
+			fail("did not honor export-all flag");
+		}
+
+		export.createNewFile();
+		assertTrue("has git-daemon-export-ok", export.exists());
+		resolver = new FileResolver(base, false /* require flag */);
+		try {
+			resolver.open(null, name).close();
+		} catch (ServiceNotEnabledException e) {
+			fail("did not honor git-daemon-export-ok");
+		}
+	}
+
+	public void testNotAGitRepository() throws IOException,
+			ServiceNotEnabledException {
+		final Repository a = createBareRepository();
+		final String name = a.getDirectory().getName() + "-not-a-git";
+		final File base = a.getDirectory().getParentFile();
+		FileResolver resolver = new FileResolver(base, false);
+
+		try {
+			resolver.open(null, name);
+		} catch (RepositoryNotFoundException e) {
+			assertEquals("repository not found: " + name, e.getMessage());
+
+			Throwable why = e.getCause();
+			assertNotNull("has cause", why);
+			assertEquals("repository not found: "
+					+ new File(base, name).getAbsolutePath(), why.getMessage());
+		}
+	}
+}
diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/GitServletInitTest.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/GitServletInitTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..8407036782bb50e5999561eb821d278d97ce2f54
--- /dev/null
+++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/GitServletInitTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2010, 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.http.test;
+
+import java.util.List;
+
+import javax.servlet.ServletException;
+
+import junit.framework.TestCase;
+
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.eclipse.jgit.http.server.GitServlet;
+import org.eclipse.jgit.http.test.util.AppServer;
+import org.eclipse.jgit.http.test.util.MockServletConfig;
+import org.eclipse.jgit.http.test.util.RecordingLogger;
+
+public class GitServletInitTest extends TestCase {
+	private AppServer server;
+
+	protected void tearDown() throws Exception {
+		if (server != null) {
+			server.tearDown();
+			server = null;
+		}
+		super.tearDown();
+	}
+
+	public void testDefaultConstructor_NoBasePath() throws Exception {
+		GitServlet s = new GitServlet();
+		try {
+			s.init(new MockServletConfig());
+			fail("Init did not crash due to missing parameter");
+		} catch (ServletException e) {
+			assertTrue(e.getMessage().contains("base-path"));
+		}
+	}
+
+	public void testDefaultConstructor_WithBasePath() throws Exception {
+		MockServletConfig c = new MockServletConfig();
+		c.setInitParameter("base-path", ".");
+		c.setInitParameter("export-all", "false");
+
+		GitServlet s = new GitServlet();
+		s.init(c);
+		s.destroy();
+	}
+
+	public void testInitUnderContainer_NoBasePath() throws Exception {
+		server = new AppServer();
+
+		ServletContextHandler app = server.addContext("/");
+		ServletHolder s = app.addServlet(GitServlet.class, "/git");
+		s.setInitOrder(1);
+
+		server.setUp();
+
+		List<RecordingLogger.Warning> events = RecordingLogger.getWarnings();
+		assertFalse("Servlet started without base-path", events.isEmpty());
+
+		Throwable why = events.get(0).getCause();
+		assertTrue("Caught ServletException", why instanceof ServletException);
+		assertTrue("Wanted base-path", why.getMessage().contains("base-path"));
+	}
+
+	public void testInitUnderContainer_WithBasePath() throws Exception {
+		server = new AppServer();
+
+		ServletContextHandler app = server.addContext("/");
+		ServletHolder s = app.addServlet(GitServlet.class, "/git");
+		s.setInitOrder(1);
+		s.setInitParameter("base-path", ".");
+		s.setInitParameter("export-all", "true");
+
+		server.setUp();
+		assertTrue("no warnings", RecordingLogger.getWarnings().isEmpty());
+	}
+}
diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/HttpClientTests.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/HttpClientTests.java
new file mode 100644
index 0000000000000000000000000000000000000000..729466df3bd348a701824f46aa59ac22f496cdd9
--- /dev/null
+++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/HttpClientTests.java
@@ -0,0 +1,328 @@
+/*
+ * Copyright (C) 2009-2010, 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.http.test;
+
+import java.io.File;
+import java.net.URI;
+import java.util.List;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.eclipse.jetty.servlet.DefaultServlet;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.eclipse.jgit.errors.NoRemoteRepositoryException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.errors.TransportException;
+import org.eclipse.jgit.http.server.GitServlet;
+import org.eclipse.jgit.http.server.resolver.RepositoryResolver;
+import org.eclipse.jgit.http.server.resolver.ServiceNotEnabledException;
+import org.eclipse.jgit.http.test.util.AccessEvent;
+import org.eclipse.jgit.http.test.util.HttpTestCase;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Constants;
+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.transport.FetchConnection;
+import org.eclipse.jgit.transport.Transport;
+import org.eclipse.jgit.transport.URIish;
+
+public class HttpClientTests extends HttpTestCase {
+	private TestRepository remoteRepository;
+
+	private URIish dumbAuthNoneURI;
+
+	private URIish dumbAuthBasicURI;
+
+	private URIish smartAuthNoneURI;
+
+	private URIish smartAuthBasicURI;
+
+	protected void setUp() throws Exception {
+		super.setUp();
+
+		remoteRepository = createTestRepository();
+		remoteRepository.update(master, remoteRepository.commit().create());
+
+		ServletContextHandler dNone = dumb("/dnone");
+		ServletContextHandler dBasic = server.authBasic(dumb("/dbasic"));
+
+		ServletContextHandler sNone = smart("/snone");
+		ServletContextHandler sBasic = server.authBasic(smart("/sbasic"));
+
+		server.setUp();
+
+		final String srcName = nameOf(remoteRepository);
+		dumbAuthNoneURI = toURIish(dNone, srcName);
+		dumbAuthBasicURI = toURIish(dBasic, srcName);
+
+		smartAuthNoneURI = toURIish(sNone, srcName);
+		smartAuthBasicURI = toURIish(sBasic, srcName);
+	}
+
+	private ServletContextHandler dumb(final String path) {
+		final File srcGit = remoteRepository.getRepository().getDirectory();
+		final URI base = srcGit.getParentFile().toURI();
+
+		ServletContextHandler ctx = server.addContext(path);
+		ctx.setResourceBase(base.toString());
+		ctx.addServlet(DefaultServlet.class, "/");
+		return ctx;
+	}
+
+	private ServletContextHandler smart(final String path) {
+		GitServlet gs = new GitServlet();
+		gs.setRepositoryResolver(new RepositoryResolver() {
+			public Repository open(HttpServletRequest req, String name)
+					throws RepositoryNotFoundException,
+					ServiceNotEnabledException {
+				if (!name.equals(nameOf(remoteRepository)))
+					throw new RepositoryNotFoundException(name);
+
+				final Repository db = remoteRepository.getRepository();
+				db.incrementOpen();
+				return db;
+			}
+		});
+
+		ServletContextHandler ctx = server.addContext(path);
+		ctx.addServlet(new ServletHolder(gs), "/*");
+		return ctx;
+	}
+
+	private static String nameOf(final TestRepository db) {
+		return db.getRepository().getDirectory().getName();
+	}
+
+	public void testRepositoryNotFound_Dumb() throws Exception {
+		URIish uri = toURIish("/dumb.none/not-found");
+		Repository dst = createBareRepository();
+		Transport t = Transport.open(dst, uri);
+		try {
+			try {
+				t.openFetch();
+				fail("connection opened to not found repository");
+			} catch (NoRemoteRepositoryException err) {
+				String exp = uri + ": " + uri
+						+ "/info/refs?service=git-upload-pack not found";
+				assertEquals(exp, err.getMessage());
+			}
+		} finally {
+			t.close();
+		}
+	}
+
+	public void testRepositoryNotFound_Smart() throws Exception {
+		URIish uri = toURIish("/smart.none/not-found");
+		Repository dst = createBareRepository();
+		Transport t = Transport.open(dst, uri);
+		try {
+			try {
+				t.openFetch();
+				fail("connection opened to not found repository");
+			} catch (NoRemoteRepositoryException err) {
+				String exp = uri + ": " + uri
+						+ "/info/refs?service=git-upload-pack not found";
+				assertEquals(exp, err.getMessage());
+			}
+		} finally {
+			t.close();
+		}
+	}
+
+	public void testListRemote_Dumb_DetachedHEAD() throws Exception {
+		Repository src = remoteRepository.getRepository();
+		RefUpdate u = src.updateRef(Constants.HEAD, true);
+		RevCommit Q = remoteRepository.commit().message("Q").create();
+		u.setNewObjectId(Q);
+		assertEquals(RefUpdate.Result.FORCED, u.forceUpdate());
+
+		Repository dst = createBareRepository();
+		Ref head;
+		Transport t = Transport.open(dst, dumbAuthNoneURI);
+		try {
+			FetchConnection c = t.openFetch();
+			try {
+				head = c.getRef(Constants.HEAD);
+			} finally {
+				c.close();
+			}
+		} finally {
+			t.close();
+		}
+		assertNotNull("has " + Constants.HEAD, head);
+		assertEquals(Q, head.getObjectId());
+	}
+
+	public void testListRemote_Dumb_NoHEAD() throws Exception {
+		Repository src = remoteRepository.getRepository();
+		File headref = new File(src.getDirectory(), Constants.HEAD);
+		assertTrue("HEAD used to be present", headref.delete());
+		assertFalse("HEAD is gone", headref.exists());
+
+		Repository dst = createBareRepository();
+		Ref head;
+		Transport t = Transport.open(dst, dumbAuthNoneURI);
+		try {
+			FetchConnection c = t.openFetch();
+			try {
+				head = c.getRef(Constants.HEAD);
+			} finally {
+				c.close();
+			}
+		} finally {
+			t.close();
+		}
+		assertNull("has no " + Constants.HEAD, head);
+	}
+
+	public void testListRemote_Smart_DetachedHEAD() throws Exception {
+		Repository src = remoteRepository.getRepository();
+		RefUpdate u = src.updateRef(Constants.HEAD, true);
+		RevCommit Q = remoteRepository.commit().message("Q").create();
+		u.setNewObjectId(Q);
+		assertEquals(RefUpdate.Result.FORCED, u.forceUpdate());
+
+		Repository dst = createBareRepository();
+		Ref head;
+		Transport t = Transport.open(dst, smartAuthNoneURI);
+		try {
+			FetchConnection c = t.openFetch();
+			try {
+				head = c.getRef(Constants.HEAD);
+			} finally {
+				c.close();
+			}
+		} finally {
+			t.close();
+		}
+		assertNotNull("has " + Constants.HEAD, head);
+		assertEquals(Q, head.getObjectId());
+	}
+
+	public void testListRemote_Smart_WithQueryParameters() throws Exception {
+		URIish myURI = toURIish("/snone/do?r=1&p=test.git");
+		Repository dst = createBareRepository();
+		Transport t = Transport.open(dst, myURI);
+		try {
+			try {
+				t.openFetch();
+				fail("test did not fail to find repository as expected");
+			} catch (NoRemoteRepositoryException err) {
+				// expected
+			}
+		} finally {
+			t.close();
+		}
+
+		List<AccessEvent> requests = getRequests();
+		assertEquals(1, requests.size());
+
+		AccessEvent info = requests.get(0);
+		assertEquals("GET", info.getMethod());
+		assertEquals("/snone/do", info.getPath());
+		assertEquals(3, info.getParameters().size());
+		assertEquals("1", info.getParameter("r"));
+		assertEquals("test.git/info/refs", info.getParameter("p"));
+		assertEquals("git-upload-pack", info.getParameter("service"));
+		assertEquals(404, info.getStatus());
+	}
+
+	public void testListRemote_Dumb_NeedsAuth() throws Exception {
+		Repository dst = createBareRepository();
+		Transport t = Transport.open(dst, dumbAuthBasicURI);
+		try {
+			try {
+				t.openFetch();
+				fail("connection opened even info/refs needs auth basic");
+			} catch (TransportException err) {
+				String status = "401 Unauthorized";
+				String exp = dumbAuthBasicURI + ": " + status;
+				assertEquals(exp, err.getMessage());
+			}
+		} finally {
+			t.close();
+		}
+	}
+
+	public void testListRemote_Smart_UploadPackNeedsAuth() throws Exception {
+		Repository dst = createBareRepository();
+		Transport t = Transport.open(dst, smartAuthBasicURI);
+		try {
+			try {
+				t.openFetch();
+				fail("connection opened even though service disabled");
+			} catch (TransportException err) {
+				String status = "401 Unauthorized";
+				String exp = smartAuthBasicURI + ": " + status;
+				assertEquals(exp, err.getMessage());
+			}
+		} finally {
+			t.close();
+		}
+	}
+
+	public void testListRemote_Smart_UploadPackDisabled() throws Exception {
+		Repository src = remoteRepository.getRepository();
+		src.getConfig().setBoolean("http", null, "uploadpack", false);
+		src.getConfig().save();
+
+		Repository dst = createBareRepository();
+		Transport t = Transport.open(dst, smartAuthNoneURI);
+		try {
+			try {
+				t.openFetch();
+				fail("connection opened even though service disabled");
+			} catch (TransportException err) {
+				String exp = smartAuthNoneURI
+						+ ": git-upload-pack not permitted";
+				assertEquals(exp, err.getMessage());
+			}
+		} finally {
+			t.close();
+		}
+	}
+}
diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerTest.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..fd78bb45152d55c466fe60bc952c871f3ce0c424
--- /dev/null
+++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/SmartClientSmartServerTest.java
@@ -0,0 +1,556 @@
+/*
+ * Copyright (C) 2009-2010, 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.http.test;
+
+import static org.eclipse.jgit.util.HttpSupport.HDR_CONTENT_ENCODING;
+import static org.eclipse.jgit.util.HttpSupport.HDR_CONTENT_LENGTH;
+import static org.eclipse.jgit.util.HttpSupport.HDR_CONTENT_TYPE;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.servlet.FilterHolder;
+import org.eclipse.jetty.servlet.FilterMapping;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.errors.TransportException;
+import org.eclipse.jgit.http.server.GitServlet;
+import org.eclipse.jgit.http.server.resolver.RepositoryResolver;
+import org.eclipse.jgit.http.server.resolver.ServiceNotEnabledException;
+import org.eclipse.jgit.http.test.util.AccessEvent;
+import org.eclipse.jgit.http.test.util.HttpTestCase;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.junit.TestRng;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.ReflogReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.RepositoryConfig;
+import org.eclipse.jgit.revwalk.RevBlob;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.FetchConnection;
+import org.eclipse.jgit.transport.HttpTransport;
+import org.eclipse.jgit.transport.RemoteRefUpdate;
+import org.eclipse.jgit.transport.Transport;
+import org.eclipse.jgit.transport.TransportHttp;
+import org.eclipse.jgit.transport.URIish;
+
+public class SmartClientSmartServerTest extends HttpTestCase {
+	private static final String HDR_TRANSFER_ENCODING = "Transfer-Encoding";
+
+	private Repository remoteRepository;
+
+	private URIish remoteURI;
+
+	private URIish brokenURI;
+
+	private RevBlob A_txt;
+
+	private RevCommit A, B;
+
+	protected void setUp() throws Exception {
+		super.setUp();
+
+		final TestRepository src = createTestRepository();
+		final String srcName = src.getRepository().getDirectory().getName();
+
+		ServletContextHandler app = server.addContext("/git");
+		GitServlet gs = new GitServlet();
+		gs.setRepositoryResolver(new RepositoryResolver() {
+			public Repository open(HttpServletRequest req, String name)
+					throws RepositoryNotFoundException,
+					ServiceNotEnabledException {
+				if (!name.equals(srcName))
+					throw new RepositoryNotFoundException(name);
+
+				final Repository db = src.getRepository();
+				db.incrementOpen();
+				return db;
+			}
+		});
+		app.addServlet(new ServletHolder(gs), "/*");
+
+		ServletContextHandler broken = server.addContext("/bad");
+		broken.addFilter(new FilterHolder(new Filter() {
+			public void doFilter(ServletRequest request,
+					ServletResponse response, FilterChain chain)
+					throws IOException, ServletException {
+				final HttpServletResponse r = (HttpServletResponse) response;
+				r.setContentType("text/plain");
+				r.setCharacterEncoding("UTF-8");
+				PrintWriter w = r.getWriter();
+				w.print("OK");
+				w.close();
+			}
+
+			public void init(FilterConfig filterConfig) throws ServletException {
+				//
+			}
+
+			public void destroy() {
+				//
+			}
+		}), "/" + srcName + "/git-upload-pack", FilterMapping.DEFAULT);
+		broken.addServlet(new ServletHolder(gs), "/*");
+
+		server.setUp();
+
+		remoteRepository = src.getRepository();
+		remoteURI = toURIish(app, srcName);
+		brokenURI = toURIish(broken, srcName);
+
+		A_txt = src.blob("A");
+		A = src.commit().add("A_txt", A_txt).create();
+		B = src.commit().parent(A).add("A_txt", "C").add("B", "B").create();
+		src.update(master, B);
+
+		src.update("refs/garbage/a/very/long/ref/name/to/compress", B);
+	}
+
+	public void testListRemote() throws IOException {
+		Repository dst = createBareRepository();
+
+		assertEquals("http", remoteURI.getScheme());
+
+		Map<String, Ref> map;
+		Transport t = Transport.open(dst, remoteURI);
+		try {
+			// I didn't make up these public interface names, I just
+			// approved them for inclusion into the code base. Sorry.
+			// --spearce
+			//
+			assertTrue("isa TransportHttp", t instanceof TransportHttp);
+			assertTrue("isa HttpTransport", t instanceof HttpTransport);
+
+			FetchConnection c = t.openFetch();
+			try {
+				map = c.getRefsMap();
+			} finally {
+				c.close();
+			}
+		} finally {
+			t.close();
+		}
+
+		assertNotNull("have map of refs", map);
+		assertEquals(3, map.size());
+
+		assertNotNull("has " + master, map.get(master));
+		assertEquals(B, map.get(master).getObjectId());
+
+		assertNotNull("has " + Constants.HEAD, map.get(Constants.HEAD));
+		assertEquals(B, map.get(Constants.HEAD).getObjectId());
+
+		List<AccessEvent> requests = getRequests();
+		assertEquals(1, requests.size());
+
+		AccessEvent info = requests.get(0);
+		assertEquals("GET", info.getMethod());
+		assertEquals(join(remoteURI, "info/refs"), info.getPath());
+		assertEquals(1, info.getParameters().size());
+		assertEquals("git-upload-pack", info.getParameter("service"));
+		assertEquals(200, info.getStatus());
+		assertEquals("application/x-git-upload-pack-advertisement", info
+				.getResponseHeader(HDR_CONTENT_TYPE));
+		assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING));
+	}
+
+	public void testInitialClone_Small() throws Exception {
+		Repository dst = createBareRepository();
+		assertFalse(dst.hasObject(A_txt));
+
+		Transport t = Transport.open(dst, remoteURI);
+		try {
+			t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
+		} finally {
+			t.close();
+		}
+
+		assertTrue(dst.hasObject(A_txt));
+		assertEquals(B, dst.getRef(master).getObjectId());
+		fsck(dst, B);
+
+		List<AccessEvent> requests = getRequests();
+		assertEquals(2, requests.size());
+
+		AccessEvent info = requests.get(0);
+		assertEquals("GET", info.getMethod());
+		assertEquals(join(remoteURI, "info/refs"), info.getPath());
+		assertEquals(1, info.getParameters().size());
+		assertEquals("git-upload-pack", info.getParameter("service"));
+		assertEquals(200, info.getStatus());
+		assertEquals("application/x-git-upload-pack-advertisement", info
+				.getResponseHeader(HDR_CONTENT_TYPE));
+		assertEquals("gzip", info.getResponseHeader(HDR_CONTENT_ENCODING));
+
+		AccessEvent service = requests.get(1);
+		assertEquals("POST", service.getMethod());
+		assertEquals(join(remoteURI, "git-upload-pack"), service.getPath());
+		assertEquals(0, service.getParameters().size());
+		assertNotNull("has content-length", service
+				.getRequestHeader(HDR_CONTENT_LENGTH));
+		assertNull("not chunked", service
+				.getRequestHeader(HDR_TRANSFER_ENCODING));
+		assertNull("no compression (too small)", service
+				.getRequestHeader(HDR_CONTENT_ENCODING));
+
+		assertEquals(200, service.getStatus());
+		assertEquals("application/x-git-upload-pack-result", service
+				.getResponseHeader(HDR_CONTENT_TYPE));
+		assertNull("no compression (never compressed)", service
+				.getResponseHeader(HDR_CONTENT_ENCODING));
+	}
+
+	public void testFetchUpdateExisting() throws Exception {
+		// Bootstrap by doing the clone.
+		//
+		TestRepository dst = createTestRepository();
+		Transport t = Transport.open(dst.getRepository(), remoteURI);
+		try {
+			t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
+		} finally {
+			t.close();
+		}
+		assertEquals(B, dst.getRepository().getRef(master).getObjectId());
+		List<AccessEvent> cloneRequests = getRequests();
+
+		// Force enough into the local client that enumeration will
+		// need multiple packets, but not too many to overflow and
+		// not pick up the ACK_COMMON message.
+		//
+		TestRepository.BranchBuilder b = dst.branch(master);
+		for (int i = 0; i < 32 - 1; i++)
+			b.commit().tick(3600 /* 1 hour */).message("c" + i).create();
+
+		// Create a new commit on the remote.
+		//
+		b = new TestRepository(remoteRepository).branch(master);
+		RevCommit Z = b.commit().message("Z").create();
+
+		// Now incrementally update.
+		//
+		t = Transport.open(dst.getRepository(), remoteURI);
+		try {
+			t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
+		} finally {
+			t.close();
+		}
+		assertEquals(Z, dst.getRepository().getRef(master).getObjectId());
+
+		List<AccessEvent> requests = getRequests();
+		requests.removeAll(cloneRequests);
+		assertEquals(3, requests.size());
+
+		AccessEvent info = requests.get(0);
+		assertEquals("GET", info.getMethod());
+		assertEquals(join(remoteURI, "info/refs"), info.getPath());
+		assertEquals(1, info.getParameters().size());
+		assertEquals("git-upload-pack", info.getParameter("service"));
+		assertEquals(200, info.getStatus());
+		assertEquals("application/x-git-upload-pack-advertisement", info
+				.getResponseHeader(HDR_CONTENT_TYPE));
+
+		// We should have needed two requests to perform the fetch
+		// due to the high number of local unknown commits.
+		//
+		AccessEvent service = requests.get(1);
+		assertEquals("POST", service.getMethod());
+		assertEquals(join(remoteURI, "git-upload-pack"), service.getPath());
+		assertEquals(0, service.getParameters().size());
+		assertNotNull("has content-length", service
+				.getRequestHeader(HDR_CONTENT_LENGTH));
+		assertNull("not chunked", service
+				.getRequestHeader(HDR_TRANSFER_ENCODING));
+
+		assertEquals(200, service.getStatus());
+		assertEquals("application/x-git-upload-pack-result", service
+				.getResponseHeader(HDR_CONTENT_TYPE));
+
+		service = requests.get(2);
+		assertEquals("POST", service.getMethod());
+		assertEquals(join(remoteURI, "git-upload-pack"), service.getPath());
+		assertEquals(0, service.getParameters().size());
+		assertNotNull("has content-length", service
+				.getRequestHeader(HDR_CONTENT_LENGTH));
+		assertNull("not chunked", service
+				.getRequestHeader(HDR_TRANSFER_ENCODING));
+
+		assertEquals(200, service.getStatus());
+		assertEquals("application/x-git-upload-pack-result", service
+				.getResponseHeader(HDR_CONTENT_TYPE));
+	}
+
+	public void testInitialClone_BrokenServer() throws Exception {
+		Repository dst = createBareRepository();
+		assertFalse(dst.hasObject(A_txt));
+
+		Transport t = Transport.open(dst, brokenURI);
+		try {
+			try {
+				t.fetch(NullProgressMonitor.INSTANCE, mirror(master));
+				fail("fetch completed despite upload-pack being broken");
+			} catch (TransportException err) {
+				String exp = brokenURI + ": expected"
+						+ " Content-Type application/x-git-upload-pack-result;"
+						+ " received Content-Type text/plain;charset=UTF-8";
+				assertEquals(exp, err.getMessage());
+			}
+		} finally {
+			t.close();
+		}
+
+		List<AccessEvent> requests = getRequests();
+		assertEquals(2, requests.size());
+
+		AccessEvent info = requests.get(0);
+		assertEquals("GET", info.getMethod());
+		assertEquals(join(brokenURI, "info/refs"), info.getPath());
+		assertEquals(1, info.getParameters().size());
+		assertEquals("git-upload-pack", info.getParameter("service"));
+		assertEquals(200, info.getStatus());
+		assertEquals("application/x-git-upload-pack-advertisement", info
+				.getResponseHeader(HDR_CONTENT_TYPE));
+
+		AccessEvent service = requests.get(1);
+		assertEquals("POST", service.getMethod());
+		assertEquals(join(brokenURI, "git-upload-pack"), service.getPath());
+		assertEquals(0, service.getParameters().size());
+		assertEquals(200, service.getStatus());
+		assertEquals("text/plain;charset=UTF-8", service
+				.getResponseHeader(HDR_CONTENT_TYPE));
+	}
+
+	public void testPush_NotAuthorized() throws Exception {
+		final TestRepository src = createTestRepository();
+		final RevBlob Q_txt = src.blob("new text");
+		final RevCommit Q = src.commit().add("Q", Q_txt).create();
+		final Repository db = src.getRepository();
+		final String dstName = Constants.R_HEADS + "new.branch";
+		Transport t;
+
+		// push anonymous shouldn't be allowed.
+		//
+		t = Transport.open(db, remoteURI);
+		try {
+			final String srcExpr = Q.name();
+			final boolean forceUpdate = false;
+			final String localName = null;
+			final ObjectId oldId = null;
+
+			RemoteRefUpdate u = new RemoteRefUpdate(src.getRepository(),
+					srcExpr, dstName, forceUpdate, localName, oldId);
+			try {
+				t.push(NullProgressMonitor.INSTANCE, Collections.singleton(u));
+				fail("anonymous push incorrectly accepted without error");
+			} catch (TransportException e) {
+				final String status = "401 Unauthorized";
+				final String exp = remoteURI.toString() + ": " + status;
+				assertEquals(exp, e.getMessage());
+			}
+		} finally {
+			t.close();
+		}
+
+		List<AccessEvent> requests = getRequests();
+		assertEquals(1, requests.size());
+
+		AccessEvent info = requests.get(0);
+		assertEquals("GET", info.getMethod());
+		assertEquals(join(remoteURI, "info/refs"), info.getPath());
+		assertEquals(1, info.getParameters().size());
+		assertEquals("git-receive-pack", info.getParameter("service"));
+		assertEquals(401, info.getStatus());
+	}
+
+	public void testPush_CreateBranch() throws Exception {
+		final TestRepository src = createTestRepository();
+		final RevBlob Q_txt = src.blob("new text");
+		final RevCommit Q = src.commit().add("Q", Q_txt).create();
+		final Repository db = src.getRepository();
+		final String dstName = Constants.R_HEADS + "new.branch";
+		Transport t;
+
+		enableReceivePack();
+
+		t = Transport.open(db, remoteURI);
+		try {
+			final String srcExpr = Q.name();
+			final boolean forceUpdate = false;
+			final String localName = null;
+			final ObjectId oldId = null;
+
+			RemoteRefUpdate u = new RemoteRefUpdate(src.getRepository(),
+					srcExpr, dstName, forceUpdate, localName, oldId);
+			t.push(NullProgressMonitor.INSTANCE, Collections.singleton(u));
+		} finally {
+			t.close();
+		}
+
+		assertTrue(remoteRepository.hasObject(Q_txt));
+		assertNotNull("has " + dstName, remoteRepository.getRef(dstName));
+		assertEquals(Q, remoteRepository.getRef(dstName).getObjectId());
+		fsck(remoteRepository, Q);
+
+		final ReflogReader log = remoteRepository.getReflogReader(dstName);
+		assertNotNull("has log for " + dstName);
+
+		final ReflogReader.Entry last = log.getLastEntry();
+		assertNotNull("has last entry", last);
+		assertEquals(ObjectId.zeroId(), last.getOldId());
+		assertEquals(Q, last.getNewId());
+		assertEquals("anonymous", last.getWho().getName());
+
+		// Assumption: The host name we use to contact the server should
+		// be the server's own host name, because it should be the loopback
+		// network interface.
+		//
+		final String clientHost = remoteURI.getHost();
+		assertEquals("anonymous@" + clientHost, last.getWho().getEmailAddress());
+		assertEquals("push: created", last.getComment());
+
+		List<AccessEvent> requests = getRequests();
+		assertEquals(2, requests.size());
+
+		AccessEvent info = requests.get(0);
+		assertEquals("GET", info.getMethod());
+		assertEquals(join(remoteURI, "info/refs"), info.getPath());
+		assertEquals(1, info.getParameters().size());
+		assertEquals("git-receive-pack", info.getParameter("service"));
+		assertEquals(200, info.getStatus());
+		assertEquals("application/x-git-receive-pack-advertisement", info
+				.getResponseHeader(HDR_CONTENT_TYPE));
+
+		AccessEvent service = requests.get(1);
+		assertEquals("POST", service.getMethod());
+		assertEquals(join(remoteURI, "git-receive-pack"), service.getPath());
+		assertEquals(0, service.getParameters().size());
+		assertNotNull("has content-length", service
+				.getRequestHeader(HDR_CONTENT_LENGTH));
+		assertNull("not chunked", service
+				.getRequestHeader(HDR_TRANSFER_ENCODING));
+
+		assertEquals(200, service.getStatus());
+		assertEquals("application/x-git-receive-pack-result", service
+				.getResponseHeader(HDR_CONTENT_TYPE));
+	}
+
+	public void testPush_ChunkedEncoding() throws Exception {
+		final TestRepository src = createTestRepository();
+		final RevBlob Q_bin = src.blob(new TestRng("Q").nextBytes(128 * 1024));
+		final RevCommit Q = src.commit().add("Q", Q_bin).create();
+		final Repository db = src.getRepository();
+		final String dstName = Constants.R_HEADS + "new.branch";
+		Transport t;
+
+		enableReceivePack();
+
+		db.getConfig().setInt("core", null, "compression", 0);
+		db.getConfig().setInt("http", null, "postbuffer", 8 * 1024);
+		db.getConfig().save();
+
+		t = Transport.open(db, remoteURI);
+		try {
+			final String srcExpr = Q.name();
+			final boolean forceUpdate = false;
+			final String localName = null;
+			final ObjectId oldId = null;
+
+			RemoteRefUpdate u = new RemoteRefUpdate(src.getRepository(),
+					srcExpr, dstName, forceUpdate, localName, oldId);
+			t.push(NullProgressMonitor.INSTANCE, Collections.singleton(u));
+		} finally {
+			t.close();
+		}
+
+		assertTrue(remoteRepository.hasObject(Q_bin));
+		assertNotNull("has " + dstName, remoteRepository.getRef(dstName));
+		assertEquals(Q, remoteRepository.getRef(dstName).getObjectId());
+		fsck(remoteRepository, Q);
+
+		List<AccessEvent> requests = getRequests();
+		assertEquals(2, requests.size());
+
+		AccessEvent info = requests.get(0);
+		assertEquals("GET", info.getMethod());
+		assertEquals(join(remoteURI, "info/refs"), info.getPath());
+		assertEquals(1, info.getParameters().size());
+		assertEquals("git-receive-pack", info.getParameter("service"));
+		assertEquals(200, info.getStatus());
+		assertEquals("application/x-git-receive-pack-advertisement", info
+				.getResponseHeader(HDR_CONTENT_TYPE));
+
+		AccessEvent service = requests.get(1);
+		assertEquals("POST", service.getMethod());
+		assertEquals(join(remoteURI, "git-receive-pack"), service.getPath());
+		assertEquals(0, service.getParameters().size());
+		assertNull("no content-length", service
+				.getRequestHeader(HDR_CONTENT_LENGTH));
+		assertEquals("chunked", service.getRequestHeader(HDR_TRANSFER_ENCODING));
+
+		assertEquals(200, service.getStatus());
+		assertEquals("application/x-git-receive-pack-result", service
+				.getResponseHeader(HDR_CONTENT_TYPE));
+	}
+
+	private void enableReceivePack() throws IOException {
+		final RepositoryConfig cfg = remoteRepository.getConfig();
+		cfg.setBoolean("http", null, "receivepack", true);
+		cfg.save();
+	}
+}
diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/util/AccessEvent.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/util/AccessEvent.java
new file mode 100644
index 0000000000000000000000000000000000000000..c6d531e5b3e66844473860fe99ae38b5bf9f55b3
--- /dev/null
+++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/util/AccessEvent.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2010, 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.http.test.util;
+
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Map;
+import java.util.TreeMap;
+
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+
+/** A single request made through {@link AppServer}. */
+public class AccessEvent {
+	private final String method;
+
+	private final String uri;
+
+	private final Map<String, String> requestHeaders;
+
+	private final Map<String, String[]> parameters;
+
+	private final int status;
+
+	private final Map<String, String> responseHeaders;
+
+	AccessEvent(final Request req, final Response rsp) {
+		method = req.getMethod();
+		uri = req.getRequestURI();
+		requestHeaders = cloneHeaders(req);
+		parameters = clone(req.getParameterMap());
+
+		status = rsp.getStatus();
+		responseHeaders = cloneHeaders(rsp);
+	}
+
+	private static Map<String, String> cloneHeaders(final Request req) {
+		Map<String, String> r = new TreeMap<String, String>();
+		Enumeration hn = req.getHeaderNames();
+		while (hn.hasMoreElements()) {
+			String key = (String) hn.nextElement();
+			if (!r.containsKey(key)) {
+				r.put(key, req.getHeader(key));
+			}
+		}
+		return Collections.unmodifiableMap(r);
+	}
+
+	private static Map<String, String> cloneHeaders(final Response rsp) {
+		Map<String, String> r = new TreeMap<String, String>();
+		Enumeration<String> hn = rsp.getHttpFields().getFieldNames();
+		while (hn.hasMoreElements()) {
+			String key = hn.nextElement();
+			if (!r.containsKey(key)) {
+				Enumeration<String> v = rsp.getHttpFields().getValues(key);
+				r.put(key, v.nextElement());
+			}
+		}
+		return Collections.unmodifiableMap(r);
+	}
+
+	@SuppressWarnings("unchecked")
+	private static Map<String, String[]> clone(Map parameterMap) {
+		return new TreeMap<String, String[]>(parameterMap);
+	}
+
+	/** @return {@code "GET"} or {@code "POST"} */
+	public String getMethod() {
+		return method;
+	}
+
+	/** @return path of the file on the server, e.g. {@code /git/HEAD}. */
+	public String getPath() {
+		return uri;
+	}
+
+	/**
+	 * @param name
+	 *            name of the request header to read.
+	 * @return first value of the request header; null if not sent.
+	 */
+	public String getRequestHeader(String name) {
+		return requestHeaders.get(name);
+	}
+
+	/**
+	 * @param name
+	 *            name of the request parameter to read.
+	 * @return first value of the request parameter; null if not sent.
+	 */
+	public String getParameter(String name) {
+		String[] r = parameters.get(name);
+		return r != null && 1 <= r.length ? r[0] : null;
+	}
+
+	/** @return all parameters in the request. */
+	public Map<String, String[]> getParameters() {
+		return parameters;
+	}
+
+	/** @return HTTP status code of the response, e.g. 200, 403, 500. */
+	public int getStatus() {
+		return status;
+	}
+
+	/**
+	 * @param name
+	 *            name of the response header to read.
+	 * @return first value of the response header; null if not sent.
+	 */
+	public String getResponseHeader(String name) {
+		return responseHeaders.get(name);
+	}
+
+	public String toString() {
+		StringBuilder b = new StringBuilder();
+		b.append(method);
+		b.append(' ');
+		b.append(uri);
+		if (!parameters.isEmpty()) {
+			b.append('?');
+			boolean first = true;
+			for (Map.Entry<String, String[]> e : parameters.entrySet()) {
+				for (String val : e.getValue()) {
+					if (!first) {
+						b.append('&');
+					}
+					first = false;
+
+					b.append(e.getKey());
+					b.append('=');
+					b.append(val);
+				}
+			}
+		}
+		b.append(' ');
+		b.append(status);
+		return b.toString();
+	}
+}
diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/util/AppServer.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/util/AppServer.java
new file mode 100644
index 0000000000000000000000000000000000000000..74df7086ea099243f58031466e1018c30da8f32b
--- /dev/null
+++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/util/AppServer.java
@@ -0,0 +1,295 @@
+/*
+ * Copyright (C) 2010, 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.http.test.util;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.List;
+
+import junit.framework.Assert;
+
+import org.eclipse.jetty.http.security.Constraint;
+import org.eclipse.jetty.http.security.Password;
+import org.eclipse.jetty.security.Authenticator;
+import org.eclipse.jetty.security.ConstraintMapping;
+import org.eclipse.jetty.security.ConstraintSecurityHandler;
+import org.eclipse.jetty.security.MappedLoginService;
+import org.eclipse.jetty.security.authentication.BasicAuthenticator;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.UserIdentity;
+import org.eclipse.jetty.server.handler.ContextHandlerCollection;
+import org.eclipse.jetty.server.handler.RequestLogHandler;
+import org.eclipse.jetty.server.nio.SelectChannelConnector;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.eclipse.jgit.transport.URIish;
+
+/**
+ * Tiny web application server for unit testing.
+ * <p>
+ * Tests should start the server in their {@code setUp()} method and stop the
+ * server in their {@code tearDown()} method. Only while started the server's
+ * URL and/or port number can be obtained.
+ */
+public class AppServer {
+	/** Realm name for the secure access areas. */
+	public static final String realm = "Secure Area";
+
+	/** Username for secured access areas. */
+	public static final String username = "agitter";
+
+	/** Password for {@link #username} in secured access areas. */
+	public static final String password = "letmein";
+
+	static {
+		// Install a logger that throws warning messages.
+		//
+		final String prop = "org.eclipse.jetty.util.log.class";
+		System.setProperty(prop, RecordingLogger.class.getName());
+	}
+
+	private final Server server;
+
+	private final Connector connector;
+
+	private final ContextHandlerCollection contexts;
+
+	private final TestRequestLog log;
+
+	public AppServer() {
+		connector = new SelectChannelConnector();
+		connector.setPort(0);
+		try {
+			final InetAddress me = InetAddress.getByName("localhost");
+			connector.setHost(me.getHostAddress());
+		} catch (UnknownHostException e) {
+			throw new RuntimeException("Cannot find localhost", e);
+		}
+
+		// We need a handful of threads in the thread pool, otherwise
+		// our tests will deadlock when they can't open enough requests.
+		// In theory we only need 1 concurrent connection at a time, but
+		// I suspect the JRE isn't doing request pipelining on existing
+		// connections like we want it to.
+		//
+		final QueuedThreadPool pool = new QueuedThreadPool();
+		pool.setMinThreads(1);
+		pool.setMaxThreads(4);
+		pool.setMaxQueued(8);
+
+		contexts = new ContextHandlerCollection();
+
+		log = new TestRequestLog();
+
+		final RequestLogHandler logHandler = new RequestLogHandler();
+		logHandler.setHandler(contexts);
+		logHandler.setRequestLog(log);
+
+		server = new Server();
+		server.setConnectors(new Connector[] { connector });
+		server.setThreadPool(pool);
+		server.setHandler(logHandler);
+
+		server.setStopAtShutdown(false);
+		server.setGracefulShutdown(0);
+	}
+
+	/**
+	 * Create a new servlet context within the server.
+	 * <p>
+	 * This method should be invoked before the server is started, once for each
+	 * context the caller wants to register.
+	 *
+	 * @param path
+	 *            path of the context; use "/" for the root context if binding
+	 *            to the root is desired.
+	 * @return the context to add servlets into.
+	 */
+	public ServletContextHandler addContext(String path) {
+		assertNotYetSetUp();
+		if ("".equals(path))
+			path = "/";
+
+		ServletContextHandler ctx = new ServletContextHandler();
+		ctx.setContextPath(path);
+		contexts.addHandler(ctx);
+
+		return ctx;
+	}
+
+	public ServletContextHandler authBasic(ServletContextHandler ctx) {
+		assertNotYetSetUp();
+		auth(ctx, new BasicAuthenticator());
+		return ctx;
+	}
+
+	private void auth(ServletContextHandler ctx, Authenticator authType) {
+		final String role = "can-access";
+
+		MappedLoginService users = new MappedLoginService() {
+			@Override
+			protected UserIdentity loadUser(String who) {
+				return null;
+			}
+
+			@Override
+			protected void loadUsers() throws IOException {
+				putUser(username, new Password(password), new String[] { role });
+			}
+		};
+
+		ConstraintMapping cm = new ConstraintMapping();
+		cm.setConstraint(new Constraint());
+		cm.getConstraint().setAuthenticate(true);
+		cm.getConstraint().setDataConstraint(Constraint.DC_NONE);
+		cm.getConstraint().setRoles(new String[] { role });
+		cm.setPathSpec("/*");
+
+		ConstraintSecurityHandler sec = new ConstraintSecurityHandler();
+		sec.setStrict(false);
+		sec.setRealmName(realm);
+		sec.setAuthenticator(authType);
+		sec.setLoginService(users);
+		sec.setConstraintMappings(new ConstraintMapping[] { cm });
+		sec.setHandler(ctx);
+
+		contexts.removeHandler(ctx);
+		contexts.addHandler(sec);
+	}
+
+	/**
+	 * Start the server on a random local port.
+	 *
+	 * @throws Exception
+	 *             the server cannot be started, testing is not possible.
+	 */
+	public void setUp() throws Exception {
+		RecordingLogger.clear();
+		log.clear();
+		server.start();
+	}
+
+	/**
+	 * Shutdown the server.
+	 *
+	 * @throws Exception
+	 *             the server refuses to halt, or wasn't running.
+	 */
+	public void tearDown() throws Exception {
+		RecordingLogger.clear();
+		log.clear();
+		server.stop();
+	}
+
+	/**
+	 * Get the URI to reference this server.
+	 * <p>
+	 * The returned URI includes the proper host name and port number, but does
+	 * not contain a path.
+	 *
+	 * @return URI to reference this server's root context.
+	 */
+	public URI getURI() {
+		assertAlreadySetUp();
+		String host = connector.getHost();
+		if (host.contains(":") && !host.startsWith("["))
+			host = "[" + host + "]";
+		final String uri = "http://" + host + ":" + getPort();
+		try {
+			return new URI(uri);
+		} catch (URISyntaxException e) {
+			throw new RuntimeException("Unexpected URI error on " + uri, e);
+		}
+	}
+
+	/** @return the local port number the server is listening on. */
+	public int getPort() {
+		assertAlreadySetUp();
+		return ((SelectChannelConnector) connector).getLocalPort();
+	}
+
+	/** @return all requests since the server was started. */
+	public List<AccessEvent> getRequests() {
+		return new ArrayList<AccessEvent>(log.getEvents());
+	}
+
+	/**
+	 * @param base
+	 *            base URI used to access the server.
+	 * @param path
+	 *            the path to locate requests for, relative to {@code base}.
+	 * @return all requests which match the given path.
+	 */
+	public List<AccessEvent> getRequests(URIish base, String path) {
+		return getRequests(HttpTestCase.join(base, path));
+	}
+
+	/**
+	 * @param path
+	 *            the path to locate requests for.
+	 * @return all requests which match the given path.
+	 */
+	public List<AccessEvent> getRequests(String path) {
+		ArrayList<AccessEvent> r = new ArrayList<AccessEvent>();
+		for (AccessEvent event : log.getEvents()) {
+			if (event.getPath().equals(path)) {
+				r.add(event);
+			}
+		}
+		return r;
+	}
+
+	private void assertNotYetSetUp() {
+		Assert.assertFalse("server is not running", server.isRunning());
+	}
+
+	private void assertAlreadySetUp() {
+		Assert.assertTrue("server is running", server.isRunning());
+	}
+}
diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/util/HttpTestCase.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/util/HttpTestCase.java
new file mode 100644
index 0000000000000000000000000000000000000000..e259757615ed04f73910ff15066a29fb99ba42ef
--- /dev/null
+++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/util/HttpTestCase.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2009-2010, 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.http.test.util;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.transport.RefSpec;
+import org.eclipse.jgit.transport.RemoteRefUpdate;
+import org.eclipse.jgit.transport.URIish;
+
+/** Base class for HTTP related transport testing. */
+public abstract class HttpTestCase extends LocalDiskRepositoryTestCase {
+	protected static final String master = Constants.R_HEADS + Constants.MASTER;
+
+	/** In-memory application server; subclass must start. */
+	protected AppServer server;
+
+	protected void setUp() throws Exception {
+		super.setUp();
+		server = new AppServer();
+	}
+
+	protected void tearDown() throws Exception {
+		server.tearDown();
+		super.tearDown();
+	}
+
+	protected TestRepository createTestRepository() throws Exception {
+		return new TestRepository(createBareRepository());
+	}
+
+	protected URIish toURIish(String path) throws URISyntaxException {
+		URI u = server.getURI().resolve(path);
+		return new URIish(u.toString());
+	}
+
+	protected URIish toURIish(ServletContextHandler app, String name)
+			throws URISyntaxException {
+		String p = app.getContextPath();
+		if (!p.endsWith("/") && !name.startsWith("/"))
+			p += "/";
+		p += name;
+		return toURIish(p);
+	}
+
+	protected List<AccessEvent> getRequests() {
+		return server.getRequests();
+	}
+
+	protected List<AccessEvent> getRequests(URIish base, String path) {
+		return server.getRequests(base, path);
+	}
+
+	protected List<AccessEvent> getRequests(String path) {
+		return server.getRequests(path);
+	}
+
+	protected static void fsck(Repository db, RevObject... tips)
+			throws Exception {
+		new TestRepository(db).fsck(tips);
+	}
+
+	protected static Set<RefSpec> mirror(String... refs) {
+		HashSet<RefSpec> r = new HashSet<RefSpec>();
+		for (String name : refs) {
+			RefSpec rs = new RefSpec(name);
+			rs = rs.setDestination(name);
+			rs = rs.setForceUpdate(true);
+			r.add(rs);
+		}
+		return r;
+	}
+
+	protected static Collection<RemoteRefUpdate> push(TestRepository from,
+			RevCommit q) throws IOException {
+		final Repository db = from.getRepository();
+		final String srcExpr = q.name();
+		final String dstName = master;
+		final boolean forceUpdate = true;
+		final String localName = null;
+		final ObjectId oldId = null;
+
+		RemoteRefUpdate u = new RemoteRefUpdate(db, srcExpr, dstName,
+				forceUpdate, localName, oldId);
+		return Collections.singleton(u);
+	}
+
+	public static String loose(URIish base, AnyObjectId id) {
+		final String objectName = id.name();
+		final String d = objectName.substring(0, 2);
+		final String f = objectName.substring(2);
+		return join(base, "objects/" + d + "/" + f);
+	}
+
+	public static String join(URIish base, String path) {
+		if (path.startsWith("/"))
+			fail("Cannot join absolute path " + path + " to URIish " + base);
+
+		String dir = base.getPath();
+		if (!dir.endsWith("/"))
+			dir += "/";
+		return dir + path;
+	}
+}
diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/util/MockServletConfig.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/util/MockServletConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..cf6f6f6a8f13a2f0e0822da5b72892a4bfab512d
--- /dev/null
+++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/util/MockServletConfig.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2010, 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.http.test.util;
+
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
+
+public class MockServletConfig implements ServletConfig {
+	private final Map<String, String> parameters = new HashMap<String, String>();
+
+	public void setInitParameter(String name, String value) {
+		parameters.put(name, value);
+	}
+
+	public String getInitParameter(String name) {
+		return parameters.get(name);
+	}
+
+	public Enumeration getInitParameterNames() {
+		final Iterator<String> i = parameters.keySet().iterator();
+		return new Enumeration<String>() {
+			public boolean hasMoreElements() {
+				return i.hasNext();
+			}
+
+			public String nextElement() {
+				return i.next();
+			}
+		};
+	}
+
+	public String getServletName() {
+		return "MOCK_SERVLET";
+	}
+
+	public ServletContext getServletContext() {
+		return null;
+	}
+}
\ No newline at end of file
diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/util/RecordingLogger.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/util/RecordingLogger.java
new file mode 100644
index 0000000000000000000000000000000000000000..24e125c22b0ce3e8be0e8277aa303c5e2c453d74
--- /dev/null
+++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/util/RecordingLogger.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2010, 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.http.test.util;
+
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.jetty.util.log.Logger;
+
+/** Logs warnings into an array for later inspection. */
+public class RecordingLogger implements Logger {
+	private static List<Warning> warnings = new ArrayList<Warning>();
+
+	/** Clear the warnings, automatically done by {@link AppServer#setUp()} */
+	public static void clear() {
+		synchronized (warnings) {
+			warnings.clear();
+		}
+	}
+
+	/** @return the warnings (if any) from the last execution */
+	public static List<Warning> getWarnings() {
+		synchronized (warnings) {
+			ArrayList<Warning> copy = new ArrayList<Warning>(warnings);
+			return Collections.unmodifiableList(copy);
+		}
+	}
+
+	@SuppressWarnings("serial")
+	public static class Warning extends Exception {
+		public Warning(String msg) {
+			super(msg);
+		}
+
+		public Warning(String msg, Throwable cause) {
+			super(msg, cause);
+		}
+	}
+
+	private final String name;
+
+	public RecordingLogger() {
+		this("");
+	}
+
+	public RecordingLogger(final String name) {
+		this.name = name;
+	}
+
+	public Logger getLogger(@SuppressWarnings("hiding") String name) {
+		return new RecordingLogger(name);
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	public void warn(String msg, Object arg0, Object arg1) {
+		synchronized (warnings) {
+			warnings.add(new Warning(MessageFormat.format(msg, arg0, arg1)));
+		}
+	}
+
+	public void warn(String msg, Throwable th) {
+		synchronized (warnings) {
+			warnings.add(new Warning(msg, th));
+		}
+	}
+
+	public void warn(String msg) {
+		synchronized (warnings) {
+			warnings.add(new Warning(msg));
+		}
+	}
+
+	public void debug(String msg, Object arg0, Object arg1) {
+		// Ignore (not relevant to test failures)
+	}
+
+	public void debug(String msg, Throwable th) {
+		// Ignore (not relevant to test failures)
+	}
+
+	public void debug(String msg) {
+		// Ignore (not relevant to test failures)
+	}
+
+	public void info(String msg, Object arg0, Object arg1) {
+		// Ignore (not relevant to test failures)
+	}
+
+	public void info(String msg) {
+		// Ignore (not relevant to test failures)
+	}
+
+	public boolean isDebugEnabled() {
+		return false;
+	}
+
+	public void setDebugEnabled(boolean enabled) {
+		// Ignore (not relevant to test failures)
+	}
+}
diff --git a/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/util/TestRequestLog.java b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/util/TestRequestLog.java
new file mode 100644
index 0000000000000000000000000000000000000000..904f6aac8e5fba169d1e0d2e1e7aa04fa7594e5c
--- /dev/null
+++ b/org.eclipse.jgit.http.test/tst/org/eclipse/jgit/http/test/util/TestRequestLog.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2010, 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.http.test.util;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.RequestLog;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+
+/** Logs request made through {@link AppServer}. */
+class TestRequestLog extends AbstractLifeCycle implements RequestLog {
+	private final List<AccessEvent> events = new ArrayList<AccessEvent>();
+
+	/** Reset the log back to its original empty state. */
+	synchronized void clear() {
+		events.clear();
+	}
+
+	/** @return all of the events made since the last clear. */
+	synchronized List<AccessEvent> getEvents() {
+		return events;
+	}
+
+	public synchronized void log(Request request, Response response) {
+		events.add(new AccessEvent(request, response));
+	}
+}
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 ec9b1d7ac361b483194b39c536ca5dd2653a81d7..ddace0df2ed12f7a4391d764ff359123354ff89a 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
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2009, Google Inc.
+ * Copyright (C) 2009-2010, Google Inc.
  * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
  * Copyright (C) 2007, Shawn O. Pearce <spearce@spearce.org>
  * and other copyright owners as documented in the project's IP log.
@@ -57,8 +57,10 @@
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 
+import junit.framework.Assert;
 import junit.framework.TestCase;
 
+import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileBasedConfig;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -404,6 +406,14 @@ protected String read(final File f) throws IOException {
 		return new String(body, 0, body.length, "UTF-8");
 	}
 
+	protected static void assertEquals(AnyObjectId exp, AnyObjectId act) {
+		if (exp != null)
+			exp = exp.copy();
+		if (act != null)
+			act = act.copy();
+		Assert.assertEquals(exp, act);
+	}
+
 	private static String[] toEnvArray(final Map<String, String> env) {
 		final String[] envp = new String[env.size()];
 		int i = 0;
diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java
index ce8b3e6ee6bd9d6a946368ab3422b54f9400e532..e738276bd84a443262052c5f23cc910d588a71e7 100644
--- a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java
+++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/TestRepository.java
@@ -44,11 +44,15 @@
 package org.eclipse.jgit.junit;
 
 import java.io.File;
+import java.io.FileOutputStream;
 import java.io.IOException;
+import java.security.MessageDigest;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 import junit.framework.Assert;
 import junit.framework.AssertionFailedError;
@@ -60,21 +64,30 @@
 import org.eclipse.jgit.dircache.DirCacheEditor.DeletePath;
 import org.eclipse.jgit.dircache.DirCacheEditor.DeleteTree;
 import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.errors.ObjectWritingException;
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.Commit;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.LockFile;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectChecker;
+import org.eclipse.jgit.lib.ObjectDatabase;
 import org.eclipse.jgit.lib.ObjectDirectory;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectWriter;
+import org.eclipse.jgit.lib.PackFile;
+import org.eclipse.jgit.lib.PackWriter;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.RefWriter;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.Tag;
+import org.eclipse.jgit.lib.PackIndex.MutableEntry;
+import org.eclipse.jgit.revwalk.ObjectWalk;
 import org.eclipse.jgit.revwalk.RevBlob;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevObject;
@@ -428,26 +441,25 @@ public <T extends AnyObjectId> T update(String ref, T obj) throws Exception {
 	 * @throws Exception
 	 */
 	public void updateServerInfo() throws Exception {
-		if (db.getObjectDatabase() instanceof ObjectDirectory) {
+		final ObjectDatabase odb = db.getObjectDatabase();
+		if (odb instanceof ObjectDirectory) {
 			RefWriter rw = new RefWriter(db.getAllRefs().values()) {
 				@Override
 				protected void writeFile(final String name, final byte[] bin)
 						throws IOException {
-					final File p = new File(db.getDirectory(), name);
-					final LockFile lck = new LockFile(p);
-					if (!lck.lock())
-						throw new ObjectWritingException("Can't write " + p);
-					try {
-						lck.write(bin);
-					} catch (IOException ioe) {
-						throw new ObjectWritingException("Can't write " + p);
-					}
-					if (!lck.commit())
-						throw new ObjectWritingException("Can't write " + p);
+					TestRepository.this.writeFile(name, bin);
 				}
 			};
 			rw.writePackedRefs();
 			rw.writeInfoRefs();
+
+			final StringBuilder w = new StringBuilder();
+			for (PackFile p : ((ObjectDirectory) odb).getPacks()) {
+				w.append("P ");
+				w.append(p.getPackFile().getName());
+				w.append('\n');
+			}
+			writeFile("objects/info/packs", Constants.encodeASCII(w.toString()));
 		}
 	}
 
@@ -484,6 +496,132 @@ public BranchBuilder branch(String ref) {
 		return new BranchBuilder(ref);
 	}
 
+	/**
+	 * Run consistency checks against the object database.
+	 * <p>
+	 * This method completes silently if the checks pass. A temporary revision
+	 * pool is constructed during the checking.
+	 *
+	 * @param tips
+	 *            the tips to start checking from; if not supplied the refs of
+	 *            the repository are used instead.
+	 * @throws MissingObjectException
+	 * @throws IncorrectObjectTypeException
+	 * @throws IOException
+	 */
+	public void fsck(RevObject... tips) throws MissingObjectException,
+			IncorrectObjectTypeException, IOException {
+		ObjectWalk ow = new ObjectWalk(db);
+		if (tips.length != 0) {
+			for (RevObject o : tips)
+				ow.markStart(ow.parseAny(o));
+		} else {
+			for (Ref r : db.getAllRefs().values())
+				ow.markStart(ow.parseAny(r.getObjectId()));
+		}
+
+		ObjectChecker oc = new ObjectChecker();
+		for (;;) {
+			final RevCommit o = ow.next();
+			if (o == null)
+				break;
+
+			final byte[] bin = db.openObject(o).getCachedBytes();
+			oc.checkCommit(bin);
+			assertHash(o, bin);
+		}
+
+		for (;;) {
+			final RevObject o = ow.nextObject();
+			if (o == null)
+				break;
+
+			final byte[] bin = db.openObject(o).getCachedBytes();
+			oc.check(o.getType(), bin);
+			assertHash(o, bin);
+		}
+	}
+
+	private static void assertHash(RevObject id, byte[] bin) {
+		MessageDigest md = Constants.newMessageDigest();
+		md.update(Constants.encodedTypeString(id.getType()));
+		md.update((byte) ' ');
+		md.update(Constants.encodeASCII(bin.length));
+		md.update((byte) 0);
+		md.update(bin);
+		Assert.assertEquals(id.copy(), ObjectId.fromRaw(md.digest()));
+	}
+
+	/**
+	 * Pack all reachable objects in the repository into a single pack file.
+	 * <p>
+	 * All loose objects are automatically pruned. Existing packs however are
+	 * not removed.
+	 *
+	 * @throws Exception
+	 */
+	public void packAndPrune() throws Exception {
+		final ObjectDirectory odb = (ObjectDirectory) db.getObjectDatabase();
+		final PackWriter pw = new PackWriter(db, NullProgressMonitor.INSTANCE);
+
+		Set<ObjectId> all = new HashSet<ObjectId>();
+		for (Ref r : db.getAllRefs().values())
+			all.add(r.getObjectId());
+		pw.preparePack(all, Collections.<ObjectId> emptySet());
+
+		final ObjectId name = pw.computeName();
+		FileOutputStream out;
+
+		final File pack = nameFor(odb, name, ".pack");
+		out = new FileOutputStream(pack);
+		try {
+			pw.writePack(out);
+		} finally {
+			out.close();
+		}
+		pack.setReadOnly();
+
+		final File idx = nameFor(odb, name, ".idx");
+		out = new FileOutputStream(idx);
+		try {
+			pw.writeIndex(out);
+		} finally {
+			out.close();
+		}
+		idx.setReadOnly();
+
+		odb.openPack(pack, idx);
+		updateServerInfo();
+		prunePacked(odb);
+	}
+
+	private void prunePacked(ObjectDirectory odb) {
+		for (PackFile p : odb.getPacks()) {
+			for (MutableEntry e : p)
+				odb.fileFor(e.toObjectId()).delete();
+		}
+	}
+
+	private static File nameFor(ObjectDirectory odb, ObjectId name, String t) {
+		File packdir = new File(odb.getDirectory(), "pack");
+		return new File(packdir, "pack-" + name.name() + t);
+	}
+
+	private void writeFile(final String name, final byte[] bin)
+			throws IOException, ObjectWritingException {
+		final File p = new File(db.getDirectory(), name);
+		final LockFile lck = new LockFile(p);
+		if (!lck.lock())
+			throw new ObjectWritingException("Can't write " + p);
+		try {
+			lck.write(bin);
+		} catch (IOException ioe) {
+			throw new ObjectWritingException("Can't write " + p);
+		}
+		if (!lck.commit())
+			throw new ObjectWritingException("Can't write " + p);
+	}
+
 	/** Helper to build a branch with one or more commits */
 	public class BranchBuilder {
 		private final String ref;
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 62303de9f0bd3458ccbeb3125aa197306c91f3bb..c521e805146dd44869525c5c496c14d18721d0b9 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java
@@ -155,6 +155,8 @@ private static class HttpConfig {
 
 	private final ProxySelector proxySelector;
 
+	private boolean useSmartHttp = true;
+
 	TransportHttp(final Repository local, final URIish uri)
 			throws NotSupportedException {
 		super(local, uri);
@@ -171,6 +173,20 @@ private static class HttpConfig {
 		proxySelector = ProxySelector.getDefault();
 	}
 
+	/**
+	 * Toggle whether or not smart HTTP transport should be used.
+	 * <p>
+	 * This flag exists primarily to support backwards compatibility testing
+	 * within a testing framework, there is no need to modify it in most
+	 * applications.
+	 *
+	 * @param on
+	 *            if {@code true} (default), smart HTTP is enabled.
+	 */
+	public void setUseSmartHttp(final boolean on) {
+		useSmartHttp = on;
+	}
+
 	@Override
 	public FetchConnection openFetch() throws TransportException,
 			NotSupportedException {
@@ -271,6 +287,10 @@ public PushConnection openPush() throws NotSupportedException,
 					readSmartHeaders(in, service);
 					return new SmartHttpPushConnection(in);
 
+				} else if (!useSmartHttp) {
+					final String msg = "smart HTTP push disabled";
+					throw new NotSupportedException(msg);
+
 				} else {
 					final String msg = "remote does not support smart HTTP push";
 					throw new NotSupportedException(msg);
@@ -303,9 +323,11 @@ private HttpURLConnection connect(final String service)
 				b.append('/');
 			b.append(Constants.INFO_REFS);
 
-			b.append(b.indexOf("?") < 0 ? '?' : '&');
-			b.append("service=");
-			b.append(service);
+			if (useSmartHttp) {
+				b.append(b.indexOf("?") < 0 ? '?' : '&');
+				b.append("service=");
+				b.append(service);
+			}
 
 			u = new URL(b.toString());
 		} catch (MalformedURLException e) {
@@ -314,8 +336,12 @@ private HttpURLConnection connect(final String service)
 
 		try {
 			final HttpURLConnection conn = httpOpen(u);
-			String expType = "application/x-" + service + "-advertisement";
-			conn.setRequestProperty(HDR_ACCEPT, expType + ", */*");
+			if (useSmartHttp) {
+				String expType = "application/x-" + service + "-advertisement";
+				conn.setRequestProperty(HDR_ACCEPT, expType + ", */*");
+			} else {
+				conn.setRequestProperty(HDR_ACCEPT, "*/*");
+			}
 			final int status = HttpSupport.response(conn);
 			switch (status) {
 			case HttpURLConnection.HTTP_OK:
diff --git a/pom.xml b/pom.xml
index d3467204f4c794ae5fbe95a8d3524d45bdd9bf1d..d02cafbbe9a8a2709da86e1dd89f4372335d28ac 100644
--- a/pom.xml
+++ b/pom.xml
@@ -138,6 +138,8 @@
 
     <servlet-api-CQ>CQ 3565</servlet-api-CQ>
     <servlet-api-version>2.5</servlet-api-version>
+
+    <jetty-version>7.0.1.v20091125</jetty-version>
   </properties>
 
   <build>
@@ -265,6 +267,12 @@
         <artifactId>servlet-api</artifactId>
         <version>${servlet-api-version}</version>
       </dependency>
+
+      <dependency>
+        <groupId>org.eclipse.jetty</groupId>
+        <artifactId>jetty-servlet</artifactId>
+        <version>${jetty-version}</version>
+      </dependency>
     </dependencies>
   </dependencyManagement>
 
@@ -298,6 +306,8 @@
     <module>org.eclipse.jgit.http.server</module>
     <module>org.eclipse.jgit.pgm</module>
     <module>org.eclipse.jgit.junit</module>
+
     <module>org.eclipse.jgit.test</module>
+    <module>org.eclipse.jgit.http.test</module>
   </modules>
 </project>