internal/importers/base/importer.go
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package base
import (
"log"
"strings"
"github.com/golang/dep"
"github.com/golang/dep/gps"
fb "github.com/golang/dep/internal/feedback"
"github.com/pkg/errors"
)
// Importer provides a common implementation for importing from other
// dependency managers.
type Importer struct {
SourceManager gps.SourceManager
Logger *log.Logger
Verbose bool
Manifest *dep.Manifest
Lock *dep.Lock
}
// NewImporter creates a new Importer for embedding in an importer.
func NewImporter(logger *log.Logger, verbose bool, sm gps.SourceManager) *Importer {
return &Importer{
Logger: logger,
Verbose: verbose,
Manifest: dep.NewManifest(),
Lock: &dep.Lock{},
SourceManager: sm,
}
}
// isTag determines if the specified value is a tag (plain or semver).
func (i *Importer) isTag(pi gps.ProjectIdentifier, value string) (bool, gps.Version, error) {
versions, err := i.SourceManager.ListVersions(pi)
if err != nil {
return false, nil, errors.Wrapf(err, "unable to list versions for %s(%s)", pi.ProjectRoot, pi.Source)
}
for _, version := range versions {
if version.Type() != gps.IsVersion && version.Type() != gps.IsSemver {
continue
}
if value == version.String() {
return true, version, nil
}
}
return false, nil, nil
}
// lookupVersionForLockedProject figures out the appropriate version for a locked
// project based on the locked revision and the constraint from the manifest.
// First try matching the revision to a version, then try the constraint from the
// manifest, then finally the revision.
func (i *Importer) lookupVersionForLockedProject(pi gps.ProjectIdentifier, c gps.Constraint, rev gps.Revision) (gps.Version, error) {
// Find the version that goes with this revision, if any
versions, err := i.SourceManager.ListVersions(pi)
if err != nil {
return rev, errors.Wrapf(err, "Unable to lookup the version represented by %s in %s(%s). Falling back to locking the revision only.", rev, pi.ProjectRoot, pi.Source)
}
var branchConstraint gps.PairedVersion
gps.SortPairedForUpgrade(versions) // Sort versions in asc order
var matches []gps.Version
for _, v := range versions {
if v.Revision() == rev {
matches = append(matches, v)
}
if c != nil && v.Type() == gps.IsBranch && v.String() == c.String() {
branchConstraint = v
}
}
// Try to narrow down the matches with the constraint. Otherwise return the first match.
if len(matches) > 0 {
if c != nil {
for _, v := range matches {
if i.testConstraint(c, v) {
return v, nil
}
}
}
return matches[0], nil
}
// Use branch constraint from the manifest
if branchConstraint != nil {
return branchConstraint.Unpair().Pair(rev), nil
}
// Give up and lock only to a revision
return rev, nil
}
// ImportedPackage is a common intermediate representation of a package imported
// from an external tool's configuration.
type ImportedPackage struct {
// Required. The package path, not necessarily the project root.
Name string
// Required. Text representing a revision or tag.
LockHint string
// Optional. Alternative source, or fork, for the project.
Source string
// Optional. Text representing a branch or version.
ConstraintHint string
}
// importedProject is a consolidated representation of a set of imported packages
// for the same project root.
type importedProject struct {
Root gps.ProjectRoot
ImportedPackage
}
// loadPackages consolidates all package references into a set of project roots.
func (i *Importer) loadPackages(packages []ImportedPackage) []importedProject {
// preserve the original order of the packages so that messages that
// are printed as they are processed are in a consistent order.
orderedProjects := make([]importedProject, 0, len(packages))
projects := make(map[gps.ProjectRoot]*importedProject, len(packages))
for _, pkg := range packages {
pr, err := i.SourceManager.DeduceProjectRoot(pkg.Name)
if err != nil {
i.Logger.Printf(
" Warning: Skipping project. Cannot determine the project root for %s: %s\n",
pkg.Name, err,
)
continue
}
pkg.Name = string(pr)
prj, exists := projects[pr]
if !exists {
prj := importedProject{pr, pkg}
orderedProjects = append(orderedProjects, prj)
projects[pr] = &orderedProjects[len(orderedProjects)-1]
continue
}
// The config found first "wins", though we allow for incrementally
// setting each field because some importers have a config and lock file.
if prj.Source == "" && pkg.Source != "" {
prj.Source = pkg.Source
}
if prj.ConstraintHint == "" && pkg.ConstraintHint != "" {
prj.ConstraintHint = pkg.ConstraintHint
}
if prj.LockHint == "" && pkg.LockHint != "" {
prj.LockHint = pkg.LockHint
}
}
return orderedProjects
}
// ImportPackages loads imported packages into the manifest and lock.
// - defaultConstraintFromLock specifies if a constraint should be defaulted
// based on the locked version when there wasn't a constraint hint.
//
// Rules:
// * When a constraint is ignored, default to *.
// * HEAD revisions default to the matching branch.
// * Semantic versions default to ^VERSION.
// * Revision constraints are ignored.
// * Versions that don't satisfy the constraint, drop the constraint.
// * Untagged revisions ignore non-branch constraint hints.
func (i *Importer) ImportPackages(packages []ImportedPackage, defaultConstraintFromLock bool) {
projects := i.loadPackages(packages)
for _, prj := range projects {
source := prj.Source
if len(source) > 0 {
isDefault, err := i.isDefaultSource(prj.Root, source)
if err != nil {
i.Logger.Printf(" Ignoring imported source %s for %s: %s", source, prj.Root, err.Error())
source = ""
} else if isDefault {
source = ""
} else if strings.Contains(source, "/vendor/") {
i.Logger.Printf(" Ignoring imported source %s for %s because vendored sources aren't supported", source, prj.Root)
source = ""
}
}
pc := gps.ProjectConstraint{
Ident: gps.ProjectIdentifier{
ProjectRoot: prj.Root,
Source: source,
},
}
var err error
pc.Constraint, err = i.SourceManager.InferConstraint(prj.ConstraintHint, pc.Ident)
if err != nil {
pc.Constraint = gps.Any()
}
var version gps.Version
if prj.LockHint != "" {
var isTag bool
// Determine if the lock hint is a revision or tag
isTag, version, err = i.isTag(pc.Ident, prj.LockHint)
if err != nil {
i.Logger.Printf(
" Warning: Skipping project. Unable to import lock %q for %v: %s\n",
prj.LockHint, pc.Ident, err,
)
continue
}
// If the hint is a revision, check if it is tagged
if !isTag {
revision := gps.Revision(prj.LockHint)
version, err = i.lookupVersionForLockedProject(pc.Ident, pc.Constraint, revision)
if err != nil {
version = nil
i.Logger.Println(err)
}
}
// Default the constraint based on the locked version
if defaultConstraintFromLock && prj.ConstraintHint == "" && version != nil {
c := i.convertToConstraint(version)
if c != nil {
pc.Constraint = c
}
}
}
// Ignore pinned constraints
if i.isConstraintPinned(pc.Constraint) {
if i.Verbose {
i.Logger.Printf(" Ignoring pinned constraint %v for %v.\n", pc.Constraint, pc.Ident)
}
pc.Constraint = gps.Any()
}
// Ignore constraints which conflict with the locked revision, so that
// solve doesn't later change the revision to satisfy the constraint.
if !i.testConstraint(pc.Constraint, version) {
if i.Verbose {
i.Logger.Printf(" Ignoring constraint %v for %v because it would invalidate the locked version %v.\n", pc.Constraint, pc.Ident, version)
}
pc.Constraint = gps.Any()
}
// Add constraint to manifest that is not empty (has a branch, version or source)
if !gps.IsAny(pc.Constraint) || pc.Ident.Source != "" {
i.Manifest.Constraints[pc.Ident.ProjectRoot] = gps.ProjectProperties{
Source: pc.Ident.Source,
Constraint: pc.Constraint,
}
fb.NewConstraintFeedback(pc, fb.DepTypeImported).LogFeedback(i.Logger)
}
if version != nil {
lp := gps.NewLockedProject(pc.Ident, version, nil)
i.Lock.P = append(i.Lock.P, lp)
fb.NewLockedProjectFeedback(lp, fb.DepTypeImported).LogFeedback(i.Logger)
}
}
}
// isConstraintPinned returns if a constraint is pinned to a specific revision.
func (i *Importer) isConstraintPinned(c gps.Constraint) bool {
if version, isVersion := c.(gps.Version); isVersion {
switch version.Type() {
case gps.IsRevision, gps.IsVersion:
return true
}
}
return false
}
// testConstraint verifies that the constraint won't invalidate the locked version.
func (i *Importer) testConstraint(c gps.Constraint, v gps.Version) bool {
// Assume branch constraints are satisfied
if version, isVersion := c.(gps.Version); isVersion {
if version.Type() == gps.IsBranch {
return true
}
}
return c.Matches(v)
}
// convertToConstraint turns a version into a constraint.
// Semver tags are converted to a range with the caret operator.
func (i *Importer) convertToConstraint(v gps.Version) gps.Constraint {
if v.Type() == gps.IsSemver {
c, err := gps.NewSemverConstraintIC(v.String())
if err != nil {
// This should never fail, because the type is semver.
// If it does fail somehow, don't let that impact the import.
return nil
}
return c
}
return v
}
func (i *Importer) isDefaultSource(projectRoot gps.ProjectRoot, sourceURL string) (bool, error) {
// this condition is mainly for gopkg.in imports,
// as some importers specify the repository url as https://gopkg.in/...,
// but SourceManager.SourceURLsForPath() returns https://github.com/... urls for gopkg.in
if sourceURL == "https://"+string(projectRoot) {
return true, nil
}
sourceURLs, err := i.SourceManager.SourceURLsForPath(string(projectRoot))
if err != nil {
return false, err
}
// The first url in the slice will be the default one (usually https://...)
if len(sourceURLs) > 0 && sourceURL == sourceURLs[0].String() {
return true, nil
}
return false, nil
}