summaryrefslogtreecommitdiff
path: root/shelf
diff options
context:
space:
mode:
Diffstat (limited to 'shelf')
-rw-r--r--shelf/cmd/sum/main.go32
-rw-r--r--shelf/go.mod16
-rw-r--r--shelf/go.sum22
-rw-r--r--shelf/hash.go28
-rw-r--r--shelf/ignore.go48
-rw-r--r--shelf/item.go9
-rw-r--r--shelf/shelf.go134
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()
+}