Use pkg.in/src-d/go-git.v4/ for Git indexing

This library is written in pure Go, so:

* it simplifies compilation and deployment

* no more memory leaks due forgotten Free() calls

* potential for lower memory use in large superprojects, since
  Repository objects don't have to be Free()d individually.

This comes at the cost of a 30% end-to-end slowdown indexing the
Gerrit repo.

Change-Id: Id0e51e5cf9e6bc4cfe1bdbff0adb381a3814fab8
diff --git a/cmd/zoekt-git-index/main.go b/cmd/zoekt-git-index/main.go
index 09fb3ea..02127a3 100644
--- a/cmd/zoekt-git-index/main.go
+++ b/cmd/zoekt-git-index/main.go
@@ -67,9 +67,6 @@
 
 	gitRepos := map[string]string{}
 	for _, repoDir := range flag.Args() {
-		if _, err := os.Lstat(filepath.Join(repoDir, ".git")); err == nil {
-			repoDir = filepath.Join(repoDir, ".git")
-		}
 		repoDir, err := filepath.Abs(repoDir)
 		if err != nil {
 			log.Fatal(err)
diff --git a/cmd/zoekt-repo-index/main.go b/cmd/zoekt-repo-index/main.go
index 71a3010..95dd5c1 100644
--- a/cmd/zoekt-repo-index/main.go
+++ b/cmd/zoekt-repo-index/main.go
@@ -32,6 +32,7 @@
 	"crypto/sha1"
 	"flag"
 	"fmt"
+	"io/ioutil"
 	"log"
 	"net/url"
 	"path"
@@ -44,7 +45,9 @@
 	"github.com/google/zoekt"
 	"github.com/google/zoekt/build"
 	"github.com/google/zoekt/gitindex"
-	git "github.com/libgit2/git2go"
+
+	git "gopkg.in/src-d/go-git.v4"
+	"gopkg.in/src-d/go-git.v4/plumbing"
 )
 
 var _ = log.Println
@@ -67,6 +70,7 @@
 		if err != nil {
 			return nil, err
 		}
+
 		for _, f := range args {
 			fs := strings.SplitN(f, ":", 2)
 			if len(fs) != 2 {
@@ -81,7 +85,7 @@
 				branch:       fs[0],
 				file:         fs[1],
 				mf:           mf,
-				manifestPath: repo.Path(),
+				manifestPath: cache.Path(u),
 			})
 		}
 	} else {
@@ -169,7 +173,7 @@
 	opts.SubRepositories = map[string]*zoekt.Repository{}
 
 	// branch => repo => version
-	versionMap := map[string]map[string]git.Oid{}
+	versionMap := map[string]map[string]plumbing.Hash{}
 	for _, br := range branches {
 		br.mf.Filter()
 		files, versions, err := iterateManifest(br.mf, *baseURL, *revPrefix, repoCache)
@@ -276,24 +280,46 @@
 
 // getManifest parses the manifest XML at the given branch/path inside a Git repository.
 func getManifest(repo *git.Repository, branch, path string) (*manifest.Manifest, error) {
-	obj, err := repo.RevparseSingle(branch + ":" + path)
+	ref, err := repo.Reference(plumbing.ReferenceName("refs/heads/"+branch), true)
 	if err != nil {
 		return nil, err
 	}
-	defer obj.Free()
-	blob, err := obj.AsBlob()
+
+	commit, err := repo.CommitObject(ref.Hash())
 	if err != nil {
 		return nil, err
 	}
-	return manifest.Parse(blob.Contents())
+
+	tree, err := repo.TreeObject(commit.TreeHash)
+	if err != nil {
+		return nil, err
+	}
+
+	entry, err := tree.FindEntry(path)
+	if err != nil {
+		return nil, err
+	}
+
+	blob, err := repo.BlobObject(entry.Hash)
+	if err != nil {
+		return nil, err
+	}
+	r, err := blob.Reader()
+	if err != nil {
+		return nil, err
+	}
+	defer r.Close()
+
+	content, err := ioutil.ReadAll(r)
+	return manifest.Parse(content)
 }
 
 // iterateManifest constructs a complete tree from the given Manifest.
 func iterateManifest(mf *manifest.Manifest,
 	baseURL url.URL, revPrefix string,
-	cache *gitindex.RepoCache) (map[gitindex.FileKey]gitindex.BlobLocation, map[string]git.Oid, error) {
+	cache *gitindex.RepoCache) (map[gitindex.FileKey]gitindex.BlobLocation, map[string]plumbing.Hash, error) {
 	allFiles := map[gitindex.FileKey]gitindex.BlobLocation{}
-	allVersions := map[string]git.Oid{}
+	allVersions := map[string]plumbing.Hash{}
 	for _, p := range mf.Project {
 		rev := mf.ProjectRevision(&p)
 
@@ -305,24 +331,25 @@
 			return nil, nil, err
 		}
 
-		obj, err := topRepo.RevparseSingle(revPrefix + rev)
-		if err != nil {
-			return nil, nil, err
-		}
-		defer obj.Free()
-
-		commit, err := obj.AsCommit()
+		ref, err := topRepo.Reference(plumbing.ReferenceName(revPrefix+rev), true)
 		if err != nil {
 			return nil, nil, err
 		}
 
-		allVersions[p.GetPath()] = *commit.Id()
+		commit, err := topRepo.CommitObject(ref.Hash())
+		if err != nil {
+			return nil, nil, err
+		}
+		if err != nil {
+			return nil, nil, err
+		}
+
+		allVersions[p.GetPath()] = commit.Hash
 
 		tree, err := commit.Tree()
 		if err != nil {
 			return nil, nil, err
 		}
-		defer tree.Free()
 
 		files, versions, err := gitindex.TreeToFiles(topRepo, tree, projURL.String(), cache)
 		if err != nil {
diff --git a/gitindex/cache.go b/gitindex/cache.go
index 25e9ed5..0d150d0 100644
--- a/gitindex/cache.go
+++ b/gitindex/cache.go
@@ -20,7 +20,7 @@
 	"strings"
 	"sync"
 
-	"github.com/libgit2/git2go"
+	git "gopkg.in/src-d/go-git.v4"
 )
 
 type RepoCache struct {
@@ -40,9 +40,6 @@
 func (rc *RepoCache) Close() {
 	rc.reposMu.Lock()
 	defer rc.reposMu.Unlock()
-	for _, v := range rc.repos {
-		v.Free()
-	}
 }
 
 func repoKey(u *url.URL) string {
@@ -59,21 +56,26 @@
 	return filepath.Join(baseDir, key)
 }
 
+func (rc *RepoCache) Path(u *url.URL) string {
+	key := repoKey(u)
+	return filepath.Join(rc.baseDir, key)
+}
+
 // Open opens a git repository. The cache retains a pointer to the
 // repository, so it cannot be freed.
 func (rc *RepoCache) Open(u *url.URL) (*git.Repository, error) {
-	key := repoKey(u)
-	dir := filepath.Join(rc.baseDir, key)
 
+	dir := rc.Path(u)
 	rc.reposMu.Lock()
 	defer rc.reposMu.Unlock()
 
+	key := repoKey(u)
 	r := rc.repos[key]
 	if r != nil {
 		return r, nil
 	}
 
-	repo, err := git.OpenRepository(dir)
+	repo, err := git.PlainOpen(dir)
 	if err == nil {
 		rc.repos[key] = repo
 	}
diff --git a/gitindex/git.go b/gitindex/git.go
index a09086c..c856028 100644
--- a/gitindex/git.go
+++ b/gitindex/git.go
@@ -16,6 +16,8 @@
 
 import (
 	"fmt"
+	"io"
+	"io/ioutil"
 	"log"
 	"net/url"
 	"os"
@@ -28,7 +30,11 @@
 	"github.com/google/zoekt"
 	"github.com/google/zoekt/build"
 
-	git "github.com/libgit2/git2go"
+	"gopkg.in/src-d/go-git.v4/config"
+	"gopkg.in/src-d/go-git.v4/plumbing"
+	"gopkg.in/src-d/go-git.v4/plumbing/object"
+
+	git "gopkg.in/src-d/go-git.v4"
 )
 
 // RepoModTime returns the time of last fetch of a git repository.
@@ -129,55 +135,64 @@
 }
 
 // getCommit returns a tree object for the given reference.
-func getCommit(repo *git.Repository, ref string) (*git.Commit, error) {
-	obj, err := repo.RevparseSingle(ref)
+func getCommit(repo *git.Repository, ref string) (*object.Commit, error) {
+	sha1, err := repo.ResolveRevision(plumbing.Revision(ref))
 	if err != nil {
 		return nil, err
 	}
-	defer obj.Free()
 
-	commitObj, err := obj.Peel(git.ObjectCommit)
+	commitObj, err := repo.CommitObject(*sha1)
 	if err != nil {
 		return nil, err
 	}
-	return commitObj.AsCommit()
+	return commitObj, nil
 }
 
-func clearEmptyConfig(err error) error {
-	if git.IsErrorClass(err, git.ErrClassConfig) && git.IsErrorCode(err, git.ErrNotFound) {
-		return nil
+func configLookupRemoteURL(cfg *config.Config, key string) string {
+	rc := cfg.Remotes[key]
+	if rc == nil || len(rc.URLs) == 0 {
+		return ""
 	}
-	return err
+	return rc.URLs[0]
+}
+
+func configLookupString(cfg *config.Config, key string) string {
+	fields := strings.Split(key, ".")
+	for _, s := range cfg.Raw.Sections {
+		if s.Name != fields[0] {
+			continue
+		}
+
+		for _, o := range s.Options {
+			if o.Key != fields[1] {
+				continue
+			}
+			return o.Value
+		}
+	}
+
+	return ""
 }
 
 func isMissingBranchError(err error) bool {
-	return git.IsErrorClass(err, git.ErrClassReference) && git.IsErrorCode(err, git.ErrNotFound)
+	return err != nil && err.Error() == "reference not found"
 }
 
 func setTemplatesFromConfig(desc *zoekt.Repository, repoDir string) error {
-	base, err := git.NewConfig()
-	if err != nil {
-		return err
-	}
-	defer base.Free()
-	cfg, err := git.OpenOndisk(base, filepath.Join(repoDir, "config"))
-	if err != nil {
-		return err
-	}
-	defer cfg.Free()
-
-	webURLStr, err := cfg.LookupString("zoekt.web-url")
-	err = clearEmptyConfig(err)
+	repo, err := git.PlainOpen(repoDir)
 	if err != nil {
 		return err
 	}
 
-	webURLType, err := cfg.LookupString("zoekt.web-url-type")
-	err = clearEmptyConfig(err)
+	cfg, err := repo.Config()
 	if err != nil {
 		return err
 	}
 
+	webURLStr := configLookupString(cfg, "zoekt.web-url")
+
+	webURLType := configLookupString(cfg, "zoekt.web-url-type")
+
 	if webURLType != "" && webURLStr != "" {
 		webURL, err := url.Parse(webURLStr)
 		if err != nil {
@@ -188,19 +203,13 @@
 		}
 	}
 
-	name, err := cfg.LookupString("zoekt.name")
-	err = clearEmptyConfig(err)
-	if err != nil {
-		return err
-	}
-
+	name := configLookupString(cfg, "zoekt.name")
 	if name != "" {
 		desc.Name = name
 	} else {
-		remoteURL, err := cfg.LookupString("remote.origin.url")
-		err = clearEmptyConfig(err)
-		if err != nil || remoteURL == "" {
-			return err
+		remoteURL := configLookupRemoteURL(cfg, "origin")
+		if remoteURL == "" {
+			return nil
 		}
 		u, err := url.Parse(remoteURL)
 		if err != nil {
@@ -255,36 +264,32 @@
 	var result []string
 	for _, b := range bs {
 		if b == "HEAD" {
-			_, ref, err := repo.RevparseExt(b)
+			ref, err := repo.Head()
 			if err != nil {
 				return nil, err
 			}
 
-			result = append(result, strings.TrimPrefix(ref.Name(), prefix))
+			result = append(result, strings.TrimPrefix(ref.Name().String(), prefix))
 			continue
 		}
 
 		if strings.Contains(b, "*") {
-			iter, err := repo.NewBranchIterator(git.BranchAll)
+			iter, err := repo.Branches()
 			if err != nil {
-				log.Println("boem")
 				return nil, err
 			}
 
+			defer iter.Close()
 			for {
-				br, _, err := iter.Next()
-				if git.IsErrorCode(err, git.ErrIterOver) {
+				ref, err := iter.Next()
+				if err == io.EOF {
 					break
 				}
 				if err != nil {
-					log.Printf("bam %#v", err)
 					return nil, err
 				}
 
-				name, err := br.Name()
-				if err != nil {
-					return nil, err
-				}
+				name := ref.Name().Short()
 				if matched, err := filepath.Match(b, name); err != nil {
 					return nil, err
 				} else if !matched {
@@ -300,12 +305,11 @@
 	}
 
 	return result, nil
-
 }
 
 // IndexGitRepo indexes the git repository as specified by the options.
 func IndexGitRepo(opts Options) error {
-	repo, err := git.OpenRepository(opts.BuildOptions.RepoDir)
+	repo, err := git.PlainOpen(opts.BuildOptions.RepoDir)
 	if err != nil {
 		return err
 	}
@@ -324,7 +328,7 @@
 	branchMap := map[FileKey][]string{}
 
 	// Branch => Repo => SHA1
-	branchVersions := map[string]map[string]git.Oid{}
+	branchVersions := map[string]map[string]plumbing.Hash{}
 
 	branches, err := expandBranches(repo, opts.Branches, opts.BranchPrefix)
 	if err != nil {
@@ -341,17 +345,15 @@
 		if err != nil {
 			return err
 		}
-		defer commit.Free()
 		opts.BuildOptions.RepositoryDescription.Branches = append(opts.BuildOptions.RepositoryDescription.Branches, zoekt.RepositoryBranch{
 			Name:    b,
-			Version: commit.Id().String(),
+			Version: commit.Hash.String(),
 		})
 
 		tree, err := commit.Tree()
 		if err != nil {
 			return err
 		}
-		defer tree.Free()
 
 		files, subVersions, err := TreeToFiles(repo, tree, opts.BuildOptions.RepositoryDescription.URL, repoCache)
 		if err != nil {
@@ -417,22 +419,40 @@
 		keys := fileKeys[name]
 		for _, key := range keys {
 			brs := branchMap[key]
-			blob, err := repos[key].Repo.LookupBlob(&key.ID)
+			blob, err := repos[key].Repo.BlobObject(key.ID)
 			if err != nil {
 				return err
 			}
 
-			if blob.Size() > int64(opts.BuildOptions.SizeMax) {
+			if blob.Size > int64(opts.BuildOptions.SizeMax) {
 				continue
 			}
 
+			contents, err := blobContents(blob)
+			if err != nil {
+				return err
+			}
 			builder.Add(zoekt.Document{
 				SubRepositoryPath: key.SubRepoPath,
 				Name:              key.FullPath(),
-				Content:           blob.Contents(),
+				Content:           contents,
 				Branches:          brs,
 			})
 		}
 	}
 	return builder.Finish()
 }
+
+func blobContents(blob *object.Blob) ([]byte, error) {
+	r, err := blob.Reader()
+	if err != nil {
+		return nil, err
+	}
+	defer r.Close()
+
+	c, err := ioutil.ReadAll(r)
+	if err != nil {
+		return nil, err
+	}
+	return c, nil
+}
diff --git a/gitindex/submodule.go b/gitindex/submodule.go
index a6c3f85..ac05e6f 100644
--- a/gitindex/submodule.go
+++ b/gitindex/submodule.go
@@ -15,11 +15,9 @@
 package gitindex
 
 import (
-	"io/ioutil"
-	"os"
-	"regexp"
+	"bytes"
 
-	"github.com/libgit2/git2go"
+	"gopkg.in/src-d/go-git.v4/plumbing/format/config"
 )
 
 type SubmoduleEntry struct {
@@ -28,64 +26,36 @@
 	Branch string
 }
 
-const submodREStr = "^submodule.([^.]*)\\.(.*)"
-
-var submodRE = regexp.MustCompile(submodREStr)
-
 // ParseGitModules parses the contents of a .gitmodules file.
 func ParseGitModules(content []byte) (map[string]*SubmoduleEntry, error) {
-	base, err := git.NewConfig()
-	if err != nil {
-		return nil, err
-	}
-	defer base.Free()
+	dec := config.NewDecoder(bytes.NewBuffer(content))
+	cfg := &config.Config{}
 
-	// git2go has no API for parsing from contents. Sigh.
-	f, err := ioutil.TempFile("", "")
-	if err != nil {
-		return nil, err
-	}
-	defer os.Remove(f.Name())
-	if _, err := f.Write(content); err != nil {
-		return nil, err
-	}
-	f.Close()
-
-	cfg, err := git.OpenOndisk(base, f.Name())
-	if err != nil {
-		return nil, err
-	}
-	defer cfg.Free()
-	iter, err := cfg.NewIteratorGlob(submodREStr)
-	if err != nil {
+	if err := dec.Decode(cfg); err != nil {
 		return nil, err
 	}
 
 	result := map[string]*SubmoduleEntry{}
-	for {
-		entry, err := iter.Next()
-		if err != nil {
-			if ge, ok := err.(*git.GitError); ok && ge.Code == git.ErrIterOver {
-				break
+	for _, s := range cfg.Sections {
+		if s.Name != "submodule" {
+			continue
+		}
+
+		for _, ss := range s.Subsections {
+			name := ss.Name
+			e := &SubmoduleEntry{}
+			for _, o := range ss.Options {
+				switch o.Key {
+				case "branch":
+					e.Branch = o.Value
+				case "path":
+					e.Path = o.Value
+				case "url":
+					e.URL = o.Value
+				}
 			}
-			return nil, err
-		}
 
-		m := submodRE.FindStringSubmatch(entry.Name)
-
-		name := m[1]
-		if result[name] == nil {
-			result[name] = &SubmoduleEntry{}
-		}
-
-		e := result[name]
-		switch m[2] {
-		case "branch":
-			e.Branch = entry.Value
-		case "path":
-			e.Path = entry.Value
-		case "url":
-			e.URL = entry.Value
+			result[name] = e
 		}
 	}
 
diff --git a/gitindex/tree.go b/gitindex/tree.go
index a9d9efa..9fbd180 100644
--- a/gitindex/tree.go
+++ b/gitindex/tree.go
@@ -16,13 +16,18 @@
 
 import (
 	"fmt"
+	"io"
 	"log"
 	"net/url"
 	"path"
 	"path/filepath"
 	"strings"
 
-	git "github.com/libgit2/git2go"
+	"gopkg.in/src-d/go-git.v4/plumbing"
+	"gopkg.in/src-d/go-git.v4/plumbing/filemode"
+	"gopkg.in/src-d/go-git.v4/plumbing/object"
+
+	git "gopkg.in/src-d/go-git.v4"
 )
 
 // repoWalker walks a tree, recursing into submodules.
@@ -36,7 +41,7 @@
 	submodules map[string]*SubmoduleEntry
 
 	// Path => commit SHA1
-	subRepoVersions map[string]git.Oid
+	subRepoVersions map[string]plumbing.Hash
 	err             error
 	repoCache       *RepoCache
 
@@ -66,21 +71,20 @@
 		repoURL:                 u,
 		tree:                    map[FileKey]BlobLocation{},
 		repoCache:               repoCache,
-		subRepoVersions:         map[string]git.Oid{},
+		subRepoVersions:         map[string]plumbing.Hash{},
 		ignoreMissingSubmodules: true,
 	}
 }
 
 // parseModuleMap initializes rw.submodules.
-func (rw *repoWalker) parseModuleMap(t *git.Tree) error {
-	modEntry := t.EntryByName(".gitmodules")
+func (rw *repoWalker) parseModuleMap(t *object.Tree) error {
+	modEntry, _ := t.File(".gitmodules")
 	if modEntry != nil {
-		blob, err := rw.repo.LookupBlob(modEntry.Id)
+		c, err := blobContents(&modEntry.Blob)
 		if err != nil {
 			return err
 		}
-
-		mods, err := ParseGitModules(blob.Contents())
+		mods, err := ParseGitModules(c)
 		if err != nil {
 			return err
 		}
@@ -95,22 +99,29 @@
 // TreeToFiles fetches the blob SHA1s for a tree. If repoCache is
 // non-nil, recurse into submodules. In addition, it returns a mapping
 // that indicates in which repo each SHA1 can be found.
-func TreeToFiles(r *git.Repository, t *git.Tree,
-	repoURL string, repoCache *RepoCache) (map[FileKey]BlobLocation, map[string]git.Oid, error) {
-	ref := newRepoWalker(r, repoURL, repoCache)
+func TreeToFiles(r *git.Repository, t *object.Tree,
+	repoURL string, repoCache *RepoCache) (map[FileKey]BlobLocation, map[string]plumbing.Hash, error) {
+	rw := newRepoWalker(r, repoURL, repoCache)
 
-	if err := ref.parseModuleMap(t); err != nil {
+	if err := rw.parseModuleMap(t); err != nil {
 		return nil, nil, err
 	}
 
-	t.Walk(ref.cbInt)
-	if ref.err != nil {
-		return nil, nil, ref.err
+	tw := object.NewTreeWalker(t, true, make(map[plumbing.Hash]bool))
+	defer tw.Close()
+	for {
+		name, entry, err := tw.Next()
+		if err == io.EOF {
+			break
+		}
+		if err := rw.handleEntry(name, &entry); err != nil {
+			return nil, nil, err
+		}
 	}
-	return ref.tree, ref.subRepoVersions, nil
+	return rw.tree, rw.subRepoVersions, nil
 }
 
-func (r *repoWalker) tryHandleSubmodule(p string, id *git.Oid) error {
+func (r *repoWalker) tryHandleSubmodule(p string, id *plumbing.Hash) error {
 	err := r.handleSubmodule(p, id)
 	if r.ignoreMissingSubmodules && err != nil {
 		log.Printf("submodule %s: ignoring error %v", p, err)
@@ -120,7 +131,7 @@
 	return nil
 }
 
-func (r *repoWalker) handleSubmodule(p string, id *git.Oid) error {
+func (r *repoWalker) handleSubmodule(p string, id *plumbing.Hash) error {
 	submod := r.submodules[p]
 	if submod == nil {
 		return fmt.Errorf("no entry for submodule path %q", r.repoURL)
@@ -136,24 +147,17 @@
 		return err
 	}
 
-	obj, err := subRepo.Lookup(id)
+	obj, err := subRepo.CommitObject(*id)
 	if err != nil {
 		return err
 	}
-	defer obj.Free()
+	tree, err := subRepo.TreeObject(obj.TreeHash)
+	if err != nil {
+		return err
+	}
 
 	r.subRepoVersions[p] = *id
-	treeObj, err := obj.Peel(git.ObjectTree)
-	if err != nil {
-		return err
-	}
-	if treeObj != obj {
-		defer treeObj.Free()
-	}
-	tree, err := treeObj.AsTree()
-	if err != nil {
-		return err
-	}
+
 	subTree, subVersions, err := TreeToFiles(subRepo, tree, subURL.String(), r.repoCache)
 	if err != nil {
 		return err
@@ -172,26 +176,22 @@
 }
 
 // cb is the git2go callback
-func (r *repoWalker) cb(n string, e *git.TreeEntry) error {
-	p := filepath.Join(n, e.Name)
-	if e.Type == git.ObjectCommit && r.repoCache != nil {
-		if err := r.tryHandleSubmodule(p, e.Id); err != nil {
+func (r *repoWalker) handleEntry(p string, e *object.TreeEntry) error {
+	if e.Mode == filemode.Submodule && r.repoCache != nil {
+		if err := r.tryHandleSubmodule(p, &e.Hash); err != nil {
 			return fmt.Errorf("submodule %s: %v", p, err)
 		}
 	}
 
-	switch e.Filemode {
-	case git.FilemodeBlob, git.FilemodeBlobExecutable:
+	switch e.Mode {
+	case filemode.Regular, filemode.Executable:
 	default:
 		return nil
 	}
 
-	if e.Type != git.ObjectBlob {
-		return nil
-	}
 	r.tree[FileKey{
 		Path: p,
-		ID:   *e.Id,
+		ID:   e.Hash,
 	}] = BlobLocation{
 		Repo: r.repo,
 		URL:  r.repoURL,
@@ -199,22 +199,12 @@
 	return nil
 }
 
-// cbInt is the callback suitable for use with git2go.
-func (r *repoWalker) cbInt(n string, e *git.TreeEntry) int {
-	err := r.cb(n, e)
-	if err != nil {
-		r.err = err
-		return 1
-	}
-	return 0
-}
-
 // FileKey describes a blob at a location in the final tree. We also
 // record the subrepository from where it came.
 type FileKey struct {
 	SubRepoPath string
 	Path        string
-	ID          git.Oid
+	ID          plumbing.Hash
 }
 
 func (k *FileKey) FullPath() string {
@@ -227,11 +217,10 @@
 	URL  *url.URL
 }
 
-func (l *BlobLocation) Blob(id *git.Oid) ([]byte, error) {
-	blob, err := l.Repo.LookupBlob(id)
+func (l *BlobLocation) Blob(id *plumbing.Hash) ([]byte, error) {
+	blob, err := l.Repo.BlobObject(*id)
 	if err != nil {
 		return nil, err
 	}
-	defer blob.Free()
-	return blob.Contents(), nil
+	return blobContents(blob)
 }
diff --git a/gitindex/tree_test.go b/gitindex/tree_test.go
index 2650ac9..30e7e43 100644
--- a/gitindex/tree_test.go
+++ b/gitindex/tree_test.go
@@ -31,6 +31,8 @@
 	"github.com/google/zoekt/build"
 	"github.com/google/zoekt/query"
 	"github.com/google/zoekt/shards"
+
+	"gopkg.in/src-d/go-git.v4/plumbing"
 )
 
 func createSubmoduleRepo(dir string) error {
@@ -101,13 +103,16 @@
 		t.Fatalf("Open: %v", err)
 	}
 
-	obj, err := repo.RevparseSingle("HEAD:")
+	headRef, err := repo.Head()
 	if err != nil {
 		t.Fatalf("HEAD tree: %v", err)
 	}
+	commit, err := repo.CommitObject(headRef.Hash())
+	if err != nil {
+		t.Fatalf("commit obj HEAD: %v", err)
+	}
 
-	defer obj.Free()
-	tree, err := obj.AsTree()
+	tree, err := repo.TreeObject(commit.TreeHash)
 	if err != nil {
 		t.Fatalf("AsTree: %v", err)
 	}
@@ -116,9 +121,13 @@
 	if err != nil {
 		t.Fatalf("TreeToFiles: %v", err)
 	}
+	var bnameHash plumbing.Hash
 
-	if e, v := tree.EntryByName("bname"), versions["bname"]; e == nil || bytes.Compare(e.Id[:], v[:]) != 0 {
-		t.Fatalf("got 'bname' versions %v, want %v", v, e)
+	bnameHash = versions["bname"]
+	if entry, err := tree.FindEntry("bname"); err != nil {
+		t.Fatalf("FindEntry %v", err)
+	} else if bytes.Compare(bnameHash[:], entry.Hash[:]) != 0 {
+		t.Fatalf("got 'bname' versions %v, want %v", bnameHash, entry.Hash)
 	}
 
 	var paths []string
@@ -181,7 +190,7 @@
 	}
 
 	if len(results.Files) != 1 {
-		t.Fatalf("got %v, want 1 file", results.Files)
+		t.Fatalf("got search result %v, want 1 file", results.Files)
 	}
 
 	file := results.Files[0]