| // 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 populate |
| |
| import ( |
| "encoding/json" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "log" |
| "os" |
| "path/filepath" |
| "strings" |
| |
| git "github.com/libgit2/git2go" |
| |
| "github.com/google/slothfs/gitiles" |
| "github.com/hanwen/gitfs/manifest" |
| ) |
| |
| // fileInfo holds data files contained in the git repository within a |
| // repoTree node. |
| type fileInfo struct { |
| // the SHA1 of the file. This can be nil if getting it was too expensive. |
| sha1 *git.Oid |
| |
| // We can't do chtimes on symlinks. |
| isLink bool |
| } |
| |
| // repoTree is a nested set of Git repositories. |
| type repoTree struct { |
| // repositories under this repository |
| children map[string]*repoTree |
| |
| // files in this repository. |
| entries map[string]*fileInfo |
| } |
| |
| // findParentRepo recursively finds the deepest child that is a prefix |
| // to the given path. |
| func (t *repoTree) findParentRepo(path string) (*repoTree, string) { |
| for k, ch := range t.children { |
| if strings.HasPrefix(path, k+"/") { |
| return ch.findParentRepo(path[len(k+"/"):]) |
| } |
| } |
| return t, path |
| } |
| |
| // write dumps the tree for debugging purposes. |
| func (t *repoTree) write(w io.Writer, indent string) { |
| for nm, ch := range t.children { |
| fmt.Fprintf(w, "%s%s:\n", indent, nm) |
| ch.write(w, indent+" ") |
| } |
| } |
| |
| // repoTreeFromManifest creates a repoTree from a manifest XML. |
| func repoTreeFromManifest(xmlFile string) (*repoTree, error) { |
| mf, err := manifest.ParseFile(xmlFile) |
| if err != nil { |
| return nil, err |
| } |
| |
| var byDepth [][]*manifest.Project |
| for i, p := range mf.Project { |
| l := len(strings.Split(p.Path, "/")) |
| for len(byDepth) <= l { |
| byDepth = append(byDepth, nil) |
| } |
| |
| byDepth[l] = append(byDepth[l], &mf.Project[i]) |
| } |
| |
| root := makeRepoTree() |
| treesByPath := map[string]*repoTree{ |
| "": root, |
| } |
| |
| for _, projs := range byDepth { |
| for _, p := range projs { |
| childTree := makeRepoTree() |
| treesByPath[p.Path] = childTree |
| |
| parent, key := root.findParentRepo(p.Path) |
| parent.children[key] = childTree |
| } |
| } |
| |
| for _, p := range mf.Project { |
| for _, c := range p.Copyfile { |
| root.entries[c.Dest] = &fileInfo{} |
| } |
| for _, c := range p.Linkfile { |
| root.entries[c.Dest] = &fileInfo{} |
| } |
| } |
| return root, nil |
| } |
| |
| // fillFromSlothFS reads tree.json to fill Entries for this repoTree |
| // node only, and does not recurse. |
| func (t *repoTree) fillFromSlothFS(dir string) error { |
| c, err := ioutil.ReadFile(filepath.Join(dir, ".slothfs", "tree.json")) |
| if err != nil { |
| return err |
| } |
| |
| var tree gitiles.Tree |
| if err := json.Unmarshal(c, &tree); err != nil { |
| return err |
| } |
| |
| for _, e := range tree.Entries { |
| fi := &fileInfo{} |
| fi.sha1, err = git.NewOid(e.ID) |
| if err != nil { |
| return err |
| } |
| |
| t.entries[e.Name] = fi |
| |
| if e.Target != nil { |
| fi.isLink = true |
| } |
| } |
| |
| return nil |
| } |
| |
| // repoTreeFromSlothFS reads data from .slothfs to construct a fully |
| // populated repoTree tree. |
| func repoTreeFromSlothFS(dir string) (*repoTree, error) { |
| root, err := repoTreeFromManifest(filepath.Join(dir, ".slothfs", "manifest.xml")) |
| if err != nil { |
| return nil, err |
| } |
| |
| chs := root.allChildren() |
| errs := make(chan error, len(chs)) |
| for path, ch := range root.allChildren() { |
| go func(p string, t *repoTree) { |
| err := t.fillFromSlothFS(p) |
| errs <- err |
| }(filepath.Join(dir, path), ch) |
| } |
| |
| for i := 0; i < cap(errs); i++ { |
| err := <-errs |
| if err != nil { |
| return nil, err |
| } |
| } |
| return root, nil |
| } |
| |
| // makeRepoTree returns a repoTree struct with maps initialized. |
| func makeRepoTree() *repoTree { |
| return &repoTree{ |
| children: map[string]*repoTree{}, |
| entries: map[string]*fileInfo{}, |
| } |
| } |
| |
| // newRepoTree returns a repoTree constructed from filesystem data. |
| func newRepoTree(dir string) (*repoTree, error) { |
| t := makeRepoTree() |
| if err := t.fill(dir, ""); err != nil { |
| return nil, err |
| } |
| return t, nil |
| } |
| |
| // allChildren returns all the repositories (including the receiver) |
| // as a map keyed by relative path. |
| func (t *repoTree) allChildren() map[string]*repoTree { |
| r := map[string]*repoTree{"": t} |
| for nm, ch := range t.children { |
| for sub, subCh := range ch.allChildren() { |
| r[filepath.Join(nm, sub)] = subCh |
| } |
| } |
| return r |
| } |
| |
| // allFiles returns all the files below this repoTree. |
| func (t *repoTree) allFiles() map[string]*fileInfo { |
| r := map[string]*fileInfo{} |
| for nm, info := range t.entries { |
| r[nm] = info |
| } |
| for nm, ch := range t.children { |
| for sub, subCh := range ch.allFiles() { |
| r[filepath.Join(nm, sub)] = subCh |
| } |
| } |
| return r |
| } |
| |
| // returns whether path is the topdirectory of some git repository, |
| // either in plain git or in slothfs. |
| func isRepoDir(path string) bool { |
| if stat, err := os.Stat(filepath.Join(path, ".git")); err == nil && stat.IsDir() { |
| return true |
| } else if stat, err := os.Stat(filepath.Join(path, ".slothfs")); err == nil && stat.IsDir() { |
| return true |
| } |
| return false |
| } |
| |
| // construct fills `parent` looking through `dir` subdir of `repoRoot`. |
| func (parent *repoTree) fill(repoRoot, dir string) error { |
| entries, err := ioutil.ReadDir(filepath.Join(repoRoot, dir)) |
| if err != nil { |
| log.Println(repoRoot, err) |
| return err |
| } |
| |
| todo := map[string]*repoTree{} |
| for _, e := range entries { |
| if e.IsDir() && (e.Name() == ".git" || e.Name() == ".slothfs") { |
| continue |
| } |
| if e.IsDir() && e.Name() == "out" && dir == "" { |
| // Ignore the build output directory. |
| continue |
| } |
| |
| subName := filepath.Join(dir, e.Name()) |
| if e.IsDir() { |
| if newRoot := filepath.Join(repoRoot, subName); isRepoDir(newRoot) { |
| ch := makeRepoTree() |
| parent.children[subName] = ch |
| todo[newRoot] = ch |
| } else { |
| parent.fill(repoRoot, subName) |
| } |
| } else { |
| parent.entries[subName] = &fileInfo{} |
| } |
| } |
| |
| errs := make(chan error, len(todo)) |
| for newRoot, ch := range todo { |
| go func(r string, t *repoTree) { |
| errs <- t.fill(r, "") |
| }(newRoot, ch) |
| } |
| |
| for range todo { |
| err := <-errs |
| if err != nil { |
| return err |
| } |
| } |
| |
| return nil |
| } |