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:
Matt Ellis 2017-10-25 10:20:08 -07:00
parent 843ae4a4f6
commit 3f1197ef84
15 changed files with 469 additions and 213 deletions

34
Gopkg.lock generated
View file

@ -31,12 +31,6 @@
revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
version = "v1.0"
[[projects]]
branch = "master"
name = "github.com/mitchellh/go-homedir"
packages = ["."]
revision = "b8bc1bf767474819792c23f32d8286a45736f1c6"
[[projects]]
name = "github.com/pkg/errors"
packages = ["."]
@ -55,6 +49,12 @@
packages = ["."]
revision = "2ab6b7470a54bfa9b5b0289f9b4e8fc4839838f7"
[[projects]]
branch = "master"
name = "github.com/sergi/go-diff"
packages = ["diffmatchpatch"]
revision = "feef008d51ad2b3778f85d387ccf91735543008d"
[[projects]]
branch = "master"
name = "github.com/spf13/cobra"
@ -67,6 +67,12 @@
revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66"
version = "v1.0.0"
[[projects]]
name = "github.com/src-d/gcfg"
packages = [".","scanner","token","types"]
revision = "f187355171c936ac84a82793659ebb4936bc1c23"
version = "v1.3.0"
[[projects]]
name = "github.com/stretchr/testify"
packages = ["assert"]
@ -76,7 +82,7 @@
[[projects]]
branch = "master"
name = "golang.org/x/crypto"
packages = ["pbkdf2","ssh/terminal"]
packages = ["curve25519","ed25519","ed25519/internal/edwards25519","pbkdf2","ssh","ssh/agent","ssh/terminal"]
revision = "74b34b9dd60829a9fcaf56a59e81c3877a8ecd2c"
[[projects]]
@ -115,6 +121,18 @@
revision = "f92cdcd7dcdc69e81b2d7b338479a19a8723cfa3"
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]]
branch = "v2"
name = "gopkg.in/yaml.v2"
@ -124,6 +142,6 @@
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "687b22c2bbc91854f6457658d31cfe26d030ba8d3af1ac3d72161a554c366d4b"
inputs-digest = "f46ad74b2a7ae0946e49d4b006e2f22506e70c58deb7539954ae1764e8361e3c"
solver-name = "gps-cdcl"
solver-version = 1

View file

@ -1,3 +1,5 @@
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
package cmd
import (

View file

@ -17,9 +17,13 @@ import (
// TODO(pulumi/pulumi-service#49): Return this from a function that takes OS-idioms into account.
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
// 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.
type accountCredentials struct {
@ -84,7 +88,7 @@ func storeCredentials(creds accountCredentials) error {
if err != nil {
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.

View file

@ -1,3 +1,5 @@
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
package cmd
import (

71
cmd/init.go Normal file
View 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
}

View file

@ -18,7 +18,6 @@ import (
"github.com/pulumi/pulumi/pkg/resource/stack"
"github.com/pulumi/pulumi/pkg/tokens"
"github.com/pulumi/pulumi/pkg/util/contract"
"github.com/pulumi/pulumi/pkg/workspace"
)
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) {
workspace, err := newWorkspace()
if err != nil {
return "", nil, nil, err
}
contract.Require(name != "", "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 {
workspace, err := newWorkspace()
if err != nil {
return err
}
file := workspace.StackPath(name)
// 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 {
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.
file := workspace.StackPath(name)
backupTarget(file)

View file

@ -50,6 +50,7 @@ func NewPulumiCmd(version string) *cobra.Command {
cmd.AddCommand(newPreviewCmd())
cmd.AddCommand(newUpdateCmd())
cmd.AddCommand(newVersionCmd(version))
cmd.AddCommand(newInitCmd())
// Commands specific to the Pulumi Cloud Management Console.
cmd.AddCommand(newLoginCmd())

View file

@ -11,7 +11,6 @@ import (
"github.com/pkg/errors"
"github.com/pulumi/pulumi/pkg/encoding"
"github.com/pulumi/pulumi/pkg/workspace"
"github.com/pulumi/pulumi/pkg/tokens"
@ -66,8 +65,14 @@ func newStackLsCmd() *cobra.Command {
func getStacks() ([]tokens.QName, error) {
var stacks []tokens.QName
w, err := newWorkspace()
if err != nil {
return nil, err
}
// Read the stack directory.
path := workspace.StackPath("")
path := w.StackPath("")
files, err := ioutil.ReadDir(path)
if err != nil && !os.IsNotExist(err) {
return nil, errors.Errorf("could not read stacks: %v", err)

View file

@ -10,6 +10,11 @@ import (
"io"
"io/ioutil"
"os"
"os/user"
"path/filepath"
"strings"
"github.com/pulumi/pulumi/pkg/util/fsutil"
"github.com/pulumi/pulumi/pkg/pack"
"github.com/pulumi/pulumi/pkg/resource/config"
@ -23,15 +28,17 @@ import (
"github.com/pulumi/pulumi/pkg/tokens"
"github.com/pulumi/pulumi/pkg/util/contract"
"github.com/pulumi/pulumi/pkg/workspace"
git "gopkg.in/src-d/go-git.v4"
)
// newWorkspace creates a new workspace using the current working directory.
func newWorkspace() (workspace.W, error) {
pwd, err := os.Getwd()
cwd, err := os.Getwd()
if err != nil {
return nil, err
}
return workspace.New(pwd)
return workspace.NewProjectWorkspace(cwd)
}
// 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
}
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)
}

View file

@ -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.
// 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
}

View file

@ -86,6 +86,7 @@ func (opts ProgramTestOptions) With(overrides ProgramTestOptions) ProgramTestOpt
// yarn install
// yarn link <each opts.Depencies>
// yarn run build
// pulumi init
// pulumi stack init integrationtesting
// pulumi config text <each opts.Config>
// 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.
_, err = fmt.Fprintf(opts.Stdout, "Initializing project\n")
contract.IgnoreError(err)
RunCommand(t, []string{opts.Bin, "init"}, dir, opts)
RunCommand(t, []string{opts.Bin, "stack", "init", testStackName}, dir, opts)
for key, value := range opts.Config {
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),
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
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) {
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.
wdir := workspace.Dir
wdir := workspace.BookkeepingDir
proj := workspace.ProjectFile + ".yaml"
excl := make(map[string]bool)
if origin != "" {

65
pkg/util/fsutil/walkup.go Normal file
View 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])
}

View file

@ -3,97 +3,42 @@
package workspace
import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/pulumi/pulumi/pkg/util/fsutil"
"github.com/pulumi/pulumi/pkg/encoding"
"github.com/pulumi/pulumi/pkg/tokens"
)
const ProjectFile = "Pulumi" // the base name of a Project.
const Dir = ".pulumi" // the default name of the LumiPack output directory.
const StackDir = "stacks" // the default name of the LumiPack stack directory.
const DepDir = "packs" // the directory in which dependencies exist, either local or global.
const SettingsFile = "workspace" // the base name of a markup file for shared settings in a workspace.
// StackPath returns a path to the given stack's default location.
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)
}
const ProjectFile = "Pulumi" // the base name of a project file.
const GitDir = ".git" // the name of the folder git uses to store information
const BookkeepingDir = ".pulumi" // the name of our bookeeping folder, we store all state information here (like .git for git)
const StackDir = "stacks" // the name of the directory that holds stack information for projects.
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.
const WorkspaceFile = "workspace.json" // the name of the file that holds workspace information.
// 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.
func DetectPackage(path string) (string, error) {
// It's possible the target is already the file we seek; if so, return right away.
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
return fsutil.WalkUp(path, isProject, func(s string) bool { return !isRepositoryFolder(filepath.Join(s, BookkeepingDir)) })
}
// IsLumiDir returns true if the target is a Lumi directory.
func IsLumiDir(path string) bool {
func isGitFolder(path string) bool {
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).
func IsProject(path string) bool {
func isProject(path string) bool {
return isMarkupFile(path, ProjectFile)
}

View 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")
}

View file

@ -3,106 +3,122 @@
package workspace
import (
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/golang/glog"
homedir "github.com/mitchellh/go-homedir"
"github.com/pulumi/pulumi/pkg/encoding"
"github.com/pulumi/pulumi/pkg/pack"
"github.com/pulumi/pulumi/pkg/tokens"
)
// W offers functionality for interacting with Lumi workspaces. A workspace influences compilation; for example, it
// can specify default versions of dependencies, easing the process of working with multiple projects.
// W offers functionality for interacting with Pulumi workspaces.
type W interface {
Path() string // the base path of the current workspace.
Root() string // the root path of the current workspace.
Settings() *Settings // returns a mutable pointer to the optional workspace settings info.
DetectPackage() (string, error) // locates the nearest project file in the directory hierarchy.
Save() error // saves any modifications to the workspace.
Settings() *Settings // returns a mutable pointer to the optional workspace settings info.
Repository() *Repository // the repository this project belongs to
StackPath(stackName tokens.QName) string // returns the path to store stack information
Save() error // saves any modifications to the workspace.
}
// New creates a new workspace from the given starting path.
func New(path string) (W, error) {
// First normalize the path to an absolute one.
var err error
path, err = filepath.Abs(path)
type projectWorkspace struct {
name tokens.PackageName // the project this workspace is associated with.
project string // the path to the Pulumi.[yaml|json] file for this project.
settings *Settings // settings for this workspace.
repo *Repository // the repo this workspace is associated with.
}
func NewProjectWorkspace(dir string) (W, error) {
repo, err := GetRepository(dir)
if err != nil {
return nil, err
}
home, err := homedir.Dir()
project, err := DetectPackage(dir)
if err != nil {
return nil, err
}
ws := workspace{
path: path,
home: home,
}
// Perform our I/O: memoize the root directory and load up any settings before returning.
if err := ws.init(); err != nil {
pkg, err := pack.Load(project)
if err != nil {
return nil, err
}
return &ws, nil
}
w := projectWorkspace{
name: pkg.Name,
project: project,
repo: repo}
type workspace struct {
path string // the path at which the workspace was constructed.
home string // the home directory to use for this workspace.
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
}
}
err = w.readSettings()
if err != nil {
return nil, err
}
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
}
func (w *workspace) Path() string { return w.path }
func (w *workspace) Root() string { return w.root }
func (w *workspace) Settings() *Settings { return &w.settings }
func (w *workspace) DetectPackage() (string, error) {
return DetectPackage(w.path)
func (pw *projectWorkspace) settingsPath() string {
return filepath.Join(pw.Repository().Root, WorkspaceDir, pw.name.String(), WorkspaceFile)
}
// 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 {
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)
}