- документ самоуправления объявляется ключом governance, а не угадывается по «он один и без ключей оси»: конвенция, потерявшая topic, была от него неотличима и тихо теряла все проверки об отъезде к потребителю - проверка путей канона больше не ловит README.md и READING.md — эти два имени значат что-то и на стороне потребителя - lang.Recognize требует совпадения и слов, и номера версии; директории компонентов сверяются на вложенность, а не только на равенство - у обеих проверок появился --json, а convy sync называет ссылки на темы, которых компонент не взял
163 lines
4.5 KiB
Go
163 lines
4.5 KiB
Go
package cli
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
|
|
"git.vakhrushev.me/av/convy/internal/check"
|
|
"git.vakhrushev.me/av/convy/internal/doc"
|
|
"git.vakhrushev.me/av/convy/internal/project"
|
|
)
|
|
|
|
// convy check stays at the top level and reaches for no suite. The form of a
|
|
// rule is one and the same, the local rules of the repository on X prefixes are
|
|
// written by that same form, and checking what lies here has to work without a
|
|
// network and without knowing where the copies came from.
|
|
|
|
func runCheck(env Env, args []string) ExitCode {
|
|
fs := flag.NewFlagSet("convy check", flag.ContinueOnError)
|
|
fs.SetOutput(env.Err)
|
|
root := fs.String("root", "", "root of the project; it is looked up upwards by default")
|
|
forComponent := fs.String("for", "", "component to check; every one of them by default")
|
|
quiet := fs.Bool("quiet", false, "print findings only")
|
|
asJSON := fs.Bool("json", false, "write the findings as JSON, for a caller that is not a person")
|
|
if err := fs.Parse(args); err != nil {
|
|
return Usage
|
|
}
|
|
if code := noStrayArgs(env, "convy check", fs.Args()); code != OK {
|
|
return code
|
|
}
|
|
|
|
dir, code := projectRoot(env, *root)
|
|
if code != OK {
|
|
return code
|
|
}
|
|
m, code := loadProject(env, dir)
|
|
if code != OK {
|
|
return code
|
|
}
|
|
if err := distinctDirs(m); err != nil {
|
|
fmt.Fprintln(env.Err, err)
|
|
return Usage
|
|
}
|
|
names, code := components(env, m, *forComponent)
|
|
if code != OK {
|
|
return code
|
|
}
|
|
|
|
var docs []*doc.Document
|
|
var broken []error
|
|
for _, name := range names {
|
|
c := m.Components[name]
|
|
if c.Dir == "" {
|
|
fmt.Fprintf(env.Err, "the component %q names no dir, and there is nothing to look in\n", name)
|
|
return Usage
|
|
}
|
|
found, errs := copies(dir, c.Dir)
|
|
docs = append(docs, found...)
|
|
broken = append(broken, errs...)
|
|
}
|
|
docs = distinct(docs)
|
|
|
|
rep := check.Copies(docs)
|
|
for _, err := range broken {
|
|
fmt.Fprintf(env.Err, "%s\n", err)
|
|
}
|
|
if *asJSON {
|
|
if code := printJSON(env, rep); code != OK {
|
|
return code
|
|
}
|
|
} else {
|
|
printCopyReport(env.Out, rep, len(docs), len(names), *quiet)
|
|
}
|
|
if rep.Errors() > 0 || len(broken) > 0 {
|
|
return Failed
|
|
}
|
|
return OK
|
|
}
|
|
|
|
// copies collects the assembled conventions of one component directory.
|
|
//
|
|
// What is a copy is decided by the origin key rather than by the name of the
|
|
// file: README.md belongs to the repository, READING.md belongs to the suite,
|
|
// and a file whose origin key was taken away has become a document of the
|
|
// repository — none of the three answers to the form of a rule.
|
|
func copies(root, dir string) ([]*doc.Document, []error) {
|
|
var docs []*doc.Document
|
|
var broken []error
|
|
base := filepath.Join(root, filepath.FromSlash(dir))
|
|
|
|
err := filepath.WalkDir(base, func(name string, entry fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if entry.IsDir() || filepath.Ext(entry.Name()) != ".md" {
|
|
return nil
|
|
}
|
|
rel, err := filepath.Rel(root, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rel = filepath.ToSlash(rel)
|
|
d, err := doc.Load(rel, name)
|
|
if err != nil {
|
|
broken = append(broken, err)
|
|
return nil
|
|
}
|
|
if d.Front.Origin == "" {
|
|
return nil
|
|
}
|
|
docs = append(docs, d)
|
|
return nil
|
|
})
|
|
if err != nil && !os.IsNotExist(err) {
|
|
broken = append(broken, fmt.Errorf("walking %s: %w", dir, err))
|
|
}
|
|
sort.Slice(docs, func(i, j int) bool { return docs[i].Path < docs[j].Path })
|
|
return docs, broken
|
|
}
|
|
|
|
// distinct drops a document reached through two components. Directories are
|
|
// checked for equality before this, but one may still lie inside another, and a
|
|
// finding printed twice reads as two.
|
|
func distinct(docs []*doc.Document) []*doc.Document {
|
|
seen := make(map[string]bool, len(docs))
|
|
out := docs[:0]
|
|
for _, d := range docs {
|
|
if seen[d.Path] {
|
|
continue
|
|
}
|
|
seen[d.Path] = true
|
|
out = append(out, d)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func printCopyReport(w io.Writer, rep *check.Report, files, comps int, quiet bool) {
|
|
findings := rep.Findings()
|
|
printFindings(w, findings)
|
|
|
|
if quiet {
|
|
return
|
|
}
|
|
if len(findings) > 0 {
|
|
fmt.Fprintln(w)
|
|
}
|
|
fmt.Fprintf(w, "project: %s in %s\n", plural(files, "file"), plural(comps, "component"))
|
|
switch {
|
|
case rep.Errors() > 0:
|
|
fmt.Fprintf(w, "errors: %d, warnings: %d\n", rep.Errors(), rep.Warnings())
|
|
case rep.Warnings() > 0:
|
|
fmt.Fprintf(w, "no errors, warnings: %d\n", rep.Warnings())
|
|
case files == 0:
|
|
fmt.Fprintf(w, "nothing to check: no file carries an origin key; convy pull assembles the copies\n")
|
|
default:
|
|
fmt.Fprintf(w, "the copies hold the form; everything below %s is the repository's own\n", project.LocalMarker)
|
|
}
|
|
}
|