mirror of
https://gitea.com/gitea/runner.git
synced 2026-11-14 20:03:42 +00:00
Merge pull request #27 from nektos/parser
Replace parser with new parser from GitHub
This commit is contained in:
189 files changed
+26276
-4015
No files matched your search
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -e
|
set -e
|
||||||
golangci-lint run
|
golangci-lint run
|
||||||
go test -cover ./...
|
go test -cover -short ./...
|
||||||
@@ -50,6 +50,6 @@ endif
|
|||||||
git push origin $(NEW_VERSION)
|
git push origin $(NEW_VERSION)
|
||||||
|
|
||||||
vendor:
|
vendor:
|
||||||
go run main.go -ra vendor
|
go mod vendor
|
||||||
|
|
||||||
.PHONY: vendor
|
.PHONY: vendor
|
||||||
+28
-63
@@ -2,81 +2,46 @@ package actions
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"log"
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/nektos/act/common"
|
"github.com/actions/workflow-parser/model"
|
||||||
log "github.com/sirupsen/logrus"
|
"github.com/howeyc/gopass"
|
||||||
)
|
)
|
||||||
|
|
||||||
// imageURL is the directory where a `Dockerfile` should exist
|
var secretCache map[string]string
|
||||||
func parseImageLocal(workingDir string, contextDir string) (contextDirOut string, tag string, ok bool) {
|
|
||||||
if !strings.HasPrefix(contextDir, "./") {
|
|
||||||
return "", "", false
|
|
||||||
}
|
|
||||||
contextDir = filepath.Join(workingDir, contextDir)
|
|
||||||
if _, err := os.Stat(filepath.Join(contextDir, "Dockerfile")); os.IsNotExist(err) {
|
|
||||||
log.Debugf("Ignoring missing Dockerfile '%s/Dockerfile'", contextDir)
|
|
||||||
return "", "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
sha, _, err := common.FindGitRevision(contextDir)
|
type actionEnvironmentApplier struct {
|
||||||
if err != nil {
|
*model.Action
|
||||||
log.Warnf("Unable to determine git revision: %v", err)
|
|
||||||
sha = "latest"
|
|
||||||
}
|
|
||||||
return contextDir, fmt.Sprintf("%s:%s", filepath.Base(contextDir), sha), true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// imageURL is the URL for a docker repo
|
func newActionEnvironmentApplier(action *model.Action) environmentApplier {
|
||||||
func parseImageReference(image string) (ref string, ok bool) {
|
return &actionEnvironmentApplier{action}
|
||||||
imageURL, err := url.Parse(image)
|
|
||||||
if err != nil {
|
|
||||||
log.Debugf("Unable to parse image as url: %v", err)
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
if imageURL.Scheme != "docker" {
|
|
||||||
log.Debugf("Ignoring non-docker ref '%s'", imageURL.String())
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s%s", imageURL.Host, imageURL.Path), true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// imageURL is the directory where a `Dockerfile` should exist
|
func (action *actionEnvironmentApplier) applyEnvironment(env map[string]string) {
|
||||||
func parseImageGithub(image string) (cloneURL *url.URL, ref string, path string, ok bool) {
|
for envKey, envValue := range action.Env {
|
||||||
re := regexp.MustCompile("^([^/@]+)/([^/@]+)(/([^@]*))?(@(.*))?$")
|
env[envKey] = envValue
|
||||||
matches := re.FindStringSubmatch(image)
|
|
||||||
|
|
||||||
if matches == nil {
|
|
||||||
return nil, "", "", false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cloneURL, err := url.Parse(fmt.Sprintf("https://github.com/%s/%s", matches[1], matches[2]))
|
for _, secret := range action.Secrets {
|
||||||
if err != nil {
|
if secretVal, ok := os.LookupEnv(secret); ok {
|
||||||
log.Debugf("Unable to parse as URL: %v", err)
|
env[secret] = secretVal
|
||||||
return nil, "", "", false
|
} else {
|
||||||
}
|
if secretCache == nil {
|
||||||
|
secretCache = make(map[string]string)
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := http.Head(cloneURL.String())
|
if _, ok := secretCache[secret]; !ok {
|
||||||
if resp.StatusCode >= 400 || err != nil {
|
fmt.Printf("Provide value for '%s': ", secret)
|
||||||
log.Debugf("Unable to HEAD URL %s status=%v err=%v", cloneURL.String(), resp.StatusCode, err)
|
val, err := gopass.GetPasswdMasked()
|
||||||
return nil, "", "", false
|
if err != nil {
|
||||||
}
|
log.Fatal("abort")
|
||||||
|
}
|
||||||
|
|
||||||
ref = matches[6]
|
secretCache[secret] = string(val)
|
||||||
if ref == "" {
|
}
|
||||||
ref = "master"
|
env[secret] = secretCache[secret]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
path = matches[4]
|
|
||||||
if path == "" {
|
|
||||||
path = "."
|
|
||||||
}
|
|
||||||
|
|
||||||
return cloneURL, ref, path, true
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/actions/workflow-parser/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// return a pipeline that is run in series. pipeline is a list of steps to run in parallel
|
||||||
|
func newExecutionGraph(workflowConfig *model.Configuration, actionNames ...string) [][]string {
|
||||||
|
// first, build a list of all the necessary actions to run, and their dependencies
|
||||||
|
actionDependencies := make(map[string][]string)
|
||||||
|
for len(actionNames) > 0 {
|
||||||
|
newActionNames := make([]string, 0)
|
||||||
|
for _, aName := range actionNames {
|
||||||
|
// make sure we haven't visited this action yet
|
||||||
|
if _, ok := actionDependencies[aName]; !ok {
|
||||||
|
action := workflowConfig.GetAction(aName)
|
||||||
|
if action != nil {
|
||||||
|
actionDependencies[aName] = action.Needs
|
||||||
|
newActionNames = append(newActionNames, action.Needs...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
actionNames = newActionNames
|
||||||
|
}
|
||||||
|
|
||||||
|
// next, build an execution graph
|
||||||
|
graph := make([][]string, 0)
|
||||||
|
for len(actionDependencies) > 0 {
|
||||||
|
stage := make([]string, 0)
|
||||||
|
for aName, aDeps := range actionDependencies {
|
||||||
|
// make sure all deps are in the graph already
|
||||||
|
if listInLists(aDeps, graph...) {
|
||||||
|
stage = append(stage, aName)
|
||||||
|
delete(actionDependencies, aName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(stage) == 0 {
|
||||||
|
log.Fatalf("Unable to build dependency graph!")
|
||||||
|
}
|
||||||
|
graph = append(graph, stage)
|
||||||
|
}
|
||||||
|
|
||||||
|
return graph
|
||||||
|
}
|
||||||
|
|
||||||
|
// return true iff all strings in srcList exist in at least one of the searchLists
|
||||||
|
func listInLists(srcList []string, searchLists ...[]string) bool {
|
||||||
|
for _, src := range srcList {
|
||||||
|
found := false
|
||||||
|
for _, searchList := range searchLists {
|
||||||
|
for _, search := range searchList {
|
||||||
|
if src == search {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
package actions
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/howeyc/gopass"
|
|
||||||
)
|
|
||||||
|
|
||||||
type workflowModel struct {
|
|
||||||
On string
|
|
||||||
Resolves []string
|
|
||||||
}
|
|
||||||
|
|
||||||
type actionModel struct {
|
|
||||||
Needs []string
|
|
||||||
Uses string
|
|
||||||
Runs []string
|
|
||||||
Args []string
|
|
||||||
Env map[string]string
|
|
||||||
Secrets []string
|
|
||||||
}
|
|
||||||
|
|
||||||
type workflowsFile struct {
|
|
||||||
Workflow map[string]workflowModel
|
|
||||||
Action map[string]actionModel
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wFile *workflowsFile) getWorkflow(eventName string) (*workflowModel, string, error) {
|
|
||||||
var rtn workflowModel
|
|
||||||
for wName, w := range wFile.Workflow {
|
|
||||||
if w.On == eventName {
|
|
||||||
rtn = w
|
|
||||||
return &rtn, wName, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, "", fmt.Errorf("unsupported event: %v", eventName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wFile *workflowsFile) getAction(actionName string) (*actionModel, error) {
|
|
||||||
if a, ok := wFile.Action[actionName]; ok {
|
|
||||||
return &a, nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("unsupported action: %v", actionName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// return a pipeline that is run in series. pipeline is a list of steps to run in parallel
|
|
||||||
func (wFile *workflowsFile) newExecutionGraph(actionNames ...string) [][]string {
|
|
||||||
// first, build a list of all the necessary actions to run, and their dependencies
|
|
||||||
actionDependencies := make(map[string][]string)
|
|
||||||
for len(actionNames) > 0 {
|
|
||||||
newActionNames := make([]string, 0)
|
|
||||||
for _, aName := range actionNames {
|
|
||||||
// make sure we haven't visited this action yet
|
|
||||||
if _, ok := actionDependencies[aName]; !ok {
|
|
||||||
actionDependencies[aName] = wFile.Action[aName].Needs
|
|
||||||
newActionNames = append(newActionNames, wFile.Action[aName].Needs...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
actionNames = newActionNames
|
|
||||||
}
|
|
||||||
|
|
||||||
// next, build an execution graph
|
|
||||||
graph := make([][]string, 0)
|
|
||||||
for len(actionDependencies) > 0 {
|
|
||||||
stage := make([]string, 0)
|
|
||||||
for aName, aDeps := range actionDependencies {
|
|
||||||
// make sure all deps are in the graph already
|
|
||||||
if listInLists(aDeps, graph...) {
|
|
||||||
stage = append(stage, aName)
|
|
||||||
delete(actionDependencies, aName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(stage) == 0 {
|
|
||||||
log.Fatalf("Unable to build dependency graph!")
|
|
||||||
}
|
|
||||||
graph = append(graph, stage)
|
|
||||||
}
|
|
||||||
|
|
||||||
return graph
|
|
||||||
}
|
|
||||||
|
|
||||||
// return true iff all strings in srcList exist in at least one of the searchLists
|
|
||||||
func listInLists(srcList []string, searchLists ...[]string) bool {
|
|
||||||
for _, src := range srcList {
|
|
||||||
found := false
|
|
||||||
for _, searchList := range searchLists {
|
|
||||||
for _, search := range searchList {
|
|
||||||
if src == search {
|
|
||||||
found = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
var secretCache map[string]string
|
|
||||||
|
|
||||||
func (action *actionModel) applyEnvironment(env map[string]string) {
|
|
||||||
for envKey, envValue := range action.Env {
|
|
||||||
env[envKey] = envValue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, secret := range action.Secrets {
|
|
||||||
if secretVal, ok := os.LookupEnv(secret); ok {
|
|
||||||
env[secret] = secretVal
|
|
||||||
} else {
|
|
||||||
if secretCache == nil {
|
|
||||||
secretCache = make(map[string]string)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := secretCache[secret]; !ok {
|
|
||||||
fmt.Printf("Provide value for '%s': ", secret)
|
|
||||||
val, err := gopass.GetPasswdMasked()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("abort")
|
|
||||||
}
|
|
||||||
|
|
||||||
secretCache[secret] = string(val)
|
|
||||||
}
|
|
||||||
env[secret] = secretCache[secret]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
package actions
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/hashicorp/hcl"
|
|
||||||
"github.com/hashicorp/hcl/hcl/ast"
|
|
||||||
"github.com/hashicorp/hcl/hcl/token"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
func parseWorkflowsFile(workflowReader io.Reader) (*workflowsFile, error) {
|
|
||||||
// TODO: add validation logic
|
|
||||||
// - check for circular dependencies
|
|
||||||
// - check for valid local path refs
|
|
||||||
// - check for valid dependencies
|
|
||||||
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
_, err := buf.ReadFrom(workflowReader)
|
|
||||||
if err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
workflows := new(workflowsFile)
|
|
||||||
|
|
||||||
astFile, err := hcl.ParseBytes(buf.Bytes())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
rootNode := ast.Walk(astFile.Node, cleanWorkflowsAST)
|
|
||||||
err = hcl.DecodeObject(workflows, rootNode)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return workflows, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func cleanWorkflowsAST(node ast.Node) (ast.Node, bool) {
|
|
||||||
if objectItem, ok := node.(*ast.ObjectItem); ok {
|
|
||||||
key := objectItem.Keys[0].Token.Value()
|
|
||||||
|
|
||||||
// handle condition where value is a string but should be a list
|
|
||||||
switch key {
|
|
||||||
case "args", "runs":
|
|
||||||
if literalType, ok := objectItem.Val.(*ast.LiteralType); ok {
|
|
||||||
listType := new(ast.ListType)
|
|
||||||
parts, err := parseCommand(literalType.Token.Value().(string))
|
|
||||||
if err != nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
quote := literalType.Token.Text[0]
|
|
||||||
for _, part := range parts {
|
|
||||||
part = fmt.Sprintf("%c%s%c", quote, strings.Replace(part, "\\", "\\\\", -1), quote)
|
|
||||||
listType.Add(&ast.LiteralType{
|
|
||||||
Token: token.Token{
|
|
||||||
Type: token.STRING,
|
|
||||||
Text: part,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
objectItem.Val = listType
|
|
||||||
|
|
||||||
}
|
|
||||||
case "resolves", "needs":
|
|
||||||
if literalType, ok := objectItem.Val.(*ast.LiteralType); ok {
|
|
||||||
listType := new(ast.ListType)
|
|
||||||
listType.Add(literalType)
|
|
||||||
objectItem.Val = listType
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return node, true
|
|
||||||
}
|
|
||||||
|
|
||||||
// reused from: https://github.com/laurent22/massren/blob/ae4c57da1e09a95d9383f7eb645a9f69790dec6c/main.go#L172
|
|
||||||
// nolint: gocyclo
|
|
||||||
func parseCommand(cmd string) ([]string, error) {
|
|
||||||
var args []string
|
|
||||||
state := "start"
|
|
||||||
current := ""
|
|
||||||
quote := "\""
|
|
||||||
for i := 0; i < len(cmd); i++ {
|
|
||||||
c := cmd[i]
|
|
||||||
|
|
||||||
if state == "quotes" {
|
|
||||||
if string(c) != quote {
|
|
||||||
current += string(c)
|
|
||||||
} else {
|
|
||||||
args = append(args, current)
|
|
||||||
current = ""
|
|
||||||
state = "start"
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if c == '"' || c == '\'' {
|
|
||||||
state = "quotes"
|
|
||||||
quote = string(c)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if state == "arg" {
|
|
||||||
if c == ' ' || c == '\t' {
|
|
||||||
args = append(args, current)
|
|
||||||
current = ""
|
|
||||||
state = "start"
|
|
||||||
} else {
|
|
||||||
current += string(c)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if c != ' ' && c != '\t' {
|
|
||||||
state = "arg"
|
|
||||||
current += string(c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if state == "quotes" {
|
|
||||||
return []string{}, fmt.Errorf("unclosed quote in command line: %s", cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
if current != "" {
|
|
||||||
args = append(args, current)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(args) == 0 {
|
|
||||||
return []string{}, errors.New("empty command line")
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("Parsed literal %+q to list %+q", cmd, args)
|
|
||||||
|
|
||||||
return args, nil
|
|
||||||
}
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
package actions
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestParseWorkflowsFile(t *testing.T) {
|
|
||||||
log.SetLevel(log.DebugLevel)
|
|
||||||
|
|
||||||
conf := `
|
|
||||||
workflow "build-and-deploy" {
|
|
||||||
on = "push"
|
|
||||||
resolves = ["deploy"]
|
|
||||||
}
|
|
||||||
|
|
||||||
action "build" {
|
|
||||||
uses = "./action1"
|
|
||||||
args = "echo 'build'"
|
|
||||||
}
|
|
||||||
|
|
||||||
action "test" {
|
|
||||||
uses = "docker://ubuntu:18.04"
|
|
||||||
runs = "echo 'test'"
|
|
||||||
needs = ["build"]
|
|
||||||
}
|
|
||||||
|
|
||||||
action "deploy" {
|
|
||||||
uses = "./action2"
|
|
||||||
args = ["echo","deploy"]
|
|
||||||
needs = ["test"]
|
|
||||||
}
|
|
||||||
|
|
||||||
action "docker-login" {
|
|
||||||
uses = "docker://docker"
|
|
||||||
runs = ["sh", "-c", "echo $DOCKER_AUTH | docker login --username $REGISTRY_USER --password-stdin"]
|
|
||||||
secrets = ["DOCKER_AUTH"]
|
|
||||||
env = {
|
|
||||||
REGISTRY_USER = "username"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
action "unit-tests" {
|
|
||||||
uses = "./scripts/github_actions"
|
|
||||||
runs = "yarn test:ci-unittest || echo \"Unit tests failed, but running danger to present the results!\" 2>&1"
|
|
||||||
}
|
|
||||||
|
|
||||||
action "regex-in-args" {
|
|
||||||
uses = "actions/bin/filter@master"
|
|
||||||
args = "tag v?[0-9]+\\.[0-9]+\\.[0-9]+"
|
|
||||||
}
|
|
||||||
|
|
||||||
action "regex-in-args-array" {
|
|
||||||
uses = "actions/bin/filter@master"
|
|
||||||
args = ["tag","v?[0-9]+\\.[0-9]+\\.[0-9]+"]
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
workflows, err := parseWorkflowsFile(strings.NewReader(conf))
|
|
||||||
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, 1, len(workflows.Workflow))
|
|
||||||
|
|
||||||
w, wName, _ := workflows.getWorkflow("push")
|
|
||||||
assert.Equal(t, "build-and-deploy", wName)
|
|
||||||
assert.ElementsMatch(t, []string{"deploy"}, w.Resolves)
|
|
||||||
|
|
||||||
actions := []struct {
|
|
||||||
name string
|
|
||||||
uses string
|
|
||||||
needs []string
|
|
||||||
runs []string
|
|
||||||
args []string
|
|
||||||
secrets []string
|
|
||||||
}{
|
|
||||||
{"build",
|
|
||||||
"./action1",
|
|
||||||
nil,
|
|
||||||
nil,
|
|
||||||
[]string{"echo", "build"},
|
|
||||||
nil,
|
|
||||||
},
|
|
||||||
{"test",
|
|
||||||
"docker://ubuntu:18.04",
|
|
||||||
[]string{"build"},
|
|
||||||
[]string{"echo", "test"},
|
|
||||||
nil,
|
|
||||||
nil,
|
|
||||||
},
|
|
||||||
{"deploy",
|
|
||||||
"./action2",
|
|
||||||
[]string{"test"},
|
|
||||||
nil,
|
|
||||||
[]string{"echo", "deploy"},
|
|
||||||
nil,
|
|
||||||
},
|
|
||||||
{"docker-login",
|
|
||||||
"docker://docker",
|
|
||||||
nil,
|
|
||||||
[]string{"sh", "-c", "echo $DOCKER_AUTH | docker login --username $REGISTRY_USER --password-stdin"},
|
|
||||||
nil,
|
|
||||||
[]string{"DOCKER_AUTH"},
|
|
||||||
},
|
|
||||||
{"unit-tests",
|
|
||||||
"./scripts/github_actions",
|
|
||||||
nil,
|
|
||||||
[]string{"yarn", "test:ci-unittest", "||", "echo", "Unit tests failed, but running danger to present the results!", "2>&1"},
|
|
||||||
nil,
|
|
||||||
nil,
|
|
||||||
},
|
|
||||||
{"regex-in-args",
|
|
||||||
"actions/bin/filter@master",
|
|
||||||
nil,
|
|
||||||
nil,
|
|
||||||
[]string{"tag", `v?[0-9]+\.[0-9]+\.[0-9]+`},
|
|
||||||
nil,
|
|
||||||
},
|
|
||||||
{"regex-in-args-array",
|
|
||||||
"actions/bin/filter@master",
|
|
||||||
nil,
|
|
||||||
nil,
|
|
||||||
[]string{"tag", `v?[0-9]+\.[0-9]+\.[0-9]+`},
|
|
||||||
nil,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, exp := range actions {
|
|
||||||
act, _ := workflows.getAction(exp.name)
|
|
||||||
assert.Equal(t, exp.uses, act.Uses, "[%s] Uses", exp.name)
|
|
||||||
if exp.needs == nil {
|
|
||||||
assert.Nil(t, act.Needs, "[%s] Needs", exp.name)
|
|
||||||
} else {
|
|
||||||
assert.ElementsMatch(t, exp.needs, act.Needs, "[%s] Needs", exp.name)
|
|
||||||
}
|
|
||||||
if exp.runs == nil {
|
|
||||||
assert.Nil(t, act.Runs, "[%s] Runs", exp.name)
|
|
||||||
} else {
|
|
||||||
assert.ElementsMatch(t, exp.runs, act.Runs, "[%s] Runs", exp.name)
|
|
||||||
}
|
|
||||||
if exp.args == nil {
|
|
||||||
assert.Nil(t, act.Args, "[%s] Args", exp.name)
|
|
||||||
} else {
|
|
||||||
assert.ElementsMatch(t, exp.args, act.Args, "[%s] Args", exp.name)
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
if exp.env == nil {
|
|
||||||
assert.Nil(t, act.Env, "[%s] Env", exp.name)
|
|
||||||
} else {
|
|
||||||
assert.ElementsMatch(t, exp.env, act.Env, "[%s] Env", exp.name)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
if exp.secrets == nil {
|
|
||||||
assert.Nil(t, act.Secrets, "[%s] Secrets", exp.name)
|
|
||||||
} else {
|
|
||||||
assert.ElementsMatch(t, exp.secrets, act.Secrets, "[%s] Secrets", exp.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+43
-19
@@ -6,15 +6,17 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
|
"github.com/actions/workflow-parser/model"
|
||||||
|
"github.com/actions/workflow-parser/parser"
|
||||||
"github.com/nektos/act/common"
|
"github.com/nektos/act/common"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
type runnerImpl struct {
|
type runnerImpl struct {
|
||||||
config *RunnerConfig
|
config *RunnerConfig
|
||||||
workflows *workflowsFile
|
workflowConfig *model.Configuration
|
||||||
tempDir string
|
tempDir string
|
||||||
eventJSON string
|
eventJSON string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRunner Creates a new Runner
|
// NewRunner Creates a new Runner
|
||||||
@@ -56,7 +58,15 @@ func (runner *runnerImpl) setupWorkflows() error {
|
|||||||
|
|
||||||
defer workflowReader.Close()
|
defer workflowReader.Close()
|
||||||
|
|
||||||
runner.workflows, err = parseWorkflowsFile(workflowReader)
|
runner.workflowConfig, err = parser.Parse(workflowReader)
|
||||||
|
/*
|
||||||
|
if err != nil {
|
||||||
|
parserError := err.(*parser.ParserError)
|
||||||
|
for _, e := range parserError.Errors {
|
||||||
|
fmt.Fprintln(os.Stderr, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +98,7 @@ func (runner *runnerImpl) resolvePath(path string) string {
|
|||||||
func (runner *runnerImpl) ListEvents() []string {
|
func (runner *runnerImpl) ListEvents() []string {
|
||||||
log.Debugf("Listing all events")
|
log.Debugf("Listing all events")
|
||||||
events := make([]string, 0)
|
events := make([]string, 0)
|
||||||
for _, w := range runner.workflows.Workflow {
|
for _, w := range runner.workflowConfig.Workflows {
|
||||||
events = append(events, w.On)
|
events = append(events, w.On)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,17 +113,14 @@ func (runner *runnerImpl) ListEvents() []string {
|
|||||||
// GraphEvent builds an execution path
|
// GraphEvent builds an execution path
|
||||||
func (runner *runnerImpl) GraphEvent(eventName string) ([][]string, error) {
|
func (runner *runnerImpl) GraphEvent(eventName string) ([][]string, error) {
|
||||||
log.Debugf("Listing actions for event '%s'", eventName)
|
log.Debugf("Listing actions for event '%s'", eventName)
|
||||||
workflow, _, err := runner.workflows.getWorkflow(eventName)
|
resolves := runner.resolveEvent(runner.config.EventName)
|
||||||
if err != nil {
|
return newExecutionGraph(runner.workflowConfig, resolves...), nil
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return runner.workflows.newExecutionGraph(workflow.Resolves...), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunAction runs a set of actions in parallel, and their dependencies
|
// RunAction runs a set of actions in parallel, and their dependencies
|
||||||
func (runner *runnerImpl) RunActions(actionNames ...string) error {
|
func (runner *runnerImpl) RunActions(actionNames ...string) error {
|
||||||
log.Debugf("Running actions %+q", actionNames)
|
log.Debugf("Running actions %+q", actionNames)
|
||||||
graph := runner.workflows.newExecutionGraph(actionNames...)
|
graph := newExecutionGraph(runner.workflowConfig, actionNames...)
|
||||||
|
|
||||||
pipeline := make([]common.Executor, 0)
|
pipeline := make([]common.Executor, 0)
|
||||||
for _, actions := range graph {
|
for _, actions := range graph {
|
||||||
@@ -131,15 +138,32 @@ func (runner *runnerImpl) RunActions(actionNames ...string) error {
|
|||||||
// RunEvent runs the actions for a single event
|
// RunEvent runs the actions for a single event
|
||||||
func (runner *runnerImpl) RunEvent() error {
|
func (runner *runnerImpl) RunEvent() error {
|
||||||
log.Debugf("Running event '%s'", runner.config.EventName)
|
log.Debugf("Running event '%s'", runner.config.EventName)
|
||||||
workflow, _, err := runner.workflows.getWorkflow(runner.config.EventName)
|
resolves := runner.resolveEvent(runner.config.EventName)
|
||||||
if err != nil {
|
log.Debugf("Running actions %s -> %s", runner.config.EventName, resolves)
|
||||||
return err
|
return runner.RunActions(resolves...)
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("Running actions %s -> %s", runner.config.EventName, workflow.Resolves)
|
|
||||||
return runner.RunActions(workflow.Resolves...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (runner *runnerImpl) Close() error {
|
func (runner *runnerImpl) Close() error {
|
||||||
return os.RemoveAll(runner.tempDir)
|
return os.RemoveAll(runner.tempDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get list of resolves for an event
|
||||||
|
func (runner *runnerImpl) resolveEvent(eventName string) []string {
|
||||||
|
workflows := runner.workflowConfig.GetWorkflows(runner.config.EventName)
|
||||||
|
resolves := make([]string, 0)
|
||||||
|
for _, workflow := range workflows {
|
||||||
|
for _, resolve := range workflow.Resolves {
|
||||||
|
found := false
|
||||||
|
for _, r := range resolves {
|
||||||
|
if r == resolve {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
resolves = append(resolves, resolve)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resolves
|
||||||
|
}
|
||||||
+101
-50
@@ -9,25 +9,96 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/actions/workflow-parser/model"
|
||||||
"github.com/nektos/act/common"
|
"github.com/nektos/act/common"
|
||||||
"github.com/nektos/act/container"
|
"github.com/nektos/act/container"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (runner *runnerImpl) newActionExecutor(actionName string) common.Executor {
|
func (runner *runnerImpl) newActionExecutor(actionName string) common.Executor {
|
||||||
action, err := runner.workflows.getAction(actionName)
|
action := runner.workflowConfig.GetAction(actionName)
|
||||||
|
if action == nil {
|
||||||
|
return common.NewErrorExecutor(fmt.Errorf("Unable to find action named '%s'", actionName))
|
||||||
|
}
|
||||||
|
|
||||||
|
executors := make([]common.Executor, 0)
|
||||||
|
image, err := runner.addImageExecutor(action, &executors)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return common.NewErrorExecutor(err)
|
return common.NewErrorExecutor(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
env := make(map[string]string)
|
err = runner.addRunExecutor(action, image, &executors)
|
||||||
for _, applier := range []environmentApplier{action, runner} {
|
if err != nil {
|
||||||
applier.applyEnvironment(env)
|
return common.NewErrorExecutor(err)
|
||||||
}
|
}
|
||||||
env["GITHUB_ACTION"] = actionName
|
|
||||||
|
|
||||||
logger := newActionLogger(actionName, runner.config.Dryrun)
|
return common.NewPipelineExecutor(executors...)
|
||||||
log.Debugf("Using '%s' for action '%s'", action.Uses, actionName)
|
}
|
||||||
|
|
||||||
|
func (runner *runnerImpl) addImageExecutor(action *model.Action, executors *[]common.Executor) (string, error) {
|
||||||
|
var image string
|
||||||
|
logger := newActionLogger(action.Identifier, runner.config.Dryrun)
|
||||||
|
log.Debugf("Using '%s' for action '%s'", action.Uses, action.Identifier)
|
||||||
|
|
||||||
|
in := container.DockerExecutorInput{
|
||||||
|
Ctx: runner.config.Ctx,
|
||||||
|
Logger: logger,
|
||||||
|
Dryrun: runner.config.Dryrun,
|
||||||
|
}
|
||||||
|
switch uses := action.Uses.(type) {
|
||||||
|
|
||||||
|
case *model.UsesDockerImage:
|
||||||
|
image = uses.Image
|
||||||
|
*executors = append(*executors, container.NewDockerPullExecutor(container.NewDockerPullExecutorInput{
|
||||||
|
DockerExecutorInput: in,
|
||||||
|
Image: image,
|
||||||
|
}))
|
||||||
|
|
||||||
|
case *model.UsesPath:
|
||||||
|
contextDir := filepath.Join(runner.config.WorkingDir, uses.String())
|
||||||
|
sha, _, err := common.FindGitRevision(contextDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Unable to determine git revision: %v", err)
|
||||||
|
sha = "latest"
|
||||||
|
}
|
||||||
|
image = fmt.Sprintf("%s:%s", filepath.Base(contextDir), sha)
|
||||||
|
|
||||||
|
*executors = append(*executors, container.NewDockerBuildExecutor(container.NewDockerBuildExecutorInput{
|
||||||
|
DockerExecutorInput: in,
|
||||||
|
ContextDir: contextDir,
|
||||||
|
ImageTag: image,
|
||||||
|
}))
|
||||||
|
|
||||||
|
case *model.UsesRepository:
|
||||||
|
image = fmt.Sprintf("%s:%s", filepath.Base(uses.Repository), uses.Ref)
|
||||||
|
cloneURL := fmt.Sprintf("https://github.com/%s", uses.Repository)
|
||||||
|
|
||||||
|
cloneDir := filepath.Join(os.TempDir(), "act", action.Uses.String())
|
||||||
|
*executors = append(*executors, common.NewGitCloneExecutor(common.NewGitCloneExecutorInput{
|
||||||
|
URL: cloneURL,
|
||||||
|
Ref: uses.Ref,
|
||||||
|
Dir: cloneDir,
|
||||||
|
Logger: logger,
|
||||||
|
Dryrun: runner.config.Dryrun,
|
||||||
|
}))
|
||||||
|
|
||||||
|
contextDir := filepath.Join(cloneDir, uses.Path)
|
||||||
|
*executors = append(*executors, container.NewDockerBuildExecutor(container.NewDockerBuildExecutorInput{
|
||||||
|
DockerExecutorInput: in,
|
||||||
|
ContextDir: contextDir,
|
||||||
|
ImageTag: image,
|
||||||
|
}))
|
||||||
|
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("unable to determine executor type for image '%s'", action.Uses)
|
||||||
|
}
|
||||||
|
|
||||||
|
return image, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (runner *runnerImpl) addRunExecutor(action *model.Action, image string, executors *[]common.Executor) error {
|
||||||
|
logger := newActionLogger(action.Identifier, runner.config.Dryrun)
|
||||||
|
log.Debugf("Using '%s' for action '%s'", action.Uses, action.Identifier)
|
||||||
|
|
||||||
in := container.DockerExecutorInput{
|
in := container.DockerExecutorInput{
|
||||||
Ctx: runner.config.Ctx,
|
Ctx: runner.config.Ctx,
|
||||||
@@ -35,61 +106,37 @@ func (runner *runnerImpl) newActionExecutor(actionName string) common.Executor {
|
|||||||
Dryrun: runner.config.Dryrun,
|
Dryrun: runner.config.Dryrun,
|
||||||
}
|
}
|
||||||
|
|
||||||
var image string
|
env := make(map[string]string)
|
||||||
executors := make([]common.Executor, 0)
|
for _, applier := range []environmentApplier{newActionEnvironmentApplier(action), runner} {
|
||||||
if imageRef, ok := parseImageReference(action.Uses); ok {
|
applier.applyEnvironment(env)
|
||||||
executors = append(executors, container.NewDockerPullExecutor(container.NewDockerPullExecutorInput{
|
|
||||||
DockerExecutorInput: in,
|
|
||||||
Image: imageRef,
|
|
||||||
}))
|
|
||||||
image = imageRef
|
|
||||||
} else if contextDir, imageTag, ok := parseImageLocal(runner.config.WorkingDir, action.Uses); ok {
|
|
||||||
executors = append(executors, container.NewDockerBuildExecutor(container.NewDockerBuildExecutorInput{
|
|
||||||
DockerExecutorInput: in,
|
|
||||||
ContextDir: contextDir,
|
|
||||||
ImageTag: imageTag,
|
|
||||||
}))
|
|
||||||
image = imageTag
|
|
||||||
} else if cloneURL, ref, path, ok := parseImageGithub(action.Uses); ok {
|
|
||||||
cloneDir := filepath.Join(os.TempDir(), "act", action.Uses)
|
|
||||||
executors = append(executors, common.NewGitCloneExecutor(common.NewGitCloneExecutorInput{
|
|
||||||
URL: cloneURL,
|
|
||||||
Ref: ref,
|
|
||||||
Dir: cloneDir,
|
|
||||||
Logger: logger,
|
|
||||||
Dryrun: runner.config.Dryrun,
|
|
||||||
}))
|
|
||||||
|
|
||||||
contextDir := filepath.Join(cloneDir, path)
|
|
||||||
imageTag := fmt.Sprintf("%s:%s", filepath.Base(cloneURL.Path), ref)
|
|
||||||
|
|
||||||
executors = append(executors, container.NewDockerBuildExecutor(container.NewDockerBuildExecutorInput{
|
|
||||||
DockerExecutorInput: in,
|
|
||||||
ContextDir: contextDir,
|
|
||||||
ImageTag: imageTag,
|
|
||||||
}))
|
|
||||||
image = imageTag
|
|
||||||
} else {
|
|
||||||
return common.NewErrorExecutor(fmt.Errorf("unable to determine executor type for image '%s'", action.Uses))
|
|
||||||
}
|
}
|
||||||
|
env["GITHUB_ACTION"] = action.Identifier
|
||||||
|
|
||||||
ghReader, err := runner.createGithubTarball()
|
ghReader, err := runner.createGithubTarball()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return common.NewErrorExecutor(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
envList := make([]string, 0)
|
envList := make([]string, 0)
|
||||||
for k, v := range env {
|
for k, v := range env {
|
||||||
envList = append(envList, fmt.Sprintf("%s=%s", k, v))
|
envList = append(envList, fmt.Sprintf("%s=%s", k, v))
|
||||||
}
|
}
|
||||||
executors = append(executors, container.NewDockerRunExecutor(container.NewDockerRunExecutorInput{
|
|
||||||
|
var cmd, entrypoint []string
|
||||||
|
if action.Args != nil {
|
||||||
|
cmd = action.Args.Split()
|
||||||
|
}
|
||||||
|
if action.Runs != nil {
|
||||||
|
entrypoint = action.Runs.Split()
|
||||||
|
}
|
||||||
|
*executors = append(*executors, container.NewDockerRunExecutor(container.NewDockerRunExecutorInput{
|
||||||
DockerExecutorInput: in,
|
DockerExecutorInput: in,
|
||||||
Cmd: action.Args,
|
Cmd: cmd,
|
||||||
Entrypoint: action.Runs,
|
Entrypoint: entrypoint,
|
||||||
Image: image,
|
Image: image,
|
||||||
WorkingDir: "/github/workspace",
|
WorkingDir: "/github/workspace",
|
||||||
Env: envList,
|
Env: envList,
|
||||||
Name: runner.createContainerName(actionName),
|
Name: runner.createContainerName(action.Identifier),
|
||||||
Binds: []string{
|
Binds: []string{
|
||||||
fmt.Sprintf("%s:%s", runner.config.WorkingDir, "/github/workspace"),
|
fmt.Sprintf("%s:%s", runner.config.WorkingDir, "/github/workspace"),
|
||||||
fmt.Sprintf("%s:%s", runner.tempDir, "/github/home"),
|
fmt.Sprintf("%s:%s", runner.tempDir, "/github/home"),
|
||||||
@@ -99,13 +146,17 @@ func (runner *runnerImpl) newActionExecutor(actionName string) common.Executor {
|
|||||||
ReuseContainers: runner.config.ReuseContainers,
|
ReuseContainers: runner.config.ReuseContainers,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return common.NewPipelineExecutor(executors...)
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (runner *runnerImpl) applyEnvironment(env map[string]string) {
|
func (runner *runnerImpl) applyEnvironment(env map[string]string) {
|
||||||
repoPath := runner.config.WorkingDir
|
repoPath := runner.config.WorkingDir
|
||||||
|
|
||||||
_, workflowName, _ := runner.workflows.getWorkflow(runner.config.EventName)
|
workflows := runner.workflowConfig.GetWorkflows(runner.config.EventName)
|
||||||
|
if len(workflows) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
workflowName := workflows[0].Identifier
|
||||||
|
|
||||||
env["HOME"] = "/github/home"
|
env["HOME"] = "/github/home"
|
||||||
env["GITHUB_ACTOR"] = "nektos/act"
|
env["GITHUB_ACTOR"] = "nektos/act"
|
||||||
|
|||||||
+32
-76
@@ -1,91 +1,47 @@
|
|||||||
package actions
|
package actions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"context"
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/nektos/act/common"
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/stretchr/testify/assert"
|
"gotest.tools/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseImageReference(t *testing.T) {
|
func TestRunEvent(t *testing.T) {
|
||||||
log.SetLevel(log.DebugLevel)
|
if testing.Short() {
|
||||||
tables := []struct {
|
t.Skip("skipping integration test")
|
||||||
refIn string
|
|
||||||
refOut string
|
|
||||||
ok bool
|
|
||||||
}{
|
|
||||||
{"docker://myhost.com/foo/bar", "myhost.com/foo/bar", true},
|
|
||||||
{"docker://ubuntu", "ubuntu", true},
|
|
||||||
{"docker://ubuntu:18.04", "ubuntu:18.04", true},
|
|
||||||
{"docker://cibuilds/hugo:0.53", "cibuilds/hugo:0.53", true},
|
|
||||||
{"http://google.com:8080", "", false},
|
|
||||||
{"./foo", "", false},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tables := []struct {
|
||||||
|
workflowPath string
|
||||||
|
eventName string
|
||||||
|
errorMessage string
|
||||||
|
}{
|
||||||
|
{"basic.workflow", "push", ""},
|
||||||
|
{"pipe.workflow", "push", ""},
|
||||||
|
{"fail.workflow", "push", "exit with `FAILURE`: 1"},
|
||||||
|
{"regex.workflow", "push", "exit with `NEUTRAL`: 78"},
|
||||||
|
{"gitref.workflow", "push", ""},
|
||||||
|
{"env.workflow", "push", ""},
|
||||||
|
}
|
||||||
|
log.SetLevel(log.DebugLevel)
|
||||||
|
|
||||||
for _, table := range tables {
|
for _, table := range tables {
|
||||||
refOut, ok := parseImageReference(table.refIn)
|
runnerConfig := &RunnerConfig{
|
||||||
assert.Equal(t, table.refOut, refOut)
|
Ctx: context.Background(),
|
||||||
assert.Equal(t, table.ok, ok)
|
WorkflowPath: table.workflowPath,
|
||||||
}
|
WorkingDir: "testdata",
|
||||||
|
EventName: table.eventName,
|
||||||
|
}
|
||||||
|
runner, err := NewRunner(runnerConfig)
|
||||||
|
assert.NilError(t, err, table.workflowPath)
|
||||||
|
|
||||||
}
|
err = runner.RunEvent()
|
||||||
|
if table.errorMessage == "" {
|
||||||
func TestParseImageLocal(t *testing.T) {
|
assert.NilError(t, err, table.workflowPath)
|
||||||
log.SetLevel(log.DebugLevel)
|
} else {
|
||||||
tables := []struct {
|
assert.Error(t, err, table.errorMessage)
|
||||||
pathIn string
|
|
||||||
contextDir string
|
|
||||||
refTag string
|
|
||||||
ok bool
|
|
||||||
}{
|
|
||||||
{"docker://myhost.com/foo/bar", "", "", false},
|
|
||||||
{"http://google.com:8080", "", "", false},
|
|
||||||
{"example/action1", "", "", false},
|
|
||||||
{"./example/action1", "/example/action1", "action1:", true},
|
|
||||||
}
|
|
||||||
|
|
||||||
revision, _, err := common.FindGitRevision(".")
|
|
||||||
assert.Nil(t, err)
|
|
||||||
basedir, err := filepath.Abs("..")
|
|
||||||
assert.Nil(t, err)
|
|
||||||
for _, table := range tables {
|
|
||||||
contextDir, refTag, ok := parseImageLocal(basedir, table.pathIn)
|
|
||||||
assert.Equal(t, table.ok, ok, "ok match for %s", table.pathIn)
|
|
||||||
if ok {
|
|
||||||
assert.Equal(t, fmt.Sprintf("%s%s", basedir, table.contextDir), contextDir, "context dir doesn't match for %s", table.pathIn)
|
|
||||||
assert.Equal(t, fmt.Sprintf("%s%s", table.refTag, revision), refTag)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
func TestParseImageGithub(t *testing.T) {
|
|
||||||
log.SetLevel(log.DebugLevel)
|
|
||||||
tables := []struct {
|
|
||||||
image string
|
|
||||||
cloneURL string
|
|
||||||
ref string
|
|
||||||
path string
|
|
||||||
ok bool
|
|
||||||
}{
|
|
||||||
{"nektos/act", "https://github.com/nektos/act", "master", ".", true},
|
|
||||||
{"nektos/act/foo", "https://github.com/nektos/act", "master", "foo", true},
|
|
||||||
{"nektos/act@xxxxx", "https://github.com/nektos/act", "xxxxx", ".", true},
|
|
||||||
{"nektos/act/bar/baz@zzzzz", "https://github.com/nektos/act", "zzzzz", "bar/baz", true},
|
|
||||||
{"assimovt/actions-github-deploy/github-deploy@deployment-status-metadata", "https://github.com/assimovt/actions-github-deploy", "deployment-status-metadata", "github-deploy", true},
|
|
||||||
{"nektos/zzzzundefinedzzzz", "", "", "", false},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, table := range tables {
|
|
||||||
cloneURL, ref, path, ok := parseImageGithub(table.image)
|
|
||||||
assert.Equal(t, table.ok, ok, "ok match for %s", table.image)
|
|
||||||
if ok {
|
|
||||||
assert.Equal(t, table.cloneURL, cloneURL.String())
|
|
||||||
assert.Equal(t, table.ref, ref)
|
|
||||||
assert.Equal(t, table.path, path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
File renamed without changes.
File renamed without changes.
@@ -10,7 +10,7 @@ action "build" {
|
|||||||
|
|
||||||
action "test" {
|
action "test" {
|
||||||
uses = "docker://ubuntu:18.04"
|
uses = "docker://ubuntu:18.04"
|
||||||
args = "echo 'test'"
|
args = "env"
|
||||||
needs = ["build"]
|
needs = ["build"]
|
||||||
}
|
}
|
||||||
|
|
||||||
Vendored
+9
@@ -0,0 +1,9 @@
|
|||||||
|
workflow "test" {
|
||||||
|
on = "push"
|
||||||
|
resolves = ["test-action"]
|
||||||
|
}
|
||||||
|
|
||||||
|
action "test-action" {
|
||||||
|
uses = "docker://alpine:3.9"
|
||||||
|
runs = ["sh", "-c", "echo $GITHUB_REPOSITORY | grep '^nektos/act$'"]
|
||||||
|
}
|
||||||
Vendored
+13
@@ -0,0 +1,13 @@
|
|||||||
|
workflow "test" {
|
||||||
|
on = "push"
|
||||||
|
resolves = ["test-action"]
|
||||||
|
}
|
||||||
|
|
||||||
|
action "test-action" {
|
||||||
|
uses = "docker://alpine:3.9"
|
||||||
|
runs = ["sh", "-c", "echo $IN | grep $OUT"]
|
||||||
|
env = {
|
||||||
|
IN = "foo"
|
||||||
|
OUT = "bar"
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+14
@@ -0,0 +1,14 @@
|
|||||||
|
workflow "New workflow" {
|
||||||
|
on = "push"
|
||||||
|
resolves = ["branch-ref","commit-ref"]
|
||||||
|
}
|
||||||
|
|
||||||
|
action "branch-ref" {
|
||||||
|
uses = "actions/docker/cli@master"
|
||||||
|
args = "version"
|
||||||
|
}
|
||||||
|
|
||||||
|
action "commit-ref" {
|
||||||
|
uses = "actions/docker/cli@c08a5fc9e0286844156fefff2c141072048141f6"
|
||||||
|
args = "version"
|
||||||
|
}
|
||||||
Vendored
+13
@@ -0,0 +1,13 @@
|
|||||||
|
workflow "test" {
|
||||||
|
on = "push"
|
||||||
|
resolves = ["test-action"]
|
||||||
|
}
|
||||||
|
|
||||||
|
action "test-action" {
|
||||||
|
uses = "docker://alpine:3.9"
|
||||||
|
runs = ["sh", "-c", "echo $IN | grep $OUT"]
|
||||||
|
env = {
|
||||||
|
IN = "foo"
|
||||||
|
OUT = "foo"
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+9
@@ -0,0 +1,9 @@
|
|||||||
|
workflow "New workflow" {
|
||||||
|
on = "push"
|
||||||
|
resolves = ["filter-version-before-deploy"]
|
||||||
|
}
|
||||||
|
|
||||||
|
action "filter-version-before-deploy" {
|
||||||
|
uses = "actions/bin/filter@master"
|
||||||
|
args = "tag z?[0-9]+\\.[0-9]+\\.[0-9]+"
|
||||||
|
}
|
||||||
+19
-12
@@ -6,7 +6,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -185,7 +184,7 @@ func findGitDirectory(fromFile string) (string, error) {
|
|||||||
|
|
||||||
// NewGitCloneExecutorInput the input for the NewGitCloneExecutor
|
// NewGitCloneExecutorInput the input for the NewGitCloneExecutor
|
||||||
type NewGitCloneExecutorInput struct {
|
type NewGitCloneExecutorInput struct {
|
||||||
URL *url.URL
|
URL string
|
||||||
Ref string
|
Ref string
|
||||||
Dir string
|
Dir string
|
||||||
Logger *log.Entry
|
Logger *log.Entry
|
||||||
@@ -195,8 +194,8 @@ type NewGitCloneExecutorInput struct {
|
|||||||
// NewGitCloneExecutor creates an executor to clone git repos
|
// NewGitCloneExecutor creates an executor to clone git repos
|
||||||
func NewGitCloneExecutor(input NewGitCloneExecutorInput) Executor {
|
func NewGitCloneExecutor(input NewGitCloneExecutorInput) Executor {
|
||||||
return func() error {
|
return func() error {
|
||||||
input.Logger.Infof("git clone '%s'", input.URL.String())
|
input.Logger.Infof("git clone '%s' # ref=%s", input.URL, input.Ref)
|
||||||
input.Logger.Debugf(" cloning %s to %s", input.URL.String(), input.Dir)
|
input.Logger.Debugf(" cloning %s to %s", input.URL, input.Dir)
|
||||||
|
|
||||||
if input.Dryrun {
|
if input.Dryrun {
|
||||||
return nil
|
return nil
|
||||||
@@ -210,11 +209,12 @@ func NewGitCloneExecutor(input NewGitCloneExecutorInput) Executor {
|
|||||||
r, err := git.PlainOpen(input.Dir)
|
r, err := git.PlainOpen(input.Dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r, err = git.PlainClone(input.Dir, false, &git.CloneOptions{
|
r, err = git.PlainClone(input.Dir, false, &git.CloneOptions{
|
||||||
URL: input.URL.String(),
|
URL: input.URL,
|
||||||
Progress: input.Logger.WriterLevel(log.DebugLevel),
|
Progress: input.Logger.WriterLevel(log.DebugLevel),
|
||||||
ReferenceName: refName,
|
//ReferenceName: refName,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
input.Logger.Errorf("Unable to clone %v %s: %v", input.URL, refName, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -225,17 +225,24 @@ func NewGitCloneExecutor(input NewGitCloneExecutorInput) Executor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
err = w.Pull(&git.PullOptions{
|
err = w.Pull(&git.PullOptions{
|
||||||
ReferenceName: refName,
|
//ReferenceName: refName,
|
||||||
Force: true,
|
Force: true,
|
||||||
})
|
})
|
||||||
if err != nil && err.Error() != "already up-to-date" {
|
if err != nil && err.Error() != "already up-to-date" {
|
||||||
input.Logger.Errorf("Unable to pull %s: %v", refName, err)
|
input.Logger.Errorf("Unable to pull %s: %v", refName, err)
|
||||||
}
|
}
|
||||||
input.Logger.Debugf("Cloned %s to %s", input.URL.String(), input.Dir)
|
input.Logger.Debugf("Cloned %s to %s", input.URL, input.Dir)
|
||||||
|
|
||||||
|
hash, err := r.ResolveRevision(plumbing.Revision(input.Ref))
|
||||||
|
if err != nil {
|
||||||
|
input.Logger.Errorf("Unable to resolve %s: %v", input.Ref, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
err = w.Checkout(&git.CheckoutOptions{
|
err = w.Checkout(&git.CheckoutOptions{
|
||||||
Branch: refName,
|
//Branch: refName,
|
||||||
Force: true,
|
Hash: *hash,
|
||||||
|
Force: true,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
input.Logger.Errorf("Unable to checkout %s: %v", refName, err)
|
input.Logger.Errorf("Unable to checkout %s: %v", refName, err)
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ func waitContainer(input NewDockerRunExecutorInput, cli *client.Client, containe
|
|||||||
if statusCode == 0 {
|
if statusCode == 0 {
|
||||||
return nil
|
return nil
|
||||||
} else if statusCode == 78 {
|
} else if statusCode == 78 {
|
||||||
return fmt.Errorf("exiting with `NEUTRAL`: 78")
|
return fmt.Errorf("exit with `NEUTRAL`: 78")
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("exit with `FAILURE`: %v", statusCode)
|
return fmt.Errorf("exit with `FAILURE`: %v", statusCode)
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"foo": "bar"
|
|
||||||
}
|
|
||||||
@@ -4,40 +4,47 @@ require (
|
|||||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
|
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
|
||||||
github.com/Microsoft/go-winio v0.4.11 // indirect
|
github.com/Microsoft/go-winio v0.4.11 // indirect
|
||||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
|
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
|
||||||
|
github.com/actions/workflow-parser v1.0.0
|
||||||
github.com/containerd/continuity v0.0.0-20181203112020-004b46473808 // indirect
|
github.com/containerd/continuity v0.0.0-20181203112020-004b46473808 // indirect
|
||||||
github.com/docker/distribution v2.7.0+incompatible // indirect
|
github.com/docker/distribution v2.7.1+incompatible // indirect
|
||||||
github.com/docker/docker v1.13.1
|
github.com/docker/docker v1.13.1
|
||||||
github.com/docker/go-connections v0.4.0 // indirect
|
github.com/docker/go-connections v0.4.0 // indirect
|
||||||
github.com/docker/go-units v0.3.3 // indirect
|
github.com/docker/go-units v0.3.3 // indirect
|
||||||
|
github.com/emirpasic/gods v1.12.0 // indirect
|
||||||
github.com/go-ini/ini v1.41.0
|
github.com/go-ini/ini v1.41.0
|
||||||
github.com/gogo/protobuf v1.2.0 // indirect
|
github.com/gogo/protobuf v1.2.0 // indirect
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
|
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
|
||||||
github.com/gorilla/context v1.1.1 // indirect
|
github.com/gorilla/mux v1.7.0 // indirect
|
||||||
github.com/gorilla/mux v1.6.2 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
github.com/hashicorp/hcl v1.0.0
|
|
||||||
github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c
|
github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c
|
||||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||||
github.com/jtolds/gls v4.2.1+incompatible // indirect
|
github.com/jtolds/gls v4.2.1+incompatible // indirect
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
|
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
|
||||||
github.com/opencontainers/image-spec v1.0.1 // indirect
|
github.com/opencontainers/image-spec v1.0.1 // indirect
|
||||||
github.com/opencontainers/runc v0.1.1 // indirect
|
github.com/opencontainers/runc v0.1.1 // indirect
|
||||||
github.com/pkg/errors v0.8.1 // indirect
|
github.com/pkg/errors v0.8.1 // indirect
|
||||||
github.com/sirupsen/logrus v1.3.0
|
github.com/sirupsen/logrus v1.3.0
|
||||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect
|
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 // indirect
|
||||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect
|
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect
|
||||||
|
github.com/soniakeys/graph v0.0.0 // indirect
|
||||||
github.com/spf13/cobra v0.0.3
|
github.com/spf13/cobra v0.0.3
|
||||||
github.com/spf13/pflag v1.0.3 // indirect
|
github.com/spf13/pflag v1.0.3 // indirect
|
||||||
github.com/stretchr/testify v1.3.0
|
github.com/stretchr/testify v1.3.0
|
||||||
golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc
|
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613
|
||||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3 // indirect
|
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3 // indirect
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 // indirect
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 // indirect
|
||||||
golang.org/x/sys v0.0.0-20190102155601-82a175fd1598 // indirect
|
golang.org/x/sys v0.0.0-20190201152629-afcc84fd7533 // indirect
|
||||||
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 // indirect
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c // indirect
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c // indirect
|
||||||
google.golang.org/grpc v1.17.0 // indirect
|
google.golang.org/genproto v0.0.0-20190128161407-8ac453e89fca // indirect
|
||||||
|
google.golang.org/grpc v1.18.0 // indirect
|
||||||
gopkg.in/ini.v1 v1.41.0 // indirect
|
gopkg.in/ini.v1 v1.41.0 // indirect
|
||||||
gopkg.in/src-d/go-git.v4 v4.8.1
|
gopkg.in/src-d/go-billy.v4 v4.3.0 // indirect
|
||||||
|
gopkg.in/src-d/go-git-fixtures.v3 v3.3.0 // indirect
|
||||||
|
gopkg.in/src-d/go-git.v4 v4.9.1
|
||||||
gopkg.in/yaml.v2 v2.2.2
|
gopkg.in/yaml.v2 v2.2.2
|
||||||
gotest.tools v2.2.0+incompatible // indirect
|
gotest.tools v2.2.0+incompatible
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/docker/docker => github.com/docker/engine v0.0.0-20181106193140-f5749085e9cb
|
replace github.com/docker/docker => github.com/docker/engine v0.0.0-20181106193140-f5749085e9cb
|
||||||
@@ -5,6 +5,8 @@ github.com/Microsoft/go-winio v0.4.11 h1:zoIOcVf0xPN1tnMVbTtEdI+P8OofVk3NObnwOQ6
|
|||||||
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
|
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
|
||||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
|
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
|
||||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
|
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
|
||||||
|
github.com/actions/workflow-parser v1.0.0 h1:Zz2Ke31f3OMYCSzU2pqZSsk/Oz+lWXfEiXMisjxgGcc=
|
||||||
|
github.com/actions/workflow-parser v1.0.0/go.mod h1:jz9ZVl8zUIcjMfDQearQjvUHIBhx9l1ys4keDd6be34=
|
||||||
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
|
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
|
||||||
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
|
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
|
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
|
||||||
@@ -15,8 +17,8 @@ github.com/containerd/continuity v0.0.0-20181203112020-004b46473808/go.mod h1:GL
|
|||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/docker/distribution v2.7.0+incompatible h1:neUDAlf3wX6Ml4HdqTrbcOHXtfRN0TFIwt6YFL7N9RU=
|
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
|
||||||
github.com/docker/distribution v2.7.0+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||||
github.com/docker/docker v1.13.1 h1:IkZjBSIc8hBjLpqeAbeE5mca5mNgeatLHBy3GO78BWo=
|
github.com/docker/docker v1.13.1 h1:IkZjBSIc8hBjLpqeAbeE5mca5mNgeatLHBy3GO78BWo=
|
||||||
github.com/docker/docker v1.13.1/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
github.com/docker/docker v1.13.1/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
github.com/docker/engine v0.0.0-20181106193140-f5749085e9cb h1:PyjxRdW1mqCmSoxy/6uP01P7CGbsD+woX+oOWbaUPwQ=
|
github.com/docker/engine v0.0.0-20181106193140-f5749085e9cb h1:PyjxRdW1mqCmSoxy/6uP01P7CGbsD+woX+oOWbaUPwQ=
|
||||||
@@ -27,6 +29,8 @@ github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk
|
|||||||
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
github.com/emirpasic/gods v1.9.0 h1:rUF4PuzEjMChMiNsVjdI+SyLu7rEqpQ5reNFnhC7oFo=
|
github.com/emirpasic/gods v1.9.0 h1:rUF4PuzEjMChMiNsVjdI+SyLu7rEqpQ5reNFnhC7oFo=
|
||||||
github.com/emirpasic/gods v1.9.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
|
github.com/emirpasic/gods v1.9.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
|
||||||
|
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
|
||||||
|
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
|
||||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
|
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
|
||||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||||
github.com/gliderlabs/ssh v0.1.1 h1:j3L6gSLQalDETeEg/Jg0mGY0/y/N6zI2xX1978P0Uqw=
|
github.com/gliderlabs/ssh v0.1.1 h1:j3L6gSLQalDETeEg/Jg0mGY0/y/N6zI2xX1978P0Uqw=
|
||||||
@@ -37,6 +41,7 @@ github.com/gogo/protobuf v1.2.0 h1:xU6/SpYbvkNYiptHJYEDRseDLvYE7wSqhYYNy0QSUzI=
|
|||||||
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
|
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
@@ -44,10 +49,8 @@ github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
|||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
|
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
github.com/gorilla/mux v1.7.0 h1:tOSd0UKHQd6urX6ApfOn4XdBMY6Sh1MfxV3kmaazO+U=
|
||||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||||
github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
|
|
||||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
|
||||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c h1:kQWxfPIHVLbgLzphqk3QUflDy9QdksZR4ygR807bpy0=
|
github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c h1:kQWxfPIHVLbgLzphqk3QUflDy9QdksZR4ygR807bpy0=
|
||||||
@@ -72,6 +75,8 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
|||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0=
|
github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0=
|
||||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
|
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
|
||||||
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||||
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
|
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
|
||||||
@@ -90,10 +95,14 @@ github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
|||||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||||
github.com/sirupsen/logrus v1.3.0 h1:hI/7Q+DtNZ2kINb6qt/lS+IyXnHQe9e90POfeewL/ME=
|
github.com/sirupsen/logrus v1.3.0 h1:hI/7Q+DtNZ2kINb6qt/lS+IyXnHQe9e90POfeewL/ME=
|
||||||
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY=
|
||||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w=
|
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w=
|
||||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
|
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
|
||||||
|
github.com/soniakeys/bits v1.0.0 h1:Rune9VFefdJvLE0Q5iRCVGiKdSu2iDihs2I6SCm7evw=
|
||||||
|
github.com/soniakeys/bits v1.0.0/go.mod h1:7yJHB//UizrUr64VFneewK6SX5oeCf0SMbDYe2ey1JA=
|
||||||
|
github.com/soniakeys/graph v0.0.0 h1:C/Rr8rv9wbhZIsYHcWJFoI84pkipJocMYdRteE+/PQA=
|
||||||
|
github.com/soniakeys/graph v0.0.0/go.mod h1:lxpIbor/bIzWUAqvt1Dx92Hr63uWeyuEAbPnsjYbVwM=
|
||||||
github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
|
github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
|
||||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||||
@@ -111,13 +120,16 @@ github.com/xanzy/ssh-agent v0.2.0 h1:Adglfbi5p9Z0BmK2oKU9nTG+zKfniSfnaMYB+ULd+Ro
|
|||||||
github.com/xanzy/ssh-agent v0.2.0/go.mod h1:0NyE30eGUDliuLEHJgYte/zncp2zdTStcOnWhgSqHD8=
|
github.com/xanzy/ssh-agent v0.2.0/go.mod h1:0NyE30eGUDliuLEHJgYte/zncp2zdTStcOnWhgSqHD8=
|
||||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I=
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I=
|
||||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc h1:F5tKCVGp+MUAHhKp5MZtGqAlGX3+oCsiL1Q629FL90M=
|
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613 h1:MQ/ZZiDsUapFFiMS+vzwXkCTeEKaum+Do5rINYJDmxc=
|
||||||
golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3 h1:eH6Eip3UpmR+yM/qI9Ijluzb1bNv/cAU/n+6l8tRSis=
|
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3 h1:ulvT7fqt0yHWzpJwI57MezWnYDVpCAYBVuYst/L+fAY=
|
||||||
|
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -127,10 +139,12 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h
|
|||||||
golang.org/x/sys v0.0.0-20180903190138-2b024373dcd9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180903190138-2b024373dcd9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190102155601-82a175fd1598 h1:S8GOgffXV1X3fpVG442QRfWOt0iFl79eHJ7OPt725bo=
|
golang.org/x/sys v0.0.0-20190201152629-afcc84fd7533 h1:bLfqnzrpeG4usq5OvMCrwTdmMJ6aTmlCuo1eKl0mhkI=
|
||||||
golang.org/x/sys v0.0.0-20190102155601-82a175fd1598/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190201152629-afcc84fd7533/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To=
|
||||||
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg=
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52 h1:JG/0uqcGdTNgq7FdU+61l5Pdmb8putNZlXb65bJBROs=
|
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52 h1:JG/0uqcGdTNgq7FdU+61l5Pdmb8putNZlXb65bJBROs=
|
||||||
@@ -138,8 +152,11 @@ golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGm
|
|||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
|
||||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
google.golang.org/grpc v1.17.0 h1:TRJYBgMclJvGYn2rIMjj+h9KtMt5r1Ij7ODVRIZkwhk=
|
google.golang.org/genproto v0.0.0-20190128161407-8ac453e89fca h1:L1odPN6KVjhk0Lbg41BKcjGjP7ELTvh/qDcyh6hEfv0=
|
||||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
google.golang.org/genproto v0.0.0-20190128161407-8ac453e89fca/go.mod h1:L3J43x8/uS+qIUoksaLKe6OS3nUKxOKuIFz1sl2/jx4=
|
||||||
|
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||||
|
google.golang.org/grpc v1.18.0 h1:IZl7mfBGfbhYx2p2rKRtYgDFw6SBz+kclmxYrCksPPA=
|
||||||
|
google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
@@ -148,10 +165,14 @@ gopkg.in/ini.v1 v1.41.0 h1:Ka3ViY6gNYSKiVy71zXBEqKplnV35ImDLVG+8uoIklE=
|
|||||||
gopkg.in/ini.v1 v1.41.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
gopkg.in/ini.v1 v1.41.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
gopkg.in/src-d/go-billy.v4 v4.2.1 h1:omN5CrMrMcQ+4I8bJ0wEhOBPanIRWzFC953IiXKdYzo=
|
gopkg.in/src-d/go-billy.v4 v4.2.1 h1:omN5CrMrMcQ+4I8bJ0wEhOBPanIRWzFC953IiXKdYzo=
|
||||||
gopkg.in/src-d/go-billy.v4 v4.2.1/go.mod h1:tm33zBoOwxjYHZIE+OV8bxTWFMJLrconzFMd38aARFk=
|
gopkg.in/src-d/go-billy.v4 v4.2.1/go.mod h1:tm33zBoOwxjYHZIE+OV8bxTWFMJLrconzFMd38aARFk=
|
||||||
|
gopkg.in/src-d/go-billy.v4 v4.3.0 h1:KtlZ4c1OWbIs4jCv5ZXrTqG8EQocr0g/d4DjNg70aek=
|
||||||
|
gopkg.in/src-d/go-billy.v4 v4.3.0/go.mod h1:tm33zBoOwxjYHZIE+OV8bxTWFMJLrconzFMd38aARFk=
|
||||||
gopkg.in/src-d/go-git-fixtures.v3 v3.1.1 h1:XWW/s5W18RaJpmo1l0IYGqXKuJITWRFuA45iOf1dKJs=
|
gopkg.in/src-d/go-git-fixtures.v3 v3.1.1 h1:XWW/s5W18RaJpmo1l0IYGqXKuJITWRFuA45iOf1dKJs=
|
||||||
gopkg.in/src-d/go-git-fixtures.v3 v3.1.1/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
|
gopkg.in/src-d/go-git-fixtures.v3 v3.1.1/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
|
||||||
gopkg.in/src-d/go-git.v4 v4.8.1 h1:aAyBmkdE1QUUEHcP4YFCGKmsMQRAuRmUcPEQR7lOAa0=
|
gopkg.in/src-d/go-git-fixtures.v3 v3.3.0 h1:AxUOwLW3at53ysFqs0Lg+H+8KSQXl7AEHBvWj8wEsT8=
|
||||||
gopkg.in/src-d/go-git.v4 v4.8.1/go.mod h1:Vtut8izDyrM8BUVQnzJ+YvmNcem2J89EmfZYCkLokZk=
|
gopkg.in/src-d/go-git-fixtures.v3 v3.3.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
|
||||||
|
gopkg.in/src-d/go-git.v4 v4.9.1 h1:0oKHJZY8tM7B71378cfTg2c5jmWyNlXvestTT6WfY+4=
|
||||||
|
gopkg.in/src-d/go-git.v4 v4.9.1/go.mod h1:Vtut8izDyrM8BUVQnzJ+YvmNcem2J89EmfZYCkLokZk=
|
||||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||||
|
|||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2019 GitHub
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Command represents the optional "runs" and "args" attributes.
|
||||||
|
// Each one takes one of two forms:
|
||||||
|
// - runs="entrypoint arg1 arg2 ..."
|
||||||
|
// - runs=[ "entrypoint", "arg1", "arg2", ... ]
|
||||||
|
type Command interface {
|
||||||
|
isCommand()
|
||||||
|
Split() []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringCommand represents the string based form of the "runs" or "args"
|
||||||
|
// attribute.
|
||||||
|
// - runs="entrypoint arg1 arg2 ..."
|
||||||
|
type StringCommand struct {
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListCommand represents the list based form of the "runs" or "args" attribute.
|
||||||
|
// - runs=[ "entrypoint", "arg1", "arg2", ... ]
|
||||||
|
type ListCommand struct {
|
||||||
|
Values []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StringCommand) isCommand() {}
|
||||||
|
func (l *ListCommand) isCommand() {}
|
||||||
|
|
||||||
|
func (s *StringCommand) Split() []string {
|
||||||
|
return strings.Fields(s.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ListCommand) Split() []string {
|
||||||
|
return l.Values
|
||||||
|
}
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Configuration is a parsed main.workflow file
|
||||||
|
type Configuration struct {
|
||||||
|
Actions []*Action
|
||||||
|
Workflows []*Workflow
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action represents a single "action" stanza in a .workflow file.
|
||||||
|
type Action struct {
|
||||||
|
Identifier string
|
||||||
|
Uses Uses
|
||||||
|
Runs, Args Command
|
||||||
|
Needs []string
|
||||||
|
Env map[string]string
|
||||||
|
Secrets []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workflow represents a single "workflow" stanza in a .workflow file.
|
||||||
|
type Workflow struct {
|
||||||
|
Identifier string
|
||||||
|
On string
|
||||||
|
Resolves []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAction looks up action by identifier.
|
||||||
|
//
|
||||||
|
// If the action is not found, nil is returned.
|
||||||
|
func (c *Configuration) GetAction(id string) *Action {
|
||||||
|
for _, action := range c.Actions {
|
||||||
|
if action.Identifier == id {
|
||||||
|
return action
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWorkflow looks up a workflow by identifier.
|
||||||
|
//
|
||||||
|
// If the workflow is not found, nil is returned.
|
||||||
|
func (c *Configuration) GetWorkflow(id string) *Workflow {
|
||||||
|
for _, workflow := range c.Workflows {
|
||||||
|
if workflow.Identifier == id {
|
||||||
|
return workflow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWorkflows gets all Workflow structures that match a given type of event.
|
||||||
|
// e.g., GetWorkflows("push")
|
||||||
|
func (c *Configuration) GetWorkflows(eventType string) []*Workflow {
|
||||||
|
var ret []*Workflow
|
||||||
|
for _, workflow := range c.Workflows {
|
||||||
|
if strings.EqualFold(workflow.On, eventType) {
|
||||||
|
ret = append(ret, workflow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Uses interface {
|
||||||
|
fmt.Stringer
|
||||||
|
isUses()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UsesDockerImage represents `uses = "docker://<image>"`
|
||||||
|
type UsesDockerImage struct {
|
||||||
|
Image string
|
||||||
|
}
|
||||||
|
|
||||||
|
// UsesRepository represents `uses = "<owner>/<repo>[/<path>]@<ref>"`
|
||||||
|
type UsesRepository struct {
|
||||||
|
Repository string
|
||||||
|
Path string
|
||||||
|
Ref string
|
||||||
|
}
|
||||||
|
|
||||||
|
// UsesPath represents `uses = "./<path>"`
|
||||||
|
type UsesPath struct {
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
// UsesInvalid represents any invalid `uses = "<raw>"` value
|
||||||
|
type UsesInvalid struct {
|
||||||
|
Raw string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsesDockerImage) isUses() {}
|
||||||
|
func (u *UsesRepository) isUses() {}
|
||||||
|
func (u *UsesPath) isUses() {}
|
||||||
|
func (u *UsesInvalid) isUses() {}
|
||||||
|
|
||||||
|
func (u *UsesDockerImage) String() string {
|
||||||
|
return fmt.Sprintf("docker://%s", u.Image)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsesRepository) String() string {
|
||||||
|
if u.Path == "" {
|
||||||
|
return fmt.Sprintf("%s@%s", u.Repository, u.Ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s/%s@%s", u.Repository, u.Path, u.Ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsesPath) String() string {
|
||||||
|
return fmt.Sprintf("./%s", u.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UsesInvalid) String() string {
|
||||||
|
return u.Raw
|
||||||
|
}
|
||||||
+136
@@ -0,0 +1,136 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/actions/workflow-parser/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Error struct {
|
||||||
|
message string
|
||||||
|
Errors []*ParseError
|
||||||
|
Actions []*model.Action
|
||||||
|
Workflows []*model.Workflow
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Error) Error() string {
|
||||||
|
buffer := bytes.NewBuffer(nil)
|
||||||
|
buffer.WriteString(e.message)
|
||||||
|
for _, pe := range e.Errors {
|
||||||
|
buffer.WriteString("\n ")
|
||||||
|
buffer.WriteString(pe.Error())
|
||||||
|
}
|
||||||
|
return buffer.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FirstError searches a Configuration for the first error at or above a
|
||||||
|
// given severity level. Checking the return value against nil is a good
|
||||||
|
// way to see if the file has any errors at or above the given severity.
|
||||||
|
// A caller intending to execute the file might check for
|
||||||
|
// `errors.FirstError(parser.WARNING)`, while a caller intending to
|
||||||
|
// display the file might check for `errors.FirstError(parser.FATAL)`.
|
||||||
|
func (e *Error) FirstError(severity Severity) error {
|
||||||
|
for _, pe := range e.Errors {
|
||||||
|
if pe.Severity >= severity {
|
||||||
|
return pe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseError represents an error identified by the parser, either syntactic
|
||||||
|
// (HCL) or semantic (.workflow) in nature. There are fields for location
|
||||||
|
// (File, Line, Column), severity, and base error string. The `Error()`
|
||||||
|
// function on this type concatenates whatever bits of the location are
|
||||||
|
// available with the message. The severity is only used for filtering.
|
||||||
|
type ParseError struct {
|
||||||
|
message string
|
||||||
|
Pos ErrorPos
|
||||||
|
Severity Severity
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorPos represents the location of an error in a user's workflow
|
||||||
|
// file(s).
|
||||||
|
type ErrorPos struct {
|
||||||
|
File string
|
||||||
|
Line int
|
||||||
|
Column int
|
||||||
|
}
|
||||||
|
|
||||||
|
// newFatal creates a new error at the FATAL level, indicating that the
|
||||||
|
// file is so broken it should not be displayed.
|
||||||
|
func newFatal(pos ErrorPos, format string, a ...interface{}) *ParseError {
|
||||||
|
return &ParseError{
|
||||||
|
message: fmt.Sprintf(format, a...),
|
||||||
|
Pos: pos,
|
||||||
|
Severity: FATAL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newError creates a new error at the ERROR level, indicating that the
|
||||||
|
// file can be displayed but cannot be run.
|
||||||
|
func newError(pos ErrorPos, format string, a ...interface{}) *ParseError {
|
||||||
|
return &ParseError{
|
||||||
|
message: fmt.Sprintf(format, a...),
|
||||||
|
Pos: pos,
|
||||||
|
Severity: ERROR,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newWarning creates a new error at the WARNING level, indicating that
|
||||||
|
// the file might be runnable but might not execute as intended.
|
||||||
|
func newWarning(pos ErrorPos, format string, a ...interface{}) *ParseError {
|
||||||
|
return &ParseError{
|
||||||
|
message: fmt.Sprintf(format, a...),
|
||||||
|
Pos: pos,
|
||||||
|
Severity: WARNING,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ParseError) Error() string {
|
||||||
|
var sb strings.Builder
|
||||||
|
if e.Pos.Line != 0 {
|
||||||
|
sb.WriteString("Line ") // nolint: errcheck
|
||||||
|
sb.WriteString(strconv.Itoa(e.Pos.Line)) // nolint: errcheck
|
||||||
|
sb.WriteString(": ") // nolint: errcheck
|
||||||
|
}
|
||||||
|
if sb.Len() > 0 {
|
||||||
|
sb.WriteString(e.message) // nolint: errcheck
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
return e.message
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
_ = iota
|
||||||
|
|
||||||
|
// WARNING indicates a mistake that might affect correctness
|
||||||
|
WARNING
|
||||||
|
|
||||||
|
// ERROR indicates a mistake that prevents execution of any workflows in the file
|
||||||
|
ERROR
|
||||||
|
|
||||||
|
// FATAL indicates a mistake that prevents even drawing the file
|
||||||
|
FATAL
|
||||||
|
)
|
||||||
|
|
||||||
|
// Severity represents the level of an error encountered while parsing a
|
||||||
|
// workflow file. See the comments for WARNING, ERROR, and FATAL, above.
|
||||||
|
type Severity int
|
||||||
|
|
||||||
|
type errorList []*ParseError
|
||||||
|
|
||||||
|
func (a errorList) Len() int { return len(a) }
|
||||||
|
func (a errorList) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||||
|
func (a errorList) Less(i, j int) bool { return a[i].Pos.Line < a[j].Pos.Line }
|
||||||
|
|
||||||
|
// sortErrors sorts the errors reported by the parser. Do this after
|
||||||
|
// parsing is complete. The sort is stable, so order is preserved within
|
||||||
|
// a single line: left to right, syntax errors before validation errors.
|
||||||
|
func (errors errorList) sort() {
|
||||||
|
sort.Stable(errors)
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// isAllowedEventType returns true if the event type is supported.
|
||||||
|
func isAllowedEventType(eventType string) bool {
|
||||||
|
_, ok := eventTypeWhitelist[strings.ToLower(eventType)]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://developer.github.com/actions/creating-workflows/workflow-configuration-options/#events-supported-in-workflow-files
|
||||||
|
var eventTypeWhitelist = map[string]struct{}{
|
||||||
|
"check_run": {},
|
||||||
|
"check_suite": {},
|
||||||
|
"commit_comment": {},
|
||||||
|
"create": {},
|
||||||
|
"delete": {},
|
||||||
|
"deployment": {},
|
||||||
|
"deployment_status": {},
|
||||||
|
"fork": {},
|
||||||
|
"gollum": {},
|
||||||
|
"issue_comment": {},
|
||||||
|
"issues": {},
|
||||||
|
"label": {},
|
||||||
|
"member": {},
|
||||||
|
"milestone": {},
|
||||||
|
"page_build": {},
|
||||||
|
"project_card": {},
|
||||||
|
"project_column": {},
|
||||||
|
"project": {},
|
||||||
|
"public": {},
|
||||||
|
"pull_request_review_comment": {},
|
||||||
|
"pull_request_review": {},
|
||||||
|
"pull_request": {},
|
||||||
|
"push": {},
|
||||||
|
"release": {},
|
||||||
|
"repository_dispatch": {},
|
||||||
|
"status": {},
|
||||||
|
"watch": {},
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
type OptionFunc func(*Parser)
|
||||||
|
|
||||||
|
func WithSuppressWarnings() OptionFunc {
|
||||||
|
return func(ps *Parser) {
|
||||||
|
ps.suppressSeverity = WARNING
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithSuppressErrors() OptionFunc {
|
||||||
|
return func(ps *Parser) {
|
||||||
|
ps.suppressSeverity = ERROR
|
||||||
|
}
|
||||||
|
}
|
||||||
+807
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -11,7 +11,7 @@ type EnumerableWithIndex interface {
|
|||||||
|
|
||||||
// Map invokes the given function once for each element and returns a
|
// Map invokes the given function once for each element and returns a
|
||||||
// container containing the values returned by the given function.
|
// container containing the values returned by the given function.
|
||||||
// TODO need help on how to enforce this in containers (don't want to type assert when chaining)
|
// TODO would appreciate help on how to enforce this in containers (don't want to type assert when chaining)
|
||||||
// Map(func(index int, value interface{}) interface{}) Container
|
// Map(func(index int, value interface{}) interface{}) Container
|
||||||
|
|
||||||
// Select returns a new container containing all elements for which the given function returns a true value.
|
// Select returns a new container containing all elements for which the given function returns a true value.
|
||||||
|
|||||||
+40
-12
@@ -11,9 +11,10 @@ package arraylist
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/emirpasic/gods/lists"
|
"github.com/emirpasic/gods/lists"
|
||||||
"github.com/emirpasic/gods/utils"
|
"github.com/emirpasic/gods/utils"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func assertListImplementation() {
|
func assertListImplementation() {
|
||||||
@@ -31,9 +32,13 @@ const (
|
|||||||
shrinkFactor = float32(0.25) // shrink when size is 25% of capacity (0 means never shrink)
|
shrinkFactor = float32(0.25) // shrink when size is 25% of capacity (0 means never shrink)
|
||||||
)
|
)
|
||||||
|
|
||||||
// New instantiates a new empty list
|
// New instantiates a new list and adds the passed values, if any, to the list
|
||||||
func New() *List {
|
func New(values ...interface{}) *List {
|
||||||
return &List{}
|
list := &List{}
|
||||||
|
if len(values) > 0 {
|
||||||
|
list.Add(values...)
|
||||||
|
}
|
||||||
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add appends a value at the end of the list
|
// Add appends a value at the end of the list
|
||||||
@@ -56,7 +61,7 @@ func (list *List) Get(index int) (interface{}, bool) {
|
|||||||
return list.elements[index], true
|
return list.elements[index], true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove removes one or more elements from the list with the supplied indices.
|
// Remove removes the element at the given index from the list.
|
||||||
func (list *List) Remove(index int) {
|
func (list *List) Remove(index int) {
|
||||||
|
|
||||||
if !list.withinRange(index) {
|
if !list.withinRange(index) {
|
||||||
@@ -98,6 +103,19 @@ func (list *List) Values() []interface{} {
|
|||||||
return newElements
|
return newElements
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//IndexOf returns index of provided element
|
||||||
|
func (list *List) IndexOf(value interface{}) int {
|
||||||
|
if list.size == 0 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
for index, element := range list.elements {
|
||||||
|
if element == value {
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
// Empty returns true if list does not contain any elements.
|
// Empty returns true if list does not contain any elements.
|
||||||
func (list *List) Empty() bool {
|
func (list *List) Empty() bool {
|
||||||
return list.size == 0
|
return list.size == 0
|
||||||
@@ -145,14 +163,24 @@ func (list *List) Insert(index int, values ...interface{}) {
|
|||||||
l := len(values)
|
l := len(values)
|
||||||
list.growBy(l)
|
list.growBy(l)
|
||||||
list.size += l
|
list.size += l
|
||||||
// Shift old to right
|
copy(list.elements[index+l:], list.elements[index:list.size-l])
|
||||||
for i := list.size - 1; i >= index+l; i-- {
|
copy(list.elements[index:], values)
|
||||||
list.elements[i] = list.elements[i-l]
|
}
|
||||||
}
|
|
||||||
// Insert new
|
// Set the value at specified index
|
||||||
for i, value := range values {
|
// Does not do anything if position is negative or bigger than list's size
|
||||||
list.elements[index+i] = value
|
// Note: position equal to list's size is valid, i.e. append.
|
||||||
|
func (list *List) Set(index int, value interface{}) {
|
||||||
|
|
||||||
|
if !list.withinRange(index) {
|
||||||
|
// Append
|
||||||
|
if index == list.size {
|
||||||
|
list.Add(value)
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
list.elements[index] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns a string representation of container
|
// String returns a string representation of container
|
||||||
|
|||||||
+1
@@ -23,6 +23,7 @@ type List interface {
|
|||||||
Sort(comparator utils.Comparator)
|
Sort(comparator utils.Comparator)
|
||||||
Swap(index1, index2 int)
|
Swap(index1, index2 int)
|
||||||
Insert(index int, values ...interface{})
|
Insert(index int, values ...interface{})
|
||||||
|
Set(index int, value interface{})
|
||||||
|
|
||||||
containers.Container
|
containers.Container
|
||||||
// Empty() bool
|
// Empty() bool
|
||||||
|
|||||||
+2
-2
@@ -11,12 +11,12 @@ func assertSerializationImplementation() {
|
|||||||
var _ containers.JSONDeserializer = (*Heap)(nil)
|
var _ containers.JSONDeserializer = (*Heap)(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToJSON outputs the JSON representation of list's elements.
|
// ToJSON outputs the JSON representation of the heap.
|
||||||
func (heap *Heap) ToJSON() ([]byte, error) {
|
func (heap *Heap) ToJSON() ([]byte, error) {
|
||||||
return heap.list.ToJSON()
|
return heap.list.ToJSON()
|
||||||
}
|
}
|
||||||
|
|
||||||
// FromJSON populates list's elements from the input JSON representation.
|
// FromJSON populates the heap from the input JSON representation.
|
||||||
func (heap *Heap) FromJSON(data []byte) error {
|
func (heap *Heap) FromJSON(data []byte) error {
|
||||||
return heap.list.FromJSON(data)
|
return heap.list.FromJSON(data)
|
||||||
}
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
Copyright (c) 2017 The Go Authors. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above
|
||||||
|
copyright notice, this list of conditions and the following disclaimer
|
||||||
|
in the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
* Neither the name of Google Inc. nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
+553
File diff suppressed because it is too large.
Load diff
+17
@@ -0,0 +1,17 @@
|
|||||||
|
// 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.md file.
|
||||||
|
|
||||||
|
// +build !debug
|
||||||
|
|
||||||
|
package diff
|
||||||
|
|
||||||
|
var debug debugger
|
||||||
|
|
||||||
|
type debugger struct{}
|
||||||
|
|
||||||
|
func (debugger) Begin(_, _ int, f EqualFunc, _, _ *EditScript) EqualFunc {
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
func (debugger) Update() {}
|
||||||
|
func (debugger) Finish() {}
|
||||||
+122
@@ -0,0 +1,122 @@
|
|||||||
|
// 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.md file.
|
||||||
|
|
||||||
|
// +build debug
|
||||||
|
|
||||||
|
package diff
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The algorithm can be seen running in real-time by enabling debugging:
|
||||||
|
// go test -tags=debug -v
|
||||||
|
//
|
||||||
|
// Example output:
|
||||||
|
// === RUN TestDifference/#34
|
||||||
|
// ┌───────────────────────────────┐
|
||||||
|
// │ \ · · · · · · · · · · · · · · │
|
||||||
|
// │ · # · · · · · · · · · · · · · │
|
||||||
|
// │ · \ · · · · · · · · · · · · · │
|
||||||
|
// │ · · \ · · · · · · · · · · · · │
|
||||||
|
// │ · · · X # · · · · · · · · · · │
|
||||||
|
// │ · · · # \ · · · · · · · · · · │
|
||||||
|
// │ · · · · · # # · · · · · · · · │
|
||||||
|
// │ · · · · · # \ · · · · · · · · │
|
||||||
|
// │ · · · · · · · \ · · · · · · · │
|
||||||
|
// │ · · · · · · · · \ · · · · · · │
|
||||||
|
// │ · · · · · · · · · \ · · · · · │
|
||||||
|
// │ · · · · · · · · · · \ · · # · │
|
||||||
|
// │ · · · · · · · · · · · \ # # · │
|
||||||
|
// │ · · · · · · · · · · · # # # · │
|
||||||
|
// │ · · · · · · · · · · # # # # · │
|
||||||
|
// │ · · · · · · · · · # # # # # · │
|
||||||
|
// │ · · · · · · · · · · · · · · \ │
|
||||||
|
// └───────────────────────────────┘
|
||||||
|
// [.Y..M.XY......YXYXY.|]
|
||||||
|
//
|
||||||
|
// The grid represents the edit-graph where the horizontal axis represents
|
||||||
|
// list X and the vertical axis represents list Y. The start of the two lists
|
||||||
|
// is the top-left, while the ends are the bottom-right. The '·' represents
|
||||||
|
// an unexplored node in the graph. The '\' indicates that the two symbols
|
||||||
|
// from list X and Y are equal. The 'X' indicates that two symbols are similar
|
||||||
|
// (but not exactly equal) to each other. The '#' indicates that the two symbols
|
||||||
|
// are different (and not similar). The algorithm traverses this graph trying to
|
||||||
|
// make the paths starting in the top-left and the bottom-right connect.
|
||||||
|
//
|
||||||
|
// The series of '.', 'X', 'Y', and 'M' characters at the bottom represents
|
||||||
|
// the currently established path from the forward and reverse searches,
|
||||||
|
// separated by a '|' character.
|
||||||
|
|
||||||
|
const (
|
||||||
|
updateDelay = 100 * time.Millisecond
|
||||||
|
finishDelay = 500 * time.Millisecond
|
||||||
|
ansiTerminal = true // ANSI escape codes used to move terminal cursor
|
||||||
|
)
|
||||||
|
|
||||||
|
var debug debugger
|
||||||
|
|
||||||
|
type debugger struct {
|
||||||
|
sync.Mutex
|
||||||
|
p1, p2 EditScript
|
||||||
|
fwdPath, revPath *EditScript
|
||||||
|
grid []byte
|
||||||
|
lines int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dbg *debugger) Begin(nx, ny int, f EqualFunc, p1, p2 *EditScript) EqualFunc {
|
||||||
|
dbg.Lock()
|
||||||
|
dbg.fwdPath, dbg.revPath = p1, p2
|
||||||
|
top := "┌─" + strings.Repeat("──", nx) + "┐\n"
|
||||||
|
row := "│ " + strings.Repeat("· ", nx) + "│\n"
|
||||||
|
btm := "└─" + strings.Repeat("──", nx) + "┘\n"
|
||||||
|
dbg.grid = []byte(top + strings.Repeat(row, ny) + btm)
|
||||||
|
dbg.lines = strings.Count(dbg.String(), "\n")
|
||||||
|
fmt.Print(dbg)
|
||||||
|
|
||||||
|
// Wrap the EqualFunc so that we can intercept each result.
|
||||||
|
return func(ix, iy int) (r Result) {
|
||||||
|
cell := dbg.grid[len(top)+iy*len(row):][len("│ ")+len("· ")*ix:][:len("·")]
|
||||||
|
for i := range cell {
|
||||||
|
cell[i] = 0 // Zero out the multiple bytes of UTF-8 middle-dot
|
||||||
|
}
|
||||||
|
switch r = f(ix, iy); {
|
||||||
|
case r.Equal():
|
||||||
|
cell[0] = '\\'
|
||||||
|
case r.Similar():
|
||||||
|
cell[0] = 'X'
|
||||||
|
default:
|
||||||
|
cell[0] = '#'
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dbg *debugger) Update() {
|
||||||
|
dbg.print(updateDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dbg *debugger) Finish() {
|
||||||
|
dbg.print(finishDelay)
|
||||||
|
dbg.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dbg *debugger) String() string {
|
||||||
|
dbg.p1, dbg.p2 = *dbg.fwdPath, dbg.p2[:0]
|
||||||
|
for i := len(*dbg.revPath) - 1; i >= 0; i-- {
|
||||||
|
dbg.p2 = append(dbg.p2, (*dbg.revPath)[i])
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s[%v|%v]\n\n", dbg.grid, dbg.p1, dbg.p2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dbg *debugger) print(d time.Duration) {
|
||||||
|
if ansiTerminal {
|
||||||
|
fmt.Printf("\x1b[%dA", dbg.lines) // Reset terminal cursor
|
||||||
|
}
|
||||||
|
fmt.Print(dbg)
|
||||||
|
time.Sleep(d)
|
||||||
|
}
|
||||||
+363
@@ -0,0 +1,363 @@
|
|||||||
|
// 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.md file.
|
||||||
|
|
||||||
|
// Package diff implements an algorithm for producing edit-scripts.
|
||||||
|
// The edit-script is a sequence of operations needed to transform one list
|
||||||
|
// of symbols into another (or vice-versa). The edits allowed are insertions,
|
||||||
|
// deletions, and modifications. The summation of all edits is called the
|
||||||
|
// Levenshtein distance as this problem is well-known in computer science.
|
||||||
|
//
|
||||||
|
// This package prioritizes performance over accuracy. That is, the run time
|
||||||
|
// is more important than obtaining a minimal Levenshtein distance.
|
||||||
|
package diff
|
||||||
|
|
||||||
|
// EditType represents a single operation within an edit-script.
|
||||||
|
type EditType uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Identity indicates that a symbol pair is identical in both list X and Y.
|
||||||
|
Identity EditType = iota
|
||||||
|
// UniqueX indicates that a symbol only exists in X and not Y.
|
||||||
|
UniqueX
|
||||||
|
// UniqueY indicates that a symbol only exists in Y and not X.
|
||||||
|
UniqueY
|
||||||
|
// Modified indicates that a symbol pair is a modification of each other.
|
||||||
|
Modified
|
||||||
|
)
|
||||||
|
|
||||||
|
// EditScript represents the series of differences between two lists.
|
||||||
|
type EditScript []EditType
|
||||||
|
|
||||||
|
// String returns a human-readable string representing the edit-script where
|
||||||
|
// Identity, UniqueX, UniqueY, and Modified are represented by the
|
||||||
|
// '.', 'X', 'Y', and 'M' characters, respectively.
|
||||||
|
func (es EditScript) String() string {
|
||||||
|
b := make([]byte, len(es))
|
||||||
|
for i, e := range es {
|
||||||
|
switch e {
|
||||||
|
case Identity:
|
||||||
|
b[i] = '.'
|
||||||
|
case UniqueX:
|
||||||
|
b[i] = 'X'
|
||||||
|
case UniqueY:
|
||||||
|
b[i] = 'Y'
|
||||||
|
case Modified:
|
||||||
|
b[i] = 'M'
|
||||||
|
default:
|
||||||
|
panic("invalid edit-type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// stats returns a histogram of the number of each type of edit operation.
|
||||||
|
func (es EditScript) stats() (s struct{ NI, NX, NY, NM int }) {
|
||||||
|
for _, e := range es {
|
||||||
|
switch e {
|
||||||
|
case Identity:
|
||||||
|
s.NI++
|
||||||
|
case UniqueX:
|
||||||
|
s.NX++
|
||||||
|
case UniqueY:
|
||||||
|
s.NY++
|
||||||
|
case Modified:
|
||||||
|
s.NM++
|
||||||
|
default:
|
||||||
|
panic("invalid edit-type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dist is the Levenshtein distance and is guaranteed to be 0 if and only if
|
||||||
|
// lists X and Y are equal.
|
||||||
|
func (es EditScript) Dist() int { return len(es) - es.stats().NI }
|
||||||
|
|
||||||
|
// LenX is the length of the X list.
|
||||||
|
func (es EditScript) LenX() int { return len(es) - es.stats().NY }
|
||||||
|
|
||||||
|
// LenY is the length of the Y list.
|
||||||
|
func (es EditScript) LenY() int { return len(es) - es.stats().NX }
|
||||||
|
|
||||||
|
// EqualFunc reports whether the symbols at indexes ix and iy are equal.
|
||||||
|
// When called by Difference, the index is guaranteed to be within nx and ny.
|
||||||
|
type EqualFunc func(ix int, iy int) Result
|
||||||
|
|
||||||
|
// Result is the result of comparison.
|
||||||
|
// NSame is the number of sub-elements that are equal.
|
||||||
|
// NDiff is the number of sub-elements that are not equal.
|
||||||
|
type Result struct{ NSame, NDiff int }
|
||||||
|
|
||||||
|
// Equal indicates whether the symbols are equal. Two symbols are equal
|
||||||
|
// if and only if NDiff == 0. If Equal, then they are also Similar.
|
||||||
|
func (r Result) Equal() bool { return r.NDiff == 0 }
|
||||||
|
|
||||||
|
// Similar indicates whether two symbols are similar and may be represented
|
||||||
|
// by using the Modified type. As a special case, we consider binary comparisons
|
||||||
|
// (i.e., those that return Result{1, 0} or Result{0, 1}) to be similar.
|
||||||
|
//
|
||||||
|
// The exact ratio of NSame to NDiff to determine similarity may change.
|
||||||
|
func (r Result) Similar() bool {
|
||||||
|
// Use NSame+1 to offset NSame so that binary comparisons are similar.
|
||||||
|
return r.NSame+1 >= r.NDiff
|
||||||
|
}
|
||||||
|
|
||||||
|
// Difference reports whether two lists of lengths nx and ny are equal
|
||||||
|
// given the definition of equality provided as f.
|
||||||
|
//
|
||||||
|
// This function returns an edit-script, which is a sequence of operations
|
||||||
|
// needed to convert one list into the other. The following invariants for
|
||||||
|
// the edit-script are maintained:
|
||||||
|
// • eq == (es.Dist()==0)
|
||||||
|
// • nx == es.LenX()
|
||||||
|
// • ny == es.LenY()
|
||||||
|
//
|
||||||
|
// This algorithm is not guaranteed to be an optimal solution (i.e., one that
|
||||||
|
// produces an edit-script with a minimal Levenshtein distance). This algorithm
|
||||||
|
// favors performance over optimality. The exact output is not guaranteed to
|
||||||
|
// be stable and may change over time.
|
||||||
|
func Difference(nx, ny int, f EqualFunc) (es EditScript) {
|
||||||
|
// This algorithm is based on traversing what is known as an "edit-graph".
|
||||||
|
// See Figure 1 from "An O(ND) Difference Algorithm and Its Variations"
|
||||||
|
// by Eugene W. Myers. Since D can be as large as N itself, this is
|
||||||
|
// effectively O(N^2). Unlike the algorithm from that paper, we are not
|
||||||
|
// interested in the optimal path, but at least some "decent" path.
|
||||||
|
//
|
||||||
|
// For example, let X and Y be lists of symbols:
|
||||||
|
// X = [A B C A B B A]
|
||||||
|
// Y = [C B A B A C]
|
||||||
|
//
|
||||||
|
// The edit-graph can be drawn as the following:
|
||||||
|
// A B C A B B A
|
||||||
|
// ┌─────────────┐
|
||||||
|
// C │_|_|\|_|_|_|_│ 0
|
||||||
|
// B │_|\|_|_|\|\|_│ 1
|
||||||
|
// A │\|_|_|\|_|_|\│ 2
|
||||||
|
// B │_|\|_|_|\|\|_│ 3
|
||||||
|
// A │\|_|_|\|_|_|\│ 4
|
||||||
|
// C │ | |\| | | | │ 5
|
||||||
|
// └─────────────┘ 6
|
||||||
|
// 0 1 2 3 4 5 6 7
|
||||||
|
//
|
||||||
|
// List X is written along the horizontal axis, while list Y is written
|
||||||
|
// along the vertical axis. At any point on this grid, if the symbol in
|
||||||
|
// list X matches the corresponding symbol in list Y, then a '\' is drawn.
|
||||||
|
// The goal of any minimal edit-script algorithm is to find a path from the
|
||||||
|
// top-left corner to the bottom-right corner, while traveling through the
|
||||||
|
// fewest horizontal or vertical edges.
|
||||||
|
// A horizontal edge is equivalent to inserting a symbol from list X.
|
||||||
|
// A vertical edge is equivalent to inserting a symbol from list Y.
|
||||||
|
// A diagonal edge is equivalent to a matching symbol between both X and Y.
|
||||||
|
|
||||||
|
// Invariants:
|
||||||
|
// • 0 ≤ fwdPath.X ≤ (fwdFrontier.X, revFrontier.X) ≤ revPath.X ≤ nx
|
||||||
|
// • 0 ≤ fwdPath.Y ≤ (fwdFrontier.Y, revFrontier.Y) ≤ revPath.Y ≤ ny
|
||||||
|
//
|
||||||
|
// In general:
|
||||||
|
// • fwdFrontier.X < revFrontier.X
|
||||||
|
// • fwdFrontier.Y < revFrontier.Y
|
||||||
|
// Unless, it is time for the algorithm to terminate.
|
||||||
|
fwdPath := path{+1, point{0, 0}, make(EditScript, 0, (nx+ny)/2)}
|
||||||
|
revPath := path{-1, point{nx, ny}, make(EditScript, 0)}
|
||||||
|
fwdFrontier := fwdPath.point // Forward search frontier
|
||||||
|
revFrontier := revPath.point // Reverse search frontier
|
||||||
|
|
||||||
|
// Search budget bounds the cost of searching for better paths.
|
||||||
|
// The longest sequence of non-matching symbols that can be tolerated is
|
||||||
|
// approximately the square-root of the search budget.
|
||||||
|
searchBudget := 4 * (nx + ny) // O(n)
|
||||||
|
|
||||||
|
// The algorithm below is a greedy, meet-in-the-middle algorithm for
|
||||||
|
// computing sub-optimal edit-scripts between two lists.
|
||||||
|
//
|
||||||
|
// The algorithm is approximately as follows:
|
||||||
|
// • Searching for differences switches back-and-forth between
|
||||||
|
// a search that starts at the beginning (the top-left corner), and
|
||||||
|
// a search that starts at the end (the bottom-right corner). The goal of
|
||||||
|
// the search is connect with the search from the opposite corner.
|
||||||
|
// • As we search, we build a path in a greedy manner, where the first
|
||||||
|
// match seen is added to the path (this is sub-optimal, but provides a
|
||||||
|
// decent result in practice). When matches are found, we try the next pair
|
||||||
|
// of symbols in the lists and follow all matches as far as possible.
|
||||||
|
// • When searching for matches, we search along a diagonal going through
|
||||||
|
// through the "frontier" point. If no matches are found, we advance the
|
||||||
|
// frontier towards the opposite corner.
|
||||||
|
// • This algorithm terminates when either the X coordinates or the
|
||||||
|
// Y coordinates of the forward and reverse frontier points ever intersect.
|
||||||
|
//
|
||||||
|
// This algorithm is correct even if searching only in the forward direction
|
||||||
|
// or in the reverse direction. We do both because it is commonly observed
|
||||||
|
// that two lists commonly differ because elements were added to the front
|
||||||
|
// or end of the other list.
|
||||||
|
//
|
||||||
|
// Running the tests with the "debug" build tag prints a visualization of
|
||||||
|
// the algorithm running in real-time. This is educational for understanding
|
||||||
|
// how the algorithm works. See debug_enable.go.
|
||||||
|
f = debug.Begin(nx, ny, f, &fwdPath.es, &revPath.es)
|
||||||
|
for {
|
||||||
|
// Forward search from the beginning.
|
||||||
|
if fwdFrontier.X >= revFrontier.X || fwdFrontier.Y >= revFrontier.Y || searchBudget == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
for stop1, stop2, i := false, false, 0; !(stop1 && stop2) && searchBudget > 0; i++ {
|
||||||
|
// Search in a diagonal pattern for a match.
|
||||||
|
z := zigzag(i)
|
||||||
|
p := point{fwdFrontier.X + z, fwdFrontier.Y - z}
|
||||||
|
switch {
|
||||||
|
case p.X >= revPath.X || p.Y < fwdPath.Y:
|
||||||
|
stop1 = true // Hit top-right corner
|
||||||
|
case p.Y >= revPath.Y || p.X < fwdPath.X:
|
||||||
|
stop2 = true // Hit bottom-left corner
|
||||||
|
case f(p.X, p.Y).Equal():
|
||||||
|
// Match found, so connect the path to this point.
|
||||||
|
fwdPath.connect(p, f)
|
||||||
|
fwdPath.append(Identity)
|
||||||
|
// Follow sequence of matches as far as possible.
|
||||||
|
for fwdPath.X < revPath.X && fwdPath.Y < revPath.Y {
|
||||||
|
if !f(fwdPath.X, fwdPath.Y).Equal() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fwdPath.append(Identity)
|
||||||
|
}
|
||||||
|
fwdFrontier = fwdPath.point
|
||||||
|
stop1, stop2 = true, true
|
||||||
|
default:
|
||||||
|
searchBudget-- // Match not found
|
||||||
|
}
|
||||||
|
debug.Update()
|
||||||
|
}
|
||||||
|
// Advance the frontier towards reverse point.
|
||||||
|
if revPath.X-fwdFrontier.X >= revPath.Y-fwdFrontier.Y {
|
||||||
|
fwdFrontier.X++
|
||||||
|
} else {
|
||||||
|
fwdFrontier.Y++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reverse search from the end.
|
||||||
|
if fwdFrontier.X >= revFrontier.X || fwdFrontier.Y >= revFrontier.Y || searchBudget == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
for stop1, stop2, i := false, false, 0; !(stop1 && stop2) && searchBudget > 0; i++ {
|
||||||
|
// Search in a diagonal pattern for a match.
|
||||||
|
z := zigzag(i)
|
||||||
|
p := point{revFrontier.X - z, revFrontier.Y + z}
|
||||||
|
switch {
|
||||||
|
case fwdPath.X >= p.X || revPath.Y < p.Y:
|
||||||
|
stop1 = true // Hit bottom-left corner
|
||||||
|
case fwdPath.Y >= p.Y || revPath.X < p.X:
|
||||||
|
stop2 = true // Hit top-right corner
|
||||||
|
case f(p.X-1, p.Y-1).Equal():
|
||||||
|
// Match found, so connect the path to this point.
|
||||||
|
revPath.connect(p, f)
|
||||||
|
revPath.append(Identity)
|
||||||
|
// Follow sequence of matches as far as possible.
|
||||||
|
for fwdPath.X < revPath.X && fwdPath.Y < revPath.Y {
|
||||||
|
if !f(revPath.X-1, revPath.Y-1).Equal() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
revPath.append(Identity)
|
||||||
|
}
|
||||||
|
revFrontier = revPath.point
|
||||||
|
stop1, stop2 = true, true
|
||||||
|
default:
|
||||||
|
searchBudget-- // Match not found
|
||||||
|
}
|
||||||
|
debug.Update()
|
||||||
|
}
|
||||||
|
// Advance the frontier towards forward point.
|
||||||
|
if revFrontier.X-fwdPath.X >= revFrontier.Y-fwdPath.Y {
|
||||||
|
revFrontier.X--
|
||||||
|
} else {
|
||||||
|
revFrontier.Y--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join the forward and reverse paths and then append the reverse path.
|
||||||
|
fwdPath.connect(revPath.point, f)
|
||||||
|
for i := len(revPath.es) - 1; i >= 0; i-- {
|
||||||
|
t := revPath.es[i]
|
||||||
|
revPath.es = revPath.es[:i]
|
||||||
|
fwdPath.append(t)
|
||||||
|
}
|
||||||
|
debug.Finish()
|
||||||
|
return fwdPath.es
|
||||||
|
}
|
||||||
|
|
||||||
|
type path struct {
|
||||||
|
dir int // +1 if forward, -1 if reverse
|
||||||
|
point // Leading point of the EditScript path
|
||||||
|
es EditScript
|
||||||
|
}
|
||||||
|
|
||||||
|
// connect appends any necessary Identity, Modified, UniqueX, or UniqueY types
|
||||||
|
// to the edit-script to connect p.point to dst.
|
||||||
|
func (p *path) connect(dst point, f EqualFunc) {
|
||||||
|
if p.dir > 0 {
|
||||||
|
// Connect in forward direction.
|
||||||
|
for dst.X > p.X && dst.Y > p.Y {
|
||||||
|
switch r := f(p.X, p.Y); {
|
||||||
|
case r.Equal():
|
||||||
|
p.append(Identity)
|
||||||
|
case r.Similar():
|
||||||
|
p.append(Modified)
|
||||||
|
case dst.X-p.X >= dst.Y-p.Y:
|
||||||
|
p.append(UniqueX)
|
||||||
|
default:
|
||||||
|
p.append(UniqueY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for dst.X > p.X {
|
||||||
|
p.append(UniqueX)
|
||||||
|
}
|
||||||
|
for dst.Y > p.Y {
|
||||||
|
p.append(UniqueY)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Connect in reverse direction.
|
||||||
|
for p.X > dst.X && p.Y > dst.Y {
|
||||||
|
switch r := f(p.X-1, p.Y-1); {
|
||||||
|
case r.Equal():
|
||||||
|
p.append(Identity)
|
||||||
|
case r.Similar():
|
||||||
|
p.append(Modified)
|
||||||
|
case p.Y-dst.Y >= p.X-dst.X:
|
||||||
|
p.append(UniqueY)
|
||||||
|
default:
|
||||||
|
p.append(UniqueX)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for p.X > dst.X {
|
||||||
|
p.append(UniqueX)
|
||||||
|
}
|
||||||
|
for p.Y > dst.Y {
|
||||||
|
p.append(UniqueY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *path) append(t EditType) {
|
||||||
|
p.es = append(p.es, t)
|
||||||
|
switch t {
|
||||||
|
case Identity, Modified:
|
||||||
|
p.add(p.dir, p.dir)
|
||||||
|
case UniqueX:
|
||||||
|
p.add(p.dir, 0)
|
||||||
|
case UniqueY:
|
||||||
|
p.add(0, p.dir)
|
||||||
|
}
|
||||||
|
debug.Update()
|
||||||
|
}
|
||||||
|
|
||||||
|
type point struct{ X, Y int }
|
||||||
|
|
||||||
|
func (p *point) add(dx, dy int) { p.X += dx; p.Y += dy }
|
||||||
|
|
||||||
|
// zigzag maps a consecutive sequence of integers to a zig-zag sequence.
|
||||||
|
// [0 1 2 3 4 5 ...] => [0 -1 +1 -2 +2 ...]
|
||||||
|
func zigzag(x int) int {
|
||||||
|
if x&1 != 0 {
|
||||||
|
x = ^x
|
||||||
|
}
|
||||||
|
return x >> 1
|
||||||
|
}
|
||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
// 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.md file.
|
||||||
|
|
||||||
|
// Package function identifies function types.
|
||||||
|
package function
|
||||||
|
|
||||||
|
import "reflect"
|
||||||
|
|
||||||
|
type funcType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
_ funcType = iota
|
||||||
|
|
||||||
|
ttbFunc // func(T, T) bool
|
||||||
|
tibFunc // func(T, I) bool
|
||||||
|
trFunc // func(T) R
|
||||||
|
|
||||||
|
Equal = ttbFunc // func(T, T) bool
|
||||||
|
EqualAssignable = tibFunc // func(T, I) bool; encapsulates func(T, T) bool
|
||||||
|
Transformer = trFunc // func(T) R
|
||||||
|
ValueFilter = ttbFunc // func(T, T) bool
|
||||||
|
Less = ttbFunc // func(T, T) bool
|
||||||
|
)
|
||||||
|
|
||||||
|
var boolType = reflect.TypeOf(true)
|
||||||
|
|
||||||
|
// IsType reports whether the reflect.Type is of the specified function type.
|
||||||
|
func IsType(t reflect.Type, ft funcType) bool {
|
||||||
|
if t == nil || t.Kind() != reflect.Func || t.IsVariadic() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
ni, no := t.NumIn(), t.NumOut()
|
||||||
|
switch ft {
|
||||||
|
case ttbFunc: // func(T, T) bool
|
||||||
|
if ni == 2 && no == 1 && t.In(0) == t.In(1) && t.Out(0) == boolType {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
case tibFunc: // func(T, I) bool
|
||||||
|
if ni == 2 && no == 1 && t.In(0).AssignableTo(t.In(1)) && t.Out(0) == boolType {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
case trFunc: // func(T) R
|
||||||
|
if ni == 1 && no == 1 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
+277
@@ -0,0 +1,277 @@
|
|||||||
|
// 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.md file.
|
||||||
|
|
||||||
|
// Package value provides functionality for reflect.Value types.
|
||||||
|
package value
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
var stringerIface = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
|
||||||
|
|
||||||
|
// Format formats the value v as a string.
|
||||||
|
//
|
||||||
|
// This is similar to fmt.Sprintf("%+v", v) except this:
|
||||||
|
// * Prints the type unless it can be elided
|
||||||
|
// * Avoids printing struct fields that are zero
|
||||||
|
// * Prints a nil-slice as being nil, not empty
|
||||||
|
// * Prints map entries in deterministic order
|
||||||
|
func Format(v reflect.Value, conf FormatConfig) string {
|
||||||
|
conf.printType = true
|
||||||
|
conf.followPointers = true
|
||||||
|
conf.realPointers = true
|
||||||
|
return formatAny(v, conf, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
type FormatConfig struct {
|
||||||
|
UseStringer bool // Should the String method be used if available?
|
||||||
|
printType bool // Should we print the type before the value?
|
||||||
|
PrintPrimitiveType bool // Should we print the type of primitives?
|
||||||
|
followPointers bool // Should we recursively follow pointers?
|
||||||
|
realPointers bool // Should we print the real address of pointers?
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatAny(v reflect.Value, conf FormatConfig, visited map[uintptr]bool) string {
|
||||||
|
// TODO: Should this be a multi-line printout in certain situations?
|
||||||
|
|
||||||
|
if !v.IsValid() {
|
||||||
|
return "<non-existent>"
|
||||||
|
}
|
||||||
|
if conf.UseStringer && v.Type().Implements(stringerIface) && v.CanInterface() {
|
||||||
|
if (v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface) && v.IsNil() {
|
||||||
|
return "<nil>"
|
||||||
|
}
|
||||||
|
|
||||||
|
const stringerPrefix = "s" // Indicates that the String method was used
|
||||||
|
s := v.Interface().(fmt.Stringer).String()
|
||||||
|
return stringerPrefix + formatString(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v.Kind() {
|
||||||
|
case reflect.Bool:
|
||||||
|
return formatPrimitive(v.Type(), v.Bool(), conf)
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
return formatPrimitive(v.Type(), v.Int(), conf)
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||||
|
if v.Type().PkgPath() == "" || v.Kind() == reflect.Uintptr {
|
||||||
|
// Unnamed uints are usually bytes or words, so use hexadecimal.
|
||||||
|
return formatPrimitive(v.Type(), formatHex(v.Uint()), conf)
|
||||||
|
}
|
||||||
|
return formatPrimitive(v.Type(), v.Uint(), conf)
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return formatPrimitive(v.Type(), v.Float(), conf)
|
||||||
|
case reflect.Complex64, reflect.Complex128:
|
||||||
|
return formatPrimitive(v.Type(), v.Complex(), conf)
|
||||||
|
case reflect.String:
|
||||||
|
return formatPrimitive(v.Type(), formatString(v.String()), conf)
|
||||||
|
case reflect.UnsafePointer, reflect.Chan, reflect.Func:
|
||||||
|
return formatPointer(v, conf)
|
||||||
|
case reflect.Ptr:
|
||||||
|
if v.IsNil() {
|
||||||
|
if conf.printType {
|
||||||
|
return fmt.Sprintf("(%v)(nil)", v.Type())
|
||||||
|
}
|
||||||
|
return "<nil>"
|
||||||
|
}
|
||||||
|
if visited[v.Pointer()] || !conf.followPointers {
|
||||||
|
return formatPointer(v, conf)
|
||||||
|
}
|
||||||
|
visited = insertPointer(visited, v.Pointer())
|
||||||
|
return "&" + formatAny(v.Elem(), conf, visited)
|
||||||
|
case reflect.Interface:
|
||||||
|
if v.IsNil() {
|
||||||
|
if conf.printType {
|
||||||
|
return fmt.Sprintf("%v(nil)", v.Type())
|
||||||
|
}
|
||||||
|
return "<nil>"
|
||||||
|
}
|
||||||
|
return formatAny(v.Elem(), conf, visited)
|
||||||
|
case reflect.Slice:
|
||||||
|
if v.IsNil() {
|
||||||
|
if conf.printType {
|
||||||
|
return fmt.Sprintf("%v(nil)", v.Type())
|
||||||
|
}
|
||||||
|
return "<nil>"
|
||||||
|
}
|
||||||
|
if visited[v.Pointer()] {
|
||||||
|
return formatPointer(v, conf)
|
||||||
|
}
|
||||||
|
visited = insertPointer(visited, v.Pointer())
|
||||||
|
fallthrough
|
||||||
|
case reflect.Array:
|
||||||
|
var ss []string
|
||||||
|
subConf := conf
|
||||||
|
subConf.printType = v.Type().Elem().Kind() == reflect.Interface
|
||||||
|
for i := 0; i < v.Len(); i++ {
|
||||||
|
s := formatAny(v.Index(i), subConf, visited)
|
||||||
|
ss = append(ss, s)
|
||||||
|
}
|
||||||
|
s := fmt.Sprintf("{%s}", strings.Join(ss, ", "))
|
||||||
|
if conf.printType {
|
||||||
|
return v.Type().String() + s
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
case reflect.Map:
|
||||||
|
if v.IsNil() {
|
||||||
|
if conf.printType {
|
||||||
|
return fmt.Sprintf("%v(nil)", v.Type())
|
||||||
|
}
|
||||||
|
return "<nil>"
|
||||||
|
}
|
||||||
|
if visited[v.Pointer()] {
|
||||||
|
return formatPointer(v, conf)
|
||||||
|
}
|
||||||
|
visited = insertPointer(visited, v.Pointer())
|
||||||
|
|
||||||
|
var ss []string
|
||||||
|
keyConf, valConf := conf, conf
|
||||||
|
keyConf.printType = v.Type().Key().Kind() == reflect.Interface
|
||||||
|
keyConf.followPointers = false
|
||||||
|
valConf.printType = v.Type().Elem().Kind() == reflect.Interface
|
||||||
|
for _, k := range SortKeys(v.MapKeys()) {
|
||||||
|
sk := formatAny(k, keyConf, visited)
|
||||||
|
sv := formatAny(v.MapIndex(k), valConf, visited)
|
||||||
|
ss = append(ss, fmt.Sprintf("%s: %s", sk, sv))
|
||||||
|
}
|
||||||
|
s := fmt.Sprintf("{%s}", strings.Join(ss, ", "))
|
||||||
|
if conf.printType {
|
||||||
|
return v.Type().String() + s
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
case reflect.Struct:
|
||||||
|
var ss []string
|
||||||
|
subConf := conf
|
||||||
|
subConf.printType = true
|
||||||
|
for i := 0; i < v.NumField(); i++ {
|
||||||
|
vv := v.Field(i)
|
||||||
|
if isZero(vv) {
|
||||||
|
continue // Elide zero value fields
|
||||||
|
}
|
||||||
|
name := v.Type().Field(i).Name
|
||||||
|
subConf.UseStringer = conf.UseStringer
|
||||||
|
s := formatAny(vv, subConf, visited)
|
||||||
|
ss = append(ss, fmt.Sprintf("%s: %s", name, s))
|
||||||
|
}
|
||||||
|
s := fmt.Sprintf("{%s}", strings.Join(ss, ", "))
|
||||||
|
if conf.printType {
|
||||||
|
return v.Type().String() + s
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("%v kind not handled", v.Kind()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatString(s string) string {
|
||||||
|
// Use quoted string if it the same length as a raw string literal.
|
||||||
|
// Otherwise, attempt to use the raw string form.
|
||||||
|
qs := strconv.Quote(s)
|
||||||
|
if len(qs) == 1+len(s)+1 {
|
||||||
|
return qs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disallow newlines to ensure output is a single line.
|
||||||
|
// Only allow printable runes for readability purposes.
|
||||||
|
rawInvalid := func(r rune) bool {
|
||||||
|
return r == '`' || r == '\n' || !unicode.IsPrint(r)
|
||||||
|
}
|
||||||
|
if strings.IndexFunc(s, rawInvalid) < 0 {
|
||||||
|
return "`" + s + "`"
|
||||||
|
}
|
||||||
|
return qs
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatPrimitive(t reflect.Type, v interface{}, conf FormatConfig) string {
|
||||||
|
if conf.printType && (conf.PrintPrimitiveType || t.PkgPath() != "") {
|
||||||
|
return fmt.Sprintf("%v(%v)", t, v)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%v", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatPointer(v reflect.Value, conf FormatConfig) string {
|
||||||
|
p := v.Pointer()
|
||||||
|
if !conf.realPointers {
|
||||||
|
p = 0 // For deterministic printing purposes
|
||||||
|
}
|
||||||
|
s := formatHex(uint64(p))
|
||||||
|
if conf.printType {
|
||||||
|
return fmt.Sprintf("(%v)(%s)", v.Type(), s)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatHex(u uint64) string {
|
||||||
|
var f string
|
||||||
|
switch {
|
||||||
|
case u <= 0xff:
|
||||||
|
f = "0x%02x"
|
||||||
|
case u <= 0xffff:
|
||||||
|
f = "0x%04x"
|
||||||
|
case u <= 0xffffff:
|
||||||
|
f = "0x%06x"
|
||||||
|
case u <= 0xffffffff:
|
||||||
|
f = "0x%08x"
|
||||||
|
case u <= 0xffffffffff:
|
||||||
|
f = "0x%010x"
|
||||||
|
case u <= 0xffffffffffff:
|
||||||
|
f = "0x%012x"
|
||||||
|
case u <= 0xffffffffffffff:
|
||||||
|
f = "0x%014x"
|
||||||
|
case u <= 0xffffffffffffffff:
|
||||||
|
f = "0x%016x"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(f, u)
|
||||||
|
}
|
||||||
|
|
||||||
|
// insertPointer insert p into m, allocating m if necessary.
|
||||||
|
func insertPointer(m map[uintptr]bool, p uintptr) map[uintptr]bool {
|
||||||
|
if m == nil {
|
||||||
|
m = make(map[uintptr]bool)
|
||||||
|
}
|
||||||
|
m[p] = true
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// isZero reports whether v is the zero value.
|
||||||
|
// This does not rely on Interface and so can be used on unexported fields.
|
||||||
|
func isZero(v reflect.Value) bool {
|
||||||
|
switch v.Kind() {
|
||||||
|
case reflect.Bool:
|
||||||
|
return v.Bool() == false
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
return v.Int() == 0
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||||
|
return v.Uint() == 0
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return v.Float() == 0
|
||||||
|
case reflect.Complex64, reflect.Complex128:
|
||||||
|
return v.Complex() == 0
|
||||||
|
case reflect.String:
|
||||||
|
return v.String() == ""
|
||||||
|
case reflect.UnsafePointer:
|
||||||
|
return v.Pointer() == 0
|
||||||
|
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Ptr, reflect.Map, reflect.Slice:
|
||||||
|
return v.IsNil()
|
||||||
|
case reflect.Array:
|
||||||
|
for i := 0; i < v.Len(); i++ {
|
||||||
|
if !isZero(v.Index(i)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
case reflect.Struct:
|
||||||
|
for i := 0; i < v.NumField(); i++ {
|
||||||
|
if !isZero(v.Field(i)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
+111
@@ -0,0 +1,111 @@
|
|||||||
|
// 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.md file.
|
||||||
|
|
||||||
|
package value
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SortKeys sorts a list of map keys, deduplicating keys if necessary.
|
||||||
|
// The type of each value must be comparable.
|
||||||
|
func SortKeys(vs []reflect.Value) []reflect.Value {
|
||||||
|
if len(vs) == 0 {
|
||||||
|
return vs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the map keys.
|
||||||
|
sort.Sort(valueSorter(vs))
|
||||||
|
|
||||||
|
// Deduplicate keys (fails for NaNs).
|
||||||
|
vs2 := vs[:1]
|
||||||
|
for _, v := range vs[1:] {
|
||||||
|
if isLess(vs2[len(vs2)-1], v) {
|
||||||
|
vs2 = append(vs2, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vs2
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Use sort.Slice once Google AppEngine is on Go1.8 or above.
|
||||||
|
type valueSorter []reflect.Value
|
||||||
|
|
||||||
|
func (vs valueSorter) Len() int { return len(vs) }
|
||||||
|
func (vs valueSorter) Less(i, j int) bool { return isLess(vs[i], vs[j]) }
|
||||||
|
func (vs valueSorter) Swap(i, j int) { vs[i], vs[j] = vs[j], vs[i] }
|
||||||
|
|
||||||
|
// isLess is a generic function for sorting arbitrary map keys.
|
||||||
|
// The inputs must be of the same type and must be comparable.
|
||||||
|
func isLess(x, y reflect.Value) bool {
|
||||||
|
switch x.Type().Kind() {
|
||||||
|
case reflect.Bool:
|
||||||
|
return !x.Bool() && y.Bool()
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
return x.Int() < y.Int()
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||||
|
return x.Uint() < y.Uint()
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
fx, fy := x.Float(), y.Float()
|
||||||
|
return fx < fy || math.IsNaN(fx) && !math.IsNaN(fy)
|
||||||
|
case reflect.Complex64, reflect.Complex128:
|
||||||
|
cx, cy := x.Complex(), y.Complex()
|
||||||
|
rx, ix, ry, iy := real(cx), imag(cx), real(cy), imag(cy)
|
||||||
|
if rx == ry || (math.IsNaN(rx) && math.IsNaN(ry)) {
|
||||||
|
return ix < iy || math.IsNaN(ix) && !math.IsNaN(iy)
|
||||||
|
}
|
||||||
|
return rx < ry || math.IsNaN(rx) && !math.IsNaN(ry)
|
||||||
|
case reflect.Ptr, reflect.UnsafePointer, reflect.Chan:
|
||||||
|
return x.Pointer() < y.Pointer()
|
||||||
|
case reflect.String:
|
||||||
|
return x.String() < y.String()
|
||||||
|
case reflect.Array:
|
||||||
|
for i := 0; i < x.Len(); i++ {
|
||||||
|
if isLess(x.Index(i), y.Index(i)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if isLess(y.Index(i), x.Index(i)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
case reflect.Struct:
|
||||||
|
for i := 0; i < x.NumField(); i++ {
|
||||||
|
if isLess(x.Field(i), y.Field(i)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if isLess(y.Field(i), x.Field(i)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
case reflect.Interface:
|
||||||
|
vx, vy := x.Elem(), y.Elem()
|
||||||
|
if !vx.IsValid() || !vy.IsValid() {
|
||||||
|
return !vx.IsValid() && vy.IsValid()
|
||||||
|
}
|
||||||
|
tx, ty := vx.Type(), vy.Type()
|
||||||
|
if tx == ty {
|
||||||
|
return isLess(x.Elem(), y.Elem())
|
||||||
|
}
|
||||||
|
if tx.Kind() != ty.Kind() {
|
||||||
|
return vx.Kind() < vy.Kind()
|
||||||
|
}
|
||||||
|
if tx.String() != ty.String() {
|
||||||
|
return tx.String() < ty.String()
|
||||||
|
}
|
||||||
|
if tx.PkgPath() != ty.PkgPath() {
|
||||||
|
return tx.PkgPath() < ty.PkgPath()
|
||||||
|
}
|
||||||
|
// This can happen in rare situations, so we fallback to just comparing
|
||||||
|
// the unique pointer for a reflect.Type. This guarantees deterministic
|
||||||
|
// ordering within a program, but it is obviously not stable.
|
||||||
|
return reflect.ValueOf(vx.Type()).Pointer() < reflect.ValueOf(vy.Type()).Pointer()
|
||||||
|
default:
|
||||||
|
// Must be Func, Map, or Slice; which are not comparable.
|
||||||
|
panic(fmt.Sprintf("%T is not comparable", x.Type()))
|
||||||
|
}
|
||||||
|
}
|
||||||
+453
@@ -0,0 +1,453 @@
|
|||||||
|
// 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.md file.
|
||||||
|
|
||||||
|
package cmp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp/internal/function"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Option configures for specific behavior of Equal and Diff. In particular,
|
||||||
|
// the fundamental Option functions (Ignore, Transformer, and Comparer),
|
||||||
|
// configure how equality is determined.
|
||||||
|
//
|
||||||
|
// The fundamental options may be composed with filters (FilterPath and
|
||||||
|
// FilterValues) to control the scope over which they are applied.
|
||||||
|
//
|
||||||
|
// The cmp/cmpopts package provides helper functions for creating options that
|
||||||
|
// may be used with Equal and Diff.
|
||||||
|
type Option interface {
|
||||||
|
// filter applies all filters and returns the option that remains.
|
||||||
|
// Each option may only read s.curPath and call s.callTTBFunc.
|
||||||
|
//
|
||||||
|
// An Options is returned only if multiple comparers or transformers
|
||||||
|
// can apply simultaneously and will only contain values of those types
|
||||||
|
// or sub-Options containing values of those types.
|
||||||
|
filter(s *state, vx, vy reflect.Value, t reflect.Type) applicableOption
|
||||||
|
}
|
||||||
|
|
||||||
|
// applicableOption represents the following types:
|
||||||
|
// Fundamental: ignore | invalid | *comparer | *transformer
|
||||||
|
// Grouping: Options
|
||||||
|
type applicableOption interface {
|
||||||
|
Option
|
||||||
|
|
||||||
|
// apply executes the option, which may mutate s or panic.
|
||||||
|
apply(s *state, vx, vy reflect.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// coreOption represents the following types:
|
||||||
|
// Fundamental: ignore | invalid | *comparer | *transformer
|
||||||
|
// Filters: *pathFilter | *valuesFilter
|
||||||
|
type coreOption interface {
|
||||||
|
Option
|
||||||
|
isCore()
|
||||||
|
}
|
||||||
|
|
||||||
|
type core struct{}
|
||||||
|
|
||||||
|
func (core) isCore() {}
|
||||||
|
|
||||||
|
// Options is a list of Option values that also satisfies the Option interface.
|
||||||
|
// Helper comparison packages may return an Options value when packing multiple
|
||||||
|
// Option values into a single Option. When this package processes an Options,
|
||||||
|
// it will be implicitly expanded into a flat list.
|
||||||
|
//
|
||||||
|
// Applying a filter on an Options is equivalent to applying that same filter
|
||||||
|
// on all individual options held within.
|
||||||
|
type Options []Option
|
||||||
|
|
||||||
|
func (opts Options) filter(s *state, vx, vy reflect.Value, t reflect.Type) (out applicableOption) {
|
||||||
|
for _, opt := range opts {
|
||||||
|
switch opt := opt.filter(s, vx, vy, t); opt.(type) {
|
||||||
|
case ignore:
|
||||||
|
return ignore{} // Only ignore can short-circuit evaluation
|
||||||
|
case invalid:
|
||||||
|
out = invalid{} // Takes precedence over comparer or transformer
|
||||||
|
case *comparer, *transformer, Options:
|
||||||
|
switch out.(type) {
|
||||||
|
case nil:
|
||||||
|
out = opt
|
||||||
|
case invalid:
|
||||||
|
// Keep invalid
|
||||||
|
case *comparer, *transformer, Options:
|
||||||
|
out = Options{out, opt} // Conflicting comparers or transformers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opts Options) apply(s *state, _, _ reflect.Value) {
|
||||||
|
const warning = "ambiguous set of applicable options"
|
||||||
|
const help = "consider using filters to ensure at most one Comparer or Transformer may apply"
|
||||||
|
var ss []string
|
||||||
|
for _, opt := range flattenOptions(nil, opts) {
|
||||||
|
ss = append(ss, fmt.Sprint(opt))
|
||||||
|
}
|
||||||
|
set := strings.Join(ss, "\n\t")
|
||||||
|
panic(fmt.Sprintf("%s at %#v:\n\t%s\n%s", warning, s.curPath, set, help))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opts Options) String() string {
|
||||||
|
var ss []string
|
||||||
|
for _, opt := range opts {
|
||||||
|
ss = append(ss, fmt.Sprint(opt))
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Options{%s}", strings.Join(ss, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterPath returns a new Option where opt is only evaluated if filter f
|
||||||
|
// returns true for the current Path in the value tree.
|
||||||
|
//
|
||||||
|
// The option passed in may be an Ignore, Transformer, Comparer, Options, or
|
||||||
|
// a previously filtered Option.
|
||||||
|
func FilterPath(f func(Path) bool, opt Option) Option {
|
||||||
|
if f == nil {
|
||||||
|
panic("invalid path filter function")
|
||||||
|
}
|
||||||
|
if opt := normalizeOption(opt); opt != nil {
|
||||||
|
return &pathFilter{fnc: f, opt: opt}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type pathFilter struct {
|
||||||
|
core
|
||||||
|
fnc func(Path) bool
|
||||||
|
opt Option
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f pathFilter) filter(s *state, vx, vy reflect.Value, t reflect.Type) applicableOption {
|
||||||
|
if f.fnc(s.curPath) {
|
||||||
|
return f.opt.filter(s, vx, vy, t)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f pathFilter) String() string {
|
||||||
|
fn := getFuncName(reflect.ValueOf(f.fnc).Pointer())
|
||||||
|
return fmt.Sprintf("FilterPath(%s, %v)", fn, f.opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterValues returns a new Option where opt is only evaluated if filter f,
|
||||||
|
// which is a function of the form "func(T, T) bool", returns true for the
|
||||||
|
// current pair of values being compared. If the type of the values is not
|
||||||
|
// assignable to T, then this filter implicitly returns false.
|
||||||
|
//
|
||||||
|
// The filter function must be
|
||||||
|
// symmetric (i.e., agnostic to the order of the inputs) and
|
||||||
|
// deterministic (i.e., produces the same result when given the same inputs).
|
||||||
|
// If T is an interface, it is possible that f is called with two values with
|
||||||
|
// different concrete types that both implement T.
|
||||||
|
//
|
||||||
|
// The option passed in may be an Ignore, Transformer, Comparer, Options, or
|
||||||
|
// a previously filtered Option.
|
||||||
|
func FilterValues(f interface{}, opt Option) Option {
|
||||||
|
v := reflect.ValueOf(f)
|
||||||
|
if !function.IsType(v.Type(), function.ValueFilter) || v.IsNil() {
|
||||||
|
panic(fmt.Sprintf("invalid values filter function: %T", f))
|
||||||
|
}
|
||||||
|
if opt := normalizeOption(opt); opt != nil {
|
||||||
|
vf := &valuesFilter{fnc: v, opt: opt}
|
||||||
|
if ti := v.Type().In(0); ti.Kind() != reflect.Interface || ti.NumMethod() > 0 {
|
||||||
|
vf.typ = ti
|
||||||
|
}
|
||||||
|
return vf
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type valuesFilter struct {
|
||||||
|
core
|
||||||
|
typ reflect.Type // T
|
||||||
|
fnc reflect.Value // func(T, T) bool
|
||||||
|
opt Option
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f valuesFilter) filter(s *state, vx, vy reflect.Value, t reflect.Type) applicableOption {
|
||||||
|
if !vx.IsValid() || !vy.IsValid() {
|
||||||
|
return invalid{}
|
||||||
|
}
|
||||||
|
if (f.typ == nil || t.AssignableTo(f.typ)) && s.callTTBFunc(f.fnc, vx, vy) {
|
||||||
|
return f.opt.filter(s, vx, vy, t)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f valuesFilter) String() string {
|
||||||
|
fn := getFuncName(f.fnc.Pointer())
|
||||||
|
return fmt.Sprintf("FilterValues(%s, %v)", fn, f.opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore is an Option that causes all comparisons to be ignored.
|
||||||
|
// This value is intended to be combined with FilterPath or FilterValues.
|
||||||
|
// It is an error to pass an unfiltered Ignore option to Equal.
|
||||||
|
func Ignore() Option { return ignore{} }
|
||||||
|
|
||||||
|
type ignore struct{ core }
|
||||||
|
|
||||||
|
func (ignore) isFiltered() bool { return false }
|
||||||
|
func (ignore) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption { return ignore{} }
|
||||||
|
func (ignore) apply(_ *state, _, _ reflect.Value) { return }
|
||||||
|
func (ignore) String() string { return "Ignore()" }
|
||||||
|
|
||||||
|
// invalid is a sentinel Option type to indicate that some options could not
|
||||||
|
// be evaluated due to unexported fields.
|
||||||
|
type invalid struct{ core }
|
||||||
|
|
||||||
|
func (invalid) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption { return invalid{} }
|
||||||
|
func (invalid) apply(s *state, _, _ reflect.Value) {
|
||||||
|
const help = "consider using AllowUnexported or cmpopts.IgnoreUnexported"
|
||||||
|
panic(fmt.Sprintf("cannot handle unexported field: %#v\n%s", s.curPath, help))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transformer returns an Option that applies a transformation function that
|
||||||
|
// converts values of a certain type into that of another.
|
||||||
|
//
|
||||||
|
// The transformer f must be a function "func(T) R" that converts values of
|
||||||
|
// type T to those of type R and is implicitly filtered to input values
|
||||||
|
// assignable to T. The transformer must not mutate T in any way.
|
||||||
|
//
|
||||||
|
// To help prevent some cases of infinite recursive cycles applying the
|
||||||
|
// same transform to the output of itself (e.g., in the case where the
|
||||||
|
// input and output types are the same), an implicit filter is added such that
|
||||||
|
// a transformer is applicable only if that exact transformer is not already
|
||||||
|
// in the tail of the Path since the last non-Transform step.
|
||||||
|
//
|
||||||
|
// The name is a user provided label that is used as the Transform.Name in the
|
||||||
|
// transformation PathStep. If empty, an arbitrary name is used.
|
||||||
|
func Transformer(name string, f interface{}) Option {
|
||||||
|
v := reflect.ValueOf(f)
|
||||||
|
if !function.IsType(v.Type(), function.Transformer) || v.IsNil() {
|
||||||
|
panic(fmt.Sprintf("invalid transformer function: %T", f))
|
||||||
|
}
|
||||||
|
if name == "" {
|
||||||
|
name = "λ" // Lambda-symbol as place-holder for anonymous transformer
|
||||||
|
}
|
||||||
|
if !isValid(name) {
|
||||||
|
panic(fmt.Sprintf("invalid name: %q", name))
|
||||||
|
}
|
||||||
|
tr := &transformer{name: name, fnc: reflect.ValueOf(f)}
|
||||||
|
if ti := v.Type().In(0); ti.Kind() != reflect.Interface || ti.NumMethod() > 0 {
|
||||||
|
tr.typ = ti
|
||||||
|
}
|
||||||
|
return tr
|
||||||
|
}
|
||||||
|
|
||||||
|
type transformer struct {
|
||||||
|
core
|
||||||
|
name string
|
||||||
|
typ reflect.Type // T
|
||||||
|
fnc reflect.Value // func(T) R
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tr *transformer) isFiltered() bool { return tr.typ != nil }
|
||||||
|
|
||||||
|
func (tr *transformer) filter(s *state, _, _ reflect.Value, t reflect.Type) applicableOption {
|
||||||
|
for i := len(s.curPath) - 1; i >= 0; i-- {
|
||||||
|
if t, ok := s.curPath[i].(*transform); !ok {
|
||||||
|
break // Hit most recent non-Transform step
|
||||||
|
} else if tr == t.trans {
|
||||||
|
return nil // Cannot directly use same Transform
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tr.typ == nil || t.AssignableTo(tr.typ) {
|
||||||
|
return tr
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tr *transformer) apply(s *state, vx, vy reflect.Value) {
|
||||||
|
// Update path before calling the Transformer so that dynamic checks
|
||||||
|
// will use the updated path.
|
||||||
|
s.curPath.push(&transform{pathStep{tr.fnc.Type().Out(0)}, tr})
|
||||||
|
defer s.curPath.pop()
|
||||||
|
|
||||||
|
vx = s.callTRFunc(tr.fnc, vx)
|
||||||
|
vy = s.callTRFunc(tr.fnc, vy)
|
||||||
|
s.compareAny(vx, vy)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tr transformer) String() string {
|
||||||
|
return fmt.Sprintf("Transformer(%s, %s)", tr.name, getFuncName(tr.fnc.Pointer()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comparer returns an Option that determines whether two values are equal
|
||||||
|
// to each other.
|
||||||
|
//
|
||||||
|
// The comparer f must be a function "func(T, T) bool" and is implicitly
|
||||||
|
// filtered to input values assignable to T. If T is an interface, it is
|
||||||
|
// possible that f is called with two values of different concrete types that
|
||||||
|
// both implement T.
|
||||||
|
//
|
||||||
|
// The equality function must be:
|
||||||
|
// • Symmetric: equal(x, y) == equal(y, x)
|
||||||
|
// • Deterministic: equal(x, y) == equal(x, y)
|
||||||
|
// • Pure: equal(x, y) does not modify x or y
|
||||||
|
func Comparer(f interface{}) Option {
|
||||||
|
v := reflect.ValueOf(f)
|
||||||
|
if !function.IsType(v.Type(), function.Equal) || v.IsNil() {
|
||||||
|
panic(fmt.Sprintf("invalid comparer function: %T", f))
|
||||||
|
}
|
||||||
|
cm := &comparer{fnc: v}
|
||||||
|
if ti := v.Type().In(0); ti.Kind() != reflect.Interface || ti.NumMethod() > 0 {
|
||||||
|
cm.typ = ti
|
||||||
|
}
|
||||||
|
return cm
|
||||||
|
}
|
||||||
|
|
||||||
|
type comparer struct {
|
||||||
|
core
|
||||||
|
typ reflect.Type // T
|
||||||
|
fnc reflect.Value // func(T, T) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cm *comparer) isFiltered() bool { return cm.typ != nil }
|
||||||
|
|
||||||
|
func (cm *comparer) filter(_ *state, _, _ reflect.Value, t reflect.Type) applicableOption {
|
||||||
|
if cm.typ == nil || t.AssignableTo(cm.typ) {
|
||||||
|
return cm
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cm *comparer) apply(s *state, vx, vy reflect.Value) {
|
||||||
|
eq := s.callTTBFunc(cm.fnc, vx, vy)
|
||||||
|
s.report(eq, vx, vy)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cm comparer) String() string {
|
||||||
|
return fmt.Sprintf("Comparer(%s)", getFuncName(cm.fnc.Pointer()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllowUnexported returns an Option that forcibly allows operations on
|
||||||
|
// unexported fields in certain structs, which are specified by passing in a
|
||||||
|
// value of each struct type.
|
||||||
|
//
|
||||||
|
// Users of this option must understand that comparing on unexported fields
|
||||||
|
// from external packages is not safe since changes in the internal
|
||||||
|
// implementation of some external package may cause the result of Equal
|
||||||
|
// to unexpectedly change. However, it may be valid to use this option on types
|
||||||
|
// defined in an internal package where the semantic meaning of an unexported
|
||||||
|
// field is in the control of the user.
|
||||||
|
//
|
||||||
|
// For some cases, a custom Comparer should be used instead that defines
|
||||||
|
// equality as a function of the public API of a type rather than the underlying
|
||||||
|
// unexported implementation.
|
||||||
|
//
|
||||||
|
// For example, the reflect.Type documentation defines equality to be determined
|
||||||
|
// by the == operator on the interface (essentially performing a shallow pointer
|
||||||
|
// comparison) and most attempts to compare *regexp.Regexp types are interested
|
||||||
|
// in only checking that the regular expression strings are equal.
|
||||||
|
// Both of these are accomplished using Comparers:
|
||||||
|
//
|
||||||
|
// Comparer(func(x, y reflect.Type) bool { return x == y })
|
||||||
|
// Comparer(func(x, y *regexp.Regexp) bool { return x.String() == y.String() })
|
||||||
|
//
|
||||||
|
// In other cases, the cmpopts.IgnoreUnexported option can be used to ignore
|
||||||
|
// all unexported fields on specified struct types.
|
||||||
|
func AllowUnexported(types ...interface{}) Option {
|
||||||
|
if !supportAllowUnexported {
|
||||||
|
panic("AllowUnexported is not supported on purego builds, Google App Engine Standard, or GopherJS")
|
||||||
|
}
|
||||||
|
m := make(map[reflect.Type]bool)
|
||||||
|
for _, typ := range types {
|
||||||
|
t := reflect.TypeOf(typ)
|
||||||
|
if t.Kind() != reflect.Struct {
|
||||||
|
panic(fmt.Sprintf("invalid struct type: %T", typ))
|
||||||
|
}
|
||||||
|
m[t] = true
|
||||||
|
}
|
||||||
|
return visibleStructs(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
type visibleStructs map[reflect.Type]bool
|
||||||
|
|
||||||
|
func (visibleStructs) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption {
|
||||||
|
panic("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// reporter is an Option that configures how differences are reported.
|
||||||
|
type reporter interface {
|
||||||
|
// TODO: Not exported yet.
|
||||||
|
//
|
||||||
|
// Perhaps add PushStep and PopStep and change Report to only accept
|
||||||
|
// a PathStep instead of the full-path? Adding a PushStep and PopStep makes
|
||||||
|
// it clear that we are traversing the value tree in a depth-first-search
|
||||||
|
// manner, which has an effect on how values are printed.
|
||||||
|
|
||||||
|
Option
|
||||||
|
|
||||||
|
// Report is called for every comparison made and will be provided with
|
||||||
|
// the two values being compared, the equality result, and the
|
||||||
|
// current path in the value tree. It is possible for x or y to be an
|
||||||
|
// invalid reflect.Value if one of the values is non-existent;
|
||||||
|
// which is possible with maps and slices.
|
||||||
|
Report(x, y reflect.Value, eq bool, p Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalizeOption normalizes the input options such that all Options groups
|
||||||
|
// are flattened and groups with a single element are reduced to that element.
|
||||||
|
// Only coreOptions and Options containing coreOptions are allowed.
|
||||||
|
func normalizeOption(src Option) Option {
|
||||||
|
switch opts := flattenOptions(nil, Options{src}); len(opts) {
|
||||||
|
case 0:
|
||||||
|
return nil
|
||||||
|
case 1:
|
||||||
|
return opts[0]
|
||||||
|
default:
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// flattenOptions copies all options in src to dst as a flat list.
|
||||||
|
// Only coreOptions and Options containing coreOptions are allowed.
|
||||||
|
func flattenOptions(dst, src Options) Options {
|
||||||
|
for _, opt := range src {
|
||||||
|
switch opt := opt.(type) {
|
||||||
|
case nil:
|
||||||
|
continue
|
||||||
|
case Options:
|
||||||
|
dst = flattenOptions(dst, opt)
|
||||||
|
case coreOption:
|
||||||
|
dst = append(dst, opt)
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("invalid option type: %T", opt))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
|
||||||
|
// getFuncName returns a short function name from the pointer.
|
||||||
|
// The string parsing logic works up until Go1.9.
|
||||||
|
func getFuncName(p uintptr) string {
|
||||||
|
fnc := runtime.FuncForPC(p)
|
||||||
|
if fnc == nil {
|
||||||
|
return "<unknown>"
|
||||||
|
}
|
||||||
|
name := fnc.Name() // E.g., "long/path/name/mypkg.(mytype).(long/path/name/mypkg.myfunc)-fm"
|
||||||
|
if strings.HasSuffix(name, ")-fm") || strings.HasSuffix(name, ")·fm") {
|
||||||
|
// Strip the package name from method name.
|
||||||
|
name = strings.TrimSuffix(name, ")-fm")
|
||||||
|
name = strings.TrimSuffix(name, ")·fm")
|
||||||
|
if i := strings.LastIndexByte(name, '('); i >= 0 {
|
||||||
|
methodName := name[i+1:] // E.g., "long/path/name/mypkg.myfunc"
|
||||||
|
if j := strings.LastIndexByte(methodName, '.'); j >= 0 {
|
||||||
|
methodName = methodName[j+1:] // E.g., "myfunc"
|
||||||
|
}
|
||||||
|
name = name[:i] + methodName // E.g., "long/path/name/mypkg.(mytype)." + "myfunc"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if i := strings.LastIndexByte(name, '/'); i >= 0 {
|
||||||
|
// Strip the package name.
|
||||||
|
name = name[i+1:] // E.g., "mypkg.(mytype).myfunc"
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
+309
@@ -0,0 +1,309 @@
|
|||||||
|
// 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.md file.
|
||||||
|
|
||||||
|
package cmp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// Path is a list of PathSteps describing the sequence of operations to get
|
||||||
|
// from some root type to the current position in the value tree.
|
||||||
|
// The first Path element is always an operation-less PathStep that exists
|
||||||
|
// simply to identify the initial type.
|
||||||
|
//
|
||||||
|
// When traversing structs with embedded structs, the embedded struct will
|
||||||
|
// always be accessed as a field before traversing the fields of the
|
||||||
|
// embedded struct themselves. That is, an exported field from the
|
||||||
|
// embedded struct will never be accessed directly from the parent struct.
|
||||||
|
Path []PathStep
|
||||||
|
|
||||||
|
// PathStep is a union-type for specific operations to traverse
|
||||||
|
// a value's tree structure. Users of this package never need to implement
|
||||||
|
// these types as values of this type will be returned by this package.
|
||||||
|
PathStep interface {
|
||||||
|
String() string
|
||||||
|
Type() reflect.Type // Resulting type after performing the path step
|
||||||
|
isPathStep()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SliceIndex is an index operation on a slice or array at some index Key.
|
||||||
|
SliceIndex interface {
|
||||||
|
PathStep
|
||||||
|
Key() int // May return -1 if in a split state
|
||||||
|
|
||||||
|
// SplitKeys returns the indexes for indexing into slices in the
|
||||||
|
// x and y values, respectively. These indexes may differ due to the
|
||||||
|
// insertion or removal of an element in one of the slices, causing
|
||||||
|
// all of the indexes to be shifted. If an index is -1, then that
|
||||||
|
// indicates that the element does not exist in the associated slice.
|
||||||
|
//
|
||||||
|
// Key is guaranteed to return -1 if and only if the indexes returned
|
||||||
|
// by SplitKeys are not the same. SplitKeys will never return -1 for
|
||||||
|
// both indexes.
|
||||||
|
SplitKeys() (x int, y int)
|
||||||
|
|
||||||
|
isSliceIndex()
|
||||||
|
}
|
||||||
|
// MapIndex is an index operation on a map at some index Key.
|
||||||
|
MapIndex interface {
|
||||||
|
PathStep
|
||||||
|
Key() reflect.Value
|
||||||
|
isMapIndex()
|
||||||
|
}
|
||||||
|
// TypeAssertion represents a type assertion on an interface.
|
||||||
|
TypeAssertion interface {
|
||||||
|
PathStep
|
||||||
|
isTypeAssertion()
|
||||||
|
}
|
||||||
|
// StructField represents a struct field access on a field called Name.
|
||||||
|
StructField interface {
|
||||||
|
PathStep
|
||||||
|
Name() string
|
||||||
|
Index() int
|
||||||
|
isStructField()
|
||||||
|
}
|
||||||
|
// Indirect represents pointer indirection on the parent type.
|
||||||
|
Indirect interface {
|
||||||
|
PathStep
|
||||||
|
isIndirect()
|
||||||
|
}
|
||||||
|
// Transform is a transformation from the parent type to the current type.
|
||||||
|
Transform interface {
|
||||||
|
PathStep
|
||||||
|
Name() string
|
||||||
|
Func() reflect.Value
|
||||||
|
|
||||||
|
// Option returns the originally constructed Transformer option.
|
||||||
|
// The == operator can be used to detect the exact option used.
|
||||||
|
Option() Option
|
||||||
|
|
||||||
|
isTransform()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (pa *Path) push(s PathStep) {
|
||||||
|
*pa = append(*pa, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pa *Path) pop() {
|
||||||
|
*pa = (*pa)[:len(*pa)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last returns the last PathStep in the Path.
|
||||||
|
// If the path is empty, this returns a non-nil PathStep that reports a nil Type.
|
||||||
|
func (pa Path) Last() PathStep {
|
||||||
|
return pa.Index(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index returns the ith step in the Path and supports negative indexing.
|
||||||
|
// A negative index starts counting from the tail of the Path such that -1
|
||||||
|
// refers to the last step, -2 refers to the second-to-last step, and so on.
|
||||||
|
// If index is invalid, this returns a non-nil PathStep that reports a nil Type.
|
||||||
|
func (pa Path) Index(i int) PathStep {
|
||||||
|
if i < 0 {
|
||||||
|
i = len(pa) + i
|
||||||
|
}
|
||||||
|
if i < 0 || i >= len(pa) {
|
||||||
|
return pathStep{}
|
||||||
|
}
|
||||||
|
return pa[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the simplified path to a node.
|
||||||
|
// The simplified path only contains struct field accesses.
|
||||||
|
//
|
||||||
|
// For example:
|
||||||
|
// MyMap.MySlices.MyField
|
||||||
|
func (pa Path) String() string {
|
||||||
|
var ss []string
|
||||||
|
for _, s := range pa {
|
||||||
|
if _, ok := s.(*structField); ok {
|
||||||
|
ss = append(ss, s.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.TrimPrefix(strings.Join(ss, ""), ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GoString returns the path to a specific node using Go syntax.
|
||||||
|
//
|
||||||
|
// For example:
|
||||||
|
// (*root.MyMap["key"].(*mypkg.MyStruct).MySlices)[2][3].MyField
|
||||||
|
func (pa Path) GoString() string {
|
||||||
|
var ssPre, ssPost []string
|
||||||
|
var numIndirect int
|
||||||
|
for i, s := range pa {
|
||||||
|
var nextStep PathStep
|
||||||
|
if i+1 < len(pa) {
|
||||||
|
nextStep = pa[i+1]
|
||||||
|
}
|
||||||
|
switch s := s.(type) {
|
||||||
|
case *indirect:
|
||||||
|
numIndirect++
|
||||||
|
pPre, pPost := "(", ")"
|
||||||
|
switch nextStep.(type) {
|
||||||
|
case *indirect:
|
||||||
|
continue // Next step is indirection, so let them batch up
|
||||||
|
case *structField:
|
||||||
|
numIndirect-- // Automatic indirection on struct fields
|
||||||
|
case nil:
|
||||||
|
pPre, pPost = "", "" // Last step; no need for parenthesis
|
||||||
|
}
|
||||||
|
if numIndirect > 0 {
|
||||||
|
ssPre = append(ssPre, pPre+strings.Repeat("*", numIndirect))
|
||||||
|
ssPost = append(ssPost, pPost)
|
||||||
|
}
|
||||||
|
numIndirect = 0
|
||||||
|
continue
|
||||||
|
case *transform:
|
||||||
|
ssPre = append(ssPre, s.trans.name+"(")
|
||||||
|
ssPost = append(ssPost, ")")
|
||||||
|
continue
|
||||||
|
case *typeAssertion:
|
||||||
|
// As a special-case, elide type assertions on anonymous types
|
||||||
|
// since they are typically generated dynamically and can be very
|
||||||
|
// verbose. For example, some transforms return interface{} because
|
||||||
|
// of Go's lack of generics, but typically take in and return the
|
||||||
|
// exact same concrete type.
|
||||||
|
if s.Type().PkgPath() == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ssPost = append(ssPost, s.String())
|
||||||
|
}
|
||||||
|
for i, j := 0, len(ssPre)-1; i < j; i, j = i+1, j-1 {
|
||||||
|
ssPre[i], ssPre[j] = ssPre[j], ssPre[i]
|
||||||
|
}
|
||||||
|
return strings.Join(ssPre, "") + strings.Join(ssPost, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
pathStep struct {
|
||||||
|
typ reflect.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
sliceIndex struct {
|
||||||
|
pathStep
|
||||||
|
xkey, ykey int
|
||||||
|
}
|
||||||
|
mapIndex struct {
|
||||||
|
pathStep
|
||||||
|
key reflect.Value
|
||||||
|
}
|
||||||
|
typeAssertion struct {
|
||||||
|
pathStep
|
||||||
|
}
|
||||||
|
structField struct {
|
||||||
|
pathStep
|
||||||
|
name string
|
||||||
|
idx int
|
||||||
|
|
||||||
|
// These fields are used for forcibly accessing an unexported field.
|
||||||
|
// pvx, pvy, and field are only valid if unexported is true.
|
||||||
|
unexported bool
|
||||||
|
force bool // Forcibly allow visibility
|
||||||
|
pvx, pvy reflect.Value // Parent values
|
||||||
|
field reflect.StructField // Field information
|
||||||
|
}
|
||||||
|
indirect struct {
|
||||||
|
pathStep
|
||||||
|
}
|
||||||
|
transform struct {
|
||||||
|
pathStep
|
||||||
|
trans *transformer
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (ps pathStep) Type() reflect.Type { return ps.typ }
|
||||||
|
func (ps pathStep) String() string {
|
||||||
|
if ps.typ == nil {
|
||||||
|
return "<nil>"
|
||||||
|
}
|
||||||
|
s := ps.typ.String()
|
||||||
|
if s == "" || strings.ContainsAny(s, "{}\n") {
|
||||||
|
return "root" // Type too simple or complex to print
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("{%s}", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (si sliceIndex) String() string {
|
||||||
|
switch {
|
||||||
|
case si.xkey == si.ykey:
|
||||||
|
return fmt.Sprintf("[%d]", si.xkey)
|
||||||
|
case si.ykey == -1:
|
||||||
|
// [5->?] means "I don't know where X[5] went"
|
||||||
|
return fmt.Sprintf("[%d->?]", si.xkey)
|
||||||
|
case si.xkey == -1:
|
||||||
|
// [?->3] means "I don't know where Y[3] came from"
|
||||||
|
return fmt.Sprintf("[?->%d]", si.ykey)
|
||||||
|
default:
|
||||||
|
// [5->3] means "X[5] moved to Y[3]"
|
||||||
|
return fmt.Sprintf("[%d->%d]", si.xkey, si.ykey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (mi mapIndex) String() string { return fmt.Sprintf("[%#v]", mi.key) }
|
||||||
|
func (ta typeAssertion) String() string { return fmt.Sprintf(".(%v)", ta.typ) }
|
||||||
|
func (sf structField) String() string { return fmt.Sprintf(".%s", sf.name) }
|
||||||
|
func (in indirect) String() string { return "*" }
|
||||||
|
func (tf transform) String() string { return fmt.Sprintf("%s()", tf.trans.name) }
|
||||||
|
|
||||||
|
func (si sliceIndex) Key() int {
|
||||||
|
if si.xkey != si.ykey {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return si.xkey
|
||||||
|
}
|
||||||
|
func (si sliceIndex) SplitKeys() (x, y int) { return si.xkey, si.ykey }
|
||||||
|
func (mi mapIndex) Key() reflect.Value { return mi.key }
|
||||||
|
func (sf structField) Name() string { return sf.name }
|
||||||
|
func (sf structField) Index() int { return sf.idx }
|
||||||
|
func (tf transform) Name() string { return tf.trans.name }
|
||||||
|
func (tf transform) Func() reflect.Value { return tf.trans.fnc }
|
||||||
|
func (tf transform) Option() Option { return tf.trans }
|
||||||
|
|
||||||
|
func (pathStep) isPathStep() {}
|
||||||
|
func (sliceIndex) isSliceIndex() {}
|
||||||
|
func (mapIndex) isMapIndex() {}
|
||||||
|
func (typeAssertion) isTypeAssertion() {}
|
||||||
|
func (structField) isStructField() {}
|
||||||
|
func (indirect) isIndirect() {}
|
||||||
|
func (transform) isTransform() {}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ SliceIndex = sliceIndex{}
|
||||||
|
_ MapIndex = mapIndex{}
|
||||||
|
_ TypeAssertion = typeAssertion{}
|
||||||
|
_ StructField = structField{}
|
||||||
|
_ Indirect = indirect{}
|
||||||
|
_ Transform = transform{}
|
||||||
|
|
||||||
|
_ PathStep = sliceIndex{}
|
||||||
|
_ PathStep = mapIndex{}
|
||||||
|
_ PathStep = typeAssertion{}
|
||||||
|
_ PathStep = structField{}
|
||||||
|
_ PathStep = indirect{}
|
||||||
|
_ PathStep = transform{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// isExported reports whether the identifier is exported.
|
||||||
|
func isExported(id string) bool {
|
||||||
|
r, _ := utf8.DecodeRuneInString(id)
|
||||||
|
return unicode.IsUpper(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// isValid reports whether the identifier is valid.
|
||||||
|
// Empty and underscore-only strings are not valid.
|
||||||
|
func isValid(id string) bool {
|
||||||
|
ok := id != "" && id != "_"
|
||||||
|
for j, c := range id {
|
||||||
|
ok = ok && (j > 0 || !unicode.IsDigit(c))
|
||||||
|
ok = ok && (c == '_' || unicode.IsLetter(c) || unicode.IsDigit(c))
|
||||||
|
}
|
||||||
|
return ok
|
||||||
|
}
|
||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
// 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.md file.
|
||||||
|
|
||||||
|
package cmp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp/internal/value"
|
||||||
|
)
|
||||||
|
|
||||||
|
type defaultReporter struct {
|
||||||
|
Option
|
||||||
|
diffs []string // List of differences, possibly truncated
|
||||||
|
ndiffs int // Total number of differences
|
||||||
|
nbytes int // Number of bytes in diffs
|
||||||
|
nlines int // Number of lines in diffs
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ reporter = (*defaultReporter)(nil)
|
||||||
|
|
||||||
|
func (r *defaultReporter) Report(x, y reflect.Value, eq bool, p Path) {
|
||||||
|
if eq {
|
||||||
|
return // Ignore equal results
|
||||||
|
}
|
||||||
|
const maxBytes = 4096
|
||||||
|
const maxLines = 256
|
||||||
|
r.ndiffs++
|
||||||
|
if r.nbytes < maxBytes && r.nlines < maxLines {
|
||||||
|
sx := value.Format(x, value.FormatConfig{UseStringer: true})
|
||||||
|
sy := value.Format(y, value.FormatConfig{UseStringer: true})
|
||||||
|
if sx == sy {
|
||||||
|
// Unhelpful output, so use more exact formatting.
|
||||||
|
sx = value.Format(x, value.FormatConfig{PrintPrimitiveType: true})
|
||||||
|
sy = value.Format(y, value.FormatConfig{PrintPrimitiveType: true})
|
||||||
|
}
|
||||||
|
s := fmt.Sprintf("%#v:\n\t-: %s\n\t+: %s\n", p, sx, sy)
|
||||||
|
r.diffs = append(r.diffs, s)
|
||||||
|
r.nbytes += len(s)
|
||||||
|
r.nlines += strings.Count(s, "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *defaultReporter) String() string {
|
||||||
|
s := strings.Join(r.diffs, "")
|
||||||
|
if r.ndiffs == len(r.diffs) {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s... %d more differences ...", s, r.ndiffs-len(r.diffs))
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
// 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.md file.
|
||||||
|
|
||||||
|
// +build purego appengine js
|
||||||
|
|
||||||
|
package cmp
|
||||||
|
|
||||||
|
import "reflect"
|
||||||
|
|
||||||
|
const supportAllowUnexported = false
|
||||||
|
|
||||||
|
func unsafeRetrieveField(reflect.Value, reflect.StructField) reflect.Value {
|
||||||
|
panic("unsafeRetrieveField is not implemented")
|
||||||
|
}
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
// 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.md file.
|
||||||
|
|
||||||
|
// +build !purego,!appengine,!js
|
||||||
|
|
||||||
|
package cmp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const supportAllowUnexported = true
|
||||||
|
|
||||||
|
// unsafeRetrieveField uses unsafe to forcibly retrieve any field from a struct
|
||||||
|
// such that the value has read-write permissions.
|
||||||
|
//
|
||||||
|
// The parent struct, v, must be addressable, while f must be a StructField
|
||||||
|
// describing the field to retrieve.
|
||||||
|
func unsafeRetrieveField(v reflect.Value, f reflect.StructField) reflect.Value {
|
||||||
|
return reflect.NewAt(f.Type, unsafe.Pointer(v.UnsafeAddr()+f.Offset)).Elem()
|
||||||
|
}
|
||||||
+10
@@ -76,6 +76,16 @@ func Expand(path string) (string, error) {
|
|||||||
return filepath.Join(dir, path[1:]), nil
|
return filepath.Join(dir, path[1:]), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset clears the cache, forcing the next call to Dir to re-detect
|
||||||
|
// the home directory. This generally never has to be called, but can be
|
||||||
|
// useful in tests if you're modifying the home directory via the HOME
|
||||||
|
// env var or something.
|
||||||
|
func Reset() {
|
||||||
|
cacheLock.Lock()
|
||||||
|
defer cacheLock.Unlock()
|
||||||
|
homedirCache = ""
|
||||||
|
}
|
||||||
|
|
||||||
func dirUnix() (string, error) {
|
func dirUnix() (string, error) {
|
||||||
homeEnv := "HOME"
|
homeEnv := "HOME"
|
||||||
if runtime.GOOS == "plan9" {
|
if runtime.GOOS == "plan9" {
|
||||||
|
|||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
sudo: false
|
||||||
|
language: go
|
||||||
|
go: master
|
||||||
|
before_script:
|
||||||
|
- go vet
|
||||||
|
- go get github.com/client9/misspell/cmd/misspell
|
||||||
|
- misspell -error *
|
||||||
|
- go get github.com/soniakeys/vetc
|
||||||
|
- vetc
|
||||||
Loaded 50 of 189 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user