Add support for +archive

Change-Id: I765d492a59592908c2d91729470b0e78b39c564b
diff --git a/gitiles/client.go b/gitiles/client.go
index 9cb6d0f..de60c5c 100644
--- a/gitiles/client.go
+++ b/gitiles/client.go
@@ -21,6 +21,7 @@
 	"encoding/json"
 	"flag"
 	"fmt"
+	"io"
 	"io/ioutil"
 	"log"
 	"net/http"
@@ -124,7 +125,7 @@
 	return s, nil
 }
 
-func (s *Service) get(u *url.URL) ([]byte, error) {
+func (s *Service) stream(u *url.URL) (*http.Response, error) {
 	ctx := context.Background()
 
 	if err := s.limiter.Wait(ctx); err != nil {
@@ -140,8 +141,9 @@
 	if err != nil {
 		return nil, err
 	}
-	defer resp.Body.Close()
+
 	if resp.StatusCode != 200 {
+		resp.Body.Close()
 		return nil, fmt.Errorf("%s: %s", u.String(), resp.Status)
 	}
 
@@ -149,12 +151,24 @@
 		log.Printf("%s %s: %d", req.Method, req.URL, resp.StatusCode)
 	}
 	if got := resp.Request.URL.String(); got != u.String() {
+		resp.Body.Close()
 		// We accept redirects, but only for authentication.
 		// If we get a 200 from a different page than we
 		// requested, it's probably some sort of login page.
 		return nil, fmt.Errorf("got URL %s, want %s", got, u.String())
 	}
 
+	return resp, nil
+}
+
+func (s *Service) get(u *url.URL) ([]byte, error) {
+	resp, err := s.stream(u)
+	if err != nil {
+		return nil, err
+	}
+
+	defer resp.Body.Close()
+
 	c, err := ioutil.ReadAll(resp.Body)
 	if err != nil {
 		return nil, err
@@ -245,6 +259,38 @@
 	return s.service.get(&blobURL)
 }
 
+// Archive formats for +archive. JGit also supports some shorthands.
+const (
+	ArchiveTbz = "tar.bz2"
+	ArchiveTgz = "tar.gz"
+	ArchiveTar = "tar"
+	ArchiveTxz = "tar.xz"
+
+	// the Gitiles source code claims .tar.xz and .tar are
+	// supported, but googlesource.com doesn't support it,
+	// apparently. In addition, JGit provides ZipFormat, but
+	// gitiles doesn't support it.
+)
+
+// GetArchive downloads an archive of the project. Format is one
+// ArchivXxx formats. dirPrefix, if given, restricts to the given
+// subpath, and strips the path prefix from the files in the resulting
+// tar archive. revision is a git revision, either a branch/tag name
+// ("master") or a hex commit SHA1.
+func (s *RepoService) GetArchive(revision, dirPrefix, format string) (io.ReadCloser, error) {
+	u := s.service.addr
+	u.Path = path.Join(u.Path, s.Name, "+archive", revision)
+	if dirPrefix != "" {
+		u.Path = path.Join(u.Path, dirPrefix)
+	}
+	u.Path += "." + format
+	resp, err := s.service.stream(&u)
+	if err != nil {
+		return nil, err
+	}
+	return resp.Body, err
+}
+
 // GetTree fetches a tree. The dir argument may not point to a
 // blob. If recursive is given, the server recursively expands the
 // tree.
diff --git a/gitiles/prod_test.go b/gitiles/prod_test.go
new file mode 100644
index 0000000..8ee9f87
--- /dev/null
+++ b/gitiles/prod_test.go
@@ -0,0 +1,61 @@
+// Copyright 2017 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package gitiles
+
+import (
+	"archive/tar"
+	"compress/gzip"
+	"io"
+	"testing"
+)
+
+func TestProductionArchive(t *testing.T) {
+	gs, err := NewService(Options{
+		Address: "https://go.googlesource.com",
+	})
+	if err != nil {
+		t.Fatalf("NewService: %v", err)
+	}
+
+	repo := gs.NewRepoService("crypto")
+	if err != nil {
+		t.Fatalf("NewRepoService: %v", err)
+	}
+
+	stream, err := repo.GetArchive("master", "ssh", ArchiveTgz)
+	if err != nil {
+		t.Fatalf("GetArchive: %v", err)
+	}
+	defer stream.Close()
+
+	gz, err := gzip.NewReader(stream)
+	if err != nil {
+		t.Fatalf("gzip.NewReader: %v", err)
+	}
+	r := tar.NewReader(gz)
+
+	names := map[string]bool{}
+	for {
+		hdr, err := r.Next()
+		if err == io.EOF {
+			break
+		}
+		names[hdr.Name] = true
+	}
+
+	if !names["mux.go"] {
+		t.Fatal("did not find 'mux.go', got %v", names)
+	}
+}