blob: 2e72a016c4dd0fe565a3efb731846f25d2617a86 [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 fs
import (
"fmt"
"log"
"path/filepath"
"strings"
"github.com/google/gitfs/cache"
"github.com/google/gitfs/gitiles"
"github.com/google/gitfs/manifest"
"github.com/hanwen/go-fuse/fuse/nodefs"
git "github.com/libgit2/git2go"
)
type manifestFSRoot struct {
nodefs.Node
service *gitiles.Service
cache *cache.Cache
trees map[string]*gitiles.Tree
options ManifestOptions
}
// NewManifestFS creates a Manifest FS root node.
func NewManifestFS(service *gitiles.Service, cache *cache.Cache, opts ManifestOptions) (nodefs.Node, error) {
root := &manifestFSRoot{
Node: nodefs.NewDefaultNode(),
cache: cache,
service: service,
options: opts,
}
for _, p := range opts.Manifest.Project {
if _, err := git.NewOid(p.Revision); err != nil {
return nil, fmt.Errorf("project %s revision %q does not parse: %v", p.Name, p.Revision, err)
}
}
var err error
root.trees, err = fetchTreeMap(cache.Tree, service, opts.Manifest)
if err != nil {
return nil, err
}
return root, nil
}
func (fs *manifestFSRoot) OnMount(fsConn *nodefs.FileSystemConnector) {
if err := fs.onMount(fsConn); err != nil {
log.Printf("onMount: %v", err)
for k := range fs.Inode().Children() {
fs.Inode().RmChild(k)
}
fs.Inode().NewChild("ERROR", false, newDataNode(err.Error()))
}
// Don't need the trees anymore.
fs.trees = nil
}
func (fs *manifestFSRoot) onMount(fsConn *nodefs.FileSystemConnector) error {
var byDepth [][]string
for p := range fs.trees {
d := len(strings.Split(p, "/"))
for len(byDepth) <= d {
byDepth = append(byDepth, nil)
}
byDepth[d] = append(byDepth[d], p)
}
revmap := map[string]*manifest.Project{}
for i, p := range fs.options.Manifest.Project {
revmap[p.Path] = &fs.options.Manifest.Project[i]
}
for _, ps := range byDepth {
for _, p := range ps {
dir, base := filepath.Split(p)
parent, left := fsConn.Node(fs.Inode(), dir)
for _, c := range left {
ch := parent.NewChild(c, true, nodefs.NewDefaultNode())
parent = ch
}
clone := true
for _, e := range fs.options.RepoCloneOption {
if e.RE.FindString(p) != "" {
clone = e.Clone
break
}
}
cloneURL := revmap[p].CloneURL
if !clone {
cloneURL = ""
}
repoService := fs.service.NewRepoService(revmap[p].Name)
opts := GitilesOptions{
Revision: revmap[p].Revision,
CloneURL: cloneURL,
CloneOption: fs.options.FileCloneOption,
}
subRoot := NewGitilesRoot(fs.cache, fs.trees[p], repoService, opts)
parent.NewChild(base, true, subRoot)
if err := subRoot.(*gitilesRoot).onMount(fsConn); err != nil {
return fmt.Errorf("onMount(%s): %v", p, err)
}
}
}
// Do Linkfile, Copyfile after setting up the repos, so we
// have directories to attach the copy/link nodes to.
for _, p := range fs.options.Manifest.Project {
for _, cp := range p.Copyfile {
srcNode, left := fsConn.Node(fs.Inode(), filepath.Join(p.Path, cp.Src))
if len(left) > 0 {
return fmt.Errorf("Copyfile(%s): source %s does not exist", p.Name, cp.Src)
}
dir, left := fsConn.Node(fs.Inode(), cp.Dest)
switch len(left) {
case 0:
return fmt.Errorf("Copyfile(%s): dest %s already exists.", p.Name, cp.Dest)
case 1:
default:
return fmt.Errorf("Copyfile(%s): directory for dest %s does not exist.", p.Name, cp.Dest)
}
dir.AddChild(left[0], srcNode)
}
for _, lf := range p.Linkfile {
dir, left := fsConn.Node(fs.Inode(), lf.Dest)
switch len(left) {
case 0:
return fmt.Errorf("Linkfile(%s): dest %s already exists.", p.Name, lf.Dest)
case 1:
default:
return fmt.Errorf("Linkfile(%s): directory for dest %s does not exist.", p.Name, lf.Dest)
}
src := filepath.Join(p.Path, lf.Src)
rel, err := filepath.Rel(filepath.Dir(lf.Dest), src)
if err != nil {
return err
}
node := newLinkNode(filepath.Join(rel))
dir.NewChild(left[0], false, node)
}
}
return nil
}
func fetchTreeMap(treeCache *cache.TreeCache, service *gitiles.Service, mf *manifest.Manifest) (map[string]*gitiles.Tree, error) {
type resultT struct {
path string
resp *gitiles.Tree
err error
}
// TODO(hanwen): if we have the repository, and commit
// locally, we should use cache.GetTree() instead of putting
// load on the remote Gitiles.
// Fetch all the trees in parallel.
out := make(chan resultT, len(mf.Project))
for _, p := range mf.Project {
go func(p manifest.Project) {
revID, err := git.NewOid(p.Revision)
if err != nil {
out <- resultT{p.Path, nil, err}
return
}
tree, err := treeCache.Get(revID)
if err != nil {
repoService := service.NewRepoService(p.Name)
tree, err = repoService.GetTree(p.Revision, "", true)
if err == nil {
if err := treeCache.Add(revID, tree); err != nil {
log.Printf("treeCache.Add: %v", err)
}
}
}
out <- resultT{p.Path, tree, err}
}(p)
}
// drain goroutines
var result []resultT
for range mf.Project {
r := <-out
result = append(result, r)
}
//
resmap := map[string]*gitiles.Tree{}
for _, r := range result {
if r.err != nil {
return nil, fmt.Errorf("Tree(%s): %v", r.path, r.err)
}
resmap[r.path] = r.resp
}
return resmap, nil
}