gps/identifier.go

Summary

Maintainability
A
0 mins
Test Coverage
// 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 gps

import (
    "fmt"
    "math/rand"
    "strconv"
)

// ProjectRoot is the topmost import path in a tree of other import paths - the
// root of the tree. In gps' current design, ProjectRoots have to correspond to
// a repository root (mostly), but their real purpose is to identify the root
// import path of a "project", logically encompassing all child packages.
//
// Projects are a crucial unit of operation in gps. Constraints are declared by
// a project's manifest, and apply to all packages in a ProjectRoot's tree.
// Solving itself mostly proceeds on a project-by-project basis.
//
// Aliasing string types is usually a bit of an anti-pattern. gps does it here
// as a means of clarifying API intent. This is important because Go's package
// management domain has lots of different path-ish strings floating around:
//
//  actual directories:
//    /home/sdboyer/go/src/github.com/sdboyer/gps/example
//  URLs:
//    https://github.com/sdboyer/gps
//  import paths:
//    github.com/sdboyer/gps/example
//  portions of import paths that refer to a package:
//    example
//  portions that could not possibly refer to anything sane:
//    github.com/sdboyer
//  portions that correspond to a repository root:
//    github.com/sdboyer/gps
//
// While not a panacea, having ProjectRoot allows gps to clearly indicate via
// the type system when a path-ish string must have particular semantics.
type ProjectRoot string

// A ProjectIdentifier provides the name and source location of a dependency. It
// is related to, but differs in two key ways from, a plain import path.
//
// First, ProjectIdentifiers do not identify a single package. Rather, they
// encompass the whole tree of packages, including tree's root - the
// ProjectRoot. In gps' current design, this ProjectRoot almost always
// corresponds to the root of a repository.
//
// Second, ProjectIdentifiers can optionally carry a Source, which
// identifies where the underlying source code can be located on the network.
// These can be either a full URL, including protocol, or plain import paths.
// So, these are all valid data for Source:
//
//  github.com/sdboyer/gps
//  github.com/fork/gps
//  git@github.com:sdboyer/gps
//  https://github.com/sdboyer/gps
//
// With plain import paths, network addresses are derived purely through an
// algorithm. By having an explicit network name, it becomes possible to, for
// example, transparently substitute a fork for the original upstream source
// repository.
//
// Note that gps makes no guarantees about the actual import paths contained in
// a repository aligning with ImportRoot. If tools, or their users, specify an
// alternate Source that contains a repository with incompatible internal
// import paths, gps' solving operations will error. (gps does no import
// rewriting.)
//
// Also note that if different projects' manifests report a different
// Source for a given ImportRoot, it is a solve failure. Everyone has to
// agree on where a given import path should be sourced from.
//
// If Source is not explicitly set, gps will derive the network address from
// the ImportRoot using a similar algorithm to that utilized by `go get`.
type ProjectIdentifier struct {
    ProjectRoot ProjectRoot
    Source      string
}

// Less compares by ProjectRoot then normalized Source.
func (i ProjectIdentifier) Less(j ProjectIdentifier) bool {
    if i.ProjectRoot < j.ProjectRoot {
        return true
    }
    if j.ProjectRoot < i.ProjectRoot {
        return false
    }
    return i.normalizedSource() < j.normalizedSource()
}

func (i ProjectIdentifier) eq(j ProjectIdentifier) bool {
    if i.ProjectRoot != j.ProjectRoot {
        return false
    }
    if i.Source == j.Source {
        return true
    }

    if (i.Source == "" && j.Source == string(j.ProjectRoot)) ||
        (j.Source == "" && i.Source == string(i.ProjectRoot)) {
        return true
    }

    return false
}

// equiv will check if the two identifiers are "equivalent," under special
// rules.
//
// Given that the ProjectRoots are equal (==), equivalency occurs if:
//
// 1. The Sources are equal (==), OR
// 2. The LEFT (the receiver) Source is non-empty, and the right
// Source is empty.
//
// *This is asymmetry in this binary relation is intentional.* It facilitates
// the case where we allow for a ProjectIdentifier with an explicit Source
// to match one without.
func (i ProjectIdentifier) equiv(j ProjectIdentifier) bool {
    if i.ProjectRoot != j.ProjectRoot {
        return false
    }
    if i.Source == j.Source {
        return true
    }

    if i.Source != "" && j.Source == "" {
        return true
    }

    return false
}

func (i ProjectIdentifier) normalizedSource() string {
    if i.Source == "" {
        return string(i.ProjectRoot)
    }
    return i.Source
}

func (i ProjectIdentifier) String() string {
    if i.Source == "" || i.Source == string(i.ProjectRoot) {
        return string(i.ProjectRoot)
    }
    return fmt.Sprintf("%s (from %s)", i.ProjectRoot, i.Source)
}

func (i ProjectIdentifier) normalize() ProjectIdentifier {
    if i.Source == "" {
        i.Source = string(i.ProjectRoot)
    }

    return i
}

// ProjectProperties comprise the properties that can be attached to a
// ProjectRoot.
//
// In general, these are declared in the context of a map of ProjectRoot to its
// ProjectProperties; they make little sense without their corresponding
// ProjectRoot.
type ProjectProperties struct {
    Source     string
    Constraint Constraint
}

// bimodalIdentifiers are used to track work to be done in the unselected queue.
type bimodalIdentifier struct {
    id ProjectIdentifier
    // List of packages required within/under the ProjectIdentifier
    pl []string
    // prefv is used to indicate a 'preferred' version. This is expected to be
    // derived from a dep's lock data, or else is empty.
    prefv Version
    // Indicates that the bmi came from the root project originally
    fromRoot bool
}

type atom struct {
    id ProjectIdentifier
    v  Version
}

// With a random revision and no name, collisions are...unlikely
var nilpa = atom{
    v: Revision(strconv.FormatInt(rand.Int63(), 36)),
}

type atomWithPackages struct {
    a  atom
    pl []string
}

// bmi converts an atomWithPackages into a bimodalIdentifier.
//
// This is mostly intended for (read-only) trace use, so the package list slice
// is not copied. It is the callers responsibility to not modify the pl slice,
// lest that backpropagate and cause inconsistencies.
func (awp atomWithPackages) bmi() bimodalIdentifier {
    return bimodalIdentifier{
        id: awp.a.id,
        pl: awp.pl,
    }
}

// completeDep (name hopefully to change) provides the whole picture of a
// dependency - the root (repo and project, since currently we assume the two
// are the same) name, a constraint, and the actual packages needed that are
// under that root.
type completeDep struct {
    // The base workingConstraint
    workingConstraint
    // The specific packages required from the ProjectDep
    pl []string
}

// dependency represents an incomplete edge in the depgraph. It has a
// fully-realized atom as the depender (the tail/source of the edge), and a set
// of requirements that any atom to be attached at the head/target must satisfy.
type dependency struct {
    depender atom
    dep      completeDep
}