| package cfg |
| |
| import ( |
| "crypto/sha256" |
| "encoding/hex" |
| "fmt" |
| "io/ioutil" |
| "sort" |
| "strings" |
| "time" |
| |
| "github.com/sdboyer/gps" |
| |
| "gopkg.in/yaml.v2" |
| ) |
| |
| // Lockfile represents a glide.lock file. |
| type Lockfile struct { |
| Hash string `yaml:"hash"` |
| Updated time.Time `yaml:"updated"` |
| Imports Locks `yaml:"imports"` |
| DevImports Locks `yaml:"testImports"` // TODO remove and fold in as prop |
| } |
| |
| // LockfileFromSolverLock transforms a gps.Lock into a glide *Lockfile. |
| func LockfileFromSolverLock(r gps.Lock) (*Lockfile, error) { |
| if r == nil { |
| return nil, fmt.Errorf("no gps lock data provided to transform") |
| } |
| |
| // Create and write out a new lock file from the result |
| lf := &Lockfile{ |
| Hash: hex.EncodeToString(r.InputHash()), |
| Updated: time.Now(), |
| } |
| |
| for _, p := range r.Projects() { |
| pi := p.Ident() |
| l := &Lock{ |
| Name: string(pi.ProjectRoot), |
| } |
| |
| if l.Name != pi.NetworkName && pi.NetworkName != "" { |
| l.Repository = pi.NetworkName |
| } |
| |
| v := p.Version() |
| // There's (currently) no way gps can emit a non-paired version in a |
| // solution, so this unchecked type assertion should be safe. |
| // |
| // TODO might still be better to check and return out with an err if |
| // not, though |
| switch tv := v.(type) { |
| case gps.Revision: |
| l.Revision = tv.String() |
| case gps.PairedVersion: |
| l.Revision = v.(gps.PairedVersion).Underlying().String() |
| switch v.Type() { |
| case "branch": |
| l.Branch = v.String() |
| case "semver", "version": |
| l.Version = v.String() |
| } |
| case gps.UnpairedVersion: |
| // this should not be possible - error if we hit it |
| return nil, fmt.Errorf("should not be possible - gps returned an unpaired version for %s", pi) |
| } |
| |
| lf.Imports = append(lf.Imports, l) |
| } |
| |
| return lf, nil |
| } |
| |
| // LockfileFromYaml returns an instance of Lockfile from YAML |
| func LockfileFromYaml(yml []byte) (*Lockfile, bool, error) { |
| lock := &Lockfile{} |
| err := yaml.Unmarshal([]byte(yml), lock) |
| if err == nil { |
| return lock, false, nil |
| } |
| |
| llock := &lLockfile1{} |
| err2 := yaml.Unmarshal([]byte(yml), llock) |
| if err2 != nil { |
| return nil, false, err2 |
| } |
| return llock.Convert(), true, nil |
| } |
| |
| // Marshal converts a Lockfile instance to YAML |
| func (lf *Lockfile) Marshal() ([]byte, error) { |
| sort.Sort(lf.Imports) |
| sort.Sort(lf.DevImports) |
| yml, err := yaml.Marshal(&lf) |
| if err != nil { |
| return []byte{}, err |
| } |
| return yml, nil |
| } |
| |
| // MarshalYAML is a hook for gopkg.in/yaml.v2. |
| // It sorts import subpackages lexicographically for reproducibility. |
| func (lf *Lockfile) MarshalYAML() (interface{}, error) { |
| // Ensure elements on testImport don't already exist on import. |
| var newDI Locks |
| var found bool |
| for _, imp := range lf.DevImports { |
| found = false |
| for i := 0; i < len(lf.Imports); i++ { |
| if lf.Imports[i].Name == imp.Name { |
| found = true |
| if lf.Imports[i].Version != imp.Version { |
| return lf, fmt.Errorf("Generating lock YAML produced conflicting versions of %s. import (%s), testImport (%s)", imp.Name, lf.Imports[i].Version, imp.Version) |
| } |
| } |
| } |
| |
| if !found { |
| newDI = append(newDI, imp) |
| } |
| } |
| lf.DevImports = newDI |
| |
| return lf, nil |
| } |
| |
| // WriteFile writes a Glide lock 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 (lf *Lockfile) WriteFile(lockpath string) error { |
| o, err := lf.Marshal() |
| if err != nil { |
| return err |
| } |
| return ioutil.WriteFile(lockpath, o, 0666) |
| } |
| |
| // InputHash returns the hash of the input arguments that resulted in this lock |
| // file. |
| func (lf *Lockfile) InputHash() []byte { |
| b, err := hex.DecodeString(lf.Hash) |
| if err != nil { |
| return nil |
| } |
| return b |
| } |
| |
| // Projects returns the list of projects enumerated in the lock file. |
| func (lf *Lockfile) Projects() []gps.LockedProject { |
| all := append(lf.Imports, lf.DevImports...) |
| lp := make([]gps.LockedProject, len(all)) |
| |
| for k, l := range all { |
| r := gps.Revision(l.Revision) |
| |
| var v gps.Version |
| if l.Version != "" { |
| v = gps.NewVersion(l.Version).Is(r) |
| } else if l.Branch != "" { |
| v = gps.NewBranch(l.Branch).Is(r) |
| } else { |
| v = r |
| } |
| |
| id := gps.ProjectIdentifier{ |
| ProjectRoot: gps.ProjectRoot(l.Name), |
| NetworkName: l.Repository, |
| } |
| lp[k] = gps.NewLockedProject(id, v, nil) |
| } |
| |
| return lp |
| } |
| |
| // Clone returns a clone of Lockfile |
| func (lf *Lockfile) Clone() *Lockfile { |
| n := &Lockfile{} |
| n.Hash = lf.Hash |
| n.Updated = lf.Updated |
| n.Imports = lf.Imports.Clone() |
| n.DevImports = lf.DevImports.Clone() |
| |
| return n |
| } |
| |
| // Fingerprint returns a hash of the contents minus the date. This allows for |
| // two lockfiles to be compared irrespective of their updated times. |
| // TODO remove, or seriously re-adapt |
| func (lf *Lockfile) Fingerprint() ([32]byte, error) { |
| c := lf.Clone() |
| c.Updated = time.Time{} // Set the time to be the nil equivalent |
| sort.Sort(c.Imports) |
| sort.Sort(c.DevImports) |
| yml, err := c.Marshal() |
| if err != nil { |
| return [32]byte{}, err |
| } |
| |
| return sha256.Sum256(yml), nil |
| } |
| |
| // ReadLockFile loads the contents of a glide.lock file. |
| func ReadLockFile(lockpath string) (*Lockfile, error) { |
| yml, err := ioutil.ReadFile(lockpath) |
| if err != nil { |
| return nil, err |
| } |
| lock, _, err := LockfileFromYaml(yml) |
| if err != nil { |
| return nil, err |
| } |
| return lock, nil |
| } |
| |
| // Locks is a slice of locked dependencies. |
| type Locks []*Lock |
| |
| // Clone returns a Clone of Locks. |
| func (l Locks) Clone() Locks { |
| n := make(Locks, 0, len(l)) |
| for _, v := range l { |
| n = append(n, v.Clone()) |
| } |
| return n |
| } |
| |
| // Len returns the length of the Locks. This is needed for sorting with |
| // the sort package. |
| func (l Locks) Len() int { |
| return len(l) |
| } |
| |
| // Less is needed for the sort interface. It compares two locks based on |
| // their name. |
| func (l Locks) Less(i, j int) bool { |
| |
| // Names are normalized to lowercase because case affects sorting order. For |
| // example, Masterminds comes before kylelemons. Making them lowercase |
| // causes kylelemons to come first which is what is expected. |
| return strings.ToLower(l[i].Name) < strings.ToLower(l[j].Name) |
| } |
| |
| // Swap is needed for the sort interface. It swaps the position of two |
| // locks. |
| func (l Locks) Swap(i, j int) { |
| l[i], l[j] = l[j], l[i] |
| } |
| |
| // Lock represents an individual locked dependency. |
| type Lock struct { |
| Name string `yaml:"name"` |
| Version string `yaml:"version,omitempty"` |
| Branch string `yaml:"branch,omitempty"` |
| Revision string `yaml:"revision"` |
| Repository string `yaml:"repo,omitempty"` |
| } |
| |
| func (l *Lock) UnmarshalYAML(unmarshal func(interface{}) error) error { |
| nl := struct { |
| Name string `yaml:"name"` |
| Version string `yaml:"version,omitempty"` |
| Branch string `yaml:"branch,omitempty"` |
| Revision string `yaml:"revision"` |
| Repository string `yaml:"repo,omitempty"` |
| }{} |
| |
| err := unmarshal(&nl) |
| if err != nil { |
| return err |
| } |
| |
| // If Revision field is empty, then we can be certain this is either a |
| // legacy file, or just plain invalid |
| if nl.Revision == "" { |
| return fmt.Errorf("dependency %s is missing a revision; is this a legacy glide.lock file?", nl.Name) |
| } |
| |
| l.Name = nl.Name |
| l.Version = nl.Version |
| l.Branch = nl.Branch |
| l.Revision = nl.Revision |
| l.Repository = nl.Repository |
| |
| return nil |
| } |
| |
| // Clone creates a clone of a Lock. |
| func (l *Lock) Clone() *Lock { |
| var l2 Lock |
| l2 = *l |
| return &l2 |
| } |
| |
| // LockFromDependency converts a Dependency to a Lock |
| // TODO remove |
| func LockFromDependency(dep *Dependency) *Lock { |
| l := &Lock{ |
| Name: dep.Name, |
| Repository: dep.Repository, |
| } |
| |
| return l |
| } |
| |
| // NewLockfile is used to create an instance of Lockfile. |
| // TODO remove |
| func NewLockfile(ds, tds Dependencies, hash string) (*Lockfile, error) { |
| lf := &Lockfile{ |
| Hash: hash, |
| Updated: time.Now(), |
| Imports: make([]*Lock, len(ds)), |
| DevImports: make([]*Lock, len(tds)), |
| } |
| |
| for i := 0; i < len(ds); i++ { |
| lf.Imports[i] = LockFromDependency(ds[i]) |
| } |
| |
| sort.Sort(lf.Imports) |
| |
| var found bool |
| for i := 0; i < len(tds); i++ { |
| found = false |
| for ii := 0; ii < len(ds); ii++ { |
| if ds[ii].Name == tds[i].Name { |
| found = true |
| if ds[ii].ConstraintsEq(*tds[i]) { |
| return &Lockfile{}, fmt.Errorf("Generating lock produced conflicting versions of %s. import (%s), testImport (%s)", tds[i].Name, ds[ii].GetConstraint(), tds[i].GetConstraint()) |
| } |
| break |
| } |
| } |
| if !found { |
| lf.DevImports[i] = LockFromDependency(tds[i]) |
| } |
| } |
| |
| sort.Sort(lf.DevImports) |
| |
| return lf, nil |
| } |
| |
| // LockfileFromMap takes a map of dependencies and generates a lock Lockfile instance. |
| // TODO remove |
| func LockfileFromMap(ds map[string]*Dependency, hash string) *Lockfile { |
| lf := &Lockfile{ |
| Hash: hash, |
| Updated: time.Now(), |
| Imports: make([]*Lock, len(ds)), |
| } |
| |
| i := 0 |
| for name, dep := range ds { |
| lf.Imports[i] = LockFromDependency(dep) |
| lf.Imports[i].Name = name |
| i++ |
| } |
| |
| sort.Sort(lf.Imports) |
| |
| return lf |
| } |