- документ самоуправления объявляется ключом governance, а не угадывается по «он один и без ключей оси»: конвенция, потерявшая topic, была от него неотличима и тихо теряла все проверки об отъезде к потребителю - проверка путей канона больше не ловит README.md и READING.md — эти два имени значат что-то и на стороне потребителя - lang.Recognize требует совпадения и слов, и номера версии; директории компонентов сверяются на вложенность, а не только на равенство - у обеих проверок появился --json, а convy sync называет ссылки на темы, которых компонент не взял
199 lines
6.4 KiB
Go
199 lines
6.4 KiB
Go
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"git.vakhrushev.me/av/convy/internal/manifest"
|
|
"git.vakhrushev.me/av/convy/internal/source"
|
|
"git.vakhrushev.me/av/convy/internal/suite"
|
|
)
|
|
|
|
// A project command works against two levels at once: the manifest lying in the
|
|
// repository and the suite it names. Reaching the suite costs a clone when the
|
|
// reference is a git one, so it is done once per command and released at the
|
|
// end — nothing of the suite is left lying about in the project.
|
|
|
|
// opened is a project together with the levels above it.
|
|
type opened struct {
|
|
Root string
|
|
Manifest *manifest.Project
|
|
Suite *suite.Suite
|
|
// LangRoot is where the documents about the language lie. It is the root
|
|
// of the suite while the language has no repository of its own.
|
|
LangRoot string
|
|
|
|
trees []*source.Tree
|
|
}
|
|
|
|
// Close releases whatever the opening fetched.
|
|
func (o *opened) Close() {
|
|
for _, t := range o.trees {
|
|
t.Close()
|
|
}
|
|
}
|
|
|
|
// openProject finds the project, resolves its source and loads the suite.
|
|
func openProject(env Env, root string) (*opened, ExitCode) {
|
|
dir, code := projectRoot(env, root)
|
|
if code != OK {
|
|
return nil, code
|
|
}
|
|
m, code := loadProject(env, dir)
|
|
if code != OK {
|
|
return nil, code
|
|
}
|
|
if m.Source == "" {
|
|
fmt.Fprintf(env.Err, "%s names no source: a copy comes from a suite, and the manifest is where the suite is named\n", m.Path)
|
|
return nil, Usage
|
|
}
|
|
|
|
ref, err := source.Parse(m.Source)
|
|
if err != nil {
|
|
fmt.Fprintln(env.Err, err)
|
|
return nil, Usage
|
|
}
|
|
tree, err := source.Open(ref, dir)
|
|
if err != nil {
|
|
fmt.Fprintln(env.Err, err)
|
|
return nil, Failed
|
|
}
|
|
|
|
o := &opened{Root: dir, Manifest: m, LangRoot: tree.Dir(), trees: []*source.Tree{tree}}
|
|
o.Suite, err = suite.Load(tree.Dir())
|
|
if err != nil {
|
|
fmt.Fprintf(env.Err, "the source %s is not a conventions suite: %s\n", ref, tree.Describe(err))
|
|
o.Close()
|
|
return nil, Failed
|
|
}
|
|
|
|
// The language is a level of its own, and a suite may keep its documents
|
|
// apart from itself. While it does not, the suite is where they lie.
|
|
if spec := o.Suite.Manifest.Language.Source; spec != "" {
|
|
ref, err := source.Parse(spec)
|
|
if err != nil {
|
|
o.Close()
|
|
fmt.Fprintf(env.Err, "%s: [language] source: %s\n", o.Suite.Manifest.Path, err)
|
|
return nil, Usage
|
|
}
|
|
langTree, err := source.Open(ref, tree.Dir())
|
|
if err != nil {
|
|
o.Close()
|
|
fmt.Fprintln(env.Err, err)
|
|
return nil, Failed
|
|
}
|
|
o.trees = append(o.trees, langTree)
|
|
o.LangRoot = langTree.Dir()
|
|
}
|
|
return o, OK
|
|
}
|
|
|
|
// loadProject reads the project manifest and says what it did not understand.
|
|
// A typo in a key costs a whole component: `dyr` instead of `dir` leaves the
|
|
// component pointing at the root of the repository, and nothing else would say
|
|
// so.
|
|
func loadProject(env Env, dir string) (*manifest.Project, ExitCode) {
|
|
m, err := manifest.LoadProject(dir)
|
|
if err != nil {
|
|
fmt.Fprintln(env.Err, err)
|
|
return nil, Usage
|
|
}
|
|
for _, key := range m.Undecoded {
|
|
fmt.Fprintf(env.Err, "warning: %s: the key %s is unknown to the tool\n", m.Path, key)
|
|
}
|
|
return m, OK
|
|
}
|
|
|
|
// projectRoot finds the manifest of the project. A project command typed inside
|
|
// a suite does not do anything at a guess: it says where it is and names the
|
|
// command of that level.
|
|
func projectRoot(env Env, given string) (string, ExitCode) {
|
|
if given != "" {
|
|
return given, OK
|
|
}
|
|
found, err := manifest.FindProject(env.Dir)
|
|
if err == nil {
|
|
return found, OK
|
|
}
|
|
if _, suiteErr := manifest.Find(env.Dir); suiteErr == nil {
|
|
fmt.Fprintf(env.Err, "this is a conventions suite, not a project that takes copies: %s lies here, %s does not\n", manifest.Name, manifest.ProjectName)
|
|
fmt.Fprintln(env.Err, "the commands of a suite are under convy suite")
|
|
return "", Usage
|
|
}
|
|
fmt.Fprintf(env.Err, "not a project with conventions: no %s here or above\n", manifest.ProjectName)
|
|
fmt.Fprintln(env.Err, "convy init wires one up")
|
|
return "", Usage
|
|
}
|
|
|
|
// componentOf resolves the --for flag against the manifest.
|
|
func componentOf(env Env, m *manifest.Project, name string) (string, manifest.Component, ExitCode) {
|
|
got, c, err := m.Only(name)
|
|
if err != nil {
|
|
fmt.Fprintln(env.Err, err)
|
|
return "", manifest.Component{}, Usage
|
|
}
|
|
if c.Dir == "" {
|
|
fmt.Fprintf(env.Err, "the component %q names no dir, and a copy has to be written somewhere\n", got)
|
|
return "", manifest.Component{}, Usage
|
|
}
|
|
return got, c, OK
|
|
}
|
|
|
|
// components picks the components a command works on: the one named by --for,
|
|
// or every one of them.
|
|
func components(env Env, m *manifest.Project, name string) ([]string, ExitCode) {
|
|
if name != "" {
|
|
if _, ok := m.Components[name]; !ok {
|
|
fmt.Fprintf(env.Err, "the project declares no component %q; it declares: %s\n", name, strings.Join(m.Names(), ", "))
|
|
return nil, Usage
|
|
}
|
|
return []string{name}, OK
|
|
}
|
|
if len(m.Components) == 0 {
|
|
fmt.Fprintf(env.Err, "%s declares no component, and a copy is assembled for a component\n", m.Path)
|
|
return nil, Usage
|
|
}
|
|
return m.Names(), OK
|
|
}
|
|
|
|
// distinctDirs checks that no two components write into the same place. Two
|
|
// copies of one topic would otherwise collide by name, and that is an error of
|
|
// the manifest rather than a reason to rename files.
|
|
//
|
|
// One directory inside another is the same error told less plainly: whatever
|
|
// walks the outer one finds the copies of the inner, and every command that
|
|
// counts files counts them twice.
|
|
func distinctDirs(m *manifest.Project) error {
|
|
dirs := make(map[string]string, len(m.Components))
|
|
for _, name := range m.Names() {
|
|
dirs[name] = filepath.ToSlash(filepath.Clean(m.Components[name].Dir))
|
|
}
|
|
names := m.Names()
|
|
for i, a := range names {
|
|
for _, b := range names[i+1:] {
|
|
switch {
|
|
case dirs[a] == dirs[b]:
|
|
return fmt.Errorf("the components %q and %q share the directory %s: copies of one topic would collide there", a, b, dirs[a])
|
|
case within(dirs[a], dirs[b]):
|
|
return fmt.Errorf("the directory of the component %q (%s) lies inside the one of %q (%s): whatever walks the outer one finds the copies of the inner", b, dirs[b], a, dirs[a])
|
|
case within(dirs[b], dirs[a]):
|
|
return fmt.Errorf("the directory of the component %q (%s) lies inside the one of %q (%s): whatever walks the outer one finds the copies of the inner", a, dirs[a], b, dirs[b])
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// within reports whether inner lies under outer.
|
|
func within(outer, inner string) bool {
|
|
return strings.HasPrefix(inner, outer+"/")
|
|
}
|
|
|
|
// exists reports whether a path is there.
|
|
func exists(name string) bool {
|
|
_, err := os.Stat(name)
|
|
return err == nil
|
|
}
|