| package cfg |
| |
| import ( |
| "crypto/sha256" |
| "encoding/hex" |
| "fmt" |
| "io/ioutil" |
| "sort" |
| "strconv" |
| "strings" |
| |
| "github.com/Masterminds/glide/mirrors" |
| "github.com/Masterminds/vcs" |
| "github.com/sdboyer/gps" |
| "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"` |
| |
| // Imports contains a list of all dependency constraints for a project. For |
| // more detail on how these are captured see the Dependency type. |
| // TODO rename |
| // TODO mapify |
| Imports Dependencies `yaml:"dependencies"` |
| |
| // DevImports contains the test or other development dependency constraints |
| // for a project. See the Dependency type for more details on how this is |
| // recorded. |
| // TODO rename |
| // TODO mapify |
| DevImports Dependencies `yaml:"testDependencies"` |
| } |
| |
| // 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"` |
| Imports Dependencies `yaml:"dependencies,omitempty"` |
| DevImports Dependencies `yaml:"testDependencies,omitempty"` |
| // these fields guarantee that this struct fails to unmarshal legacy yamls |
| Compat int `yaml:"import,omitempty"` |
| Compat2 int `yaml:"testImport,omitempty"` |
| } |
| |
| // ConfigFromYaml returns an instance of Config from YAML |
| func ConfigFromYaml(yml []byte) (cfg *Config, legacy bool, err error) { |
| cfg = &Config{} |
| err = yaml.Unmarshal(yml, cfg) |
| if err != nil { |
| lcfg := &lConfig1{} |
| err = yaml.Unmarshal(yml, &lcfg) |
| if err == nil { |
| legacy = true |
| cfg, err = lcfg.Convert() |
| } |
| } |
| |
| return |
| } |
| |
| // 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.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, |
| } |
| 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 |
| } |
| |
| // DependencyConstraints lists all the non-test dependency constraints |
| // described in a glide manifest in a way gps will understand. |
| func (c *Config) DependencyConstraints() gps.ProjectConstraints { |
| return gpsifyDeps(c.Imports) |
| } |
| |
| // TestDependencyConstraints lists all the test dependency constraints described |
| // in a glide manifest in a way gps will understand. |
| func (c *Config) TestDependencyConstraints() gps.ProjectConstraints { |
| return gpsifyDeps(c.DevImports) |
| } |
| |
| func gpsifyDeps(deps Dependencies) gps.ProjectConstraints { |
| cp := make(gps.ProjectConstraints, len(deps)) |
| for _, d := range deps { |
| cp[gps.ProjectRoot(d.Name)] = gps.ProjectProperties{ |
| NetworkName: d.Repository, |
| Constraint: d.GetConstraint(), |
| } |
| } |
| |
| return cp |
| } |
| |
| func (c *Config) IgnorePackages() map[string]bool { |
| m := make(map[string]bool) |
| for _, ig := range c.Ignore { |
| m[ig] = true |
| } |
| return m |
| } |
| |
| func (c *Config) Overrides() gps.ProjectConstraints { |
| return nil |
| } |
| |
| // 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 |
| } |
| |
| // 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.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] |
| // Have to do string-based comparison |
| if dep.ConstraintsEq(*v) { |
| return d, fmt.Errorf("Import %s repeated with different versions '%s' and '%s'", dep.Name, dep.GetConstraint(), v.GetConstraint()) |
| } |
| if dep.Repository != v.Repository { |
| return d, fmt.Errorf("Import %s repeated with different Repository details", dep.Name) |
| } |
| } |
| } |
| |
| return imports, nil |
| } |
| |
| // Dependency describes a package that the present package depends upon. |
| type Dependency struct { |
| Name string |
| VcsType string // TODO remove |
| Repository string |
| Branch string |
| Version string |
| } |
| |
| // A transitive representation of a dependency for yaml import/export. |
| type dep struct { |
| Name string `yaml:"package"` |
| Version string `yaml:"version,omitempty"` |
| Branch string `yaml:"branch,omitempty"` |
| Repository string `yaml:"repo,omitempty"` |
| } |
| |
| // DependencyFromLock converts a Lock to a Dependency |
| func DependencyFromLock(lock *Lock) *Dependency { |
| d := &Dependency{ |
| Name: lock.Name, |
| Repository: lock.Repository, |
| } |
| |
| // Because it's not allowed to have both, if we see both, prefer version |
| // over branch |
| if lock.Version != "" { |
| d.Version = lock.Version |
| } else if lock.Branch != "" { |
| d.Branch = lock.Branch |
| } else { |
| d.Version = lock.Revision |
| } |
| |
| return d |
| } |
| |
| // GetConstraint constructs an appropriate gps.Constraint from the Dependency's |
| // string input data. |
| func (d Dependency) GetConstraint() gps.Constraint { |
| // If neither or both Version and Branch are set, accept anything |
| if d.IsUnconstrained() { |
| return gps.Any() |
| } else if d.Version != "" { |
| return DeduceConstraint(d.Version) |
| } else { |
| // only case left is a non-empty branch |
| return gps.NewBranch(d.Branch) |
| } |
| } |
| |
| // IsUnconstrained indicates if this dependency has no constraint information, |
| // version or branch. |
| func (d Dependency) IsUnconstrained() bool { |
| return (d.Version != "" && d.Branch != "") || (d.Version == "" && d.Branch == "") |
| } |
| |
| // ConstraintsEq checks if the constraints on two Dependency are exactly equal. |
| func (d Dependency) ConstraintsEq(d2 Dependency) bool { |
| // Having both branch and version set is always an error, so if either have |
| // it, then return false |
| if (d.Version != "" && d.Branch != "") || (d2.Version != "" && d2.Branch != "") { |
| return false |
| } |
| // Neither being set, though, is OK |
| if (d.Version == "" && d.Branch == "") || (d2.Version == "" && d2.Branch == "") { |
| return true |
| } |
| |
| // Now, xors |
| if d.Version != "" && d.Version == d2.Version { |
| return true |
| } |
| if d.Branch == d2.Branch { |
| return true |
| } |
| return false |
| } |
| |
| // 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 |
| } |
| |
| if newDep.Version != "" && newDep.Branch != "" { |
| return fmt.Errorf("Cannot set both a both a branch and a version constraint for %q", d.Name) |
| } |
| |
| d.Name = newDep.Name |
| d.Repository = newDep.Repository |
| d.Version = newDep.Version |
| d.Branch = newDep.Branch |
| |
| return nil |
| } |
| |
| // DeduceConstraint tries to puzzle out what kind of version is given in a string - |
| // semver, a revision, or as a fallback, a plain tag |
| func DeduceConstraint(s string) gps.Constraint { |
| // always semver if we can |
| c, err := gps.NewSemverConstraint(s) |
| if err == nil { |
| return c |
| } |
| |
| slen := len(s) |
| if slen == 40 { |
| if _, err = hex.DecodeString(s); err == nil { |
| // Whether or not it's intended to be a SHA1 digest, this is a |
| // valid byte sequence for that, so go with Revision. This |
| // covers git and hg |
| return gps.Revision(s) |
| } |
| } |
| // Next, try for bzr, which has a three-component GUID separated by |
| // dashes. There should be two, but the email part could contain |
| // internal dashes |
| if strings.Count(s, "-") >= 2 { |
| // Work from the back to avoid potential confusion from the email |
| i3 := strings.LastIndex(s, "-") |
| // Skip if - is last char, otherwise this would panic on bounds err |
| if slen == i3+1 { |
| return gps.NewVersion(s) |
| } |
| |
| if _, err = hex.DecodeString(s[i3+1:]); err == nil { |
| i2 := strings.LastIndex(s[:i3], "-") |
| if _, err = strconv.ParseUint(s[i2+1:i3], 10, 64); err == nil { |
| // Getting this far means it'd pretty much be nuts if it's not a |
| // bzr rev, so don't bother parsing the email. |
| return gps.Revision(s) |
| } |
| } |
| } |
| |
| // If not a plain SHA1 or bzr custom GUID, assume a plain version. |
| // |
| // svn, you ask? lol, madame. lol. |
| return gps.NewVersion(s) |
| } |
| |
| // MarshalYAML is a hook for gopkg.in/yaml.v2 in the marshaling process |
| func (d *Dependency) MarshalYAML() (interface{}, error) { |
| newDep := &dep{ |
| Name: d.Name, |
| Repository: d.Repository, |
| Version: d.Version, |
| Branch: d.Branch, |
| } |
| |
| 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. |
| // TODO remove |
| 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 { |
| var d2 Dependency |
| d2 = *d |
| return &d2 |
| } |
| |
| // 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) |
| } |