blob: b1a6e2cd314260c6e6493f7519af2f1eea3bcb11 [file] [log] [blame] [edit]
package cmd
import (
"bytes"
"fmt"
"io"
"os"
"reflect"
"strings"
"github.com/Masterminds/cookoo"
v "github.com/Masterminds/vcs"
"github.com/kylelemons/go-gypsy/yaml"
)
// ParseYaml parses the glide.yaml format and returns a Configuration object.
//
// Params:
// - filename (string): YAML filename as a string
//
// Context:
// - yaml.File: This puts the parsed YAML file into the context.
//
// Returns:
// - *Config: The configuration.
func ParseYaml(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) {
fname := p.Get("filename", "glide.yaml").(string)
//conf := new(Config)
f, err := yaml.ReadFile(fname)
if err != nil {
return nil, err
}
c.Put("yaml.File", f)
return FromYaml(f.Root)
}
// ParseYamlString parses a YAML string. This is similar but different to
// ParseYaml that parses an external file.
//
// Params:
// - yaml (string): YAML as a string.
//
// Returns:
// - *Config: The configuration.
func ParseYamlString(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) {
yamlString := p.Get("yaml", "").(string)
// Unfortunately, this does not wrap the root in a YAML file object.
root, err := yaml.Parse(bytes.NewBufferString(yamlString))
if err != nil {
return nil, err
}
return FromYaml(root)
}
// WriteYaml writes a yaml.Node to the console as a string.
//
// Params:
// - yaml.Node (yaml.Node): A yaml.Node to render.
// - out (io.Writer): An output stream to write to. Default is os.Stdout.
// - filename (string): If set, the file will be opened and the content will be written to it.
func WriteYaml(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) {
top := p.Get("yaml.Node", yaml.Scalar("nothing to print")).(yaml.Node)
toStdout := p.Get("toStdout", true).(bool)
var out io.Writer
if nn, ok := p.Has("filename"); ok && len(nn.(string)) > 0 {
file, err := os.Create(nn.(string))
if err != nil {
}
defer file.Close()
out = io.Writer(file)
fmt.Fprint(out, yaml.Render(top))
} else if toStdout {
out = p.Get("out", os.Stdout).(io.Writer)
fmt.Fprint(out, yaml.Render(top))
}
// Otherwise we supress output.
return true, nil
}
// MergeToYaml converts a Config object and a yaml.File to a single yaml.File.
//
// Params:
// - conf (*Config): The configuration to merge.
// - overwriteImports (bool, default true): If this is true, old config will
// overwritten. If false, we attempt to merge the old and new config, with
// preference to the old.
//
// Returns:
// - The root yaml.Node of the modified config.
//
// Uses:
// - cxt.Get("yaml.File") as the source for the YAML file.
func MergeToYaml(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) {
root := c.Get("yaml.File", nil).(*yaml.File).Root
cfg := p.Get("conf", nil).(*Config)
overwrite := p.Get("overwriteImports", true).(bool)
rootMap, ok := root.(yaml.Map)
if !ok {
return nil, fmt.Errorf("Expected root node to be a map.")
}
if len(cfg.Name) > 0 {
rootMap["package"] = yaml.Scalar(cfg.Name)
}
if cfg.InCommand != "" {
rootMap["incmd"] = yaml.Scalar(cfg.InCommand)
}
if cfg.Flatten == true {
rootMap["flatten"] = yaml.Scalar("true")
}
if overwrite {
// Imports
imports := make([]yaml.Node, len(cfg.Imports))
for i, imp := range cfg.Imports {
imports[i] = imp.ToYaml()
}
rootMap["import"] = yaml.List(imports)
} else {
var err error
rootMap, err = mergeImports(rootMap, cfg)
if err != nil {
Warn("Problem merging imports: %s\n", err)
}
}
return root, nil
}
// mergeImports merges the imports on a *Config into an existing YAML doc.
func mergeImports(root yaml.Map, cfg *Config) (yaml.Map, error) {
left, err := FromYaml(root)
if err != nil {
return root, err
}
leftnames := make(map[string]bool, len(left.Imports))
for _, i := range left.Imports {
leftnames[i.Name] = true
}
for _, right := range cfg.Imports {
if _, ok := leftnames[right.Name]; !ok {
left.Imports = append(left.Imports, right)
}
}
return left.ToYaml().(yaml.Map), nil
}
// AddDependencies adds a list of *Dependency objects to the given *Config.
//
// This is used to merge in packages from other sources or config files.
func AddDependencies(c cookoo.Context, p *cookoo.Params) (interface{}, cookoo.Interrupt) {
deps := p.Get("dependencies", []*Dependency{}).([]*Dependency)
config := p.Get("conf", nil).(*Config)
// Make a set of existing package names for quick comparison.
pkgSet := make(map[string]bool, len(config.Imports))
for _, p := range config.Imports {
pkgSet[p.Name] = true
}
// If a dep is not already present, add it.
for _, dep := range deps {
if _, ok := pkgSet[dep.Name]; ok {
Warn("Package %s is already in glide.yaml. Skipping.\n", dep.Name)
continue
}
config.Imports = append(config.Imports, dep)
}
return true, nil
}
func valOrEmpty(key string, store map[string]yaml.Node) string {
val, ok := store[key]
if !ok {
return ""
}
return strings.TrimSpace(val.(yaml.Scalar).String())
}
// boolOrDefault returns a bool, with the dft returned if there is an error or the value is not true/false
func boolOrDefault(key string, store map[string]yaml.Node, dft bool) bool {
val, ok := store[key]
if !ok {
return dft
}
switch val.(yaml.Scalar).String() {
case "true":
return true
case "false":
return false
default:
return dft
}
}
// valOrList gets a single value or a list of values.
//
// Supports syntaxes like:
//
// subpkg: foo
//
// and
//
// supkpg:
// -foo
// -bar
func valOrList(key string, store map[string]yaml.Node) []string {
val, ok := store[key]
subpackages := []string{}
if !ok {
return subpackages
}
pkgs, ok := val.(yaml.List)
if !ok {
// Special case: Allow 'subpackages: justOne'
if one, ok := val.(yaml.Scalar); ok {
return []string{one.String()}
}
Warn("Expected list of subpackages.\n")
return subpackages
}
for _, pkg := range pkgs {
subpackages = append(subpackages, pkg.(yaml.Scalar).String())
}
return subpackages
}
func getVcsType(store map[string]yaml.Node) string {
val, ok := store["vcs"]
if !ok {
return string(v.NoVCS)
}
name := val.(yaml.Scalar).String()
switch name {
case "git", "hg", "bzr", "svn":
return name
case "mercurial":
return "hg"
case "bazaar":
return "bzr"
case "subversion":
return "svn"
default:
return ""
}
}
// NormalizeName takes a package name and normalizes it to the top level package.
//
// For example, golang.org/x/crypto/ssh becomes golang.org/x/crypto. 'ssh' is
// returned as extra data.
func NormalizeName(name string) (string, string) {
parts := strings.SplitN(name, "/", 4)
extra := ""
if len(parts) < 3 {
return name, extra
}
if len(parts) == 4 {
extra = parts[3]
}
return strings.Join(parts[0:3], "/"), extra
}
// Config is the top-level configuration object.
type Config struct {
Parent *Config
Name string
Imports Dependencies
DevImports Dependencies
// InCommand is the default shell command run to start a 'glide in'
// session.
InCommand string
Flatten bool
}
// HasDependency returns true if the given name is listed as an import or dev import.
func (c *Config) HasDependency(name string) bool {
for _, d := range c.Imports {
if d.Name == name {
return true
}
}
for _, d := range c.DevImports {
if d.Name == name {
return true
}
}
return false
}
// HasRecursiveDependency returns true if this config or one of it's parents has this dependency
func (c *Config) HasRecursiveDependency(name string) bool {
if c.HasDependency(name) == true {
return true
} else if c.Parent != nil {
return c.Parent.HasRecursiveDependency(name)
}
return false
}
// GetRoot follows the Parent down to the top node
func (c *Config) GetRoot() *Config {
if c.Parent != nil {
return c.Parent.GetRoot()
}
return c
}
// FromYaml creates a *Config from a YAML node.
func FromYaml(top yaml.Node) (*Config, error) {
conf := new(Config)
vals, ok := top.(yaml.Map)
if !ok {
return conf, fmt.Errorf("Top YAML node must be a map.")
}
if name, ok := vals["package"]; ok {
conf.Name = name.(yaml.Scalar).String()
} else {
Warn("The 'package' directive is required in Glide YAML.\n")
conf.Name = "main"
}
// Allow the user to override the behavior of `glide in`.
if incmd, ok := vals["incmd"]; ok {
conf.InCommand = incmd.(yaml.Scalar).String()
}
// Package level Flatten
conf.Flatten = boolOrDefault("flatten", vals, false)
conf.Imports = make(Dependencies, 0, 1)
if imp, ok := vals["import"]; ok {
imports, ok := imp.(yaml.List)
if ok {
for _, v := range imports {
dep, err := DependencyFromYaml(v)
if err != nil {
Warn("Could not add a dependency: %s\n", err)
}
conf.Imports = append(conf.Imports, dep)
}
}
}
i, err := conf.Imports.DeDupe()
if err != nil {
return conf, err
}
conf.Imports = i
// Same for (experimental) devimport.
// These are currently unused. Not sure what we'll do with it yet.
conf.DevImports = make(Dependencies, 0, 0)
if imp, ok := vals["devimport"]; ok {
imports, ok := imp.(yaml.List)
if ok {
for _, v := range imports {
dep, err := DependencyFromYaml(v)
if err != nil {
Warn("Could not add a dependency: %s\n", err)
}
conf.DevImports = append(conf.DevImports, dep)
}
}
}
conf.DevImports, err = conf.DevImports.DeDupe()
if err != nil {
return conf, err
}
return conf, nil
}
// ToYaml returns a yaml.Map containing the data from Config.
func (c *Config) ToYaml() yaml.Node {
cfg := make(map[string]yaml.Node, 5)
cfg["package"] = yaml.Scalar(c.Name)
if len(c.InCommand) > 0 {
cfg["incmd"] = yaml.Scalar(c.InCommand)
}
if c.Flatten == true {
cfg["flatten"] = yaml.Scalar("true")
}
imps := make([]yaml.Node, len(c.Imports))
for i, imp := range c.Imports {
imps[i] = imp.ToYaml()
}
devimps := make([]yaml.Node, len(c.DevImports))
for i, dimp := range c.DevImports {
devimps[i] = dimp.ToYaml()
}
// Fixed in 0.5.0. Prior to that, these were not being printed. Worried
// that the "fix" might introduce an unintended side effect.
if len(imps) > 0 {
cfg["import"] = yaml.List(imps)
}
if len(devimps) > 0 {
cfg["devimport"] = yaml.List(devimps)
}
return yaml.Map(cfg)
}
// Dependency describes a package that the present package depends upon.
type Dependency struct {
Name, Reference, Repository string
VcsType string
Subpackages, Arch, Os []string
UpdateAsVendored bool
Flatten bool
Flattened bool
}
// DependencyFromYaml creates a dependency from a yaml.Node.
func DependencyFromYaml(node yaml.Node) (*Dependency, error) {
pkg, ok := node.(yaml.Map)
if !ok {
return &Dependency{}, fmt.Errorf("Expected yaml.Node to be a dependency map.")
}
dep := &Dependency{
Name: valOrEmpty("package", pkg),
Reference: valOrEmpty("ref", pkg),
VcsType: getVcsType(pkg),
Repository: valOrEmpty("repo", pkg),
Subpackages: valOrList("subpackages", pkg),
Arch: valOrList("arch", pkg),
Os: valOrList("os", pkg),
Flatten: boolOrDefault("flatten", pkg, false),
}
if dep.Name != "" {
orig := dep.Name
dep.Name = getRepoRootFromPackage(orig)
// The package name listed was actually a sub-package. Modify the
// config to reflect reality.
if orig != dep.Name {
subpkg := strings.TrimPrefix(orig, dep.Name)
if len(subpkg) > 0 && subpkg != "/" {
dep.Subpackages = append(dep.Subpackages, strings.TrimPrefix(subpkg, "/"))
}
}
}
return dep, nil
}
// GetRepo retrieves a Masterminds/vcs repo object configured for the root
// of the package being retrieved.
func (d *Dependency) GetRepo(dest string) (v.Repo, error) {
// The remote location is either the configured repo or the package
// name as an https url.
var remote string
if len(d.Repository) > 0 {
remote = d.Repository
} else {
remote = "https://" + d.Name
}
// If the VCS type has a value we try that first.
if len(d.VcsType) > 0 && d.VcsType != "None" {
switch v.Type(d.VcsType) {
case v.Git:
return v.NewGitRepo(remote, dest)
case v.Svn:
return v.NewSvnRepo(remote, dest)
case v.Hg:
return v.NewHgRepo(remote, dest)
case v.Bzr:
return v.NewBzrRepo(remote, dest)
default:
return nil, fmt.Errorf("Unknown VCS type %s set for %s", d.VcsType, d.Name)
}
}
// When no type set we try to autodetect.
return v.NewRepo(remote, dest)
}
func stripScheme(u string) string {
parts := strings.Split(u, "://")
if len(parts) > 1 {
return parts[1]
}
return u
}
// ToYaml converts a *Dependency to a YAML Map node.
func (d *Dependency) ToYaml() yaml.Node {
dep := make(map[string]yaml.Node, 8)
dep["package"] = yaml.Scalar(d.Name)
if len(d.Subpackages) > 0 {
subp := make([]yaml.Node, len(d.Subpackages))
for i, item := range d.Subpackages {
subp[i] = yaml.Scalar(item)
}
dep["subpackages"] = yaml.List(subp)
}
vcs := d.VcsType
if len(vcs) > 0 {
dep["vcs"] = yaml.Scalar(vcs)
}
if len(d.Reference) > 0 {
dep["ref"] = yaml.Scalar(d.Reference)
}
if len(d.Repository) > 0 {
dep["repo"] = yaml.Scalar(d.Repository)
}
if len(d.Arch) > 0 {
archs := make([]yaml.Node, len(d.Arch))
for i, a := range d.Arch {
archs[i] = yaml.Scalar(a)
}
dep["arch"] = yaml.List(archs)
}
if len(d.Os) > 0 {
oses := make([]yaml.Node, len(d.Os))
for i, a := range d.Os {
oses[i] = yaml.Scalar(a)
}
dep["os"] = yaml.List(oses)
}
// Note, the yaml package we use sorts strings of scalars so flatten
// will always be the top item.
if d.Flatten == true {
dep["flatten"] = yaml.Scalar("true")
}
return yaml.Map(dep)
}
// Dependencies is a collection of Dependency
type Dependencies []*Dependency
// Get a dependency by name
func (d Dependencies) Get(name string) *Dependency {
for _, dep := range d {
if dep.Name == name {
return dep
}
}
return nil
}
// DeDupe cleans up duplicates on a list of dependencies.
func (d Dependencies) DeDupe() (Dependencies, error) {
checked := map[string]*Dependency{}
for _, dep := range d {
// The first time we encounter a dependency add it to the list
if val, ok := checked[dep.Name]; !ok {
checked[dep.Name] = dep
} else {
// In here we've encountered a dependency for the second time.
// Make sure the details are the same or return an error.
if dep.Reference != val.Reference {
return d, fmt.Errorf("Import %s repeated with different versions '%s' and '%s'", dep.Name, dep.Reference, val.Reference)
}
if dep.Repository != val.Repository || dep.VcsType != val.VcsType {
return d, fmt.Errorf("Import %s repeated with different Repository details", dep.Name)
}
if !reflect.DeepEqual(dep.Os, val.Os) || !reflect.DeepEqual(dep.Arch, val.Arch) {
return d, fmt.Errorf("Import %s repeated with different OS or Architecture filtering", dep.Name)
}
if dep.Flatten != val.Flatten {
Warn("Import %s repeated in glide.yaml with differing flatten values. Flattening.", dep.Name)
checked[dep.Name].Flatten = true
}
checked[dep.Name].Subpackages = stringArrayDeDupe(checked[dep.Name].Subpackages, dep.Subpackages...)
}
}
imports := make(Dependencies, 0, 1)
for _, dep := range checked {
imports = append(imports, dep)
}
return imports, nil
}
func stringArrayDeDupe(s []string, items ...string) []string {
for _, item := range items {
exists := false
for _, v := range s {
if v == item {
exists = true
}
}
if !exists {
s = append(s, item)
}
}
return s
}