Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ package main
import (
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"path"
"strings"
)

type FS interface {
Open(name string) (File, error)
Paths(name string) ([]string, error)
}

type File interface {
Expand Down Expand Up @@ -47,3 +51,20 @@ func (g *gitfs) Open(name string) (File, error) {
Buffer: bytes.NewBuffer(buf),
}, nil
}

// Paths returns the paths of every file with the given name at the revision
func (g *gitfs) Paths(name string) ([]string, error) {
cmd := exec.Command("git", "-C", g.cwd, "ls-tree", "-r", "-z", "--name-only", g.rev)
out, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("error listing files in %s: %w", g.rev, err)
}

paths := []string{}
for _, file := range strings.Split(string(out), "\x00") {
if file != "" && path.Base(file) == name {
paths = append(paths, file)
}
}
return paths, nil
}
131 changes: 94 additions & 37 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -459,9 +459,19 @@ func readLines(b []byte) ([]string, error) {
}

func notifications(fs FS, paths []string, notifyFilename string) (map[string][]string, error) {
ruleFilePaths, err := fs.Paths(notifyFilename)
if err != nil {
return nil, err
}

ruleFileCache := make(map[string]*ruleset, len(ruleFilePaths))
for _, ruleFilePath := range ruleFilePaths {
ruleFileCache[ruleFilePath] = nil
}

notifications := map[string][]string{}
for _, path := range paths {
subs, err := subscribers(fs, path, notifyFilename)
subs, err := subscribers(fs, ruleFileCache, path, notifyFilename)
if err != nil {
return nil, err
}
Expand All @@ -474,61 +484,108 @@ func notifications(fs FS, paths []string, notifyFilename string) (map[string][]s
return notifications, nil
}

func subscribers(fs FS, path string, notifyFilename string) ([]string, error) {
fmt.Fprintf(verbose, "analyzing subscribers in %s files\n", notifyFilename)
// rule is a single parsed line of a rule file
type rule struct {
pattern *regexp.Regexp
subscribers []string
}

// ruleset represents a parsed rule file
type ruleset struct {
rules []rule
}

// subscribers returns the subscribers of every rule matching the given path
func (r *ruleset) subscribers(path string) []string {
subscribers := []string{}
for _, rule := range r.rules {
if rule.pattern.MatchString(path) {
subscribers = append(subscribers, rule.subscribers...)
}
}
return subscribers
}

// subscribers returns the subscribers to notify about a change to path
func subscribers(fs FS, ruleFileCache map[string]*ruleset, path string, notifyFilename string) ([]string, error) {
seen := map[string]struct{}{}

parts := strings.Split(path, string(os.PathSeparator))
for i := range parts {
base := filepath.Join(parts[:i]...)
rulefilepath := filepath.Join(base, notifyFilename)
ruleFilePath := filepath.Join(base, notifyFilename)

rulefile, err := fs.Open(rulefilepath)
if err != nil {
if err == os.ErrNotExist {
continue
ruleFile, ok := ruleFileCache[ruleFilePath]
if !ok {
// No rule file in this directory
continue
}

if ruleFile == nil {
loaded, err := readRuleset(fs, ruleFilePath)
if err != nil {
return nil, err
}
ruleFileCache[ruleFilePath] = loaded
ruleFile = loaded
}

rel, err := filepath.Rel(base, path)
if err != nil {
return nil, err
}

scanner := bufio.NewScanner(rulefile)
for scanner.Scan() {
rule := scanner.Text()
if rule != "" && rule[0] == '#' {
// skip comment
continue
}
for _, subscriber := range ruleFile.subscribers(rel) {
seen[subscriber] = struct{}{}
}
}

fields := strings.Fields(rule)
switch len(fields) {
case 0:
// skip blank line
continue
case 1:
return nil, fmt.Errorf("expected at least two fields for rule in %s: %s", rulefilepath, rule)
}
subscribers := make([]string, 0, len(seen))
for subscriber := range seen {
subscribers = append(subscribers, subscriber)
}
return subscribers, nil
}

rel, err := filepath.Rel(base, path)
if err != nil {
return nil, err
}
func readRuleset(fs FS, path string) (*ruleset, error) {
fmt.Fprintf(verbose, "reading %s\n", path)

re, err := patternToRegexp(fields[0])
if err != nil {
return nil, fmt.Errorf("invalid pattern in %s: %s: %w", rulefilepath, rule, err)
}
rulefile, err := fs.Open(path)
if err != nil {
return nil, err
}

if re.MatchString(rel) {
subscribers = append(subscribers, fields[1:]...)
}
rules := ruleset{}
scanner := bufio.NewScanner(rulefile)
for scanner.Scan() {
line := scanner.Text()
if line != "" && line[0] == '#' {
// skip comment
continue
}

if err := scanner.Err(); err != nil {
return nil, err
fields := strings.Fields(line)
switch len(fields) {
case 0:
// skip blank line
continue
case 1:
return nil, fmt.Errorf("expected at least two fields for rule in %s: %s", path, line)
}

pattern, err := patternToRegexp(fields[0])
if err != nil {
return nil, fmt.Errorf("invalid pattern in %s: %s: %w", path, line, err)
}

rules.rules = append(rules.rules, rule{pattern: pattern, subscribers: fields[1:]})
}

return subscribers, nil
if err := scanner.Err(); err != nil {
return nil, err
}

return &rules, nil
}

func patternToRegexp(pattern string) (*regexp.Regexp, error) {
Expand Down
10 changes: 10 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -726,3 +726,13 @@ func (m memfs) Open(name string) (File, error) {

return mf, nil
}

func (m memfs) Paths(name string) ([]string, error) {
paths := []string{}
for path := range m {
if filepath.Base(path) == name {
paths = append(paths, path)
}
}
return paths, nil
}