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) } }