A bare pattern meant for a binary (mesh-build-node) matched the command directory of the same name, so git ignored every new file there and publish.go was never committed (catalogue review F1). The kind: every bare pattern of a module's .gitignore is anchored to the binary (nfs-server's hid all of cmd/nfs-server), and checks/gitignored refuses an ignore pattern, the module's or the catalogue's, that hides a directory under a module's cmd/ or internal/, a Go file added there, or a source file there, in a git checkout and a plain folder alike. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
125 lines
4.1 KiB
Go
125 lines
4.1 KiB
Go
// Package gitignored refuses an ignore pattern that hides a module's source (catalogue review F1, and its kind): the
|
|
// bare pattern `mesh-build-node` in a module's .gitignore, meant for the binary `go build` leaves, also matched the
|
|
// directory cmd/mesh-build-node, so git ignored every new file there and publish.go was never committed. A path under
|
|
// a module's cmd/ or internal/ is source: no .gitignore, the module's or the catalogue's, may ignore one, tracked or
|
|
// new. A binary is ignored by an anchored pattern (`/mesh-build-node`, `/cmd/mesh-build-node/mesh-build-node`).
|
|
package gitignored
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// Probe is the file name asked of each source directory: a Go file a change might add there.
|
|
const Probe = "added_by_a_change.go"
|
|
|
|
// Hidden is one source path an ignore pattern hides, and the pattern (its file, line and text, as git names it).
|
|
type Hidden struct {
|
|
Path, Pattern string
|
|
}
|
|
|
|
// Sources are the source directories of root's modules, relative to root: every directory under modules/*/cmd and
|
|
// modules/*/internal (node_modules left out).
|
|
func Sources(root string) ([]string, error) {
|
|
mods, err := filepath.Glob(filepath.Join(root, "modules", "*"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var out []string
|
|
for _, m := range mods {
|
|
for _, top := range []string{"cmd", "internal"} {
|
|
base := filepath.Join(m, top)
|
|
if st, err := os.Stat(base); err != nil || !st.IsDir() {
|
|
continue
|
|
}
|
|
err := filepath.WalkDir(base, func(p string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !d.IsDir() {
|
|
return nil
|
|
}
|
|
if d.Name() == "node_modules" {
|
|
return filepath.SkipDir
|
|
}
|
|
rel, _ := filepath.Rel(root, p)
|
|
out = append(out, filepath.ToSlash(rel))
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
sort.Strings(out)
|
|
return out, nil
|
|
}
|
|
|
|
// isSource is whether a file is source a module's build reads: Go and TypeScript files and a Go module's own files.
|
|
// What a build or a test script leaves beside them (a binary, compiled JavaScript) may be ignored.
|
|
func isSource(name string) bool {
|
|
switch filepath.Ext(name) {
|
|
case ".go", ".ts", ".tsx":
|
|
return true
|
|
}
|
|
return name == "go.mod" || name == "go.sum"
|
|
}
|
|
|
|
// Find asks git which of the source paths root's ignore files hide: each source directory, a Go file added in it, and
|
|
// every source file there (isSource). It reads the .gitignore files of root's tree through a repository of its own, so it judges a git
|
|
// checkout and a plain folder alike, and no one's global ignores count.
|
|
func Find(root string, dirs []string) ([]Hidden, error) {
|
|
gitDir, err := os.MkdirTemp("", "gitignored-")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer os.RemoveAll(gitDir)
|
|
if out, err := exec.Command("git", "init", "-q", "--bare", gitDir).CombinedOutput(); err != nil {
|
|
return nil, fmt.Errorf("git init: %v: %s", err, out)
|
|
}
|
|
var ask bytes.Buffer
|
|
for _, d := range dirs {
|
|
ask.WriteString(d + "/\x00" + d + "/" + Probe + "\x00")
|
|
entries, err := os.ReadDir(filepath.Join(root, d))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, e := range entries {
|
|
if !e.IsDir() && isSource(e.Name()) {
|
|
ask.WriteString(d + "/" + e.Name() + "\x00")
|
|
}
|
|
}
|
|
}
|
|
abs, err := filepath.Abs(root)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cmd := exec.Command("git", "--git-dir="+gitDir, "--work-tree="+abs, "-c", "core.excludesFile=/dev/null",
|
|
"check-ignore", "--no-index", "-v", "-z", "--stdin")
|
|
cmd.Dir = abs
|
|
cmd.Stdin = &ask
|
|
out, err := cmd.Output()
|
|
if ee, ok := err.(*exec.ExitError); ok && ee.ExitCode() == 1 {
|
|
return nil, nil // nothing is ignored
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("git check-ignore: %w", err)
|
|
}
|
|
// -v -z answers source, line, pattern and path, each ended by NUL.
|
|
f := strings.Split(strings.TrimRight(string(out), "\x00"), "\x00")
|
|
var hidden []Hidden
|
|
for i := 0; i+3 < len(f); i += 4 {
|
|
if f[i+2] == "" || strings.HasPrefix(f[i+2], "!") {
|
|
continue // a negation keeps the path
|
|
}
|
|
hidden = append(hidden, Hidden{Path: f[i+3], Pattern: fmt.Sprintf("%s:%s: %s", f[i], f[i+1], f[i+2])})
|
|
}
|
|
return hidden, nil
|
|
}
|