github.com/golang/dep@v0.5.4/internal/importers/base/importer.go (about)

     1  // Copyright 2017 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package base
     6  
     7  import (
     8  	"log"
     9  	"strings"
    10  
    11  	"github.com/golang/dep"
    12  	"github.com/golang/dep/gps"
    13  	fb "github.com/golang/dep/internal/feedback"
    14  	"github.com/pkg/errors"
    15  )
    16  
    17  // Importer provides a common implementation for importing from other
    18  // dependency managers.
    19  type Importer struct {
    20  	SourceManager gps.SourceManager
    21  	Logger        *log.Logger
    22  	Verbose       bool
    23  	Manifest      *dep.Manifest
    24  	Lock          *dep.Lock
    25  }
    26  
    27  // NewImporter creates a new Importer for embedding in an importer.
    28  func NewImporter(logger *log.Logger, verbose bool, sm gps.SourceManager) *Importer {
    29  	return &Importer{
    30  		Logger:        logger,
    31  		Verbose:       verbose,
    32  		Manifest:      dep.NewManifest(),
    33  		Lock:          &dep.Lock{},
    34  		SourceManager: sm,
    35  	}
    36  }
    37  
    38  // isTag determines if the specified value is a tag (plain or semver).
    39  func (i *Importer) isTag(pi gps.ProjectIdentifier, value string) (bool, gps.Version, error) {
    40  	versions, err := i.SourceManager.ListVersions(pi)
    41  	if err != nil {
    42  		return false, nil, errors.Wrapf(err, "unable to list versions for %s(%s)", pi.ProjectRoot, pi.Source)
    43  	}
    44  
    45  	for _, version := range versions {
    46  		if version.Type() != gps.IsVersion && version.Type() != gps.IsSemver {
    47  			continue
    48  		}
    49  
    50  		if value == version.String() {
    51  			return true, version, nil
    52  		}
    53  	}
    54  
    55  	return false, nil, nil
    56  }
    57  
    58  // lookupVersionForLockedProject figures out the appropriate version for a locked
    59  // project based on the locked revision and the constraint from the manifest.
    60  // First try matching the revision to a version, then try the constraint from the
    61  // manifest, then finally the revision.
    62  func (i *Importer) lookupVersionForLockedProject(pi gps.ProjectIdentifier, c gps.Constraint, rev gps.Revision) (gps.Version, error) {
    63  	// Find the version that goes with this revision, if any
    64  	versions, err := i.SourceManager.ListVersions(pi)
    65  	if err != nil {
    66  		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)
    67  	}
    68  
    69  	var branchConstraint gps.PairedVersion
    70  	gps.SortPairedForUpgrade(versions) // Sort versions in asc order
    71  	var matches []gps.Version
    72  	for _, v := range versions {
    73  		if v.Revision() == rev {
    74  			matches = append(matches, v)
    75  		}
    76  		if c != nil && v.Type() == gps.IsBranch && v.String() == c.String() {
    77  			branchConstraint = v
    78  		}
    79  	}
    80  
    81  	// Try to narrow down the matches with the constraint. Otherwise return the first match.
    82  	if len(matches) > 0 {
    83  		if c != nil {
    84  			for _, v := range matches {
    85  				if i.testConstraint(c, v) {
    86  					return v, nil
    87  				}
    88  			}
    89  		}
    90  		return matches[0], nil
    91  	}
    92  
    93  	// Use branch constraint from the manifest
    94  	if branchConstraint != nil {
    95  		return branchConstraint.Unpair().Pair(rev), nil
    96  	}
    97  
    98  	// Give up and lock only to a revision
    99  	return rev, nil
   100  }
   101  
   102  // ImportedPackage is a common intermediate representation of a package imported
   103  // from an external tool's configuration.
   104  type ImportedPackage struct {
   105  	// Required. The package path, not necessarily the project root.
   106  	Name string
   107  
   108  	// Required. Text representing a revision or tag.
   109  	LockHint string
   110  
   111  	// Optional. Alternative source, or fork, for the project.
   112  	Source string
   113  
   114  	// Optional. Text representing a branch or version.
   115  	ConstraintHint string
   116  }
   117  
   118  // importedProject is a consolidated representation of a set of imported packages
   119  // for the same project root.
   120  type importedProject struct {
   121  	Root gps.ProjectRoot
   122  	ImportedPackage
   123  }
   124  
   125  // loadPackages consolidates all package references into a set of project roots.
   126  func (i *Importer) loadPackages(packages []ImportedPackage) []importedProject {
   127  	// preserve the original order of the packages so that messages that
   128  	// are printed as they are processed are in a consistent order.
   129  	orderedProjects := make([]importedProject, 0, len(packages))
   130  
   131  	projects := make(map[gps.ProjectRoot]*importedProject, len(packages))
   132  	for _, pkg := range packages {
   133  		pr, err := i.SourceManager.DeduceProjectRoot(pkg.Name)
   134  		if err != nil {
   135  			i.Logger.Printf(
   136  				"  Warning: Skipping project. Cannot determine the project root for %s: %s\n",
   137  				pkg.Name, err,
   138  			)
   139  			continue
   140  		}
   141  		pkg.Name = string(pr)
   142  
   143  		prj, exists := projects[pr]
   144  		if !exists {
   145  			prj := importedProject{pr, pkg}
   146  			orderedProjects = append(orderedProjects, prj)
   147  			projects[pr] = &orderedProjects[len(orderedProjects)-1]
   148  			continue
   149  		}
   150  
   151  		// The config found first "wins", though we allow for incrementally
   152  		// setting each field because some importers have a config and lock file.
   153  		if prj.Source == "" && pkg.Source != "" {
   154  			prj.Source = pkg.Source
   155  		}
   156  
   157  		if prj.ConstraintHint == "" && pkg.ConstraintHint != "" {
   158  			prj.ConstraintHint = pkg.ConstraintHint
   159  		}
   160  
   161  		if prj.LockHint == "" && pkg.LockHint != "" {
   162  			prj.LockHint = pkg.LockHint
   163  		}
   164  	}
   165  
   166  	return orderedProjects
   167  }
   168  
   169  // ImportPackages loads imported packages into the manifest and lock.
   170  // - defaultConstraintFromLock specifies if a constraint should be defaulted
   171  //   based on the locked version when there wasn't a constraint hint.
   172  //
   173  // Rules:
   174  // * When a constraint is ignored, default to *.
   175  // * HEAD revisions default to the matching branch.
   176  // * Semantic versions default to ^VERSION.
   177  // * Revision constraints are ignored.
   178  // * Versions that don't satisfy the constraint, drop the constraint.
   179  // * Untagged revisions ignore non-branch constraint hints.
   180  func (i *Importer) ImportPackages(packages []ImportedPackage, defaultConstraintFromLock bool) {
   181  	projects := i.loadPackages(packages)
   182  
   183  	for _, prj := range projects {
   184  		source := prj.Source
   185  		if len(source) > 0 {
   186  			isDefault, err := i.isDefaultSource(prj.Root, source)
   187  			if err != nil {
   188  				i.Logger.Printf("  Ignoring imported source %s for %s: %s", source, prj.Root, err.Error())
   189  				source = ""
   190  			} else if isDefault {
   191  				source = ""
   192  			} else if strings.Contains(source, "/vendor/") {
   193  				i.Logger.Printf("  Ignoring imported source %s for %s because vendored sources aren't supported", source, prj.Root)
   194  				source = ""
   195  			}
   196  		}
   197  
   198  		pc := gps.ProjectConstraint{
   199  			Ident: gps.ProjectIdentifier{
   200  				ProjectRoot: prj.Root,
   201  				Source:      source,
   202  			},
   203  		}
   204  
   205  		var err error
   206  		pc.Constraint, err = i.SourceManager.InferConstraint(prj.ConstraintHint, pc.Ident)
   207  		if err != nil {
   208  			pc.Constraint = gps.Any()
   209  		}
   210  
   211  		var version gps.Version
   212  		if prj.LockHint != "" {
   213  			var isTag bool
   214  			// Determine if the lock hint is a revision or tag
   215  			isTag, version, err = i.isTag(pc.Ident, prj.LockHint)
   216  			if err != nil {
   217  				i.Logger.Printf(
   218  					"  Warning: Skipping project. Unable to import lock %q for %v: %s\n",
   219  					prj.LockHint, pc.Ident, err,
   220  				)
   221  				continue
   222  			}
   223  			// If the hint is a revision, check if it is tagged
   224  			if !isTag {
   225  				revision := gps.Revision(prj.LockHint)
   226  				version, err = i.lookupVersionForLockedProject(pc.Ident, pc.Constraint, revision)
   227  				if err != nil {
   228  					version = nil
   229  					i.Logger.Println(err)
   230  				}
   231  			}
   232  
   233  			// Default the constraint based on the locked version
   234  			if defaultConstraintFromLock && prj.ConstraintHint == "" && version != nil {
   235  				c := i.convertToConstraint(version)
   236  				if c != nil {
   237  					pc.Constraint = c
   238  				}
   239  			}
   240  		}
   241  
   242  		// Ignore pinned constraints
   243  		if i.isConstraintPinned(pc.Constraint) {
   244  			if i.Verbose {
   245  				i.Logger.Printf("  Ignoring pinned constraint %v for %v.\n", pc.Constraint, pc.Ident)
   246  			}
   247  			pc.Constraint = gps.Any()
   248  		}
   249  
   250  		// Ignore constraints which conflict with the locked revision, so that
   251  		// solve doesn't later change the revision to satisfy the constraint.
   252  		if !i.testConstraint(pc.Constraint, version) {
   253  			if i.Verbose {
   254  				i.Logger.Printf("  Ignoring constraint %v for %v because it would invalidate the locked version %v.\n", pc.Constraint, pc.Ident, version)
   255  			}
   256  			pc.Constraint = gps.Any()
   257  		}
   258  
   259  		// Add constraint to manifest that is not empty (has a branch, version or source)
   260  		if !gps.IsAny(pc.Constraint) || pc.Ident.Source != "" {
   261  			i.Manifest.Constraints[pc.Ident.ProjectRoot] = gps.ProjectProperties{
   262  				Source:     pc.Ident.Source,
   263  				Constraint: pc.Constraint,
   264  			}
   265  			fb.NewConstraintFeedback(pc, fb.DepTypeImported).LogFeedback(i.Logger)
   266  		}
   267  
   268  		if version != nil {
   269  			lp := gps.NewLockedProject(pc.Ident, version, nil)
   270  			i.Lock.P = append(i.Lock.P, lp)
   271  			fb.NewLockedProjectFeedback(lp, fb.DepTypeImported).LogFeedback(i.Logger)
   272  		}
   273  	}
   274  }
   275  
   276  // isConstraintPinned returns if a constraint is pinned to a specific revision.
   277  func (i *Importer) isConstraintPinned(c gps.Constraint) bool {
   278  	if version, isVersion := c.(gps.Version); isVersion {
   279  		switch version.Type() {
   280  		case gps.IsRevision, gps.IsVersion:
   281  			return true
   282  		}
   283  	}
   284  	return false
   285  }
   286  
   287  // testConstraint verifies that the constraint won't invalidate the locked version.
   288  func (i *Importer) testConstraint(c gps.Constraint, v gps.Version) bool {
   289  	// Assume branch constraints are satisfied
   290  	if version, isVersion := c.(gps.Version); isVersion {
   291  		if version.Type() == gps.IsBranch {
   292  
   293  			return true
   294  		}
   295  	}
   296  
   297  	return c.Matches(v)
   298  }
   299  
   300  // convertToConstraint turns a version into a constraint.
   301  // Semver tags are converted to a range with the caret operator.
   302  func (i *Importer) convertToConstraint(v gps.Version) gps.Constraint {
   303  	if v.Type() == gps.IsSemver {
   304  		c, err := gps.NewSemverConstraintIC(v.String())
   305  		if err != nil {
   306  			// This should never fail, because the type is semver.
   307  			// If it does fail somehow, don't let that impact the import.
   308  			return nil
   309  		}
   310  		return c
   311  	}
   312  	return v
   313  }
   314  
   315  func (i *Importer) isDefaultSource(projectRoot gps.ProjectRoot, sourceURL string) (bool, error) {
   316  	// this condition is mainly for gopkg.in imports,
   317  	// as some importers specify the repository url as https://gopkg.in/...,
   318  	// but SourceManager.SourceURLsForPath() returns https://github.com/... urls for gopkg.in
   319  	if sourceURL == "https://"+string(projectRoot) {
   320  		return true, nil
   321  	}
   322  
   323  	sourceURLs, err := i.SourceManager.SourceURLsForPath(string(projectRoot))
   324  	if err != nil {
   325  		return false, err
   326  	}
   327  	// The first url in the slice will be the default one (usually https://...)
   328  	if len(sourceURLs) > 0 && sourceURL == sourceURLs[0].String() {
   329  		return true, nil
   330  	}
   331  
   332  	return false, nil
   333  }