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 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 }