проектные команды и ссылки на источник
- заведён internal/source: уровни ссылаются друг на друга путём на диске или git-репозиторием, ревизия закрепляется хвостом #ref; клон делается заново и удаляется, кэша нет - добавлены init, add, pull, list, check в проекте — манифест .conventions.toml, сборка копий по разу на компонент, маркер локальной части, READING.md рядом - проверки формы развязаны с набором: принимают lang.Vocabulary, а язык копии узнаётся по строке о версии — манифеста рядом с ней нет
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.vakhrushev.me/av/convy/internal/manifest"
|
||||
"git.vakhrushev.me/av/convy/internal/project"
|
||||
)
|
||||
|
||||
// convy add does two things at once, and they are one thing: it writes the
|
||||
// subscription into the manifest and assembles the file. A subscription that
|
||||
// left no file behind, or a file nothing subscribed to, is the state the model
|
||||
// has no name for.
|
||||
|
||||
func runAdd(env Env, args []string) ExitCode {
|
||||
fs := flag.NewFlagSet("convy add", 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 assemble for; needed when there is more than one")
|
||||
topicFlag := fs.String("topic", "", "topic to subscribe to; may also be given as the first argument")
|
||||
|
||||
// The topic is taken off the front before the flags are parsed. The flag
|
||||
// package stops at the first argument that is not a flag, so "convy add
|
||||
// time --for backend" — the form the model writes — would otherwise lose
|
||||
// the component silently.
|
||||
topic := ""
|
||||
rest := args
|
||||
if len(rest) > 0 && !strings.HasPrefix(rest[0], "-") {
|
||||
topic, rest = rest[0], rest[1:]
|
||||
}
|
||||
if err := fs.Parse(rest); err != nil {
|
||||
return Usage
|
||||
}
|
||||
if *topicFlag != "" {
|
||||
if topic != "" && topic != *topicFlag {
|
||||
fmt.Fprintf(env.Err, "the topic is named twice and differently: %q and %q\n", topic, *topicFlag)
|
||||
return Usage
|
||||
}
|
||||
topic = *topicFlag
|
||||
}
|
||||
if rest := fs.Args(); len(rest) > 0 {
|
||||
if topic != "" && topic != rest[0] {
|
||||
fmt.Fprintf(env.Err, "the topic is named twice and differently: %q and %q\n", topic, rest[0])
|
||||
return Usage
|
||||
}
|
||||
topic = rest[0]
|
||||
}
|
||||
|
||||
o, code := openProject(env, *root)
|
||||
if code != OK {
|
||||
return code
|
||||
}
|
||||
defer o.Close()
|
||||
|
||||
if err := distinctDirs(o.Manifest); err != nil {
|
||||
fmt.Fprintln(env.Err, err)
|
||||
return Usage
|
||||
}
|
||||
|
||||
name, c, code := componentOf(env, o.Manifest, *forComponent)
|
||||
if code != OK {
|
||||
return code
|
||||
}
|
||||
|
||||
if topic == "" {
|
||||
if !env.Interactive {
|
||||
fmt.Fprintln(env.Err, "convy add without arguments asks which topic, and there is no terminal to ask on; name the topic as an argument")
|
||||
return Usage
|
||||
}
|
||||
answer, err := newDialogue(env).ask(topicField(o, c))
|
||||
if err != nil {
|
||||
fmt.Fprintln(env.Err, "\ninterrupted, nothing was written")
|
||||
return Usage
|
||||
}
|
||||
topic = answer
|
||||
}
|
||||
|
||||
if !o.Suite.Manifest.TopicLive(topic) {
|
||||
if o.Suite.Manifest.TopicRetired(topic) {
|
||||
fmt.Fprintf(env.Err, "the suite has retired the topic %q: %s\n", topic, o.Suite.Manifest.Topics.Retired[topic])
|
||||
return Usage
|
||||
}
|
||||
fmt.Fprintf(env.Err, "the suite declares no topic %q; it declares: %s\n", topic, strings.Join(o.Suite.Manifest.LiveTopics(), ", "))
|
||||
return Usage
|
||||
}
|
||||
if c.Subscribed(topic) {
|
||||
fmt.Fprintf(env.Err, "the component %q is subscribed to %q already; convy pull reassembles it\n", name, topic)
|
||||
return Usage
|
||||
}
|
||||
|
||||
made, err := project.Assemble(o.Suite, o.Root, c, topic)
|
||||
if err != nil {
|
||||
fmt.Fprintln(env.Err, err)
|
||||
return Failed
|
||||
}
|
||||
|
||||
// The manifest is written after the file: a subscription recorded against
|
||||
// an assembly that failed would send the next pull looking for a copy that
|
||||
// was never made.
|
||||
source, err := os.ReadFile(o.Manifest.Path)
|
||||
if err != nil {
|
||||
fmt.Fprintln(env.Err, err)
|
||||
return Failed
|
||||
}
|
||||
updated, err := manifest.AddToList(source, "components."+name, "topics", topic)
|
||||
if err != nil {
|
||||
fmt.Fprintln(env.Err, err)
|
||||
return Failed
|
||||
}
|
||||
if err := os.WriteFile(o.Manifest.Path, updated, 0o644); err != nil {
|
||||
fmt.Fprintln(env.Err, err)
|
||||
return Failed
|
||||
}
|
||||
|
||||
guide, err := project.Reading(o.LangRoot, o.Suite.Manifest.Language.Reading, o.Root, c.Dir)
|
||||
if err != nil {
|
||||
fmt.Fprintf(env.Err, "warning: %s\n", err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(env.Out, "\n%s → %s\n", topic, made.Path)
|
||||
for _, layer := range made.Layers {
|
||||
fmt.Fprintf(env.Out, " %s\n", layer)
|
||||
}
|
||||
if guide != "" {
|
||||
fmt.Fprintf(env.Out, "\n%s refreshed\n", guide)
|
||||
}
|
||||
fmt.Fprintf(env.Out, "subscribed the component %q in %s\n", name, manifest.ProjectName)
|
||||
return OK
|
||||
}
|
||||
|
||||
// topicField offers the topics the component has not taken yet.
|
||||
func topicField(o *opened, c manifest.Component) Field {
|
||||
var free []string
|
||||
for _, t := range o.Suite.Manifest.LiveTopics() {
|
||||
if !c.Subscribed(t) {
|
||||
free = append(free, t)
|
||||
}
|
||||
}
|
||||
return Field{
|
||||
Flag: "topic",
|
||||
Ask: "Topic",
|
||||
Hint: "A topic is taken whole: the file gathers every layer of it the component fits. " + describeTopics(o, free),
|
||||
Options: free,
|
||||
Check: func(v string) error {
|
||||
if !o.Suite.Manifest.TopicLive(v) {
|
||||
return fmt.Errorf("the suite declares no topic %q", v)
|
||||
}
|
||||
if c.Subscribed(v) {
|
||||
return fmt.Errorf("this component is subscribed to %q already", v)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func describeTopics(o *opened, free []string) string {
|
||||
if len(free) == 0 {
|
||||
return "The component is subscribed to everything the suite has."
|
||||
}
|
||||
parts := make([]string, 0, len(free))
|
||||
for _, t := range free {
|
||||
parts = append(parts, fmt.Sprintf("%s — %s", t, o.Suite.Manifest.Topics.Live[t]))
|
||||
}
|
||||
return "On offer: " + strings.Join(parts, "; ") + "."
|
||||
}
|
||||
Reference in New Issue
Block a user