From 02b5f1369ce09d597336e77df98e56d467b8d1ff Mon Sep 17 00:00:00 2001 From: Alexander Larsson Date: Fri, 13 Sep 2013 12:56:58 +0200 Subject: [PATCH] Change how ChangesDirs() works Rather than scan the files in the old directory twice to detect the deletions we now scan both directories twice and then do all the diffing on the in-memory structure. This is more efficient, but it also lets us diff more complex things later that are not exact on-disk trees. --- changes.go | 201 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 143 insertions(+), 58 deletions(-) diff --git a/changes.go b/changes.go index 00c9cc7c77..d1b0a25b0d 100644 --- a/changes.go +++ b/changes.go @@ -106,50 +106,85 @@ func ChangesAUFS(layers []string, rw string) ([]Change, error) { return changes, nil } -func ChangesDirs(newDir, oldDir string) ([]Change, error) { - var changes []Change - err := filepath.Walk(newDir, func(newPath string, f os.FileInfo, err error) error { - if err != nil { - return err - } +type FileInfo struct { + parent *FileInfo + name string + stat syscall.Stat_t + children map[string]*FileInfo +} - var newStat syscall.Stat_t - err = syscall.Lstat(newPath, &newStat) - if err != nil { - return err - } +func (root *FileInfo) LookUp(path string) *FileInfo { + parent := root + if path == "/" { + return root + } - // Rebase path - relPath, err := filepath.Rel(newDir, newPath) - if err != nil { - return err - } - relPath = filepath.Join("/", relPath) - - // Skip root - if relPath == "/" || relPath == "/.docker-id" { - return nil - } - - change := Change{ - Path: relPath, - } - - oldPath := filepath.Join(oldDir, relPath) - - var oldStat = &syscall.Stat_t{} - err = syscall.Lstat(oldPath, oldStat) - if err != nil { - if !os.IsNotExist(err) { - return err + pathElements := strings.Split(path, "/") + for _, elem := range pathElements { + if elem != "" { + child := parent.children[elem] + if child == nil { + return nil } - oldStat = nil + parent = child } + } + return parent +} - if oldStat == nil { - change.Kind = ChangeAdd - changes = append(changes, change) - } else { +func (info *FileInfo)path() string { + if info.parent == nil { + return "/" + } + return filepath.Join(info.parent.path(), info.name) +} + +func (info *FileInfo)unlink() { + if info.parent != nil { + delete(info.parent.children, info.name) + } +} + +func (info *FileInfo)Remove(path string) bool { + child := info.LookUp(path) + if child != nil { + child.unlink() + return true + } + return false +} + +func (info *FileInfo)isDir() bool { + return info.parent == nil || info.stat.Mode&syscall.S_IFDIR == syscall.S_IFDIR +} + + +func (info *FileInfo)addChanges(oldInfo *FileInfo, changes *[]Change) { + if oldInfo == nil { + // add + change := Change{ + Path: info.path(), + Kind: ChangeAdd, + } + *changes = append(*changes, change) + } + + // We make a copy so we can modify it to detect additions + // also, we only recurse on the old dir if the new info is a directory + // otherwise any previous delete/change is considered recursive + oldChildren := make(map[string]*FileInfo) + if oldInfo != nil && info.isDir() { + for k, v := range oldInfo.children { + oldChildren[k] = v + } + } + + for name, newChild := range info.children { + oldChild, _ := oldChildren[name] + if oldChild != nil { + // change? + oldStat := &oldChild.stat + newStat := &newChild.stat if oldStat.Ino != newStat.Ino || oldStat.Mode != newStat.Mode || oldStat.Uid != newStat.Uid || @@ -159,50 +194,100 @@ func ChangesDirs(newDir, oldDir string) ([]Change, error) { oldStat.Blocks != newStat.Blocks || oldStat.Mtim != newStat.Mtim || oldStat.Ctim != newStat.Ctim { - change.Kind = ChangeModify - changes = append(changes, change) + change := Change{ + Path: newChild.path(), + Kind: ChangeModify, + } + *changes = append(*changes, change) } + + // Remove from copy so we can detect deletions + delete(oldChildren, name) } - return nil - }) - if err != nil { - return nil, err + newChild.addChanges(oldChild, changes) } - err = filepath.Walk(oldDir, func(oldPath string, f os.FileInfo, err error) error { + for _, oldChild := range oldChildren { + // delete + change := Change{ + Path: oldChild.path(), + Kind: ChangeDelete, + } + *changes = append(*changes, change) + } + + +} + +func (info *FileInfo)Changes(oldInfo *FileInfo) []Change { + var changes []Change + + info.addChanges(oldInfo, &changes) + + return changes +} + + +func collectFileInfo(sourceDir string) (*FileInfo, error) { + root := &FileInfo { + name: "/", + children: make(map[string]*FileInfo), + } + + err := filepath.Walk(sourceDir, func(path string, f os.FileInfo, err error) error { if err != nil { return err } // Rebase path - relPath, err := filepath.Rel(oldDir, oldPath) + relPath, err := filepath.Rel(sourceDir, path) if err != nil { return err } relPath = filepath.Join("/", relPath) - // Skip root if relPath == "/" { return nil } - change := Change{ - Path: relPath, + parent := root.LookUp(filepath.Dir(relPath)) + if parent == nil { + return fmt.Errorf("collectFileInfo: Unexpectedly no parent for %s", relPath) } - newPath := filepath.Join(newDir, relPath) - - var newStat = &syscall.Stat_t{} - err = syscall.Lstat(newPath, newStat) - if err != nil && os.IsNotExist(err) { - change.Kind = ChangeDelete - changes = append(changes, change) + info := &FileInfo { + name: filepath.Base(relPath), + children: make(map[string]*FileInfo), + parent: parent, } + if err := syscall.Lstat(path, &info.stat); err != nil { + return err + } + + parent.children[info.name] = info + return nil }) if err != nil { return nil, err } - return changes, nil + return root, nil +} + +func ChangesDirs(newDir, oldDir string) ([]Change, error) { + oldRoot, err := collectFileInfo(oldDir) + if err != nil { + return nil, err + } + newRoot, err := collectFileInfo(newDir) + if err != nil { + return nil, err + } + + // Ignore changes in .docker-id + _ = newRoot.Remove("/.docker-id") + _ = oldRoot.Remove("/.docker-id") + + return newRoot.Changes(oldRoot), nil }