Browse Source

Initial Commit

Signed-off-by: jolheiser <john.olheiser@gmail.com>
pull/7/head
jolheiser 1 year ago
commit
6de6e4ef70
Signed by: jolheiser GPG Key ID: B853ADA5DA7BBF7A
  1. 6
      .gitignore
  2. 7
      LICENSE
  3. 18
      Makefile
  4. 76
      cmd/app.go
  5. 69
      cmd/download.go
  6. 7
      cmd/flags/flags.go
  7. 55
      cmd/init.go
  8. 37
      cmd/list.go
  9. 36
      cmd/remove.go
  10. 58
      cmd/save.go
  11. 104
      cmd/source.go
  12. 39
      cmd/test.go
  13. 45
      cmd/update.go
  14. 41
      cmd/use.go
  15. 32
      docs.go
  16. 12
      go.mod
  17. 126
      go.sum
  18. 17
      main.go
  19. 29
      registry/error.go
  20. 204
      registry/registry.go
  21. 15
      registry/source.go
  22. 141
      registry/template.go

6
.gitignore

@ -0,0 +1,6 @@
# GoLand
.idea/
# Binaries
/tmpl
/tmpl.exe

7
LICENSE

@ -0,0 +1,7 @@
Copyright 2020 John Olheiser
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

18
Makefile

@ -0,0 +1,18 @@
GO ?= go
VERSION ?= $(shell git describe --tags --always | sed 's/-/+/' | sed 's/^v//')
.PHONY: build
build:
$(GO) build -ldflags '-s -w -X "go.jolheiser.com/tmpl/cmd.Version=$(VERSION)"'
.PHONY: vet
vet:
$(GO) vet ./...
.PHONY: fmt
fmt:
$(GO) fmt ./...
.PHONY: test
test:
$(GO) test -race ./...

76
cmd/app.go

@ -0,0 +1,76 @@
package cmd
import (
"os"
"path/filepath"
"go.jolheiser.com/tmpl/cmd/flags"
"github.com/urfave/cli/v2"
"go.jolheiser.com/beaver"
)
var (
Version = "develop"
defaultDir string
)
func init() {
home, err := os.UserHomeDir()
if err != nil {
beaver.Error("could not locate user's home directory, tmpl will use temp dir for registry")
return
}
defaultDir = filepath.Join(home, ".tmpl")
}
func NewApp() *cli.App {
app := cli.NewApp()
app.Name = "tmpl"
app.Usage = "Template automation"
app.Description = "Template automation"
app.Version = Version
app.Flags = []cli.Flag{
&cli.BoolFlag{
Name: "debug",
Aliases: []string{"d"},
Usage: "Debug mode",
Destination: &flags.Debug,
},
&cli.StringFlag{
Name: "registry",
Aliases: []string{"r"},
Usage: "Registry directory of tmpl",
Value: defaultDir,
Destination: &flags.Registry,
},
&cli.StringFlag{
Name: "source",
Aliases: []string{"s"},
Usage: "Short-name source to use",
Destination: &flags.Source,
},
}
app.Before = before
app.Commands = []*cli.Command{
Download,
Init,
List,
Remove,
Save,
Source,
Test,
Update,
Use,
}
return app
}
func before(ctx *cli.Context) error {
if ctx.Bool("debug") {
beaver.Console.Level = beaver.DEBUG
}
return nil
}

69
cmd/download.go

@ -0,0 +1,69 @@
package cmd
import (
"errors"
"fmt"
"strings"
"go.jolheiser.com/tmpl/cmd/flags"
"go.jolheiser.com/tmpl/registry"
"github.com/urfave/cli/v2"
"go.jolheiser.com/beaver"
)
var Download = &cli.Command{
Name: "download",
Usage: "Download a template",
Description: "Download a template and save it to the local registry",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "branch",
Aliases: []string{"b"},
Usage: "Branch to clone",
Value: "main",
EnvVars: []string{"TMPL_BRANCH"},
},
},
Action: runDownload,
}
func runDownload(ctx *cli.Context) error {
if ctx.NArg() < 2 {
return errors.New("<repo> <name>")
}
reg, err := registry.Open(flags.Registry)
if err != nil {
return err
}
var source *registry.Source
if flags.Source != "" {
for _, s := range reg.Sources {
if strings.EqualFold(s.Name, flags.Source) {
source = s
break
}
}
if source == nil {
return fmt.Errorf("could not find source for %s", flags.Source)
}
}
cloneURL := ctx.Args().First()
if !strings.HasSuffix(cloneURL, ".git") {
cloneURL += ".git"
}
if source != nil {
cloneURL = source.CloneURL(cloneURL)
}
t, err := reg.AddTemplate(ctx.Args().Get(1), cloneURL, ctx.String("branch"))
if err != nil {
return err
}
beaver.Infof("Added new template %s", t.Name)
return nil
}

7
cmd/flags/flags.go

@ -0,0 +1,7 @@
package flags
var (
Debug bool
Registry string
Source string
)

55
cmd/init.go

@ -0,0 +1,55 @@
package cmd
import (
"errors"
"os"
"github.com/urfave/cli/v2"
"go.jolheiser.com/beaver"
)
var Init = &cli.Command{
Name: "init",
Usage: "Initialize a template",
Description: "Initializes a template structure for creating a new tmpl template",
Action: runInit,
}
func runInit(_ *cli.Context) error {
if _, err := os.Lstat("template.toml"); !os.IsNotExist(err) {
if err != nil {
return err
}
return errors.New("template.toml already detected, aborting initialization")
}
if fi, err := os.Lstat("template"); !os.IsNotExist(err) {
if err != nil {
return err
}
if !fi.IsDir() {
return errors.New("template file found instead of directory, aborting initialization")
}
return errors.New("template directory already detected, aborting initialization")
}
fi, err := os.Create("template.toml")
if err != nil {
return err
}
if _, err := fi.WriteString(comments); err != nil {
return err
}
if err := os.Mkdir("template", os.ModePerm); err != nil {
return err
}
beaver.Info("Template initialized!")
return fi.Close()
}
var comments = `# template.toml
# Write any template args here to prompt the user for, giving any defaults/options as applicable
name = "MyProject"
lang = ["Go", "Rust", "Python"]
`

37
cmd/list.go

@ -0,0 +1,37 @@
package cmd
import (
"fmt"
"os"
"text/tabwriter"
"go.jolheiser.com/tmpl/cmd/flags"
"go.jolheiser.com/tmpl/registry"
"github.com/urfave/cli/v2"
)
var List = &cli.Command{
Name: "list",
Usage: "List templates in the registry",
Description: "List all usable templates currently downloaded in the registry",
Action: runList,
}
func runList(_ *cli.Context) error {
reg, err := registry.Open(flags.Registry)
if err != nil {
return err
}
wr := tabwriter.NewWriter(os.Stdout, 0, 8, 0, '\t', 0)
fmt.Println("Templates")
for _, t := range reg.Templates {
if _, err := fmt.Fprintf(wr, "%s\t%s@%s\t%s", t.Name, t.Repository, t.Branch, t.Created); err != nil {
return err
}
}
fmt.Println()
return nil
}

36
cmd/remove.go

@ -0,0 +1,36 @@
package cmd
import (
"errors"
"go.jolheiser.com/tmpl/cmd/flags"
"go.jolheiser.com/tmpl/registry"
"github.com/urfave/cli/v2"
"go.jolheiser.com/beaver"
)
var Remove = &cli.Command{
Name: "remove",
Usage: "Remove a template",
Description: "Remove a template from the registry",
Action: runRemove,
}
func runRemove(ctx *cli.Context) error {
if ctx.NArg() < 1 {
return errors.New("<name>")
}
reg, err := registry.Open(flags.Registry)
if err != nil {
return err
}
if err := reg.RemoveTemplate(ctx.Args().First()); err != nil {
return err
}
beaver.Infof("Successfully removed %s", ctx.Args().First())
return nil
}

58
cmd/save.go

@ -0,0 +1,58 @@
package cmd
import (
"errors"
"path/filepath"
"strings"
"go.jolheiser.com/tmpl/cmd/flags"
"go.jolheiser.com/tmpl/registry"
"github.com/urfave/cli/v2"
"go.jolheiser.com/beaver"
)
var Save = &cli.Command{
Name: "save",
Usage: "Save a local template",
Description: "Save a local template to the registry",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "branch",
Aliases: []string{"b"},
Usage: "Branch to clone",
Value: "main",
EnvVars: []string{"TMPL_BRANCH"},
},
},
Action: runSave,
}
func runSave(ctx *cli.Context) error {
if ctx.NArg() < 2 {
return errors.New("<path> <name>")
}
reg, err := registry.Open(flags.Registry)
if err != nil {
return err
}
// Did the user give us the root path, or the .git directory?
localPath := ctx.Args().First()
if !strings.HasSuffix(localPath, ".git") {
localPath = filepath.Join(localPath, ".git")
}
localPath, err = filepath.Abs(localPath)
if err != nil {
return err
}
t, err := reg.AddTemplate(ctx.Args().Get(1), localPath, ctx.String("branch"))
if err != nil {
return err
}
beaver.Infof("Added new template %s", t.Name)
return nil
}

104
cmd/source.go

@ -0,0 +1,104 @@
package cmd
import (
"errors"
"fmt"
"os"
"text/tabwriter"
"go.jolheiser.com/tmpl/cmd/flags"
"go.jolheiser.com/tmpl/registry"
"github.com/urfave/cli/v2"
"go.jolheiser.com/beaver"
)
var (
Source = &cli.Command{
Name: "source",
Usage: "Commands for working with sources",
Description: "Commands for working with sources, short-hand flags for easier downloads",
Action: SourceList.Action,
Subcommands: []*cli.Command{
SourceList,
SourceAdd,
SourceRemove,
},
}
SourceList = &cli.Command{
Name: "list",
Usage: "List available sources",
Description: "List all available sources in the registry",
Action: runSourceList,
}
SourceAdd = &cli.Command{
Name: "add",
Usage: "AddTemplate a source",
Description: "AddTemplate a new source to the registry",
Action: runSourceAdd,
}
SourceRemove = &cli.Command{
Name: "remove",
Usage: "RemoveTemplate a source",
Description: "RemoveTemplate a source from the registry",
Action: runSourceRemove,
}
)
func runSourceList(_ *cli.Context) error {
reg, err := registry.Open(flags.Registry)
if err != nil {
return err
}
wr := tabwriter.NewWriter(os.Stdout, 0, 8, 0, '\t', 0)
fmt.Println("Sources")
for _, s := range reg.Sources {
if _, err := fmt.Fprintf(wr, "%s\t%s", s.Name, s.URL); err != nil {
return err
}
}
fmt.Println()
return nil
}
func runSourceAdd(ctx *cli.Context) error {
if ctx.NArg() < 2 {
return errors.New("<repo> <name>")
}
reg, err := registry.Open(flags.Registry)
if err != nil {
return err
}
s, err := reg.AddSource(ctx.Args().First(), ctx.Args().Get(1))
if err != nil {
return err
}
beaver.Infof("Added new source %s", s.Name)
return nil
}
func runSourceRemove(ctx *cli.Context) error {
if ctx.NArg() < 1 {
return errors.New("<name>")
}
reg, err := registry.Open(flags.Registry)
if err != nil {
return err
}
if err := reg.RemoveSource(ctx.Args().First()); err != nil {
return err
}
beaver.Infof("Successfully removed source for %s", ctx.Args().First())
return nil
}

39
cmd/test.go

@ -0,0 +1,39 @@
package cmd
import (
"os"
"github.com/urfave/cli/v2"
"go.jolheiser.com/beaver"
)
var Test = &cli.Command{
Name: "test",
Usage: "Test if a directory is a valid template",
Description: "Test whether the current directory is valid for use with tmpl",
Action: runTest,
}
func runTest(_ *cli.Context) error {
var errs []string
if _, err := os.Lstat("template.toml"); err != nil {
errs = append(errs, "could not find template.toml")
}
fi, err := os.Lstat("template")
if err != nil {
errs = append(errs, "no template directory found")
}
if err == nil && !fi.IsDir() {
errs = append(errs, "template path is a file, not a directory")
}
if len(errs) > 0 {
for _, err := range errs {
beaver.Error(err)
}
return nil
}
beaver.Info("this is a valid tmpl template")
return nil
}

45
cmd/update.go

@ -0,0 +1,45 @@
package cmd
import (
"errors"
"go.jolheiser.com/tmpl/cmd/flags"
"go.jolheiser.com/tmpl/registry"
"github.com/urfave/cli/v2"
"go.jolheiser.com/beaver"
)
var Update = &cli.Command{
Name: "update",
Usage: "Update a template",
Description: "Update a template in the registry from the original source",
Action: runUpdate,
}
func runUpdate(ctx *cli.Context) error {
if ctx.NArg() < 1 {
return errors.New("<name>")
}
reg, err := registry.Open(flags.Registry)
if err != nil {
return err
}
tmpl, err := reg.GetTemplate(ctx.Args().First())
if err != nil {
return err
}
if err := reg.RemoveTemplate(tmpl.Name); err != nil {
return err
}
if _, err := reg.AddTemplate(tmpl.Name, tmpl.Repository, tmpl.Branch); err != nil {
return err
}
beaver.Infof("Successfully updated %s", tmpl.Name)
return nil
}

41
cmd/use.go

@ -0,0 +1,41 @@
package cmd
import (
"errors"
"go.jolheiser.com/tmpl/cmd/flags"
"go.jolheiser.com/tmpl/registry"
"github.com/urfave/cli/v2"
"go.jolheiser.com/beaver"
)
var Use = &cli.Command{
Name: "use",
Usage: "Use a template",
Description: "Use (execute) a template from the registry",
Action: runUse,
}
func runUse(ctx *cli.Context) error {
if ctx.NArg() < 2 {
return errors.New("<name> <dest>")
}
reg, err := registry.Open(flags.Registry)
if err != nil {
return err
}
tmpl, err := reg.GetTemplate(ctx.Args().First())
if err != nil {
return err
}
if err := tmpl.Execute(ctx.Args().Get(1)); err != nil {
return err
}
beaver.Infof("Successfully executed %s", tmpl.Name)
return nil
}

32
docs.go

@ -0,0 +1,32 @@
// +build docs
package main
import (
"os"
"strings"
"go.jolheiser.com/tmpl/cmd"
)
func main() {
app := cmd.NewApp()
fi, err := os.Create("DOCS.md")
if err != nil {
panic(err)
}
defer fi.Close()
md, err := app.ToMarkdown()
if err != nil {
panic(err)
}
// CLI ToMarkdown issue related to man-pages
md = md[strings.Index(md, "#"):]
if _, err := fi.WriteString(md); err != nil {
panic(err)
}
}

12
go.mod

@ -0,0 +1,12 @@
module go.jolheiser.com/tmpl
go 1.15
require (
github.com/AlecAivazis/survey/v2 v2.2.2
github.com/go-git/go-git/v5 v5.2.0
github.com/mholt/archiver/v3 v3.5.0
github.com/pelletier/go-toml v1.8.1
github.com/urfave/cli/v2 v2.3.0
go.jolheiser.com/beaver v1.0.2
)

126
go.sum

@ -0,0 +1,126 @@
github.com/AlecAivazis/survey v1.8.8 h1:Y4yypp763E8cbqb5RBqZhGgkCFLRFnbRBHrxnpMMsgQ=
github.com/AlecAivazis/survey/v2 v2.2.2 h1:1I4qBrNsHQE+91tQCqVlfrKe9DEL65949d1oKZWVELY=
github.com/AlecAivazis/survey/v2 v2.2.2/go.mod h1:9FJRdMdDm8rnT+zHVbvQT2RTSTLq0Ttd6q3Vl2fahjk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
github.com/andybalholm/brotli v1.0.0 h1:7UCwP93aiSfvWpapti8g88vVVGp2qqtGyePsSuDafo4=
github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
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/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.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
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/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM=
github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
github.com/go-git/go-git v1.0.0 h1:YcN9iDGDoXuIw0vHls6rINwV416HYa0EB2X+RBsyYp4=
github.com/go-git/go-git v4.7.0+incompatible h1:+W9rgGY4DOKKdX2x6HxSR7HNeTxqiKrOvKnuittYVdA=
github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw=
github.com/go-git/go-git/v5 v5.2.0 h1:YPBLG/3UK1we1ohRkncLjaXWLW+HKp5QNM/jTli2JgI=
github.com/go-git/go-git/v5 v5.2.0/go.mod h1:kh02eMX+wdqqxgNMEyq8YgwlIOsDOa9homkUq1PoTMs=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
github.com/imdario/mergo v0.3.9/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/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
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/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.10.10 h1:a/y8CglcM7gLGYmlbP/stPE5sR3hbhFRUjCBfd/0B3I=
github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.4 h1:TQ7CNpYKovDOmqzRHKxJh0BeaBI7UdQZYc6p7pMQh1A=
github.com/klauspost/pgzip v1.2.4/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
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.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mholt/archiver v1.1.2 h1:xukR55YIrnhDHp10lrNtRSsAK5THpWrOCuviweNSBw4=
github.com/mholt/archiver v3.1.1+incompatible h1:1dCVxuqs0dJseYEhi5pl7MYPH9zDa1wBi7mF09cbNkU=
github.com/mholt/archiver/v3 v3.5.0 h1:nE8gZIrw66cu4osS/U7UW7YDuGMHssxKutU8IfWxwWE=
github.com/mholt/archiver/v3 v3.5.0/go.mod h1:qqTTPUK/HZPFgFQ/TJ3BzvTpF/dPtFVJXdQbCmeMxwc=
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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ=
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM=
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
github.com/pierrec/lz4/v4 v4.0.3 h1:vNQKSVZNYUEAvRY9FaUXAF1XPbSOHJtDTiP41kzDz2E=
github.com/pierrec/lz4/v4 v4.0.3/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/errors v0.8.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.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
github.com/ulikunitz/xz v0.5.7 h1:YvTNdFzX6+W5m9msiYg/zpkSURPPtOlzbqYjrFn7Yt4=
github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU=
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
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=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
go.jolheiser.com/beaver v1.0.2 h1:KA2D6iO8MQhZi1nZYi/Chak/f1Cxfrs6b1XO623+Khk=
go.jolheiser.com/beaver v1.0.2/go.mod h1:7X4F5+XOGSC3LejTShoBdqtRCnPWcnRgmYGmG3EKW8g=
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-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
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-20190222072716-a9d3bda3a223/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-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

17
main.go

@ -0,0 +1,17 @@
package main
import (
"os"
"go.jolheiser.com/tmpl/cmd"
"go.jolheiser.com/beaver"
)
func main() {
app := cmd.NewApp()
if err := app.Run(os.Args); err != nil {
beaver.Fatal(err)
}
}

29
registry/error.go

@ -0,0 +1,29 @@
package registry
import "fmt"
type ErrTemplateNotFound struct {
Name string
}
func (e ErrTemplateNotFound) Error() string {
return fmt.Sprintf("template not found for %s", e.Name)
}
func IsErrTemplateNotFound(err error) bool {
_, ok := err.(ErrTemplateNotFound)
return ok
}
type ErrSourceNotFound struct {
Name string
}
func (e ErrSourceNotFound) Error() string {
return fmt.Sprintf("Source not found for %s", e.Name)
}
func IsErrSourceNotFound(err error) bool {
_, ok := err.(ErrSourceNotFound)
return ok
}

204
registry/registry.go

@ -0,0 +1,204 @@
package registry
import (
"errors"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/mholt/archiver/v3"
"github.com/pelletier/go-toml"
)
// Registry is a collection of Template
type Registry struct {
dir string
Sources []*Source
Templates []*Template
}
func (r *Registry) save() error {
fi, err := os.Create(r.MetaFilePath())
if err != nil {
return err
}
if err := toml.NewEncoder(fi).Encode(r); err != nil {
return err
}
return fi.Close()
}
// MetaFilePath is the path to the Registry meta-file
func (r *Registry) MetaFilePath() string {
return filepath.Join(r.dir, "registry.toml")
}
// GetTemplate retrieves a Template from the Registry
func (r *Registry) GetTemplate(name string) (*Template, error) {
for _, t := range r.Templates {
if strings.EqualFold(name, t.Name) {
t.reg = r
return t, nil
}
}
return nil, ErrTemplateNotFound{Name: name}
}
// AddTemplate downloads and adds a new Template to the Registry
func (r *Registry) AddTemplate(name, repo, branch string) (*Template, error) {
t := &Template{
reg: r,
Name: name,
Repository: repo,
Branch: branch,
Created: time.Now(),
}
r.Templates = append(r.Templates, t)
if err := download(repo, branch, t.ArchivePath()); err != nil {
return nil, err
}
return t, r.save()
}
// RemoveTemplate removes the Template from disk and meta
func (r *Registry) RemoveTemplate(name string) error {
_, err := r.GetTemplate(name)
if err != nil {
return err
}
for idx, t := range r.Templates {
if strings.EqualFold(name, t.Name) {
r.Templates = append(r.Templates[:idx], r.Templates[idx+1:]...)
if err := os.Remove(t.ArchivePath()); err != nil {
return err
}
}
}
return r.save()
}
// GetSource retrieves a Source from the Registry
func (r *Registry) GetSource(name string) (*Source, error) {
for _, s := range r.Sources {
if strings.EqualFold(name, s.Name) {
return s, nil
}
}
return nil, ErrSourceNotFound{Name: name}
}
// AddSource adds a new Source to the Registry
func (r *Registry) AddSource(url, name string) (*Source, error) {
url = strings.TrimSuffix(url, "/") + "/"
s := &Source{
Name: name,
URL: url,
}
r.Sources = append(r.Sources, s)
return s, r.save()
}
// RemoveSource removes the Source from the registry meta
func (r *Registry) RemoveSource(name string) error {
_, err := r.GetSource(name)
if err != nil {
return err
}
for idx, s := range r.Sources {
if strings.EqualFold(name, s.Name) {
r.Sources = append(r.Sources[:idx], r.Sources[idx+1:]...)
}
}
return r.save()
}
// Open opens a Registry, creating one if none exists at dir
func Open(dir string) (*Registry, error) {
reg := Registry{
dir: dir,
}
_, err := os.Lstat(reg.MetaFilePath())
if err != nil {
if os.IsNotExist(err) {
if err := create(reg.MetaFilePath()); err != nil {
return nil, err
}
} else {
return nil, err
}
}
tree, err := toml.LoadFile(reg.MetaFilePath())
if err != nil {
return nil, err
}
return &reg, tree.Unmarshal(&reg)
}
func create(regFile string) error {
if err := os.MkdirAll(filepath.Dir(regFile), os.ModePerm); err != nil {
return err
}
fi, err := os.Create(regFile)
if err != nil {
return err
}
return fi.Close()
}
func download(cloneURL, branch, dest string) error {
tmp, err := ioutil.TempDir(os.TempDir(), "tmpl")
if err != nil {
return err
}
defer os.RemoveAll(tmp)
// Clone the repo
if _, err := git.PlainClone(tmp, false, &git.CloneOptions{
URL: cloneURL,
ReferenceName: plumbing.NewBranchReferenceName(branch),
SingleBranch: true,
Depth: 1,
}); err != nil {
return err
}
// RemoveTemplate .git
if err := os.RemoveAll(filepath.Join(tmp, ".git")); err != nil {
return err
}
// Make sure it's a valid template
if _, err := os.Lstat(filepath.Join(tmp, "template.toml")); err != nil {
return err
}
fi, err := os.Lstat(filepath.Join(tmp, "template"))
if err != nil {
return err
}
if !fi.IsDir() {
return errors.New("template found, expected directory")
}
// Create archive
glob, err := filepath.Glob(filepath.Join(tmp, "*"))
if err != nil {
return err
}
if err := archiver.Archive(glob, dest); err != nil {
return err
}
return nil
}

15
registry/source.go

@ -0,0 +1,15 @@
package registry
import "fmt"
// Source is a quick way to specify a git source
// e.g. Gitea, GitHub, etc.
type Source struct {
Name string `toml:"name"`
URL string `toml:"url"`
}
// CloneURL constructs a URL suitable for cloning a repository
func (s *Source) CloneURL(namespace string) string {
return fmt.Sprintf("%s%s", s.URL, namespace)
}

141
registry/template.go

@ -0,0 +1,141 @@
package registry
import (
"fmt"
"html/template"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
"github.com/AlecAivazis/survey/v2"
"github.com/mholt/archiver/v3"
"github.com/pelletier/go-toml"
)
// Template is a tmpl project
type Template struct {
reg *Registry `toml:"-"`
Name string `toml:"name"`
Repository string `toml:"repository"`
Branch string `toml:"branch"`
Created time.Time `toml:"created"`
}
// ArchiveName is the name given to the archive for this Template
func (t *Template) ArchiveName() string {
return fmt.Sprintf("%s.tar.gz", t.Name)
}
// ArchivePath is the full path to the archive for this Template within the Registry
func (t *Template) ArchivePath() string {
return filepath.Join(t.reg.dir, t.ArchiveName())
}
// Execute runs the Template and copies to dest
func (t *Template) Execute(dest string) error {
tmp, err := ioutil.TempDir(os.TempDir(), "tmpl")
if err != nil {
return err
}
defer os.RemoveAll(tmp)
if err := archiver.Unarchive(t.ArchivePath(), tmp); err != nil {
return err
}
vars, err := prompt(tmp)
if err != nil {
return err
}
base := filepath.Join(tmp, "template")
return filepath.Walk(base, func(walkPath string, walkInfo os.FileInfo, walkErr error) error {
if walkErr != nil {
return walkErr
}
if walkInfo.IsDir() {
return nil
}
contents, err := ioutil.ReadFile(walkPath)
if err != nil {
return err
}
tmpl, err := template.New("tmpl").Parse(string(contents))
if err != nil {
return err
}
newDest := strings.TrimPrefix(walkPath, base+"/")
newDest = filepath.Join(dest, newDest)
if err := os.MkdirAll(filepath.Dir(newDest), os.ModePerm); err != nil {
return err
}
oldFi, err := os.Lstat(walkPath)
if err != nil {
return err
}
newFi, err := os.OpenFile(newDest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, oldFi.Mode())
if err != nil {
return err
}
if err := tmpl.Execute(newFi, vars); err != nil {
return err
}
return newFi.Close()
})
}
func prompt(dir string) (map[string]interface{}, error) {
templatePath := filepath.Join(dir, "template.toml")
if _, err := os.Lstat(templatePath); err != nil {
return nil, err
}
tree, err := toml.LoadFile(templatePath)
if err != nil {
return nil, err
}
vars := tree.ToMap()
for k, v := range vars {
var p survey.Prompt
switch t := v.(type) {
case []string:
p = &survey.Select{
Message: k,
Options: t,
}
default:
p = &survey.Input{
Message: k,
Default: fmt.Sprintf("%v", t),
}
}
q := []*survey.Question{
{
Name: "response",
Prompt: p,
Validate: survey.Required,
},
}
a := struct {
Response string
}{}
if err := survey.Ask(q, &a); err != nil {
return nil, err
}
vars[k] = a.Response
}
return vars, nil
}
Loading…
Cancel
Save