--- /dev/null
+/*
+goredo -- djb's redo implementation on pure Go
+Copyright (C) 2020-2022 Sergey Matveev <stargrave@stargrave.org>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, version 3 of the License.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+package main
+
+import (
+ "errors"
+ "io"
+ "os"
+ "path"
+ "strings"
+
+ "go.cypherpunks.ru/recfile"
+)
+
+func depFix(root string) error {
+ tracef(CDebug, "depfix: entering %s", root)
+ dir, err := os.Open(root)
+ if err != nil {
+ return err
+ }
+ defer dir.Close()
+ for {
+ fis, err := dir.Readdir(1 << 10)
+ if err != nil {
+ if err == io.EOF {
+ break
+ }
+ return err
+ }
+ for _, fi := range fis {
+ if fi.IsDir() {
+ if err = depFix(path.Join(root, fi.Name())); err != nil {
+ return err
+ }
+ }
+ }
+ }
+ dir.Close()
+
+ redoDir := path.Join(root, RedoDir)
+ dir, err = os.Open(redoDir)
+ if err != nil {
+ if os.IsNotExist(err) {
+ return nil
+ }
+ return err
+ }
+ defer dir.Close()
+ redoDirChanged := false
+ for {
+ fis, err := dir.Readdir(1 << 10)
+ if err != nil {
+ if err == io.EOF {
+ break
+ }
+ return err
+ }
+ for _, fi := range fis {
+ if !strings.HasSuffix(fi.Name(), DepSuffix) {
+ continue
+ }
+ tracef(CDebug, "depfix: checking %s/%s", root, fi.Name())
+ fdDepPath := path.Join(redoDir, fi.Name())
+ fdDep, err := os.Open(fdDepPath)
+ if err != nil {
+ return err
+ }
+ defer fdDep.Close()
+ r := recfile.NewReader(fdDep)
+ var fieldses [][]recfile.Field
+ depChanged := false
+ for {
+ fields, err := r.Next()
+ if err != nil {
+ if errors.Is(err, io.EOF) {
+ break
+ }
+ return err
+ }
+ fieldses = append(fieldses, fields)
+ m := make(map[string]string, len(fields))
+ for _, f := range fields {
+ m[f.Name] = f.Value
+ }
+ if m["Type"] != DepTypeIfchange {
+ continue
+ }
+ dep := m["Target"]
+ if dep == "" {
+ return ErrMissingTarget
+ }
+ tracef(CDebug, "depfix: checking %s/%s -> %s", root, fi.Name(), dep)
+ theirInode, err := inodeFromRec(m)
+ if err != nil {
+ return err
+ }
+ theirHsh := m["Hash"]
+ fd, err := os.Open(path.Join(root, dep))
+ if err != nil {
+ if os.IsNotExist(err) {
+ tracef(
+ CDebug, "depfix: %s/%s -> %s: not exists",
+ root, fi.Name(), dep,
+ )
+ continue
+ }
+ return err
+ }
+ inode, err := inodeFromFile(fd)
+ if err != nil {
+ return err
+ }
+ if inode.Size != theirInode.Size {
+ tracef(
+ CDebug, "depfix: %s/%s -> %s: size differs",
+ root, fi.Name(), dep,
+ )
+ fd.Close()
+ continue
+ }
+ if inode.Equals(theirInode) {
+ tracef(
+ CDebug, "depfix: %s/%s -> %s: inode is equal",
+ root, fi.Name(), dep,
+ )
+ fd.Close()
+ continue
+ }
+ hsh, err := fileHash(fd)
+ fd.Close()
+ if err != nil {
+ return err
+ }
+ if hsh != theirHsh {
+ tracef(
+ CDebug, "depfix: %s/%s -> %s: hash differs",
+ root, fi.Name(), dep,
+ )
+ continue
+ }
+ fields = []recfile.Field{
+ {Name: "Type", Value: DepTypeIfchange},
+ {Name: "Target", Value: dep},
+ {Name: "Hash", Value: hsh},
+ }
+ fields = append(fields, inode.RecfileFields()...)
+ fieldses[len(fieldses)-1] = fields
+ tracef(
+ CDebug, "depfix: %s/%s -> %s: inode updated",
+ root, fi.Name(), dep,
+ )
+ depChanged = true
+ }
+ fdDep.Close()
+ if !depChanged {
+ continue
+ }
+ redoDirChanged = true
+ fdDep, err = tempfile(redoDir, fi.Name())
+ if err != nil {
+ return err
+ }
+ defer fdDep.Close()
+ tracef(
+ CDebug, "depfix: %s/%s: tmp %s",
+ root, fi.Name(), fdDep.Name(),
+ )
+ w := recfile.NewWriter(fdDep)
+ if _, err := w.WriteFields(fieldses[0]...); err != nil {
+ return err
+ }
+ fieldses = fieldses[1:]
+ for _, fields := range fieldses {
+ if _, err := w.RecordStart(); err != nil {
+ return err
+ }
+ if _, err := w.WriteFields(fields...); err != nil {
+ return err
+ }
+ }
+ if !NoSync {
+ if err = fdDep.Sync(); err != nil {
+ return err
+ }
+ }
+ fdDep.Close()
+ if err = os.Rename(fdDep.Name(), fdDepPath); err != nil {
+ return err
+ }
+ tracef(CRedo, "%s", fdDepPath)
+ }
+ }
+ if redoDirChanged && !NoSync {
+ if err = syncDir(redoDir); err != nil {
+ return nil
+ }
+ }
+ return nil
+}
d = `Usage: redo-affects target [...]
List all targets that will be affected by changing the specified ones.`
+ case CmdNameRedoDepFix:
+ d = `Usage: redo-depfix
+
+Traverse over all .redo directories beneath and check if inode's information
+(ctime/mtime) differs. Update dependency if file's content is still the same.`
default:
d = `Usage: goredo -symlinks
goredo expects to be called through the symbolic link to it.
Available commands: redo, redo-affects, redo-always, redo-cleanup,
-redo-dot, redo-ifchange, redo-ifcreate, redo-log, redo-ood,
-redo-sources, redo-stamp, redo-targets, redo-whichdo.`
+redo-depfix, redo-dot, redo-ifchange, redo-ifcreate, redo-log,
+redo-ood, redo-sources, redo-stamp, redo-targets, redo-whichdo.`
}
fmt.Fprintf(os.Stderr, "%s\n\nCommon options:\n", d)
flag.PrintDefaults()