blob: ae01039a1ac7b00a6a802f0ee36fd60c97bdc906 [file] [log] [blame]
package cfg
import (
"crypto/sha256"
"fmt"
"io/ioutil"
"reflect"
"sort"
"strings"
"github.com/Masterminds/glide/mirrors"
"github.com/Masterminds/glide/util"
"github.com/Masterminds/vcs"
"gopkg.in/yaml.v2"
)
// Config is the top-level configuration object.
type Config struct {
// Name is the name of the package or application.
Name string `yaml:"package"`
// Description is a short description for a package, application, or library.
// This description is similar but different to a Go package description as
// it is for marketing and presentation purposes rather than technical ones.
Description string `json:"description,omitempty"`
// Home is a url to a website for the package.
Home string `yaml:"homepage,omitempty"`
// License provides either a SPDX license or a path to a file containing
// the license. For more information on SPDX see http://spdx.org/licenses/.
// When more than one license an SPDX expression can be used.
License string `yaml:"license,omitempty"`
// Owners is an array of owners for a project. See the Owner type for
// more detail. These can be one or more people, companies, or other
// organizations.
Owners Owners `yaml:"owners,omitempty"`
// Ignore contains a list of packages to ignore fetching. This is useful
// when walking the package tree (including packages of packages) to list
// those to skip.
Ignore []string `yaml:"ignore,omitempty"`
// Exclude contains a list of directories in the local application to
// exclude from scanning for dependencies.
Exclude []string `yaml:"excludeDirs,omitempty"`
// Imports contains a list of all non-development imports for a project. For
// more detail on how these are captured see the Dependency type.
Imports Dependencies `yaml:"import"`
// DevImports contains the test or other development imports for a project.
// See the Dependency type for more details on how this is recorded.
DevImports Dependencies `yaml:"testImport,omitempty"`
}
// A transitive representation of a dependency for importing and exporting to yaml.
type cf struct {
Name string `yaml:"package"`
Description string `yaml:"description,omitempty"`
Home string `yaml:"homepage,omitempty"`
License string `yaml:"license,omitempty"`
Owners Owners `yaml:"owners,omitempty"`
Ignore []string `yaml:"ignore,omitempty"`
Exclude []string `yaml:"excludeDirs,omitempty"`
Imports Dependencies `yaml:"import"`
DevImports Dependencies `yaml:"testImport,omitempty"`
}
// ConfigFromYaml returns an instance of Config from YAML
func ConfigFromYaml(yml []byte) (*Config, error) {
cfg := &Config{}
err := yaml.Unmarshal([]byte(yml), &cfg)
return cfg, err
}
// Marshal converts a Config instance to YAML
func (c *Config) Marshal() ([]byte, error) {
yml, err := yaml.Marshal(&c)
if err != nil {
return []byte{}, err
}
return yml, nil
}
// UnmarshalYAML is a hook for gopkg.in/yaml.v2 in the unmarshalling process
func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
newConfig := &cf{}
if err := unmarshal(&newConfig); err != nil {
return err
}
c.Name = newConfig.Name
c.Description = newConfig.Description
c.Home = newConfig.Home
c.License = newConfig.License
c.Owners = newConfig.Owners
c.Ignore = newConfig.Ignore
c.Exclude = newConfig.Exclude
c.Imports = newConfig.Imports
c.DevImports = newConfig.DevImports
// Cleanup the Config object now that we have it.
err := c.DeDupe()
return err
}
// MarshalYAML is a hook for gopkg.in/yaml.v2 in the marshaling process
func (c *Config) MarshalYAML() (interface{}, error) {
newConfig := &cf{
Name: c.Name,
Description: c.Description,
Home: c.Home,
License: c.License,
Owners: c.Owners,
Ignore: c.Ignore,
Exclude: c.Exclude,
}
i, err := c.Imports.Clone().DeDupe()
if err != nil {
return newConfig, err
}
di, err := c.DevImports.Clone().DeDupe()
if err != nil {
return newConfig, err
}
newConfig.Imports = i
newConfig.DevImports = di
return newConfig, nil
}
// 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
}
// HasIgnore returns true if the given name is listed on the ignore list.
func (c *Config) HasIgnore(name string) bool {
for _, v := range c.Ignore {
// Check for both a name and to make sure sub-packages are ignored as
// well.
if v == name || strings.HasPrefix(name, v+"/") {
return true
}
}
return false
}
// HasExclude returns true if the given name is listed on the exclude list.
func (c *Config) HasExclude(ex string) bool {
ep := normalizeSlash(ex)
for _, v := range c.Exclude {
if vp := normalizeSlash(v); vp == ep {
return true
}
}
return false
}
// Clone performs a deep clone of the Config instance
func (c *Config) Clone() *Config {
n := &Config{}
n.Name = c.Name
n.Description = c.Description
n.Home = c.Home
n.License = c.License
n.Owners = c.Owners.Clone()
n.Ignore = c.Ignore
n.Exclude = c.Exclude
n.Imports = c.Imports.Clone()
n.DevImports = c.DevImports.Clone()
return n
}
// WriteFile writes a Glide YAML file.
//
// This is a convenience function that marshals the YAML and then writes it to
// the given file. If the file exists, it will be clobbered.
func (c *Config) WriteFile(glidepath string) error {
o, err := c.Marshal()
if err != nil {
return err
}
return ioutil.WriteFile(glidepath, o, 0666)
}
// DeDupe consolidates duplicate dependencies on a Config instance
func (c *Config) DeDupe() error {
// Remove duplicates in the imports
var err error
c.Imports, err = c.Imports.DeDupe()
if err != nil {
return err
}
c.DevImports, err = c.DevImports.DeDupe()
if err != nil {
return err
}
// If the name on the config object is part of the imports remove it.
found := -1
for i, dep := range c.Imports {
if dep.Name == c.Name {
found = i
}
}
if found >= 0 {
c.Imports = append(c.Imports[:found], c.Imports[found+1:]...)
}
found = -1
for i, dep := range c.DevImports {
if dep.Name == c.Name {
found = i
}
}
if found >= 0 {
c.DevImports = append(c.DevImports[:found], c.DevImports[found+1:]...)
}
// If something is on the ignore list remove it from the imports.
for _, v := range c.Ignore {
found = -1
for k, d := range c.Imports {
if v == d.Name {
found = k
}
}
if found >= 0 {
c.Imports = append(c.Imports[:found], c.Imports[found+1:]...)
}
found = -1
for k, d := range c.DevImports {
if v == d.Name {
found = k
}
}
if found >= 0 {
c.DevImports = append(c.DevImports[:found], c.DevImports[found+1:]...)
}
}
return nil
}
// AddImport appends dependencies to the import list, deduplicating as we go.
func (c *Config) AddImport(deps ...*Dependency) error {
t := c.Imports
t = append(t, deps...)
t, err := t.DeDupe()
if err != nil {
return err
}
c.Imports = t
return nil
}
// Hash generates a sha256 hash for a given Config
func (c *Config) Hash() (string, error) {
yml, err := c.Marshal()
if err != nil {
return "", err
}
hash := sha256.New()
hash.Write(yml)
return fmt.Sprintf("%x", hash.Sum(nil)), nil
}
// 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
}
// Has checks if a dependency is on a list of dependencies such as import or testImport
func (d Dependencies) Has(name string) bool {
for _, dep := range d {
if dep.Name == name {
return true
}
}
return false
}
// Remove removes a dependency from a list of dependencies
func (d Dependencies) Remove(name string) Dependencies {
found := -1
for i, dep := range d {
if dep.Name == name {
found = i
}
}
if found >= 0 {
copy(d[found:], d[found+1:])
d[len(d)-1] = nil
return d[:len(d)-1]
}
return d
}
// Clone performs a deep clone of Dependencies
func (d Dependencies) Clone() Dependencies {
n := make(Dependencies, 0, len(d))
for _, v := range d {
n = append(n, v.Clone())
}
return n
}
// DeDupe cleans up duplicates on a list of dependencies.
func (d Dependencies) DeDupe() (Dependencies, error) {
checked := map[string]int{}
imports := make(Dependencies, 0, 1)
i := 0
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] = i
imports = append(imports, dep)
i++
} else {
// In here we've encountered a dependency for the second time.
// Make sure the details are the same or return an error.
v := imports[val]
if dep.Reference != v.Reference {
return d, fmt.Errorf("Import %s repeated with different versions '%s' and '%s'", dep.Name, dep.Reference, v.Reference)
}
if dep.Repository != v.Repository || dep.VcsType != v.VcsType {
return d, fmt.Errorf("Import %s repeated with different Repository details", dep.Name)
}
if !reflect.DeepEqual(dep.Os, v.Os) || !reflect.DeepEqual(dep.Arch, v.Arch) {
return d, fmt.Errorf("Import %s repeated with different OS or Architecture filtering", dep.Name)
}
imports[checked[dep.Name]].Subpackages = stringArrayDeDupe(v.Subpackages, dep.Subpackages...)
}
}
return imports, nil
}
// Dependency describes a package that the present package depends upon.
type Dependency struct {
Name string `yaml:"package"`
Reference string `yaml:"version,omitempty"`
Pin string `yaml:"-"`
Repository string `yaml:"repo,omitempty"`
VcsType string `yaml:"vcs,omitempty"`
Subpackages []string `yaml:"subpackages,omitempty"`
Arch []string `yaml:"arch,omitempty"`
Os []string `yaml:"os,omitempty"`
}
// A transitive representation of a dependency for importing and exploting to yaml.
type dep struct {
Name string `yaml:"package"`
Reference string `yaml:"version,omitempty"`
Ref string `yaml:"ref,omitempty"`
Repository string `yaml:"repo,omitempty"`
VcsType string `yaml:"vcs,omitempty"`
Subpackages []string `yaml:"subpackages,omitempty"`
Arch []string `yaml:"arch,omitempty"`
Os []string `yaml:"os,omitempty"`
}
// DependencyFromLock converts a Lock to a Dependency
func DependencyFromLock(lock *Lock) *Dependency {
return &Dependency{
Name: lock.Name,
Reference: lock.Version,
Repository: lock.Repository,
VcsType: lock.VcsType,
Subpackages: lock.Subpackages,
Arch: lock.Arch,
Os: lock.Os,
}
}
// UnmarshalYAML is a hook for gopkg.in/yaml.v2 in the unmarshaling process
func (d *Dependency) UnmarshalYAML(unmarshal func(interface{}) error) error {
newDep := &dep{}
err := unmarshal(&newDep)
if err != nil {
return err
}
d.Name = newDep.Name
d.Reference = newDep.Reference
d.Repository = newDep.Repository
d.VcsType = newDep.VcsType
d.Subpackages = newDep.Subpackages
d.Arch = newDep.Arch
d.Os = newDep.Os
if d.Reference == "" && newDep.Ref != "" {
d.Reference = newDep.Ref
}
// Make sure only legitimate VCS are listed.
d.VcsType = filterVcsType(d.VcsType)
// Get the root name for the package
tn, subpkg := util.NormalizeName(d.Name)
d.Name = tn
if subpkg != "" {
d.Subpackages = append(d.Subpackages, subpkg)
}
// Older versions of Glide had a / prefix on subpackages in some cases.
// Here that's cleaned up. Someday we should be able to remove this.
for k, v := range d.Subpackages {
d.Subpackages[k] = strings.TrimPrefix(v, "/")
}
return nil
}
// MarshalYAML is a hook for gopkg.in/yaml.v2 in the marshaling process
func (d *Dependency) MarshalYAML() (interface{}, error) {
// Make sure we only write the correct vcs type to file
t := filterVcsType(d.VcsType)
newDep := &dep{
Name: d.Name,
Reference: d.Reference,
Repository: d.Repository,
VcsType: t,
Subpackages: d.Subpackages,
Arch: d.Arch,
Os: d.Os,
}
return newDep, nil
}
// Remote returns the remote location to fetch source from. This location is
// the central place where mirrors can alter the location.
func (d *Dependency) Remote() string {
var r string
if d.Repository != "" {
r = d.Repository
} else {
r = "https://" + d.Name
}
f, nr, _ := mirrors.Get(r)
if f {
return nr
}
return r
}
// Vcs returns the VCS type to fetch source from.
func (d *Dependency) Vcs() string {
var r string
if d.Repository != "" {
r = d.Repository
} else {
r = "https://" + d.Name
}
f, _, nv := mirrors.Get(r)
if f {
return nv
}
return d.VcsType
}
// GetRepo retrieves a Masterminds/vcs repo object configured for the root
// of the package being retrieved.
func (d *Dependency) GetRepo(dest string) (vcs.Repo, error) {
// The remote location is either the configured repo or the package
// name as an https url.
remote := d.Remote()
VcsType := d.Vcs()
// If the VCS type has a value we try that first.
if len(VcsType) > 0 && VcsType != "None" {
switch vcs.Type(VcsType) {
case vcs.Git:
return vcs.NewGitRepo(remote, dest)
case vcs.Svn:
return vcs.NewSvnRepo(remote, dest)
case vcs.Hg:
return vcs.NewHgRepo(remote, dest)
case vcs.Bzr:
return vcs.NewBzrRepo(remote, dest)
default:
return nil, fmt.Errorf("Unknown VCS type %s set for %s", VcsType, d.Name)
}
}
// When no type set we try to autodetect.
return vcs.NewRepo(remote, dest)
}
// Clone creates a clone of a Dependency
func (d *Dependency) Clone() *Dependency {
return &Dependency{
Name: d.Name,
Reference: d.Reference,
Pin: d.Pin,
Repository: d.Repository,
VcsType: d.VcsType,
Subpackages: d.Subpackages,
Arch: d.Arch,
Os: d.Os,
}
}
// HasSubpackage returns if the subpackage is present on the dependency
func (d *Dependency) HasSubpackage(sub string) bool {
for _, v := range d.Subpackages {
if sub == v {
return true
}
}
return false
}
// Owners is a list of owners for a project.
type Owners []*Owner
// Clone performs a deep clone of Owners
func (o Owners) Clone() Owners {
n := make(Owners, 0, 1)
for _, v := range o {
n = append(n, v.Clone())
}
return n
}
// Owner describes an owner of a package. This can be a person, company, or
// other organization. This is useful if someone needs to contact the
// owner of a package to address things like a security issue.
type Owner struct {
// Name describes the name of an organization.
Name string `yaml:"name,omitempty"`
// Email is an email address to reach the owner at.
Email string `yaml:"email,omitempty"`
// Home is a url to a website for the owner.
Home string `yaml:"homepage,omitempty"`
}
// Clone creates a clone of a Dependency
func (o *Owner) Clone() *Owner {
return &Owner{
Name: o.Name,
Email: o.Email,
Home: o.Home,
}
}
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)
}
}
sort.Strings(s)
return s
}
func filterVcsType(vcs string) string {
switch vcs {
case "git", "hg", "bzr", "svn":
return vcs
case "mercurial":
return "hg"
case "bazaar":
return "bzr"
case "subversion":
return "svn"
default:
return ""
}
}
func normalizeSlash(k string) string {
return strings.Replace(k, "\\", "/", -1)
}