diff --git a/README.md b/README.md
index 29e7394dd017c18c3254206f441c84da0bfb5991..743870dbe3e6e277df736a45c2a03730eafbe0be 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,9 @@
 # gitlab-git-http-server
 
 gitlab-git-http-server was designed to unload Git HTTP traffic from
-the GitLab Rails app (Unicorn) to a separate daemon. All authentication
-and authorization logic is still handled by the GitLab Rails app.
+the GitLab Rails app (Unicorn) to a separate daemon.  It also serves
+'git archive' downloads for GitLab.  All authentication and
+authorization logic is still handled by the GitLab Rails app.
 
 Architecture: Git client -> NGINX -> gitlab-git-http-server (makes
 auth request to GitLab Rails app) -> git-upload-pack
@@ -10,7 +11,7 @@ auth request to GitLab Rails app) -> git-upload-pack
 ## Usage
 
 ```
-  gitlab-git-http-server [OPTIONS] REPO_ROOT
+  gitlab-git-http-server [OPTIONS]
 
 Options:
   -authBackend string
@@ -27,11 +28,13 @@ Options:
     	Print version and exit
 ```
 
-gitlab-git-http-server allows Git HTTP clients to push and pull to and from Git
-repositories under REPO_ROOT. Each incoming request is first replayed (with an
-empty request body) to an external authentication/authorization HTTP server:
-the 'auth backend'. The auth backend is expected to be a GitLab Unicorn
-process.
+gitlab-git-http-server allows Git HTTP clients to push and pull to
+and from Git repositories. Each incoming request is first replayed
+(with an empty request body) to an external authentication/authorization
+HTTP server: the 'auth backend'. The auth backend is expected to
+be a GitLab Unicorn process.  The 'auth response' is a JSON message
+which tells gitlab-git-http-server the path of the Git repository
+to read from/write to.
 
 gitlab-git-http-server can listen on either a TCP or a Unix domain socket. It
 can also open a second listening TCP listening socket with the Go
@@ -63,14 +66,19 @@ You can try out the Git server without authentication as follows:
 
 ```
 # Start a fake auth backend that allows everything/everybody
-go run support/say-yes.go &
+make test/data/test.git
+go run support/fake-auth-backend.go ~+/test/data/test.git &
 # Start gitlab-git-http-server
-go build && ./gitlab-git-http-server /path/to/git-repos
+make
+./gitlab-git-http-server
 ```
 
-Now if you have a Git repository in `/path/to/git-repos/my-repo.git`,
-you can push to and pull from it at the URL
-`http://localhost:8181/my-repo.git`.
+Now you can try things like:
+
+```
+git clone http://localhost:8181/test.git
+curl -JO http://localhost:8181/test/repository/archive.zip
+```
 
 ## Example request flow
 
diff --git a/githandler.go b/githandler.go
index 3af8e3d268751332555762bee1dda466f3ffbb72..e5529062d82f2fbdeb72880abaf6c4dfb7493c3f 100644
--- a/githandler.go
+++ b/githandler.go
@@ -11,6 +11,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
+	"io/ioutil"
 	"log"
 	"net/http"
 	"os"
@@ -18,23 +19,40 @@ import (
 	"path"
 	"strings"
 	"syscall"
+	"time"
 )
 
 type gitHandler struct {
 	httpClient  *http.Client
-	repoRoot    string
 	authBackend string
 }
 
 type gitService struct {
 	method     string
 	suffix     string
-	handleFunc func(gitEnv, string, string, http.ResponseWriter, *http.Request)
+	handleFunc func(w http.ResponseWriter, r *gitRequest, rpc string)
 	rpc        string
 }
 
-type gitEnv struct {
+// A gitReqest is an *http.Request decorated with attributes returned by the
+// GitLab Rails application.
+type gitRequest struct {
+	*http.Request
+	// GL_ID is an environment variable used by gitlab-shell hooks during 'git
+	// push' and 'git pull'
 	GL_ID string
+	// RepoPath is the full path on disk to the Git repository the request is
+	// about
+	RepoPath string
+	// ArchivePath is the full path where we should find/create a cached copy
+	// of a requested archive
+	ArchivePath string
+	// ArchivePrefix is used to put extracted archive contents in a
+	// subdirectory
+	ArchivePrefix string
+	// CommitId is used do prevent race conditions between the 'time of check'
+	// in the GitLab Rails app and the 'time of use' in gitlab-git-http-server.
+	CommitId string
 }
 
 // Routing table
@@ -42,14 +60,18 @@ var gitServices = [...]gitService{
 	gitService{"GET", "/info/refs", handleGetInfoRefs, ""},
 	gitService{"POST", "/git-upload-pack", handlePostRPC, "git-upload-pack"},
 	gitService{"POST", "/git-receive-pack", handlePostRPC, "git-receive-pack"},
+	gitService{"GET", "/repository/archive", handleGetArchive, "tar.gz"},
+	gitService{"GET", "/repository/archive.zip", handleGetArchive, "zip"},
+	gitService{"GET", "/repository/archive.tar", handleGetArchive, "tar"},
+	gitService{"GET", "/repository/archive.tar.gz", handleGetArchive, "tar.gz"},
+	gitService{"GET", "/repository/archive.tar.bz2", handleGetArchive, "tar.bz2"},
 }
 
-func newGitHandler(repoRoot, authBackend string) *gitHandler {
-	return &gitHandler{&http.Client{}, repoRoot, authBackend}
+func newGitHandler(authBackend string) *gitHandler {
+	return &gitHandler{&http.Client{}, authBackend}
 }
 
 func (h *gitHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	var env gitEnv
 	var g gitService
 
 	log.Printf("%s %q", r.Method, r.URL)
@@ -92,11 +114,11 @@ func (h *gitHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	// The auth backend validated the client request and told us who
-	// the user is according to them (GL_ID). We must extract this
-	// information from the auth response body.
-	dec := json.NewDecoder(authResponse.Body)
-	if err := dec.Decode(&env); err != nil {
+	// The auth backend validated the client request and told us additional
+	// request metadata. We must extract this information from the auth
+	// response body.
+	gitReq := &gitRequest{Request: r}
+	if err := json.NewDecoder(authResponse.Body).Decode(gitReq); err != nil {
 		fail500(w, "decode JSON GL_ID", err)
 		return
 	}
@@ -106,26 +128,18 @@ func (h *gitHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	// Negotiate authentication (Kerberos) may need to return a WWW-Authenticate
 	// header to the client even in case of success as per RFC4559.
 	for k, v := range authResponse.Header {
-                // Case-insensitive comparison as per RFC7230
+		// Case-insensitive comparison as per RFC7230
 		if strings.EqualFold(k, "WWW-Authenticate") {
 			w.Header()[k] = v
 		}
 	}
 
-	// About path traversal: the Go net/http HTTP server, or
-	// rather ServeMux, makes the following promise: "ServeMux
-	// also takes care of sanitizing the URL request path, redirecting
-	// any request containing . or .. elements to an equivalent
-	// .- and ..-free URL.". In other words, we may assume that
-	// r.URL.Path does not contain '/../', so there is no possibility
-	// of path traversal here.
-	repoPath := path.Join(h.repoRoot, strings.TrimSuffix(r.URL.Path, g.suffix))
-	if !looksLikeRepo(repoPath) {
+	if !looksLikeRepo(gitReq.RepoPath) {
 		http.Error(w, "Not Found", 404)
 		return
 	}
 
-	g.handleFunc(env, g.rpc, repoPath, w, r)
+	g.handleFunc(w, gitReq, g.rpc)
 }
 
 func looksLikeRepo(p string) bool {
@@ -159,7 +173,7 @@ func (h *gitHandler) doAuthRequest(r *http.Request) (result *http.Response, err
 	return h.httpClient.Do(authReq)
 }
 
-func handleGetInfoRefs(env gitEnv, _ string, path string, w http.ResponseWriter, r *http.Request) {
+func handleGetInfoRefs(w http.ResponseWriter, r *gitRequest, _ string) {
 	rpc := r.URL.Query().Get("service")
 	if !(rpc == "git-upload-pack" || rpc == "git-receive-pack") {
 		// The 'dumb' Git HTTP protocol is not supported
@@ -168,7 +182,7 @@ func handleGetInfoRefs(env gitEnv, _ string, path string, w http.ResponseWriter,
 	}
 
 	// Prepare our Git subprocess
-	cmd := gitCommand(env, "git", subCommand(rpc), "--stateless-rpc", "--advertise-refs", path)
+	cmd := gitCommand(r.GL_ID, "git", subCommand(rpc), "--stateless-rpc", "--advertise-refs", r.RepoPath)
 	stdout, err := cmd.StdoutPipe()
 	if err != nil {
 		fail500(w, "handleGetInfoRefs", err)
@@ -203,7 +217,136 @@ func handleGetInfoRefs(env gitEnv, _ string, path string, w http.ResponseWriter,
 	}
 }
 
-func handlePostRPC(env gitEnv, rpc string, path string, w http.ResponseWriter, r *http.Request) {
+func handleGetArchive(w http.ResponseWriter, r *gitRequest, format string) {
+	archiveFilename := path.Base(r.ArchivePath)
+
+	if cachedArchive, err := os.Open(r.ArchivePath); err == nil {
+		defer cachedArchive.Close()
+		log.Printf("Serving cached file %q", r.ArchivePath)
+		setArchiveHeaders(w, format, archiveFilename)
+		// Even if somebody deleted the cachedArchive from disk since we opened
+		// the file, Unix file semantics guarantee we can still read from the
+		// open file in this process.
+		http.ServeContent(w, r.Request, "", time.Unix(0, 0), cachedArchive)
+		return
+	}
+
+	// We assume the tempFile has a unique name so that concurrent requests are
+	// safe. We create the tempfile in the same directory as the final cached
+	// archive we want to create so that we can use an atomic link(2) operation
+	// to finalize the cached archive.
+	tempFile, err := prepareArchiveTempfile(path.Dir(r.ArchivePath),
+		archiveFilename)
+	if err != nil {
+		fail500(w, "handleGetArchive create tempfile for archive", err)
+	}
+	defer tempFile.Close()
+	defer os.Remove(tempFile.Name())
+
+	compressCmd, archiveFormat := parseArchiveFormat(format)
+
+	archiveCmd := gitCommand("", "git", "--git-dir="+r.RepoPath, "archive", "--format="+archiveFormat, "--prefix="+r.ArchivePrefix+"/", r.CommitId)
+	archiveStdout, err := archiveCmd.StdoutPipe()
+	if err != nil {
+		fail500(w, "handleGetArchive", err)
+		return
+	}
+	defer archiveStdout.Close()
+	if err := archiveCmd.Start(); err != nil {
+		fail500(w, "handleGetArchive", err)
+		return
+	}
+	defer cleanUpProcessGroup(archiveCmd) // Ensure brute force subprocess clean-up
+
+	var stdout io.ReadCloser
+	if compressCmd == nil {
+		stdout = archiveStdout
+	} else {
+		compressCmd.Stdin = archiveStdout
+
+		stdout, err = compressCmd.StdoutPipe()
+		if err != nil {
+			fail500(w, "handleGetArchive compressCmd stdout pipe", err)
+			return
+		}
+		defer stdout.Close()
+
+		if err := compressCmd.Start(); err != nil {
+			fail500(w, "handleGetArchive start compressCmd process", err)
+			return
+		}
+		defer compressCmd.Wait()
+
+		archiveStdout.Close()
+	}
+	// Every Read() from stdout will be synchronously written to tempFile
+	// before it comes out the TeeReader.
+	archiveReader := io.TeeReader(stdout, tempFile)
+
+	// Start writing the response
+	setArchiveHeaders(w, format, archiveFilename)
+	w.WriteHeader(200) // Don't bother with HTTP 500 from this point on, just return
+	if _, err := io.Copy(w, archiveReader); err != nil {
+		logContext("handleGetArchive read from subprocess", err)
+		return
+	}
+	if err := archiveCmd.Wait(); err != nil {
+		logContext("handleGetArchive wait for archiveCmd", err)
+		return
+	}
+	if compressCmd != nil {
+		if err := compressCmd.Wait(); err != nil {
+			logContext("handleGetArchive wait for compressCmd", err)
+			return
+		}
+	}
+
+	if err := finalizeCachedArchive(tempFile, r.ArchivePath); err != nil {
+		logContext("handleGetArchive finalize cached archive", err)
+		return
+	}
+}
+
+func setArchiveHeaders(w http.ResponseWriter, format string, archiveFilename string) {
+	w.Header().Add("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, archiveFilename))
+	if format == "zip" {
+		w.Header().Add("Content-Type", "application/zip")
+	} else {
+		w.Header().Add("Content-Type", "application/octet-stream")
+	}
+	w.Header().Add("Content-Transfer-Encoding", "binary")
+	w.Header().Add("Cache-Control", "private")
+}
+
+func parseArchiveFormat(format string) (*exec.Cmd, string) {
+	switch format {
+	case "tar":
+		return nil, "tar"
+	case "tar.gz":
+		return exec.Command("gzip", "-c", "-n"), "tar"
+	case "tar.bz2":
+		return exec.Command("bzip2", "-c"), "tar"
+	case "zip":
+		return nil, "zip"
+	}
+	return nil, "unknown"
+}
+
+func prepareArchiveTempfile(dir string, prefix string) (*os.File, error) {
+	if err := os.MkdirAll(dir, 0700); err != nil {
+		return nil, err
+	}
+	return ioutil.TempFile(dir, prefix)
+}
+
+func finalizeCachedArchive(tempFile *os.File, archivePath string) error {
+	if err := tempFile.Close(); err != nil {
+		return err
+	}
+	return os.Link(tempFile.Name(), archivePath)
+}
+
+func handlePostRPC(w http.ResponseWriter, r *gitRequest, rpc string) {
 	var body io.ReadCloser
 	var err error
 
@@ -220,7 +363,7 @@ func handlePostRPC(env gitEnv, rpc string, path string, w http.ResponseWriter, r
 	defer body.Close()
 
 	// Prepare our Git subprocess
-	cmd := gitCommand(env, "git", subCommand(rpc), "--stateless-rpc", path)
+	cmd := gitCommand(r.GL_ID, "git", subCommand(rpc), "--stateless-rpc", r.RepoPath)
 	stdout, err := cmd.StdoutPipe()
 	if err != nil {
 		fail500(w, "handlePostRPC", err)
@@ -284,14 +427,14 @@ func subCommand(rpc string) string {
 	return strings.TrimPrefix(rpc, "git-")
 }
 
-func gitCommand(env gitEnv, name string, args ...string) *exec.Cmd {
+func gitCommand(gl_id string, name string, args ...string) *exec.Cmd {
 	cmd := exec.Command(name, args...)
 	// Start the command in its own process group (nice for signalling)
 	cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
 	// Explicitly set the environment for the Git command
 	cmd.Env = []string{
 		fmt.Sprintf("PATH=%s", os.Getenv("PATH")),
-		fmt.Sprintf("GL_ID=%s", env.GL_ID),
+		fmt.Sprintf("GL_ID=%s", gl_id),
 	}
 	// If we don't do something with cmd.Stderr, Git errors will be lost
 	cmd.Stderr = os.Stderr
diff --git a/main.go b/main.go
index 674707c8001cd8eade41ce0190aecddf08da5812..a7dfdd8eac28453ec78f58796a7583218cb84d12 100644
--- a/main.go
+++ b/main.go
@@ -36,22 +36,18 @@ func main() {
 	pprofListenAddr := flag.String("pprofListenAddr", "", "pprof listening address, e.g. 'localhost:6060'")
 	flag.Usage = func() {
 		fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
-		fmt.Fprintf(os.Stderr, "\n  %s [OPTIONS] REPO_ROOT\n\nOptions:\n", os.Args[0])
+		fmt.Fprintf(os.Stderr, "\n  %s [OPTIONS]\n\nOptions:\n", os.Args[0])
 		flag.PrintDefaults()
 	}
 	flag.Parse()
 
+	version := fmt.Sprintf("gitlab-git-http-server %s", Version)
 	if *printVersion {
-		fmt.Printf("gitlab-git-http-server %s\n", Version)
+		fmt.Println(version)
 		os.Exit(0)
 	}
 
-	repoRoot := flag.Arg(0)
-	if repoRoot == "" {
-		flag.Usage()
-		os.Exit(1)
-	}
-	log.Printf("repoRoot: %s", repoRoot)
+	log.Printf("Starting %s", version)
 
 	// Good housekeeping for Unix sockets: unlink before binding
 	if *listenNetwork == "unix" {
@@ -81,6 +77,6 @@ func main() {
 	// Because net/http/pprof installs itself in the DefaultServeMux
 	// we create a fresh one for the Git server.
 	serveMux := http.NewServeMux()
-	serveMux.Handle("/", newGitHandler(repoRoot, *authBackend))
+	serveMux.Handle("/", newGitHandler(*authBackend))
 	log.Fatal(http.Serve(listener, serveMux))
 }
diff --git a/main_test.go b/main_test.go
index 4e64671b8514190c20cf91c42a97fd752e025abb..029b9429dd39bb4eb71fa966dd1ac70b3430532d 100644
--- a/main_test.go
+++ b/main_test.go
@@ -1,7 +1,9 @@
 package main
 
 import (
+	"bytes"
 	"fmt"
+	"io/ioutil"
 	"net"
 	"net/http"
 	"net/http/httptest"
@@ -19,9 +21,11 @@ const servWaitSleep = 100    // milliseconds sleep interval
 const scratchDir = "test/scratch"
 const testRepoRoot = "test/data"
 const testRepo = "test.git"
+const testProject = "test"
 
 var remote = fmt.Sprintf("http://%s/%s", servAddr, testRepo)
 var checkoutDir = path.Join(scratchDir, "test")
+var cacheDir = path.Join(scratchDir, "cache")
 
 func TestAllowedClone(t *testing.T) {
 	// Prepare clone directory
@@ -30,7 +34,7 @@ func TestAllowedClone(t *testing.T) {
 	}
 
 	// Prepare test server and backend
-	ts := testAuthServer(200, `{"GL_ID":"user-123"}`)
+	ts := testAuthServer(200, gitOkBody(t))
 	defer ts.Close()
 	defer cleanUpProcessGroup(startServerOrFail(t, ts))
 
@@ -68,7 +72,7 @@ func TestAllowedPush(t *testing.T) {
 	preparePushRepo(t)
 
 	// Prepare the test server and backend
-	ts := testAuthServer(200, `{"GL_ID":"user-123"}`)
+	ts := testAuthServer(200, gitOkBody(t))
 	defer ts.Close()
 	defer cleanUpProcessGroup(startServerOrFail(t, ts))
 
@@ -96,6 +100,154 @@ func TestDeniedPush(t *testing.T) {
 	}
 }
 
+func TestAllowedDownloadZip(t *testing.T) {
+	prepareDownloadDir(t)
+
+	// Prepare test server and backend
+	archiveName := "foobar.zip"
+	ts := testAuthServer(200, archiveOkBody(t, archiveName))
+	defer ts.Close()
+	defer cleanUpProcessGroup(startServerOrFail(t, ts))
+
+	downloadCmd := exec.Command("curl", "-J", "-O", fmt.Sprintf("http://%s/%s/repository/archive.zip", servAddr, testProject))
+	downloadCmd.Dir = scratchDir
+	runOrFail(t, downloadCmd)
+
+	extractCmd := exec.Command("unzip", archiveName)
+	extractCmd.Dir = scratchDir
+	runOrFail(t, extractCmd)
+}
+
+func TestAllowedDownloadTar(t *testing.T) {
+	prepareDownloadDir(t)
+
+	// Prepare test server and backend
+	archiveName := "foobar.tar"
+	ts := testAuthServer(200, archiveOkBody(t, archiveName))
+	defer ts.Close()
+	defer cleanUpProcessGroup(startServerOrFail(t, ts))
+
+	downloadCmd := exec.Command("curl", "-J", "-O", fmt.Sprintf("http://%s/%s/repository/archive.tar", servAddr, testProject))
+	downloadCmd.Dir = scratchDir
+	runOrFail(t, downloadCmd)
+
+	extractCmd := exec.Command("tar", "xf", archiveName)
+	extractCmd.Dir = scratchDir
+	runOrFail(t, extractCmd)
+}
+
+func TestAllowedDownloadTarGz(t *testing.T) {
+	prepareDownloadDir(t)
+
+	// Prepare test server and backend
+	archiveName := "foobar.tar.gz"
+	ts := testAuthServer(200, archiveOkBody(t, archiveName))
+	defer ts.Close()
+	defer cleanUpProcessGroup(startServerOrFail(t, ts))
+
+	downloadCmd := exec.Command("curl", "-J", "-O", fmt.Sprintf("http://%s/%s/repository/archive.tar.gz", servAddr, testProject))
+	downloadCmd.Dir = scratchDir
+	runOrFail(t, downloadCmd)
+
+	extractCmd := exec.Command("tar", "zxf", archiveName)
+	extractCmd.Dir = scratchDir
+	runOrFail(t, extractCmd)
+}
+
+func TestAllowedDownloadTarBz2(t *testing.T) {
+	prepareDownloadDir(t)
+
+	// Prepare test server and backend
+	archiveName := "foobar.tar.bz2"
+	ts := testAuthServer(200, archiveOkBody(t, archiveName))
+	defer ts.Close()
+	defer cleanUpProcessGroup(startServerOrFail(t, ts))
+
+	downloadCmd := exec.Command("curl", "-J", "-O", fmt.Sprintf("http://%s/%s/repository/archive.tar.bz2", servAddr, testProject))
+	downloadCmd.Dir = scratchDir
+	runOrFail(t, downloadCmd)
+
+	extractCmd := exec.Command("tar", "jxf", archiveName)
+	extractCmd.Dir = scratchDir
+	runOrFail(t, extractCmd)
+}
+
+func TestAllowedApiDownloadZip(t *testing.T) {
+	prepareDownloadDir(t)
+
+	// Prepare test server and backend
+	archiveName := "foobar.zip"
+	ts := testAuthServer(200, archiveOkBody(t, archiveName))
+	defer ts.Close()
+	defer cleanUpProcessGroup(startServerOrFail(t, ts))
+
+	downloadCmd := exec.Command("curl", "-J", "-O", fmt.Sprintf("http://%s/api/v3/projects/123/repository/archive.zip", servAddr))
+	downloadCmd.Dir = scratchDir
+	runOrFail(t, downloadCmd)
+
+	extractCmd := exec.Command("unzip", archiveName)
+	extractCmd.Dir = scratchDir
+	runOrFail(t, extractCmd)
+}
+
+func TestDownloadCacheHit(t *testing.T) {
+	prepareDownloadDir(t)
+
+	// Prepare test server and backend
+	archiveName := "foobar.zip"
+	ts := testAuthServer(200, archiveOkBody(t, archiveName))
+	defer ts.Close()
+	defer cleanUpProcessGroup(startServerOrFail(t, ts))
+
+	if err := os.MkdirAll(cacheDir, 0755); err != nil {
+		t.Fatal(err)
+	}
+	cachedContent := []byte{'c', 'a', 'c', 'h', 'e', 'd'}
+	if err := ioutil.WriteFile(path.Join(cacheDir, archiveName), cachedContent, 0644); err != nil {
+		t.Fatal(err)
+	}
+
+	downloadCmd := exec.Command("curl", "-J", "-O", fmt.Sprintf("http://%s/api/v3/projects/123/repository/archive.zip", servAddr))
+	downloadCmd.Dir = scratchDir
+	runOrFail(t, downloadCmd)
+
+	actual, err := ioutil.ReadFile(path.Join(scratchDir, archiveName))
+	if err != nil {
+		t.Fatal(err)
+	}
+	if bytes.Compare(actual, cachedContent) != 0 {
+		t.Fatal("Unexpected file contents in download")
+	}
+}
+
+func TestDownloadCacheCreate(t *testing.T) {
+	prepareDownloadDir(t)
+
+	// Prepare test server and backend
+	archiveName := "foobar.zip"
+	ts := testAuthServer(200, archiveOkBody(t, archiveName))
+	defer ts.Close()
+	defer cleanUpProcessGroup(startServerOrFail(t, ts))
+
+	downloadCmd := exec.Command("curl", "-J", "-O", fmt.Sprintf("http://%s/api/v3/projects/123/repository/archive.zip", servAddr))
+	downloadCmd.Dir = scratchDir
+	runOrFail(t, downloadCmd)
+
+	compareCmd := exec.Command("cmp", path.Join(cacheDir, archiveName), path.Join(scratchDir, archiveName))
+	if err := compareCmd.Run(); err != nil {
+		t.Fatalf("Comparison between downloaded file and cache item failed: %s", err)
+	}
+}
+
+func prepareDownloadDir(t *testing.T) {
+	if err := os.RemoveAll(scratchDir); err != nil {
+		t.Fatal(err)
+	}
+	if err := os.MkdirAll(scratchDir, 0755); err != nil {
+		t.Fatal(err)
+	}
+}
+
 func preparePushRepo(t *testing.T) {
 	if err := os.RemoveAll(scratchDir); err != nil {
 		t.Fatal(err)
@@ -117,7 +269,7 @@ func testAuthServer(code int, body string) *httptest.Server {
 }
 
 func startServerOrFail(t *testing.T, ts *httptest.Server) *exec.Cmd {
-	cmd := exec.Command("go", "run", "main.go", "githandler.go", fmt.Sprintf("-authBackend=%s", ts.URL), fmt.Sprintf("-listenAddr=%s", servAddr), testRepoRoot)
+	cmd := exec.Command("go", "run", "main.go", "githandler.go", fmt.Sprintf("-authBackend=%s", ts.URL), fmt.Sprintf("-listenAddr=%s", servAddr))
 	cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
 	cmd.Stdout = os.Stdout
 	cmd.Stderr = os.Stderr
@@ -154,3 +306,30 @@ func runOrFail(t *testing.T, cmd *exec.Cmd) {
 		t.Fatal(err)
 	}
 }
+
+func gitOkBody(t *testing.T) string {
+	return fmt.Sprintf(`{"GL_ID":"user-123","RepoPath":"%s"}`, repoPath(t))
+}
+
+func archiveOkBody(t *testing.T, archiveName string) string {
+	cwd, err := os.Getwd()
+	if err != nil {
+		t.Fatal(err)
+	}
+	archivePath := path.Join(cwd, cacheDir, archiveName)
+	jsonString := `{
+		"RepoPath":"%s",
+		"ArchivePath":"%s",
+		"CommitId":"c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd",
+		"ArchivePrefix":"foobar123"
+	}`
+	return fmt.Sprintf(jsonString, repoPath(t), archivePath)
+}
+
+func repoPath(t *testing.T) string {
+	cwd, err := os.Getwd()
+	if err != nil {
+		t.Fatal(err)
+	}
+	return path.Join(cwd, testRepoRoot, testRepo)
+}
diff --git a/support/fake-auth-backend.go b/support/fake-auth-backend.go
new file mode 100644
index 0000000000000000000000000000000000000000..1780208ddb55d0fd3097f816a3e04a4255ad414f
--- /dev/null
+++ b/support/fake-auth-backend.go
@@ -0,0 +1,21 @@
+package main
+
+import (
+	"fmt"
+	"log"
+	"net/http"
+	"os"
+)
+
+func main() {
+	if len(os.Args) == 1 {
+		fmt.Fprintf(os.Stderr, "Usage: %s /path/to/test-repo.git\n", os.Args[0])
+		os.Exit(1)
+	}
+
+	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		fmt.Fprintf(w, `{"RepoPath":"%s","ArchivePath":"%s"}`, os.Args[1], r.URL.Path)
+	})
+
+	log.Fatal(http.ListenAndServe("localhost:8080", nil))
+}
diff --git a/support/say-yes.go b/support/say-yes.go
deleted file mode 100644
index 6e6ec2e1ab19e8d7d157bc47632cab85a6846a55..0000000000000000000000000000000000000000
--- a/support/say-yes.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package main
-
-import (
-	"fmt"
-	"log"
-	"net/http"
-)
-
-func main() {
-	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
-		fmt.Fprint(w, `{"GL_ID":""}`)
-	})
-
-	log.Fatal(http.ListenAndServe("localhost:8080", nil))
-}