├── .gitignore ├── README.md ├── Makefile └── gut.go /.gitignore: -------------------------------------------------------------------------------- 1 | gut 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gut 2 | :scroll: gut is a template printing, aka scaffolding, tool for Erlang. Like rails generate or yeoman 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | INSTALL_PATH ?= /usr/local/bin/ 2 | 3 | GOX_OSARCH ?= linux/386 linux/amd64 freebsd/386 freebsd/amd64 openbsd/386 openbsd/amd64 windows/386 windows/amd64 freebsd/arm netbsd/386 netbsd/amd64 4 | 5 | DIST_PATH ?= dist 6 | 7 | ifeq ($(shell uname -s), Darwin) 8 | GOX_OSARCH += darwin/amd64 9 | endif 10 | 11 | GOX_FLAGS ?= -output="$(DIST_PATH)/{{.Dir}}.{{.OS}}-{{.Arch}}" -osarch="$(GOX_OSARCH)" 12 | 13 | default: build 14 | 15 | build: 16 | go build ./... 17 | 18 | deps: 19 | go get ./. 20 | 21 | updatedeps: 22 | go get -u -v ./... 23 | 24 | clean: 25 | git clean -xdf 26 | 27 | install: 28 | cp $(DIST_PATH)/gut $(INSTALL_PATH) 29 | 30 | tools: 31 | go get github.com/tcnksm/ghr 32 | go get github.com/mitchellh/gox 33 | go get github.com/alecthomas/gometalinter 34 | gometalinter --install --update 35 | 36 | gox: tools 37 | gox -build-toolchain -osarch="$(GOX_OSARCH)" 38 | 39 | dist: 40 | which gox || make tools 41 | gox $(GOX_FLAGS) $(GOBUILD_LDFLAGS) 42 | 43 | release: dist 44 | ghr $(REPO_VERSION) $(DIST_PATH) 45 | 46 | lint: 47 | gometalinter ./... 48 | -------------------------------------------------------------------------------- /gut.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "os/exec" 9 | "path" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/codegangsta/cli" 14 | "github.com/fatih/color" 15 | "github.com/google/go-github/github" 16 | "github.com/hoisie/mustache" 17 | git "github.com/libgit2/git2go" 18 | "gopkg.in/yaml.v2" 19 | ) 20 | 21 | var ( 22 | version = "0.1" 23 | suffix = "-gut-template" 24 | templateFile = "gut.template" 25 | templateConfig configuration 26 | cwd string 27 | name string 28 | fullPath string 29 | ) 30 | 31 | var ( 32 | boldCyan *color.Color 33 | boldWhite *color.Color 34 | ) 35 | 36 | type configuration struct { 37 | Variables map[string]string `yaml:"variables"` 38 | Options map[string]string `yaml:"options"` 39 | Commands []string `yaml:"commands"` 40 | } 41 | 42 | func main() { 43 | boldCyan = color.New(color.FgCyan).Add(color.Bold) 44 | boldWhite = color.New(color.FgWhite).Add(color.Bold) 45 | 46 | cwd, _ = filepath.Abs(filepath.Dir(os.Args[0])) 47 | 48 | app := cli.NewApp() 49 | app.Name = "gut" 50 | app.Usage = "tool that retrieves templates and compile them to scaffold projects and create standalone files" 51 | app.Version = version 52 | app.Author = "Federico Carrone" 53 | app.Email = "federico.carrone@gmail.com" 54 | 55 | app.Commands = []cli.Command{ 56 | { 57 | Name: "search", 58 | Aliases: []string{"s"}, 59 | Usage: "search templates", 60 | Action: func(c *cli.Context) { 61 | filter := c.Args().First() 62 | search(filter) 63 | }, 64 | }, 65 | { 66 | Name: "new", 67 | Aliases: []string{"n"}, 68 | Usage: "new", 69 | Action: func(c *cli.Context) { 70 | if len(c.Args()) < 2 { 71 | fmt.Print("Not enough arguments") 72 | } 73 | templateName := c.Args().First() 74 | name = c.Args()[1] 75 | new(templateName) 76 | }, 77 | }, 78 | { 79 | Name: "describe", 80 | Aliases: []string{"d"}, 81 | Usage: "describe", 82 | Action: func(c *cli.Context) { 83 | templateName := c.Args().First() 84 | describe(templateName) 85 | }, 86 | }, 87 | { 88 | Name: "version", 89 | Aliases: []string{"v"}, 90 | Usage: "version", 91 | Action: func(c *cli.Context) { 92 | fmt.Printf("gut version %s\n", version) 93 | }, 94 | }, 95 | } 96 | 97 | app.Run(os.Args) 98 | } 99 | 100 | func search(filter string) { 101 | search := filter + suffix 102 | client := github.NewClient(nil) 103 | opt := &github.SearchOptions{ 104 | Sort: "stars", 105 | } 106 | 107 | results, _, error := client.Search.Repositories(search, opt) 108 | 109 | if error != nil { 110 | log.Fatal(error) 111 | } 112 | 113 | for _, repository := range results.Repositories { 114 | repoName := *repository.Name 115 | repoUsername := *(*repository.Owner).Login 116 | repoDescription := *repository.Description 117 | repoStars := *repository.StargazersCount 118 | 119 | print := strings.HasSuffix(repoName, suffix) 120 | 121 | if print { 122 | repoName = strings.TrimSuffix(repoName, suffix) 123 | 124 | boldCyan.Printf("%s/", repoUsername) 125 | boldWhite.Printf("%s", repoName) 126 | fmt.Printf(" (%d)\n", repoStars) 127 | fmt.Printf(" %s\n", repoDescription) 128 | } 129 | } 130 | } 131 | 132 | func new(templateName string) { 133 | repoName := templateName + suffix 134 | repoURL := "https://github.com/" + repoName + ".git" 135 | 136 | fullPath = path.Join(cwd, name) 137 | gitopts := &git.CloneOptions{} 138 | 139 | boldWhite.Printf("Retrieving %s via %s\n", templateName, repoURL) 140 | _, err := git.Clone(repoURL, fullPath, gitopts) 141 | 142 | if err != nil { 143 | log.Fatal(err) 144 | } else { 145 | println() 146 | } 147 | 148 | config(name, path.Join(fullPath, templateFile)) 149 | askForVariables() 150 | clean() 151 | compile() 152 | commands() 153 | fmt.Printf("Your %s project was created successfully.\n", templateName) 154 | } 155 | 156 | func config(name string, configPath string) { 157 | configFile, errFile := ioutil.ReadFile(configPath) 158 | 159 | if errFile != nil { 160 | log.Fatal("Configuration file not present inside template\nError: ", errFile) 161 | } 162 | 163 | errYaml := yaml.Unmarshal(configFile, &templateConfig) 164 | if errYaml != nil { 165 | log.Fatal("Configuration file is not correctly formated\nError: ", errYaml) 166 | } 167 | 168 | templateConfig.Variables["name"] = name 169 | } 170 | 171 | func askForVariables() { 172 | boldWhite.Println("Assign values to variables [default value]") 173 | for key, defaultValue := range templateConfig.Variables { 174 | if key != "name" { 175 | fmt.Printf("%s [%s]: ", key, defaultValue) 176 | 177 | input := "" 178 | fmt.Scanln(&input) 179 | 180 | if input != "" { 181 | templateConfig.Variables[key] = input 182 | } else { 183 | templateConfig.Variables[key] = defaultValue 184 | } 185 | } 186 | } 187 | println() 188 | } 189 | 190 | func clean() { 191 | os.Remove(path.Join(fullPath, templateFile)) 192 | os.RemoveAll(path.Join(fullPath, ".git/")) 193 | os.Remove(path.Join(fullPath, ".gitignore")) 194 | } 195 | 196 | func compile() { 197 | boldWhite.Printf("Generating fresh 'gut new' %s project:\n", templateConfig.Variables["name"]) 198 | filepath.Walk(fullPath, compileDirs) 199 | filepath.Walk(fullPath, compileFiles) 200 | println() 201 | } 202 | 203 | func compileDirs(fullPath string, f os.FileInfo, err error) error { 204 | if f.IsDir() { 205 | newPath := mustache.Render(fullPath, templateConfig.Variables) 206 | os.Remove(fullPath) 207 | os.Mkdir(newPath, f.Mode()) 208 | printCreating(newPath) 209 | } 210 | 211 | return nil 212 | } 213 | 214 | func printCreating(path string) { 215 | relativePath, err := filepath.Rel(cwd, path) 216 | 217 | if err != nil { 218 | log.Fatal(err) 219 | } 220 | 221 | color.New(color.FgGreen).Print("* creating") 222 | fmt.Printf(" %s\n", relativePath) 223 | } 224 | 225 | func compileFiles(fullPath string, f os.FileInfo, err error) error { 226 | if !f.IsDir() { 227 | newPath := mustache.Render(fullPath, templateConfig.Variables) 228 | rendered := mustache.RenderFile(fullPath, templateConfig.Variables) 229 | 230 | os.Remove(fullPath) 231 | ioutil.WriteFile(newPath, []byte(rendered), f.Mode()) 232 | 233 | printCreating(newPath) 234 | } 235 | 236 | return nil 237 | } 238 | 239 | func commands() { 240 | if len(templateConfig.Commands) != 0 { 241 | boldWhite.Printf("Template wants to run the following commands with path=%s:\n", fullPath) 242 | 243 | for n, command := range templateConfig.Commands { 244 | cmdCompiled := mustache.Render(command, templateConfig.Variables) 245 | templateConfig.Commands[n] = cmdCompiled 246 | 247 | fmt.Printf("%d. ", n+1) 248 | fmt.Printf("%s", cmdCompiled) 249 | println() 250 | } 251 | 252 | println() 253 | boldWhite.Printf("Do you want to run this commands? [N/y] ") 254 | input := "" 255 | fmt.Scanln(&input) 256 | 257 | if input == "y" || input == "Y" { 258 | for _, cmdTemplate := range templateConfig.Commands { 259 | color.New(color.FgGreen).Print("* running ") 260 | fmt.Printf("%s", cmdTemplate) 261 | println() 262 | 263 | cmdArray := strings.Split(cmdTemplate, " ") 264 | cmdString := cmdArray[0] 265 | arguments := cmdArray[1:len(cmdArray)] 266 | 267 | command := exec.Command(cmdString, arguments...) 268 | command.Dir = fullPath 269 | 270 | out, err := command.Output() 271 | if err != nil { 272 | log.Fatal(err) 273 | } else if string(out) != "" { 274 | fmt.Printf("> %s", out) 275 | } 276 | } 277 | } 278 | println() 279 | } 280 | } 281 | 282 | func describe(templateName string) { 283 | repoName := templateName + suffix 284 | repoURL := "https://github.com/" + repoName + ".git" 285 | 286 | tempDir := os.TempDir() 287 | fullPath = path.Join(tempDir, "prueba") 288 | gitopts := &git.CloneOptions{} 289 | 290 | _, err := git.Clone(repoURL, fullPath, gitopts) 291 | if err != nil { 292 | os.RemoveAll(fullPath) 293 | log.Fatal(err) 294 | } else { 295 | println() 296 | } 297 | 298 | config(name, path.Join(fullPath, templateFile)) 299 | 300 | boldWhite.Println("Variables in template") 301 | for key, defaultValue := range templateConfig.Variables { 302 | if key != "name" { 303 | fmt.Printf("%s [%s]\n", key, defaultValue) 304 | } 305 | } 306 | 307 | println() 308 | 309 | boldWhite.Println("Commands in template") 310 | for n, command := range templateConfig.Commands { 311 | fmt.Printf("%d. ", n+1) 312 | fmt.Printf("%s", command) 313 | println() 314 | } 315 | 316 | os.RemoveAll(fullPath) 317 | } 318 | 319 | // inmediate tasks: 320 | // remove tmp path if issue while executing config in describe 321 | // in describe print author, github description and url 322 | // print help a la mix help. execute command to check it. 323 | // if in describe and new the repo does not exist you get 2016/03/13 17:57:58 authentication required but no callback set 324 | 325 | // support templates that remove the cloned folder using the delete folder option 326 | // work on temporary directory? if yes, then use os tempdir function. think if this is useful 327 | 328 | // add subcommand to compile current working dir. useful for developing templates 329 | // develop erlang cowboy template 330 | 331 | // add option to fill from shell like ./rebar3 new plugin name=demo author_name="Fred H." 332 | 333 | // store templates in /home/.config/gut/templates/. only fetch if template not found in that path 334 | // update stored templates 335 | 336 | // clone templates with depth one 337 | // paginate to get all answers in search 338 | 339 | // add curl command in readme like docker compose has. generate gox builds 340 | // add homebrew formulas 341 | // add documentation in the readme 342 | --------------------------------------------------------------------------------