diff options
| author | Jonas Knobloch <jonas.knobloch@t-online.de> | 2026-04-23 18:14:50 +0200 |
|---|---|---|
| committer | Jonas Knobloch <jonas.knobloch@t-online.de> | 2026-04-23 20:21:12 +0200 |
| commit | 875ac19922932b12f6357d64a460c06d031000e7 (patch) | |
| tree | f4f268ef2b029a5e187613d6ebbec1a166615dbc /shelf | |
| parent | d35f5fad1d26fc09cfd98f6e7faf17d6a48e4e8f (diff) | |
Add shelf module
Diffstat (limited to 'shelf')
| -rw-r--r-- | shelf/cmd/sum/main.go | 32 | ||||
| -rw-r--r-- | shelf/go.mod | 16 | ||||
| -rw-r--r-- | shelf/go.sum | 22 | ||||
| -rw-r--r-- | shelf/hash.go | 28 | ||||
| -rw-r--r-- | shelf/ignore.go | 48 | ||||
| -rw-r--r-- | shelf/item.go | 9 | ||||
| -rw-r--r-- | shelf/shelf.go | 134 |
7 files changed, 289 insertions, 0 deletions
diff --git a/shelf/cmd/sum/main.go b/shelf/cmd/sum/main.go new file mode 100644 index 0000000..441a706 --- /dev/null +++ b/shelf/cmd/sum/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "flag" + "log" + "os" + + "go.jknobloc.com/x/shelf" +) + +func main() { + rootPath := flag.String("root", ".shelf", "Path to the shelf") + ignorePath := flag.String("ignore", ".shelfignore", "Path to .shelfignore") + + flag.Parse() + + shelf.Root = *rootPath + + ignore, _ := os.ReadFile(*ignorePath) + + var sum shelf.Sum + + if s, err := shelf.Index(ignore); err != nil { + log.Fatal(err) + } else { + sum = s + } + + if err := shelf.Serialize(sum, os.Stdout); err != nil { + log.Fatal(err) + } +} diff --git a/shelf/go.mod b/shelf/go.mod new file mode 100644 index 0000000..0140285 --- /dev/null +++ b/shelf/go.mod @@ -0,0 +1,16 @@ +module go.jknobloc.com/x/shelf + +go 1.25.0 + +require ( + github.com/go-git/go-git/v5 v5.18.0 + golang.org/x/sync v0.20.0 +) + +require ( + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.8.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + golang.org/x/net v0.50.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect +) diff --git a/shelf/go.sum b/shelf/go.sum new file mode 100644 index 0000000..7663167 --- /dev/null +++ b/shelf/go.sum @@ -0,0 +1,22 @@ +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0= +github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY= +github.com/go-git/go-git/v5 v5.18.0 h1:O831KI+0PR51hM2kep6T8k+w0/LIAD490gvqMCvL5hM= +github.com/go-git/go-git/v5 v5.18.0/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= diff --git a/shelf/hash.go b/shelf/hash.go new file mode 100644 index 0000000..eea81d5 --- /dev/null +++ b/shelf/hash.go @@ -0,0 +1,28 @@ +package shelf + +import ( + "crypto/sha256" + "encoding/hex" + "io" + "os" +) + +func hashFile(name string) (string, error) { + var file *os.File + + if f, err := os.Open(name); err != nil { + return "", err + } else { + file = f + } + + defer file.Close() + + hash := sha256.New() + + if _, err := io.Copy(hash, file); err != nil { + return "", err + } + + return hex.EncodeToString(hash.Sum(nil)), nil +} diff --git a/shelf/ignore.go b/shelf/ignore.go new file mode 100644 index 0000000..be061f3 --- /dev/null +++ b/shelf/ignore.go @@ -0,0 +1,48 @@ +package shelf + +import ( + "bufio" + "bytes" + "path/filepath" + "strings" + + "github.com/go-git/go-git/v5/plumbing/format/gitignore" +) + +type Ignore struct { + matcher gitignore.Matcher +} + +func newIgnore(data []byte) *Ignore { + var patterns []gitignore.Pattern + + scanner := bufio.NewScanner(bytes.NewReader(data)) + + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + patterns = append(patterns, gitignore.ParsePattern(line, nil)) + } + + return &Ignore{ + matcher: gitignore.NewMatcher(patterns), + } +} + +func (i *Ignore) Match(path string, isDir bool) bool { + parts := strings.Split(filepath.ToSlash(path), "/") + + var segments []string + + for _, p := range parts { + if p != "" { + segments = append(segments, p) + } + } + + return i.matcher.Match(segments, isDir) +} diff --git a/shelf/item.go b/shelf/item.go new file mode 100644 index 0000000..f026d57 --- /dev/null +++ b/shelf/item.go @@ -0,0 +1,9 @@ +package shelf + +import "path" + +type Item string + +func Abs(item Item) string { + return path.Join(Root, string(item)) +} diff --git a/shelf/shelf.go b/shelf/shelf.go new file mode 100644 index 0000000..6df46fc --- /dev/null +++ b/shelf/shelf.go @@ -0,0 +1,134 @@ +package shelf + +import ( + "bufio" + "context" + "fmt" + "io" + "io/fs" + "path/filepath" + "runtime" + "slices" + "strings" + "sync" + + "golang.org/x/sync/errgroup" +) + +var Root = ".shelf" + +type Sum map[Item]string + +func Index(ignore []byte) (Sum, error) { + sum := make(Sum) + + var rw sync.RWMutex + + g, ctx := errgroup.WithContext(context.Background()) + + sem := make(chan struct{}, max(1, runtime.NumCPU()-1)) + + ign := newIgnore(ignore) + + rootPath := filepath.Clean(Root) + + walkErr := filepath.WalkDir(rootPath, func(abs string, d fs.DirEntry, err error) error { + if err != nil { + return nil + } + + rel, relErr := filepath.Rel(rootPath, abs) + + if relErr != nil { + return err + } + + if rel == "." { + return nil + } + + if ign.Match(rel, d.IsDir()) { + if d.IsDir() { + return fs.SkipDir + } + + return nil + } + + if d.IsDir() || !d.Type().IsRegular() { + return nil + } + + select { + case <-ctx.Done(): + return ctx.Err() + case sem <- struct{}{}: + } + + g.Go(func() error { + defer func() { <-sem }() + + hash, hashErr := hashFile(abs) + + if hashErr != nil { + return hashErr + } + + rw.Lock() + + sum[Item(rel)] = hash + + rw.Unlock() + + return nil + }) + + return nil + }) + + if err := g.Wait(); err != nil { + return nil, err + } + + return sum, walkErr +} + +func Serialize(s Sum, w io.Writer) error { + items := make([]Item, 0, len(s)) + + for item := range s { + items = append(items, item) + } + + slices.SortFunc(items, func(a, b Item) int { + return strings.Compare(string(a), string(b)) + }) + + for _, item := range items { + if _, err := fmt.Fprintf(w, "%s %s\n", s[item], item); err != nil { + return err + } + } + + return nil +} + +func Deserialize(r io.Reader) (Sum, error) { + sum := make(Sum) + + s := bufio.NewScanner(r) + + for s.Scan() { + line := s.Text() + + parts := strings.SplitN(line, " ", 2) + + if len(parts) != 2 { + return nil, fmt.Errorf("malformed line: %q", line) + } + + sum[Item(parts[1])] = parts[0] + } + + return sum, s.Err() +} |
