all: propagate context throughout the codebase.

Change-Id: Icdb6f5911e49e121014077067ac8c9a83b0ea8cd
Reviewed-on: https://go-review.googlesource.com/66112
Reviewed-by: Ross Light <light@google.com>
diff --git a/database/database.go b/database/database.go
index 0eea7c8..4f112fb 100644
--- a/database/database.go
+++ b/database/database.go
@@ -31,6 +31,7 @@
 
 import (
 	"bytes"
+	"context"
 	"encoding/gob"
 	"errors"
 	"fmt"
@@ -47,7 +48,6 @@
 
 	"github.com/garyburd/redigo/redis"
 	"github.com/golang/snappy"
-	"golang.org/x/net/context"
 	"golang.org/x/oauth2/google"
 	"google.golang.org/appengine"
 	"google.golang.org/appengine/remote_api"
@@ -62,7 +62,7 @@
 		Get() redis.Conn
 	}
 
-	AppEngineContext context.Context
+	RemoteClient *remote_api.Client
 }
 
 // Package represents the content of a package both for the search index and
@@ -121,7 +121,7 @@
 	}
 }
 
-func newAppEngineContext(host string) (context.Context, error) {
+func newRemoteClient(host string) (*remote_api.Client, error) {
 	client, err := google.DefaultClient(context.TODO(),
 		"https://www.googleapis.com/auth/appengine.apis",
 	)
@@ -129,7 +129,7 @@
 		return nil, err
 	}
 
-	return remote_api.NewRemoteContext(host, client)
+	return remote_api.NewClient(host, client)
 }
 
 // New creates a gddo database. serverURI, idleTimeout, and logConn configure
@@ -148,15 +148,15 @@
 	}
 	c.Close()
 
-	gaeCtx := context.TODO()
+	var rc *remote_api.Client
 	if gaeEndpoint != "" {
 		var err error
-		if gaeCtx, err = newAppEngineContext(gaeEndpoint); err != nil {
+		if rc, err = newRemoteClient(gaeEndpoint); err != nil {
 			return nil, err
 		}
 	}
 
-	return &Database{Pool: pool, AppEngineContext: gaeCtx}, nil
+	return &Database{Pool: pool, RemoteClient: rc}, nil
 }
 
 // Exists returns true if package with import path exists in the database.
@@ -235,7 +235,7 @@
 }
 
 // Put adds the package documentation to the database.
-func (db *Database) Put(pdoc *doc.Package, nextCrawl time.Time, hide bool) error {
+func (db *Database) Put(ctx context.Context, pdoc *doc.Package, nextCrawl time.Time, hide bool) error {
 	c := db.Pool.Get()
 	defer c.Close()
 
@@ -284,7 +284,7 @@
 
 	// Get old version of the package to extract its imports.
 	// If the package does not exist, both oldDoc and err will be nil.
-	old, _, err := db.getDoc(c, pdoc.ImportPath)
+	old, _, err := db.getDoc(ctx, c, pdoc.ImportPath)
 	if err != nil {
 		return err
 	}
@@ -300,17 +300,17 @@
 	}
 
 	if score > 0 {
-		if err := db.PutIndex(db.AppEngineContext, pdoc, id, score, n); err != nil {
+		if err := db.PutIndex(ctx, pdoc, id, score, n); err != nil {
 			log.Printf("Cannot put %q in index: %v", pdoc.ImportPath, err)
 		}
 
 		if old != nil {
-			if err := db.updateImportsIndex(c, db.AppEngineContext, old, pdoc); err != nil {
+			if err := db.updateImportsIndex(ctx, c, old, pdoc); err != nil {
 				return err
 			}
 		}
 	} else {
-		if err := deleteIndex(db.AppEngineContext, id); err != nil {
+		if err := db.DeleteIndex(ctx, id); err != nil {
 			return err
 		}
 	}
@@ -364,7 +364,7 @@
 	return id, numImported, nil
 }
 
-func (db *Database) updateImportsIndex(c redis.Conn, ctx context.Context, oldDoc, newDoc *doc.Package) error {
+func (db *Database) updateImportsIndex(ctx context.Context, c redis.Conn, oldDoc, newDoc *doc.Package) error {
 	// Create a map to store any import change since last time we indexed the package.
 	changes := make(map[string]bool)
 	for _, p := range oldDoc.Imports {
@@ -387,7 +387,7 @@
 			return err
 		}
 		if id != "" {
-			db.PutIndex(db.AppEngineContext, nil, id, -1, n)
+			db.PutIndex(ctx, nil, id, -1, n)
 		}
 	}
 	return nil
@@ -481,7 +481,7 @@
     return {gob, nextCrawl}
 `)
 
-func (db *Database) getDoc(c redis.Conn, path string) (*doc.Package, time.Time, error) {
+func (db *Database) getDoc(ctx context.Context, c redis.Conn, path string) (*doc.Package, time.Time, error) {
 	r, err := redis.Values(getDocScript.Do(c, path))
 	if err == redis.ErrNil {
 		return nil, time.Time{}, nil
@@ -573,11 +573,11 @@
 
 // Get gets the package documentation and sub-directories for the the given
 // import path.
-func (db *Database) Get(path string) (*doc.Package, []Package, time.Time, error) {
+func (db *Database) Get(ctx context.Context, path string) (*doc.Package, []Package, time.Time, error) {
 	c := db.Pool.Get()
 	defer c.Close()
 
-	pdoc, nextCrawl, err := db.getDoc(c, path)
+	pdoc, nextCrawl, err := db.getDoc(ctx, c, path)
 	if err != nil {
 		return nil, nil, time.Time{}, err
 	}
@@ -594,10 +594,10 @@
 	return pdoc, subdirs, nextCrawl, nil
 }
 
-func (db *Database) GetDoc(path string) (*doc.Package, time.Time, error) {
+func (db *Database) GetDoc(ctx context.Context, path string) (*doc.Package, time.Time, error) {
 	c := db.Pool.Get()
 	defer c.Close()
-	return db.getDoc(c, path)
+	return db.getDoc(ctx, c, path)
 }
 
 var deleteScript = redis.NewScript(0, `
@@ -620,7 +620,7 @@
 `)
 
 // Delete deletes the documentation for the given import path.
-func (db *Database) Delete(path string) error {
+func (db *Database) Delete(ctx context.Context, path string) error {
 	c := db.Pool.Get()
 	defer c.Close()
 
@@ -631,7 +631,7 @@
 	if err != nil {
 		return err
 	}
-	if err := deleteIndex(db.AppEngineContext, id); err != nil {
+	if err := db.DeleteIndex(ctx, id); err != nil {
 		return err
 	}
 
@@ -1200,6 +1200,10 @@
 // This will update the search index with the path, synopsis, score, import counts
 // of all the packages in the database.
 func (db *Database) Reindex(ctx context.Context) error {
+	if db.RemoteClient == nil {
+		return errors.New("database.Reindex: no App Engine endpoint given")
+	}
+
 	c := db.Pool.Get()
 	defer c.Close()
 
@@ -1246,7 +1250,7 @@
 			if err != nil {
 				return err
 			}
-			if _, err := idx.Put(ctx, id, &Package{
+			if _, err := idx.Put(db.RemoteClient.NewContext(ctx), id, &Package{
 				Path:        pdoc.ImportPath,
 				Synopsis:    pdoc.Synopsis,
 				Score:       score,
@@ -1265,9 +1269,24 @@
 }
 
 func (db *Database) Search(ctx context.Context, q string) ([]Package, error) {
-	return searchAE(db.AppEngineContext, q)
+	if db.RemoteClient == nil {
+		return nil, errors.New("remote_api client not setup to use App Engine search")
+	}
+	return searchAE(db.RemoteClient.NewContext(ctx), q)
 }
 
+// PutIndex puts a package into App Engine search index. ID is the package ID in the database.
 func (db *Database) PutIndex(ctx context.Context, pdoc *doc.Package, id string, score float64, importCount int) error {
-	return putIndex(db.AppEngineContext, pdoc, id, score, importCount)
+	if db.RemoteClient == nil {
+		return errors.New("remote_api client not setup to use App Engine search")
+	}
+	return putIndex(db.RemoteClient.NewContext(ctx), pdoc, id, score, importCount)
+}
+
+// DeleteIndex deletes a package from App Engine search index. ID is the package ID in the database.
+func (db *Database) DeleteIndex(ctx context.Context, id string) error {
+	if db.RemoteClient == nil {
+		return errors.New("database.DeleteIndex: no App Engine endpoint given")
+	}
+	return deleteIndex(db.RemoteClient.NewContext(ctx), id)
 }
diff --git a/database/indexae.go b/database/indexae.go
index e86fa73..86b6169 100644
--- a/database/indexae.go
+++ b/database/indexae.go
@@ -8,6 +8,7 @@
 
 import (
 	"bytes"
+	"context"
 	"errors"
 	"fmt"
 	"log"
@@ -15,7 +16,6 @@
 	"strings"
 	"unicode"
 
-	"golang.org/x/net/context"
 	"google.golang.org/appengine/search"
 
 	"github.com/golang/gddo/doc"
@@ -181,6 +181,7 @@
 }
 
 // PurgeIndex deletes all the packages from the search index.
+// TODO(shantuo): wrap this with db and use db.RemoteClient to create the context.
 func PurgeIndex(c context.Context) error {
 	idx, err := search.Open("packages")
 	if err != nil {
diff --git a/doc/get.go b/doc/get.go
index 67a21a6..1028d3f 100644
--- a/doc/get.go
+++ b/doc/get.go
@@ -8,6 +8,7 @@
 package doc
 
 import (
+	"context"
 	"go/doc"
 	"net/http"
 	"strings"
@@ -15,7 +16,7 @@
 	"github.com/golang/gddo/gosrc"
 )
 
-func Get(client *http.Client, importPath string, etag string) (*Package, error) {
+func Get(ctx context.Context, client *http.Client, importPath string, etag string) (*Package, error) {
 
 	const versionPrefix = PackageVersion + "-"
 
@@ -25,7 +26,7 @@
 		etag = ""
 	}
 
-	dir, err := gosrc.Get(client, importPath, etag)
+	dir, err := gosrc.Get(ctx, client, importPath, etag)
 	if err != nil {
 		return nil, err
 	}
@@ -41,7 +42,7 @@
 		pdoc.Name != "" &&
 		dir.ImportPath == dir.ProjectRoot &&
 		len(pdoc.Errors) == 0 {
-		project, err := gosrc.GetProject(client, dir.ResolvedPath)
+		project, err := gosrc.GetProject(ctx, client, dir.ResolvedPath)
 		switch {
 		case err == nil:
 			pdoc.Synopsis = doc.Synopsis(project.Description)
diff --git a/gddo-admin/delete.go b/gddo-admin/delete.go
index 0ac2418..7a260f0 100644
--- a/gddo-admin/delete.go
+++ b/gddo-admin/delete.go
@@ -7,6 +7,7 @@
 package main
 
 import (
+	"context"
 	"log"
 	"os"
 
@@ -28,7 +29,7 @@
 	if err != nil {
 		log.Fatal(err)
 	}
-	if err := db.Delete(c.flag.Args()[0]); err != nil {
+	if err := db.Delete(context.Background(), c.flag.Args()[0]); err != nil {
 		log.Fatal(err)
 	}
 }
diff --git a/gddo-admin/reindex.go b/gddo-admin/reindex.go
index 4b3fb88..63d13fa 100644
--- a/gddo-admin/reindex.go
+++ b/gddo-admin/reindex.go
@@ -7,6 +7,7 @@
 package main
 
 import (
+	"context"
 	"log"
 	"os"
 	"time"
@@ -59,7 +60,7 @@
 	err = db.Do(func(pi *database.PackageInfo) error {
 		n++
 		fix(pi.PDoc)
-		return db.Put(pi.PDoc, time.Time{}, false)
+		return db.Put(context.Background(), pi.PDoc, time.Time{}, false)
 	})
 	if err != nil {
 		log.Fatal(err)
diff --git a/gddo-server/background.go b/gddo-server/background.go
index 025e6e1..3a9650a 100644
--- a/gddo-server/background.go
+++ b/gddo-server/background.go
@@ -7,6 +7,7 @@
 package main
 
 import (
+	"context"
 	"log"
 	"time"
 
@@ -15,7 +16,7 @@
 	"github.com/golang/gddo/gosrc"
 )
 
-func doCrawl() error {
+func doCrawl(ctx context.Context) error {
 	// Look for new package to crawl.
 	importPath, hasSubdirs, err := db.PopNewCrawl()
 	if err != nil {
@@ -23,7 +24,7 @@
 		return nil
 	}
 	if importPath != "" {
-		if pdoc, err := crawlDoc("new", importPath, nil, hasSubdirs, time.Time{}); pdoc == nil && err == nil {
+		if pdoc, err := crawlDoc(ctx, "new", importPath, nil, hasSubdirs, time.Time{}); pdoc == nil && err == nil {
 			if err := db.AddBadCrawl(importPath); err != nil {
 				log.Printf("ERROR db.AddBadCrawl(%q): %v", importPath, err)
 			}
@@ -32,7 +33,7 @@
 	}
 
 	// Crawl existing doc.
-	pdoc, pkgs, nextCrawl, err := db.Get("-")
+	pdoc, pkgs, nextCrawl, err := db.Get(ctx, "-")
 	if err != nil {
 		log.Printf("db.Get(\"-\") returned error %v", err)
 		return nil
@@ -40,7 +41,7 @@
 	if pdoc == nil || nextCrawl.After(time.Now()) {
 		return nil
 	}
-	if _, err = crawlDoc("crawl", pdoc.ImportPath, pdoc, len(pkgs) > 0, nextCrawl); err != nil {
+	if _, err = crawlDoc(ctx, "crawl", pdoc.ImportPath, pdoc, len(pkgs) > 0, nextCrawl); err != nil {
 		// Touch package so that crawl advances to next package.
 		if err := db.SetNextCrawl(pdoc.ImportPath, time.Now().Add(viper.GetDuration(ConfigMaxAge)/3)); err != nil {
 			log.Printf("ERROR db.SetNextCrawl(%q): %v", pdoc.ImportPath, err)
@@ -49,13 +50,13 @@
 	return nil
 }
 
-func readGitHubUpdates() error {
+func readGitHubUpdates(ctx context.Context) error {
 	const key = "gitHubUpdates"
 	var last string
 	if err := db.GetGob(key, &last); err != nil {
 		return err
 	}
-	last, names, err := gosrc.GetGitHubUpdates(httpClient, last)
+	last, names, err := gosrc.GetGitHubUpdates(ctx, httpClient, last)
 	if err != nil {
 		return err
 	}
diff --git a/gddo-server/crawl.go b/gddo-server/crawl.go
index 959ab22..c4d1064 100644
--- a/gddo-server/crawl.go
+++ b/gddo-server/crawl.go
@@ -7,6 +7,7 @@
 package main
 
 import (
+	"context"
 	"fmt"
 	"log"
 	"regexp"
@@ -24,7 +25,7 @@
 )
 
 // crawlDoc fetches the package documentation from the VCS and updates the database.
-func crawlDoc(source string, importPath string, pdoc *doc.Package, hasSubdirs bool, nextCrawl time.Time) (*doc.Package, error) {
+func crawlDoc(ctx context.Context, source string, importPath string, pdoc *doc.Package, hasSubdirs bool, nextCrawl time.Time) (*doc.Package, error) {
 	message := []interface{}{source}
 	defer func() {
 		message = append(message, importPath)
@@ -58,7 +59,7 @@
 		err = gosrc.NotFoundError{Message: "testdata."}
 	} else {
 		var pdocNew *doc.Package
-		pdocNew, err = doc.Get(httpClient, importPath, etag)
+		pdocNew, err = doc.Get(ctx, httpClient, importPath, etag)
 		message = append(message, "fetch:", int64(time.Since(start)/time.Millisecond))
 		if err == nil && pdocNew.Name == "" && !hasSubdirs {
 			for _, e := range pdocNew.Errors {
@@ -83,7 +84,7 @@
 
 	if err == nil {
 		message = append(message, "put:", pdoc.Etag)
-		if err := put(pdoc, nextCrawl); err != nil {
+		if err := put(ctx, pdoc, nextCrawl); err != nil {
 			log.Println(err)
 		}
 		return pdoc, nil
@@ -94,7 +95,7 @@
 			}
 			message = append(message, "archive", e)
 			pdoc.Status = e.Status
-			if err := db.Put(pdoc, nextCrawl, false); err != nil {
+			if err := db.Put(ctx, pdoc, nextCrawl, false); err != nil {
 				log.Printf("ERROR db.Put(%q): %v", importPath, err)
 			}
 		} else {
@@ -107,7 +108,7 @@
 		return pdoc, nil
 	} else if e, ok := err.(gosrc.NotFoundError); ok {
 		message = append(message, "notfound:", e)
-		if err := db.Delete(importPath); err != nil {
+		if err := db.Delete(ctx, importPath); err != nil {
 			log.Printf("ERROR db.Delete(%q): %v", importPath, err)
 		}
 		return nil, e
@@ -117,12 +118,12 @@
 	}
 }
 
-func put(pdoc *doc.Package, nextCrawl time.Time) error {
+func put(ctx context.Context, pdoc *doc.Package, nextCrawl time.Time) error {
 	if pdoc.Status == gosrc.NoRecentCommits &&
 		isActivePkg(pdoc.ImportPath, gosrc.NoRecentCommits) {
 		pdoc.Status = gosrc.Active
 	}
-	if err := db.Put(pdoc, nextCrawl, false); err != nil {
+	if err := db.Put(ctx, pdoc, nextCrawl, false); err != nil {
 		return fmt.Errorf("ERROR db.Put(%q): %v", pdoc.ImportPath, err)
 	}
 	return nil
diff --git a/gddo-server/main.go b/gddo-server/main.go
index da97770..c81a6c9 100644
--- a/gddo-server/main.go
+++ b/gddo-server/main.go
@@ -9,6 +9,7 @@
 
 import (
 	"bytes"
+	"context"
 	"crypto/md5"
 	"encoding/json"
 	"errors"
@@ -30,8 +31,6 @@
 	"cloud.google.com/go/compute/metadata"
 	"cloud.google.com/go/logging"
 	"github.com/spf13/viper"
-	"golang.org/x/net/context"
-	"google.golang.org/appengine"
 
 	"github.com/golang/gddo/database"
 	"github.com/golang/gddo/doc"
@@ -74,7 +73,7 @@
 
 // getDoc gets the package documentation from the database or from the version
 // control system as needed.
-func getDoc(path string, requestType int) (*doc.Package, []database.Package, error) {
+func getDoc(ctx context.Context, path string, requestType int) (*doc.Package, []database.Package, error) {
 	if path == "-" {
 		// A hack in the database package uses the path "-" to represent the
 		// next document to crawl. Block "-" here so that requests to /- always
@@ -82,7 +81,7 @@
 		return nil, nil, &httpError{status: http.StatusNotFound}
 	}
 
-	pdoc, pkgs, nextCrawl, err := db.Get(path)
+	pdoc, pkgs, nextCrawl, err := db.Get(ctx, path)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -103,7 +102,7 @@
 
 	c := make(chan crawlResult, 1)
 	go func() {
-		pdoc, err := crawlDoc("web  ", path, pdoc, len(pkgs) > 0, nextCrawl)
+		pdoc, err := crawlDoc(ctx, "web  ", path, pdoc, len(pkgs) > 0, nextCrawl)
 		c <- crawlResult{pdoc, err}
 	}()
 
@@ -236,12 +235,12 @@
 	}
 
 	importPath := strings.TrimPrefix(req.URL.Path, "/")
-	pdoc, pkgs, err := getDoc(importPath, requestType)
+	pdoc, pkgs, err := getDoc(req.Context(), importPath, requestType)
 
 	if e, ok := err.(gosrc.NotFoundError); ok && e.Redirect != "" {
 		// To prevent dumb clients from following redirect loops, respond with
 		// status 404 if the target document is not found.
-		if _, _, err := getDoc(e.Redirect, requestType); gosrc.IsNotFound(err) {
+		if _, _, err := getDoc(req.Context(), e.Redirect, requestType); gosrc.IsNotFound(err) {
 			return &httpError{status: http.StatusNotFound}
 		}
 		u := "/" + e.Redirect
@@ -262,7 +261,7 @@
 		if len(pkgs) == 0 {
 			return &httpError{status: http.StatusNotFound}
 		}
-		pdocChild, _, _, err := db.Get(pkgs[0].Path)
+		pdocChild, _, _, err := db.Get(req.Context(), pkgs[0].Path)
 		if err != nil {
 			return err
 		}
@@ -420,13 +419,13 @@
 
 func serveRefresh(resp http.ResponseWriter, req *http.Request) error {
 	importPath := req.Form.Get("path")
-	_, pkgs, _, err := db.Get(importPath)
+	_, pkgs, _, err := db.Get(req.Context(), importPath)
 	if err != nil {
 		return err
 	}
 	c := make(chan error, 1)
 	go func() {
-		_, err := crawlDoc("rfrsh", importPath, nil, len(pkgs) > 0, time.Time{})
+		_, err := crawlDoc(req.Context(), "rfrsh", importPath, nil, len(pkgs) > 0, time.Time{})
 		c <- err
 	}()
 	select {
@@ -552,7 +551,7 @@
 	}
 
 	if gosrc.IsValidRemotePath(q) || (strings.Contains(q, "/") && gosrc.IsGoRepoPath(q)) {
-		pdoc, pkgs, err := getDoc(q, queryRequest)
+		pdoc, pkgs, err := getDoc(req.Context(), q, queryRequest)
 		if e, ok := err.(gosrc.NotFoundError); ok && e.Redirect != "" {
 			http.Redirect(resp, req, "/"+e.Redirect, http.StatusFound)
 			return nil
@@ -611,9 +610,9 @@
 	var pkgs []database.Package
 
 	if gosrc.IsValidRemotePath(q) || (strings.Contains(q, "/") && gosrc.IsGoRepoPath(q)) {
-		pdoc, _, err := getDoc(q, apiRequest)
+		pdoc, _, err := getDoc(req.Context(), q, apiRequest)
 		if e, ok := err.(gosrc.NotFoundError); ok && e.Redirect != "" {
-			pdoc, _, err = getDoc(e.Redirect, robotRequest)
+			pdoc, _, err = getDoc(req.Context(), e.Redirect, robotRequest)
 		}
 		if err == nil && pdoc != nil {
 			pkgs = []database.Package{{Path: pdoc.ImportPath, Synopsis: pdoc.Synopsis}}
@@ -668,7 +667,7 @@
 
 func serveAPIImports(resp http.ResponseWriter, req *http.Request) error {
 	importPath := strings.TrimPrefix(req.URL.Path, "/imports/")
-	pdoc, _, err := getDoc(importPath, robotRequest)
+	pdoc, _, err := getDoc(req.Context(), importPath, robotRequest)
 	if err != nil {
 		return err
 	}
@@ -922,14 +921,14 @@
 
 	go func() {
 		for range time.Tick(viper.GetDuration(ConfigCrawlInterval)) {
-			if err := doCrawl(); err != nil {
+			if err := doCrawl(context.Background()); err != nil {
 				log.Printf("Task Crawl: %v", err)
 			}
 		}
 	}()
 	go func() {
 		for range time.Tick(viper.GetDuration(ConfigGithubInterval)) {
-			if err := readGitHubUpdates(); err != nil {
+			if err := readGitHubUpdates(context.Background()); err != nil {
 				log.Printf("Task GitHub updates: %v", err)
 			}
 		}
@@ -1010,6 +1009,5 @@
 		gceLogger = newGCELogger(logger)
 	}
 
-	http.Handle("/", root)
-	appengine.Main()
+	log.Fatal(http.ListenAndServe(viper.GetString(ConfigBindAddress), root))
 }
diff --git a/gosrc/bitbucket.go b/gosrc/bitbucket.go
index 375e1f9..8435148 100644
--- a/gosrc/bitbucket.go
+++ b/gosrc/bitbucket.go
@@ -7,6 +7,7 @@
 package gosrc
 
 import (
+	"context"
 	"log"
 	"net/http"
 	"path"
@@ -40,14 +41,14 @@
 	Timestamp string `json:"utctimestamp"`
 }
 
-func getBitbucketDir(client *http.Client, match map[string]string, savedEtag string) (*Directory, error) {
+func getBitbucketDir(ctx context.Context, client *http.Client, match map[string]string, savedEtag string) (*Directory, error) {
 	var repo *bitbucketRepo
 	c := &httpClient{client: client}
 
 	if m := bitbucketEtagRe.FindStringSubmatch(savedEtag); m != nil {
 		match["vcs"] = m[1]
 	} else {
-		repo, err := getBitbucketRepo(c, match)
+		repo, err := getBitbucketRepo(ctx, c, match)
 		if err != nil {
 			return nil, err
 		}
@@ -60,7 +61,7 @@
 
 	for _, nodeType := range []string{"branches", "tags"} {
 		var nodes map[string]bitbucketNode
-		if _, err := c.getJSON(expand("https://api.bitbucket.org/1.0/repositories/{owner}/{repo}/{0}", match, nodeType), &nodes); err != nil {
+		if _, err := c.getJSON(ctx, expand("https://api.bitbucket.org/1.0/repositories/{owner}/{repo}/{0}", match, nodeType), &nodes); err != nil {
 			return nil, err
 		}
 		for t, n := range nodes {
@@ -88,7 +89,7 @@
 	}
 
 	if repo == nil {
-		repo, err = getBitbucketRepo(c, match)
+		repo, err = getBitbucketRepo(ctx, c, match)
 		if err != nil {
 			return nil, err
 		}
@@ -101,7 +102,7 @@
 		}
 	}
 
-	if _, err := c.getJSON(expand("https://api.bitbucket.org/1.0/repositories/{owner}/{repo}/src/{tag}{dir}/", match), &contents); err != nil {
+	if _, err := c.getJSON(ctx, expand("https://api.bitbucket.org/1.0/repositories/{owner}/{repo}/src/{tag}{dir}/", match), &contents); err != nil {
 		return nil, err
 	}
 
@@ -116,7 +117,7 @@
 		}
 	}
 
-	if err := c.getFiles(dataURLs, files); err != nil {
+	if err := c.getFiles(ctx, dataURLs, files); err != nil {
 		return nil, err
 	}
 
@@ -141,9 +142,9 @@
 	}, nil
 }
 
-func getBitbucketRepo(c *httpClient, match map[string]string) (*bitbucketRepo, error) {
+func getBitbucketRepo(ctx context.Context, c *httpClient, match map[string]string) (*bitbucketRepo, error) {
 	var repo bitbucketRepo
-	if _, err := c.getJSON(expand("https://api.bitbucket.org/1.0/repositories/{owner}/{repo}", match), &repo); err != nil {
+	if _, err := c.getJSON(ctx, expand("https://api.bitbucket.org/1.0/repositories/{owner}/{repo}", match), &repo); err != nil {
 		return nil, err
 	}
 
diff --git a/gosrc/client.go b/gosrc/client.go
index 09d54e3..2f4663f 100644
--- a/gosrc/client.go
+++ b/gosrc/client.go
@@ -7,6 +7,7 @@
 package gosrc
 
 import (
+	"context"
 	"encoding/json"
 	"fmt"
 	"io"
@@ -31,11 +32,12 @@
 }
 
 // get issues a GET to the specified URL.
-func (c *httpClient) get(url string) (*http.Response, error) {
+func (c *httpClient) get(ctx context.Context, url string) (*http.Response, error) {
 	req, err := http.NewRequest("GET", url, nil)
 	if err != nil {
 		return nil, err
 	}
+
 	for k, vs := range c.header {
 		req.Header[k] = vs
 	}
@@ -66,8 +68,8 @@
 	return resp, err
 }
 
-func (c *httpClient) getBytes(url string) ([]byte, error) {
-	resp, err := c.get(url)
+func (c *httpClient) getBytes(ctx context.Context, url string) ([]byte, error) {
+	resp, err := c.get(ctx, url)
 	if err != nil {
 		return nil, err
 	}
@@ -79,8 +81,8 @@
 	return p, err
 }
 
-func (c *httpClient) getReader(url string) (io.ReadCloser, error) {
-	resp, err := c.get(url)
+func (c *httpClient) getReader(ctx context.Context, url string) (io.ReadCloser, error) {
+	resp, err := c.get(ctx, url)
 	if err != nil {
 		return nil, err
 	}
@@ -92,8 +94,8 @@
 	return resp.Body, nil
 }
 
-func (c *httpClient) getJSON(url string, v interface{}) (*http.Response, error) {
-	resp, err := c.get(url)
+func (c *httpClient) getJSON(ctx context.Context, url string, v interface{}) (*http.Response, error) {
+	resp, err := c.get(ctx, url)
 	if err != nil {
 		return resp, err
 	}
@@ -108,11 +110,11 @@
 	return resp, err
 }
 
-func (c *httpClient) getFiles(urls []string, files []*File) error {
+func (c *httpClient) getFiles(ctx context.Context, urls []string, files []*File) error {
 	ch := make(chan error, len(files))
 	for i := range files {
 		go func(i int) {
-			resp, err := c.get(urls[i])
+			resp, err := c.get(ctx, urls[i])
 			if err != nil {
 				ch <- err
 				return
diff --git a/gosrc/github.go b/gosrc/github.go
index 6445327..e1d9528 100644
--- a/gosrc/github.go
+++ b/gosrc/github.go
@@ -7,6 +7,7 @@
 package gosrc
 
 import (
+	"context"
 	"encoding/json"
 	"fmt"
 	"net/http"
@@ -57,7 +58,7 @@
 	return &RemoteError{resp.Request.URL.Host, fmt.Errorf("%d: (%s)", resp.StatusCode, resp.Request.URL.String())}
 }
 
-func getGitHubDir(client *http.Client, match map[string]string, savedEtag string) (*Directory, error) {
+func getGitHubDir(ctx context.Context, client *http.Client, match map[string]string, savedEtag string) (*Directory, error) {
 
 	c := &httpClient{client: client, errFn: gitHubError}
 
@@ -69,7 +70,7 @@
 		DefaultBranch string    `json:"default_branch"`
 	}
 
-	if _, err := c.getJSON(expand("https://api.github.com/repos/{owner}/{repo}", match), &repo); err != nil {
+	if _, err := c.getJSON(ctx, expand("https://api.github.com/repos/{owner}/{repo}", match), &repo); err != nil {
 		return nil, err
 	}
 
@@ -79,7 +80,7 @@
 	if match["dir"] != "" {
 		u += fmt.Sprintf("?path=%s", url.QueryEscape(match["dir"]))
 	}
-	if _, err := c.getJSON(u, &commits); err != nil {
+	if _, err := c.getJSON(ctx, u, &commits); err != nil {
 		return nil, err
 	}
 	if len(commits) == 0 {
@@ -110,7 +111,7 @@
 		HTMLURL string `json:"html_url"`
 	}
 
-	if _, err := c.getJSON(expand("https://api.github.com/repos/{owner}/{repo}/contents{dir}", match), &contents); err != nil {
+	if _, err := c.getJSON(ctx, expand("https://api.github.com/repos/{owner}/{repo}/contents{dir}", match), &contents); err != nil {
 		// The GitHub content API returns array values for directories
 		// and object values for files. If there's a type mismatch at
 		// the beginning of the response, then assume that the path is
@@ -153,7 +154,7 @@
 	}
 
 	c.header = gitHubRawHeader
-	if err := c.getFiles(dataURLs, files); err != nil {
+	if err := c.getFiles(ctx, dataURLs, files); err != nil {
 		return nil, err
 	}
 
@@ -200,18 +201,18 @@
 	return n < 3
 }
 
-func getGitHubPresentation(client *http.Client, match map[string]string) (*Presentation, error) {
+func getGitHubPresentation(ctx context.Context, client *http.Client, match map[string]string) (*Presentation, error) {
 	c := &httpClient{client: client, header: gitHubRawHeader}
 
 	var repo struct {
 		DefaultBranch string `json:"default_branch"`
 	}
-	if _, err := c.getJSON(expand("https://api.github.com/repos/{owner}/{repo}", match), &repo); err != nil {
+	if _, err := c.getJSON(ctx, expand("https://api.github.com/repos/{owner}/{repo}", match), &repo); err != nil {
 		return nil, err
 	}
 	branch := repo.DefaultBranch
 
-	p, err := c.getBytes(expand("https://api.github.com/repos/{owner}/{repo}/contents{dir}/{file}", match))
+	p, err := c.getBytes(ctx, expand("https://api.github.com/repos/{owner}/{repo}/contents{dir}/{file}", match))
 	if err != nil {
 		return nil, err
 	}
@@ -242,7 +243,7 @@
 				files = append(files, &File{Name: fname})
 				dataURLs = append(dataURLs, u.String())
 			}
-			err := c.getFiles(dataURLs, files)
+			err := c.getFiles(ctx, dataURLs, files)
 			return files, err
 		},
 		resolveURL: func(fname string) string {
@@ -262,7 +263,7 @@
 
 // GetGitHubUpdates returns the full names ("owner/repo") of recently pushed GitHub repositories.
 // by pushedAfter.
-func GetGitHubUpdates(client *http.Client, pushedAfter string) (maxPushedAt string, names []string, err error) {
+func GetGitHubUpdates(ctx context.Context, client *http.Client, pushedAfter string) (maxPushedAt string, names []string, err error) {
 	c := httpClient{client: client, header: gitHubPreviewHeader}
 
 	if pushedAfter == "" {
@@ -275,7 +276,7 @@
 			PushedAt string `json:"pushed_at"`
 		}
 	}
-	_, err = c.getJSON(u, &updates)
+	_, err = c.getJSON(ctx, u, &updates)
 	if err != nil {
 		return pushedAfter, nil, err
 	}
@@ -290,14 +291,14 @@
 	return maxPushedAt, names, nil
 }
 
-func getGitHubProject(client *http.Client, match map[string]string) (*Project, error) {
+func getGitHubProject(ctx context.Context, client *http.Client, match map[string]string) (*Project, error) {
 	c := &httpClient{client: client, errFn: gitHubError}
 
 	var repo struct {
 		Description string
 	}
 
-	if _, err := c.getJSON(expand("https://api.github.com/repos/{owner}/{repo}", match), &repo); err != nil {
+	if _, err := c.getJSON(ctx, expand("https://api.github.com/repos/{owner}/{repo}", match), &repo); err != nil {
 		return nil, err
 	}
 
@@ -306,7 +307,7 @@
 	}, nil
 }
 
-func getGistDir(client *http.Client, match map[string]string, savedEtag string) (*Directory, error) {
+func getGistDir(ctx context.Context, client *http.Client, match map[string]string, savedEtag string) (*Directory, error) {
 	c := &httpClient{client: client, errFn: gitHubError}
 
 	var gist struct {
@@ -319,7 +320,7 @@
 		}
 	}
 
-	if _, err := c.getJSON(expand("https://api.github.com/gists/{gist}", match), &gist); err != nil {
+	if _, err := c.getJSON(ctx, expand("https://api.github.com/gists/{gist}", match), &gist); err != nil {
 		return nil, err
 	}
 
diff --git a/gosrc/golang.go b/gosrc/golang.go
index faed15a..d075349 100644
--- a/gosrc/golang.go
+++ b/gosrc/golang.go
@@ -7,6 +7,7 @@
 package gosrc
 
 import (
+	"context"
 	"errors"
 	"net/http"
 	"regexp"
@@ -18,11 +19,11 @@
 	golangFileRe         = regexp.MustCompile(`<a href="([^"]+)"`)
 )
 
-func getStandardDir(client *http.Client, importPath string, savedEtag string) (*Directory, error) {
+func getStandardDir(ctx context.Context, client *http.Client, importPath string, savedEtag string) (*Directory, error) {
 	c := &httpClient{client: client}
 
 	browseURL := "https://golang.org/src/" + importPath + "/"
-	p, err := c.getBytes(browseURL)
+	p, err := c.getBytes(ctx, browseURL)
 	if err != nil {
 		return nil, err
 	}
@@ -47,7 +48,7 @@
 		}
 	}
 
-	if err := c.getFiles(dataURLs, files); err != nil {
+	if err := c.getFiles(ctx, dataURLs, files); err != nil {
 		return nil, err
 	}
 
diff --git a/gosrc/google.go b/gosrc/google.go
index 3a2fc36..693f766 100644
--- a/gosrc/google.go
+++ b/gosrc/google.go
@@ -7,6 +7,7 @@
 package gosrc
 
 import (
+	"context"
 	"errors"
 	"net/http"
 	"net/url"
@@ -30,7 +31,7 @@
 	googleFileRe     = regexp.MustCompile(`<li><a href="([^"]+)"`)
 )
 
-func checkGoogleRedir(c *httpClient, match map[string]string) error {
+func checkGoogleRedir(ctx context.Context, c *httpClient, match map[string]string) error {
 	resp, err := c.getNoFollow(expand("https://code.google.com/{pr}/{repo}/", match))
 	if err != nil {
 		return err
@@ -48,22 +49,22 @@
 	return c.err(resp)
 }
 
-func getGoogleDir(client *http.Client, match map[string]string, savedEtag string) (*Directory, error) {
+func getGoogleDir(ctx context.Context, client *http.Client, match map[string]string, savedEtag string) (*Directory, error) {
 	setupGoogleMatch(match)
 	c := &httpClient{client: client}
 
-	if err := checkGoogleRedir(c, match); err != nil {
+	if err := checkGoogleRedir(ctx, c, match); err != nil {
 		return nil, err
 	}
 
 	if m := googleEtagRe.FindStringSubmatch(savedEtag); m != nil {
 		match["vcs"] = m[1]
-	} else if err := getGoogleVCS(c, match); err != nil {
+	} else if err := getGoogleVCS(ctx, c, match); err != nil {
 		return nil, err
 	}
 
 	// Scrape the repo browser to find the project revision and individual Go files.
-	p, err := c.getBytes(expand("http://{subrepo}{dot}{repo}.googlecode.com/{vcs}{dir}/", match))
+	p, err := c.getBytes(ctx, expand("http://{subrepo}{dot}{repo}.googlecode.com/{vcs}{dir}/", match))
 	if err != nil {
 		return nil, err
 	}
@@ -95,7 +96,7 @@
 		}
 	}
 
-	if err := c.getFiles(dataURLs, files); err != nil {
+	if err := c.getFiles(ctx, dataURLs, files); err != nil {
 		return nil, err
 	}
 
@@ -128,9 +129,9 @@
 	}
 }
 
-func getGoogleVCS(c *httpClient, match map[string]string) error {
+func getGoogleVCS(ctx context.Context, c *httpClient, match map[string]string) error {
 	// Scrape the HTML project page to find the VCS.
-	p, err := c.getBytes(expand("http://code.google.com/{pr}/{repo}/source/checkout", match))
+	p, err := c.getBytes(ctx, expand("http://code.google.com/{pr}/{repo}/source/checkout", match))
 	if err != nil {
 		return err
 	}
@@ -142,11 +143,11 @@
 	return nil
 }
 
-func getGooglePresentation(client *http.Client, match map[string]string) (*Presentation, error) {
+func getGooglePresentation(ctx context.Context, client *http.Client, match map[string]string) (*Presentation, error) {
 	c := &httpClient{client: client}
 
 	setupGoogleMatch(match)
-	if err := getGoogleVCS(c, match); err != nil {
+	if err := getGoogleVCS(ctx, c, match); err != nil {
 		return nil, err
 	}
 
@@ -155,7 +156,7 @@
 		return nil, err
 	}
 
-	p, err := c.getBytes(expand("http://{subrepo}{dot}{repo}.googlecode.com/{vcs}{dir}/{file}", match))
+	p, err := c.getBytes(ctx, expand("http://{subrepo}{dot}{repo}.googlecode.com/{vcs}{dir}/{file}", match))
 	if err != nil {
 		return nil, err
 	}
@@ -174,7 +175,7 @@
 				files = append(files, &File{Name: fname})
 				dataURLs = append(dataURLs, u.String())
 			}
-			err := c.getFiles(dataURLs, files)
+			err := c.getFiles(ctx, dataURLs, files)
 			return files, err
 		},
 		resolveURL: func(fname string) string {
diff --git a/gosrc/gosrc.go b/gosrc/gosrc.go
index 68181df..38005ec 100644
--- a/gosrc/gosrc.go
+++ b/gosrc/gosrc.go
@@ -8,6 +8,7 @@
 package gosrc
 
 import (
+	"context"
 	"encoding/xml"
 	"errors"
 	"fmt"
@@ -150,9 +151,9 @@
 type service struct {
 	pattern         *regexp.Regexp
 	prefix          string
-	get             func(*http.Client, map[string]string, string) (*Directory, error)
-	getPresentation func(*http.Client, map[string]string) (*Presentation, error)
-	getProject      func(*http.Client, map[string]string) (*Project, error)
+	get             func(context.Context, *http.Client, map[string]string, string) (*Directory, error)
+	getPresentation func(context.Context, *http.Client, map[string]string) (*Presentation, error)
+	getProject      func(context.Context, *http.Client, map[string]string) (*Project, error)
 }
 
 var services []*service
@@ -224,7 +225,7 @@
 	return ""
 }
 
-func fetchMeta(client *http.Client, importPath string) (scheme string, im *importMeta, sm *sourceMeta, redir bool, err error) {
+func fetchMeta(ctx context.Context, client *http.Client, importPath string) (scheme string, im *importMeta, sm *sourceMeta, redir bool, err error) {
 	uri := importPath
 	if !strings.Contains(uri, "/") {
 		// Add slash for root of domain.
@@ -234,13 +235,13 @@
 
 	c := httpClient{client: client}
 	scheme = "https"
-	resp, err := c.get(scheme + "://" + uri)
+	resp, err := c.get(ctx, scheme+"://"+uri)
 	if err != nil || resp.StatusCode != 200 {
 		if err == nil {
 			resp.Body.Close()
 		}
 		scheme = "http"
-		resp, err = c.get(scheme + "://" + uri)
+		resp, err = c.get(ctx, scheme+"://"+uri)
 		if err != nil {
 			return scheme, nil, nil, false, err
 		}
@@ -340,20 +341,20 @@
 // getVCSDirFn is called by getDynamic to fetch source using VCS commands. The
 // default value here does nothing. If the code is not built for App Engine,
 // then getVCSDirFn is set getVCSDir, the function that actually does the work.
-var getVCSDirFn = func(client *http.Client, m map[string]string, etag string) (*Directory, error) {
+var getVCSDirFn = func(ctx context.Context, client *http.Client, m map[string]string, etag string) (*Directory, error) {
 	return nil, errNoMatch
 }
 
 // getDynamic gets a directory from a service that is not statically known.
-func getDynamic(client *http.Client, importPath, etag string) (*Directory, error) {
-	metaProto, im, sm, redir, err := fetchMeta(client, importPath)
+func getDynamic(ctx context.Context, client *http.Client, importPath, etag string) (*Directory, error) {
+	metaProto, im, sm, redir, err := fetchMeta(ctx, client, importPath)
 	if err != nil {
 		return nil, err
 	}
 
 	if im.projectRoot != importPath {
 		var imRoot *importMeta
-		metaProto, imRoot, _, redir, err = fetchMeta(client, im.projectRoot)
+		metaProto, imRoot, _, redir, err = fetchMeta(ctx, client, im.projectRoot)
 		if err != nil {
 			return nil, err
 		}
@@ -376,7 +377,7 @@
 	dirName := importPath[len(im.projectRoot):]
 
 	resolvedPath := repo + dirName
-	dir, err := getStatic(client, resolvedPath, etag)
+	dir, err := getStatic(ctx, client, resolvedPath, etag)
 	if err == errNoMatch {
 		resolvedPath = repo + "." + im.vcs + dirName
 		match := map[string]string{
@@ -387,7 +388,7 @@
 			"scheme":     proto,
 			"vcs":        im.vcs,
 		}
-		dir, err = getVCSDirFn(client, match, etag)
+		dir, err = getVCSDirFn(ctx, client, match, etag)
 	}
 	if err != nil || dir == nil {
 		return nil, err
@@ -442,7 +443,7 @@
 
 // getStatic gets a diretory from a statically known service. getStatic
 // returns errNoMatch if the import path is not recognized.
-func getStatic(client *http.Client, importPath, etag string) (*Directory, error) {
+func getStatic(ctx context.Context, client *http.Client, importPath, etag string) (*Directory, error) {
 	for _, s := range services {
 		if s.get == nil {
 			continue
@@ -452,7 +453,7 @@
 			return nil, err
 		}
 		if match != nil {
-			dir, err := s.get(client, match, etag)
+			dir, err := s.get(ctx, client, match, etag)
 			if dir != nil {
 				dir.ImportPath = importPath
 				dir.ResolvedPath = importPath
@@ -463,16 +464,16 @@
 	return nil, errNoMatch
 }
 
-func Get(client *http.Client, importPath string, etag string) (dir *Directory, err error) {
+func Get(ctx context.Context, client *http.Client, importPath string, etag string) (dir *Directory, err error) {
 	switch {
 	case localPath != "":
 		dir, err = getLocal(importPath)
 	case IsGoRepoPath(importPath):
-		dir, err = getStandardDir(client, importPath, etag)
+		dir, err = getStandardDir(ctx, client, importPath, etag)
 	case IsValidRemotePath(importPath):
-		dir, err = getStatic(client, importPath, etag)
+		dir, err = getStatic(ctx, client, importPath, etag)
 		if err == errNoMatch {
-			dir, err = getDynamic(client, importPath, etag)
+			dir, err = getDynamic(ctx, client, importPath, etag)
 		}
 	default:
 		err = errNoMatch
@@ -486,7 +487,7 @@
 }
 
 // GetPresentation gets a presentation from the the given path.
-func GetPresentation(client *http.Client, importPath string) (*Presentation, error) {
+func GetPresentation(ctx context.Context, client *http.Client, importPath string) (*Presentation, error) {
 	ext := path.Ext(importPath)
 	if ext != ".slide" && ext != ".article" {
 		return nil, NotFoundError{Message: "unknown file extension."}
@@ -504,14 +505,14 @@
 		}
 		if match != nil {
 			match["file"] = file
-			return s.getPresentation(client, match)
+			return s.getPresentation(ctx, client, match)
 		}
 	}
 	return nil, NotFoundError{Message: "path does not match registered service"}
 }
 
 // GetProject gets information about a repository.
-func GetProject(client *http.Client, importPath string) (*Project, error) {
+func GetProject(ctx context.Context, client *http.Client, importPath string) (*Project, error) {
 	for _, s := range services {
 		if s.getProject == nil {
 			continue
@@ -521,7 +522,7 @@
 			return nil, err
 		}
 		if match != nil {
-			return s.getProject(client, match)
+			return s.getProject(ctx, client, match)
 		}
 	}
 	return nil, NotFoundError{Message: "path does not match registered service"}
diff --git a/gosrc/launchpad.go b/gosrc/launchpad.go
index 3d561c8..a8c7714 100644
--- a/gosrc/launchpad.go
+++ b/gosrc/launchpad.go
@@ -10,6 +10,7 @@
 	"archive/tar"
 	"bytes"
 	"compress/gzip"
+	"context"
 	"crypto/md5"
 	"encoding/hex"
 	"io"
@@ -41,11 +42,11 @@
 	copy(p[j*md5.Size:], temp[:])
 }
 
-func getLaunchpadDir(client *http.Client, match map[string]string, savedEtag string) (*Directory, error) {
+func getLaunchpadDir(ctx context.Context, client *http.Client, match map[string]string, savedEtag string) (*Directory, error) {
 	c := &httpClient{client: client}
 
 	if match["project"] != "" && match["series"] != "" {
-		rc, err := c.getReader(expand("https://code.launchpad.net/{project}{series}/.bzr/branch-format", match))
+		rc, err := c.getReader(ctx, expand("https://code.launchpad.net/{project}{series}/.bzr/branch-format", match))
 		switch {
 		case err == nil:
 			rc.Close()
@@ -59,7 +60,7 @@
 		}
 	}
 
-	p, err := c.getBytes(expand("https://bazaar.launchpad.net/+branch/{repo}/tarball", match))
+	p, err := c.getBytes(ctx, expand("https://bazaar.launchpad.net/+branch/{repo}/tarball", match))
 	if err != nil {
 		return nil, err
 	}
diff --git a/gosrc/vcs.go b/gosrc/vcs.go
index bd7cc06..f21670d 100644
--- a/gosrc/vcs.go
+++ b/gosrc/vcs.go
@@ -10,6 +10,7 @@
 
 import (
 	"bytes"
+	"context"
 	"errors"
 	"io/ioutil"
 	"log"
@@ -245,7 +246,7 @@
 	return "", NotFoundError{Message: "Last changed revision not found"}
 }
 
-func getVCSDir(client *http.Client, match map[string]string, etagSaved string) (*Directory, error) {
+func getVCSDir(ctx context.Context, client *http.Client, match map[string]string, etagSaved string) (*Directory, error) {
 	cmd := vcsCmds[match["vcs"]]
 	if cmd == nil {
 		return nil, NotFoundError{Message: expand("VCS not supported: {vcs}", match)}