проектные команды и ссылки на источник
- заведён internal/source: уровни ссылаются друг на друга путём на диске или git-репозиторием, ревизия закрепляется хвостом #ref; клон делается заново и удаляется, кэша нет - добавлены init, add, pull, list, check в проекте — манифест .conventions.toml, сборка копий по разу на компонент, маркер локальной части, READING.md рядом - проверки формы развязаны с набором: принимают lang.Vocabulary, а язык копии узнаётся по строке о версии — манифеста рядом с ней нет
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"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, 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)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// 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\n", name)
|
||||
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 directory.
|
||||
// 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.
|
||||
func distinctDirs(m *manifest.Project) error {
|
||||
seen := make(map[string]string)
|
||||
for _, name := range m.Names() {
|
||||
dir := filepath.ToSlash(filepath.Clean(m.Components[name].Dir))
|
||||
if other, taken := seen[dir]; taken {
|
||||
return fmt.Errorf("the components %q and %q share the directory %s: copies of one topic would collide there", other, name, dir)
|
||||
}
|
||||
seen[dir] = name
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// exists reports whether a path is there.
|
||||
func exists(name string) bool {
|
||||
_, err := os.Stat(name)
|
||||
return err == nil
|
||||
}
|
||||
Reference in New Issue
Block a user