Initial commit

This commit is contained in:
2020-02-20 08:31:22 +01:00
commit b7bdd7bbea
17 changed files with 826 additions and 0 deletions

9
internal/command/all.go Normal file
View File

@ -0,0 +1,9 @@
package command
import "github.com/urfave/cli/v2"
func All() []*cli.Command {
return []*cli.Command{
newProjectCommand(),
}
}

View File

@ -0,0 +1,139 @@
package command
import (
"log"
"net/url"
"os"
"gitlab.com/wpetit/scaffold/internal/template"
"github.com/manifoldco/promptui"
"gitlab.com/wpetit/scaffold/internal/project"
"github.com/pkg/errors"
"github.com/urfave/cli/v2"
)
func newProjectCommand() *cli.Command {
return &cli.Command{
Name: "new",
Aliases: []string{"n"},
Usage: "generate a new project from a given template url",
ArgsUsage: "<URL>",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "directory",
Aliases: []string{"d"},
Usage: "Set destination to `DIR`",
Value: "./",
},
&cli.StringFlag{
Name: "manifest",
Aliases: []string{"m"},
Usage: "The scaffold manifest `FILE`",
Value: "scaffold.yml",
},
},
Action: newProjectAction,
}
}
func newProjectAction(c *cli.Context) error {
rawURL := c.Args().First()
projectURL, err := url.Parse(rawURL)
if err != nil {
return errors.Wrap(err, "could not parse url")
}
availableFetchers := []project.Fetcher{
project.NewGitFetcher(),
project.NewLocalFetcher(),
}
var fetcher project.Fetcher
for _, f := range availableFetchers {
if f.Match(projectURL) {
fetcher = f
break
}
}
vfs, err := fetcher.Fetch(projectURL)
if err != nil {
return errors.Wrap(err, "could not fetch project")
}
manifestFile := c.String("manifest")
manifestStat, err := vfs.Stat(manifestFile)
if err != nil && !os.IsNotExist(err) {
return errors.Wrap(err, "could not stat manifest file")
}
templateData := make(map[string]interface{})
if os.IsNotExist(err) {
log.Println("Could not find scaffold manifest.")
} else {
if manifestStat.IsDir() {
return errors.New("scaffold manifest is not a file")
}
log.Println("Loading template scaffold manifest...")
}
directory := c.String("directory")
return template.CopyDir(vfs, ".", directory, &template.Option{
TemplateData: templateData,
TemplateExt: ".gotpl",
IgnorePatterns: []string{manifestFile},
})
}
func promptForProjectName() (string, error) {
validate := func(input string) error {
if input == "" {
return errors.New("Project name cannot be empty")
}
return nil
}
prompt := promptui.Prompt{
Label: "Project Name",
Validate: validate,
}
return prompt.Run()
}
func promptForProjectNamespace() (string, error) {
validate := func(input string) error {
if input == "" {
return errors.New("Project namespace cannot be empty")
}
return nil
}
prompt := promptui.Prompt{
Label: "Project namespace",
Validate: validate,
}
return prompt.Run()
}
func promptForProjectType() (string, error) {
prompt := promptui.Select{
Label: "Project Type",
Items: []string{"web"},
}
_, result, err := prompt.Run()
return result, err
}

82
internal/fs/walk.go Normal file
View File

@ -0,0 +1,82 @@
package fs
import (
"os"
"path/filepath"
"sort"
"strings"
vfs "gopkg.in/src-d/go-billy.v4"
)
// Walk walks the file tree rooted at root, calling walkFunc for each file or
// directory in the tree, including root. All errors that arise visiting files
// and directories are filtered by walkFn. The files are walked in lexical
// order, which makes the output deterministic but means that for very
// large directories Walk can be inefficient.
// Walk does not follow symbolic links.
func Walk(fs vfs.Filesystem, root string, walkFunc filepath.WalkFunc) error {
info, err := fs.Lstat(root)
if err != nil {
err = walkFunc(root, nil, err)
} else {
err = walk(fs, root, info, walkFunc)
}
if err == filepath.SkipDir {
return nil
}
return err
}
// readDirNames reads the directory named by dirname and returns
// a sorted list of directory entries.
func readDirNames(fs vfs.Filesystem, dirname string) ([]string, error) {
infos, err := fs.ReadDir(dirname)
if err != nil {
return nil, err
}
names := make([]string, 0, len(infos))
for _, info := range infos {
names = append(names, info.Name())
}
sort.Strings(names)
return names, nil
}
// walk recursively descends path, calling walkFunc.
func walk(fs vfs.Filesystem, path string, info os.FileInfo, walkFunc filepath.WalkFunc) error {
if !info.IsDir() {
return walkFunc(path, info, nil)
}
names, err := readDirNames(fs, path)
err1 := walkFunc(path, info, err)
// If err != nil, walk can't walk into this directory.
// err1 != nil means walkFn want walk to skip this directory or stop walking.
// Therefore, if one of err and err1 isn't nil, walk will return.
if err != nil || err1 != nil {
// The caller's behavior is controlled by the return value, which is decided
// by walkFn. walkFn may ignore err and return nil.
// If walkFn returns SkipDir, it will be handled by the caller.
// So walk should return whatever walkFn returns.
return err1
}
for _, name := range names {
filename := strings.Join([]string{path, name}, string(os.PathSeparator))
fileInfo, err := fs.Lstat(filename)
if err != nil {
if err := walkFunc(filename, fileInfo, err); err != nil && err != filepath.SkipDir {
return err
}
} else {
err = walk(fs, filename, fileInfo, walkFunc)
if err != nil {
if !fileInfo.IsDir() || err != filepath.SkipDir {
return err
}
}
}
}
return nil
}

View File

@ -0,0 +1,12 @@
package project
import (
"net/url"
"gopkg.in/src-d/go-billy.v4"
)
type Fetcher interface {
Match(*url.URL) bool
Fetch(*url.URL) (billy.Filesystem, error)
}

98
internal/project/git.go Normal file
View File

@ -0,0 +1,98 @@
package project
import (
"log"
"net/url"
"github.com/pkg/errors"
"gopkg.in/src-d/go-billy.v4"
"gopkg.in/src-d/go-billy.v4/memfs"
git "gopkg.in/src-d/go-git.v4"
"gopkg.in/src-d/go-git.v4/plumbing"
"gopkg.in/src-d/go-git.v4/plumbing/transport"
"gopkg.in/src-d/go-git.v4/plumbing/transport/http"
"gopkg.in/src-d/go-git.v4/storage/memory"
)
const GitScheme = "git"
type GitFetcher struct{}
func (f *GitFetcher) Fetch(url *url.URL) (billy.Filesystem, error) {
fs := memfs.New()
var auth transport.AuthMethod
if user := url.User; user != nil {
user := url.User
basicAuth := &http.BasicAuth{
Username: user.Username(),
}
password, exists := user.Password()
if exists {
basicAuth.Password = password
}
auth = basicAuth
}
if url.Scheme == "" {
url.Scheme = "https"
}
branchName := plumbing.NewBranchReferenceName("master")
if url.Fragment != "" {
branchName = plumbing.NewBranchReferenceName(url.Fragment)
url.Fragment = ""
}
log.Printf("Cloning repository '%s'...", url.String())
repo, err := git.Clone(memory.NewStorage(), fs, &git.CloneOptions{
URL: url.String(),
Auth: auth,
ReferenceName: branchName,
})
if err != nil {
if err == transport.ErrRepositoryNotFound {
return nil, errors.Wrapf(err, "could not find repository")
}
return nil, errors.Wrap(err, "could not clone repository")
}
worktree, err := repo.Worktree()
if err != nil {
return nil, errors.Wrap(err, "could not retrieve worktree")
}
log.Printf("Checking out branch '%s'...", branchName)
err = worktree.Checkout(&git.CheckoutOptions{
Force: true,
Branch: branchName,
})
if err != nil {
return nil, errors.Wrapf(err, "could not checkout branch '%s'", branchName)
}
return fs, nil
}
func (f *GitFetcher) Match(url *url.URL) bool {
if url.Scheme == GitScheme {
return true
}
isFilesystemPath := isFilesystemPath(url.Path)
if url.Scheme == "" && !isFilesystemPath {
return true
}
return false
}
func NewGitFetcher() *GitFetcher {
return &GitFetcher{}
}

View File

@ -0,0 +1,51 @@
package project
import (
"net/url"
"testing"
)
func TestGitFetcher(t *testing.T) {
git := NewGitFetcher()
projectURL, err := url.Parse("forge.cadoles.com/wpetit/goweb")
if err != nil {
t.Fatal(err)
}
fs, err := git.Fetch(projectURL)
if err != nil {
t.Fatal(err)
}
if fs == nil {
t.Fatal("fs should not be nil")
}
}
func TestGitMatch(t *testing.T) {
testCases := []struct {
RawURL string
ShouldMatch bool
}{
{"git://wpetit/scaffold", true},
{"forge.cadoles.com/wpetit/scaffold", true},
}
git := NewGitFetcher()
for _, tc := range testCases {
func(rawURL string, shouldMatch bool) {
t.Run(rawURL, func(t *testing.T) {
projectURL, err := url.Parse(rawURL)
if err != nil {
t.Fatal(err)
}
if e, g := shouldMatch, git.Match(projectURL); g != e {
t.Errorf("git.Match(url): expected '%v', got '%v'", e, g)
}
})
}(tc.RawURL, tc.ShouldMatch)
}
}

33
internal/project/local.go Normal file
View File

@ -0,0 +1,33 @@
package project
import (
"net/url"
"strings"
"gopkg.in/src-d/go-billy.v4"
)
const LocalScheme = "local"
const FileScheme = "file"
type LocalFetcher struct{}
func (f *LocalFetcher) Fetch(url *url.URL) (billy.Filesystem, error) {
return nil, nil
}
func (f *LocalFetcher) Match(url *url.URL) bool {
if url.Scheme == LocalScheme || url.Scheme == FileScheme {
return true
}
return isFilesystemPath(url.Path)
}
func NewLocalFetcher() *LocalFetcher {
return &LocalFetcher{}
}
func isFilesystemPath(path string) bool {
return strings.HasPrefix(path, "./") || strings.HasPrefix(path, "/")
}

View File

@ -0,0 +1,33 @@
package project
import (
"net/url"
"testing"
)
func TestLocalMatch(t *testing.T) {
testCases := []struct {
RawURL string
ShouldMatch bool
}{
{"local://wpetit/scaffold", true},
{"./forge.cadoles.com/wpetit/scaffold", true},
}
local := NewLocalFetcher()
for _, tc := range testCases {
func(rawURL string, shouldMatch bool) {
t.Run(rawURL, func(t *testing.T) {
projectURL, err := url.Parse(rawURL)
if err != nil {
t.Fatal(err)
}
if e, g := shouldMatch, local.Match(projectURL); g != e {
t.Errorf("local.Match(url): expected '%v', got '%v'", e, g)
}
})
}(tc.RawURL, tc.ShouldMatch)
}
}

170
internal/template/copy.go Normal file
View File

@ -0,0 +1,170 @@
package template
import (
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"text/template"
"gitlab.com/wpetit/scaffold/internal/fs"
"gopkg.in/src-d/go-billy.v4"
"github.com/Masterminds/sprig"
"github.com/pkg/errors"
)
func CopyDir(vfs billy.Filesystem, baseDir string, dst string, opts *Option) error {
if opts == nil {
opts = &Option{}
}
baseDir = filepath.Clean(baseDir)
dst = filepath.Clean(dst)
_, err := os.Stat(dst)
if err != nil && !os.IsNotExist(err) {
return err
}
if err := os.MkdirAll(dst, 0755); err != nil {
return errors.Wrapf(err, "could not create directory '%s'", dst)
}
err = fs.Walk(vfs, baseDir, func(srcPath string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if srcPath == baseDir {
return nil
}
for _, p := range opts.IgnorePatterns {
match, err := filepath.Match(p, srcPath)
if err != nil {
return errors.Wrap(err, "could not match ignored file")
}
if match {
log.Printf("Ignoring %s.", srcPath)
return nil
}
}
relSrcPath, err := filepath.Rel(baseDir, srcPath)
if err != nil {
return err
}
dstPath := filepath.Join(dst, relSrcPath)
log.Printf("relSrcPath: %s, dstPath: %s", relSrcPath, dstPath)
if info.IsDir() {
log.Printf("creating dir '%s'", dstPath)
if err := os.MkdirAll(dstPath, 0755); err != nil {
return errors.Wrapf(err, "could not create directory '%s'", dstPath)
}
return nil
}
err = CopyFile(vfs, srcPath, dstPath, opts)
if err != nil {
return errors.Wrapf(err, "could not copy file '%s'", srcPath)
}
return nil
})
if err != nil {
return errors.Wrapf(err, "could not walk source directory '%s'", baseDir)
}
return nil
}
func CopyFile(vfs billy.Filesystem, src, dst string, opts *Option) (err error) {
if opts == nil {
opts = &Option{}
}
if !strings.HasSuffix(src, opts.TemplateExt) {
return copyFile(vfs, src, dst)
}
in, err := vfs.Open(src)
if err != nil {
return err
}
defer in.Close()
templateData, err := ioutil.ReadAll(in)
if err != nil {
return err
}
tmpl, err := template.New(filepath.Base(src)).Funcs(sprig.TxtFuncMap()).Parse(string(templateData))
if err != nil {
return err
}
dst = strings.TrimSuffix(dst, opts.TemplateExt)
log.Printf("templating file from '%s' to '%s'", src, dst)
out, err := os.Create(dst)
if err != nil {
return err
}
defer func() {
if e := out.Close(); e != nil {
err = e
}
}()
opts.TemplateData["SourceFile"] = src
opts.TemplateData["DestFile"] = dst
if err := tmpl.Execute(out, opts.TemplateData); err != nil {
return err
}
return nil
}
func copyFile(vfs billy.Filesystem, src, dst string) (err error) {
log.Printf("copying file '%s' to '%s'", src, dst)
in, err := vfs.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer func() {
if e := out.Close(); e != nil {
err = e
}
}()
_, err = io.Copy(out, in)
if err != nil {
return err
}
err = out.Sync()
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,11 @@
package template
type Manifest struct {
Version string `yaml:"version"`
Vars []Var `yaml:"vars"`
}
type Var struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
}

View File

@ -0,0 +1,7 @@
package template
type Option struct {
IgnorePatterns []string
TemplateData map[string]interface{}
TemplateExt string
}