blob: 145cfd69da85dd3fe06263928abf2e06eaf566e6 [file] [log] [blame]
// Copyright 2016 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 gitindex
import (
"fmt"
"io"
"log"
"net/url"
"path"
"path/filepath"
"strings"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/filemode"
"github.com/go-git/go-git/v5/plumbing/object"
git "github.com/go-git/go-git/v5"
)
// repoWalker walks a tree, recursing into submodules.
type repoWalker struct {
repo *git.Repository
repoURL *url.URL
tree map[fileKey]BlobLocation
// Path => SubmoduleEntry
submodules map[string]*SubmoduleEntry
// Path => commit SHA1
subRepoVersions map[string]plumbing.Hash
repoCache *RepoCache
}
// subURL returns the URL for a submodule.
func (w *repoWalker) subURL(relURL string) (*url.URL, error) {
if w.repoURL == nil {
return nil, fmt.Errorf("no URL for base repo")
}
if strings.HasPrefix(relURL, "../") {
u := *w.repoURL
u.Path = path.Join(u.Path, relURL)
return &u, nil
}
return url.Parse(relURL)
}
// newRepoWalker creates a new repoWalker.
func newRepoWalker(r *git.Repository, repoURL string, repoCache *RepoCache) *repoWalker {
u, _ := url.Parse(repoURL)
return &repoWalker{
repo: r,
repoURL: u,
tree: map[fileKey]BlobLocation{},
repoCache: repoCache,
subRepoVersions: map[string]plumbing.Hash{},
}
}
// parseModuleMap initializes rw.submodules.
func (rw *repoWalker) parseModuleMap(t *object.Tree) error {
modEntry, _ := t.File(".gitmodules")
if modEntry != nil {
c, err := blobContents(&modEntry.Blob)
if err != nil {
return err
}
mods, err := ParseGitModules(c)
if err != nil {
return err
}
rw.submodules = map[string]*SubmoduleEntry{}
for _, entry := range mods {
rw.submodules[entry.Path] = entry
}
}
return nil
}
// 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 *object.Tree,
repoURL string, repoCache *RepoCache) (map[fileKey]BlobLocation, map[string]plumbing.Hash, error) {
rw := newRepoWalker(r, repoURL, repoCache)
if err := rw.parseModuleMap(t); err != nil {
return nil, nil, 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 rw.tree, rw.subRepoVersions, nil
}
func (r *repoWalker) tryHandleSubmodule(p string, id *plumbing.Hash) error {
if err := r.handleSubmodule(p, id); err != nil {
log.Printf("submodule %s: ignoring error %v", p, err)
}
return nil
}
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)
}
subURL, err := r.subURL(submod.URL)
if err != nil {
return err
}
subRepo, err := r.repoCache.Open(subURL)
if err != nil {
return err
}
obj, err := subRepo.CommitObject(*id)
if err != nil {
return err
}
tree, err := subRepo.TreeObject(obj.TreeHash)
if err != nil {
return err
}
r.subRepoVersions[p] = *id
subTree, subVersions, err := TreeToFiles(subRepo, tree, subURL.String(), r.repoCache)
if err != nil {
return err
}
for k, repo := range subTree {
r.tree[fileKey{
SubRepoPath: filepath.Join(p, k.SubRepoPath),
Path: k.Path,
ID: k.ID,
}] = repo
}
for k, v := range subVersions {
r.subRepoVersions[filepath.Join(p, k)] = v
}
return 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.Mode {
case filemode.Regular, filemode.Executable:
default:
return nil
}
r.tree[fileKey{
Path: p,
ID: e.Hash,
}] = BlobLocation{
Repo: r.repo,
URL: r.repoURL,
}
return nil
}
// 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 plumbing.Hash
}
func (k *fileKey) FullPath() string {
return filepath.Join(k.SubRepoPath, k.Path)
}
// BlobLocation holds data where a blob can be found.
type BlobLocation struct {
Repo *git.Repository
URL *url.URL
}
func (l *BlobLocation) Blob(id *plumbing.Hash) ([]byte, error) {
blob, err := l.Repo.BlobObject(*id)
if err != nil {
return nil, err
}
return blobContents(blob)
}