blob: e8b91a33898be92c03ee71ba9d66fd1159773bec [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.
// populate holds the code to augment a partial R/W checkout with a
// symlink forest into a SlothFS workspace.
package populate
import (
"bytes"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
)
// symlinkRepo creates symlinks for all the files in `child`.
func symlinkRepo(name string, child *repoTree, roRoot, rwRoot string) error {
fi, err := os.Stat(filepath.Join(rwRoot, name))
if err == nil && fi.IsDir() {
return nil
}
for e := range child.entries {
dest := filepath.Join(rwRoot, name, e)
if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil {
return err
}
if err := os.Symlink(filepath.Join(roRoot, name, e), dest); err != nil {
return err
}
}
return nil
}
// createTreeLinks tries to short-cut symlinks for whole trees by
// symlinking to the root of a repository in the RO tree.
func createTreeLinks(ro, rw *repoTree, roRoot, rwRoot string) error {
allRW := rw.allChildren()
outer:
for nm, ch := range ro.children {
foundCheckout := false
foundRecurse := false
for k := range allRW {
if k == "" {
continue
}
if nm == k {
foundRecurse = true
break
}
rel, err := filepath.Rel(nm, k)
if err != nil {
return err
}
if strings.HasPrefix(rel, "..") {
continue
}
// we have a checkout below "nm".
foundCheckout = true
break
}
switch {
case foundRecurse:
if err := createTreeLinks(ch, rw.children[nm], filepath.Join(roRoot, nm), filepath.Join(rwRoot, nm)); err != nil {
return err
}
continue outer
case !foundCheckout:
dest := filepath.Join(rwRoot, nm)
if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil {
return err
}
if err := os.Symlink(filepath.Join(roRoot, nm), dest); err != nil {
return err
}
}
}
return nil
}
// createLinks will populate a RW tree with symlinks to the RO tree.
func createLinks(ro, rw *repoTree, roRoot, rwRoot string) error {
if err := createTreeLinks(ro, rw, roRoot, rwRoot); err != nil {
return err
}
rwc := rw.allChildren()
for nm, ch := range ro.allChildren() {
if _, ok := rwc[nm]; !ok {
if err := symlinkRepo(nm, ch, roRoot, rwRoot); err != nil {
return err
}
}
}
return nil
}
// clearLinks removes all symlinks to the RO tree. It returns the workspace name that was linked before.
func clearLinks(mount, dir string) (string, error) {
mount = filepath.Clean(mount)
var prefix string
var dirs []string
if err := filepath.Walk(dir, func(n string, fi os.FileInfo, err error) error {
if fi == nil {
return fmt.Errorf("Walk %s: nil fileinfo for %s", dir, n)
}
if fi.Mode()&os.ModeSymlink != 0 {
target, err := os.Readlink(n)
if err != nil {
return err
}
if strings.HasPrefix(target, mount) {
prefix = target
if err := os.Remove(n); err != nil {
return err
}
}
}
if fi.IsDir() && n != dir {
dirs = append(dirs, n)
}
return nil
}); err != nil {
return "", fmt.Errorf("Walk %s: %v", dir, err)
}
// Reverse the ordering, so we get the deepest subdirs first.
sort.Strings(dirs)
for i := range dirs {
d := dirs[len(dirs)-1-i]
// Ignore error: dir may still contain entries.
os.Remove(d)
}
prefix = strings.TrimPrefix(prefix, mount+"/")
if i := strings.Index(prefix, "/"); i != -1 {
prefix = prefix[:i]
}
return prefix, nil
}
// Returns the filenames (as relative paths) in newDir that have
// changed relative to the files in oldDir.
func changedFiles(oldInfos map[string]*fileInfo, newInfos map[string]*fileInfo) ([]string, error) {
var changed []string
for path, info := range newInfos {
old, ok := oldInfos[path]
if !ok {
changed = append(changed, path)
continue
}
if info.isLink {
// TODO(hanwen): maybe we should we deref the link?
continue
}
if old.sha1 == nil || info.sha1 == nil {
changed = append(changed, path)
continue
}
if bytes.Compare(old.sha1[:], info.sha1[:]) != 0 {
changed = append(changed, path)
continue
}
}
sort.Strings(changed)
return changed, nil
}
// Checkout updates a RW dir with new symlinks to the given RO dir.
// Returns the files that should be touched.
func Checkout(ro, rw string) ([]string, error) {
ro = filepath.Clean(ro)
wsName, err := clearLinks(filepath.Dir(ro), rw)
if err != nil {
return nil, err
}
oldRoot := filepath.Join(filepath.Dir(ro), wsName)
// Do the file system traversals in parallel.
errs := make(chan error, 3)
var rwTree, roTree *repoTree
var oldInfos map[string]*fileInfo
if wsName != "" {
go func() {
t, err := repoTreeFromSlothFS(oldRoot)
if t != nil {
oldInfos = t.allFiles()
}
errs <- err
}()
} else {
oldInfos = map[string]*fileInfo{}
errs <- nil
}
go func() {
t, err := newRepoTree(rw)
rwTree = t
errs <- err
}()
go func() {
t, err := repoTreeFromSlothFS(ro)
roTree = t
errs <- err
}()
for i := 0; i < cap(errs); i++ {
err := <-errs
if err != nil {
return nil, err
}
}
if err := createLinks(roTree, rwTree, ro, rw); err != nil {
return nil, err
}
newInfos := roTree.allFiles()
changed, err := changedFiles(oldInfos, newInfos)
if err != nil {
return nil, fmt.Errorf("changedFiles: %v", err)
}
for i, p := range changed {
changed[i] = filepath.Join(ro, p)
}
return changed, nil
}