diff --git a/go.mod b/go.mod index 07f7faa..1fb12fa 100644 --- a/go.mod +++ b/go.mod @@ -4,4 +4,12 @@ go 1.18 require github.com/ssgo/u v1.7.5 -require gopkg.in/yaml.v3 v3.0.1 // indirect +require ( + github.com/ssgo/config v1.7.5 // indirect + github.com/ssgo/httpclient v1.7.5 // indirect + github.com/ssgo/log v1.7.5 // indirect + github.com/ssgo/standard v1.7.5 // indirect + golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53 // indirect + golang.org/x/text v0.3.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/main.go b/main.go index 6ca821d..1775dd6 100644 --- a/main.go +++ b/main.go @@ -1,8 +1,10 @@ package main import ( + "archive/zip" _ "embed" "fmt" + "github.com/ssgo/httpclient" "github.com/ssgo/u" "io" "os" @@ -10,8 +12,10 @@ import ( "path" "path/filepath" "regexp" + "strconv" "strings" "text/template" + "time" ) var version = "v0.0.6" @@ -52,8 +56,8 @@ type Command struct { } var commands = []Command{ - {"new", "+", "[name]", "create a new project, will create in the current directory if no name is specified", newProject}, - {"new plugin", "+p", "[name]", "create a new plugin project, will create in the current directory if no name is specified", newPluginProject}, + {"init", "i", "", "init a new project for empty dir", initProject}, + {"init plugin", "i p", "", "init a new plugin project for empty dir", initPluginProject}, //{"new server", "+s", "[name]", "create a new server project, will create in the current directory if no name is specified", newServerProject}, //{"new api", "+a", "[path] [method]", "create a new api for server project, will use restful api if specified http method", newServerAPI}, //{"new game", "+g", "[name]", "create a new game project, will create in the current directory if no name is specified", newServerProject}, @@ -65,24 +69,92 @@ var commands = []Command{ {"watch test", "tt", "[...]", "test project use gowatch, if project files changed will restart auto, ... args see gowatch help https://github.com/ssgo/tool", devTestProject}, //{"export plugins", "ep", "", "export typescript code for used plugins into \"plugins/\"", makePluginCode}, {"tidy", "td", "", "tidy project, find imported plugins from .js files add import code to jsImports.go, export typescript code for used plugins into \"plugins/\"", tidy}, - //{"git login", "/lg", "", "login to apigo.cloud/git", }, - //{"git new", "/+", "name", "create new public repository from apigo.cloud/git", }, - //{"git new pri", "/++", "name", "create new private repository from apigo.cloud/git", }, - //{"git clone", "/cl", "name", "clone repository to current dir from apigo.cloud/git", }, - //{"git list", "/l", "", "list current repository tags from apigo.cloud/git", }, - //{"git commit", "/c", "comment", "commit current repository to apigo.cloud/git", }, - //{"git tag", "/t", "tag", "create new tag for current repository and push to apigo.cloud/git", }, - //{"build", "b", "[-m]", "build for current os, output to build/, -m will mix js files into exec file", }, - //{"build mac", "bm", "", "build", }, - //{"build macarm", "bma", "", "build", }, - //{"build win", "bw", "", "build", }, - //{"build win32", "bw32", "", "build", }, - //{"build linux", "bl", "", "build", }, - //{"build linuxarm", "bla", "", "build", }, - //{"build all", "ba", "", "build", }, + {"tags", "", "", "show git tags", showGitTags}, + {"commit", "co", "comment", "commit git repo and push, comment is need", commitGitRepo}, + {"tag+", "t+", "[version]", "add git tag push, if no new tag specified will use last tag +1", addGitTag}, + //{"build", "b", "[-m]", "build for current os, output to build/, -m will mix js files into exec file"}, + //{"build mac", "bm", "", "build"}, + //{"build macarm", "bma", "", "build"}, + //{"build win", "bw", "", "build"}, + //{"build win32", "bw32", "", "build"}, + //{"build linux", "bl", "", "build"}, + //{"build linuxarm", "bla", "", "build"}, + //{"build all", "ba", "", "build"}, //{"publish", "p", "", "publish project to target server", }, - // TODO 从 apigo.cloud/git 中读取信息增加命令,例如:server // TODO 从 plugins 中读取信息增加命令,例如:dao + // TODO 支持 build + // TODO 支持 publish 到 apigo.cloud +} + +var hc = httpclient.GetClient(30 * time.Second) +var cachePath = "c:\\.ag\\cache" + +func init() { + hc.SetGlobalHeader("Content-Type", "application/json") + if homePath, err := os.UserHomeDir(); err == nil { + cachePath = path.Join(homePath, ".ag", "cache") + } +} + +func showGitTags(args []string) { + _ = runCommand("git", "tag", "-l", "v*", "--sort=-taggerdate", "--format=%(refname:short) %(taggerdate:short) %(*objectname:short)") +} + +func commitGitRepo(args []string) { + comment := strings.Join(args, " ") + if comment != "" { + if err := runCommand("git", "commit", "-a", "-m", comment); err == nil { + if err := runCommand("git", "push"); err != nil { + fmt.Println("git push failed:", err.Error()) + } + } else { + fmt.Println("git commit failed:", err.Error()) + } + } else { + fmt.Println("commit message is empty") + } +} + +func addGitTag(args []string) { + newVer := "" + if len(args) > 0 { + newVer = args[0] + } + if newVer == "" { + if outs, err := u.RunCommand("git", "tag", "-l", "v*", "--sort=-taggerdate"); err == nil { + oldVer := "v0.0.0" + for i := len(outs) - 1; i >= 0; i-- { + if outs[i][0] == 'v' && strings.IndexByte(outs[i], '.') != -1 { + oldVer = outs[len(outs)-1] + break + } + } + + if len(os.Args) > 2 { + newVer = os.Args[2] + } else { + versionParts := strings.Split(oldVer, ".") + v, err := strconv.Atoi(versionParts[len(versionParts)-1]) + if err != nil { + v = 0 + } + versionParts[len(versionParts)-1] = strconv.Itoa(v + 1) + newVer = strings.Join(versionParts, ".") + } + } else { + fmt.Println("get last tag failed:", err.Error()) + } + } + + if newVer != "" { + if err := runCommand("git", "tag", "-a", newVer, "-m", "update tag by 'ag tag+'"); err == nil { + if err := runCommand("git", "push", "origin", newVer); err != nil { + fmt.Println("git push failed:", err.Error()) + } + } else { + fmt.Println("git add tag failed:", err.Error()) + } + } } func findTool() (goWatchPath string, logVPath string) { @@ -110,36 +182,173 @@ func checkSSGOTool() { } } -func checkProjectPath(args []string) string { - if len(args) > 0 { - if err := os.Mkdir(args[0], 0755); err != nil { - fmt.Println(u.BRed("mkdir error: " + err.Error())) - return "" - } - if err := os.Chdir(args[0]); err != nil { - fmt.Println(u.BRed("chdir error: " + err.Error())) - return "" - } - return args[0] +func checkProjectPath() string { + //if len(args) > 0 { + // if err := os.Mkdir(args[0], 0755); err != nil { + // fmt.Println(u.BRed("mkdir error: " + err.Error())) + // return "" + // } + // if err := os.Chdir(args[0]); err != nil { + // fmt.Println(u.BRed("chdir error: " + err.Error())) + // return "" + // } + // return args[0] + //} else { + if len(u.ReadDirN(".")) != 0 { + fmt.Println(u.BRed("current path is not empty")) + return "" + } + if pathname, err := os.Getwd(); err != nil { + fmt.Println(u.BRed("getwd error: " + err.Error())) + return "" } else { - if pathname, err := os.Getwd(); err != nil { - fmt.Println(u.BRed("getwd error: " + err.Error())) - return "" + return path.Base(pathname) + } + //} +} + +func parseRepo(url string) (apiUrl, owner, repo string) { + name := "" + if strings.HasPrefix(url, "github.com/") { + apiUrl = "https://api.github.com/" + name = url[11:] + } else if strings.HasPrefix(url, "gitee.com/") { + apiUrl = "https://gitee.com/api/v5/" + name = url[10:] + } else if strings.HasPrefix(url, "apigo.cloud/git/") { + apiUrl = "https://apigo.cloud/git/api/v1/" + name = url[16:] + } else { + apiUrl = "https://apigo.cloud/git/api/v1/" + if strings.ContainsRune(url, '/') { + name = url } else { - return path.Base(pathname) + name = "apigo/" + url } } + + if !strings.ContainsRune(name, '/') { + name = name + "/" + name + } + + a := strings.Split(name, "/") + owner = a[0] + repo = a[1] + + return +} + +type Tag struct { + Name string + Commit struct { + Sha string + Url string + } + Zipball_url string + Tarball_url string +} + +func fetchRepo(name string) (repoPath string) { + apiUrl, owner, repo := parseRepo(name) + tagsUrl := apiUrl + path.Join("repos", owner, repo, "tags") + tags := make([]Tag, 0) + fmt.Println("try to fetch tags", tagsUrl) + r := hc.Get(tagsUrl) + if r.Error == nil { + r.To(&tags) + } else if apiUrl != "https://api.github.com/" { + tagsUrl = "https://api.github.com/" + path.Join("repos", owner, repo, "tags") + fmt.Println("try to fetch tags", tagsUrl) + r = hc.Get(tagsUrl) + if r.Error == nil { + r.To(&tags) + } else { + fmt.Println("fetch tags error", r.Error) + } + } + if len(tags) > 0 { + lastVersion := tags[0].Name + zipUrl := tags[0].Zipball_url + tagPath := filepath.Join(cachePath, owner, repo, lastVersion) + if !u.FileExists(tagPath) { + zipFile := filepath.Join(cachePath, owner, repo, lastVersion+".zip") + fmt.Println("Download file ", zipUrl) + _, err := hc.Download(zipFile, zipUrl, func(start, end int64, ok bool, finished, total int64) { + fmt.Printf(" %.2f%%", float32(finished)/float32(total)*100) + }) + fmt.Println() + if err != nil { + fmt.Println(u.BRed("download file failed")) + return + } + if u.FileExists(zipFile) { + if reader, err := zip.OpenReader(zipFile); err == nil { + for _, f := range reader.File { + toFile := filepath.Join(tagPath, f.Name) + if f.FileInfo().IsDir() { + //fmt.Println("extract dir", f.Name, toFile) + os.MkdirAll(toFile, 0755) + } else { + //fmt.Println("extract file", f.Name, toFile) + u.CheckPath(toFile) + if fp, err := os.OpenFile(toFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644); err == nil { + if rd, err := f.Open(); err == nil { + if _, err := io.Copy(fp, rd); err != nil { + fmt.Println(u.BRed("write file failed"), toFile, err.Error()) + } else { + //fmt.Println(u.BGreen("write file success"), toFile) + } + _ = rd.Close() + } else { + fmt.Println(u.BRed("open file in zip failed"), f.Name, err.Error()) + } + _ = fp.Close() + } else { + fmt.Println(u.BRed("open dst file failed"), toFile, err.Error()) + } + } + } + _ = reader.Close() + } else { + fmt.Println(u.BRed("open zip file failed"), zipFile, err.Error()) + } + _ = os.Remove(zipFile) + } + } + return filepath.Join(tagPath, repo) + } else { + fmt.Println(u.BRed("no repo found for"), name) + return "" + } } -func newProject(args []string) { - if name := checkProjectPath(args); name != "" { +func initProject(args []string) { + if name := checkProjectPath(); name != "" { + projectOK := false + if len(args) > 0 { + repoPath := fetchRepo(args[0]) + projectPath := filepath.Join(repoPath, "_project") + if u.FileExists(projectPath) { + if err := CopyFile(projectPath, "."); err == nil { + projectOK = true + fmt.Println(u.Green("make project files success")) + } else { + fmt.Println(u.BRed("make project files failed"), projectPath, err.Error()) + } + } + } + + if len(args) == 0 || !projectOK { + writeFile("main.go", mainCodeTPL, map[string]any{"name": name}) + writeFile("main_test.go", mainTestCodeTPL, map[string]any{"name": name}) + writeFile("main.js", mainJSCodeTPL, map[string]any{"name": name}) + } + _ = runCommand("go", "mod", "init", name) _ = runCommand("go", "mod", "edit", "-go=1.18") _ = runCommand("go", "get", "-u", "apigo.cloud/git/apigo/gojs") _ = runCommand("go", "get", "-u", "apigo.cloud/git/apigo/plugins") - writeFile("main.go", mainCodeTPL, map[string]any{"name": name}) - writeFile("main_test.go", mainTestCodeTPL, map[string]any{"name": name}) - writeFile("main.js", mainJSCodeTPL, map[string]any{"name": name}) + writeFile(".gitignore", gitignoreTPL, map[string]any{"name": name}) _ = runCommand("go", "mod", "tidy") checkSSGOTool() @@ -147,8 +356,41 @@ func newProject(args []string) { } } -func newPluginProject(args []string) { - if name := checkProjectPath(args); name != "" { +func CopyFile(from, to string) error { + fromStat, _ := os.Stat(from) + if fromStat.IsDir() { + // copy dir + for _, f := range u.ReadDirN(from) { + err := CopyFile(filepath.Join(from, f.Name), filepath.Join(to, f.Name)) + if err != nil { + return err + } + } + return nil + } else { + // copy file + toStat, err := os.Stat(to) + if err == nil && toStat.IsDir() { + to = filepath.Join(to, filepath.Base(from)) + } + u.CheckPath(to) + if writer, err := os.OpenFile(to, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644); err == nil { + defer writer.Close() + if reader, err := os.OpenFile(from, os.O_RDONLY, 0644); err == nil { + defer reader.Close() + _, err = io.Copy(writer, reader) + return err + } else { + return err + } + } else { + return err + } + } +} + +func initPluginProject(args []string) { + if name := checkProjectPath(); name != "" { _ = runCommand("go", "mod", "init", name) _ = runCommand("go", "mod", "edit", "-go=1.18") _ = runCommand("go", "get", "-u", "apigo.cloud/git/apigo/plugin") @@ -171,12 +413,6 @@ func newPluginProject(args []string) { } } -func newServerProject(args []string) { -} - -func newServerAPI(args []string) { -} - func _runProject(args []string, isWatch bool) { _ = runCommand("go", "mod", "tidy") goBinPath, logVPath := findTool() @@ -297,7 +533,9 @@ func makePluginCodeImports(from string, imports *[]string, parentModuleName stri if strings.HasSuffix(f.Name, ".go") && !strings.HasPrefix(f.Name, ".") && f.Name != "makePluginCode.go" { if code, err := u.ReadFile(filepath.Join(from, f.Name)); err == nil { if m := pkgNameMatcher.FindStringSubmatch(code); m != nil { - currentPkgName = m[1] + if currentPkgName+"_test" != m[1] { + currentPkgName = m[1] + } } for _, m := range pkgMatcher.FindAllStringSubmatch(code, 1024) { if m[2] != "current-plugin" && !strings.HasPrefix(f.Name, "jsImports") {