From b7bdd7bbea6153dfe0e3c286b72139cdd19c9b8b Mon Sep 17 00:00:00 2001 From: William Petit Date: Thu, 20 Feb 2020 08:31:22 +0100 Subject: [PATCH] Initial commit --- .gitignore | 3 + Makefile | 25 +++++ cmd/scaffold/main.go | 21 ++++ go.mod | 19 ++++ go.sum | 106 ++++++++++++++++++++ internal/command/all.go | 9 ++ internal/command/new_project.go | 139 ++++++++++++++++++++++++++ internal/fs/walk.go | 82 +++++++++++++++ internal/project/fetcher.go | 12 +++ internal/project/git.go | 98 ++++++++++++++++++ internal/project/git_test.go | 51 ++++++++++ internal/project/local.go | 33 +++++++ internal/project/local_test.go | 33 +++++++ internal/template/copy.go | 170 ++++++++++++++++++++++++++++++++ internal/template/manifest.go | 11 +++ internal/template/option.go | 7 ++ modd.conf | 7 ++ 17 files changed, 826 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 cmd/scaffold/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/command/all.go create mode 100644 internal/command/new_project.go create mode 100644 internal/fs/walk.go create mode 100644 internal/project/fetcher.go create mode 100644 internal/project/git.go create mode 100644 internal/project/git_test.go create mode 100644 internal/project/local.go create mode 100644 internal/project/local_test.go create mode 100644 internal/template/copy.go create mode 100644 internal/template/manifest.go create mode 100644 internal/template/option.go create mode 100644 modd.conf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..df860f8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/coverage +/bin +rice-box.go \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..59d5249 --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +build: + CGO_ENABLED=0 go build -o ./bin/scaffold ./cmd/scaffold + +test: + go clean -testcache + go test -v -race ./... + +watch: + modd + +deps: + go get -u golang.org/x/tools/cmd/godoc + go get -u github.com/cortesi/modd/cmd/modd + go get -u github.com/golangci/golangci-lint/cmd/golangci-lint + +coverage: + @script/coverage + +lint: + golangci-lint run --tests=false --enable-all + +clean: + rm -rf ./bin ./release ./coverage + +.PHONY: test clean lint coverage doc \ No newline at end of file diff --git a/cmd/scaffold/main.go b/cmd/scaffold/main.go new file mode 100644 index 0000000..7a87c3b --- /dev/null +++ b/cmd/scaffold/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "log" + "os" + + "gitlab.com/wpetit/scaffold/internal/command" + + "github.com/urfave/cli/v2" +) + +func main() { + app := &cli.App{ + Usage: "generate source code for goweb based projects", + Commands: command.All(), + } + + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..dbf3ff0 --- /dev/null +++ b/go.mod @@ -0,0 +1,19 @@ +module gitlab.com/wpetit/scaffold + +go 1.13 + +require ( + github.com/Masterminds/goutils v1.1.0 // indirect + github.com/Masterminds/semver v1.5.0 // indirect + github.com/Masterminds/sprig v2.22.0+incompatible + github.com/google/uuid v1.1.1 // indirect + github.com/huandu/xstrings v1.3.0 // indirect + github.com/imdario/mergo v0.3.8 // indirect + github.com/manifoldco/promptui v0.7.0 + github.com/mitchellh/copystructure v1.0.0 // indirect + github.com/pkg/errors v0.9.1 + github.com/urfave/cli/v2 v2.1.1 + golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6 // indirect + gopkg.in/src-d/go-billy.v4 v4.3.2 + gopkg.in/src-d/go-git.v4 v4.13.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..29557d2 --- /dev/null +++ b/go.sum @@ -0,0 +1,106 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg= +github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= +github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= +github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= +github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/huandu/xstrings v1.3.0 h1:gvV6jG9dTgFEncxo+AF7PH6MZXi/vZl25owA/8Dg8Wo= +github.com/huandu/xstrings v1.3.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= +github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= +github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= +github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= +github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw= +github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/manifoldco/promptui v0.7.0 h1:3l11YT8tm9MnwGFQ4kETwkzpAwY2Jt9lCrumCUW4+z4= +github.com/manifoldco/promptui v0.7.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ= +github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4= +github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/urfave/cli v1.22.2 h1:gsqYFH8bb9ekPA12kRo0hfjngWQjkJPlN9R0N78BoUo= +github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k= +github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= +github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= +github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= +golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6 h1:Sy5bstxEqwwbYs6n0/pBuxKENqOeZUgD45Gp3Q3pqLg= +golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e h1:D5TXcfTk7xF7hvieo4QErS3qqCB4teTffacDWr7CI+0= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= +gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= +gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= +gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE= +gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/command/all.go b/internal/command/all.go new file mode 100644 index 0000000..2b95dfb --- /dev/null +++ b/internal/command/all.go @@ -0,0 +1,9 @@ +package command + +import "github.com/urfave/cli/v2" + +func All() []*cli.Command { + return []*cli.Command{ + newProjectCommand(), + } +} diff --git a/internal/command/new_project.go b/internal/command/new_project.go new file mode 100644 index 0000000..8e2f44e --- /dev/null +++ b/internal/command/new_project.go @@ -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: "", + 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 +} diff --git a/internal/fs/walk.go b/internal/fs/walk.go new file mode 100644 index 0000000..47025c0 --- /dev/null +++ b/internal/fs/walk.go @@ -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 +} diff --git a/internal/project/fetcher.go b/internal/project/fetcher.go new file mode 100644 index 0000000..9c78b7f --- /dev/null +++ b/internal/project/fetcher.go @@ -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) +} diff --git a/internal/project/git.go b/internal/project/git.go new file mode 100644 index 0000000..0242c94 --- /dev/null +++ b/internal/project/git.go @@ -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{} +} diff --git a/internal/project/git_test.go b/internal/project/git_test.go new file mode 100644 index 0000000..9b1fa4c --- /dev/null +++ b/internal/project/git_test.go @@ -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) + } +} diff --git a/internal/project/local.go b/internal/project/local.go new file mode 100644 index 0000000..a0ddcfb --- /dev/null +++ b/internal/project/local.go @@ -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, "/") +} diff --git a/internal/project/local_test.go b/internal/project/local_test.go new file mode 100644 index 0000000..02efd9b --- /dev/null +++ b/internal/project/local_test.go @@ -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) + } +} diff --git a/internal/template/copy.go b/internal/template/copy.go new file mode 100644 index 0000000..49437d1 --- /dev/null +++ b/internal/template/copy.go @@ -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 +} diff --git a/internal/template/manifest.go b/internal/template/manifest.go new file mode 100644 index 0000000..03fa24a --- /dev/null +++ b/internal/template/manifest.go @@ -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"` +} diff --git a/internal/template/option.go b/internal/template/option.go new file mode 100644 index 0000000..cb24c3f --- /dev/null +++ b/internal/template/option.go @@ -0,0 +1,7 @@ +package template + +type Option struct { + IgnorePatterns []string + TemplateData map[string]interface{} + TemplateExt string +} diff --git a/modd.conf b/modd.conf new file mode 100644 index 0000000..3aab55f --- /dev/null +++ b/modd.conf @@ -0,0 +1,7 @@ +**/*.go { + prep: make build +} + +**/*.go { + prep: make test +} \ No newline at end of file