feat: absolute imports (#63)

* feat: go-like import style

jb now creates a directory structure inside of vendor/ that is similar to how go
does (github.com/grafana/jsonnet-libs). This is reflected in the final import
paths, which means they will be go-like

* refactor(spec/deps): named regexs

* feat: make goImportStyle configurable

Defaults to off, can be enabled in `jsonnetfile.json`

* fix: integration test

* doc: license headers

* fix(deps): remove GO_IMPORT_STYLE

not an option anymore, will always do so and symlink

* feat: symlink to legacy location

* feat: allow to disable legacy links

* fix(test): legacyImports in integration tests

* fix(spec): test

* fix: respect legacyName aliases

It was possible to alias packages by changing `name` previously.

While names are now absolute (and computed), legacy links should still respect
old aliases to avoid breaking code.

* fix(test): integration

* fix(init): keep legacyImports enabled for now

* feat: rewrite imports

adds a command to automatically rewrite imports from legacy to absolute style

* fix(tool): rewrite confused by prefixing packages

When a package was a prefix of another one, it broke.
Fixed that by using a proper regular expression. Added a test to make sure it
works as expected

* Update cmd/jb/init.go

* fix: exclude local packages from legacy linking

They actually still use the old style, which is fine. LegacyLinking
messed them up, but from now on it just ignores symlinks that match a localPackage.
This commit is contained in:
Tom 2020-01-24 08:02:34 +01:00 committed by GitHub
parent 184841238b
commit 7b8a7836a4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 1129 additions and 361 deletions

View file

@ -106,6 +106,9 @@ Commands:
update
Update all dependencies.
rewrite
Automatically rewrite legacy imports to absolute ones
```

View file

@ -15,32 +15,37 @@
package main
import (
"encoding/json"
"io/ioutil"
"path/filepath"
kingpin "gopkg.in/alecthomas/kingpin.v2"
"github.com/jsonnet-bundler/jsonnet-bundler/pkg/jsonnetfile"
"github.com/jsonnet-bundler/jsonnet-bundler/spec"
)
func initCommand(dir string) int {
exists, err := jsonnetfile.Exists(jsonnetfile.File)
if err != nil {
kingpin.Errorf("Failed to check for jsonnetfile.json: %v", err)
return 1
}
kingpin.FatalIfError(err, "Failed to check for jsonnetfile.json")
if exists {
kingpin.Errorf("jsonnetfile.json already exists")
return 1
}
s := spec.New()
// TODO: disable them by default eventually
// s.LegacyImports = false
contents, err := json.MarshalIndent(s, "", " ")
kingpin.FatalIfError(err, "formatting jsonnetfile contents as json")
contents = append(contents, []byte("\n")...)
filename := filepath.Join(dir, jsonnetfile.File)
if err := ioutil.WriteFile(filename, []byte("{}\n"), 0644); err != nil {
kingpin.Errorf("Failed to write new jsonnetfile.json: %v", err)
return 1
}
ioutil.WriteFile(filename, contents, 0644)
kingpin.FatalIfError(err, "Failed to write new jsonnetfile.json")
return 0
}

View file

@ -27,6 +27,7 @@ import (
"github.com/jsonnet-bundler/jsonnet-bundler/pkg"
"github.com/jsonnet-bundler/jsonnet-bundler/pkg/jsonnetfile"
"github.com/jsonnet-bundler/jsonnet-bundler/spec"
"github.com/jsonnet-bundler/jsonnet-bundler/spec/deps"
)
func installCommand(dir, jsonnetHome string, uris []string) int {
@ -53,23 +54,25 @@ func installCommand(dir, jsonnetHome string, uris []string) int {
"creating vendor folder")
for _, u := range uris {
d := parseDependency(dir, u)
d := deps.Parse(dir, u)
if d == nil {
kingpin.Fatalf("Unable to parse package URI `%s`", u)
}
if !depEqual(jsonnetFile.Dependencies[d.Name], *d) {
if !depEqual(jsonnetFile.Dependencies[d.Name()], *d) {
// the dep passed on the cli is different from the jsonnetFile
jsonnetFile.Dependencies[d.Name] = *d
jsonnetFile.Dependencies[d.Name()] = *d
// we want to install the passed version (ignore the lock)
delete(lockFile.Dependencies, d.Name)
delete(lockFile.Dependencies, d.Name())
}
}
locked, err := pkg.Ensure(jsonnetFile, jsonnetHome, lockFile.Dependencies)
kingpin.FatalIfError(err, "failed to install packages")
pkg.CleanLegacyName(jsonnetFile.Dependencies)
kingpin.FatalIfError(
writeChangedJsonnetFile(jbfilebytes, &jsonnetFile, filepath.Join(dir, jsonnetfile.File)),
"updating jsonnetfile.json")
@ -81,8 +84,8 @@ func installCommand(dir, jsonnetHome string, uris []string) int {
return 0
}
func depEqual(d1, d2 spec.Dependency) bool {
name := d1.Name == d2.Name
func depEqual(d1, d2 deps.Dependency) bool {
name := d1.Name() == d2.Name()
version := d1.Version == d2.Version
source := reflect.DeepEqual(d1.Source, d2.Source)

View file

@ -25,8 +25,12 @@ import (
"github.com/jsonnet-bundler/jsonnet-bundler/pkg/jsonnetfile"
"github.com/jsonnet-bundler/jsonnet-bundler/spec"
"github.com/jsonnet-bundler/jsonnet-bundler/spec/deps"
)
// TODO: Change legacyImports to false eventually
const initContents = `{"dependencies": [], "legacyImports": true}`
func TestInstallCommand(t *testing.T) {
testcases := []struct {
Name string
@ -38,19 +42,21 @@ func TestInstallCommand(t *testing.T) {
{
Name: "NoURLs",
ExpectedCode: 0,
ExpectedJsonnetFile: []byte(`{}`),
}, {
ExpectedJsonnetFile: []byte(initContents),
},
{
Name: "OneURL",
URIs: []string{"github.com/jsonnet-bundler/jsonnet-bundler@v0.1.0"},
ExpectedCode: 0,
ExpectedJsonnetFile: []byte(`{"dependencies": [{"name": "jsonnet-bundler", "source": {"git": {"remote": "https://github.com/jsonnet-bundler/jsonnet-bundler", "subdir": ""}}, "version": "v0.1.0"}]}`),
ExpectedJsonnetLockFile: []byte(`{"dependencies": [{"name": "jsonnet-bundler", "source": {"git": {"remote": "https://github.com/jsonnet-bundler/jsonnet-bundler", "subdir": ""}}, "version": "080f157c7fb85ad0281ea78f6c641eaa570a582f", "sum": "W1uI550rQ66axRpPXA2EZDquyPg/5PHZlvUz1NEzefg="}]}`),
}, {
Name: "Relative",
ExpectedJsonnetFile: []byte(`{"dependencies": [{"source": {"git": {"remote": "https://github.com/jsonnet-bundler/jsonnet-bundler", "subdir": ""}}, "version": "v0.1.0"}], "legacyImports": true}`),
ExpectedJsonnetLockFile: []byte(`{"dependencies": [{"source": {"git": {"remote": "https://github.com/jsonnet-bundler/jsonnet-bundler", "subdir": ""}}, "version": "080f157c7fb85ad0281ea78f6c641eaa570a582f", "sum": "W1uI550rQ66axRpPXA2EZDquyPg/5PHZlvUz1NEzefg="}], "legacyImports": false}`),
},
{
Name: "Local",
URIs: []string{"jsonnet/foobar"},
ExpectedCode: 0,
ExpectedJsonnetFile: []byte(`{"dependencies": [{"name": "foobar", "source": {"local": {"directory": "jsonnet/foobar"}}, "version": ""}]}`),
ExpectedJsonnetLockFile: []byte(`{"dependencies": [{"name": "foobar", "source": {"local": {"directory": "jsonnet/foobar"}}, "version": ""}]}`),
ExpectedJsonnetFile: []byte(`{"dependencies": [{"source": {"local": {"directory": "jsonnet/foobar"}}, "version": ""}], "legacyImports": true}`),
ExpectedJsonnetLockFile: []byte(`{"dependencies": [{"source": {"local": {"directory": "jsonnet/foobar"}}, "version": ""}], "legacyImports": false}`),
},
}
@ -70,12 +76,12 @@ func TestInstallCommand(t *testing.T) {
err := os.MkdirAll(localDependency, os.ModePerm)
assert.NoError(t, err)
// init + check it works correctly (legacyImports true, empty dependencies)
initCommand("")
jsonnetFileContent(t, jsonnetfile.File, []byte(initContents))
jsonnetFileContent(t, jsonnetfile.File, []byte(`{}`))
// install something, check it writes only if required, etc.
installCommand("", "vendor", tc.URIs)
jsonnetFileContent(t, jsonnetfile.File, tc.ExpectedJsonnetFile)
if tc.ExpectedJsonnetLockFile != nil {
jsonnetFileContent(t, jsonnetfile.LockFile, tc.ExpectedJsonnetLockFile)
@ -111,11 +117,11 @@ func TestWriteChangedJsonnetFile(t *testing.T) {
},
{
Name: "NoDiffNotEmpty",
JsonnetFileBytes: []byte(`{"dependencies": [{"name": "foobar"}]}`),
JsonnetFileBytes: []byte(`{"dependencies": [{"version": "master"}]}`),
NewJsonnetFile: spec.JsonnetFile{
Dependencies: map[string]spec.Dependency{
"foobar": {
Name: "foobar",
Dependencies: map[string]deps.Dependency{
"": {
Version: "master",
},
},
},
@ -123,11 +129,10 @@ func TestWriteChangedJsonnetFile(t *testing.T) {
},
{
Name: "DiffVersion",
JsonnetFileBytes: []byte(`{"dependencies": [{"name": "foobar", "version": "1.0"}]}`),
JsonnetFileBytes: []byte(`{"dependencies": [{"version": "1.0"}]}`),
NewJsonnetFile: spec.JsonnetFile{
Dependencies: map[string]spec.Dependency{
"foobar": {
Name: "foobar",
Dependencies: map[string]deps.Dependency{
"": {
Version: "2.0",
},
},
@ -138,17 +143,18 @@ func TestWriteChangedJsonnetFile(t *testing.T) {
Name: "Diff",
JsonnetFileBytes: []byte(`{}`),
NewJsonnetFile: spec.JsonnetFile{
Dependencies: map[string]spec.Dependency{
"foobar": {
Name: "foobar",
Source: spec.Source{
GitSource: &spec.GitSource{
Remote: "https://github.com/foobar/foobar",
Dependencies: map[string]deps.Dependency{
"github.com/foobar/foobar": {
Source: deps.Source{
GitSource: &deps.Git{
Scheme: deps.GitSchemeHTTPS,
Host: "github.com",
User: "foobar",
Repo: "foobar",
Subdir: "",
},
},
Version: "master",
DepSource: "",
Version: "master",
}},
},
ExpectWrite: true,

View file

@ -17,32 +17,16 @@ package main
import (
"fmt"
"os"
"path"
"path/filepath"
"regexp"
"github.com/fatih/color"
"github.com/pkg/errors"
"github.com/jsonnet-bundler/jsonnet-bundler/spec"
)
const (
installActionName = "install"
updateActionName = "update"
initActionName = "init"
)
var (
gitSSHRegex = regexp.MustCompile(`git\+ssh://git@([^:]+):([^/]+)/([^/]+).git`)
gitSSHWithVersionRegex = regexp.MustCompile(`git\+ssh://git@([^:]+):([^/]+)/([^/]+).git@(.*)`)
gitSSHWithPathRegex = regexp.MustCompile(`git\+ssh://git@([^:]+):([^/]+)/([^/]+).git/(.*)`)
gitSSHWithPathAndVersionRegex = regexp.MustCompile(`git\+ssh://git@([^:]+):([^/]+)/([^/]+).git/(.*)@(.*)`)
githubSlugRegex = regexp.MustCompile("github.com/([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)")
githubSlugWithVersionRegex = regexp.MustCompile("github.com/([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)@(.*)")
githubSlugWithPathRegex = regexp.MustCompile("github.com/([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)/(.*)")
githubSlugWithPathAndVersionRegex = regexp.MustCompile("github.com/([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)/(.*)@(.*)")
rewriteActionName = "rewrite"
)
func main() {
@ -69,6 +53,8 @@ func Main() int {
updateCmd := a.Command(updateActionName, "Update all dependencies.")
rewriteCmd := a.Command(rewriteActionName, "Automatically rewrite legacy imports to absolute ones")
command, err := a.Parse(os.Args[1:])
if err != nil {
fmt.Fprintln(os.Stderr, errors.Wrapf(err, "Error parsing commandline arguments"))
@ -88,148 +74,11 @@ func Main() int {
return installCommand(workdir, cfg.JsonnetHome, *installCmdURIs)
case updateCmd.FullCommand():
return updateCommand(workdir, cfg.JsonnetHome)
case rewriteCmd.FullCommand():
return rewriteCommand(workdir, cfg.JsonnetHome)
default:
installCommand(workdir, cfg.JsonnetHome, []string{})
}
return 0
}
func parseDependency(dir, uri string) *spec.Dependency {
if uri == "" {
return nil
}
if githubSlugRegex.MatchString(uri) {
return parseGithubDependency(uri)
}
if gitSSHRegex.MatchString(uri) {
return parseGitSSHDependency(uri)
}
return parseLocalDependency(dir, uri)
}
func parseGitSSHDependency(p string) *spec.Dependency {
subdir := ""
host := ""
org := ""
repo := ""
version := "master"
switch {
case gitSSHWithPathAndVersionRegex.MatchString(p):
matches := gitSSHWithPathAndVersionRegex.FindStringSubmatch(p)
host = matches[1]
org = matches[2]
repo = matches[3]
subdir = matches[4]
version = matches[5]
case gitSSHWithPathRegex.MatchString(p):
matches := gitSSHWithPathRegex.FindStringSubmatch(p)
host = matches[1]
org = matches[2]
repo = matches[3]
subdir = matches[4]
case gitSSHWithVersionRegex.MatchString(p):
matches := gitSSHWithVersionRegex.FindStringSubmatch(p)
host = matches[1]
org = matches[2]
repo = matches[3]
version = matches[4]
default:
matches := gitSSHRegex.FindStringSubmatch(p)
host = matches[1]
org = matches[2]
repo = matches[3]
}
return &spec.Dependency{
Name: repo,
Source: spec.Source{
GitSource: &spec.GitSource{
Remote: fmt.Sprintf("git@%s:%s/%s", host, org, repo),
Subdir: subdir,
},
},
Version: version,
}
}
func parseGithubDependency(p string) *spec.Dependency {
if !githubSlugRegex.MatchString(p) {
return nil
}
name := ""
user := ""
repo := ""
subdir := ""
version := "master"
if githubSlugWithPathRegex.MatchString(p) {
if githubSlugWithPathAndVersionRegex.MatchString(p) {
matches := githubSlugWithPathAndVersionRegex.FindStringSubmatch(p)
user = matches[1]
repo = matches[2]
subdir = matches[3]
version = matches[4]
name = path.Base(subdir)
} else {
matches := githubSlugWithPathRegex.FindStringSubmatch(p)
user = matches[1]
repo = matches[2]
subdir = matches[3]
name = path.Base(subdir)
}
} else {
if githubSlugWithVersionRegex.MatchString(p) {
matches := githubSlugWithVersionRegex.FindStringSubmatch(p)
user = matches[1]
repo = matches[2]
name = repo
version = matches[3]
} else {
matches := githubSlugRegex.FindStringSubmatch(p)
user = matches[1]
repo = matches[2]
name = repo
}
}
return &spec.Dependency{
Name: name,
Source: spec.Source{
GitSource: &spec.GitSource{
Remote: fmt.Sprintf("https://github.com/%s/%s", user, repo),
Subdir: subdir,
},
},
Version: version,
}
}
func parseLocalDependency(dir, p string) *spec.Dependency {
clean := filepath.Clean(p)
abs := filepath.Join(dir, clean)
info, err := os.Stat(abs)
if err != nil {
return nil
}
if !info.IsDir() {
return nil
}
return &spec.Dependency{
Name: info.Name(),
Source: spec.Source{
LocalSource: &spec.LocalSource{
Directory: clean,
},
},
Version: "",
}
}

View file

@ -20,7 +20,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/jsonnet-bundler/jsonnet-bundler/spec"
"github.com/jsonnet-bundler/jsonnet-bundler/spec/deps"
)
func TestParseDependency(t *testing.T) {
@ -34,7 +34,7 @@ func TestParseDependency(t *testing.T) {
tests := []struct {
name string
path string
want *spec.Dependency
want *deps.Dependency
}{
{
name: "Empty",
@ -49,11 +49,13 @@ func TestParseDependency(t *testing.T) {
{
name: "GitHub",
path: "github.com/jsonnet-bundler/jsonnet-bundler",
want: &spec.Dependency{
Name: "jsonnet-bundler",
Source: spec.Source{
GitSource: &spec.GitSource{
Remote: "https://github.com/jsonnet-bundler/jsonnet-bundler",
want: &deps.Dependency{
Source: deps.Source{
GitSource: &deps.Git{
Scheme: deps.GitSchemeHTTPS,
Host: "github.com",
User: "jsonnet-bundler",
Repo: "jsonnet-bundler",
Subdir: "",
},
},
@ -63,11 +65,13 @@ func TestParseDependency(t *testing.T) {
{
name: "SSH",
path: "git+ssh://git@github.com:jsonnet-bundler/jsonnet-bundler.git",
want: &spec.Dependency{
Name: "jsonnet-bundler",
Source: spec.Source{
GitSource: &spec.GitSource{
Remote: "git@github.com:jsonnet-bundler/jsonnet-bundler",
want: &deps.Dependency{
Source: deps.Source{
GitSource: &deps.Git{
Scheme: deps.GitSchemeSSH,
Host: "github.com",
User: "jsonnet-bundler",
Repo: "jsonnet-bundler",
Subdir: "",
},
},
@ -77,10 +81,9 @@ func TestParseDependency(t *testing.T) {
{
name: "local",
path: testFolder,
want: &spec.Dependency{
Name: "foobar",
Source: spec.Source{
LocalSource: &spec.LocalSource{
want: &deps.Dependency{
Source: deps.Source{
LocalSource: &deps.Local{
Directory: "test/jsonnet/foobar",
},
},
@ -90,7 +93,7 @@ func TestParseDependency(t *testing.T) {
}
for _, tt := range tests {
_ = t.Run(tt.name, func(t *testing.T) {
dependency := parseDependency("", tt.path)
dependency := deps.Parse("", tt.path)
if tt.path == "" {
assert.Nil(t, dependency)

36
cmd/jb/rewrite.go Normal file
View file

@ -0,0 +1,36 @@
// Copyright 2018 jsonnet-bundler authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"path/filepath"
kingpin "gopkg.in/alecthomas/kingpin.v2"
"github.com/jsonnet-bundler/jsonnet-bundler/pkg/jsonnetfile"
"github.com/jsonnet-bundler/jsonnet-bundler/tool/rewrite"
)
func rewriteCommand(dir, vendorDir string) int {
locks, err := jsonnetfile.Load(filepath.Join(dir, jsonnetfile.LockFile))
if err != nil {
kingpin.Fatalf("Failed to load lockFile: %s.\nThe locks are required to compute the new import names. Make sure to run `jb install` first.", err)
}
if err := rewrite.Rewrite(dir, vendorDir, locks.Dependencies); err != nil {
kingpin.FatalIfError(err, "")
}
return 0
}

View file

@ -24,6 +24,7 @@ import (
"github.com/jsonnet-bundler/jsonnet-bundler/pkg"
"github.com/jsonnet-bundler/jsonnet-bundler/pkg/jsonnetfile"
"github.com/jsonnet-bundler/jsonnet-bundler/spec"
"github.com/jsonnet-bundler/jsonnet-bundler/spec/deps"
)
func updateCommand(dir, jsonnetHome string, urls ...*url.URL) int {
@ -39,7 +40,7 @@ func updateCommand(dir, jsonnetHome string, urls ...*url.URL) int {
"creating vendor folder")
// When updating, locks are ignored.
locks := map[string]spec.Dependency{}
locks := map[string]deps.Dependency{}
locked, err := pkg.Ensure(jsonnetFile, jsonnetHome, locks)
kingpin.FatalIfError(err, "failed to install packages")

View file

@ -33,14 +33,14 @@ import (
"github.com/fatih/color"
"github.com/pkg/errors"
"github.com/jsonnet-bundler/jsonnet-bundler/spec"
"github.com/jsonnet-bundler/jsonnet-bundler/spec/deps"
)
type GitPackage struct {
Source *spec.GitSource
Source *deps.Git
}
func NewGitPackage(source *spec.GitSource) Interface {
func NewGitPackage(source *deps.Git) Interface {
return &GitPackage{
Source: source,
}
@ -169,7 +169,7 @@ func remoteResolveRef(ctx context.Context, remote string, ref string) (string, e
func (p *GitPackage) Install(ctx context.Context, name, dir, version string) (string, error) {
destPath := path.Join(dir, name)
tmpDir, err := ioutil.TempDir(filepath.Join(dir, ".tmp"), fmt.Sprintf("jsonnetpkg-%s-%s", name, version))
tmpDir, err := ioutil.TempDir(filepath.Join(dir, ".tmp"), fmt.Sprintf("jsonnetpkg-%s-%s", strings.Replace(name, "/", "-", -1), version))
if err != nil {
return "", errors.Wrap(err, "failed to create tmp dir")
}
@ -177,11 +177,11 @@ func (p *GitPackage) Install(ctx context.Context, name, dir, version string) (st
// Optimization for GitHub sources: download a tarball archive of the requested
// version instead of cloning the entire repository.
isGitHubRemote, err := regexp.MatchString(`^(https|ssh)://github\.com/.+$`, p.Source.Remote)
isGitHubRemote, err := regexp.MatchString(`^(https|ssh)://github\.com/.+$`, p.Source.Remote())
if isGitHubRemote {
// Let git ls-remote decide if "version" is a ref or a commit SHA in the unlikely
// but possible event that a ref is comprised of 40 or more hex characters
commitSha, err := remoteResolveRef(ctx, p.Source.Remote, version)
commitSha, err := remoteResolveRef(ctx, p.Source.Remote(), version)
// If the ref resolution failed and "version" looks like a SHA,
// assume it is one and proceed.
@ -190,7 +190,7 @@ func (p *GitPackage) Install(ctx context.Context, name, dir, version string) (st
commitSha = version
}
archiveUrl := fmt.Sprintf("%s/archive/%s.tar.gz", p.Source.Remote, commitSha)
archiveUrl := fmt.Sprintf("%s/archive/%s.tar.gz", p.Source.Remote(), commitSha)
archiveFilepath := fmt.Sprintf("%s.tar.gz", tmpDir)
defer os.Remove(archiveFilepath)
@ -205,7 +205,12 @@ func (p *GitPackage) Install(ctx context.Context, name, dir, version string) (st
// Move the extracted directory to its final destination
if err == nil {
err = os.Rename(path.Join(tmpDir, p.Source.Subdir), destPath)
if err := os.MkdirAll(filepath.Dir(destPath), os.ModePerm); err != nil {
panic(err)
}
if err := os.Rename(path.Join(tmpDir, p.Source.Subdir), destPath); err != nil {
panic(err)
}
}
}
}
@ -230,7 +235,7 @@ func (p *GitPackage) Install(ctx context.Context, name, dir, version string) (st
return "", err
}
cmd = exec.CommandContext(ctx, "git", "remote", "add", "origin", p.Source.Remote)
cmd = exec.CommandContext(ctx, "git", "remote", "add", "origin", p.Source.Remote())
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

View file

@ -22,6 +22,7 @@ import (
"github.com/pkg/errors"
"github.com/jsonnet-bundler/jsonnet-bundler/spec"
"github.com/jsonnet-bundler/jsonnet-bundler/spec/deps"
)
const (
@ -52,7 +53,7 @@ func Unmarshal(bytes []byte) (spec.JsonnetFile, error) {
return m, errors.Wrap(err, "failed to unmarshal file")
}
if m.Dependencies == nil {
m.Dependencies = make(map[string]spec.Dependency)
m.Dependencies = make(map[string]deps.Dependency)
}
return m, nil

View file

@ -24,17 +24,17 @@ import (
"github.com/jsonnet-bundler/jsonnet-bundler/pkg/jsonnetfile"
"github.com/jsonnet-bundler/jsonnet-bundler/spec"
"github.com/jsonnet-bundler/jsonnet-bundler/spec/deps"
)
const notExist = "/this/does/not/exist"
func TestLoad(t *testing.T) {
empty := spec.New()
jsonnetfileContent := `{
jsonnetfileContent := `
{
"legacyImports": false,
"dependencies": [
{
"name": "foobar",
"source": {
"git": {
"remote": "https://github.com/foobar/foobar",
@ -46,62 +46,61 @@ func TestLoad(t *testing.T) {
]
}
`
jsonnetFileExpected := spec.JsonnetFile{
Dependencies: map[string]spec.Dependency{
"foobar": {
Name: "foobar",
Source: spec.Source{
GitSource: &spec.GitSource{
Remote: "https://github.com/foobar/foobar",
LegacyImports: false,
Dependencies: map[string]deps.Dependency{
"github.com/foobar/foobar": {
Source: deps.Source{
GitSource: &deps.Git{
Scheme: deps.GitSchemeHTTPS,
Host: "github.com",
User: "foobar",
Repo: "foobar",
Subdir: "",
},
},
Version: "master",
DepSource: "",
Version: "master",
}},
}
{
jf, err := jsonnetfile.Load(notExist)
assert.Equal(t, empty, jf)
assert.Error(t, err)
tempDir, err := ioutil.TempDir("", "jb-load-jsonnetfile")
if err != nil {
t.Fatal(err)
}
{
tempDir, err := ioutil.TempDir("", "jb-load-jsonnetfile")
if err != nil {
t.Fatal(err)
}
defer func() {
err := os.RemoveAll(tempDir)
assert.Nil(t, err)
}()
defer os.RemoveAll(tempDir)
tempFile := filepath.Join(tempDir, jsonnetfile.File)
err = ioutil.WriteFile(tempFile, []byte(`{}`), os.ModePerm)
assert.Nil(t, err)
tempFile := filepath.Join(tempDir, jsonnetfile.File)
err = ioutil.WriteFile(tempFile, []byte(jsonnetfileContent), os.ModePerm)
assert.Nil(t, err)
jf, err := jsonnetfile.Load(tempFile)
assert.Nil(t, err)
assert.Equal(t, empty, jf)
jf, err := jsonnetfile.Load(tempFile)
assert.Nil(t, err)
assert.Equal(t, jsonnetFileExpected, jf)
}
func TestLoadEmpty(t *testing.T) {
tempDir, err := ioutil.TempDir("", "jb-load-empty")
if err != nil {
t.Fatal(err)
}
{
tempDir, err := ioutil.TempDir("", "jb-load-jsonnetfile")
if err != nil {
t.Fatal(err)
}
defer func() {
err := os.RemoveAll(tempDir)
assert.Nil(t, err)
}()
defer os.RemoveAll(tempDir)
tempFile := filepath.Join(tempDir, jsonnetfile.File)
err = ioutil.WriteFile(tempFile, []byte(jsonnetfileContent), os.ModePerm)
assert.Nil(t, err)
// write empty json file
tempFile := filepath.Join(tempDir, jsonnetfile.File)
err = ioutil.WriteFile(tempFile, []byte(`{}`), os.ModePerm)
assert.Nil(t, err)
jf, err := jsonnetfile.Load(tempFile)
assert.Nil(t, err)
assert.Equal(t, jsonnetFileExpected, jf)
}
// expect it to be loaded properly
got, err := jsonnetfile.Load(tempFile)
assert.Nil(t, err)
assert.Equal(t, spec.New(), got)
}
func TestLoadNotExist(t *testing.T) {
jf, err := jsonnetfile.Load(notExist)
assert.Equal(t, spec.New(), jf)
assert.Error(t, err)
}
func TestFileExists(t *testing.T) {

View file

@ -19,15 +19,16 @@ import (
"os"
"path/filepath"
"github.com/jsonnet-bundler/jsonnet-bundler/spec"
"github.com/pkg/errors"
"github.com/jsonnet-bundler/jsonnet-bundler/spec/deps"
)
type LocalPackage struct {
Source *spec.LocalSource
Source *deps.Local
}
func NewLocalPackage(source *spec.LocalSource) Interface {
func NewLocalPackage(source *deps.Local) Interface {
return &LocalPackage{
Source: source,
}

View file

@ -22,19 +22,21 @@ import (
"io"
"os"
"path/filepath"
"strings"
"github.com/fatih/color"
"github.com/pkg/errors"
"github.com/jsonnet-bundler/jsonnet-bundler/pkg/jsonnetfile"
"github.com/jsonnet-bundler/jsonnet-bundler/spec"
"github.com/jsonnet-bundler/jsonnet-bundler/spec/deps"
)
var (
VersionMismatch = errors.New("multiple colliding versions specified")
)
// Ensure receives all direct packages as, the directory to vendor in and all known locks.
// Ensure receives all direct packages, the directory to vendor into and all known locks.
// It then makes sure all direct and nested dependencies are present in vendor at the correct version:
//
// If the package is locked and the files in vendor match the sha256 checksum,
@ -46,57 +48,183 @@ var (
// desired version in case by `jb install`ing it.
//
// Finally, all unknown files and directories are removed from vendor/
func Ensure(direct spec.JsonnetFile, vendorDir string, locks map[string]spec.Dependency) (map[string]spec.Dependency, error) {
// The full list of locked depedencies is returned
func Ensure(direct spec.JsonnetFile, vendorDir string, oldLocks map[string]deps.Dependency) (map[string]deps.Dependency, error) {
// ensure all required files are in vendor
deps, err := ensure(direct.Dependencies, vendorDir, locks)
// This is the actual installation
locks, err := ensure(direct.Dependencies, vendorDir, oldLocks)
if err != nil {
return nil, err
}
// cleanup unknown dirs from vendor/
f, err := os.Open(vendorDir)
if err != nil {
return nil, err
}
names, err := f.Readdirnames(0)
if err != nil {
return nil, err
}
for _, name := range names {
if _, ok := deps[name]; !ok {
dir := filepath.Join(vendorDir, name)
// remove unchanged legacyNames
CleanLegacyName(locks)
// find unknown dirs in vendor/
names := []string{}
err = filepath.Walk(vendorDir, func(path string, i os.FileInfo, err error) error {
if path == vendorDir {
return nil
}
if !i.IsDir() {
return nil
}
names = append(names, path)
return nil
})
// remove them
for _, dir := range names {
name := strings.TrimPrefix(dir, "vendor/")
if !known(locks, name) {
if err := os.RemoveAll(dir); err != nil {
return nil, err
}
if name != ".tmp" {
if !strings.HasPrefix(name, ".tmp") {
color.Magenta("CLEAN %s", dir)
}
}
}
// remove all symlinks, optionally adding known ones back later if wished
if err := cleanLegacySymlinks(vendorDir, locks); err != nil {
return nil, err
}
if !direct.LegacyImports {
return locks, nil
}
if err := linkLegacy(vendorDir, locks); err != nil {
return nil, err
}
// return the final lockfile contents
return deps, nil
return locks, nil
}
func ensure(direct map[string]spec.Dependency, vendorDir string, locks map[string]spec.Dependency) (map[string]spec.Dependency, error) {
deps := make(map[string]spec.Dependency)
func CleanLegacyName(list map[string]deps.Dependency) {
for k, d := range list {
// unset if not changed by user
if d.LegacyNameCompat == d.Source.LegacyName() {
dep := list[k]
dep.LegacyNameCompat = ""
list[k] = dep
}
}
}
func cleanLegacySymlinks(vendorDir string, locks map[string]deps.Dependency) error {
// local packages need to be ignored
locals := map[string]bool{}
for _, d := range locks {
if d.Source.LocalSource == nil {
continue
}
locals[filepath.Join(vendorDir, d.Name())] = true
}
// remove all symlinks first
return filepath.Walk(vendorDir, func(path string, i os.FileInfo, err error) error {
if locals[path] {
return nil
}
if i.Mode()&os.ModeSymlink != 0 {
if err := os.Remove(path); err != nil {
return err
}
}
return nil
})
}
func linkLegacy(vendorDir string, locks map[string]deps.Dependency) error {
// create only the ones we want
for _, d := range locks {
// localSource still uses the relative style
if d.Source.LocalSource != nil {
continue
}
legacyName := filepath.Join("vendor", d.LegacyName())
pkgName := d.Name()
taken, err := checkLegacyNameTaken(legacyName, pkgName)
if err != nil {
fmt.Println(err)
continue
}
if taken {
continue
}
// create the symlink
if err := os.Symlink(
filepath.Join(pkgName),
filepath.Join(legacyName),
); err != nil {
return err
}
}
return nil
}
func checkLegacyNameTaken(legacyName string, pkgName string) (bool, error) {
fi, err := os.Lstat(legacyName)
if err != nil {
// does not exist: not taken
if os.IsNotExist(err) {
return false, nil
}
// a real error
return false, err
}
// is it a symlink?
if fi.Mode()&os.ModeSymlink != 0 {
s, err := os.Readlink(legacyName)
if err != nil {
return false, err
}
color.Yellow("WARN: cannot link '%s' to '%s', because package '%s' already uses that name. The absolute import still works\n", pkgName, legacyName, s)
return true, nil
}
// sth else
color.Yellow("WARN: cannot link '%s' to '%s', because the file/directory already exists. The absolute import still works.\n", pkgName, legacyName)
return true, nil
}
func known(deps map[string]deps.Dependency, p string) bool {
for _, d := range deps {
k := d.Name()
if strings.HasPrefix(p, k) || strings.HasPrefix(k, p) {
return true
}
}
return false
}
func ensure(direct map[string]deps.Dependency, vendorDir string, locks map[string]deps.Dependency) (map[string]deps.Dependency, error) {
deps := make(map[string]deps.Dependency)
for _, d := range direct {
l, present := locks[d.Name]
l, present := locks[d.Name()]
// already locked and the integrity is intact
if present {
d.Version = locks[d.Name].Version
d.Version = locks[d.Name()].Version
if check(l, vendorDir) {
deps[d.Name] = l
deps[d.Name()] = l
continue
}
}
expectedSum := locks[d.Name].Sum
expectedSum := locks[d.Name()].Sum
// either not present or not intact: download again
dir := filepath.Join(vendorDir, d.Name)
dir := filepath.Join(vendorDir, d.Name())
os.RemoveAll(dir)
locked, err := download(d, vendorDir)
@ -104,15 +232,15 @@ func ensure(direct map[string]spec.Dependency, vendorDir string, locks map[strin
return nil, errors.Wrap(err, "downloading")
}
if expectedSum != "" && locked.Sum != expectedSum {
return nil, fmt.Errorf("checksum mismatch for %s. Expected %s but got %s", d.Name, expectedSum, locked.Sum)
return nil, fmt.Errorf("checksum mismatch for %s. Expected %s but got %s", d.Name(), expectedSum, locked.Sum)
}
deps[d.Name] = *locked
deps[d.Name()] = *locked
// we settled on a new version, add it to the locks for recursion
locks[d.Name] = *locked
locks[d.Name()] = *locked
}
for _, d := range deps {
f, err := jsonnetfile.Load(filepath.Join(vendorDir, d.Name, jsonnetfile.File))
f, err := jsonnetfile.Load(filepath.Join(vendorDir, d.Name(), jsonnetfile.File))
if err != nil {
if os.IsNotExist(err) {
continue
@ -126,8 +254,8 @@ func ensure(direct map[string]spec.Dependency, vendorDir string, locks map[strin
}
for _, d := range nested {
if _, ok := deps[d.Name]; !ok {
deps[d.Name] = d
if _, ok := deps[d.Name()]; !ok {
deps[d.Name()] = d
}
}
}
@ -137,7 +265,7 @@ func ensure(direct map[string]spec.Dependency, vendorDir string, locks map[strin
// download retrieves a package from a remote upstream. The checksum of the
// files is generated afterwards.
func download(d spec.Dependency, vendorDir string) (*spec.Dependency, error) {
func download(d deps.Dependency, vendorDir string) (*deps.Dependency, error) {
var p Interface
switch {
case d.Source.GitSource != nil:
@ -150,32 +278,29 @@ func download(d spec.Dependency, vendorDir string) (*spec.Dependency, error) {
return nil, errors.New("either git or local source is required")
}
version, err := p.Install(context.TODO(), d.Name, vendorDir, d.Version)
version, err := p.Install(context.TODO(), d.Name(), vendorDir, d.Version)
if err != nil {
return nil, err
}
var sum string
if d.Source.LocalSource == nil {
sum = hashDir(filepath.Join(vendorDir, d.Name))
sum = hashDir(filepath.Join(vendorDir, d.Name()))
}
return &spec.Dependency{
Name: d.Name,
Source: d.Source,
Version: version,
Sum: sum,
}, nil
d.Version = version
d.Sum = sum
return &d, nil
}
// check returns whether the files present at the vendor/ folder match the
// sha256 sum of the package. local-directory dependencies are not checked as
// their purpose is to change during development where integrity checking would
// be a hindrance.
func check(d spec.Dependency, vendorDir string) bool {
func check(d deps.Dependency, vendorDir string) bool {
// assume a local dependency is intact as long as it exists
if d.Source.LocalSource != nil {
x, err := jsonnetfile.Exists(filepath.Join(vendorDir, d.Name))
x, err := jsonnetfile.Exists(filepath.Join(vendorDir, d.Name()))
if err != nil {
return false
}
@ -187,7 +312,7 @@ func check(d spec.Dependency, vendorDir string) bool {
return false
}
dir := filepath.Join(vendorDir, d.Name)
dir := filepath.Join(vendorDir, d.Name())
sum := hashDir(dir)
return d.Sum == sum
}

View file

@ -13,3 +13,83 @@
// limitations under the License.
package pkg
import (
"testing"
"github.com/jsonnet-bundler/jsonnet-bundler/spec/deps"
)
func TestKnown(t *testing.T) {
deps := map[string]deps.Dependency{
"ksonnet-lib": deps.Dependency{
Source: deps.Source{GitSource: &deps.Git{
Scheme: deps.GitSchemeHTTPS,
Host: "github.com",
User: "ksonnet",
Repo: "ksonnet-lib",
Subdir: "/ksonnet.beta.4",
}},
},
}
paths := []string{
"github.com",
"github.com/ksonnet",
"github.com/ksonnet/ksonnet-lib",
"github.com/ksonnet/ksonnet-lib/ksonnet.beta.4",
"github.com/ksonnet/ksonnet-lib/ksonnet.beta.4/k.libsonnet",
"github.com/ksonnet-util", // don't know that one
"ksonnet.beta.4", // the symlink
}
want := []string{
"github.com",
"github.com/ksonnet",
"github.com/ksonnet/ksonnet",
"github.com/ksonnet/ksonnet-lib",
"github.com/ksonnet/ksonnet-lib/ksonnet.beta.4",
"github.com/ksonnet/ksonnet-lib/ksonnet.beta.4/k.libsonnet",
}
w := make(map[string]bool)
for _, k := range want {
w[k] = true
}
for _, p := range paths {
if known(deps, p) != w[p] {
t.Fatalf("expected %s to be %v", p, w[p])
}
}
}
func TestCleanLegacyName(t *testing.T) {
deps := func(name string) map[string]deps.Dependency {
return map[string]deps.Dependency{
"ksonnet-lib": deps.Dependency{
LegacyNameCompat: name,
Source: deps.Source{GitSource: &deps.Git{
Scheme: deps.GitSchemeHTTPS,
Host: "github.com",
User: "ksonnet",
Repo: "ksonnet-lib",
Subdir: "/ksonnet.beta.4",
}},
},
}
}
cases := map[string]bool{
"ksonnet": false,
"ksonnet.beta.4": true,
}
for name, want := range cases {
list := deps(name)
CleanLegacyName(list)
if (list["ksonnet-lib"].LegacyNameCompat == "") != want {
t.Fatalf("expected `%s` to be removed: %v", name, want)
}
}
}

106
spec/deps/dependencies.go Normal file
View file

@ -0,0 +1,106 @@
// Copyright 2018 jsonnet-bundler authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package deps
import (
"os"
"path/filepath"
)
type Dependency struct {
Source Source `json:"source"`
Version string `json:"version"`
Sum string `json:"sum,omitempty"`
// older schema used to have `name`. We still need that data for
// `LegacyName`
LegacyNameCompat string `json:"name,omitempty"`
}
func Parse(dir, uri string) *Dependency {
if uri == "" {
return nil
}
if d := parseGit(uri); d != nil {
return d
}
return parseLocal(dir, uri)
}
func (d Dependency) Name() string {
return d.Source.Name()
}
func (d Dependency) LegacyName() string {
if d.LegacyNameCompat != "" {
return d.LegacyNameCompat
}
return d.Source.LegacyName()
}
type Source struct {
GitSource *Git `json:"git,omitempty"`
LocalSource *Local `json:"local,omitempty"`
}
func (s Source) Name() string {
switch {
case s.GitSource != nil:
return s.GitSource.Name()
case s.LocalSource != nil:
return s.LegacyName()
default:
return ""
}
}
func (s Source) LegacyName() string {
switch {
case s.GitSource != nil:
return s.GitSource.LegacyName()
case s.LocalSource != nil:
return filepath.Base(s.LocalSource.Directory)
default:
return ""
}
}
type Local struct {
Directory string `json:"directory"`
}
func parseLocal(dir, p string) *Dependency {
clean := filepath.Clean(p)
abs := filepath.Join(dir, clean)
info, err := os.Stat(abs)
if err != nil {
return nil
}
if !info.IsDir() {
return nil
}
return &Dependency{
Source: Source{
LocalSource: &Local{
Directory: clean,
},
},
Version: "",
}
}

View file

@ -0,0 +1,70 @@
// Copyright 2018 jsonnet-bundler authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package deps
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseDependency(t *testing.T) {
const testFolder = "test/jsonnet/foobar"
err := os.MkdirAll(testFolder, os.ModePerm)
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll("test")
tests := []struct {
name string
path string
want *Dependency
}{
{
name: "Empty",
path: "",
want: nil,
},
{
name: "Invalid",
path: "github.com/foo",
want: nil,
},
{
name: "local",
path: testFolder,
want: &Dependency{
Source: Source{
LocalSource: &Local{
Directory: "test/jsonnet/foobar",
},
},
Version: "",
},
},
}
for _, tt := range tests {
_ = t.Run(tt.name, func(t *testing.T) {
dependency := Parse("", tt.path)
if tt.path == "" {
assert.Nil(t, dependency)
} else {
assert.Equal(t, tt.want, dependency)
}
})
}
}

203
spec/deps/git.go Normal file
View file

@ -0,0 +1,203 @@
// Copyright 2018 jsonnet-bundler authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package deps
import (
"encoding/json"
"fmt"
"path/filepath"
"regexp"
"strings"
)
const (
GitSchemeSSH = "ssh://git@"
GitSchemeHTTPS = "https://"
)
// Git holds all required information for cloning a package from git
type Git struct {
// Scheme (Protocol) used (https, git+ssh)
Scheme string
// Hostname the repo is located at
Host string
// User (github.com/<user>)
User string
// Repo (github.com/<user>/<repo>)
Repo string
// Subdir (github.com/<user>/<repo>/<subdir>)
Subdir string
}
// json representation of Git (for compatiblity with old format)
type jsonGit struct {
Remote string `json:"remote"`
Subdir string `json:"subdir"`
}
// MarshalJSON takes care of translating between Git and jsonGit
func (gs *Git) MarshalJSON() ([]byte, error) {
j := jsonGit{
Remote: gs.Remote(),
Subdir: strings.TrimPrefix(gs.Subdir, "/"),
}
return json.Marshal(j)
}
// UnmarshalJSON takes care of translating between Git and jsonGit
func (gs *Git) UnmarshalJSON(data []byte) error {
var j jsonGit
if err := json.Unmarshal(data, &j); err != nil {
return err
}
if j.Subdir != "" {
gs.Subdir = "/" + strings.TrimPrefix(j.Subdir, "/")
}
tmp := parseGit(j.Remote)
gs.Host = tmp.Source.GitSource.Host
gs.User = tmp.Source.GitSource.User
gs.Repo = tmp.Source.GitSource.Repo
gs.Scheme = tmp.Source.GitSource.Scheme
return nil
}
// Name returns the repository in a go-like format (github.com/user/repo/subdir)
func (gs *Git) Name() string {
return fmt.Sprintf("%s/%s/%s%s", gs.Host, gs.User, gs.Repo, gs.Subdir)
}
// LegacyName returns the last element of the packages path
// example: github.com/ksonnet/ksonnet-lib/ksonnet.beta.4 becomes ksonnet.beta.4
func (gs *Git) LegacyName() string {
return filepath.Base(gs.Repo + gs.Subdir)
}
var gitProtoFmts = map[string]string{
GitSchemeSSH: GitSchemeSSH + "%s:%s/%s.git",
GitSchemeHTTPS: GitSchemeHTTPS + "%s/%s/%s",
}
// Remote returns a remote string that can be passed to git
func (gs *Git) Remote() string {
return fmt.Sprintf(gitProtoFmts[gs.Scheme],
gs.Host, gs.User, gs.Repo,
)
}
// regular expressions for matching package uris
const (
gitSSHExp = `git\+ssh://git@(?P<host>[^:]+):(?P<user>[^/]+)/(?P<repo>[^/]+).git`
githubSlugExp = `github.com/(?P<user>[-_a-zA-Z0-9]+)/(?P<repo>[-_a-zA-Z0-9]+)`
)
var (
gitSSHRegex = regexp.MustCompile(gitSSHExp)
gitSSHWithVersionRegex = regexp.MustCompile(gitSSHExp + `@(?P<version>.*)`)
gitSSHWithPathRegex = regexp.MustCompile(gitSSHExp + `/(?P<subdir>.*)`)
gitSSHWithPathAndVersionRegex = regexp.MustCompile(gitSSHExp + `/(?P<subdir>.*)@(?P<version>.*)`)
githubSlugRegex = regexp.MustCompile(githubSlugExp)
githubSlugWithVersionRegex = regexp.MustCompile(githubSlugExp + `@(?P<version>.*)`)
githubSlugWithPathRegex = regexp.MustCompile(githubSlugExp + `/(?P<subdir>.*)`)
githubSlugWithPathAndVersionRegex = regexp.MustCompile(githubSlugExp + `/(?P<subdir>.*)@(?P<version>.*)`)
)
func parseGit(uri string) *Dependency {
var d = Dependency{
Version: "master",
Source: Source{},
}
var gs *Git
var version string
switch {
case githubSlugRegex.MatchString(uri):
gs, version = parseGitHub(uri)
case gitSSHRegex.MatchString(uri):
gs, version = parseGitSSH(uri)
default:
return nil
}
if gs.Subdir != "" {
gs.Subdir = "/" + gs.Subdir
}
d.Source.GitSource = gs
if version != "" {
d.Version = version
}
return &d
}
func parseGitSSH(p string) (gs *Git, version string) {
gs, version = match(p, []*regexp.Regexp{
gitSSHWithPathAndVersionRegex,
gitSSHWithPathRegex,
gitSSHWithVersionRegex,
gitSSHRegex,
})
gs.Scheme = GitSchemeSSH
return gs, version
}
func parseGitHub(p string) (gs *Git, version string) {
gs, version = match(p, []*regexp.Regexp{
githubSlugWithPathAndVersionRegex,
githubSlugWithPathRegex,
githubSlugWithVersionRegex,
githubSlugRegex,
})
gs.Scheme = GitSchemeHTTPS
gs.Host = "github.com"
return gs, version
}
func match(p string, exps []*regexp.Regexp) (gs *Git, version string) {
gs = &Git{}
for _, e := range exps {
if !e.MatchString(p) {
continue
}
matches := reSubMatchMap(e, p)
gs.Host = matches["host"]
gs.User = matches["user"]
gs.Repo = matches["repo"]
if sd, ok := matches["subdir"]; ok {
gs.Subdir = sd
}
return gs, matches["version"]
}
return gs, ""
}
func reSubMatchMap(r *regexp.Regexp, str string) map[string]string {
match := r.FindStringSubmatch(str)
subMatchMap := make(map[string]string)
for i, name := range r.SubexpNames() {
if i != 0 {
subMatchMap[name] = match[i]
}
}
return subMatchMap
}

68
spec/deps/git_test.go Normal file
View file

@ -0,0 +1,68 @@
// Copyright 2018 jsonnet-bundler authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package deps
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseGit(t *testing.T) {
tests := []struct {
name string
uri string
want *Dependency
wantRemote string
}{
{
name: "GitHub",
uri: "github.com/ksonnet/ksonnet-lib/ksonnet.beta.3",
want: &Dependency{
Version: "master",
Source: Source{GitSource: &Git{
Scheme: GitSchemeHTTPS,
Host: "github.com",
User: "ksonnet",
Repo: "ksonnet-lib",
Subdir: "/ksonnet.beta.3",
}},
},
wantRemote: "https://github.com/ksonnet/ksonnet-lib",
},
{
name: "SSH",
uri: "git+ssh://git@my.host:user/repo.git/foobar@v1",
want: &Dependency{
Version: "v1",
Source: Source{GitSource: &Git{
Scheme: GitSchemeSSH,
Host: "my.host",
User: "user",
Repo: "repo",
Subdir: "/foobar",
}},
},
wantRemote: "ssh://git@my.host:user/repo.git",
},
}
for _, c := range tests {
t.Run(c.name, func(t *testing.T) {
got := Parse("", c.uri)
assert.Equal(t, c.want, got)
assert.Equal(t, c.wantRemote, got.Source.GitSource.Remote())
})
}
}

View file

@ -17,73 +17,67 @@ package spec
import (
"encoding/json"
"sort"
"github.com/jsonnet-bundler/jsonnet-bundler/spec/deps"
)
// JsonnetFile is the structure of a `.json` file describing a set of jsonnet
// dependencies. It is used for both, the jsonnetFile and the lockFile.
type JsonnetFile struct {
Dependencies map[string]Dependency
// List of dependencies
Dependencies map[string]deps.Dependency
// Symlink files to old location
LegacyImports bool
}
// New returns a new JsonnetFile with the dependencies map initialized
func New() JsonnetFile {
return JsonnetFile{
Dependencies: make(map[string]Dependency),
Dependencies: make(map[string]deps.Dependency),
LegacyImports: true,
}
}
// jsonFile is the json representation of a JsonnetFile, which is different for
// compatibility reasons.
type jsonFile struct {
Dependencies []Dependency `json:"dependencies"`
Dependencies []deps.Dependency `json:"dependencies"`
LegacyImports bool `json:"legacyImports"`
}
// UnmarshalJSON unmarshals a `jsonFile`'s json into a JsonnetFile
func (jf *JsonnetFile) UnmarshalJSON(data []byte) error {
var s jsonFile
s.LegacyImports = jf.LegacyImports // adpot default
if err := json.Unmarshal(data, &s); err != nil {
return err
}
jf.Dependencies = make(map[string]Dependency)
jf.Dependencies = make(map[string]deps.Dependency)
for _, d := range s.Dependencies {
jf.Dependencies[d.Name] = d
jf.Dependencies[d.Name()] = d
}
jf.LegacyImports = s.LegacyImports
return nil
}
// MarshalJSON serializes a JsonnetFile into json of the format of a `jsonFile`
func (jf JsonnetFile) MarshalJSON() ([]byte, error) {
var s jsonFile
s.LegacyImports = jf.LegacyImports
for _, d := range jf.Dependencies {
s.Dependencies = append(s.Dependencies, d)
}
sort.SliceStable(s.Dependencies, func(i int, j int) bool {
return s.Dependencies[i].Name < s.Dependencies[j].Name
return s.Dependencies[i].Name() < s.Dependencies[j].Name()
})
if s.Dependencies == nil {
s.Dependencies = make([]deps.Dependency, 0, 0)
}
return json.Marshal(s)
}
type Dependency struct {
Name string `json:"name"`
Source Source `json:"source"`
Version string `json:"version"`
Sum string `json:"sum,omitempty"`
DepSource string `json:"-"`
}
type Source struct {
GitSource *GitSource `json:"git,omitempty"`
LocalSource *LocalSource `json:"local,omitempty"`
}
type GitSource struct {
Remote string `json:"remote"`
Subdir string `json:"subdir"`
}
type LocalSource struct {
Directory string `json:"directory"`
}

View file

@ -18,6 +18,7 @@ import (
"encoding/json"
"testing"
"github.com/jsonnet-bundler/jsonnet-bundler/spec/deps"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -25,7 +26,6 @@ import (
const jsonJF = `{
"dependencies": [
{
"name": "grafana-builder",
"source": {
"git": {
"remote": "https://github.com/grafana/jsonnet-libs",
@ -36,7 +36,7 @@ const jsonJF = `{
"sum": "ELsYwK+kGdzX1mee2Yy+/b2mdO4Y503BOCDkFzwmGbE="
},
{
"name": "prometheus-mixin",
"name": "prometheus",
"source": {
"git": {
"remote": "https://github.com/prometheus/prometheus",
@ -46,29 +46,36 @@ const jsonJF = `{
"version": "7c039a6b3b4b2a9d7c613ac8bd3fc16e8ca79684",
"sum": "bVGOsq3hLOw2irNPAS91a5dZJqQlBUNWy3pVwM4+kIY="
}
]
],
"legacyImports": false
}`
func testData() JsonnetFile {
return JsonnetFile{
Dependencies: map[string]Dependency{
"grafana-builder": {
Name: "grafana-builder",
Source: Source{
GitSource: &GitSource{
Remote: "https://github.com/grafana/jsonnet-libs",
Subdir: "grafana-builder",
LegacyImports: false,
Dependencies: map[string]deps.Dependency{
"github.com/grafana/jsonnet-libs/grafana-builder": {
Source: deps.Source{
GitSource: &deps.Git{
Scheme: deps.GitSchemeHTTPS,
Host: "github.com",
User: "grafana",
Repo: "jsonnet-libs",
Subdir: "/grafana-builder",
},
},
Version: "54865853ebc1f901964e25a2e7a0e4d2cb6b9648",
Sum: "ELsYwK+kGdzX1mee2Yy+/b2mdO4Y503BOCDkFzwmGbE=",
},
"prometheus-mixin": {
Name: "prometheus-mixin",
Source: Source{
GitSource: &GitSource{
Remote: "https://github.com/prometheus/prometheus",
Subdir: "documentation/prometheus-mixin",
"github.com/prometheus/prometheus/documentation/prometheus-mixin": {
LegacyNameCompat: "prometheus",
Source: deps.Source{
GitSource: &deps.Git{
Scheme: deps.GitSchemeHTTPS,
Host: "github.com",
User: "prometheus",
Repo: "prometheus",
Subdir: "/documentation/prometheus-mixin",
},
},
Version: "7c039a6b3b4b2a9d7c613ac8bd3fc16e8ca79684",

127
tool/rewrite/rewrite.go Normal file
View file

@ -0,0 +1,127 @@
// Copyright 2018 jsonnet-bundler authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package rewrite provides a tool that automatically rewrites legacy imports to
// absolute ones
package rewrite
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/jsonnet-bundler/jsonnet-bundler/spec/deps"
)
var expr = regexp.MustCompile(`(?mU)(import ["'])(.*)(\/.*["'])`)
// Rewrite changes all imports in `dir` from legacy to absolute style
// All files in `vendorDir` are ignored
func Rewrite(dir, vendorDir string, packages map[string]deps.Dependency) error {
imports := make(map[string]string)
for _, p := range packages {
if p.LegacyName() == p.Name() {
continue
}
imports[p.LegacyName()] = p.Name()
}
vendorFi, err := os.Stat(filepath.Join(dir, vendorDir))
if err != nil {
return err
}
// list all Jsonnet files
files := []string{}
if err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if os.SameFile(vendorFi, info) {
return filepath.SkipDir
}
if ext := filepath.Ext(path); ext == ".jsonnet" || ext == ".libsonnet" {
files = append(files, path)
}
return nil
}); err != nil {
return err
}
// change the imports
for _, s := range files {
if err := replaceFile(s, imports); err != nil {
return err
}
}
return nil
}
func wrap(s, q string) string {
return fmt.Sprintf(`import %s%s`, q, s)
}
func replaceFile(name string, imports map[string]string) error {
raw, err := ioutil.ReadFile(name)
if err != nil {
return err
}
out := replace(string(raw), imports)
return ioutil.WriteFile(name, out, 0644)
}
func replace(data string, imports map[string]string) []byte {
contents := strings.Split(string(data), "\n")
// try to fix imports line by line
buf := make([]string, 0, len(contents))
for _, line := range contents {
match := expr.FindStringSubmatch(line)
// no import in this line: push unmodified
if len(match) == 0 {
buf = append(buf, line)
continue
}
// the legacyName
matchedName := match[2]
replaced := false
for legacy, absolute := range imports {
// not this import
if matchedName != legacy {
continue
}
// fix the import
replaced = true
buf = append(buf, expr.ReplaceAllString(line, "${1}"+absolute+"${3}"))
}
// no matching known import found? push unmodified
if !replaced {
buf = append(buf, line)
}
}
return []byte(strings.Join(buf, "\n"))
}

View file

@ -0,0 +1,76 @@
// Copyright 2018 jsonnet-bundler authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package rewrite
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/jsonnet-bundler/jsonnet-bundler/spec/deps"
)
const sample = `
(import "k.libsonnet") + // not vendored
(import "ksonnet/abc.jsonnet") + // prefix of next
(import "ksonnet.beta.4/k.libsonnet") + // normal import
(import "github.com/ksonnet/ksonnet/def.jsonnet") + // already absolute
(import "prometheus/mixin/whatever/abc.libsonnet") + // nested
(import "mylib/foo.libsonnet") + // not managed by jb
// completely unrelated line:
[ "nice" ]
`
const want = `
(import "k.libsonnet") + // not vendored
(import "github.com/ksonnet/ksonnet/abc.jsonnet") + // prefix of next
(import "github.com/ksonnet/ksonnet-lib/ksonnet.beta.4/k.libsonnet") + // normal import
(import "github.com/ksonnet/ksonnet/def.jsonnet") + // already absolute
(import "github.com/prometheus/prometheus/mixin/whatever/abc.libsonnet") + // nested
(import "mylib/foo.libsonnet") + // not managed by jb
// completely unrelated line:
[ "nice" ]
`
func TestRewrite(t *testing.T) {
dir, err := ioutil.TempDir("", "jbrewrite")
require.Nil(t, err)
defer os.RemoveAll(dir)
name := filepath.Join(dir, "test.jsonnet")
err = ioutil.WriteFile(name, []byte(sample), 0644)
require.Nil(t, err)
vendorDir := filepath.Join(dir, "vendor")
err = os.MkdirAll(vendorDir, os.ModePerm)
require.Nil(t, err)
err = Rewrite(dir, "vendor", locks)
require.Nil(t, err)
content, err := ioutil.ReadFile(name)
require.Nil(t, err)
assert.Equal(t, want, string(content))
}
var locks = map[string]deps.Dependency{
"ksonnet": *deps.Parse("", "github.com/ksonnet/ksonnet"),
"ksonnet.beta.4": *deps.Parse("", "github.com/ksonnet/ksonnet-lib/ksonnet.beta.4"),
"prometheus": *deps.Parse("", "github.com/prometheus/prometheus"),
}