2020-09-03 17:13:19 +02:00
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2020 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// +build mage
package main
import (
"bufio"
"bytes"
"context"
"crypto/sha256"
"fmt"
"github.com/magefile/mage/mg"
"golang.org/x/sync/errgroup"
2020-10-17 10:07:39 +02:00
"gopkg.in/yaml.v3"
2020-09-03 17:13:19 +02:00
"io"
2020-10-17 10:07:39 +02:00
"io/ioutil"
2020-09-03 17:13:19 +02:00
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
2020-09-04 10:15:33 +02:00
"time"
2020-09-03 17:13:19 +02:00
)
const (
PACKAGE = ` code.vikunja.io/api `
DIST = ` dist `
)
var (
Goflags = [ ] string {
"-v" ,
}
Executable = "vikunja"
Ldflags = ""
Tags = ""
VersionNumber = "dev"
Version = "master" // This holds the built version, master by default, when building from a tag or release branch, their name
BinLocation = ""
PkgVersion = "master"
ApiPackages = [ ] string { }
RootPath = ""
GoFiles = [ ] string { }
// Aliases are mage aliases of targets
Aliases = map [ string ] interface { } {
2020-09-26 23:02:17 +02:00
"build" : Build . Build ,
2020-09-04 10:15:33 +02:00
"do-the-swag" : DoTheSwag ,
"check:got-swag" : Check . GotSwag ,
"release:os-package" : Release . OsPackage ,
"dev:create-migration" : Dev . CreateMigration ,
2020-10-17 10:07:39 +02:00
"generate-docs" : GenerateDocs ,
2020-09-03 17:13:19 +02:00
}
)
func setVersion ( ) {
versionCmd := exec . Command ( "git" , "describe" , "--tags" , "--always" , "--abbrev=10" )
version , err := versionCmd . Output ( )
if err != nil {
fmt . Printf ( "Error getting version: %s\n" , err )
os . Exit ( 1 )
}
VersionNumber = strings . Trim ( string ( version ) , "\n" )
VersionNumber = strings . Replace ( VersionNumber , "-" , "+" , 1 )
VersionNumber = strings . Replace ( VersionNumber , "-g" , "-" , 1 )
if os . Getenv ( "DRONE_TAG" ) != "" {
Version = os . Getenv ( "DRONE_TAG" )
} else if os . Getenv ( "DRONE_BRANCH" ) != "" {
Version = strings . Replace ( os . Getenv ( "DRONE_BRANCH" ) , "release/v" , "" , 1 )
}
}
func setBinLocation ( ) {
if os . Getenv ( "DRONE_WORKSPACE" ) != "" {
BinLocation = DIST + ` /binaries/ ` + Executable + ` - ` + Version + ` -linux-amd64 `
} else {
BinLocation = Executable
}
}
func setPkgVersion ( ) {
if Version == "master" {
PkgVersion = VersionNumber
}
}
func setExecutable ( ) {
if runtime . GOOS == "windows" {
Executable += ".exe"
}
}
func setApiPackages ( ) {
cmd := exec . Command ( "go" , "list" , "all" )
pkgs , err := cmd . Output ( )
if err != nil {
fmt . Printf ( "Error getting packages: %s\n" , err )
os . Exit ( 1 )
}
for _ , p := range strings . Split ( string ( pkgs ) , "\n" ) {
if strings . Contains ( p , "code.vikunja.io/api" ) && ! strings . Contains ( p , "code.vikunja.io/api/pkg/integrations" ) {
ApiPackages = append ( ApiPackages , p )
}
}
}
func setRootPath ( ) {
pwd , err := os . Getwd ( )
if err != nil {
fmt . Printf ( "Error getting pwd: %s\n" , err )
os . Exit ( 1 )
}
if err := os . Setenv ( "VIKUNJA_SERVICE_ROOTPATH" , pwd ) ; err != nil {
fmt . Printf ( "Error setting root path: %s\n" , err )
os . Exit ( 1 )
}
RootPath = pwd
}
func setGoFiles ( ) {
// GOFILES := $(shell find . -name "*.go" -type f ! -path "*/bindata.go")
cmd := exec . Command ( "find" , "." , "-name" , "*.go" , "-type" , "f" , "!" , "-path" , "*/bindata.go" )
files , err := cmd . Output ( )
if err != nil {
fmt . Printf ( "Error getting go files: %s\n" , err )
os . Exit ( 1 )
}
for _ , f := range strings . Split ( string ( files ) , "\n" ) {
if strings . HasSuffix ( f , ".go" ) {
GoFiles = append ( GoFiles , RootPath + strings . TrimLeft ( f , "." ) )
}
}
}
2020-09-03 22:14:30 +02:00
// Some variables can always get initialized, so we do just that.
2020-09-03 17:13:19 +02:00
func init ( ) {
2020-09-03 22:14:30 +02:00
setExecutable ( )
setRootPath ( )
}
// Some variables have external dependencies (like git) which may not always be available.
func initVars ( ) {
2020-09-03 17:13:19 +02:00
Tags = os . Getenv ( "TAGS" )
setVersion ( )
setBinLocation ( )
setPkgVersion ( )
setApiPackages ( )
setGoFiles ( )
Ldflags = ` -X " ` + PACKAGE + ` /pkg/version.Version= ` + VersionNumber + ` " -X "main.Tags= ` + Tags + ` " `
}
func runAndStreamOutput ( cmd string , args ... string ) {
c := exec . Command ( cmd , args ... )
c . Env = os . Environ ( )
c . Dir = RootPath
fmt . Printf ( "%s\n\n" , c . String ( ) )
stdout , _ := c . StdoutPipe ( )
errbuf := bytes . Buffer { }
c . Stderr = & errbuf
c . Start ( )
reader := bufio . NewReader ( stdout )
line , err := reader . ReadString ( '\n' )
for err == nil {
fmt . Print ( line )
line , err = reader . ReadString ( '\n' )
}
if err := c . Wait ( ) ; err != nil {
fmt . Printf ( errbuf . String ( ) )
fmt . Printf ( "Error: %s\n" , err )
os . Exit ( 1 )
}
}
// Will check if the tool exists and if not install it from the provided import path
// If any errors occur, it will exit with a status code of 1.
func checkAndInstallGoTool ( tool , importPath string ) {
if err := exec . Command ( tool ) . Run ( ) ; err != nil && strings . Contains ( err . Error ( ) , "executable file not found" ) {
fmt . Printf ( "%s not installed, installing %s...\n" , tool , importPath )
if err := exec . Command ( "go" , "install" , Goflags [ 0 ] , importPath ) . Run ( ) ; err != nil {
fmt . Printf ( "Error installing %s\n" , tool )
os . Exit ( 1 )
}
fmt . Println ( "Installed." )
}
}
// Calculates a hash of a file
func calculateSha256FileHash ( path string ) ( hash string , err error ) {
f , err := os . Open ( path )
if err != nil {
return "" , err
}
defer f . Close ( )
h := sha256 . New ( )
if _ , err := io . Copy ( h , f ) ; err != nil {
return "" , err
}
return fmt . Sprintf ( "%x" , h . Sum ( nil ) ) , nil
}
// Copy the src file to dst. Any existing file will be overwritten and will not
// copy file attributes.
func copyFile ( src , dst string ) error {
in , err := os . Open ( src )
if err != nil {
return err
}
defer in . Close ( )
out , err := os . Create ( dst )
if err != nil {
return err
}
defer out . Close ( )
_ , err = io . Copy ( out , in )
if err != nil {
return err
}
si , err := os . Stat ( src )
if err != nil {
return err
}
if err := os . Chmod ( dst , si . Mode ( ) ) ; err != nil {
return err
}
return out . Close ( )
}
2020-09-03 20:42:26 +02:00
// os.Rename has issues with moving files between docker volumes.
// Because of this limitaion, it fails in drone.
// Source: https://gist.github.com/var23rav/23ae5d0d4d830aff886c3c970b8f6c6b
2020-09-04 14:00:54 +02:00
func moveFile ( src , dst string ) error {
inputFile , err := os . Open ( src )
defer inputFile . Close ( )
2020-09-03 20:42:26 +02:00
if err != nil {
return fmt . Errorf ( "couldn't open source file: %s" , err )
}
2020-09-04 14:00:54 +02:00
outputFile , err := os . Create ( dst )
defer outputFile . Close ( )
2020-09-03 20:42:26 +02:00
if err != nil {
return fmt . Errorf ( "couldn't open dest file: %s" , err )
}
2020-09-04 14:00:54 +02:00
2020-09-03 20:42:26 +02:00
_ , err = io . Copy ( outputFile , inputFile )
if err != nil {
return fmt . Errorf ( "writing to output file failed: %s" , err )
}
2020-09-04 14:00:54 +02:00
// Make sure to copy copy the permissions of the original file as well
si , err := os . Stat ( src )
if err != nil {
return err
}
if err := os . Chmod ( dst , si . Mode ( ) ) ; err != nil {
return err
}
2020-09-03 20:42:26 +02:00
// The copy was successful, so now delete the original file
2020-09-04 14:00:54 +02:00
err = os . Remove ( src )
2020-09-03 20:42:26 +02:00
if err != nil {
return fmt . Errorf ( "failed removing original file: %s" , err )
}
return nil
}
2020-09-03 17:13:19 +02:00
// Formats the code using go fmt
func Fmt ( ) {
2020-09-03 22:14:30 +02:00
mg . Deps ( initVars )
2020-09-03 17:13:19 +02:00
args := append ( [ ] string { "-s" , "-w" } , GoFiles ... )
runAndStreamOutput ( "gofmt" , args ... )
}
// Generates the swagger docs from the code annotations
func DoTheSwag ( ) {
2020-09-03 22:14:30 +02:00
mg . Deps ( initVars )
2020-09-03 17:13:19 +02:00
checkAndInstallGoTool ( "swag" , "github.com/swaggo/swag/cmd/swag" )
runAndStreamOutput ( "swag" , "init" , "-g" , "./pkg/routes/routes.go" , "--parseDependency" , "-d" , RootPath , "-o" , RootPath + "/pkg/swagger" )
}
type Test mg . Namespace
// Runs all tests except integration tests
func ( Test ) Unit ( ) {
2020-09-03 22:14:30 +02:00
mg . Deps ( initVars )
2020-09-03 17:13:19 +02:00
// We run everything sequentially and not in parallel to prevent issues with real test databases
args := append ( [ ] string { "test" , Goflags [ 0 ] , "-p" , "1" } , ApiPackages ... )
runAndStreamOutput ( "go" , args ... )
}
// Runs the tests and builds the coverage html file from coverage output
func ( Test ) Coverage ( ) {
2020-09-03 22:14:30 +02:00
mg . Deps ( initVars )
2020-09-03 17:13:19 +02:00
mg . Deps ( Test . Unit )
runAndStreamOutput ( "go" , "tool" , "cover" , "-html=cover.out" , "-o" , "cover.html" )
}
// Runs the integration tests
func ( Test ) Integration ( ) {
2020-09-03 22:14:30 +02:00
mg . Deps ( initVars )
2020-09-03 17:13:19 +02:00
// We run everything sequentially and not in parallel to prevent issues with real test databases
runAndStreamOutput ( "go" , "test" , Goflags [ 0 ] , "-p" , "1" , PACKAGE + "/pkg/integrations" )
}
type Check mg . Namespace
// Checks if the swagger docs need to be re-generated from the code annotations
func ( Check ) GotSwag ( ) {
2020-09-03 22:14:30 +02:00
mg . Deps ( initVars )
2020-09-03 17:13:19 +02:00
// The check is pretty cheaply done: We take the hash of the swagger.json file, generate the docs,
// hash the file again and compare the two hashes to see if anything changed. If that's the case,
// regenerating the docs is necessary.
// swag is not capable of just outputting the generated docs to stdout, therefore we need to do it this way.
// Another drawback of this is obviously it will only work once - we're not resetting the newly generated
// docs after the check. This behaviour is good enough for ci though.
oldHash , err := calculateSha256FileHash ( RootPath + "/pkg/swagger/swagger.json" )
if err != nil {
fmt . Printf ( "Error getting old hash of the swagger docs: %s" , err )
os . Exit ( 1 )
}
DoTheSwag ( )
newHash , err := calculateSha256FileHash ( RootPath + "/pkg/swagger/swagger.json" )
if err != nil {
fmt . Printf ( "Error getting new hash of the swagger docs: %s" , err )
os . Exit ( 1 )
}
if oldHash != newHash {
fmt . Println ( "Swagger docs are not up to date." )
fmt . Println ( "Please run 'mage do-the-swag' and commit the result." )
os . Exit ( 1 )
}
}
2020-10-11 22:10:03 +02:00
func checkGolangCiLintInstalled ( ) {
2020-09-03 22:14:30 +02:00
mg . Deps ( initVars )
2020-10-11 22:10:03 +02:00
if err := exec . Command ( "golangci-lint" ) . Run ( ) ; err != nil && strings . Contains ( err . Error ( ) , "executable file not found" ) {
fmt . Println ( "Please manually install golangci-lint by running" )
fmt . Println ( "curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.31.0" )
2020-09-03 17:13:19 +02:00
os . Exit ( 1 )
}
}
2020-10-11 22:10:03 +02:00
func ( Check ) Golangci ( ) {
checkGolangCiLintInstalled ( )
runAndStreamOutput ( "golangci-lint" , "run" )
}
func ( Check ) GolangciFix ( ) {
checkGolangCiLintInstalled ( )
runAndStreamOutput ( "golangci-lint" , "run" , "--fix" )
2020-09-03 17:13:19 +02:00
}
// Runs fmt-check, lint, got-swag, misspell-check, ineffasign-check, gocyclo-check, static-check, gosec-check, goconst-check all in parallel
func ( Check ) All ( ) {
2020-09-03 22:14:30 +02:00
mg . Deps ( initVars )
2020-09-03 17:13:19 +02:00
mg . Deps (
2020-10-11 22:10:03 +02:00
Check . Golangci ,
2020-09-03 17:13:19 +02:00
Check . GotSwag ,
)
}
type Build mg . Namespace
// Cleans all build, executable and bindata files
func ( Build ) Clean ( ) error {
2020-09-03 22:14:30 +02:00
mg . Deps ( initVars )
2020-09-03 17:13:19 +02:00
if err := exec . Command ( "go" , "clean" , "./..." ) . Run ( ) ; err != nil {
return err
}
if err := os . Remove ( Executable ) ; err != nil && ! os . IsNotExist ( err ) {
return err
}
if err := os . RemoveAll ( DIST ) ; err != nil && ! os . IsNotExist ( err ) {
return err
}
if err := os . RemoveAll ( BinLocation ) ; err != nil && ! os . IsNotExist ( err ) {
return err
}
return nil
}
// Generates static content into the final binary
func ( Build ) Generate ( ) {
2020-09-03 22:14:30 +02:00
mg . Deps ( initVars )
2020-09-03 17:13:19 +02:00
runAndStreamOutput ( "go" , "generate" , PACKAGE + "/pkg/static" )
}
// Builds a vikunja binary, ready to run
func ( Build ) Build ( ) {
2020-09-03 22:14:30 +02:00
mg . Deps ( initVars )
2020-09-03 17:13:19 +02:00
mg . Deps ( Build . Generate )
runAndStreamOutput ( "go" , "build" , Goflags [ 0 ] , "-tags" , Tags , "-ldflags" , "-s -w " + Ldflags , "-o" , Executable )
}
type Release mg . Namespace
// Runs all steps in the right order to create release packages for various platforms
func ( Release ) Release ( ctx context . Context ) error {
2020-09-03 22:14:30 +02:00
mg . Deps ( initVars )
2020-09-03 17:13:19 +02:00
mg . Deps ( Build . Generate , Release . Dirs )
mg . Deps ( Release . Windows , Release . Linux , Release . Darwin )
// Run compiling in parallel to speed it up
errs , _ := errgroup . WithContext ( ctx )
errs . Go ( ( Release { } ) . Windows )
errs . Go ( ( Release { } ) . Linux )
errs . Go ( ( Release { } ) . Darwin )
if err := errs . Wait ( ) ; err != nil {
return err
}
if err := ( Release { } ) . Compress ( ctx ) ; err != nil {
return err
}
if err := ( Release { } ) . Copy ( ) ; err != nil {
return err
}
if err := ( Release { } ) . Check ( ) ; err != nil {
return err
}
if err := ( Release { } ) . OsPackage ( ) ; err != nil {
return err
}
if err := ( Release { } ) . Zip ( ) ; err != nil {
return err
}
return nil
}
// Creates all directories needed to release vikunja
func ( Release ) Dirs ( ) error {
for _ , d := range [ ] string { "binaries" , "release" , "zip" } {
if err := os . MkdirAll ( RootPath + "/" + DIST + "/" + d , 0755 ) ; err != nil {
return err
}
}
return nil
}
func runXgo ( targets string ) error {
2020-09-03 22:14:30 +02:00
mg . Deps ( initVars )
2020-09-03 17:13:19 +02:00
checkAndInstallGoTool ( "xgo" , "src.techknowlogick.com/xgo" )
2020-09-03 21:22:41 +02:00
extraLdflags := ` -linkmode external -extldflags "-static" `
// See https://github.com/techknowlogick/xgo/issues/79
if strings . HasPrefix ( targets , "darwin" ) {
extraLdflags = ""
}
2020-09-03 17:13:19 +02:00
runAndStreamOutput ( "xgo" ,
"-dest" , RootPath + "/" + DIST + "/binaries" ,
"-tags" , "netgo " + Tags ,
2020-09-03 21:22:41 +02:00
"-ldflags" , extraLdflags + Ldflags ,
2020-09-03 17:13:19 +02:00
"-targets" , targets ,
"-out" , Executable + "-" + Version ,
RootPath )
if os . Getenv ( "DRONE_WORKSPACE" ) != "" {
return filepath . Walk ( "/build/" , func ( path string , info os . FileInfo , err error ) error {
2020-09-03 18:08:57 +02:00
// Skip directories
if info . IsDir ( ) {
return nil
}
2020-09-03 20:42:26 +02:00
return moveFile ( path , RootPath + "/" + DIST + "/binaries/" + info . Name ( ) )
2020-09-03 17:13:19 +02:00
} )
}
return nil
}
// Builds binaries for windows
func ( Release ) Windows ( ) error {
return runXgo ( "windows/*" )
}
// Builds binaries for linux
func ( Release ) Linux ( ) error {
return runXgo ( "linux/*" )
}
// Builds binaries for darwin
func ( Release ) Darwin ( ) error {
return runXgo ( "darwin/*" )
}
// Compresses the built binaries in dist/binaries/ to reduce their filesize
func ( Release ) Compress ( ctx context . Context ) error {
// $(foreach file,$(filter-out $(wildcard $(wildcard $(DIST)/binaries/$(EXECUTABLE)-*mips*)),$(wildcard $(DIST)/binaries/$(EXECUTABLE)-*)), upx -9 $(file);)
errs , _ := errgroup . WithContext ( ctx )
filepath . Walk ( RootPath + "/" + DIST + "/binaries/" , func ( path string , info os . FileInfo , err error ) error {
// Only executable files
if ! strings . Contains ( info . Name ( ) , Executable ) {
return nil
}
// No mips or s390x for you today
if strings . Contains ( info . Name ( ) , "mips" ) || strings . Contains ( info . Name ( ) , "s390x" ) {
return nil
}
// Runs compressing in parallel since upx is single-threaded
errs . Go ( func ( ) error {
2020-09-03 22:45:04 +02:00
runAndStreamOutput ( "chmod" , "+x" , path ) // Make sure all binaries are executable. Sometimes the CI does weired things and they're not.
2020-09-03 17:13:19 +02:00
runAndStreamOutput ( "upx" , "-9" , path )
return nil
} )
return nil
} )
return errs . Wait ( )
}
// Copies all built binaries to dist/release/ in preparation for creating the os packages
func ( Release ) Copy ( ) error {
return filepath . Walk ( RootPath + "/" + DIST + "/binaries/" , func ( path string , info os . FileInfo , err error ) error {
// Only executable files
if ! strings . Contains ( info . Name ( ) , Executable ) {
return nil
}
return copyFile ( path , RootPath + "/" + DIST + "/release/" + info . Name ( ) )
} )
}
// Creates sha256 checksum files for each binary in dist/release/
func ( Release ) Check ( ) error {
p := RootPath + "/" + DIST + "/release/"
return filepath . Walk ( p , func ( path string , info os . FileInfo , err error ) error {
if info . IsDir ( ) {
return nil
}
f , err := os . Create ( p + info . Name ( ) + ".sha256" )
if err != nil {
return err
}
hash , err := calculateSha256FileHash ( path )
if err != nil {
return err
}
_ , err = f . WriteString ( hash + " " + info . Name ( ) )
if err != nil {
return err
}
return f . Close ( )
} )
}
// Creates a folder for each
func ( Release ) OsPackage ( ) error {
p := RootPath + "/" + DIST + "/release/"
// We first put all files in a map to then iterate over it since the walk function would otherwise also iterate
// over the newly created files, creating some kind of endless loop.
bins := make ( map [ string ] os . FileInfo )
if err := filepath . Walk ( p , func ( path string , info os . FileInfo , err error ) error {
if strings . Contains ( info . Name ( ) , ".sha256" ) || info . IsDir ( ) {
return nil
}
bins [ path ] = info
return nil
} ) ; err != nil {
return err
}
for path , info := range bins {
folder := p + info . Name ( ) + "-full/"
if err := os . Mkdir ( folder , 0755 ) ; err != nil {
return err
}
2020-09-03 20:42:26 +02:00
if err := moveFile ( p + info . Name ( ) + ".sha256" , folder + info . Name ( ) + ".sha256" ) ; err != nil {
2020-09-03 17:13:19 +02:00
return err
}
2020-09-03 20:42:26 +02:00
if err := moveFile ( path , folder + info . Name ( ) ) ; err != nil {
2020-09-03 17:13:19 +02:00
return err
}
if err := copyFile ( RootPath + "/config.yml.sample" , folder + "config.yml.sample" ) ; err != nil {
return err
}
if err := copyFile ( RootPath + "/LICENSE" , folder + "LICENSE" ) ; err != nil {
return err
}
}
return nil
}
// Creates a zip file from all os-package folders in dist/release
func ( Release ) Zip ( ) error {
p := RootPath + "/" + DIST + "/release/"
if err := filepath . Walk ( p , func ( path string , info os . FileInfo , err error ) error {
if ! info . IsDir ( ) || info . Name ( ) == "release" {
return nil
}
fmt . Printf ( "Zipping %s...\n" , info . Name ( ) )
c := exec . Command ( "zip" , "-r" , RootPath + "/" + DIST + "/zip/" + info . Name ( ) , "." , "-i" , "*" )
c . Dir = path
out , err := c . Output ( )
fmt . Print ( string ( out ) )
return err
} ) ; err != nil {
return err
}
return nil
}
// Creates a debian package from a built binary
func ( Release ) Deb ( ) {
runAndStreamOutput (
"fpm" ,
"-s" , "dir" ,
"-t" , "deb" ,
"--url" , "https://vikunja.io" ,
"-n" , "vikunja" ,
"-v" , PkgVersion ,
"--license" , "GPLv3" ,
"--directories" , "/opt/vikunja" ,
"--after-install" , "./build/after-install.sh" ,
"--description" , "'Vikunja is an open-source todo application, written in Go. It lets you create lists,tasks and share them via teams or directly between users.'" ,
"-m" , "maintainers@vikunja.io" ,
"-p" , RootPath + "/" + Executable + "-" + Version + "_amd64.deb" ,
2020-09-04 10:42:32 +02:00
RootPath + "/" + BinLocation + "=/opt/vikunja/vikunja" ,
2020-09-03 17:13:19 +02:00
"./config.yml.sample=/etc/vikunja/config.yml" ,
)
}
// Creates a debian repo structure
func ( Release ) Reprepro ( ) {
runAndStreamOutput ( "reprepro_expect" , "debian" , "includedeb" , "strech" , RootPath + "/" + Executable + "-" + Version + "_amd64.deb" )
}
2020-09-04 10:15:33 +02:00
type Dev mg . Namespace
// Creates a new bare db migration skeleton in pkg/migration with the current date
func ( Dev ) CreateMigration ( ) error {
reader := bufio . NewReader ( os . Stdin )
fmt . Print ( "Enter the name of the struct: " )
str , _ := reader . ReadString ( '\n' )
str = strings . Trim ( str , "\n" )
date := time . Now ( ) . Format ( "20060102150405" )
migration := ` // Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2020 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package migration
import (
"src.techknowlogick.com/xormigrate"
"xorm.io/xorm"
)
type ` + str + date + ` struct {
}
func ( ` + str + date + ` ) TableName ( ) string {
return "` + str + `"
}
func init ( ) {
migrations = append ( migrations , & xormigrate . Migration {
ID : "` + date + `" ,
Description : "" ,
Migrate : func ( tx * xorm . Engine ) error {
return tx . Sync2 ( ` + str + date + ` { } )
} ,
Rollback : func ( tx * xorm . Engine ) error {
return nil
} ,
} )
}
`
f , err := os . Create ( RootPath + "/pkg/migration/" + date + ".go" )
defer f . Close ( )
if err != nil {
return err
}
_ , err = f . WriteString ( migration )
return err
}
2020-10-17 10:07:39 +02:00
type configOption struct {
key string
description string
defaultValue string
children [ ] * configOption
}
func parseYamlConfigNode ( node * yaml . Node ) ( config * configOption ) {
config = & configOption {
key : node . Value ,
description : strings . ReplaceAll ( node . HeadComment , "# " , "" ) ,
}
valMap := make ( map [ string ] * configOption )
var lastOption * configOption
for i , n2 := range node . Content {
coo := & configOption {
key : n2 . Value ,
description : strings . ReplaceAll ( n2 . HeadComment , "# " , "" ) ,
}
// If there's a key in valMap for the current key we should use that to append etc
// Else we just create a new configobject
co , exists := valMap [ n2 . Value ]
if exists {
co . description = coo . description
} else {
valMap [ n2 . Value ] = coo
config . children = append ( config . children , coo )
}
// fmt.Println(i, coo.key, coo.description, n2.Value)
if i % 2 == 0 {
lastOption = coo
continue
} else {
lastOption . defaultValue = n2 . Value
}
if i - 1 >= 0 && i - 1 <= len ( node . Content ) && node . Content [ i - 1 ] . Value != "" {
coo . defaultValue = n2 . Value
coo . key = node . Content [ i - 1 ] . Value
}
if len ( n2 . Content ) > 0 {
for _ , n := range n2 . Content {
coo . children = append ( coo . children , parseYamlConfigNode ( n ) )
}
}
}
return config
}
func printConfig ( config [ ] * configOption , level int ) ( rendered string ) {
// Keep track of what we already printed to prevent printing things twice
printed := make ( map [ string ] bool )
for _ , option := range config {
if option . key != "" {
// Filter out all config objects where the default value == key
// Yaml is weired: It gives you a slice with an entry each for the key and their value.
if printed [ option . key ] {
continue
}
if level == 0 {
rendered += "---\n\n"
}
rendered += "#"
for i := 0 ; i <= level ; i ++ {
rendered += "#"
}
rendered += " " + option . key + "\n\n"
if option . description != "" {
rendered += option . description + "\n\n"
}
// Top level config values never have a default value
if level > 0 {
rendered += "Default: `" + option . defaultValue
if option . defaultValue == "" {
rendered += "<empty>"
}
rendered += "`\n"
}
}
printed [ option . key ] = true
rendered += "\n" + printConfig ( option . children , level + 1 )
}
return
}
const (
configDocPath = ` docs/content/doc/setup/config.md `
configInjectComment = ` <!-- Generated config will be injected here --> `
)
// Generates the error docs from a commented config.yml.sample file in the repo root.
func GenerateDocs ( ) error {
config , err := ioutil . ReadFile ( "config.yml.sample" )
if err != nil {
return err
}
var d yaml . Node
err = yaml . Unmarshal ( config , & d )
if err != nil {
return err
}
conf := [ ] * configOption { }
for _ , node := range d . Content {
for _ , n := range node . Content {
co := parseYamlConfigNode ( n )
conf = append ( conf , co )
}
}
renderedConfig := printConfig ( conf , 0 )
// Rebuild the config
file , err := os . OpenFile ( configDocPath , os . O_RDWR , 0 )
if err != nil {
return err
}
defer file . Close ( )
// We read the config doc up until the marker, then stop and append our generated config
fullConfig := ""
scanner := bufio . NewScanner ( file )
for scanner . Scan ( ) {
t := scanner . Text ( )
fullConfig += t + "\n"
if t == configInjectComment {
break
}
}
if err := scanner . Err ( ) ; err != nil {
return err
}
fullConfig += "\n" + renderedConfig
// We write the full file to prevent old content leftovers at the end
// I know, there are probably better ways to do this.
if err := ioutil . WriteFile ( configDocPath , [ ] byte ( fullConfig ) , 0 ) ; err != nil {
return err
}
return nil
}