func doubleWalkDiff(ctx context.Context, changeFn ChangeFunc, a, b string) (err error) {
	g, ctx := errgroup.WithContext(ctx)
	var (
		c1 = make(chan *currentPath)
		c2 = make(chan *currentPath)
		f1, f2 *currentPath
		rmdir  string
	)
	g.Go(func() error {
		defer close(c1)
		return pathWalk(ctx, a, c1)
	})
	g.Go(func() error {
		defer close(c2)
		return pathWalk(ctx, b, c2)
	})
	g.Go(func() error {
		for c1 != nil || c2 != nil {
			if f1 == nil && c1 != nil {
				f1, err = nextPath(ctx, c1)
				if err != nil {
					return err
				}
				if f1 == nil {
					c1 = nil
				}
			}
			if f2 == nil && c2 != nil {
				f2, err = nextPath(ctx, c2)
				if err != nil {
					return err
				}
				if f2 == nil {
					c2 = nil
				}
			}
			if f1 == nil && f2 == nil {
				continue
			}
			var f os.FileInfo
			k, p := pathChange(f1, f2)
			switch k {
			case ChangeKindAdd:
				if rmdir != "" {
					rmdir = ""
				}
				f = f2.f
				f2 = nil
			case ChangeKindDelete:
				if rmdir != "" && strings.HasPrefix(f1.path, rmdir) {
					f1 = nil
					continue
				} else if f1.f.IsDir() {
					rmdir = f1.path + string(os.PathSeparator)
				} else if rmdir != "" {
					rmdir = ""
				}
				f1 = nil
			case ChangeKindModify:
				same, err := sameFile(f1, f2)
				if err != nil {
					return err
				}
				if f1.f.IsDir() && !f2.f.IsDir() {
					rmdir = f1.path + string(os.PathSeparator)
				} else if rmdir != "" {
					rmdir = ""
				}
				f = f2.f
				f1 = nil
				f2 = nil
				if same {
					if !isLinked(f) {
						continue
					}
					k = ChangeKindUnmodified
				}
			}
			if err := changeFn(k, p, f, nil); err != nil {
				return err
			}
		}
		return nil
	})
	return g.Wait()
}
doubleWalkDiff walks both directories to create a diff
package fs
import (
	"context"
	"os"
	"path/filepath"
	"strings"
	"golang.org/x/sync/errgroup"
	"github.com/sirupsen/logrus"
)
type ChangeKind int
const (
	ChangeKindUnmodified = iota
	ChangeKindAdd
	ChangeKindModify
	ChangeKindDelete
)
func (k ChangeKind) String() string {
	switch k {
	case ChangeKindUnmodified:
		return "unmodified"
	case ChangeKindAdd:
		return "add"
	case ChangeKindModify:
		return "modify"
	case ChangeKindDelete:
		return "delete"
	default:
		return ""
	}
}
type Change struct {
	Kind ChangeKind
	Path string
}
type ChangeFunc func(ChangeKind, string, os.FileInfo, error) error
func Changes(ctx context.Context, a, b string, changeFn ChangeFunc) error {
	if a == "" {
		logrus.Debugf("Using single walk diff for %s", b)
		return addDirChanges(ctx, changeFn, b)
	} else if diffOptions := detectDirDiff(b, a); diffOptions != nil {
		logrus.Debugf("Using single walk diff for %s from %s", diffOptions.diffDir, a)
		return diffDirChanges(ctx, changeFn, a, diffOptions)
	}
	logrus.Debugf("Using double walk diff for %s from %s", b, a)
	return doubleWalkDiff(ctx, changeFn, a, b)
}
func addDirChanges(ctx context.Context, changeFn ChangeFunc, root string) error {
	return filepath.Walk(root, func(path string, f os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		path, err = filepath.Rel(root, path)
		if err != nil {
			return err
		}
		path = filepath.Join(string(os.PathSeparator), path)
		if path == string(os.PathSeparator) {
			return nil
		}
		return changeFn(ChangeKindAdd, path, f, nil)
	})
}
type diffDirOptions struct {
	diffDir      string
	skipChange   func(string) (bool, error)
	deleteChange func(string, string, os.FileInfo) (string, error)
}
func diffDirChanges(ctx context.Context, changeFn ChangeFunc, base string, o *diffDirOptions) error {
	changedDirs := make(map[string]struct{})
	return filepath.Walk(o.diffDir, func(path string, f os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		path, err = filepath.Rel(o.diffDir, path)
		if err != nil {
			return err
		}
		path = filepath.Join(string(os.PathSeparator), path)
		if path == string(os.PathSeparator) {
			return nil
		}
		if o.skipChange != nil {
			if skip, err := o.skipChange(path); skip {
				return err
			}
		}
		var kind ChangeKind
		deletedFile, err := o.deleteChange(o.diffDir, path, f)
		if err != nil {
			return err
		}
		if deletedFile != "" {
			path = deletedFile
			kind = ChangeKindDelete
			f = nil
		} else {
			kind = ChangeKindAdd
			stat, err := os.Stat(filepath.Join(base, path))
			if err != nil && !os.IsNotExist(err) {
				return err
			}
			if err == nil {
				if stat.IsDir() && f.IsDir() {
					if f.Size() == stat.Size() && f.Mode() == stat.Mode() && sameFsTime(f.ModTime(), stat.ModTime()) {
						return nil
					}
				}
				kind = ChangeKindModify
			}
		}
		if f.IsDir() {
			changedDirs[path] = struct{}{}
		}
		if kind == ChangeKindAdd || kind == ChangeKindDelete {
			parent := filepath.Dir(path)
			if _, ok := changedDirs[parent]; !ok && parent != "/" {
				pi, err := os.Stat(filepath.Join(o.diffDir, parent))
				if err := changeFn(ChangeKindModify, parent, pi, err); err != nil {
					return err
				}
				changedDirs[parent] = struct{}{}
			}
		}
		return changeFn(kind, path, f, nil)
	})
}