Move .pulumi to root of a repository
Now, instead of having a .pulumi folder next to each project, we have a single .pulumi folder in the root of the repository. This is created by running `pulumi init`. When run in a git repository, `pulumi init` will place the .pulumi file next to the .git folder, so it can be shared across all projects in a repository. When not in a git repository, it will be created in the current working directory. We also start tracking information about the repository itself, in a new `repo.json` file stored in the root of the .pulumi folder. The information we track are "owner" and "name" which map to information we use on pulumi.com. When run in a git repository with a remote named origin pointing to a GitHub project, we compute the owner and name by deconstructing information from the remote's URL. Otherwise, we just use the current user's username and the name of the current working directory as the owner and name, respectively.
This commit is contained in:
parent
843ae4a4f6
commit
3f1197ef84
34
Gopkg.lock
generated
34
Gopkg.lock
generated
|
@ -31,12 +31,6 @@
|
||||||
revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
|
revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
|
||||||
version = "v1.0"
|
version = "v1.0"
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/mitchellh/go-homedir"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "b8bc1bf767474819792c23f32d8286a45736f1c6"
|
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
name = "github.com/pkg/errors"
|
name = "github.com/pkg/errors"
|
||||||
packages = ["."]
|
packages = ["."]
|
||||||
|
@ -55,6 +49,12 @@
|
||||||
packages = ["."]
|
packages = ["."]
|
||||||
revision = "2ab6b7470a54bfa9b5b0289f9b4e8fc4839838f7"
|
revision = "2ab6b7470a54bfa9b5b0289f9b4e8fc4839838f7"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/sergi/go-diff"
|
||||||
|
packages = ["diffmatchpatch"]
|
||||||
|
revision = "feef008d51ad2b3778f85d387ccf91735543008d"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
name = "github.com/spf13/cobra"
|
name = "github.com/spf13/cobra"
|
||||||
|
@ -67,6 +67,12 @@
|
||||||
revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66"
|
revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66"
|
||||||
version = "v1.0.0"
|
version = "v1.0.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/src-d/gcfg"
|
||||||
|
packages = [".","scanner","token","types"]
|
||||||
|
revision = "f187355171c936ac84a82793659ebb4936bc1c23"
|
||||||
|
version = "v1.3.0"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
name = "github.com/stretchr/testify"
|
name = "github.com/stretchr/testify"
|
||||||
packages = ["assert"]
|
packages = ["assert"]
|
||||||
|
@ -76,7 +82,7 @@
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
name = "golang.org/x/crypto"
|
name = "golang.org/x/crypto"
|
||||||
packages = ["pbkdf2","ssh/terminal"]
|
packages = ["curve25519","ed25519","ed25519/internal/edwards25519","pbkdf2","ssh","ssh/agent","ssh/terminal"]
|
||||||
revision = "74b34b9dd60829a9fcaf56a59e81c3877a8ecd2c"
|
revision = "74b34b9dd60829a9fcaf56a59e81c3877a8ecd2c"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
|
@ -115,6 +121,18 @@
|
||||||
revision = "f92cdcd7dcdc69e81b2d7b338479a19a8723cfa3"
|
revision = "f92cdcd7dcdc69e81b2d7b338479a19a8723cfa3"
|
||||||
version = "v1.6.0"
|
version = "v1.6.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "gopkg.in/src-d/go-git.v4"
|
||||||
|
packages = [".","config","plumbing","plumbing/format/config","plumbing/format/idxfile","plumbing/format/index","plumbing/format/objfile","plumbing/format/packfile","plumbing/format/pktline","plumbing/object","plumbing/protocol/packp","plumbing/protocol/packp/capability","plumbing/protocol/packp/sideband","plumbing/storer","plumbing/transport","plumbing/transport/client","plumbing/transport/file","plumbing/transport/git","plumbing/transport/http","plumbing/transport/internal/common","plumbing/transport/ssh","storage/filesystem","storage/filesystem/internal/dotgit","storage/memory","utils/binary","utils/diff","utils/fs","utils/fs/os","utils/ioutil"]
|
||||||
|
revision = "c9353b2bd7c1cbdf8f78dad6deac64ed2f2ed9eb"
|
||||||
|
version = "v4.0.0-rc5"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "gopkg.in/warnings.v0"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "8a331561fe74dadba6edfc59f3be66c22c3b065d"
|
||||||
|
version = "v0.1.1"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "v2"
|
branch = "v2"
|
||||||
name = "gopkg.in/yaml.v2"
|
name = "gopkg.in/yaml.v2"
|
||||||
|
@ -124,6 +142,6 @@
|
||||||
[solve-meta]
|
[solve-meta]
|
||||||
analyzer-name = "dep"
|
analyzer-name = "dep"
|
||||||
analyzer-version = 1
|
analyzer-version = 1
|
||||||
inputs-digest = "687b22c2bbc91854f6457658d31cfe26d030ba8d3af1ac3d72161a554c366d4b"
|
inputs-digest = "f46ad74b2a7ae0946e49d4b006e2f22506e70c58deb7539954ae1764e8361e3c"
|
||||||
solver-name = "gps-cdcl"
|
solver-name = "gps-cdcl"
|
||||||
solver-version = 1
|
solver-version = 1
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
|
||||||
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
|
@ -17,9 +17,13 @@ import (
|
||||||
// TODO(pulumi/pulumi-service#49): Return this from a function that takes OS-idioms into account.
|
// TODO(pulumi/pulumi-service#49): Return this from a function that takes OS-idioms into account.
|
||||||
const pulumiSettingsFolder = ".pulumi"
|
const pulumiSettingsFolder = ".pulumi"
|
||||||
|
|
||||||
|
// permUserRWRestNone defines the file permissions that the
|
||||||
|
// user has RW access, and group and other have no access.
|
||||||
|
const permUserRWRestNone = 0600
|
||||||
|
|
||||||
// permUserAllRestNone defines the file permissions that the
|
// permUserAllRestNone defines the file permissions that the
|
||||||
// user has RWX access, and group and other have no access.
|
// user has RWX access, and group and other have no access.
|
||||||
const permUserAllRestNone = 0600
|
const permUserAllRestNone = 0700
|
||||||
|
|
||||||
// accountCredentials hold the information necessary for authenticating Pulumi Cloud API requests.
|
// accountCredentials hold the information necessary for authenticating Pulumi Cloud API requests.
|
||||||
type accountCredentials struct {
|
type accountCredentials struct {
|
||||||
|
@ -84,7 +88,7 @@ func storeCredentials(creds accountCredentials) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("marshalling credentials object: %v", err)
|
return fmt.Errorf("marshalling credentials object: %v", err)
|
||||||
}
|
}
|
||||||
return ioutil.WriteFile(credsFile, raw, permUserAllRestNone)
|
return ioutil.WriteFile(credsFile, raw, permUserRWRestNone)
|
||||||
}
|
}
|
||||||
|
|
||||||
// deleteStoredCredentials deletes the user's stored credentials.
|
// deleteStoredCredentials deletes the user's stored credentials.
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
|
||||||
|
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
71
cmd/init.go
Normal file
71
cmd/init.go
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/pulumi/pulumi/pkg/util/cmdutil"
|
||||||
|
"github.com/pulumi/pulumi/pkg/workspace"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newInitCmd() *cobra.Command {
|
||||||
|
var owner string
|
||||||
|
var name string
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "init",
|
||||||
|
Short: "Initialize a new Pulumi repository",
|
||||||
|
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
repo, err := workspace.GetRepository(cwd)
|
||||||
|
if err != nil && err != workspace.ErrNoRepository {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err == workspace.ErrNoRepository {
|
||||||
|
// No existing repository, so we'll need to create one
|
||||||
|
repo = workspace.NewRepository(cwd)
|
||||||
|
|
||||||
|
detectedOwner, detectedName, detectErr := detectOwnerAndName(cwd)
|
||||||
|
if detectErr != nil {
|
||||||
|
return detectErr
|
||||||
|
}
|
||||||
|
repo.Owner = detectedOwner
|
||||||
|
repo.Name = detectedName
|
||||||
|
}
|
||||||
|
|
||||||
|
// explicit command line arguments should overwrite any existing values
|
||||||
|
if owner != "" {
|
||||||
|
repo.Owner = owner
|
||||||
|
}
|
||||||
|
|
||||||
|
if name != "" {
|
||||||
|
repo.Name = name
|
||||||
|
}
|
||||||
|
|
||||||
|
err = repo.Save()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Initialized Pulumi repository in %s\n", repo.Root)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.PersistentFlags().StringVar(
|
||||||
|
&owner, "owner", "",
|
||||||
|
"Override the repository owner; default is taken from current Git repository or username")
|
||||||
|
cmd.PersistentFlags().StringVar(
|
||||||
|
&name, "name", "",
|
||||||
|
"Override the repository name; default is taken from current Git repository or current working directory")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
|
@ -18,7 +18,6 @@ import (
|
||||||
"github.com/pulumi/pulumi/pkg/resource/stack"
|
"github.com/pulumi/pulumi/pkg/resource/stack"
|
||||||
"github.com/pulumi/pulumi/pkg/tokens"
|
"github.com/pulumi/pulumi/pkg/tokens"
|
||||||
"github.com/pulumi/pulumi/pkg/util/contract"
|
"github.com/pulumi/pulumi/pkg/util/contract"
|
||||||
"github.com/pulumi/pulumi/pkg/workspace"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type localStackProvider struct {
|
type localStackProvider struct {
|
||||||
|
@ -74,6 +73,11 @@ func (m localStackMutation) End(snapshot *deploy.Snapshot) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getStack(name tokens.QName) (tokens.QName, map[tokens.ModuleMember]config.Value, *deploy.Snapshot, error) {
|
func getStack(name tokens.QName) (tokens.QName, map[tokens.ModuleMember]config.Value, *deploy.Snapshot, error) {
|
||||||
|
workspace, err := newWorkspace()
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
contract.Require(name != "", "name")
|
contract.Require(name != "", "name")
|
||||||
file := workspace.StackPath(name)
|
file := workspace.StackPath(name)
|
||||||
|
|
||||||
|
@ -107,6 +111,11 @@ func getStack(name tokens.QName) (tokens.QName, map[tokens.ModuleMember]config.V
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveStack(name tokens.QName, config map[tokens.ModuleMember]config.Value, snap *deploy.Snapshot) error {
|
func saveStack(name tokens.QName, config map[tokens.ModuleMember]config.Value, snap *deploy.Snapshot) error {
|
||||||
|
workspace, err := newWorkspace()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
file := workspace.StackPath(name)
|
file := workspace.StackPath(name)
|
||||||
|
|
||||||
// Make a serializable stack and then use the encoder to encode it.
|
// Make a serializable stack and then use the encoder to encode it.
|
||||||
|
@ -152,6 +161,12 @@ func isTruthy(s string) bool {
|
||||||
|
|
||||||
func removeStack(name tokens.QName) error {
|
func removeStack(name tokens.QName) error {
|
||||||
contract.Require(name != "", "name")
|
contract.Require(name != "", "name")
|
||||||
|
|
||||||
|
workspace, err := newWorkspace()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Just make a backup of the file and don't write out anything new.
|
// Just make a backup of the file and don't write out anything new.
|
||||||
file := workspace.StackPath(name)
|
file := workspace.StackPath(name)
|
||||||
backupTarget(file)
|
backupTarget(file)
|
||||||
|
|
|
@ -50,6 +50,7 @@ func NewPulumiCmd(version string) *cobra.Command {
|
||||||
cmd.AddCommand(newPreviewCmd())
|
cmd.AddCommand(newPreviewCmd())
|
||||||
cmd.AddCommand(newUpdateCmd())
|
cmd.AddCommand(newUpdateCmd())
|
||||||
cmd.AddCommand(newVersionCmd(version))
|
cmd.AddCommand(newVersionCmd(version))
|
||||||
|
cmd.AddCommand(newInitCmd())
|
||||||
|
|
||||||
// Commands specific to the Pulumi Cloud Management Console.
|
// Commands specific to the Pulumi Cloud Management Console.
|
||||||
cmd.AddCommand(newLoginCmd())
|
cmd.AddCommand(newLoginCmd())
|
||||||
|
|
|
@ -11,7 +11,6 @@ import (
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/pulumi/pulumi/pkg/encoding"
|
"github.com/pulumi/pulumi/pkg/encoding"
|
||||||
"github.com/pulumi/pulumi/pkg/workspace"
|
|
||||||
|
|
||||||
"github.com/pulumi/pulumi/pkg/tokens"
|
"github.com/pulumi/pulumi/pkg/tokens"
|
||||||
|
|
||||||
|
@ -66,8 +65,14 @@ func newStackLsCmd() *cobra.Command {
|
||||||
func getStacks() ([]tokens.QName, error) {
|
func getStacks() ([]tokens.QName, error) {
|
||||||
var stacks []tokens.QName
|
var stacks []tokens.QName
|
||||||
|
|
||||||
|
w, err := newWorkspace()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// Read the stack directory.
|
// Read the stack directory.
|
||||||
path := workspace.StackPath("")
|
path := w.StackPath("")
|
||||||
|
|
||||||
files, err := ioutil.ReadDir(path)
|
files, err := ioutil.ReadDir(path)
|
||||||
if err != nil && !os.IsNotExist(err) {
|
if err != nil && !os.IsNotExist(err) {
|
||||||
return nil, errors.Errorf("could not read stacks: %v", err)
|
return nil, errors.Errorf("could not read stacks: %v", err)
|
||||||
|
|
71
cmd/util.go
71
cmd/util.go
|
@ -10,6 +10,11 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pulumi/pulumi/pkg/util/fsutil"
|
||||||
|
|
||||||
"github.com/pulumi/pulumi/pkg/pack"
|
"github.com/pulumi/pulumi/pkg/pack"
|
||||||
"github.com/pulumi/pulumi/pkg/resource/config"
|
"github.com/pulumi/pulumi/pkg/resource/config"
|
||||||
|
@ -23,15 +28,17 @@ import (
|
||||||
"github.com/pulumi/pulumi/pkg/tokens"
|
"github.com/pulumi/pulumi/pkg/tokens"
|
||||||
"github.com/pulumi/pulumi/pkg/util/contract"
|
"github.com/pulumi/pulumi/pkg/util/contract"
|
||||||
"github.com/pulumi/pulumi/pkg/workspace"
|
"github.com/pulumi/pulumi/pkg/workspace"
|
||||||
|
|
||||||
|
git "gopkg.in/src-d/go-git.v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
// newWorkspace creates a new workspace using the current working directory.
|
// newWorkspace creates a new workspace using the current working directory.
|
||||||
func newWorkspace() (workspace.W, error) {
|
func newWorkspace() (workspace.W, error) {
|
||||||
pwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return workspace.New(pwd)
|
return workspace.NewProjectWorkspace(cwd)
|
||||||
}
|
}
|
||||||
|
|
||||||
// explicitOrCurrent returns an stack name after ensuring the stack exists. When a empty
|
// explicitOrCurrent returns an stack name after ensuring the stack exists. When a empty
|
||||||
|
@ -233,3 +240,63 @@ func hasSecureValue(config map[tokens.ModuleMember]config.Value) bool {
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func detectOwnerAndName(dir string) (string, string, error) {
|
||||||
|
owner, repo, err := getGitHubProjectForOrigin(dir)
|
||||||
|
if err == nil {
|
||||||
|
return owner, repo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return user.Username, filepath.Base(dir), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getGitHubProjectForOrigin(dir string) (string, string, error) {
|
||||||
|
|
||||||
|
gitRoot, err := fsutil.WalkUp(dir, func(s string) bool { return filepath.Base(s) == ".git" }, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", errors.Wrap(err, "could not detect git repository")
|
||||||
|
}
|
||||||
|
if gitRoot == "" {
|
||||||
|
return "", "", errors.Errorf("could not locate git repository starting at: %s", dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
repo, err := git.NewFilesystemRepository(gitRoot)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
remote, err := repo.Remote("origin")
|
||||||
|
if err != nil {
|
||||||
|
return "", "", errors.Wrap(err, "could not read origin information")
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteURL := remote.Config().URL
|
||||||
|
project := ""
|
||||||
|
|
||||||
|
const GitHubSSHPrefix = "git@github.com:"
|
||||||
|
const GitHubHTTPSPrefix = "https://github.com/"
|
||||||
|
const GitHubRepositorySuffix = ".git"
|
||||||
|
|
||||||
|
if strings.HasPrefix(remoteURL, GitHubSSHPrefix) {
|
||||||
|
project = trimGitRemoteURL(remoteURL, GitHubSSHPrefix, GitHubRepositorySuffix)
|
||||||
|
} else if strings.HasPrefix(remoteURL, GitHubHTTPSPrefix) {
|
||||||
|
project = trimGitRemoteURL(remoteURL, GitHubHTTPSPrefix, GitHubRepositorySuffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
split := strings.Split(project, "/")
|
||||||
|
|
||||||
|
if len(split) != 2 {
|
||||||
|
return "", "", errors.Errorf("could not detect GitHub project from url: %v", remote)
|
||||||
|
}
|
||||||
|
|
||||||
|
return split[0], split[1], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimGitRemoteURL(url string, prefix string, suffix string) string {
|
||||||
|
return strings.TrimSuffix(strings.TrimPrefix(url, prefix), suffix)
|
||||||
|
}
|
||||||
|
|
|
@ -606,7 +606,7 @@ func (a *Archive) readPath() (map[string]*Blob, error) {
|
||||||
|
|
||||||
// Finally, if this was a .pulumi directory, we will skip this by default.
|
// Finally, if this was a .pulumi directory, we will skip this by default.
|
||||||
// TODO[pulumi/pulumi#122]: when we support .pulumiignore, this will be customizable.
|
// TODO[pulumi/pulumi#122]: when we support .pulumiignore, this will be customizable.
|
||||||
if !f.IsDir() && f.Name() == workspace.Dir {
|
if !f.IsDir() && f.Name() == workspace.BookkeepingDir {
|
||||||
return filepath.SkipDir
|
return filepath.SkipDir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -86,6 +86,7 @@ func (opts ProgramTestOptions) With(overrides ProgramTestOptions) ProgramTestOpt
|
||||||
// yarn install
|
// yarn install
|
||||||
// yarn link <each opts.Depencies>
|
// yarn link <each opts.Depencies>
|
||||||
// yarn run build
|
// yarn run build
|
||||||
|
// pulumi init
|
||||||
// pulumi stack init integrationtesting
|
// pulumi stack init integrationtesting
|
||||||
// pulumi config text <each opts.Config>
|
// pulumi config text <each opts.Config>
|
||||||
// pulumi config secret <each opts.Secrets>
|
// pulumi config secret <each opts.Secrets>
|
||||||
|
@ -108,6 +109,7 @@ func ProgramTest(t *testing.T, opts ProgramTestOptions) {
|
||||||
// Ensure all links are present, the stack is created, and all configs are applied.
|
// Ensure all links are present, the stack is created, and all configs are applied.
|
||||||
_, err = fmt.Fprintf(opts.Stdout, "Initializing project\n")
|
_, err = fmt.Fprintf(opts.Stdout, "Initializing project\n")
|
||||||
contract.IgnoreError(err)
|
contract.IgnoreError(err)
|
||||||
|
RunCommand(t, []string{opts.Bin, "init"}, dir, opts)
|
||||||
RunCommand(t, []string{opts.Bin, "stack", "init", testStackName}, dir, opts)
|
RunCommand(t, []string{opts.Bin, "stack", "init", testStackName}, dir, opts)
|
||||||
for key, value := range opts.Config {
|
for key, value := range opts.Config {
|
||||||
RunCommand(t, []string{opts.Bin, "config", "text", key, value}, dir, opts)
|
RunCommand(t, []string{opts.Bin, "config", "text", key, value}, dir, opts)
|
||||||
|
@ -169,9 +171,9 @@ func performExtraRuntimeValidation(
|
||||||
extraRuntimeValidation func(t *testing.T, checkpoint stack.Checkpoint),
|
extraRuntimeValidation func(t *testing.T, checkpoint stack.Checkpoint),
|
||||||
dir string) (err error) {
|
dir string) (err error) {
|
||||||
|
|
||||||
checkpointFile := path.Join(dir, workspace.Dir, "stacks", testStackName+".json")
|
checkpointFile := path.Join(dir, workspace.BookkeepingDir, "stacks", filepath.Base(dir), testStackName+".json")
|
||||||
var byts []byte
|
var byts []byte
|
||||||
byts, err = ioutil.ReadFile(path.Join(dir, workspace.Dir, "stacks", testStackName+".json"))
|
byts, err = ioutil.ReadFile(checkpointFile)
|
||||||
if !assert.NoError(t, err, "Expected to be able to read checkpoint file at %v: %v", checkpointFile, err) {
|
if !assert.NoError(t, err, "Expected to be able to read checkpoint file at %v: %v", checkpointFile, err) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -287,7 +289,7 @@ func prepareProject(t *testing.T, src string, origin string, opts ProgramTestOpt
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now copy the source into it, ignoring .pulumi/ and Pulumi.yaml if there's an origin.
|
// Now copy the source into it, ignoring .pulumi/ and Pulumi.yaml if there's an origin.
|
||||||
wdir := workspace.Dir
|
wdir := workspace.BookkeepingDir
|
||||||
proj := workspace.ProjectFile + ".yaml"
|
proj := workspace.ProjectFile + ".yaml"
|
||||||
excl := make(map[string]bool)
|
excl := make(map[string]bool)
|
||||||
if origin != "" {
|
if origin != "" {
|
||||||
|
|
65
pkg/util/fsutil/walkup.go
Normal file
65
pkg/util/fsutil/walkup.go
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
|
||||||
|
|
||||||
|
package fsutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WalkUp walks each file in path, passing the full path to `walkFn`. If walkFn returns true,
|
||||||
|
// this method returns the path that was passed to walkFn. Before visiting the parent directory,
|
||||||
|
// visitParentFn is called, if that returns false, WalkUp stops its search
|
||||||
|
func WalkUp(path string, walkFn func(string) bool, visitParentFn func(string) bool) (string, error) {
|
||||||
|
if visitParentFn == nil {
|
||||||
|
visitParentFn = func(dir string) bool { return true }
|
||||||
|
}
|
||||||
|
|
||||||
|
curr := pathDir(path)
|
||||||
|
|
||||||
|
for {
|
||||||
|
// visit each file
|
||||||
|
files, err := ioutil.ReadDir(curr)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
for _, file := range files {
|
||||||
|
name := file.Name()
|
||||||
|
path := filepath.Join(curr, name)
|
||||||
|
if walkFn(path) {
|
||||||
|
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we are at the root, stop walking
|
||||||
|
if isTop(curr) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if !visitParentFn(curr) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// visit the parent
|
||||||
|
curr = filepath.Dir(curr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// pathDir returns the nearest directory to the given path (identity if a directory; parent otherwise).
|
||||||
|
func pathDir(path string) string {
|
||||||
|
// If the path is a file, we want the directory it is in
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
if err != nil || info.IsDir() {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
return filepath.Dir(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// isTop returns true if the path represents the top of the filesystem.
|
||||||
|
func isTop(path string) bool {
|
||||||
|
return os.IsPathSeparator(path[len(path)-1])
|
||||||
|
}
|
|
@ -3,97 +3,42 @@
|
||||||
package workspace
|
package workspace
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pulumi/pulumi/pkg/util/fsutil"
|
||||||
|
|
||||||
"github.com/pulumi/pulumi/pkg/encoding"
|
"github.com/pulumi/pulumi/pkg/encoding"
|
||||||
"github.com/pulumi/pulumi/pkg/tokens"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const ProjectFile = "Pulumi" // the base name of a Project.
|
const ProjectFile = "Pulumi" // the base name of a project file.
|
||||||
const Dir = ".pulumi" // the default name of the LumiPack output directory.
|
const GitDir = ".git" // the name of the folder git uses to store information
|
||||||
const StackDir = "stacks" // the default name of the LumiPack stack directory.
|
const BookkeepingDir = ".pulumi" // the name of our bookeeping folder, we store all state information here (like .git for git)
|
||||||
const DepDir = "packs" // the directory in which dependencies exist, either local or global.
|
const StackDir = "stacks" // the name of the directory that holds stack information for projects.
|
||||||
const SettingsFile = "workspace" // the base name of a markup file for shared settings in a workspace.
|
const WorkspaceDir = "workspaces" // the name of the directory that holds workspace information for projects.
|
||||||
|
const RepoFile = "settings.json" // the name of the file that holds information specific to the entire repository.
|
||||||
// StackPath returns a path to the given stack's default location.
|
const WorkspaceFile = "workspace.json" // the name of the file that holds workspace information.
|
||||||
func StackPath(stack tokens.QName) string {
|
|
||||||
path := filepath.Join(Dir, StackDir)
|
|
||||||
if stack != "" {
|
|
||||||
path = filepath.Join(path, qnamePath(stack)+encoding.Exts[0])
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
|
|
||||||
// isTop returns true if the path represents the top of the filesystem.
|
|
||||||
func isTop(path string) bool {
|
|
||||||
return os.IsPathSeparator(path[len(path)-1])
|
|
||||||
}
|
|
||||||
|
|
||||||
// pathDir returns the nearest directory to the given path (identity if a directory; parent otherwise).
|
|
||||||
func pathDir(path string) string {
|
|
||||||
// It's possible that the path is a file (e.g., a Lumi.yaml file); if so, we want the directory.
|
|
||||||
info, err := os.Stat(path)
|
|
||||||
if err != nil || info.IsDir() {
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
return filepath.Dir(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DetectPackage locates the closest package from the given path, searching "upwards" in the directory hierarchy. If no
|
// DetectPackage locates the closest package from the given path, searching "upwards" in the directory hierarchy. If no
|
||||||
// Project is found, an empty path is returned. If problems are detected, they are logged to the diag.Sink.
|
// Project is found, an empty path is returned. If problems are detected, they are logged to the diag.Sink.
|
||||||
func DetectPackage(path string) (string, error) {
|
func DetectPackage(path string) (string, error) {
|
||||||
// It's possible the target is already the file we seek; if so, return right away.
|
return fsutil.WalkUp(path, isProject, func(s string) bool { return !isRepositoryFolder(filepath.Join(s, BookkeepingDir)) })
|
||||||
if IsProject(path) {
|
|
||||||
return path, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
curr := pathDir(path)
|
|
||||||
for {
|
|
||||||
stop := false
|
|
||||||
|
|
||||||
// Enumerate the current path's files, checking each to see if it's a Project.
|
|
||||||
files, err := ioutil.ReadDir(curr)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
for _, file := range files {
|
|
||||||
name := file.Name()
|
|
||||||
path := filepath.Join(curr, name)
|
|
||||||
if IsProject(path) {
|
|
||||||
return path, nil
|
|
||||||
} else if IsLumiDir(path) {
|
|
||||||
// If we hit a workspace, stop looking.
|
|
||||||
stop = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we encountered a stop condition, break out of the loop.
|
|
||||||
if stop {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// If neither succeeded, keep looking in our parent directory.
|
|
||||||
curr = filepath.Dir(curr)
|
|
||||||
if isTop(curr) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsLumiDir returns true if the target is a Lumi directory.
|
func isGitFolder(path string) bool {
|
||||||
func IsLumiDir(path string) bool {
|
|
||||||
info, err := os.Stat(path)
|
info, err := os.Stat(path)
|
||||||
return err == nil && info.IsDir() && info.Name() == Dir
|
return err == nil && info.IsDir() && info.Name() == ".git"
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsProject returns true if the path references what appears to be a valid project. If problems are detected -- like
|
func isRepositoryFolder(path string) bool {
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
return err == nil && info.IsDir() && info.Name() == BookkeepingDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// isProject returns true if the path references what appears to be a valid project. If problems are detected -- like
|
||||||
// an incorrect extension -- they are logged to the provided diag.Sink (if non-nil).
|
// an incorrect extension -- they are logged to the provided diag.Sink (if non-nil).
|
||||||
func IsProject(path string) bool {
|
func isProject(path string) bool {
|
||||||
return isMarkupFile(path, ProjectFile)
|
return isMarkupFile(path, ProjectFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
91
pkg/workspace/repository.go
Normal file
91
pkg/workspace/repository.go
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
|
||||||
|
|
||||||
|
package workspace
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/pulumi/pulumi/pkg/util/fsutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Repository struct {
|
||||||
|
Owner string `json:"owner" yaml:"owner"` // the owner of this repository
|
||||||
|
Name string `json:"name" yaml:"name"` // the name of the repository
|
||||||
|
Root string `json:"-" yaml:"-"` // storage location
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) Save() error {
|
||||||
|
b, err := json.Marshal(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.MkdirAll(r.Root, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ioutil.WriteFile(filepath.Join(r.Root, RepoFile), b, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRepository(root string) *Repository {
|
||||||
|
return &Repository{Root: getDotPulumiDirectoryPath(root)}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrNoRepository = errors.New("no repository")
|
||||||
|
|
||||||
|
func GetRepository(root string) (*Repository, error) {
|
||||||
|
dotPulumiPath := getDotPulumiDirectoryPath(root)
|
||||||
|
|
||||||
|
repofilePath := filepath.Join(dotPulumiPath, RepoFile)
|
||||||
|
|
||||||
|
_, err := os.Stat(repofilePath)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil, ErrNoRepository
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := ioutil.ReadFile(repofilePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var repo Repository
|
||||||
|
err = json.Unmarshal(b, &repo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if repo.Owner == "" {
|
||||||
|
return nil, errors.New("invalid repo.json file, missing name property")
|
||||||
|
}
|
||||||
|
|
||||||
|
if repo.Name == "" {
|
||||||
|
return nil, errors.New("invalid repo.json file, missing name property")
|
||||||
|
}
|
||||||
|
|
||||||
|
repo.Root = dotPulumiPath
|
||||||
|
|
||||||
|
return &repo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDotPulumiDirectoryPath(dir string) string {
|
||||||
|
// First, let's look to see if there's an existing .pulumi folder
|
||||||
|
dotpulumipath, _ := fsutil.WalkUp(dir, isRepositoryFolder, nil)
|
||||||
|
if dotpulumipath != "" {
|
||||||
|
return dotpulumipath
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's a .git folder, put .pulumi there
|
||||||
|
dotgitpath, _ := fsutil.WalkUp(dir, isGitFolder, nil)
|
||||||
|
if dotgitpath != "" {
|
||||||
|
return filepath.Join(filepath.Dir(dotgitpath), ".pulumi")
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(dir, ".pulumi")
|
||||||
|
}
|
|
@ -3,106 +3,122 @@
|
||||||
package workspace
|
package workspace
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/golang/glog"
|
"github.com/pulumi/pulumi/pkg/pack"
|
||||||
homedir "github.com/mitchellh/go-homedir"
|
|
||||||
|
|
||||||
"github.com/pulumi/pulumi/pkg/encoding"
|
|
||||||
"github.com/pulumi/pulumi/pkg/tokens"
|
"github.com/pulumi/pulumi/pkg/tokens"
|
||||||
)
|
)
|
||||||
|
|
||||||
// W offers functionality for interacting with Lumi workspaces. A workspace influences compilation; for example, it
|
// W offers functionality for interacting with Pulumi workspaces.
|
||||||
// can specify default versions of dependencies, easing the process of working with multiple projects.
|
|
||||||
type W interface {
|
type W interface {
|
||||||
Path() string // the base path of the current workspace.
|
Settings() *Settings // returns a mutable pointer to the optional workspace settings info.
|
||||||
Root() string // the root path of the current workspace.
|
Repository() *Repository // the repository this project belongs to
|
||||||
Settings() *Settings // returns a mutable pointer to the optional workspace settings info.
|
StackPath(stackName tokens.QName) string // returns the path to store stack information
|
||||||
DetectPackage() (string, error) // locates the nearest project file in the directory hierarchy.
|
Save() error // saves any modifications to the workspace.
|
||||||
Save() error // saves any modifications to the workspace.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new workspace from the given starting path.
|
type projectWorkspace struct {
|
||||||
func New(path string) (W, error) {
|
name tokens.PackageName // the project this workspace is associated with.
|
||||||
// First normalize the path to an absolute one.
|
project string // the path to the Pulumi.[yaml|json] file for this project.
|
||||||
var err error
|
settings *Settings // settings for this workspace.
|
||||||
path, err = filepath.Abs(path)
|
repo *Repository // the repo this workspace is associated with.
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProjectWorkspace(dir string) (W, error) {
|
||||||
|
repo, err := GetRepository(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
home, err := homedir.Dir()
|
project, err := DetectPackage(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ws := workspace{
|
pkg, err := pack.Load(project)
|
||||||
path: path,
|
if err != nil {
|
||||||
home: home,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform our I/O: memoize the root directory and load up any settings before returning.
|
|
||||||
if err := ws.init(); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &ws, nil
|
w := projectWorkspace{
|
||||||
}
|
name: pkg.Name,
|
||||||
|
project: project,
|
||||||
|
repo: repo}
|
||||||
|
|
||||||
type workspace struct {
|
err = w.readSettings()
|
||||||
path string // the path at which the workspace was constructed.
|
if err != nil {
|
||||||
home string // the home directory to use for this workspace.
|
return nil, err
|
||||||
root string // the root of the workspace.
|
|
||||||
settings Settings // an optional bag of workspace-wide settings.
|
|
||||||
}
|
|
||||||
|
|
||||||
// init finds the root of the workspace, caches it for fast lookups, and loads up any workspace settings.
|
|
||||||
func (w *workspace) init() error {
|
|
||||||
if w.root == "" {
|
|
||||||
// Detect the root of the workspace and cache it.
|
|
||||||
root := pathDir(w.path)
|
|
||||||
Search:
|
|
||||||
for {
|
|
||||||
files, err := ioutil.ReadDir(root)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, file := range files {
|
|
||||||
// A lumi directory delimits the root of the workspace.
|
|
||||||
lumidir := filepath.Join(root, file.Name())
|
|
||||||
if IsLumiDir(lumidir) {
|
|
||||||
glog.V(3).Infof("Lumi workspace detected; setting root to %v", root)
|
|
||||||
w.root = root // remember the root.
|
|
||||||
w.settings, err = w.readSettings() // load up optional settings.
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
break Search
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If neither succeeded, keep looking in our parent directory.
|
|
||||||
if root = filepath.Dir(root); isTop(root) {
|
|
||||||
// We reached the top of the filesystem. Just set root back to the path and stop.
|
|
||||||
glog.V(3).Infof("No Lumi workspace found; defaulting to current path %v", w.root)
|
|
||||||
w.root = w.path
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return &w, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pw *projectWorkspace) Settings() *Settings {
|
||||||
|
return pw.settings
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pw *projectWorkspace) Repository() *Repository {
|
||||||
|
return pw.repo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pw *projectWorkspace) DetectPackage() (string, error) {
|
||||||
|
return pw.project, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pw *projectWorkspace) Save() error {
|
||||||
|
settingsFile := pw.settingsPath()
|
||||||
|
|
||||||
|
// ensure the path exists
|
||||||
|
err := os.MkdirAll(filepath.Dir(settingsFile), 0700)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := json.Marshal(pw.settings)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ioutil.WriteFile(settingsFile, b, 0600)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pw *projectWorkspace) StackPath(stackName tokens.QName) string {
|
||||||
|
path := filepath.Join(pw.Repository().Root, StackDir, pw.name.String())
|
||||||
|
if stackName != "" {
|
||||||
|
path = filepath.Join(path, qnamePath(stackName)+".json")
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pw *projectWorkspace) readSettings() error {
|
||||||
|
settingsPath := pw.settingsPath()
|
||||||
|
|
||||||
|
b, err := ioutil.ReadFile(settingsPath)
|
||||||
|
if err != nil && os.IsNotExist(err) {
|
||||||
|
// not an error to not have an existing settings file.
|
||||||
|
pw.settings = &Settings{}
|
||||||
|
return nil
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var settings Settings
|
||||||
|
|
||||||
|
err = json.Unmarshal(b, &settings)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pw.settings = &settings
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *workspace) Path() string { return w.path }
|
func (pw *projectWorkspace) settingsPath() string {
|
||||||
func (w *workspace) Root() string { return w.root }
|
return filepath.Join(pw.Repository().Root, WorkspaceDir, pw.name.String(), WorkspaceFile)
|
||||||
func (w *workspace) Settings() *Settings { return &w.settings }
|
|
||||||
|
|
||||||
func (w *workspace) DetectPackage() (string, error) {
|
|
||||||
return DetectPackage(w.path)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// qnamePath just cleans a name and makes sure it's appropriate to use as a path.
|
// qnamePath just cleans a name and makes sure it's appropriate to use as a path.
|
||||||
|
@ -114,51 +130,3 @@ func qnamePath(nm tokens.QName) string {
|
||||||
func stringNamePath(nm string) string {
|
func stringNamePath(nm string) string {
|
||||||
return strings.Replace(nm, tokens.QNameDelimiter, string(os.PathSeparator), -1)
|
return strings.Replace(nm, tokens.QNameDelimiter, string(os.PathSeparator), -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save persists any in-memory changes made to the workspace.
|
|
||||||
func (w *workspace) Save() error {
|
|
||||||
// For now, the only changes to commit are the settings file changes.
|
|
||||||
return w.saveSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
// settingsFile returns the settings file location for this workspace.
|
|
||||||
func (w *workspace) settingsFile(ext string) string {
|
|
||||||
return filepath.Join(w.root, Dir, SettingsFile+ext)
|
|
||||||
}
|
|
||||||
|
|
||||||
// readSettings loads a settings file from the workspace, probing for all available extensions.
|
|
||||||
func (w *workspace) readSettings() (Settings, error) {
|
|
||||||
// Attempt to load the raw bytes from all available extensions.
|
|
||||||
var settings Settings
|
|
||||||
for _, ext := range encoding.Exts {
|
|
||||||
// See if the file exists.
|
|
||||||
path := w.settingsFile(ext)
|
|
||||||
b, err := ioutil.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
continue // try the next extension
|
|
||||||
}
|
|
||||||
return settings, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it does, go ahead and decode it.
|
|
||||||
m := encoding.Marshalers[ext]
|
|
||||||
if err := m.Unmarshal(b, &settings); err != nil {
|
|
||||||
return settings, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return settings, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// saveSettings saves the settings into a file for this workspace, committing any in-memory changes that have been made.
|
|
||||||
// IDEA: right now, we only support JSON. It'd be ideal if we supported YAML too (and it would be quite easy).
|
|
||||||
func (w *workspace) saveSettings() error {
|
|
||||||
m := encoding.Default()
|
|
||||||
settings := w.Settings()
|
|
||||||
b, err := m.Marshal(settings)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
path := w.settingsFile(encoding.DefaultExt())
|
|
||||||
return ioutil.WriteFile(path, b, 0644)
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue