118 lines
3.0 KiB
Go
118 lines
3.0 KiB
Go
package client
|
|
|
|
import (
|
|
"bytes"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
|
|
"lodge/internal/gitrepo"
|
|
"lodge/internal/protocol"
|
|
)
|
|
|
|
const (
|
|
RulesFile = ".lodge"
|
|
sharedKeyword = "shared"
|
|
branchKeyword = "branch"
|
|
ignoreKeyword = "ignore"
|
|
excludeBegin = "# lodge begin"
|
|
excludeEnd = "# lodge end"
|
|
DefaultRules = "shared docs/agents\nshared CLAUDE.local.md\nbranch .agent\n"
|
|
)
|
|
|
|
type Rule struct {
|
|
Path string `json:"path"`
|
|
Shared bool `json:"shared"`
|
|
Ignore bool `json:"ignore,omitempty"`
|
|
}
|
|
|
|
type Rules []Rule
|
|
|
|
func parseRules(text string) Rules {
|
|
rules := Rules{{Path: RulesFile, Shared: true}}
|
|
for _, line := range strings.Split(text, "\n") {
|
|
fields := strings.Fields(line)
|
|
if len(fields) != 2 || !slices.Contains([]string{sharedKeyword, branchKeyword, ignoreKeyword}, fields[0]) {
|
|
continue
|
|
}
|
|
rulePath := path.Clean(strings.Trim(filepath.ToSlash(fields[1]), "/"))
|
|
if protocol.SafePath(rulePath) {
|
|
rules = append(rules, Rule{Path: rulePath, Shared: fields[0] == sharedKeyword, Ignore: fields[0] == ignoreKeyword})
|
|
}
|
|
}
|
|
return rules
|
|
}
|
|
|
|
func LoadRules(root string) Rules {
|
|
data, err := os.ReadFile(filepath.Join(root, RulesFile))
|
|
if err != nil {
|
|
return parseRules(DefaultRules)
|
|
}
|
|
return parseRules(string(data))
|
|
}
|
|
|
|
func (rules Rules) synced() Rules {
|
|
return slices.DeleteFunc(slices.Clone(rules), func(rule Rule) bool { return rule.Ignore })
|
|
}
|
|
|
|
func (rules Rules) paths() []string {
|
|
synced := rules.synced()
|
|
paths := make([]string, len(synced))
|
|
for i, rule := range synced {
|
|
paths[i] = rule.Path
|
|
}
|
|
return paths
|
|
}
|
|
|
|
func (rules Rules) covers(file string, shared bool) bool {
|
|
best := Rule{}
|
|
for _, rule := range rules.synced() {
|
|
if within(file, rule.Path) && len(rule.Path) > len(best.Path) {
|
|
best = rule
|
|
}
|
|
}
|
|
return best.Path != "" && best.Shared == shared && !rules.ignored(file)
|
|
}
|
|
|
|
func (rules Rules) ignored(file string) bool {
|
|
return file != RulesFile && slices.ContainsFunc(rules, func(rule Rule) bool { return rule.Ignore && rule.matches(file) })
|
|
}
|
|
|
|
func (rule Rule) matches(file string) bool {
|
|
for name := file; name != "." && name != "/"; name = path.Dir(name) {
|
|
target := name
|
|
if !strings.Contains(rule.Path, "/") {
|
|
target = path.Base(name)
|
|
}
|
|
if matched, _ := path.Match(rule.Path, target); matched {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func within(path, dir string) bool {
|
|
return path == dir || strings.HasPrefix(path, dir+"/")
|
|
}
|
|
|
|
func writeExclude(repo *gitrepo.Repo, rules Rules) error {
|
|
file := filepath.Join(repo.CommonDir, "info", "exclude")
|
|
current, _ := os.ReadFile(file)
|
|
kept := current
|
|
if before, rest, found := bytes.Cut(current, []byte(excludeBegin)); found {
|
|
_, after, _ := bytes.Cut(rest, []byte(excludeEnd+"\n"))
|
|
kept = append(bytes.Clone(before), after...)
|
|
}
|
|
if len(kept) > 0 && !bytes.HasSuffix(kept, []byte("\n")) {
|
|
kept = append(kept, '\n')
|
|
}
|
|
block := excludeBegin + "\n/" + strings.Join(rules.paths(), "\n/") + "\n" + excludeEnd + "\n"
|
|
kept = append(kept, block...)
|
|
if bytes.Equal(kept, current) {
|
|
return nil
|
|
}
|
|
return writeFile(file, kept, 0o644)
|
|
}
|