проектные команды и ссылки на источник

- заведён internal/source: уровни ссылаются друг на друга путём на диске
  или git-репозиторием, ревизия закрепляется хвостом #ref; клон делается
  заново и удаляется, кэша нет
- добавлены init, add, pull, list, check в проекте — манифест
  .conventions.toml, сборка копий по разу на компонент, маркер локальной
  части, READING.md рядом
- проверки формы развязаны с набором: принимают lang.Vocabulary, а язык
  копии узнаётся по строке о версии — манифеста рядом с ней нет
This commit is contained in:
av
2026-07-27 20:42:18 +03:00
parent 4615de6e86
commit 23d88c4048
27 changed files with 3009 additions and 57 deletions
+118
View File
@@ -0,0 +1,118 @@
package manifest
import (
"fmt"
"os"
"path/filepath"
"slices"
"sort"
"strings"
"github.com/BurntSushi/toml"
)
// ProjectName is the name of the manifest of a consuming repository. Which of
// the two manifests lies next to you tells you where you are, so the project
// one is never named like the suite one.
const ProjectName = ".conventions.toml"
// Component is one addressee of assembly inside a project: a region where every
// selected layer holds at once — one language, one set of tools, one kind of
// application (META-36).
//
// Lang and Stack are lists because a component may well take two stack layers
// at a time — sqlite and postgres in the schema topic hold together, being
// different tables of one service. Two languages never do: a line of code is
// written in one of them, which is what a component exists to separate.
type Component struct {
Dir string `toml:"dir"`
Lang []string `toml:"lang"`
Stack []string `toml:"stack"`
Topics []string `toml:"topics"`
}
// Project is a parsed .conventions.toml: where the copies are taken from and
// which components take what.
type Project struct {
// Source is the reference to the suite. How the tool reaches it — a path
// on disk, a git repository — is a matter of the reference itself.
Source string `toml:"source"`
Components map[string]Component `toml:"components"`
// Path is where the manifest was read from.
Path string `toml:"-"`
// Root is the directory the manifest lies in; every path in it is relative
// to that directory.
Root string `toml:"-"`
// Undecoded lists keys the tool does not know: a typo in a component name
// or in a key would otherwise cost a whole subscription in silence.
Undecoded []string `toml:"-"`
}
// LoadProject reads the project manifest from directory root.
func LoadProject(root string) (*Project, error) {
path := filepath.Join(root, ProjectName)
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("reading the project manifest: %w", err)
}
var p Project
meta, err := toml.Decode(string(data), &p)
if err != nil {
return nil, fmt.Errorf("parsing %s: %w", path, err)
}
p.Path = path
p.Root = root
for _, key := range meta.Undecoded() {
p.Undecoded = append(p.Undecoded, key.String())
}
sort.Strings(p.Undecoded)
return &p, nil
}
// FindProject walks up from start looking for a project manifest, so that a
// command works from any subdirectory of a repository.
func FindProject(start string) (string, error) {
return findUp(start, ProjectName)
}
// Names lists the components in an order stable between runs.
func (p *Project) Names() []string {
names := make([]string, 0, len(p.Components))
for name := range p.Components {
names = append(names, name)
}
sort.Strings(names)
return names
}
// Only picks the component a command works on. With a name given it is that
// one; without a name it is the single component of the project. A project of
// several components does not get one guessed for it — it gets the list.
func (p *Project) Only(name string) (string, Component, error) {
if name != "" {
c, ok := p.Components[name]
if !ok {
return "", Component{}, fmt.Errorf("the project declares no component %q; it declares: %s", name, joinNames(p.Names()))
}
return name, c, nil
}
switch len(p.Components) {
case 0:
return "", Component{}, fmt.Errorf("%s declares no component, and a copy is assembled for a component", p.Path)
case 1:
only := p.Names()[0]
return only, p.Components[only], nil
}
return "", Component{}, fmt.Errorf("the project holds several components, and the command names none: pass --for with one of %s", joinNames(p.Names()))
}
// Subscribed reports whether a component takes a topic.
func (c Component) Subscribed(topic string) bool {
return slices.Contains(c.Topics, topic)
}
func joinNames(names []string) string {
return strings.Join(names, ", ")
}