blob: b4dd65c68b9b2d6406e88575bcd12fbabb77a9ce [file] [log] [blame]
// Copyright 2019 The CUE Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cmd
import (
"bytes"
"errors"
"fmt"
"html/template"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"cuelang.org/go/cue"
"cuelang.org/go/cue/build"
"cuelang.org/go/cue/format"
"cuelang.org/go/cue/load"
"cuelang.org/go/cue/parser"
"cuelang.org/go/internal"
)
func newAddCmd(c *Command) *cobra.Command {
cmd := &cobra.Command{
// TODO: this command is still experimental, don't show it in
// the documentation just yet.
Hidden: true,
Use: "add <glob> [--list]",
Short: "bulk append to CUE files",
Long: `Append a common snippet of CUE to many files and commit atomically.
`,
RunE: mkRunE(c, runAdd),
}
f := cmd.Flags()
f.Bool(string(flagList), false,
"text executed as Go template with instance info")
f.BoolP(string(flagDryrun), "n", false,
"only run simulation")
return cmd
}
func runAdd(cmd *Command, args []string) (err error) {
return doAdd(cmd, stdin, args)
}
var stdin io.Reader = os.Stdin
func doAdd(cmd *Command, stdin io.Reader, args []string) (err error) {
// Offsets at which to restore original files, if any, if any of the
// appends fail.
// Ideally this is placed below where it is used, but we want to make
// absolutely sure that the error variable used in defer is the named
// returned value and not some shadowed value.
originals := []originalFile{}
defer func() {
if err != nil {
restoreOriginals(cmd, originals)
}
}()
// build instance cache
builds := map[string]*build.Instance{}
getBuild := func(path string) *build.Instance {
if b, ok := builds[path]; ok {
return b
}
b := load.Instances([]string{path}, nil)[0]
builds[path] = b
return b
}
// determine file set.
todo := []*fileInfo{}
done := map[string]bool{}
for _, arg := range args {
dir, base := filepath.Split(arg)
dir = filepath.Clean(dir)
matches, err := filepath.Glob(dir)
if err != nil {
return err
}
for _, m := range matches {
if fi, err := os.Stat(m); err != nil || !fi.IsDir() {
continue
}
file := filepath.Join(m, base)
if done[file] {
continue
}
if s := filepath.ToSlash(file); strings.HasPrefix(s, "pkg/") || strings.Contains(s, "/pkg/") {
continue
}
done[file] = true
fi, err := initFile(cmd, file, getBuild)
if err != nil {
return err
}
todo = append(todo, fi)
b := fi.build
if flagList.Bool(cmd) && (b == nil) {
return fmt.Errorf("instance info not available for %s", fi.filename)
}
}
}
// Read text to be appended.
text, err := ioutil.ReadAll(stdin)
if err != nil {
return err
}
var tmpl *template.Template
if flagList.Bool(cmd) {
tmpl, err = template.New("append").Parse(string(text))
if err != nil {
return err
}
}
for _, fi := range todo {
if tmpl == nil {
fi.contents.Write(text)
continue
}
if err := tmpl.Execute(fi.contents, fi.build); err != nil {
return err
}
}
if flagDryrun.Bool(cmd) {
stdout := cmd.OutOrStdout()
for _, fi := range todo {
fmt.Fprintln(stdout, "---", fi.filename)
_, _ = io.Copy(stdout, fi.contents)
}
return nil
}
// All verified. Execute the todo plan
for _, fi := range todo {
fo, err := fi.appendAndCheck()
if err != nil {
return err
}
originals = append(originals, fo)
}
// Verify resulting builds
for _, fi := range todo {
builds = map[string]*build.Instance{}
b := getBuild(fi.buildArg)
if b.Err != nil {
return b.Err
}
i := cue.Build([]*build.Instance{b})[0]
if i.Err != nil {
return i.Err
}
if err := i.Value().Validate(); err != nil {
return i.Err
}
}
return nil
}
type originalFile struct {
filename string
contents []byte
}
func restoreOriginals(cmd *Command, originals []originalFile) {
for _, fo := range originals {
if err := fo.restore(); err != nil {
fmt.Fprintln(cmd.Stderr(), "Error restoring file: ", err)
}
}
}
func (fo *originalFile) restore() error {
if len(fo.contents) == 0 {
return os.Remove(fo.filename)
}
return ioutil.WriteFile(fo.filename, fo.contents, 0644)
}
type fileInfo struct {
filename string
buildArg string
contents *bytes.Buffer
build *build.Instance
}
func initFile(cmd *Command, file string, getBuild func(path string) *build.Instance) (todo *fileInfo, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("init file: %v", err)
}
}()
dir := filepath.Dir(file)
todo = &fileInfo{file, dir, &bytes.Buffer{}, nil}
if fi, err := os.Stat(file); err != nil {
if !os.IsNotExist(err) {
return nil, err
}
// File does not exist
b := getBuild(dir)
todo.build = b
pkg := flagPackage.String(cmd)
if pkg != "" {
// TODO: do something more intelligent once the package name is
// computed on a module basis, even for empty directories.
b.PkgName = pkg
b.Err = nil
} else {
pkg = b.PkgName
}
if pkg == "" {
return nil, errors.New("must specify package using -p for new files")
}
todo.buildArg = file
fmt.Fprintf(todo.contents, "package %s\n\n", pkg)
} else {
if fi.IsDir() {
return nil, fmt.Errorf("cannot append to directory %s", file)
}
f, err := parser.ParseFile(file, nil)
if err != nil {
return nil, err
}
if _, pkgName, _ := internal.PackageInfo(f); pkgName != "" {
if pkg := flagPackage.String(cmd); pkg != "" && pkgName != pkg {
return nil, fmt.Errorf("package mismatch (%s vs %s) for file %s", pkgName, pkg, file)
}
todo.build = getBuild(dir)
} else {
if pkg := flagPackage.String(cmd); pkg != "" {
return nil, fmt.Errorf("file %s has no package clause but package %s requested", file, pkg)
}
todo.build = getBuild(file)
todo.buildArg = file
}
}
return todo, nil
}
func (fi *fileInfo) appendAndCheck() (fo originalFile, err error) {
// Read original file
b, err := ioutil.ReadFile(fi.filename)
if err == nil {
fo.filename = fi.filename
fo.contents = b
} else if !os.IsNotExist(err) {
return originalFile{}, err
}
if !bytes.HasSuffix(b, []byte("\n")) {
b = append(b, '\n')
}
b = append(b, fi.contents.Bytes()...)
b, err = format.Source(b)
if err != nil {
return originalFile{}, err
}
if err = ioutil.WriteFile(fi.filename, b, 0644); err != nil {
// Just in case, attempt to restore original file.
_ = fo.restore()
return originalFile{}, err
}
return fo, nil
}