- заведён internal/source: уровни ссылаются друг на друга путём на диске или git-репозиторием, ревизия закрепляется хвостом #ref; клон делается заново и удаляется, кэша нет - добавлены init, add, pull, list, check в проекте — манифест .conventions.toml, сборка копий по разу на компонент, маркер локальной части, READING.md рядом - проверки формы развязаны с набором: принимают lang.Vocabulary, а язык копии узнаётся по строке о версии — манифеста рядом с ней нет
143 lines
3.9 KiB
Go
143 lines
3.9 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/manifest"
|
|
"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")
|
|
if err := fs.Parse(args); err != nil {
|
|
return Usage
|
|
}
|
|
|
|
dir, code := projectRoot(env, *root)
|
|
if code != OK {
|
|
return code
|
|
}
|
|
m, err := manifest.LoadProject(dir)
|
|
if 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 {
|
|
found, errs := copies(dir, m.Components[name].Dir)
|
|
docs = append(docs, found...)
|
|
broken = append(broken, errs...)
|
|
}
|
|
|
|
rep := check.Copies(docs)
|
|
for _, err := range broken {
|
|
fmt.Fprintf(env.Err, "%s\n", err)
|
|
}
|
|
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
|
|
}
|
|
|
|
func printCopyReport(w io.Writer, rep *check.Report, files, comps int, quiet bool) {
|
|
findings := rep.Findings()
|
|
current := ""
|
|
for _, f := range findings {
|
|
if f.Path != current {
|
|
if current != "" {
|
|
fmt.Fprintln(w)
|
|
}
|
|
fmt.Fprintf(w, "%s\n", f.Path)
|
|
current = f.Path
|
|
}
|
|
where := ""
|
|
if f.Line > 0 {
|
|
where = fmt.Sprintf(":%d", f.Line)
|
|
}
|
|
fmt.Fprintf(w, " %s%s %s [%s]\n", f.Severity, where, f.Msg, f.Family)
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|