манифесты стали данными, заведён convy sync
- убраны комментарии из suite.toml и .conventions.toml: файл, который машина переписывает, комментарий через круг не проносит; объяснения ушли в README рядом, который suite init теперь заводит - удалена текстовая правка манифеста целиком — 520 строк ручного лексера TOML вместе со всем классом ошибок порчи данных - запись идёт из структур энкодером; ключ, которого инструмент не знает, запись останавливает, а не теряется молча - convy sync сверяет манифест и подводит под него раскладку файлов: чего не хватает — собирает, что осиротело — удаляет, копию с локальной частью не трогает никогда
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.vakhrushev.me/av/convy/internal/manifest"
|
||||
"git.vakhrushev.me/av/convy/internal/project"
|
||||
)
|
||||
|
||||
// convy sync makes the files agree with the manifest. The manifest is the
|
||||
// truth: it says which components exist, where they write and what each takes,
|
||||
// and everything under those directories follows from that.
|
||||
//
|
||||
// It divides from pull by what it is about. pull is about the contents of a
|
||||
// copy — it takes the text of every subscription afresh, and the diff it leaves
|
||||
// is the point of running it. sync is about the set of files: what the manifest
|
||||
// calls for and is not there gets assembled, what is there and nothing calls
|
||||
// for gets reported and, when nothing of the repository is in it, removed.
|
||||
//
|
||||
// A copy carrying a local part is never removed. Below the marker is the one
|
||||
// thing in the directory that exists nowhere else, and a command that tidies up
|
||||
// has no business deciding it is spent.
|
||||
|
||||
func runSync(env Env, args []string) ExitCode {
|
||||
fs := flag.NewFlagSet("convy sync", 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 bring in line; every one of them by default")
|
||||
dry := fs.Bool("dry-run", false, "say what would change and change nothing")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return Usage
|
||||
}
|
||||
if code := noStrayArgs(env, "convy sync", fs.Args()); code != OK {
|
||||
return code
|
||||
}
|
||||
|
||||
o, code := openProject(env, *root)
|
||||
if code != OK {
|
||||
return code
|
||||
}
|
||||
defer o.Close()
|
||||
|
||||
names, code := components(env, o.Manifest, *forComponent)
|
||||
if code != OK {
|
||||
return code
|
||||
}
|
||||
|
||||
problems := validateManifest(o, names)
|
||||
for _, p := range problems {
|
||||
fmt.Fprintf(env.Err, "%s\n", p)
|
||||
}
|
||||
if len(problems) > 0 {
|
||||
fmt.Fprintln(env.Err, "\nthe manifest is what the layout follows, so nothing was touched")
|
||||
return Failed
|
||||
}
|
||||
|
||||
changed, left := 0, 0
|
||||
for i, name := range names {
|
||||
c := o.Manifest.Components[name]
|
||||
if i > 0 {
|
||||
fmt.Fprintln(env.Out)
|
||||
}
|
||||
fmt.Fprintf(env.Out, "%s → %s\n", name, c.Dir)
|
||||
n, stuck, code := syncComponent(env, o, c, *dry)
|
||||
if code != OK {
|
||||
return code
|
||||
}
|
||||
changed += n
|
||||
left += stuck
|
||||
}
|
||||
|
||||
fmt.Fprintln(env.Out)
|
||||
switch {
|
||||
case *dry && changed > 0:
|
||||
fmt.Fprintf(env.Out, "%s would change; run without --dry-run to do it\n", plural(changed, "file"))
|
||||
case changed > 0:
|
||||
fmt.Fprintf(env.Out, "%s changed; convy pull takes the text of the rest afresh\n", plural(changed, "file"))
|
||||
case left == 0:
|
||||
fmt.Fprintln(env.Out, "the layout already follows the manifest")
|
||||
}
|
||||
if left > 0 {
|
||||
fmt.Fprintf(env.Out, "%s left alone: nothing subscribes to it and it holds a local part\n", plural(left, "file"))
|
||||
return Failed
|
||||
}
|
||||
return OK
|
||||
}
|
||||
|
||||
// validate checks the manifest against itself and against the suite. Everything
|
||||
// wrong is reported at once: being sent back one line at a time is the worst way
|
||||
// to learn what a file wants.
|
||||
func validateManifest(o *opened, names []string) []string {
|
||||
var out []string
|
||||
if err := distinctDirs(o.Manifest); err != nil {
|
||||
out = append(out, err.Error())
|
||||
}
|
||||
for _, name := range names {
|
||||
c := o.Manifest.Components[name]
|
||||
if c.Dir == "" {
|
||||
out = append(out, fmt.Sprintf("the component %q names no dir, and a copy has to be written somewhere", name))
|
||||
}
|
||||
if len(c.Lang) > 1 {
|
||||
out = append(out, fmt.Sprintf("the component %q declares two languages (%s): a line of code is written in one of them, and a component is the region where every chosen layer holds at once — split it",
|
||||
name, strings.Join(c.Lang, ", ")))
|
||||
}
|
||||
seen := make(map[string]bool, len(c.Topics))
|
||||
for _, topic := range c.Topics {
|
||||
switch {
|
||||
case seen[topic]:
|
||||
out = append(out, fmt.Sprintf("the component %q takes %q twice", name, topic))
|
||||
case o.Suite.Manifest.TopicRetired(topic):
|
||||
out = append(out, fmt.Sprintf("the component %q takes %q, which the suite has retired: %s",
|
||||
name, topic, o.Suite.Manifest.Topics.Retired[topic]))
|
||||
case !o.Suite.Manifest.TopicLive(topic):
|
||||
out = append(out, fmt.Sprintf("the component %q takes %q, and the suite declares no such topic", name, topic))
|
||||
default:
|
||||
if taken, _ := o.Suite.Assemble(topic, project.Axis(c)); len(taken) == 0 {
|
||||
out = append(out, fmt.Sprintf("the component %q takes %q, and no layer of it fits this component", name, topic))
|
||||
}
|
||||
}
|
||||
seen[topic] = true
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// syncComponent brings one directory in line. It returns how many files moved
|
||||
// and how many it would not touch.
|
||||
func syncComponent(env Env, o *opened, c manifest.Component, dry bool) (changed, left int, code ExitCode) {
|
||||
|
||||
for _, topic := range c.Topics {
|
||||
rel := filepath.ToSlash(filepath.Join(c.Dir, topic+".md"))
|
||||
if exists(filepath.Join(o.Root, filepath.FromSlash(rel))) {
|
||||
continue
|
||||
}
|
||||
changed++
|
||||
if dry {
|
||||
fmt.Fprintf(env.Out, " + %-28s subscribed, and no file\n", rel)
|
||||
continue
|
||||
}
|
||||
made, err := project.Assemble(o.Suite, o.Root, c, topic)
|
||||
if err != nil {
|
||||
fmt.Fprintf(env.Err, " %s: %s\n", topic, err)
|
||||
return changed, left, Failed
|
||||
}
|
||||
fmt.Fprintf(env.Out, " + %-28s %s\n", made.Path, plural(len(made.Layers), "layer"))
|
||||
}
|
||||
|
||||
orphans, err := orphaned(o.Root, c)
|
||||
if err != nil {
|
||||
fmt.Fprintln(env.Err, err)
|
||||
return changed, left, Failed
|
||||
}
|
||||
for _, orphan := range orphans {
|
||||
switch {
|
||||
case orphan.local:
|
||||
left++
|
||||
fmt.Fprintf(env.Out, " ! %-28s nothing subscribes to %q, and it carries a local part: remove it by hand or subscribe again\n",
|
||||
orphan.path, orphan.topic)
|
||||
case dry:
|
||||
changed++
|
||||
fmt.Fprintf(env.Out, " - %-28s nothing subscribes to %q\n", orphan.path, orphan.topic)
|
||||
default:
|
||||
if err := os.Remove(filepath.Join(o.Root, filepath.FromSlash(orphan.path))); err != nil {
|
||||
fmt.Fprintln(env.Err, err)
|
||||
return changed, left, Failed
|
||||
}
|
||||
changed++
|
||||
fmt.Fprintf(env.Out, " - %-28s nothing subscribes to %q\n", orphan.path, orphan.topic)
|
||||
}
|
||||
}
|
||||
|
||||
if !dry {
|
||||
guide, err := project.Reading(o.LangRoot, o.Suite.Manifest.Language.Reading, o.Root, c.Dir)
|
||||
if err != nil {
|
||||
fmt.Fprintf(env.Err, " %s\n", err)
|
||||
return changed, left, Failed
|
||||
}
|
||||
fmt.Fprintf(env.Out, " = %-28s the guide to reading a rule\n", guide)
|
||||
}
|
||||
return changed, left, OK
|
||||
}
|
||||
|
||||
// orphan is a copy in a component directory that the manifest does not call for.
|
||||
type orphan struct {
|
||||
path string
|
||||
topic string
|
||||
local bool
|
||||
}
|
||||
|
||||
// orphaned finds the copies nothing subscribes to. What is a copy is decided by
|
||||
// the origin key: README.md belongs to the repository, READING.md belongs to the
|
||||
// suite, and a file whose origin was taken away has become a document of the
|
||||
// repository — none of the three is anyone's to remove.
|
||||
func orphaned(root string, c manifest.Component) ([]orphan, error) {
|
||||
docs, broken := copies(root, c.Dir)
|
||||
if len(broken) > 0 {
|
||||
return nil, broken[0]
|
||||
}
|
||||
var out []orphan
|
||||
for _, d := range docs {
|
||||
if c.Subscribed(d.Front.Origin) {
|
||||
continue
|
||||
}
|
||||
local := ""
|
||||
if at := d.Marker(); at > 0 {
|
||||
local = strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(d.Below(at)), project.LocalMarker))
|
||||
}
|
||||
out = append(out, orphan{path: d.Path, topic: d.Front.Origin, local: local != ""})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
Reference in New Issue
Block a user