diff --git a/.gitignore b/.gitignore index 158421d04..637399f56 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,4 @@ _testmain.go *.exe~ gogs __pycache__ +*.pem \ No newline at end of file diff --git a/.gopmfile b/.gopmfile index ae92d45e3..deb6d31e9 100644 --- a/.gopmfile +++ b/.gopmfile @@ -1,28 +1,25 @@ [target] -path=github.com/gogits/gogs +path = github.com/gogits/gogs [deps] -github.com/codegangsta/cli= -github.com/go-martini/martini= -github.com/Unknwon/com= -github.com/Unknwon/cae= -github.com/Unknwon/goconfig= -github.com/dchest/scrypt= -github.com/nfnt/resize= -github.com/lunny/xorm= -github.com/go-sql-driver/mysql= -github.com/lib/pq= -github.com/gogits/logs= -github.com/gogits/binding= -github.com/gogits/git= -github.com/gogits/gfm= -github.com/gogits/cache= -github.com/gogits/session= -github.com/gogits/webdav= -github.com/martini-contrib/oauth2= -github.com/martini-contrib/sessions= -code.google.com/p/goauth2= +github.com/codegangsta/cli = +github.com/go-martini/martini = +github.com/Unknwon/com = +github.com/Unknwon/cae = +github.com/Unknwon/goconfig = +github.com/nfnt/resize = +github.com/lunny/xorm = +github.com/go-sql-driver/mysql = +github.com/lib/pq = +github.com/qiniu/log = +code.google.com/p/goauth2 = +github.com/gogits/logs = +github.com/gogits/binding = +github.com/gogits/git = +github.com/gogits/gfm = +github.com/gogits/cache = +github.com/gogits/session = +github.com/gogits/webdav = [res] -include=templates|public|conf - +include = templates|public|conf \ No newline at end of file diff --git a/README.md b/README.md index 6061f5a71..b2417b832 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@ Gogs(Go Git Service) is a Self Hosted Git Service in the Go Programming Language ![Demo](http://gowalker.org/public/gogs_demo.gif) -##### Current version: 0.2.0 Alpha +##### Current version: 0.2.3 Alpha -#### Due to testing purpose, data of [try.gogits.org](http://try.gogits.org) has been reset in March 29, 2014 and will reset multiple times after. Please do NOT put your important data on the site. +#### Due to testing purpose, data of [try.gogits.org](http://try.gogits.org) has been reset in April 6, 2014 and will reset multiple times after. Please do NOT put your important data on the site. #### Other language version @@ -29,9 +29,9 @@ More importantly, Gogs only needs one binary to setup your own project hosting o ## Features - Activity timeline -- SSH/HTTPS(Clone only) protocol support. +- SSH/HTTP(S) protocol support. - Register/delete/rename account. -- Create/delete/watch/rename public repository. +- Create/delete/watch/rename/transfer public repository. - Repository viewer. - Issue tracker. - Gravatar and cache support. @@ -63,4 +63,4 @@ This project was launched by [Unknown](https://github.com/Unknwon) and [lunny](h ## License -Gogs is under the MIT License. See the [LICENSE](https://github.com/gogits/gogs/blob/master/LICENSE) file for the full license text. +Gogs is under the MIT License. See the [LICENSE](https://github.com/gogits/gogs/blob/master/LICENSE) file for the full license text. \ No newline at end of file diff --git a/cmd/bin.go b/cmd/bin.go new file mode 100644 index 000000000..b1fd5c088 --- /dev/null +++ b/cmd/bin.go @@ -0,0 +1,191 @@ +// Copyright 2013-2014 gopm authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package cmd + +import ( + "fmt" + "os" + "path" + "path/filepath" + "runtime" + "strings" + + "github.com/Unknwon/com" + "github.com/codegangsta/cli" + + "github.com/gpmgo/gopm/doc" + "github.com/gpmgo/gopm/log" +) + +var CmdBin = cli.Command{ + Name: "bin", + Usage: "download and link dependencies and build executable binary", + Description: `Command bin downloads and links dependencies according to gopmfile, +and build executable binary to work directory + +gopm bin @[:] +gopm bin @[:] + +Can only specify one each time, and only works for projects that +contain main package`, + Action: runBin, + Flags: []cli.Flag{ + cli.BoolFlag{"dir, d", "build binary to given directory(second argument)"}, + cli.BoolFlag{"update, u", "update pakcage(s) and dependencies if any"}, + cli.BoolFlag{"verbose, v", "show process details"}, + }, +} + +func runBin(ctx *cli.Context) { + setup(ctx) + + if len(ctx.Args()) == 0 { + log.Error("bin", "Cannot start command:") + log.Fatal("", "\tNo package specified") + } + + installRepoPath = doc.HomeDir + "/repos" + log.Log("Local repository path: %s", installRepoPath) + + // Check arguments. + num := 1 + if ctx.Bool("dir") { + num = 2 + } + if len(ctx.Args()) != num { + log.Error("bin", "Cannot start command:") + log.Fatal("", "\tMissing indicated path to build binary") + } + + // Check if given directory exists. + if ctx.Bool("dir") && !com.IsDir(ctx.Args()[1]) { + log.Error("bin", "Cannot start command:") + log.Fatal("", "\tIndicated path does not exist or is not a directory") + } + + // Parse package version. + info := ctx.Args()[0] + pkgPath := info + node := doc.NewNode(pkgPath, pkgPath, doc.BRANCH, "", true) + var err error + if i := strings.Index(info, "@"); i > -1 { + pkgPath = info[:i] + node.ImportPath = pkgPath + node.DownloadURL = pkgPath + node.Type, node.Value = validPath(info[i+1:]) + } + + // Check package name. + if !strings.Contains(pkgPath, "/") { + pkgPath = doc.GetPkgFullPath(pkgPath) + } + + // Get code. + downloadPackages(ctx, []*doc.Node{node}) + + // Check if previous steps were successful. + repoPath := installRepoPath + "/" + pkgPath + versionSuffix(node.Value) + if !com.IsDir(repoPath) { + log.Error("bin", "Cannot continue command:") + log.Fatal("", "\tPrevious steps weren't successful") + } + + wd, err := os.Getwd() + if err != nil { + log.Error("bin", "Cannot get work directory:") + log.Fatal("", "\t"+err.Error()) + } + + // Change to repository path. + log.Log("Changing work directory to %s", repoPath) + if err = os.Chdir(repoPath); err != nil { + log.Error("bin", "Fail to change work directory:") + log.Fatal("", "\t"+err.Error()) + } + + // Build application. + buildBinary(ctx) + defer func() { + // Clean files. + os.RemoveAll(path.Join(repoPath, doc.VENDOR)) + }() + + includes := make([]string, 0, 3) + // Check if previous steps were successful. + if com.IsFile(doc.GOPM_FILE_NAME) { + log.Trace("Loading gopmfile...") + gf := doc.NewGopmfile(".") + + var err error + pkgName, err = gf.GetValue("target", "path") + if err == nil { + log.Log("Target name: %s", pkgName) + } + + includes = strings.Split(gf.MustValue("res", "include"), "|") + } + + if len(pkgName) == 0 { + _, pkgName = filepath.Split(pkgPath) + } + + // Because build command moved binary to root path. + binName := path.Base(pkgName) + if runtime.GOOS == "windows" { + binName += ".exe" + } + if !com.IsFile(binName) { + log.Error("bin", "Binary does not exist:") + log.Error("", "\t"+binName) + log.Fatal("", "\tPrevious steps weren't successful or the project does not contain main package") + } + + // Move binary to given directory. + movePath := wd + if ctx.Bool("dir") { + movePath = ctx.Args()[1] + } + + if com.IsExist(movePath + "/" + binName) { + if err = os.Remove(movePath + "/" + binName); err != nil { + log.Warn("Cannot remove binary in work directory:") + log.Warn("\t %s", err) + } + } + + if err = os.Rename(binName, movePath+"/"+binName); err != nil { + log.Error("bin", "Fail to move binary:") + log.Fatal("", "\t"+err.Error()) + } + os.Chmod(movePath+"/"+binName, os.ModePerm) + + if len(includes) > 0 { + log.Log("Copying resources to %s", movePath) + for _, include := range includes { + if com.IsDir(include) { + if err = com.CopyDir(include, filepath.Join(movePath, include)); err != nil { + log.Error("bin", "Fail to copy following resource:") + log.Error("", "\t"+include) + } + } + } + } + + log.Log("Changing work directory back to %s", wd) + os.Chdir(wd) + + log.Success("SUCC", "bin", "Command executed successfully!") + fmt.Println("Binary has been built into: " + movePath) +} diff --git a/cmd/build.go b/cmd/build.go new file mode 100644 index 000000000..8e8cb68b2 --- /dev/null +++ b/cmd/build.go @@ -0,0 +1,86 @@ +// Copyright 2013 gopm authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package cmd + +import ( + "os" + "path" + "path/filepath" + + "github.com/Unknwon/com" + "github.com/codegangsta/cli" + + "github.com/gpmgo/gopm/doc" + "github.com/gpmgo/gopm/log" +) + +var CmdBuild = cli.Command{ + Name: "build", + Usage: "link dependencies and go build", + Description: `Command build links dependencies according to gopmfile, +and execute 'go build' + +gopm build `, + Action: runBuild, + Flags: []cli.Flag{ + cli.BoolFlag{"update, u", "update pakcage(s) and dependencies if any"}, + cli.BoolFlag{"verbose, v", "show process details"}, + }, +} + +func runBuild(ctx *cli.Context) { + setup(ctx) + + // Get GOPATH. + installGopath = com.GetGOPATHs()[0] + if com.IsDir(installGopath) { + isHasGopath = true + log.Log("Indicated GOPATH: %s", installGopath) + installGopath += "/src" + } + + buildBinary(ctx, ctx.Args()...) + log.Success("SUCC", "build", "Command executed successfully!") +} + +func buildBinary(ctx *cli.Context, args ...string) { + genNewGoPath(ctx, false) + + log.Trace("Building...") + + cmdArgs := []string{"go", "build"} + cmdArgs = append(cmdArgs, args...) + err := execCmd(newGoPath, newCurPath, cmdArgs...) + if err != nil { + log.Error("build", "fail to build program:") + log.Fatal("", "\t"+err.Error()) + } + + if isWindowsXP { + fName := path.Base(pkgName) + binName := fName + ".exe" + os.Remove(binName) + exePath := filepath.Join(curPath, doc.VENDOR, "src", pkgName, binName) + if com.IsFile(exePath) { + err = os.Rename(exePath, filepath.Join(curPath, binName)) + if err != nil { + log.Error("build", "fail to move binary:") + log.Fatal("", "\t"+err.Error()) + } + } else { + log.Warn("No binary generated") + } + } +} diff --git a/cmd/cmd.go b/cmd/cmd.go new file mode 100644 index 000000000..279bb84d3 --- /dev/null +++ b/cmd/cmd.go @@ -0,0 +1,88 @@ +// Copyright 2013-2014 gopm authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package cmd + +import ( + "os" + "strings" + + "github.com/Unknwon/com" + "github.com/codegangsta/cli" + + "github.com/gpmgo/gopm/doc" + "github.com/gpmgo/gopm/log" +) + +var ( + workDir string // The path of gopm was executed. +) + +// setup initialize common environment for commands. +func setup(ctx *cli.Context) { + var err error + workDir, err = os.Getwd() + if err != nil { + log.Error("setup", "Fail to get work directory:") + log.Fatal("", "\t"+err.Error()) + } + + log.PureMode = ctx.GlobalBool("noterm") + log.Verbose = ctx.Bool("verbose") +} + +// parseTarget returns "." when target is empty string. +func parseTarget(target string) string { + if len(target) == 0 { + target = "." + } + return target +} + +// validPath checks if the information of the package is valid. +func validPath(info string) (string, string) { + infos := strings.Split(info, ":") + + l := len(infos) + switch { + case l == 1: + // For local imports. + if com.IsFile(infos[0]) { + return doc.LOCAL, infos[0] + } + case l == 2: + switch infos[1] { + case doc.TRUNK, doc.MASTER, doc.DEFAULT: + infos[1] = "" + } + return infos[0], infos[1] + } + + log.Error("", "Cannot parse dependency version:") + log.Error("", "\t"+info) + log.Help("Try 'gopm help get' to get more information") + return "", "" +} + +func versionSuffix(value string) string { + if len(value) > 0 { + return "." + value + } + return "" +} + +func isSubpackage(rootPath, targetPath string) bool { + return strings.HasSuffix(strings.Replace(workDir, "\\", "/", -1), rootPath) || + strings.HasPrefix(rootPath, targetPath) +} diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go new file mode 100644 index 000000000..70a146b4f --- /dev/null +++ b/cmd/cmd_test.go @@ -0,0 +1,31 @@ +// Copyright 2013-2014 gopm authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package cmd + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func Test_parseTarget(t *testing.T) { + Convey("Target is empty", t, func() { + So(parseTarget(""), ShouldEqual, ".") + }) + + Convey("Target is not empty", t, func() { + So(parseTarget("github.com/gpmgo/gopm"), ShouldEqual, "github.com/gpmgo/gopm") + }) +} diff --git a/cmd/config.go b/cmd/config.go new file mode 100644 index 000000000..28b0f6412 --- /dev/null +++ b/cmd/config.go @@ -0,0 +1,60 @@ +// Copyright 2013-2014 gopm authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package cmd + +import ( + "path" + + "github.com/Unknwon/goconfig" + "github.com/codegangsta/cli" + + "github.com/gpmgo/gopm/doc" + "github.com/gpmgo/gopm/log" +) + +var CmdConfig = cli.Command{ + Name: "config", + Usage: "configurate gopm global settings", + Description: `Command config configurates gopm global settings + +gopm config github [client_id] [client_secret] +`, + Action: runConfig, + Flags: []cli.Flag{ + cli.BoolFlag{"verbose, v", "show process details"}, + }, +} + +func runConfig(ctx *cli.Context) { + setup(ctx) + + if len(ctx.Args()) == 0 { + log.Error("config", "Cannot start command:") + log.Fatal("", "\tNo section specified") + } + + switch ctx.Args()[0] { + case "github": + if len(ctx.Args()) < 3 { + log.Error("config", "Cannot config section 'github'") + log.Fatal("", "\tNot enough arguments for client_id and client_secret") + } + doc.Cfg.SetValue("github", "client_id", ctx.Args()[1]) + doc.Cfg.SetValue("github", "client_secret", ctx.Args()[2]) + goconfig.SaveConfigFile(doc.Cfg, path.Join(doc.HomeDir, doc.GOPM_CONFIG_FILE)) + } + + log.Success("SUCC", "config", "Command executed successfully!") +} diff --git a/cmd/gen.go b/cmd/gen.go new file mode 100644 index 000000000..5e067b7a8 --- /dev/null +++ b/cmd/gen.go @@ -0,0 +1,91 @@ +// Copyright 2013-2014 gopm authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package cmd + +import ( + "os" + "strings" + + "github.com/Unknwon/com" + "github.com/Unknwon/goconfig" + "github.com/codegangsta/cli" + + "github.com/gpmgo/gopm/doc" + "github.com/gpmgo/gopm/log" +) + +var CmdGen = cli.Command{ + Name: "gen", + Usage: "generate a gopmfile according current Go project", + Description: `Command gen gets dependencies and generates a gopmfile + +gopm gen + +Make sure you run this command in the root path of a go project.`, + Action: runGen, + Flags: []cli.Flag{ + cli.BoolFlag{"example, e", "check dependencies for example(s)"}, + cli.BoolFlag{"verbose, v", "show process details"}, + }, +} + +var commonRes = []string{"views", "templates", "static", "public", "conf"} + +func runGen(ctx *cli.Context) { + setup(ctx) + + if !com.IsExist(".gopmfile") { + os.Create(".gopmfile") + } + + gf, err := goconfig.LoadConfigFile(".gopmfile") + if err != nil { + log.Error("gen", "Cannot load gopmfile:") + log.Fatal("", "\t"+err.Error()) + } + + targetPath := parseTarget(gf.MustValue("target", "path")) + // Get and set dependencies. + imports := doc.GetAllImports([]string{workDir}, targetPath, ctx.Bool("example"), false) + for _, p := range imports { + p = doc.GetProjectPath(p) + // Skip subpackage(s) of current project. + if isSubpackage(p, targetPath) { + continue + } + + // Check if user specified the version. + if value := gf.MustValue("deps", p); len(value) == 0 { + gf.SetValue("deps", p, "") + } + } + + // Get and set resources. + res := make([]string, 0, len(commonRes)) + for _, cr := range commonRes { + if com.IsExist(cr) { + res = append(res, cr) + } + } + gf.SetValue("res", "include", strings.Join(res, "|")) + + err = goconfig.SaveConfigFile(gf, ".gopmfile") + if err != nil { + log.Error("gen", "Fail to save gopmfile:") + log.Fatal("", "\t"+err.Error()) + } + + log.Success("SUCC", "gen", "Generate gopmfile successfully!") +} diff --git a/cmd/get.go b/cmd/get.go new file mode 100644 index 000000000..d747502e6 --- /dev/null +++ b/cmd/get.go @@ -0,0 +1,402 @@ +// Copyright 2013-2014 gopm authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package cmd + +import ( + "fmt" + "os" + "path" + "strings" + + "github.com/Unknwon/com" + "github.com/Unknwon/goconfig" + "github.com/codegangsta/cli" + + "github.com/gpmgo/gopm/doc" + "github.com/gpmgo/gopm/log" +) + +var ( + installRepoPath string // The path of gopm local repository. + installGopath string // The first path in the GOPATH. + isHasGopath bool // Indicates whether system has GOPATH. + + downloadCache map[string]bool // Saves packages that have been downloaded. + downloadCount int + failConut int +) + +var CmdGet = cli.Command{ + Name: "get", + Usage: "fetch remote package(s) and dependencies to local repository", + Description: `Command get fetches a package, and any pakcage that it depents on. +If the package has a gopmfile, the fetch process will be driven by that. + +gopm get +gopm get @[:] +gopm get @[:] + +Can specify one or more: gopm get beego@tag:v0.9.0 github.com/beego/bee + +If no version specified and package exists in GOPATH, +it will be skipped unless user enabled '--remote, -r' option +then all the packages go into gopm local repository.`, + Action: runGet, + Flags: []cli.Flag{ + cli.BoolFlag{"gopath, g", "download all pakcages to GOPATH"}, + cli.BoolFlag{"update, u", "update pakcage(s) and dependencies if any"}, + cli.BoolFlag{"example, e", "download dependencies for example folder"}, + cli.BoolFlag{"remote, r", "download all pakcages to gopm local repository"}, + cli.BoolFlag{"verbose, v", "show process details"}, + }, +} + +func init() { + downloadCache = make(map[string]bool) +} + +func runGet(ctx *cli.Context) { + setup(ctx) + // Check conflicts. + if ctx.Bool("gopath") && ctx.Bool("remote") { + log.Error("get", "Command options have conflicts") + log.Error("", "Following options are not supposed to use at same time:") + log.Error("", "\t'--gopath, -g' '--remote, -r'") + log.Help("Try 'gopm help get' to get more information") + } + + if !ctx.Bool("remote") { + // Get GOPATH. + installGopath = com.GetGOPATHs()[0] + if com.IsDir(installGopath) { + isHasGopath = true + log.Log("Indicated GOPATH: %s", installGopath) + installGopath += "/src" + } else { + if ctx.Bool("gopath") { + log.Error("get", "Invalid GOPATH path") + log.Error("", "GOPATH does not exist or is not a directory:") + log.Error("", "\t"+installGopath) + log.Help("Try 'go help gopath' to get more information") + } else { + // It's OK that no GOPATH setting + // when user does not specify to use. + log.Warn("No GOPATH setting available") + } + } + } + + // The gopm local repository. + installRepoPath = path.Join(doc.HomeDir, "repos") + log.Log("Local repository path: %s", installRepoPath) + + // Check number of arguments to decide which function to call. + switch len(ctx.Args()) { + case 0: + getByGopmfile(ctx) + case 1: + getByPath(ctx) + default: + log.Error("get", "too many arguments") + log.Help("Try 'gopm help get' to get more information") + } +} + +func getByGopmfile(ctx *cli.Context) { + // Check if gopmfile exists and generate one if not. + if !com.IsFile(".gopmfile") { + runGen(ctx) + } + gf := doc.NewGopmfile(".") + + targetPath := parseTarget(gf.MustValue("target", "path")) + // Get dependencies. + imports := doc.GetAllImports([]string{workDir}, targetPath, ctx.Bool("example"), false) + + nodes := make([]*doc.Node, 0, len(imports)) + for _, p := range imports { + // TODO: DOING TEST CASES!!! + p = doc.GetProjectPath(p) + // Skip subpackage(s) of current project. + if isSubpackage(p, targetPath) { + continue + } + node := doc.NewNode(p, p, doc.BRANCH, "", true) + + // Check if user specified the version. + if v, err := gf.GetValue("deps", p); err == nil && len(v) > 0 { + node.Type, node.Value = validPath(v) + } + nodes = append(nodes, node) + } + + downloadPackages(ctx, nodes) + doc.SaveLocalNodes() + + log.Log("%d package(s) downloaded, %d failed", downloadCount, failConut) +} + +func getByPath(ctx *cli.Context) { + nodes := make([]*doc.Node, 0, len(ctx.Args())) + for _, info := range ctx.Args() { + pkgPath := info + node := doc.NewNode(pkgPath, pkgPath, doc.BRANCH, "", true) + + if i := strings.Index(info, "@"); i > -1 { + pkgPath = info[:i] + tp, ver := validPath(info[i+1:]) + node = doc.NewNode(pkgPath, pkgPath, tp, ver, true) + } + + // Check package name. + if !strings.Contains(pkgPath, "/") { + pkgPath = doc.GetPkgFullPath(pkgPath) + } + + nodes = append(nodes, node) + } + + downloadPackages(ctx, nodes) + doc.SaveLocalNodes() + + log.Log("%d package(s) downloaded, %d failed", downloadCount, failConut) +} + +func copyToGopath(srcPath, destPath string) { + importPath := strings.TrimPrefix(destPath, installGopath+"/") + if len(getVcsName(destPath)) > 0 { + log.Warn("Package in GOPATH has version control: %s", importPath) + return + } + + os.RemoveAll(destPath) + err := com.CopyDir(srcPath, destPath) + if err != nil { + log.Error("download", "Fail to copy to GOPATH:") + log.Fatal("", "\t"+err.Error()) + } + + log.Log("Package copied to GOPATH: %s", importPath) +} + +// downloadPackages downloads packages with certain commit, +// if the commit is empty string, then it downloads all dependencies, +// otherwise, it only downloada package with specific commit only. +func downloadPackages(ctx *cli.Context, nodes []*doc.Node) { + // Check all packages, they may be raw packages path. + for _, n := range nodes { + // Check if local reference + if n.Type == doc.LOCAL { + continue + } + // Check if it is a valid remote path or C. + if n.ImportPath == "C" { + continue + } else if !doc.IsValidRemotePath(n.ImportPath) { + // Invalid import path. + log.Error("download", "Skipped invalid package: "+fmt.Sprintf("%s@%s:%s", + n.ImportPath, n.Type, doc.CheckNodeValue(n.Value))) + failConut++ + continue + } + + // Valid import path. + gopathDir := path.Join(installGopath, n.ImportPath) + n.RootPath = doc.GetProjectPath(n.ImportPath) + installPath := path.Join(installRepoPath, n.RootPath) + versionSuffix(n.Value) + + if isSubpackage(n.RootPath, ".") { + continue + } + + // Indicates whether need to download package again. + if n.IsFixed() && com.IsExist(installPath) { + n.IsGetDepsOnly = true + } + + if !ctx.Bool("update") { + // Check if package has been downloaded. + if (len(n.Value) == 0 && !ctx.Bool("remote") && com.IsExist(gopathDir)) || + com.IsExist(installPath) { + log.Trace("Skipped installed package: %s@%s:%s", + n.ImportPath, n.Type, doc.CheckNodeValue(n.Value)) + + // Only copy when no version control. + if ctx.Bool("gopath") && (com.IsExist(installPath) || + len(getVcsName(gopathDir)) == 0) { + copyToGopath(installPath, gopathDir) + } + continue + } else { + doc.LocalNodes.SetValue(n.RootPath, "value", "") + } + } + + if downloadCache[n.RootPath] { + log.Trace("Skipped downloaded package: %s@%s:%s", + n.ImportPath, n.Type, doc.CheckNodeValue(n.Value)) + continue + } + + // Download package. + nod, imports := downloadPackage(ctx, n) + if len(imports) > 0 { + var gf *goconfig.ConfigFile + + // Check if has gopmfile. + if com.IsFile(installPath + "/" + doc.GOPM_FILE_NAME) { + log.Log("Found gopmfile: %s@%s:%s", + n.ImportPath, n.Type, doc.CheckNodeValue(n.Value)) + gf = doc.NewGopmfile(installPath) + } + + // Need to download dependencies. + // Generate temporary nodes. + nodes := make([]*doc.Node, len(imports)) + for i := range nodes { + nodes[i] = doc.NewNode(imports[i], imports[i], doc.BRANCH, "", true) + + if gf == nil { + continue + } + + // Check if user specified the version. + if v, err := gf.GetValue("deps", imports[i]); err == nil && len(v) > 0 { + nodes[i].Type, nodes[i].Value = validPath(v) + } + } + downloadPackages(ctx, nodes) + } + + // Only save package information with specific commit. + if nod == nil { + continue + } + + // Save record in local nodes. + log.Success("SUCC", "GET", fmt.Sprintf("%s@%s:%s", + n.ImportPath, n.Type, doc.CheckNodeValue(n.Value))) + downloadCount++ + + // Only save non-commit node. + if len(nod.Value) == 0 && len(nod.Revision) > 0 { + doc.LocalNodes.SetValue(nod.RootPath, "value", nod.Revision) + } + + if ctx.Bool("gopath") && com.IsExist(installPath) && !ctx.Bool("update") && + len(getVcsName(path.Join(installGopath, nod.RootPath))) == 0 { + copyToGopath(installPath, gopathDir) + } + } +} + +// downloadPackage downloads package either use version control tools or not. +func downloadPackage(ctx *cli.Context, nod *doc.Node) (*doc.Node, []string) { + log.Message("Downloading", fmt.Sprintf("package: %s@%s:%s", + nod.ImportPath, nod.Type, doc.CheckNodeValue(nod.Value))) + // Mark as donwloaded. + downloadCache[nod.RootPath] = true + + // Check if only need to use VCS tools. + var imports []string + var err error + gopathDir := path.Join(installGopath, nod.RootPath) + vcs := getVcsName(gopathDir) + if ctx.Bool("update") && ctx.Bool("gopath") && len(vcs) > 0 { + err = updateByVcs(vcs, gopathDir) + imports = doc.GetAllImports([]string{gopathDir}, nod.RootPath, false, false) + } else { + // If package has revision and exist, then just check dependencies. + if nod.IsGetDepsOnly { + return nod, doc.GetAllImports([]string{path.Join(installRepoPath, nod.RootPath) + versionSuffix(nod.Value)}, + nod.RootPath, ctx.Bool("example"), false) + } + nod.Revision = doc.LocalNodes.MustValue(nod.RootPath, "value") + imports, err = doc.PureDownload(nod, installRepoPath, ctx) //CmdGet.Flags) + } + + if err != nil { + log.Error("get", "Fail to download pakage: "+nod.ImportPath) + log.Error("", "\t"+err.Error()) + failConut++ + os.RemoveAll(installRepoPath + "/" + nod.RootPath) + return nil, nil + } + return nod, imports +} + +func getVcsName(dirPath string) string { + switch { + case com.IsExist(path.Join(dirPath, ".git")): + return "git" + case com.IsExist(path.Join(dirPath, ".hg")): + return "hg" + case com.IsExist(path.Join(dirPath, ".svn")): + return "svn" + } + return "" +} + +func updateByVcs(vcs, dirPath string) error { + err := os.Chdir(dirPath) + if err != nil { + log.Error("Update by VCS", "Fail to change work directory:") + log.Fatal("", "\t"+err.Error()) + } + defer os.Chdir(workDir) + + switch vcs { + case "git": + branch, _, err := com.ExecCmd("git", "rev-parse", "--abbrev-ref", "HEAD") + if err != nil { + log.Error("", "Error occurs when 'git rev-parse --abbrev-ref HEAD'") + log.Error("", "\t"+err.Error()) + } + + _, _, err = com.ExecCmd("git", "pull", "origin", branch) + if err != nil { + log.Error("", "Error occurs when 'git pull origin "+branch+"'") + log.Error("", "\t"+err.Error()) + } + case "hg": + _, stderr, err := com.ExecCmd("hg", "pull") + if err != nil { + log.Error("", "Error occurs when 'hg pull'") + log.Error("", "\t"+err.Error()) + } + if len(stderr) > 0 { + log.Error("", "Error: "+stderr) + } + + _, stderr, err = com.ExecCmd("hg", "up") + if err != nil { + log.Error("", "Error occurs when 'hg up'") + log.Error("", "\t"+err.Error()) + } + if len(stderr) > 0 { + log.Error("", "Error: "+stderr) + } + case "svn": + _, stderr, err := com.ExecCmd("svn", "update") + if err != nil { + log.Error("", "Error occurs when 'svn update'") + log.Error("", "\t"+err.Error()) + } + if len(stderr) > 0 { + log.Error("", "Error: "+stderr) + } + } + return nil +} diff --git a/cmd/gopath.go b/cmd/gopath.go new file mode 100644 index 000000000..1c5c321cb --- /dev/null +++ b/cmd/gopath.go @@ -0,0 +1,292 @@ +// Copyright 2013-2014 gopm authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package cmd + +import ( + "errors" + "go/build" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + + "github.com/Unknwon/com" + "github.com/codegangsta/cli" + + "github.com/gpmgo/gopm/doc" + "github.com/gpmgo/gopm/log" +) + +var isWindowsXP = false + +func getGopmPkgs(dirPath string, isTest bool) (pkgs map[string]*doc.Pkg, err error) { + absPath, err := filepath.Abs(dirPath) + if err != nil { + log.Error("", "Fail to get absolute path of work directory:") + log.Fatal("", "\t"+err.Error()) + } + + var builds map[string]string + + if com.IsFile(absPath + "/" + doc.GOPM_FILE_NAME) { + gf := doc.NewGopmfile(absPath) + + if builds, err = gf.GetSection("deps"); err != nil { + builds = nil + } + } + + imports := doc.GetAllImports([]string{dirPath}, ".", false, false) + pkgs = make(map[string]*doc.Pkg) + for _, name := range imports { + if name == "C" { + continue + } + if !doc.IsGoRepoPath(name) { + if builds != nil { + if info, ok := builds[name]; ok { + // Check version. there should chek + // local first because d:\ contains : + if com.IsDir(info) { + pkgs[name] = &doc.Pkg{ + ImportPath: name, + Type: doc.LOCAL, + Value: info, + } + continue + } else if i := strings.Index(info, ":"); i > -1 { + pkgs[name] = &doc.Pkg{ + ImportPath: name, + Type: info[:i], + Value: info[i+1:], + } + continue + } + } + } + pkgs[name] = doc.NewDefaultPkg(name) + } + } + return pkgs, nil +} + +func pkgInCache(name string, cachePkgs map[string]*doc.Pkg) bool { + _, ok := cachePkgs[name] + return ok +} + +func autoLink(oldPath, newPath string) error { + newPPath, _ := filepath.Split(newPath) + os.MkdirAll(newPPath, os.ModePerm) + return makeLink(oldPath, newPath) +} + +func getChildPkgs(ctx *cli.Context, cpath string, ppkg *doc.Pkg, cachePkgs map[string]*doc.Pkg, isTest bool) error { + pkgs, err := getGopmPkgs(cpath, isTest) + if err != nil { + return errors.New("Fail to get gopmfile deps: " + err.Error()) + } + for name, pkg := range pkgs { + pkg.RootPath = doc.GetProjectPath(pkg.ImportPath) + if !pkgInCache(pkg.RootPath, cachePkgs) { + var newPath string + if !build.IsLocalImport(name) && pkg.Type != doc.LOCAL { + suf := versionSuffix(pkg.Value) + pkgPath := strings.Replace( + pkg.ImportPath, pkg.RootPath, pkg.RootPath+suf, 1) + newPath = filepath.Join(installRepoPath, pkgPath) + if len(suf) == 0 && !ctx.Bool("remote") && + com.IsDir(filepath.Join(installGopath, pkgPath)) { + newPath = filepath.Join(installGopath, pkgPath) + } + if pkgName != "" && strings.HasPrefix(pkg.ImportPath, pkgName) { + newPath = filepath.Join(curPath, strings.TrimPrefix(pkg.ImportPath, pkgName)) + } else { + if !com.IsExist(newPath) || ctx.Bool("update") { + node := doc.NewNode(pkg.ImportPath, pkg.ImportPath, + pkg.Type, pkg.Value, true) + nodes := []*doc.Node{node} + downloadPackages(ctx, nodes) + // TODO: Should handler download failed + } + } + } else { + if pkg.Type == doc.LOCAL { + newPath, err = filepath.Abs(pkg.Value) + } else { + newPath, err = filepath.Abs(name) + } + if err != nil { + return err + } + } + cachePkgs[pkg.RootPath] = pkg + err = getChildPkgs(ctx, newPath, pkg, cachePkgs, false) + if err != nil { + return err + } + } + } + return nil +} + +var pkgName string +var curPath string +var newCurPath string +var newGoPath string +var oldGoPath string + +func execCmd(gopath, curPath string, args ...string) error { + cwd, err := os.Getwd() + if err != nil { + log.Error("", "Fail to get work directory:") + log.Fatal("", "\t"+err.Error()) + } + + log.Log("Changing work directory to %s", curPath) + err = os.Chdir(curPath) + if err != nil { + log.Error("", "Fail to change work directory:") + log.Fatal("", "\t"+err.Error()) + } + defer func() { + log.Log("Changing work directory back to %s", cwd) + os.Chdir(cwd) + }() + + err = os.Chdir(curPath) + if err != nil { + log.Error("", "Fail to change work directory:") + log.Fatal("", "\t"+err.Error()) + } + + oldGoPath = os.Getenv("GOPATH") + log.Log("Setting GOPATH to %s", gopath) + + sep := ":" + if runtime.GOOS == "windows" { + sep = ";" + } + err = os.Setenv("GOPATH", gopath+sep+oldGoPath) + if err != nil { + log.Error("", "Fail to setting GOPATH:") + log.Fatal("", "\t"+err.Error()) + } + defer func() { + log.Log("Setting GOPATH back to %s", oldGoPath) + os.Setenv("GOPATH", oldGoPath) + }() + + cmd := exec.Command(args[0], args[1:]...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + log.Log("===== application outputs start =====\n") + + err = cmd.Run() + + log.Log("====== application outputs end ======") + return err +} + +func genNewGoPath(ctx *cli.Context, isTest bool) { + var err error + curPath, err = os.Getwd() + if err != nil { + log.Error("", "Fail to get work directory:") + log.Fatal("", "\t"+err.Error()) + } + + installRepoPath = doc.HomeDir + "/repos" + + if com.IsFile(curPath + "/" + doc.GOPM_FILE_NAME) { + log.Trace("Loading gopmfile...") + gf := doc.NewGopmfile(curPath) + + var err error + pkgName, err = gf.GetValue("target", "path") + if err == nil { + log.Log("Target name: %s", pkgName) + } + } + + if len(pkgName) == 0 { + _, pkgName = filepath.Split(curPath) + } + + cachePkgs := make(map[string]*doc.Pkg) + err = getChildPkgs(ctx, curPath, nil, cachePkgs, isTest) + if err != nil { + log.Error("", "Fail to get child pakcages:") + log.Fatal("", "\t"+err.Error()) + } + + newGoPath = filepath.Join(curPath, doc.VENDOR) + newGoPathSrc := filepath.Join(newGoPath, "src") + os.RemoveAll(newGoPathSrc) + os.MkdirAll(newGoPathSrc, os.ModePerm) + + for name, pkg := range cachePkgs { + suf := versionSuffix(pkg.Value) + + var oldPath string + if pkg.Type == doc.LOCAL { + oldPath, _ = filepath.Abs(pkg.Value) + } else { + oldPath = filepath.Join(installRepoPath, name) + suf + } + + newPath := filepath.Join(newGoPathSrc, name) + paths := strings.Split(name, "/") + var isExistP, isCurChild bool + if name == pkgName { + continue + } + + for i := 0; i < len(paths)-1; i++ { + pName := strings.Join(paths[:len(paths)-1-i], "/") + if _, ok := cachePkgs[pName]; ok { + isExistP = true + break + } + if pkgName == pName { + isCurChild = true + break + } + } + if isCurChild { + continue + } + + if !isExistP && (len(pkg.Value) > 0 || ctx.Bool("remote") || + !com.IsDir(filepath.Join(installGopath, pkg.ImportPath))) { + log.Log("Linking %s", name+suf) + err = autoLink(oldPath, newPath) + if err != nil { + log.Error("", "Fail to make link:") + log.Fatal("", "\t"+err.Error()) + } + } + } + + newCurPath = filepath.Join(newGoPathSrc, pkgName) + log.Log("Linking %s", pkgName) + err = autoLink(curPath, newCurPath) + if err != nil { + log.Error("", "Fail to make link:") + log.Fatal("", "\t"+err.Error()) + } +} diff --git a/cmd/helper.go b/cmd/helper.go new file mode 100644 index 000000000..bd7c6f52e --- /dev/null +++ b/cmd/helper.go @@ -0,0 +1,13 @@ +// +// +build !windows + +package cmd + +import ( + "os/exec" +) + +func makeLink(srcPath, destPath string) error { + cmd := exec.Command("ln", "-s", srcPath, destPath) + return cmd.Run() +} diff --git a/cmd/helper_windows.go b/cmd/helper_windows.go new file mode 100644 index 000000000..3ce70f060 --- /dev/null +++ b/cmd/helper_windows.go @@ -0,0 +1,81 @@ +package cmd + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "syscall" + "unsafe" + + "github.com/Unknwon/com" + + "github.com/gpmgo/gopm/doc" +) + +func makeLink(srcPath, destPath string) error { + // Check if Windows version is XP. + if getWindowsVersion() >= 6 { + cmd := exec.Command("cmd", "/c", "mklink", "/j", destPath, srcPath) + return cmd.Run() + } + + // XP. + isWindowsXP = true + // if both are ntfs file system + if volumnType(srcPath) == "NTFS" && volumnType(destPath) == "NTFS" { + // if has junction command installed + file, err := exec.LookPath("junction") + if err == nil { + path, _ := filepath.Abs(file) + if com.IsFile(path) { + cmd := exec.Command("cmd", "/c", "junction", destPath, srcPath) + return cmd.Run() + } + } + } + os.RemoveAll(destPath) + + return com.CopyDir(srcPath, destPath, func(filePath string) bool { + return strings.Contains(filePath, doc.VENDOR) + }) +} + +func volumnType(dir string) string { + pd := dir[:3] + dll := syscall.MustLoadDLL("kernel32.dll") + GetVolumeInformation := dll.MustFindProc("GetVolumeInformationW") + + var volumeNameSize uint32 = 260 + var nFileSystemNameSize, lpVolumeSerialNumber uint32 + var lpFileSystemFlags, lpMaximumComponentLength uint32 + var lpFileSystemNameBuffer, volumeName [260]byte + var ps *uint16 = syscall.StringToUTF16Ptr(pd) + + _, _, _ = GetVolumeInformation.Call(uintptr(unsafe.Pointer(ps)), + uintptr(unsafe.Pointer(&volumeName)), + uintptr(volumeNameSize), + uintptr(unsafe.Pointer(&lpVolumeSerialNumber)), + uintptr(unsafe.Pointer(&lpMaximumComponentLength)), + uintptr(unsafe.Pointer(&lpFileSystemFlags)), + uintptr(unsafe.Pointer(&lpFileSystemNameBuffer)), + uintptr(unsafe.Pointer(&nFileSystemNameSize)), 0) + + var bytes []byte + if lpFileSystemNameBuffer[6] == 0 { + bytes = []byte{lpFileSystemNameBuffer[0], lpFileSystemNameBuffer[2], + lpFileSystemNameBuffer[4]} + } else { + bytes = []byte{lpFileSystemNameBuffer[0], lpFileSystemNameBuffer[2], + lpFileSystemNameBuffer[4], lpFileSystemNameBuffer[6]} + } + + return string(bytes) +} + +func getWindowsVersion() int { + dll := syscall.MustLoadDLL("kernel32.dll") + p := dll.MustFindProc("GetVersion") + v, _, _ := p.Call() + return int(byte(v)) +} diff --git a/cmd/install.go b/cmd/install.go new file mode 100644 index 000000000..7d8af31a0 --- /dev/null +++ b/cmd/install.go @@ -0,0 +1,112 @@ +// Copyright 2013-2014 gopm authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package cmd + +import ( + "path/filepath" + + "github.com/Unknwon/com" + "github.com/codegangsta/cli" + + "github.com/gpmgo/gopm/doc" + "github.com/gpmgo/gopm/log" +) + +var CmdInstall = cli.Command{ + Name: "install", + Usage: "link dependencies and go install", + Description: `Command install links dependencies according to gopmfile, +and execute 'go install' + +gopm install +gopm install + +If no argument is supplied, then gopmfile must be present`, + Action: runInstall, + Flags: []cli.Flag{ + cli.BoolFlag{"pkg, p", "only install non-main packages"}, + cli.BoolFlag{"verbose, v", "show process details"}, + }, +} + +func runInstall(ctx *cli.Context) { + setup(ctx) + + var target string + switch len(ctx.Args()) { + case 0: + if !com.IsFile(".gopmfile") { + break + } + + gf := doc.NewGopmfile(".") + target = gf.MustValue("target", "path") + case 1: + target = ctx.Args()[0] + default: + log.Fatal("install", "Too many arguments") + } + + // Get GOPATH. + installGopath = com.GetGOPATHs()[0] + if com.IsDir(installGopath) { + isHasGopath = true + log.Log("Indicated GOPATH: %s", installGopath) + installGopath += "/src" + } else { + if ctx.Bool("gopath") { + log.Error("get", "Invalid GOPATH path") + log.Error("", "GOPATH does not exist or is not a directory:") + log.Error("", "\t"+installGopath) + log.Help("Try 'go help gopath' to get more information") + } else { + // It's OK that no GOPATH setting + // when user does not specify to use. + log.Warn("No GOPATH setting available") + } + } + + genNewGoPath(ctx, false) + + var installRepos []string + if ctx.Bool("pkg") { + curPath, _ := filepath.Abs(".") + installRepos = doc.GetAllImports([]string{curPath}, ".", ctx.Bool("example"), false) + } else { + if len(target) == 0 { + target = pkgName + } + + installRepos = []string{target} + } + + log.Trace("Installing...") + + for _, repo := range installRepos { + cmdArgs := []string{"go", "install"} + + if ctx.Bool("verbose") { + cmdArgs = append(cmdArgs, "-v") + } + cmdArgs = append(cmdArgs, repo) + err := execCmd(newGoPath, newCurPath, cmdArgs...) + if err != nil { + log.Error("install", "Fail to install program:") + log.Fatal("", "\t"+err.Error()) + } + } + + log.Success("SUCC", "install", "Command executed successfully!") +} diff --git a/cmd/run.go b/cmd/run.go new file mode 100644 index 000000000..fd07c454b --- /dev/null +++ b/cmd/run.go @@ -0,0 +1,58 @@ +// Copyright 2013 gopm authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package cmd + +import ( + "github.com/Unknwon/com" + "github.com/codegangsta/cli" + + "github.com/gpmgo/gopm/log" +) + +var CmdRun = cli.Command{ + Name: "run", + Usage: "link dependencies and go run", + Description: `Command run links dependencies according to gopmfile, +and execute 'go run' + +gopm run `, + Action: runRun, +} + +func runRun(ctx *cli.Context) { + setup(ctx) + + // Get GOPATH. + installGopath = com.GetGOPATHs()[0] + if com.IsDir(installGopath) { + isHasGopath = true + log.Log("Indicated GOPATH: %s", installGopath) + installGopath += "/src" + } + + genNewGoPath(ctx, false) + + log.Trace("Running...") + + cmdArgs := []string{"go", "run"} + cmdArgs = append(cmdArgs, ctx.Args()...) + err := execCmd(newGoPath, newCurPath, cmdArgs...) + if err != nil { + log.Error("run", "Fail to run program:") + log.Fatal("", "\t"+err.Error()) + } + + log.Success("SUCC", "run", "Command executed successfully!") +} diff --git a/cmd/search.go b/cmd/search.go new file mode 100644 index 000000000..f3cb1b5cc --- /dev/null +++ b/cmd/search.go @@ -0,0 +1,112 @@ +// Copyright 2013 gopm authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package cmd + +// import ( +// "encoding/json" +// "fmt" +// "io/ioutil" +// "net/http" + +// "github.com/Unknwon/com" +// ) + +// var CmdSearch = &Command{ +// UsageLine: "search [keyword]", +// Short: "search for package", +// Long: ` +// search packages + +// The search flags are: + +// -e +// search extactly, you should input an exactly package name as keyword +// `, +// } + +// func init() { +// CmdSearch.Run = runSearch +// CmdSearch.Flags = map[string]bool{ +// "-e": false, +// } +// } + +// func printSearchPrompt(flag string) { +// switch flag { +// case "-e": +// com.ColorLog("[INFO] You enabled exactly search.\n") +// } +// } + +// // search packages +// func runSearch(cmd *Command, args []string) { + +// // Check length of arguments. +// if len(args) < 1 { +// com.ColorLog("[ERROR] Please input package's keyword.\n") +// return +// } + +// var host, port string +// host = "localhost" +// port = "8991" + +// if cmd.Flags["-e"] { +// search(host, port, args[0], true) +// } else { +// search(host, port, args[0], false) +// } +// } + +// type searchRes struct { +// Pkg string +// Desc string +// } + +// /* +// request local or remote search service to find packages according to keyword inputed +// */ +// func search(host, port, keyword string, isExactly bool) { +// url := fmt.Sprintf("http://%v:%v/search?%v", host, port, keyword) +// if isExactly { +// url = fmt.Sprintf("http://%v:%v/searche?%v", host, port, keyword) +// } +// resp, err := http.Get(url) +// if err != nil { +// com.ColorLog(err.Error()) +// return +// } +// defer resp.Body.Close() + +// if resp.StatusCode == 200 { +// contents, err := ioutil.ReadAll(resp.Body) +// if err != nil { +// com.ColorLog(err.Error()) +// return +// } + +// pkgs := make([]searchRes, 0) +// err = json.Unmarshal(contents, &pkgs) +// if err != nil { +// com.ColorLog(err.Error()) +// return +// } +// for i, pkg := range pkgs { +// fmt.Println(i+1, pkg.Pkg, "\t", pkg.Desc) +// } +// } else { +// com.ColorLog(resp.Status) +// } +// } diff --git a/cmd/serve.go b/cmd/serve.go new file mode 100644 index 000000000..65cb482c7 --- /dev/null +++ b/cmd/serve.go @@ -0,0 +1,552 @@ +// Copyright 2013 gopm authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package cmd + +// import ( +// "errors" +// "fmt" +// "github.com/Unknwon/com" +// "github.com/gpmgo/gopm/doc" +// "github.com/syndtr/goleveldb/leveldb" +// "github.com/syndtr/goleveldb/leveldb/opt" +// "io/ioutil" +// "net/http" +// "net/url" +// "os" +// "os/exec" +// "path/filepath" +// "strconv" +// "strings" +// "time" +// ) + +// var ( +// dbDir = "~/.gopm/db" +// ) + +// const ( +// STOP = iota +// LOCALRUN +// RUNNING +// ) + +// var CmdServe = &Command{ +// UsageLine: "serve [:port]", +// Short: "serve for package search", +// Long: ` +// serve provide a web service to search packages, download packages + +// The serve flags are: + +// -l +// only service for localhost ip +// `, +// } + +// func init() { +// CmdServe.Run = runServe +// CmdServe.Flags = map[string]bool{ +// "-l": false, +// } +// } + +// func printServePrompt(flag string) { +// switch flag { +// case "-l": +// com.ColorLog("[INFO] You enabled start a service only localhost.\n") +// } +// } + +// // Not implemented +// func autoPort() string { +// return "8991" +// } + +// func exePath() (string, error) { +// file, err := exec.LookPath(os.Args[0]) +// if err != nil { +// return "", err +// } + +// return filepath.Abs(file) +// } + +// // search packages +// func runServe(cmd *Command, args []string) { +// // Check flags. +// num := checkFlags(cmd.Flags, args, printServePrompt) +// if num == -1 { +// return +// } +// args = args[num:] + +// var listen string +// var port string +// if cmd.Flags["-l"] { +// listen += "127.0.0.1" +// port = autoPort() +// } else { +// listen += "0.0.0.0" +// port = "8991" +// } + +// // Check length of arguments. +// if len(args) >= 1 { +// port = args[0] +// } + +// err := startService(listen, port) +// if err != nil { +// com.ColorLog("[ERRO] %v\n", err) +// } +// } + +// func splitWord(word string, res *map[string]bool) { +// for i, _ := range word { +// for j, _ := range word[i:] { +// w := word[i : i+j+1] +// (*res)[w] = true +// } +// } +// return +// } + +// func splitPkgName(pkgName string) (res map[string]bool) { +// //var src string +// ps := strings.Split(pkgName, "/") +// if len(ps) > 1 { +// ps = ps[1:] +// } + +// res = make(map[string]bool) +// res[strings.Join(ps, "/")] = true +// for _, w := range ps { +// splitWord(w, &res) +// } +// return +// } + +// func splitSynopsis(synopsis string) map[string]bool { +// res := make(map[string]bool) +// ss := strings.Fields(synopsis) +// for _, s := range ss { +// res[s] = true +// } +// return res +// } + +// var ( +// ro *opt.ReadOptions = &opt.ReadOptions{} +// wo *opt.WriteOptions = &opt.WriteOptions{} +// ) + +// func dbGet(key string) (string, error) { +// v, err := db.Get([]byte(key), ro) +// return string(v), err +// } + +// func dbPut(key string, value string) error { +// //fmt.Println("put ", key, ": ", value) +// return db.Put([]byte(key), []byte(value), wo) +// } + +// func batchPut(batch *leveldb.Batch, key string, value string) error { +// //fmt.Println("put ", key, ": ", value) +// batch.Put([]byte(key), []byte(value)) +// return nil +// } + +// func getServeHost() string { +// return "localhost" +// } + +// func getServePort() string { +// return "8991" +// } + +// // for exernal of serve to add node to db +// func saveNode(nod *doc.Node) error { +// urlPath := fmt.Sprintf("http://%v:%v/add", getServeHost(), getServePort()) +// resp, err := http.PostForm(urlPath, +// url.Values{"importPath": {nod.ImportPath}, +// "synopsis": {nod.Synopsis}, +// "downloadURL": {nod.DownloadURL}, +// "isGetDeps": {strconv.FormatBool(nod.IsGetDeps)}, +// "type": {nod.Type}, +// "value": {nod.Value}}) + +// if err != nil { +// com.ColorLog("[ERRO] Fail to save node[ %s ]\n", err) +// return err +// } +// defer resp.Body.Close() + +// if resp.StatusCode == 200 { +// return nil +// } +// return errors.New("save node failed with " + resp.Status) +// } + +// // for inetrnal of serve to add node to db +// func addNode(nod *doc.Node) error { +// batch := new(leveldb.Batch) +// strLastId, err := dbGet("lastId") +// if err != nil { +// if err == leveldb.ErrNotFound { +// strLastId = "0" +// err = batchPut(batch, "lastId", strLastId) +// } else { +// return err +// } +// } +// if err != nil { +// return err +// } + +// lastId, err := strconv.ParseInt(strLastId, 0, 64) +// if err != nil { +// return err +// } + +// nodKey := fmt.Sprintf("index:%v", nod.ImportPath) + +// id, err := dbGet(nodKey) +// if err != nil { +// if err == leveldb.ErrNotFound { +// id = fmt.Sprintf("%v", lastId+1) +// err = batchPut(batch, "lastId", id) +// if err == nil { +// err = batchPut(batch, nodKey, id) +// } +// if err == nil { +// err = batchPut(batch, "pkg:"+id, nod.ImportPath) +// } +// if err == nil { +// err = batchPut(batch, "desc:"+id, nod.Synopsis) +// } +// if err == nil { +// err = batchPut(batch, "down:"+id, nod.DownloadURL) +// } +// if err == nil { +// err = batchPut(batch, "deps:"+id, strconv.FormatBool(nod.IsGetDeps)) +// } + +// // save totals +// total, err := dbGet("total") +// if err != nil { +// if err == leveldb.ErrNotFound { +// total = "1" +// } else { +// return err +// } +// } else { +// totalInt, err := strconv.ParseInt(total, 0, 64) +// if err != nil { +// return err +// } +// totalInt = totalInt + 1 +// total = fmt.Sprintf("%v", totalInt) +// } + +// err = batchPut(batch, "total", total) +// } else { +// return err +// } +// } + +// if err != nil { +// return err +// } + +// // save vers +// vers, err := dbGet("ver:" + id) +// needSplit := (err == leveldb.ErrNotFound) +// if err != nil { +// if err != leveldb.ErrNotFound { +// return err +// } +// } else { +// return nil +// } + +// if vers == "" { +// //fmt.Println(nod) +// vers = nod.VerString() +// } else { +// if !strings.Contains(vers, nod.VerString()) { +// vers = vers + "," + nod.VerString() +// } else { +// return nil +// } +// } + +// err = batchPut(batch, "ver:"+id, vers) +// if err != nil { +// return err +// } + +// if !needSplit { +// return nil +// } + +// // indexing package name +// keys := splitPkgName(nod.ImportPath) +// for key, _ := range keys { +// err = batchPut(batch, fmt.Sprintf("key:%v:%v", strings.ToLower(key), id), "") +// if err != nil { +// return err +// } +// } + +// if nod.Synopsis != "" { +// fields := splitSynopsis(nod.Synopsis) +// for field, _ := range fields { +// err = batchPut(batch, fmt.Sprintf("key:%v:%v", strings.ToLower(field), id), "") +// if err != nil { +// return err +// } +// } +// } + +// return db.Write(batch, wo) +// } + +// func rmPkg(nod *doc.Node) { + +// } + +// var db *leveldb.DB + +// // service should be run +// func AutoRun() error { +// s, _, _ := runningStatus() +// if s == STOP { +// // current path +// curPath, err := os.Getwd() +// if err != nil { +// return err +// } + +// attr := &os.ProcAttr{ +// Dir: curPath, +// Env: os.Environ(), +// //Files: []*os.File{nil, nil, nil}, +// Files: []*os.File{os.Stdin, os.Stdout, os.Stderr}, +// } + +// p, err := exePath() +// if err != nil { +// return err +// } + +// //com.ColorLog("[INFO] now is starting search daemon ...\n") +// _, err = os.StartProcess(p, []string{"gopm", "serve", "-l"}, attr) +// if err != nil { +// return err +// } +// time.Sleep(time.Second) +// } +// return nil +// } + +// func runningStatus() (int, int, int) { +// pFile, err := getPidPath() +// if err != nil { +// return STOP, 0, 0 +// } + +// contentByte, err := ioutil.ReadFile(pFile) +// if err != nil { +// return STOP, 0, 0 +// } +// content := string(contentByte) +// if len(content) < 0 || !strings.Contains(content, ",") { +// return STOP, 0, 0 +// } +// cs := strings.Split(string(content), ",") +// if len(cs) != 3 { +// return STOP, 0, 0 +// } +// status, err := strconv.Atoi(cs[0]) +// if err != nil { +// return STOP, 0, 0 +// } +// if status < STOP || status > RUNNING { +// return STOP, 0, 0 +// } +// pid, err := strconv.Atoi(cs[1]) +// if err != nil { +// return STOP, 0, 0 +// } + +// _, err = os.FindProcess(pid) +// if err != nil { +// return STOP, 0, 0 +// } + +// port, err := strconv.Atoi(cs[2]) +// if err != nil { +// return STOP, 0, 0 +// } + +// return status, pid, port +// } + +// func getPidPath() (string, error) { +// homeDir, err := com.HomeDir() +// if err != nil { +// return "", err +// } + +// pFile := strings.Replace("~/.gopm/var/", "~", homeDir, -1) +// os.MkdirAll(pFile, os.ModePerm) +// return pFile + "pid", nil +// } + +// func startService(listen, port string) error { +// homeDir, err := com.HomeDir() +// if err != nil { +// return err +// } + +// pFile, err := getPidPath() +// if err != nil { +// return err +// } + +// f, err := os.OpenFile(pFile, os.O_RDWR|os.O_CREATE, 0700) +// if err != nil { +// return err +// } +// defer f.Close() +// _, err = f.WriteString(fmt.Sprintf("%v,%v,%v", RUNNING, os.Getpid(), port)) +// if err != nil { +// return err +// } + +// dbDir = strings.Replace(dbDir, "~", homeDir, -1) + +// db, err = leveldb.OpenFile(dbDir, nil) +// if err != nil { +// return err +// } +// defer db.Close() + +// // these handlers should only access by localhost +// http.HandleFunc("/add", addHandler) +// http.HandleFunc("/rm", rmHandler) + +// // these handlers can be accessed according listen's ip +// http.HandleFunc("/search", searchHandler) +// http.HandleFunc("/searche", searcheHandler) +// http.ListenAndServe(listen+":"+port, nil) +// return nil +// } + +// func searchHandler(w http.ResponseWriter, r *http.Request) { +// r.ParseForm() +// ids := make(map[string]bool) +// for key, _ := range r.Form { +// iter := db.NewIterator(ro) +// rkey := fmt.Sprintf("key:%v:", strings.ToLower(key)) +// if iter.Seek([]byte(rkey)) { +// k := iter.Key() +// if !strings.HasPrefix(string(k), rkey) { +// break +// } else { +// ids[string(k)] = true +// } +// } +// for iter.Next() { +// k := iter.Key() +// if !strings.HasPrefix(string(k), rkey) { +// break +// } +// ids[string(k)] = true +// } +// } + +// pkgs := make([]string, 0) + +// for id, _ := range ids { +// idkeys := strings.SplitN(id, ":", -1) +// rId := idkeys[len(idkeys)-1] +// //fmt.Println(rId) +// pkg, err := dbGet(fmt.Sprintf("pkg:%v", rId)) +// if err != nil { +// com.ColorLog(err.Error()) +// continue +// } +// desc, err := dbGet(fmt.Sprintf("desc:%v", rId)) +// if err != nil { +// com.ColorLog(err.Error()) +// continue +// } +// pkgs = append(pkgs, fmt.Sprintf(`{"pkg":"%v", "desc":"%v"}`, pkg, desc)) +// } + +// w.Write([]byte("[" + strings.Join(pkgs, ", ") + "]")) +// } + +// func searcheHandler(w http.ResponseWriter, r *http.Request) { +// //if r.Method == "POST" { +// r.ParseForm() +// pkgs := make([]string, 0) +// for key, _ := range r.Form { +// rId, err := dbGet("index:" + key) +// if err != nil { +// com.ColorLog(err.Error()) +// continue +// } + +// desc, err := dbGet(fmt.Sprintf("desc:%v", rId)) +// if err != nil { +// com.ColorLog(err.Error()) +// continue +// } + +// pkgs = append(pkgs, fmt.Sprintf(`{"pkg":"%v", "desc":"%v"}`, key, desc)) +// } + +// w.Write([]byte("[" + strings.Join(pkgs, ", ") + "]")) +// //} +// } + +// func addHandler(w http.ResponseWriter, r *http.Request) { +// //if r.Method == "POST" { +// r.ParseForm() + +// nod := new(doc.Node) +// nod.ImportPath = r.FormValue("importPath") +// nod.Synopsis = r.FormValue("synopsis") +// nod.DownloadURL = r.FormValue("downloadURL") +// isGetDeps, err := strconv.ParseBool(r.FormValue("isGetDeps")) +// if err != nil { +// com.ColorLog("[ERRO] SEVER: Cannot get deps") +// } +// nod.IsGetDeps = isGetDeps +// nod.Type = r.FormValue("type") +// nod.Value = r.FormValue("value") + +// err = addNode(nod) +// if err != nil { +// com.ColorLog("[ERRO] SEVER: Cannot add node[ %s ]\n", err) +// } +// //} +// } + +// func rmHandler(w http.ResponseWriter, r *http.Request) { + +// } diff --git a/cmd/service.go b/cmd/service.go new file mode 100644 index 000000000..b6d56dba1 --- /dev/null +++ b/cmd/service.go @@ -0,0 +1,213 @@ +// Copyright 2013 gopm authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package cmd + +// func getService(pkgName string) Service { +// for _, service := range services { +// if service.HasPkg(pkgName) { +// return service +// } +// } +// return nil +// } + +// type Service interface { +// PkgUrl(pkg *Pkg) string +// HasPkg(pkgName string) bool +// PkgExt() string +// } + +//type Pkg struct { +// Name string +// Ver string +// VerId string +//} + +// func NewPkg(pkgName string, ver string) *Pkg { +// vers := strings.Split(ver, ":") +// if len(vers) > 2 { +// return nil +// } + +// var verId string +// if len(vers) == 2 { +// verId = vers[1] +// } + +// if len(vers) == 1 { +// vers[0] = TRUNK +// } + +// service := getService(pkgName) +// if service == nil { +// return nil +// } + +// return &Pkg{service, pkgName, vers[0], verId} +// } + +// func (p *Pkg) VerSimpleString() string { +// if p.VerId != "" { +// return p.VerId +// } +// return p.Ver +// } + +// func (p *Pkg) Url() string { +// return p.Service.PkgUrl(p) +// } + +// func (p *Pkg) FileName() string { +// return fmt.Sprintf("%v.%v", p.VerSimpleString(), p.Service.PkgExt()) +// } + +// // github repository +// type GithubService struct { +// } + +// func (s *GithubService) PkgUrl(pkg *Pkg) string { +// var verPath string +// if pkg.Ver == TRUNK { +// verPath = "master" +// } else { +// verPath = pkg.VerId +// } +// return fmt.Sprintf("https://%v/archive/%v.zip", pkg.Name, verPath) +// } + +// func (s *GithubService) HasPkg(pkgName string) bool { +// return strings.HasPrefix(pkgName, "github.com") +// } + +// func (s *GithubService) PkgExt() string { +// return "zip" +// } + +// // git osc repos +// type GitOscService struct { +// } + +// func (s *GitOscService) PkgUrl(pkg *Pkg) string { +// var verPath string +// if pkg.Ver == TRUNK { +// verPath = "master" +// } else { +// verPath = pkg.VerId +// } +// return fmt.Sprintf("https://%v/repository/archive?ref=%v", pkg.Name, verPath) +// } + +// func (s *GitOscService) HasPkg(pkgName string) bool { +// return strings.HasPrefix(pkgName, "git.oschina.net") +// } + +// func (s *GitOscService) PkgExt() string { +// return "zip" +// } + +// // bitbucket.org +// type BitBucketService struct { +// } + +// func (s *BitBucketService) PkgUrl(pkg *Pkg) string { +// var verPath string +// if pkg.Ver == TRUNK { +// verPath = "default" +// } else { +// verPath = pkg.VerId +// } + +// return fmt.Sprintf("https://%v/get/%v.zip", pkg.Name, verPath) +// } + +// func (s *BitBucketService) HasPkg(pkgName string) bool { +// return strings.HasPrefix(pkgName, "bitbucket.org") +// } + +// func (s *BitBucketService) PkgExt() string { +// return "zip" +// } + +// type GitCafeService struct { +// } + +// func (s *GitCafeService) PkgUrl(pkg *Pkg) string { +// var verPath string +// if pkg.Ver == TRUNK { +// verPath = "master" +// } else { +// verPath = pkg.VerId +// } + +// return fmt.Sprintf("https://%v/tarball/%v", pkg.Name, verPath) +// } + +// func (s *GitCafeService) HasPkg(pkgName string) bool { +// return strings.HasPrefix(pkgName, "gitcafe.com") +// } + +// func (s *GitCafeService) PkgExt() string { +// return "tar.gz" +// } + +// // git lab repos, not completed +// type GitLabService struct { +// DomainOrIp string +// Username string +// Passwd string +// PrivateKey string +// } + +// func (s *GitLabService) PkgUrl(pkg *Pkg) string { +// var verPath string +// if pkg.Ver == TRUNK { +// verPath = "master" +// } else { +// verPath = pkg.VerId +// } + +// return fmt.Sprintf("https://%v/repository/archive/%v", pkg.Name, verPath) +// } + +// func (s *GitLabService) HasPkg(pkgName string) bool { +// return strings.HasPrefix(pkgName, s.DomainOrIp) +// } + +// func (s *GitLabService) PkgExt() string { +// return "tar.gz" +// } + +// // code.csdn.net +// type CodeCSDNService struct { +// } + +// func (s *CodeCSDNService) PkgUrl(pkg *Pkg) string { +// var verPath string +// if pkg.Ver == TRUNK { +// verPath = "master" +// } else { +// verPath = pkg.VerId +// } + +// return fmt.Sprintf("https://%v/repository/archive?ref=%v", pkg.Name, verPath) +// } + +// func (s *CodeCSDNService) HasPkg(pkgName string) bool { +// return strings.HasPrefix(pkgName, "code.csdn.net") +// } + +// func (s *CodeCSDNService) PkgExt() string { +// return "zip" +// } diff --git a/cmd/test.go b/cmd/test.go new file mode 100644 index 000000000..74e837aec --- /dev/null +++ b/cmd/test.go @@ -0,0 +1,47 @@ +// Copyright 2013 gopm authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package cmd + +import ( + "github.com/codegangsta/cli" + + "github.com/gpmgo/gopm/log" +) + +var CmdTest = cli.Command{ + Name: "test", + Usage: "link dependencies and go test", + Description: `Command test links dependencies according to gopmfile, +and execute 'go test' + +gopm test `, + Action: runTest, +} + +func runTest(ctx *cli.Context) { + genNewGoPath(ctx, true) + + log.Trace("Testing...") + + cmdArgs := []string{"go", "test"} + cmdArgs = append(cmdArgs, ctx.Args()...) + err := execCmd(newGoPath, newCurPath, cmdArgs...) + if err != nil { + log.Error("Test", "Fail to test program") + log.Fatal("", err.Error()) + } + + log.Success("SUCC", "Test", "Command execute successfully!") +} diff --git a/cmd/update.go b/cmd/update.go new file mode 100644 index 000000000..af7d4191f --- /dev/null +++ b/cmd/update.go @@ -0,0 +1,215 @@ +// Copyright 2013 gopm authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package cmd + +import ( + "encoding/json" + "fmt" + "net/http" + "os" + "os/exec" + "path" + "path/filepath" + "runtime" + + "github.com/Unknwon/com" + "github.com/codegangsta/cli" + + "github.com/gpmgo/gopm/doc" + "github.com/gpmgo/gopm/log" +) + +var CmdUpdate = cli.Command{ + Name: "update", + Usage: "check and update gopm resources including itself", + Description: `Command update checks updates of resources and gopm itself. + +gopm update + +Resources will be updated automatically after executed this command, +but you have to confirm before updaing gopm itself.`, + Action: runUpdate, + Flags: []cli.Flag{ + cli.BoolFlag{"verbose, v", "show process details"}, + }, +} + +func exePath() string { + file, err := exec.LookPath(os.Args[0]) + if err != nil { + log.Error("Update", "Fail to execute exec.LookPath") + log.Fatal("", err.Error()) + } + path, err := filepath.Abs(file) + if err != nil { + log.Error("Update", "Fail to get absolutely path") + log.Fatal("", err.Error()) + } + return path +} + +type version struct { + Gopm int + PackageNameList int `json:"package_name_list"` +} + +func runUpdate(ctx *cli.Context) { + setup(ctx) + + isAnythingUpdated := false + // Load local version info. + localVerInfo := loadLocalVerInfo() + + // Get remote version info. + var remoteVerInfo version + if err := com.HttpGetJSON(http.DefaultClient, "http://gopm.io/VERSION.json", &remoteVerInfo); err != nil { + log.Error("Update", "Fail to fetch VERSION.json") + log.Fatal("", err.Error()) + } + + // Package name list. + if remoteVerInfo.PackageNameList > localVerInfo.PackageNameList { + log.Log("Updating pkgname.list...%v > %v", + localVerInfo.PackageNameList, remoteVerInfo.PackageNameList) + data, err := com.HttpGetBytes(http.DefaultClient, "https://raw2.github.com/gpmgo/docs/master/pkgname.list", nil) + if err != nil { + log.Error("Update", "Fail to update pkgname.list") + log.Fatal("", err.Error()) + } + _, err = com.SaveFile(path.Join(doc.HomeDir, doc.PKG_NAME_LIST_PATH), data) + if err != nil { + log.Error("Update", "Fail to save pkgname.list") + log.Fatal("", err.Error()) + } + log.Log("Update pkgname.list to %v succeed!", remoteVerInfo.PackageNameList) + isAnythingUpdated = true + } + + // Gopm. + if remoteVerInfo.Gopm > localVerInfo.Gopm { + log.Log("Updating gopm...%v > %v", + localVerInfo.Gopm, remoteVerInfo.Gopm) + installRepoPath = doc.HomeDir + "/repos" + + tmpDirPath := filepath.Join(doc.HomeDir, "temp") + tmpBinPath := filepath.Join(tmpDirPath, "gopm") + if runtime.GOOS == "windows" { + tmpBinPath += ".exe" + } + + os.MkdirAll(tmpDirPath, os.ModePerm) + os.Remove(tmpBinPath) + + // Fetch code. + args := []string{"bin", "-u", "-d"} + if ctx.Bool("verbose") { + args = append(args, "-v") + } + args = append(args, []string{"github.com/gpmgo/gopm", tmpDirPath}...) + stdout, stderr, err := com.ExecCmd("gopm", args...) + if err != nil { + log.Error("Update", "Fail to execute 'gopm bin -u -d github.com/gpmgo/gopm "+tmpDirPath+"'") + log.Fatal("", err.Error()) + } + if len(stderr) > 0 { + fmt.Print(stderr) + } + if len(stdout) > 0 { + fmt.Print(stdout) + } + + // Check if previous steps were successful. + if !com.IsExist(tmpBinPath) { + log.Error("Update", "Fail to continue command") + log.Fatal("", "Previous steps weren't successful, no binary produced") + } + + movePath := exePath() + log.Log("New binary will be replaced for %s", movePath) + // Move binary to given directory. + if runtime.GOOS != "windows" { + err := os.Rename(tmpBinPath, movePath) + if err != nil { + log.Error("Update", "Fail to move binary") + log.Fatal("", err.Error()) + } + os.Chmod(movePath+"/"+path.Base(tmpBinPath), os.ModePerm) + } else { + batPath := filepath.Join(tmpDirPath, "update.bat") + f, err := os.Create(batPath) + if err != nil { + log.Error("Update", "Fail to generate bat file") + log.Fatal("", err.Error()) + } + f.WriteString("@echo off\r\n") + f.WriteString(fmt.Sprintf("ping -n 1 127.0.0.1>nul\r\ncopy \"%v\" \"%v\" >nul\r\ndel \"%v\" >nul\r\n\r\n", + tmpBinPath, movePath, tmpBinPath)) + //f.WriteString(fmt.Sprintf("del \"%v\"\r\n", batPath)) + f.Close() + + attr := &os.ProcAttr{ + Dir: workDir, + Env: os.Environ(), + Files: []*os.File{os.Stdin, os.Stdout, os.Stderr}, + } + + _, err = os.StartProcess(batPath, []string{batPath}, attr) + if err != nil { + log.Error("Update", "Fail to start bat process") + log.Fatal("", err.Error()) + } + } + + log.Success("SUCC", "Update", "Command execute successfully!") + isAnythingUpdated = true + } + + // Save JSON. + f, err := os.Create(path.Join(doc.HomeDir, doc.VER_PATH)) + if err != nil { + log.Error("Update", "Fail to create VERSION.json") + log.Fatal("", err.Error()) + } + if err := json.NewEncoder(f).Encode(&remoteVerInfo); err != nil { + log.Error("Update", "Fail to encode VERSION.json") + log.Fatal("", err.Error()) + } + + if !isAnythingUpdated { + log.Log("Nothing need to be updated") + } + log.Log("Exit old gopm") +} + +func loadLocalVerInfo() (ver version) { + verPath := path.Join(doc.HomeDir, doc.VER_PATH) + + // First time run should not exist. + if !com.IsExist(verPath) { + return ver + } + + f, err := os.Open(verPath) + if err != nil { + log.Error("Update", "Fail to open VERSION.json") + log.Fatal("", err.Error()) + } + + if err := json.NewDecoder(f).Decode(&ver); err != nil { + log.Error("Update", "Fail to decode VERSION.json") + log.Fatal("", err.Error()) + } + return ver +} diff --git a/doc/bitbucket.go b/doc/bitbucket.go new file mode 100644 index 000000000..7521b733c --- /dev/null +++ b/doc/bitbucket.go @@ -0,0 +1,202 @@ +// Copyright 2013 gopm authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package doc + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "errors" + "io" + "net/http" + "os" + "path" + "regexp" + "strings" + + "github.com/Unknwon/com" + "github.com/codegangsta/cli" + + //"github.com/gpmgo/gopm/log" +) + +var ( + bitbucketPattern = regexp.MustCompile(`^bitbucket\.org/(?P[a-z0-9A-Z_.\-]+)/(?P[a-z0-9A-Z_.\-]+)(?P/[a-z0-9A-Z_.\-/]*)?$`) + bitbucketEtagRe = regexp.MustCompile(`^(hg|git)-`) +) + +// getBitbucketDoc downloads tarball from bitbucket.org. +func getBitbucketDoc(client *http.Client, match map[string]string, installRepoPath string, nod *Node, ctx *cli.Context) ([]string, error) { + // Check version control. + if m := bitbucketEtagRe.FindStringSubmatch(nod.Value); m != nil { + match["vcs"] = m[1] + } else { + var repo struct { + Scm string + } + if err := com.HttpGetJSON(client, com.Expand("https://api.bitbucket.org/1.0/repositories/{owner}/{repo}", match), &repo); err != nil { + return nil, err + } + match["vcs"] = repo.Scm + } + + if nod.Type == BRANCH { + if len(nod.Value) == 0 { + match["commit"] = defaultTags[match["vcs"]] + } else { + match["commit"] = nod.Value + } + } + + if nod.IsGetDeps { + if nod.Type == COMMIT { + tags := make(map[string]string) + for _, nodeType := range []string{"branches", "tags"} { + var nodes map[string]struct { + Node string + } + if err := com.HttpGetJSON(client, com.Expand("https://api.bitbucket.org/1.0/repositories/{owner}/{repo}/{0}", match, nodeType), &nodes); err != nil { + return nil, err + } + for t, n := range nodes { + tags[t] = n.Node + } + } + + // Check revision tag. + var err error + match["tag"], match["commit"], err = bestTag(tags, defaultTags[match["vcs"]]) + if err != nil { + return nil, err + } + + nod.Value = match["commit"] + } + } else { + // Check downlaod type. + switch nod.Type { + case TAG, COMMIT, BRANCH: + match["commit"] = nod.Value + default: + return nil, errors.New("Unknown node type: " + nod.Type) + } + } + + // We use .tar.gz here. + // zip : https://bitbucket.org/{owner}/{repo}/get/{commit}.zip + // tarball : https://bitbucket.org/{owner}/{repo}/get/{commit}.tar.gz + + // Downlaod archive. + p, err := com.HttpGetBytes(client, com.Expand("https://bitbucket.org/{owner}/{repo}/get/{commit}.tar.gz", match), nil) + if err != nil { + return nil, err + } + + var installPath string + if nod.ImportPath == nod.DownloadURL { + suf := "." + nod.Value + if len(suf) == 1 { + suf = "" + } + projectPath := com.Expand("bitbucket.org/{owner}/{repo}", match) + installPath = installRepoPath + "/" + projectPath + suf + nod.ImportPath = projectPath + } else { + installPath = installRepoPath + "/" + nod.ImportPath + } + + // Remove old files. + os.RemoveAll(installPath + "/") + os.MkdirAll(installPath+"/", os.ModePerm) + + gzr, err := gzip.NewReader(bytes.NewReader(p)) + if err != nil { + return nil, err + } + defer gzr.Close() + + tr := tar.NewReader(gzr) + + var autoPath string // Auto path is the root path that generated by bitbucket.org. + // Get source file data. + dirs := make([]string, 0, 5) + for { + h, err := tr.Next() + if err == io.EOF { + break + } else if err != nil { + return nil, err + } + + fn := h.Name + + // In case that we find directory, usually we should not. + if strings.HasSuffix(fn, "/") { + continue + } + + // Check root path. + if len(autoPath) == 0 { + autoPath = fn[:strings.Index(fn, "/")] + } + absPath := strings.Replace(fn, autoPath, installPath, 1) + + // Create diretory before create file. + dir := path.Dir(absPath) + if !checkDir(dir, dirs) && !(!ctx.Bool("example") && strings.Contains(absPath, "example")) { + dirs = append(dirs, dir) + os.MkdirAll(dir+"/", os.ModePerm) + } + + // Get data from archive. + fbytes := make([]byte, h.Size) + if _, err := io.ReadFull(tr, fbytes); err != nil { + return nil, err + } + + _, err = com.SaveFile(absPath, fbytes) + if err != nil { + return nil, err + } + + // Set modify time. + os.Chtimes(absPath, h.AccessTime, h.ModTime) + } + + var imports []string + + // Check if need to check imports. + if nod.IsGetDeps { + for _, d := range dirs { + importPkgs, err := CheckImports(d+"/", match["importPath"], nod) + if err != nil { + return nil, err + } + imports = append(imports, importPkgs...) + } + } + + return imports, err +} + +// checkDir checks if current directory has been saved. +func checkDir(dir string, dirs []string) bool { + for _, d := range dirs { + if dir == d { + return true + } + } + return false +} diff --git a/doc/conf.go b/doc/conf.go new file mode 100644 index 000000000..590afafd6 --- /dev/null +++ b/doc/conf.go @@ -0,0 +1,147 @@ +// Copyright 2013 gopm authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package doc + +import ( + "os" + "path" + "path/filepath" + "strings" + + "github.com/Unknwon/com" + "github.com/Unknwon/goconfig" + + "github.com/gpmgo/gopm/log" +) + +const ( + GOPM_FILE_NAME = ".gopmfile" + GOPM_CONFIG_FILE = "data/gopm.ini" + PKG_NAME_LIST_PATH = "data/pkgname.list" + VER_PATH = "data/VERSION.json" + RawHomeDir = "~/.gopm" +) + +var ( + HomeDir = "~/.gopm" + LocalNodesFile = "/data/localnodes.list" + LocalNodes *goconfig.ConfigFile + Cfg *goconfig.ConfigFile +) + +func init() { + hd, err := com.HomeDir() + if err != nil { + log.Error("", "Fail to get current user") + log.Fatal("", err.Error()) + } + + HomeDir = strings.Replace(RawHomeDir, "~", hd, -1) + + cfgPath := path.Join(HomeDir, GOPM_CONFIG_FILE) + if !com.IsExist(cfgPath) { + os.MkdirAll(path.Dir(cfgPath), os.ModePerm) + if _, err = os.Create(cfgPath); err != nil { + log.Error("", "Fail to create gopm config file") + log.Fatal("", err.Error()) + } + } + Cfg, err = goconfig.LoadConfigFile(cfgPath) + if err != nil { + log.Error("", "Fail to load gopm config file") + log.Fatal("", err.Error()) + } + + LoadLocalNodes() + LoadPkgNameList(path.Join(HomeDir, PKG_NAME_LIST_PATH)) +} + +// NewGopmfile loads gopmgile in given directory. +func NewGopmfile(dirPath string) *goconfig.ConfigFile { + dirPath, _ = filepath.Abs(dirPath) + gf, err := goconfig.LoadConfigFile(path.Join(dirPath, GOPM_FILE_NAME)) + if err != nil { + log.Error("", "Fail to load gopmfile:") + log.Fatal("", "\t"+err.Error()) + } + return gf +} + +var PackageNameList map[string]string + +func LoadPkgNameList(filePath string) { + PackageNameList = make(map[string]string) + + // If file does not exist, simply ignore. + if !com.IsFile(filePath) { + return + } + + data, err := com.ReadFile(filePath) + if err != nil { + log.Error("Package name list", "Fail to read file") + log.Fatal("", err.Error()) + } + + pkgs := strings.Split(string(data), "\n") + for i, line := range pkgs { + infos := strings.Split(line, "=") + if len(infos) != 2 { + // Last item might be empty line. + if i == len(pkgs)-1 { + continue + } + log.Error("", "Fail to parse package name: "+line) + log.Fatal("", "Invalid package name information") + } + PackageNameList[strings.TrimSpace(infos[0])] = + strings.TrimSpace(infos[1]) + } +} + +func GetPkgFullPath(short string) string { + name, ok := PackageNameList[short] + if !ok { + log.Error("", "Invalid package name") + log.Error("", "It's not a invalid import path and no match in the package name list:") + log.Fatal("", "\t"+short) + } + return name +} + +func LoadLocalNodes() { + if !com.IsDir(HomeDir + "/data") { + os.MkdirAll(HomeDir+"/data", os.ModePerm) + } + + if !com.IsFile(HomeDir + LocalNodesFile) { + os.Create(HomeDir + LocalNodesFile) + } + + var err error + LocalNodes, err = goconfig.LoadConfigFile(path.Join(HomeDir + LocalNodesFile)) + if err != nil { + log.Error("load node", "Fail to load localnodes.list") + log.Fatal("", err.Error()) + } +} + +func SaveLocalNodes() { + if err := goconfig.SaveConfigFile(LocalNodes, + path.Join(HomeDir+LocalNodesFile)); err != nil { + log.Error("", "Fail to save localnodes.list:") + log.Error("", "\t"+err.Error()) + } +} diff --git a/doc/error.go b/doc/error.go new file mode 100644 index 000000000..56890c961 --- /dev/null +++ b/doc/error.go @@ -0,0 +1,25 @@ +// Copyright 2013 gopm authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package doc + +import ( + "errors" +) + +var ( + errNotModified = errors.New("Package not modified") + errNoMatch = errors.New("no match") + errUpdateTimeout = errors.New("update timeout") +) diff --git a/doc/github.go b/doc/github.go new file mode 100644 index 000000000..6d4099977 --- /dev/null +++ b/doc/github.go @@ -0,0 +1,191 @@ +// Copyright 2013 gopm authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package doc + +import ( + "archive/zip" + "bytes" + "errors" + "io" + "net/http" + "os" + "path" + "regexp" + "strings" + + "github.com/Unknwon/com" + "github.com/codegangsta/cli" + + "github.com/gpmgo/gopm/log" +) + +var ( + githubPattern = regexp.MustCompile(`^github\.com/(?P[a-z0-9A-Z_.\-]+)/(?P[a-z0-9A-Z_.\-]+)(?P/[a-z0-9A-Z_.\-/]*)?$`) +) + +func GetGithubCredentials() string { + return "client_id=" + Cfg.MustValue("github", "client_id") + + "&client_secret=" + Cfg.MustValue("github", "client_secret") +} + +// getGithubDoc downloads tarball from github.com. +func getGithubDoc(client *http.Client, match map[string]string, installRepoPath string, nod *Node, ctx *cli.Context) ([]string, error) { + match["cred"] = GetGithubCredentials() + + // Check downlaod type. + switch nod.Type { + case BRANCH: + if len(nod.Value) == 0 { + match["sha"] = MASTER + + // Only get and check revision with the latest version. + var refs []*struct { + Ref string + Url string + Object struct { + Sha string + Type string + Url string + } + } + + err := com.HttpGetJSON(client, com.Expand("https://api.github.com/repos/{owner}/{repo}/git/refs?{cred}", match), &refs) + if err != nil { + if strings.Contains(err.Error(), "403") { + break + } + log.Warn("GET", "Fail to get revision") + log.Warn("", err.Error()) + break + } + + var etag string + COMMIT_LOOP: + for _, ref := range refs { + switch { + case strings.HasPrefix(ref.Ref, "refs/heads/master"): + etag = ref.Object.Sha + break COMMIT_LOOP + } + } + if etag == nod.Revision { + log.Log("GET Package hasn't changed: %s", nod.ImportPath) + return nil, nil + } + nod.Revision = etag + + } else { + match["sha"] = nod.Value + } + case TAG, COMMIT: + match["sha"] = nod.Value + default: + return nil, errors.New("Unknown node type: " + nod.Type) + } + + // We use .zip here. + // zip: https://github.com/{owner}/{repo}/archive/{sha}.zip + // tarball: https://github.com/{owner}/{repo}/tarball/{sha} + + // Downlaod archive. + p, err := com.HttpGetBytes(client, com.Expand("https://github.com/{owner}/{repo}/archive/{sha}.zip", match), nil) + if err != nil { + return nil, errors.New("Fail to donwload Github repo -> " + err.Error()) + } + + shaName := com.Expand("{repo}-{sha}", match) + if nod.Type == "tag" { + shaName = strings.Replace(shaName, "-v", "-", 1) + } + + var installPath string + if nod.ImportPath == nod.DownloadURL { + suf := "." + nod.Value + if len(suf) == 1 { + suf = "" + } + projectPath := com.Expand("github.com/{owner}/{repo}", match) + installPath = installRepoPath + "/" + projectPath + suf + nod.ImportPath = projectPath + } else { + installPath = installRepoPath + "/" + nod.ImportPath + } + + // Remove old files. + os.RemoveAll(installPath + "/") + os.MkdirAll(installPath+"/", os.ModePerm) + + r, err := zip.NewReader(bytes.NewReader(p), int64(len(p))) + if err != nil { + return nil, errors.New(nod.ImportPath + " -> new zip: " + err.Error()) + } + + dirs := make([]string, 0, 5) + // Need to add root path because we cannot get from tarball. + dirs = append(dirs, installPath+"/") + for _, f := range r.File { + absPath := strings.Replace(f.Name, shaName, installPath, 1) + // Create diretory before create file. + os.MkdirAll(path.Dir(absPath)+"/", os.ModePerm) + + compareDir: + switch { + case strings.HasSuffix(absPath, "/"): // Directory. + // Check if current directory is example. + if !(!ctx.Bool("example") && strings.Contains(absPath, "example")) { + for _, d := range dirs { + if d == absPath { + break compareDir + } + } + dirs = append(dirs, absPath) + } + default: + // Get file from archive. + r, err := f.Open() + if err != nil { + return nil, err + } + + fbytes := make([]byte, f.FileInfo().Size()) + _, err = io.ReadFull(r, fbytes) + if err != nil { + return nil, err + } + + _, err = com.SaveFile(absPath, fbytes) + if err != nil { + return nil, err + } + + // Set modify time. + os.Chtimes(absPath, f.ModTime(), f.ModTime()) + } + } + + var imports []string + + // Check if need to check imports. + if nod.IsGetDeps { + for _, d := range dirs { + importPkgs, err := CheckImports(d, match["importPath"], nod) + if err != nil { + return nil, err + } + imports = append(imports, importPkgs...) + } + } + return imports, err +} diff --git a/doc/google.go b/doc/google.go new file mode 100644 index 000000000..404c33a41 --- /dev/null +++ b/doc/google.go @@ -0,0 +1,286 @@ +// Copyright 2013 gopm authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package doc + +import ( + "archive/zip" + "bytes" + "errors" + "io" + "net/http" + "os" + "path" + "regexp" + "strings" + + "github.com/Unknwon/com" + "github.com/codegangsta/cli" + + "github.com/gpmgo/gopm/log" +) + +var ( + googleRepoRe = regexp.MustCompile(`id="checkoutcmd">(hg|git|svn)`) + googleRevisionRe = regexp.MustCompile(`

(?:[^ ]+ - )?Revision *([^:]+):`) + googleFileRe = regexp.MustCompile(`
  • [a-z0-9\-]+)(:?\.(?P[a-z0-9\-]+))?(?P/[a-z0-9A-Z_.\-/]+)?$`) +) + +// getGoogleDoc downloads raw files from code.google.com. +func getGoogleDoc(client *http.Client, match map[string]string, installRepoPath string, nod *Node, ctx *cli.Context) ([]string, error) { + setupGoogleMatch(match) + // Check version control. + if err := getGoogleVCS(client, match); err != nil { + return nil, errors.New("fail to get vcs " + nod.ImportPath + " : " + err.Error()) + } + + switch nod.Type { + case BRANCH: + if len(nod.Value) == 0 { + match["tag"] = defaultTags[match["vcs"]] + + // Only get and check revision with the latest version. + p, err := com.HttpGetBytes(client, + com.Expand("http://{subrepo}{dot}{repo}.googlecode.com/{vcs}{dir}/?r={tag}", match), nil) + if err != nil { + log.Error("GET", "Fail to get revision") + log.Error("", err.Error()) + break + } + + if m := googleRevisionRe.FindSubmatch(p); m == nil { + log.Error("GET", "Fail to get revision") + log.Error("", err.Error()) + } else { + etag := string(m[1]) + if etag == nod.Revision { + log.Log("GET Package hasn't changed: %s", nod.ImportPath) + return nil, nil + } + nod.Revision = etag + } + + } else { + match["tag"] = nod.Value + } + case TAG, COMMIT: + match["tag"] = nod.Value + default: + return nil, errors.New("Unknown node type: " + nod.Type) + } + + var installPath string + projectPath := GetProjectPath(nod.ImportPath) + if nod.ImportPath == nod.DownloadURL { + suf := "." + nod.Value + if len(suf) == 1 { + suf = "" + } + installPath = installRepoPath + "/" + projectPath + suf + } else { + installPath = installRepoPath + "/" + projectPath + } + + // Remove old files. + os.RemoveAll(installPath + "/") + os.MkdirAll(installPath+"/", os.ModePerm) + + if match["vcs"] == "svn" { + com.ColorLog("[WARN] SVN detected, may take very long time.\n") + + rootPath := com.Expand("http://{subrepo}{dot}{repo}.googlecode.com/{vcs}", match) + d, f := path.Split(rootPath) + err := downloadFiles(client, match, d, installPath+"/", match["tag"], + []string{f + "/"}) + if err != nil { + return nil, errors.New("Fail to download " + nod.ImportPath + " : " + err.Error()) + } + } + + p, err := com.HttpGetBytes(client, com.Expand("http://{subrepo}{dot}{repo}.googlecode.com/archive/{tag}.zip", match), nil) + if err != nil { + return nil, errors.New("Fail to download " + nod.ImportPath + " : " + err.Error()) + } + + r, err := zip.NewReader(bytes.NewReader(p), int64(len(p))) + if err != nil { + return nil, errors.New(nod.ImportPath + " -> new zip: " + err.Error()) + } + + nameLen := strings.Index(r.File[0].Name, "/") + dirPrefix := match["dir"] + if len(dirPrefix) != 0 { + dirPrefix = dirPrefix[1:] + "/" + } + + dirs := make([]string, 0, 5) + for _, f := range r.File { + absPath := strings.Replace(f.Name, f.Name[:nameLen], installPath, 1) + + // Create diretory before create file. + dir := path.Dir(absPath) + if !checkDir(dir, dirs) && !(!ctx.Bool("example") && strings.Contains(absPath, "example")) { + dirs = append(dirs, dir+"/") + os.MkdirAll(dir+"/", os.ModePerm) + } + + // Get file from archive. + r, err := f.Open() + if err != nil { + return nil, err + } + + fbytes := make([]byte, f.FileInfo().Size()) + _, err = io.ReadFull(r, fbytes) + if err != nil { + return nil, err + } + + _, err = com.SaveFile(absPath, fbytes) + if err != nil { + return nil, err + } + } + + var imports []string + + // Check if need to check imports. + if nod.IsGetDeps { + for _, d := range dirs { + importPkgs, err := CheckImports(d, match["importPath"], nod) + if err != nil { + return nil, err + } + imports = append(imports, importPkgs...) + } + } + + return imports, err +} + +type rawFile struct { + name string + rawURL string + data []byte +} + +func (rf *rawFile) Name() string { + return rf.name +} + +func (rf *rawFile) RawUrl() string { + return rf.rawURL +} + +func (rf *rawFile) Data() []byte { + return rf.data +} + +func (rf *rawFile) SetData(p []byte) { + rf.data = p +} + +func downloadFiles(client *http.Client, match map[string]string, rootPath, installPath, commit string, dirs []string) error { + suf := "?r=" + commit + if len(commit) == 0 { + suf = "" + } + + for _, d := range dirs { + p, err := com.HttpGetBytes(client, rootPath+d+suf, nil) + if err != nil { + return err + } + + // Create destination directory. + os.MkdirAll(installPath+d, os.ModePerm) + + // Get source files in current path. + files := make([]com.RawFile, 0, 5) + for _, m := range googleFileRe.FindAllSubmatch(p, -1) { + fname := strings.Split(string(m[1]), "?")[0] + files = append(files, &rawFile{ + name: fname, + rawURL: rootPath + d + fname + suf, + }) + } + + // Fetch files from VCS. + if err := com.FetchFilesCurl(files); err != nil { + return err + } + + // Save files. + for _, f := range files { + absPath := installPath + d + + // Create diretory before create file. + os.MkdirAll(path.Dir(absPath), os.ModePerm) + + // Write data to file + fw, err := os.Create(absPath + f.Name()) + if err != nil { + return err + } + + _, err = fw.Write(f.Data()) + fw.Close() + if err != nil { + return err + } + } + files = nil + + subdirs := make([]string, 0, 3) + // Get subdirectories. + for _, m := range googleDirRe.FindAllSubmatch(p, -1) { + dirName := strings.Split(string(m[1]), "?")[0] + if strings.HasSuffix(dirName, "/") { + subdirs = append(subdirs, d+dirName) + } + } + + err = downloadFiles(client, match, rootPath, installPath, commit, subdirs) + if err != nil { + return err + } + } + return nil +} + +func setupGoogleMatch(match map[string]string) { + if s := match["subrepo"]; s != "" { + match["dot"] = "." + match["query"] = "?repo=" + s + } else { + match["dot"] = "" + match["query"] = "" + } +} + +func getGoogleVCS(client *http.Client, match map[string]string) error { + // Scrape the HTML project page to find the VCS. + p, err := com.HttpGetBytes(client, com.Expand("http://code.google.com/p/{repo}/source/checkout", match), nil) + if err != nil { + return errors.New("doc.getGoogleVCS(" + match["importPath"] + ") -> " + err.Error()) + } + m := googleRepoRe.FindSubmatch(p) + if m == nil { + return com.NotFoundError{"Could not VCS on Google Code project page."} + } + match["vcs"] = string(m[1]) + return nil +} diff --git a/doc/http.go b/doc/http.go new file mode 100644 index 000000000..e5f34e05b --- /dev/null +++ b/doc/http.go @@ -0,0 +1,52 @@ +// Copyright 2013 gopm authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package doc + +import ( + "flag" + "net" + "net/http" + "time" + + "github.com/Unknwon/com" +) + +var ( + dialTimeout = flag.Duration("dial_timeout", 10*time.Second, "Timeout for dialing an HTTP connection.") + requestTimeout = flag.Duration("request_timeout", 20*time.Second, "Time out for roundtripping an HTTP request.") +) + +func timeoutDial(network, addr string) (net.Conn, error) { + return net.DialTimeout(network, addr, *dialTimeout) +} + +type transport struct { + t http.Transport +} + +func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) { + timer := time.AfterFunc(*requestTimeout, func() { + t.t.CancelRequest(req) + com.ColorLog("[WARN] Canceled request for %s, please interrupt the program.\n", req.URL) + }) + defer timer.Stop() + resp, err := t.t.RoundTrip(req) + return resp, err +} + +var ( + httpTransport = &transport{t: http.Transport{Dial: timeoutDial, ResponseHeaderTimeout: *requestTimeout / 2}} + HttpClient = &http.Client{Transport: httpTransport} +) diff --git a/doc/launchpad.go b/doc/launchpad.go new file mode 100644 index 000000000..9cb535fd7 --- /dev/null +++ b/doc/launchpad.go @@ -0,0 +1,134 @@ +// Copyright 2013 gopm authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package doc + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "io" + "net/http" + "os" + "regexp" + "strings" + + "github.com/Unknwon/com" + "github.com/codegangsta/cli" +) + +var launchpadPattern = regexp.MustCompile(`^launchpad\.net/(?P(?P[a-z0-9A-Z_.\-]+)(?P/[a-z0-9A-Z_.\-]+)?|~[a-z0-9A-Z_.\-]+/(\+junk|[a-z0-9A-Z_.\-]+)/[a-z0-9A-Z_.\-]+)(?P/[a-z0-9A-Z_.\-/]+)*$`) + +// getLaunchpadDoc downloads tarball from launchpad.net. +func getLaunchpadDoc(client *http.Client, match map[string]string, installRepoPath string, nod *Node, ctx *cli.Context) ([]string, error) { + + if match["project"] != "" && match["series"] != "" { + rc, err := com.HttpGet(client, com.Expand("https://code.launchpad.net/{project}{series}/.bzr/branch-format", match), nil) + _, isNotFound := err.(com.NotFoundError) + switch { + case err == nil: + rc.Close() + // The structure of the import path is launchpad.net/{root}/{dir}. + case isNotFound: + // The structure of the import path is is launchpad.net/{project}/{dir}. + match["repo"] = match["project"] + match["dir"] = com.Expand("{series}{dir}", match) + default: + return nil, err + } + } + + var downloadPath string + // Check if download with specific revision. + if len(nod.Value) == 0 { + downloadPath = com.Expand("https://bazaar.launchpad.net/+branch/{repo}/tarball", match) + } else { + downloadPath = com.Expand("https://bazaar.launchpad.net/+branch/{repo}/tarball/"+nod.Value, match) + } + + // Scrape the repo browser to find the project revision and individual Go files. + p, err := com.HttpGetBytes(client, downloadPath, nil) + if err != nil { + return nil, err + } + + installPath := installRepoPath + "/" + nod.ImportPath + + // Remove old files. + os.RemoveAll(installPath + "/") + os.MkdirAll(installPath+"/", os.ModePerm) + + gzr, err := gzip.NewReader(bytes.NewReader(p)) + if err != nil { + return nil, err + } + defer gzr.Close() + + tr := tar.NewReader(gzr) + + var autoPath string // Auto path is the root path that generated by bitbucket.org. + // Get source file data. + dirs := make([]string, 0, 5) + for { + h, err := tr.Next() + if err == io.EOF { + break + } else if err != nil { + return nil, err + } + + fn := h.Name + // Check root path. + if len(autoPath) == 0 { + autoPath = fn[:strings.Index(fn, match["repo"])+len(match["repo"])] + } + absPath := strings.Replace(fn, autoPath, installPath, 1) + + switch { + case h.FileInfo().IsDir(): // Directory. + // Create diretory before create file. + os.MkdirAll(absPath+"/", os.ModePerm) + // Check if current directory is example. + if !(!ctx.Bool("example") && strings.Contains(absPath, "example")) { + dirs = append(dirs, absPath) + } + case !strings.HasPrefix(fn, "."): + // Get data from archive. + fbytes := make([]byte, h.Size) + if _, err := io.ReadFull(tr, fbytes); err != nil { + return nil, err + } + + _, err = com.SaveFile(absPath, fbytes) + if err != nil { + return nil, err + } + } + } + + var imports []string + + // Check if need to check imports. + if nod.IsGetDeps { + for _, d := range dirs { + importPkgs, err := CheckImports(d+"/", match["importPath"], nod) + if err != nil { + return nil, err + } + imports = append(imports, importPkgs...) + } + } + + return imports, err +} diff --git a/doc/oschina.go b/doc/oschina.go new file mode 100644 index 000000000..b35151030 --- /dev/null +++ b/doc/oschina.go @@ -0,0 +1,128 @@ +// Copyright 2013 gopm authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package doc + +import ( + "archive/zip" + "bytes" + "errors" + "io" + "net/http" + "os" + "regexp" + "strings" + + "github.com/Unknwon/com" + "github.com/codegangsta/cli" +) + +var ( + oscTagRe = regexp.MustCompile(`/repository/archive\?ref=(.*)">`) + oscPattern = regexp.MustCompile(`^git\.oschina\.net/(?P[a-z0-9A-Z_.\-]+)/(?P[a-z0-9A-Z_.\-]+)(?P/[a-z0-9A-Z_.\-/]*)?$`) +) + +// getGithubDoc downloads tarball from git.oschina.com. +func getOSCDoc(client *http.Client, match map[string]string, installRepoPath string, nod *Node, ctx *cli.Context) ([]string, error) { + // Check downlaod type. + switch nod.Type { + case BRANCH: + if len(nod.Value) == 0 { + match["sha"] = MASTER + } else { + match["sha"] = nod.Value + } + case TAG, COMMIT: + match["sha"] = nod.Value + default: + return nil, errors.New("Unknown node type: " + nod.Type) + } + + // zip: http://{projectRoot}/repository/archive?ref={sha} + + // Downlaod archive. + p, err := com.HttpGetBytes(client, com.Expand("http://git.oschina.net/{owner}/{repo}/repository/archive?ref={sha}", match), nil) + if err != nil { + return nil, errors.New("Fail to donwload OSChina repo -> " + err.Error()) + } + + var installPath string + if nod.ImportPath == nod.DownloadURL { + suf := "." + nod.Value + if len(suf) == 1 { + suf = "" + } + projectPath := com.Expand("git.oschina.net/{owner}/{repo}", match) + installPath = installRepoPath + "/" + projectPath + suf + nod.ImportPath = projectPath + } else { + installPath = installRepoPath + "/" + nod.ImportPath + } + + // Remove old files. + os.RemoveAll(installPath + "/") + os.MkdirAll(installPath+"/", os.ModePerm) + + r, err := zip.NewReader(bytes.NewReader(p), int64(len(p))) + if err != nil { + return nil, errors.New("Fail to unzip OSChina repo -> " + err.Error()) + } + + nameLen := len(match["repo"]) + dirs := make([]string, 0, 5) + // Need to add root path because we cannot get from tarball. + dirs = append(dirs, installPath+"/") + for _, f := range r.File { + fileName := f.Name[nameLen+1:] + absPath := installPath + "/" + fileName + + if strings.HasSuffix(absPath, "/") { + dirs = append(dirs, absPath) + os.MkdirAll(absPath, os.ModePerm) + continue + } + + // Get file from archive. + r, err := f.Open() + if err != nil { + return nil, errors.New("Fail to open OSChina repo -> " + err.Error()) + } + + fbytes := make([]byte, f.FileInfo().Size()) + _, err = io.ReadFull(r, fbytes) + if err != nil { + return nil, err + } + + _, err = com.SaveFile(absPath, fbytes) + if err != nil { + return nil, err + } + } + + var imports []string + + // Check if need to check imports. + if nod.IsGetDeps { + for _, d := range dirs { + importPkgs, err := CheckImports(d, match["importPath"], nod) + if err != nil { + return nil, err + } + imports = append(imports, importPkgs...) + } + } + + return imports, err +} diff --git a/doc/struct.go b/doc/struct.go new file mode 100644 index 000000000..02e91b5b4 --- /dev/null +++ b/doc/struct.go @@ -0,0 +1,125 @@ +// Copyright 2013 gopm authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package doc + +import ( + "fmt" + "go/token" + "net/http" + "os" + "regexp" + "time" + + "github.com/codegangsta/cli" +) + +const ( + TRUNK = "trunk" + MASTER = "master" + DEFAULT = "default" + TAG = "tag" + BRANCH = "branch" + COMMIT = "commit" + LOCAL = "local" +) + +type Pkg struct { + ImportPath string + RootPath string + Type string + Value string // Branch, tag, commit or local. +} + +// If the package is fixed and no need to updated. +// For commit, tag and local, it's fixed. For branch +func (pkg *Pkg) IsFixed() bool { + if pkg.Type == BRANCH || len(pkg.Value) == 0 { + return false + } + return true +} + +func (pkg *Pkg) VerString() string { + if pkg.Value == "" { + return pkg.Type + } + return fmt.Sprintf("%v:%v", pkg.Type, pkg.Value) +} + +func NewPkg(importPath, tp, value string) *Pkg { + return &Pkg{importPath, "", tp, value} +} + +func NewDefaultPkg(importPath string) *Pkg { + return NewPkg(importPath, BRANCH, "") +} + +type Node struct { + Pkg + DownloadURL string + Synopsis string + IsGetDeps bool + IsGetDepsOnly bool + Revision string +} + +func NewNode(importPath, downloadUrl, tp, value string, isGetDeps bool) *Node { + return &Node{ + Pkg: Pkg{ + ImportPath: importPath, + Type: tp, + Value: value, + }, + DownloadURL: downloadUrl, + IsGetDeps: isGetDeps, + } +} + +// source is source code file. +type source struct { + rawURL string + name string + data []byte +} + +func (s *source) Name() string { return s.name } +func (s *source) Size() int64 { return int64(len(s.data)) } +func (s *source) Mode() os.FileMode { return 0 } +func (s *source) ModTime() time.Time { return time.Time{} } +func (s *source) IsDir() bool { return false } +func (s *source) Sys() interface{} { return nil } + +// walker holds the state used when building the documentation. +type walker struct { + ImportPath string + srcs map[string]*source // Source files. + fset *token.FileSet +} + +// service represents a source code control service. +type service struct { + pattern *regexp.Regexp + prefix string + get func(*http.Client, map[string]string, string, *Node, *cli.Context) ([]string, error) +} + +// services is the list of source code control services handled by gopkgdoc. +var services = []*service{ + {githubPattern, "github.com/", getGithubDoc}, + {googlePattern, "code.google.com/", getGoogleDoc}, + {bitbucketPattern, "bitbucket.org/", getBitbucketDoc}, + {launchpadPattern, "launchpad.net/", getLaunchpadDoc}, + {oscPattern, "git.oschina.net/", getOSCDoc}, +} diff --git a/doc/utils.go b/doc/utils.go new file mode 100644 index 000000000..7ff1fc8a1 --- /dev/null +++ b/doc/utils.go @@ -0,0 +1,759 @@ +// Copyright 2013-2014 gopm authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package doc + +import ( + "bytes" + "go/build" + "io" + "io/ioutil" + "os" + "path" + "path/filepath" + "regexp" + "strings" + "time" + + "github.com/Unknwon/com" + + "github.com/gpmgo/gopm/log" +) + +const VENDOR = ".vendor" + +// GetDirsInfo returns os.FileInfo of all sub-directories in root path. +func GetDirsInfo(rootPath string) ([]os.FileInfo, error) { + if !com.IsDir(rootPath) { + log.Warn("Directory %s does not exist", rootPath) + return []os.FileInfo{}, nil + } + + rootDir, err := os.Open(rootPath) + if err != nil { + return nil, err + } + defer rootDir.Close() + + dirs, err := rootDir.Readdir(0) + if err != nil { + return nil, err + } + + return dirs, nil +} + +// A Source describles a Source code file. +type Source struct { + SrcName string + SrcData []byte +} + +func (s *Source) Name() string { return s.SrcName } +func (s *Source) Size() int64 { return int64(len(s.SrcData)) } +func (s *Source) Mode() os.FileMode { return 0 } +func (s *Source) ModTime() time.Time { return time.Time{} } +func (s *Source) IsDir() bool { return false } +func (s *Source) Sys() interface{} { return nil } +func (s *Source) Data() []byte { return s.SrcData } + +type Context struct { + build.Context + importPath string + srcFiles map[string]*Source +} + +func (ctx *Context) readDir(dir string) ([]os.FileInfo, error) { + fis := make([]os.FileInfo, 0, len(ctx.srcFiles)) + for _, src := range ctx.srcFiles { + fis = append(fis, src) + } + return fis, nil +} + +func (ctx *Context) openFile(path string) (r io.ReadCloser, err error) { + if src, ok := ctx.srcFiles[filepath.Base(path)]; ok { + return ioutil.NopCloser(bytes.NewReader(src.Data())), nil + } + return nil, os.ErrNotExist +} + +// GetImports returns package denpendencies. +func GetImports(absPath, importPath string, example, test bool) []string { + fis, err := GetDirsInfo(absPath) + if err != nil { + log.Error("", "Fail to get directory's information") + log.Fatal("", err.Error()) + } + absPath += "/" + + ctx := new(Context) + ctx.importPath = importPath + ctx.srcFiles = make(map[string]*Source) + ctx.Context = build.Default + ctx.JoinPath = path.Join + ctx.IsAbsPath = path.IsAbs + ctx.ReadDir = ctx.readDir + ctx.OpenFile = ctx.openFile + + // TODO: Load too much, need to make sure which is imported which are not. + dirs := make([]string, 0, 10) + for _, fi := range fis { + if strings.Contains(fi.Name(), VENDOR) { + continue + } + + if fi.IsDir() { + dirs = append(dirs, absPath+fi.Name()) + continue + } else if !test && strings.HasSuffix(fi.Name(), "_test.go") { + continue + } else if !strings.HasSuffix(fi.Name(), ".go") || strings.HasPrefix(fi.Name(), ".") || + strings.HasPrefix(fi.Name(), "_") { + continue + } + src := &Source{SrcName: fi.Name()} + src.SrcData, err = ioutil.ReadFile(absPath + fi.Name()) + if err != nil { + log.Error("", "Fail to read file") + log.Fatal("", err.Error()) + } + ctx.srcFiles[fi.Name()] = src + } + + pkg, err := ctx.ImportDir(absPath, build.AllowBinary) + if err != nil { + if _, ok := err.(*build.NoGoError); !ok { + log.Error("", "Fail to get imports") + log.Fatal("", err.Error()) + } + } + + imports := make([]string, 0, len(pkg.Imports)) + for _, p := range pkg.Imports { + if !IsGoRepoPath(p) && !strings.HasPrefix(p, importPath) { + imports = append(imports, p) + } + } + + if len(dirs) > 0 { + imports = append(imports, GetAllImports(dirs, importPath, example, test)...) + } + return imports +} + +// isVcsPath returns true if the directory was created by VCS. +func isVcsPath(dirPath string) bool { + return strings.Contains(dirPath, "/.git") || + strings.Contains(dirPath, "/.hg") || + strings.Contains(dirPath, "/.svn") +} + +// GetAllImports returns all imports in given directory and all sub-directories. +func GetAllImports(dirs []string, importPath string, example, test bool) (imports []string) { + for _, d := range dirs { + if !isVcsPath(d) && + !(!example && strings.Contains(d, "example")) { + imports = append(imports, GetImports(d, importPath, example, test)...) + } + } + return imports +} + +// GetGOPATH returns best matched GOPATH. +func GetBestMatchGOPATH(appPath string) string { + paths := com.GetGOPATHs() + for _, p := range paths { + if strings.HasPrefix(p, appPath) { + return strings.Replace(p, "\\", "/", -1) + } + } + return paths[0] +} + +// CheckIsExistWithVCS returns false if directory only has VCS folder, +// or doesn't exist. +func CheckIsExistWithVCS(path string) bool { + // Check if directory exist. + if !com.IsExist(path) { + return false + } + + // Check if only has VCS folder. + dirs, err := GetDirsInfo(path) + if err != nil { + log.Error("", "Fail to get directory's information") + log.Fatal("", err.Error()) + } + + if len(dirs) > 1 { + return true + } else if len(dirs) == 0 { + return false + } + + switch dirs[0].Name() { + case ".git", ".hg", ".svn": + return false + } + + return true +} + +// CheckIsExistInGOPATH checks if given package import path exists in any path in GOPATH/src, +// and returns corresponding GOPATH. +func CheckIsExistInGOPATH(importPath string) (string, bool) { + paths := com.GetGOPATHs() + for _, p := range paths { + if CheckIsExistWithVCS(p + "/src/" + importPath + "/") { + return p, true + } + } + return "", false +} + +// GetProjectPath returns project path of import path. +func GetProjectPath(importPath string) (projectPath string) { + projectPath = importPath + + // Check project hosting. + switch { + case strings.HasPrefix(importPath, "github.com") || + strings.HasPrefix(importPath, "git.oschina.net"): + projectPath = joinPath(importPath, 3) + case strings.HasPrefix(importPath, "code.google.com"): + projectPath = joinPath(importPath, 3) + case strings.HasPrefix(importPath, "bitbucket.org"): + projectPath = joinPath(importPath, 3) + case strings.HasPrefix(importPath, "launchpad.net"): + projectPath = joinPath(importPath, 2) + } + + return projectPath +} + +func joinPath(importPath string, num int) string { + subdirs := strings.Split(importPath, "/") + if len(subdirs) > num { + return strings.Join(subdirs[:num], "/") + } + return importPath +} + +var validTLD = map[string]bool{ + // curl http://data.iana.org/TLD/tlds-alpha-by-domain.txt | sed -e '/#/ d' -e 's/.*/"&": true,/' | tr [:upper:] [:lower:] + ".ac": true, + ".ad": true, + ".ae": true, + ".aero": true, + ".af": true, + ".ag": true, + ".ai": true, + ".al": true, + ".am": true, + ".an": true, + ".ao": true, + ".aq": true, + ".ar": true, + ".arpa": true, + ".as": true, + ".asia": true, + ".at": true, + ".au": true, + ".aw": true, + ".ax": true, + ".az": true, + ".ba": true, + ".bb": true, + ".bd": true, + ".be": true, + ".bf": true, + ".bg": true, + ".bh": true, + ".bi": true, + ".biz": true, + ".bj": true, + ".bm": true, + ".bn": true, + ".bo": true, + ".br": true, + ".bs": true, + ".bt": true, + ".bv": true, + ".bw": true, + ".by": true, + ".bz": true, + ".ca": true, + ".cat": true, + ".cc": true, + ".cd": true, + ".cf": true, + ".cg": true, + ".ch": true, + ".ci": true, + ".ck": true, + ".cl": true, + ".cm": true, + ".cn": true, + ".co": true, + ".com": true, + ".coop": true, + ".cr": true, + ".cu": true, + ".cv": true, + ".cw": true, + ".cx": true, + ".cy": true, + ".cz": true, + ".de": true, + ".dj": true, + ".dk": true, + ".dm": true, + ".do": true, + ".dz": true, + ".ec": true, + ".edu": true, + ".ee": true, + ".eg": true, + ".er": true, + ".es": true, + ".et": true, + ".eu": true, + ".fi": true, + ".fj": true, + ".fk": true, + ".fm": true, + ".fo": true, + ".fr": true, + ".ga": true, + ".gb": true, + ".gd": true, + ".ge": true, + ".gf": true, + ".gg": true, + ".gh": true, + ".gi": true, + ".gl": true, + ".gm": true, + ".gn": true, + ".gov": true, + ".gp": true, + ".gq": true, + ".gr": true, + ".gs": true, + ".gt": true, + ".gu": true, + ".gw": true, + ".gy": true, + ".hk": true, + ".hm": true, + ".hn": true, + ".hr": true, + ".ht": true, + ".hu": true, + ".id": true, + ".ie": true, + ".il": true, + ".im": true, + ".in": true, + ".info": true, + ".int": true, + ".io": true, + ".iq": true, + ".ir": true, + ".is": true, + ".it": true, + ".je": true, + ".jm": true, + ".jo": true, + ".jobs": true, + ".jp": true, + ".ke": true, + ".kg": true, + ".kh": true, + ".ki": true, + ".km": true, + ".kn": true, + ".kp": true, + ".kr": true, + ".kw": true, + ".ky": true, + ".kz": true, + ".la": true, + ".lb": true, + ".lc": true, + ".li": true, + ".lk": true, + ".lr": true, + ".ls": true, + ".lt": true, + ".lu": true, + ".lv": true, + ".ly": true, + ".ma": true, + ".mc": true, + ".md": true, + ".me": true, + ".mg": true, + ".mh": true, + ".mil": true, + ".mk": true, + ".ml": true, + ".mm": true, + ".mn": true, + ".mo": true, + ".mobi": true, + ".mp": true, + ".mq": true, + ".mr": true, + ".ms": true, + ".mt": true, + ".mu": true, + ".museum": true, + ".mv": true, + ".mw": true, + ".mx": true, + ".my": true, + ".mz": true, + ".na": true, + ".name": true, + ".nc": true, + ".ne": true, + ".net": true, + ".nf": true, + ".ng": true, + ".ni": true, + ".nl": true, + ".no": true, + ".np": true, + ".nr": true, + ".nu": true, + ".nz": true, + ".om": true, + ".org": true, + ".pa": true, + ".pe": true, + ".pf": true, + ".pg": true, + ".ph": true, + ".pk": true, + ".pl": true, + ".pm": true, + ".pn": true, + ".post": true, + ".pr": true, + ".pro": true, + ".ps": true, + ".pt": true, + ".pw": true, + ".py": true, + ".qa": true, + ".re": true, + ".ro": true, + ".rs": true, + ".ru": true, + ".rw": true, + ".sa": true, + ".sb": true, + ".sc": true, + ".sd": true, + ".se": true, + ".sg": true, + ".sh": true, + ".si": true, + ".sj": true, + ".sk": true, + ".sl": true, + ".sm": true, + ".sn": true, + ".so": true, + ".sr": true, + ".st": true, + ".su": true, + ".sv": true, + ".sx": true, + ".sy": true, + ".sz": true, + ".tc": true, + ".td": true, + ".tel": true, + ".tf": true, + ".tg": true, + ".th": true, + ".tj": true, + ".tk": true, + ".tl": true, + ".tm": true, + ".tn": true, + ".to": true, + ".tp": true, + ".tr": true, + ".travel": true, + ".tt": true, + ".tv": true, + ".tw": true, + ".tz": true, + ".ua": true, + ".ug": true, + ".uk": true, + ".us": true, + ".uy": true, + ".uz": true, + ".va": true, + ".vc": true, + ".ve": true, + ".vg": true, + ".vi": true, + ".vn": true, + ".vu": true, + ".wf": true, + ".ws": true, + ".xn--0zwm56d": true, + ".xn--11b5bs3a9aj6g": true, + ".xn--3e0b707e": true, + ".xn--45brj9c": true, + ".xn--80akhbyknj4f": true, + ".xn--80ao21a": true, + ".xn--90a3ac": true, + ".xn--9t4b11yi5a": true, + ".xn--clchc0ea0b2g2a9gcd": true, + ".xn--deba0ad": true, + ".xn--fiqs8s": true, + ".xn--fiqz9s": true, + ".xn--fpcrj9c3d": true, + ".xn--fzc2c9e2c": true, + ".xn--g6w251d": true, + ".xn--gecrj9c": true, + ".xn--h2brj9c": true, + ".xn--hgbk6aj7f53bba": true, + ".xn--hlcj6aya9esc7a": true, + ".xn--j6w193g": true, + ".xn--jxalpdlp": true, + ".xn--kgbechtv": true, + ".xn--kprw13d": true, + ".xn--kpry57d": true, + ".xn--lgbbat1ad8j": true, + ".xn--mgb9awbf": true, + ".xn--mgbaam7a8h": true, + ".xn--mgbayh7gpa": true, + ".xn--mgbbh1a71e": true, + ".xn--mgbc0a9azcg": true, + ".xn--mgberp4a5d4ar": true, + ".xn--mgbx4cd0ab": true, + ".xn--o3cw4h": true, + ".xn--ogbpf8fl": true, + ".xn--p1ai": true, + ".xn--pgbs0dh": true, + ".xn--s9brj9c": true, + ".xn--wgbh1c": true, + ".xn--wgbl6a": true, + ".xn--xkc2al3hye2a": true, + ".xn--xkc2dl3a5ee0h": true, + ".xn--yfro4i67o": true, + ".xn--ygbi2ammx": true, + ".xn--zckzah": true, + ".xxx": true, + ".ye": true, + ".yt": true, + ".za": true, + ".zm": true, + ".zw": true, +} + +var ( + validHost = regexp.MustCompile(`^[-a-z0-9]+(?:\.[-a-z0-9]+)+$`) + validPathElement = regexp.MustCompile(`^[-A-Za-z0-9~+][-A-Za-z0-9_.]*$`) +) + +// IsValidRemotePath returns true if importPath is structurally valid for "go get". +func IsValidRemotePath(importPath string) bool { + + parts := strings.Split(importPath, "/") + + if len(parts) <= 1 { + // Import path must contain at least one "/". + return false + } + + if !validTLD[path.Ext(parts[0])] { + return false + } + + if !validHost.MatchString(parts[0]) { + return false + } + for _, part := range parts[1:] { + if !validPathElement.MatchString(part) || part == "testdata" { + return false + } + } + + return true +} + +var standardPath = map[string]bool{ + "builtin": true, + + // go list -f '"{{.ImportPath}}": true,' std | grep -v 'cmd/|exp/' + "cmd/api": true, + "cmd/cgo": true, + "cmd/fix": true, + "cmd/go": true, + "cmd/gofmt": true, + "cmd/vet": true, + "cmd/yacc": true, + "archive/tar": true, + "archive/zip": true, + "bufio": true, + "bytes": true, + "compress/bzip2": true, + "compress/flate": true, + "compress/gzip": true, + "compress/lzw": true, + "compress/zlib": true, + "container/heap": true, + "container/list": true, + "container/ring": true, + "crypto": true, + "crypto/aes": true, + "crypto/cipher": true, + "crypto/des": true, + "crypto/dsa": true, + "crypto/ecdsa": true, + "crypto/elliptic": true, + "crypto/hmac": true, + "crypto/md5": true, + "crypto/rand": true, + "crypto/rc4": true, + "crypto/rsa": true, + "crypto/sha1": true, + "crypto/sha256": true, + "crypto/sha512": true, + "crypto/subtle": true, + "crypto/tls": true, + "crypto/x509": true, + "crypto/x509/pkix": true, + "database/sql": true, + "database/sql/driver": true, + "debug/dwarf": true, + "debug/elf": true, + "debug/gosym": true, + "debug/macho": true, + "debug/pe": true, + "encoding/ascii85": true, + "encoding/asn1": true, + "encoding/base32": true, + "encoding/base64": true, + "encoding/binary": true, + "encoding/csv": true, + "encoding/gob": true, + "encoding/hex": true, + "encoding/json": true, + "encoding/pem": true, + "encoding/xml": true, + "errors": true, + "expvar": true, + "flag": true, + "fmt": true, + "go/ast": true, + "go/build": true, + "go/doc": true, + "go/format": true, + "go/parser": true, + "go/printer": true, + "go/scanner": true, + "go/token": true, + "hash": true, + "hash/adler32": true, + "hash/crc32": true, + "hash/crc64": true, + "hash/fnv": true, + "html": true, + "html/template": true, + "image": true, + "image/color": true, + "image/draw": true, + "image/gif": true, + "image/jpeg": true, + "image/png": true, + "index/suffixarray": true, + "io": true, + "io/ioutil": true, + "log": true, + "log/syslog": true, + "math": true, + "math/big": true, + "math/cmplx": true, + "math/rand": true, + "mime": true, + "mime/multipart": true, + "net": true, + "net/http": true, + "net/http/cgi": true, + "net/http/cookiejar": true, + "net/http/fcgi": true, + "net/http/httptest": true, + "net/http/httputil": true, + "net/http/pprof": true, + "net/mail": true, + "net/rpc": true, + "net/rpc/jsonrpc": true, + "net/smtp": true, + "net/textproto": true, + "net/url": true, + "os": true, + "os/exec": true, + "os/signal": true, + "os/user": true, + "path": true, + "path/filepath": true, + "reflect": true, + "regexp": true, + "regexp/syntax": true, + "runtime": true, + "runtime/cgo": true, + "runtime/debug": true, + "runtime/pprof": true, + "runtime/race": true, + "sort": true, + "strconv": true, + "strings": true, + "sync": true, + "sync/atomic": true, + "syscall": true, + "testing": true, + "testing/iotest": true, + "testing/quick": true, + "text/scanner": true, + "text/tabwriter": true, + "text/template": true, + "text/template/parse": true, + "time": true, + "unicode": true, + "unicode/utf16": true, + "unicode/utf8": true, + "unsafe": true, +} + +// IsGoRepoPath returns true if package is from standard library. +func IsGoRepoPath(importPath string) bool { + return standardPath[importPath] +} + +func CheckNodeValue(v string) string { + if len(v) == 0 { + return "" + } + return v +} diff --git a/doc/utils_test.go b/doc/utils_test.go new file mode 100644 index 000000000..568cc6d0f --- /dev/null +++ b/doc/utils_test.go @@ -0,0 +1,71 @@ +// Copyright 2013-2014 gopm authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package doc + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +var VcsTestPairs = map[string]bool{ + "/.hg": true, + "/.git": true, + "/.svn": true, + "/.vendor": false, +} + +func Test_isVcsPath(t *testing.T) { + Convey("Test if the path is belonging to VCS", t, func() { + for name, expect := range VcsTestPairs { + So(isVcsPath(name), ShouldEqual, expect) + } + }) +} + +func TestGetDirsInfo(t *testing.T) { + Convey("Get directory's information that exist", t, func() { + dis, err := GetDirsInfo(".") + So(err, ShouldBeNil) + So(len(dis), ShouldEqual, 13) + }) + + Convey("Get directory's information does not exist", t, func() { + dis, err := GetDirsInfo("./404") + So(err, ShouldBeNil) + So(len(dis), ShouldEqual, 0) + }) +} + +var GoStdTestPairs = map[string]bool{ + "net/http": true, + "fmt": true, + "github.com/gpmgo/gopm": false, + "github.com/Unknwon/com": false, +} + +func TestIsGoRepoPath(t *testing.T) { + Convey("Test if the path is belonging to Go STD", t, func() { + for name, expect := range GoStdTestPairs { + So(IsGoRepoPath(name), ShouldEqual, expect) + } + }) +} + +func TestGetImports(t *testing.T) { + Convey("Get package that are imported", t, func() { + So(len(GetImports(".", "github.com/gpmgo/gopm/docs", false)), ShouldEqual, 4) + }) +} diff --git a/doc/vcs.go b/doc/vcs.go new file mode 100644 index 000000000..224ae7507 --- /dev/null +++ b/doc/vcs.go @@ -0,0 +1,307 @@ +// Copyright 2013-2014 gopm authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package doc + +import ( + "encoding/xml" + "errors" + "io" + "net/http" + "os" + "path" + "regexp" + "strings" + + "github.com/Unknwon/com" + "github.com/codegangsta/cli" + "github.com/gpmgo/gopm/log" +) + +var ( + appPath string + autoBackup bool +) + +func SetAppConfig(path string, backup bool) { + appPath = path + autoBackup = backup +} + +// TODO: specify with command line flag +const repoRoot = "/tmp/gddo" + +var urlTemplates = []struct { + re *regexp.Regexp + template string + lineFmt string +}{ + { + regexp.MustCompile(`^git\.gitorious\.org/(?P[^/]+/[^/]+)$`), + "https://gitorious.org/{repo}/blobs/{tag}/{dir}{0}", + "#line%d", + }, + { + regexp.MustCompile(`^camlistore\.org/r/p/(?P[^/]+)$`), + "http://camlistore.org/code/?p={repo}.git;hb={tag};f={dir}{0}", + "#l%d", + }, +} + +// lookupURLTemplate finds an expand() template, match map and line number +// format for well known repositories. +func lookupURLTemplate(repo, dir, tag string) (string, map[string]string, string) { + if strings.HasPrefix(dir, "/") { + dir = dir[1:] + "/" + } + for _, t := range urlTemplates { + if m := t.re.FindStringSubmatch(repo); m != nil { + match := map[string]string{ + "dir": dir, + "tag": tag, + } + for i, name := range t.re.SubexpNames() { + if name != "" { + match[name] = m[i] + } + } + return t.template, match, t.lineFmt + } + } + return "", nil, "" +} + +type vcsCmd struct { + schemes []string + download func([]string, string, string) (string, string, error) +} + +var lsremoteRe = regexp.MustCompile(`(?m)^([0-9a-f]{40})\s+refs/(?:tags|heads)/(.+)$`) + +var defaultTags = map[string]string{"git": "master", "hg": "default"} + +func bestTag(tags map[string]string, defaultTag string) (string, string, error) { + if commit, ok := tags[defaultTag]; ok { + return defaultTag, commit, nil + } + return "", "", com.NotFoundError{"Tag or branch not found."} +} + +// PureDownload downloads package without version control. +func PureDownload(nod *Node, installRepoPath string, ctx *cli.Context) ([]string, error) { + for _, s := range services { + if s.get == nil || !strings.HasPrefix(nod.DownloadURL, s.prefix) { + continue + } + m := s.pattern.FindStringSubmatch(nod.DownloadURL) + if m == nil { + if s.prefix != "" { + return nil, errors.New("Cannot match package service prefix by given path") + } + continue + } + match := map[string]string{"importPath": nod.DownloadURL} + for i, n := range s.pattern.SubexpNames() { + if n != "" { + match[n] = m[i] + } + } + return s.get(HttpClient, match, installRepoPath, nod, ctx) + } + + log.Log("Cannot match any service, getting dynamic...") + return getDynamic(HttpClient, nod, installRepoPath, ctx) +} + +func getDynamic(client *http.Client, nod *Node, installRepoPath string, ctx *cli.Context) ([]string, error) { + match, err := fetchMeta(client, nod.ImportPath) + if err != nil { + return nil, err + } + + if match["projectRoot"] != nod.ImportPath { + rootMatch, err := fetchMeta(client, match["projectRoot"]) + if err != nil { + return nil, err + } + if rootMatch["projectRoot"] != match["projectRoot"] { + return nil, com.NotFoundError{"Project root mismatch."} + } + } + + nod.DownloadURL = com.Expand("{repo}{dir}", match) + return PureDownload(nod, installRepoPath, ctx) +} + +func fetchMeta(client *http.Client, importPath string) (map[string]string, error) { + uri := importPath + if !strings.Contains(uri, "/") { + // Add slash for root of domain. + uri = uri + "/" + } + uri = uri + "?go-get=1" + + scheme := "https" + resp, err := client.Get(scheme + "://" + uri) + if err != nil || resp.StatusCode != 200 { + if err == nil { + resp.Body.Close() + } + scheme = "http" + resp, err = client.Get(scheme + "://" + uri) + if err != nil { + return nil, &com.RemoteError{strings.SplitN(importPath, "/", 2)[0], err} + } + } + defer resp.Body.Close() + return parseMeta(scheme, importPath, resp.Body) +} + +func attrValue(attrs []xml.Attr, name string) string { + for _, a := range attrs { + if strings.EqualFold(a.Name.Local, name) { + return a.Value + } + } + return "" +} + +func parseMeta(scheme, importPath string, r io.Reader) (map[string]string, error) { + var match map[string]string + + d := xml.NewDecoder(r) + d.Strict = false +metaScan: + for { + t, tokenErr := d.Token() + if tokenErr != nil { + break metaScan + } + switch t := t.(type) { + case xml.EndElement: + if strings.EqualFold(t.Name.Local, "head") { + break metaScan + } + case xml.StartElement: + if strings.EqualFold(t.Name.Local, "body") { + break metaScan + } + if !strings.EqualFold(t.Name.Local, "meta") || + attrValue(t.Attr, "name") != "go-import" { + continue metaScan + } + f := strings.Fields(attrValue(t.Attr, "content")) + if len(f) != 3 || + !strings.HasPrefix(importPath, f[0]) || + !(len(importPath) == len(f[0]) || importPath[len(f[0])] == '/') { + continue metaScan + } + if match != nil { + return nil, com.NotFoundError{"More than one found at " + scheme + "://" + importPath} + } + + projectRoot, vcs, repo := f[0], f[1], f[2] + + repo = strings.TrimSuffix(repo, "."+vcs) + i := strings.Index(repo, "://") + if i < 0 { + return nil, com.NotFoundError{"Bad repo URL in ."} + } + proto := repo[:i] + repo = repo[i+len("://"):] + + match = map[string]string{ + // Used in getVCSDoc, same as vcsPattern matches. + "importPath": importPath, + "repo": repo, + "vcs": vcs, + "dir": importPath[len(projectRoot):], + + // Used in getVCSDoc + "scheme": proto, + + // Used in getDynamic. + "projectRoot": projectRoot, + "projectName": path.Base(projectRoot), + "projectURL": scheme + "://" + projectRoot, + } + } + } + if match == nil { + return nil, com.NotFoundError{" not found."} + } + return match, nil +} + +func getImports(rootPath string, match map[string]string, cmdFlags map[string]bool, nod *Node) (imports []string) { + dirs, err := GetDirsInfo(rootPath) + if err != nil { + log.Error("", "Fail to get directory's information") + log.Fatal("", err.Error()) + } + + for _, d := range dirs { + if d.IsDir() && !(!cmdFlags["-e"] && strings.Contains(d.Name(), "example")) { + absPath := rootPath + d.Name() + "/" + importPkgs, err := CheckImports(absPath, match["importPath"], nod) + if err != nil { + return nil + } + imports = append(imports, importPkgs...) + } + } + return imports +} + +// checkImports checks package denpendencies. +func CheckImports(absPath, importPath string, nod *Node) (importPkgs []string, err error) { + dir, err := os.Open(absPath) + if err != nil { + return nil, err + } + defer dir.Close() + + // Get file info slice. + fis, err := dir.Readdir(0) + if err != nil { + return nil, err + } + + files := make([]*source, 0, 10) + for _, fi := range fis { + // Only handle files. + if strings.HasSuffix(fi.Name(), ".go") { + data, err := com.ReadFile(absPath + fi.Name()) + if err != nil { + return nil, err + } + + files = append(files, &source{ + name: fi.Name(), + data: data, + }) + } + } + + // Check if has Go source files. + if len(files) > 0 { + w := &walker{ImportPath: importPath} + importPkgs, err = w.build(files, nod) + if err != nil { + return nil, err + } + } + + return importPkgs, err +} diff --git a/doc/walker.go b/doc/walker.go new file mode 100644 index 000000000..4b9efa713 --- /dev/null +++ b/doc/walker.go @@ -0,0 +1,239 @@ +// Copyright 2013 gopm authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package doc + +import ( + "bytes" + "go/ast" + "go/build" + "go/doc" + "go/parser" + "go/token" + "io" + "io/ioutil" + "os" + "path" + "runtime" + "strings" + "unicode" + "unicode/utf8" + + "github.com/gpmgo/gopm/log" +) + +type sliceWriter struct{ p *[]byte } + +func (w sliceWriter) Write(p []byte) (int, error) { + *w.p = append(*w.p, p...) + return len(p), nil +} + +func (w *walker) readDir(dir string) ([]os.FileInfo, error) { + if dir != w.ImportPath { + panic("unexpected") + } + fis := make([]os.FileInfo, 0, len(w.srcs)) + for _, src := range w.srcs { + fis = append(fis, src) + } + return fis, nil +} + +func (w *walker) openFile(path string) (io.ReadCloser, error) { + if strings.HasPrefix(path, w.ImportPath+"/") { + if src, ok := w.srcs[path[len(w.ImportPath)+1:]]; ok { + return ioutil.NopCloser(bytes.NewReader(src.data)), nil + } + } + panic("unexpected") +} + +func simpleImporter(imports map[string]*ast.Object, path string) (*ast.Object, error) { + pkg := imports[path] + if pkg == nil { + // Guess the package name without importing it. Start with the last + // element of the path. + name := path[strings.LastIndex(path, "/")+1:] + + // Trim commonly used prefixes and suffixes containing illegal name + // runes. + name = strings.TrimSuffix(name, ".go") + name = strings.TrimSuffix(name, "-go") + name = strings.TrimPrefix(name, "go.") + name = strings.TrimPrefix(name, "go-") + name = strings.TrimPrefix(name, "biogo.") + + // It's also common for the last element of the path to contain an + // extra "go" prefix, but not always. TODO: examine unresolved ids to + // detect when trimming the "go" prefix is appropriate. + + pkg = ast.NewObj(ast.Pkg, name) + pkg.Data = ast.NewScope(nil) + imports[path] = pkg + } + return pkg, nil +} + +// build gets imports from source files. +func (w *walker) build(srcs []*source, nod *Node) ([]string, error) { + // Add source files to walker, I skipped references here. + w.srcs = make(map[string]*source) + for _, src := range srcs { + w.srcs[src.name] = src + } + + w.fset = token.NewFileSet() + + // Find the package and associated files. + ctxt := build.Context{ + GOOS: runtime.GOOS, + GOARCH: runtime.GOARCH, + CgoEnabled: true, + JoinPath: path.Join, + IsAbsPath: path.IsAbs, + SplitPathList: func(list string) []string { return strings.Split(list, ":") }, + IsDir: func(path string) bool { panic("unexpected") }, + HasSubdir: func(root, dir string) (rel string, ok bool) { panic("unexpected") }, + ReadDir: func(dir string) (fi []os.FileInfo, err error) { return w.readDir(dir) }, + OpenFile: func(path string) (r io.ReadCloser, err error) { return w.openFile(path) }, + Compiler: "gc", + } + + bpkg, err := ctxt.ImportDir(w.ImportPath, 0) + // Continue if there are no Go source files; we still want the directory info. + _, nogo := err.(*build.NoGoError) + if err != nil { + if nogo { + err = nil + } else { + log.Warn("walker: %s", err.Error()) + return nil, nil + } + } + + // Parse the Go files + + files := make(map[string]*ast.File) + for _, name := range append(bpkg.GoFiles, bpkg.CgoFiles...) { + file, err := parser.ParseFile(w.fset, name, w.srcs[name].data, parser.ParseComments) + if err != nil { + //beego.Error("doc.walker.build():", err) + continue + } + files[name] = file + } + + w.ImportPath = strings.Replace(w.ImportPath, "\\", "/", -1) + var imports []string + for _, v := range bpkg.Imports { + // Skip strandard library. + if !IsGoRepoPath(v) && + (GetProjectPath(v) != GetProjectPath(w.ImportPath)) { + imports = append(imports, v) + } + } + + apkg, _ := ast.NewPackage(w.fset, files, simpleImporter, nil) + + mode := doc.Mode(0) + if w.ImportPath == "builtin" { + mode |= doc.AllDecls + } + + pdoc := doc.New(apkg, w.ImportPath, mode) + + if nod != nil { + nod.Synopsis = Synopsis(pdoc.Doc) + if i := strings.Index(nod.Synopsis, "\n"); i > -1 { + nod.Synopsis = nod.Synopsis[:i] + } + } + + return imports, err +} + +var badSynopsisPrefixes = []string{ + "Autogenerated by Thrift Compiler", + "Automatically generated ", + "Auto-generated by ", + "Copyright ", + "COPYRIGHT ", + `THE SOFTWARE IS PROVIDED "AS IS"`, + "TODO: ", + "vim:", +} + +// Synopsis extracts the first sentence from s. All runs of whitespace are +// replaced by a single space. +func Synopsis(s string) string { + + parts := strings.SplitN(s, "\n\n", 2) + s = parts[0] + + var buf []byte + const ( + other = iota + period + space + ) + last := space +Loop: + for i := 0; i < len(s); i++ { + b := s[i] + switch b { + case ' ', '\t', '\r', '\n': + switch last { + case period: + break Loop + case other: + buf = append(buf, ' ') + last = space + } + case '.': + last = period + buf = append(buf, b) + default: + last = other + buf = append(buf, b) + } + } + + // Ensure that synopsis fits an App Engine datastore text property. + const m = 400 + if len(buf) > m { + buf = buf[:m] + if i := bytes.LastIndex(buf, []byte{' '}); i >= 0 { + buf = buf[:i] + } + buf = append(buf, " ..."...) + } + + s = string(buf) + + r, n := utf8.DecodeRuneInString(s) + if n < 0 || unicode.IsPunct(r) || unicode.IsSymbol(r) { + // ignore Markdown headings, editor settings, Go build constraints, and * in poorly formatted block comments. + s = "" + } else { + for _, prefix := range badSynopsisPrefixes { + if strings.HasPrefix(s, prefix) { + s = "" + break + } + } + } + + return s +} diff --git a/gopm.go b/gopm.go new file mode 100644 index 000000000..072f2299b --- /dev/null +++ b/gopm.go @@ -0,0 +1,67 @@ +// Copyright 2013-2014 gopm authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +// gopm(Go Package Manager) is a Go package manage tool for searching, installing, updating and sharing your packages in Go. +package main + +import ( + "os" + "runtime" + + "github.com/codegangsta/cli" + + "github.com/gpmgo/gopm/cmd" +) + +// +build go1.1 + +// Test that go1.1 tag above is included in builds. main.go refers to this definition. +const go11tag = true + +const APP_VER = "0.6.5.0320" + +// cmd.CmdTest, +// cmd.CmdSearch, +// cmdClean, +// cmdDoc, +// cmdEnv, +// cmdFix, +// cmdList, +// cmdTool, +// cmdVet, + +func init() { + runtime.GOMAXPROCS(runtime.NumCPU()) +} + +func main() { + app := cli.NewApp() + app.Name = "gopm" + app.Usage = "Go Package Manager" + app.Version = APP_VER + app.Commands = []cli.Command{ + cmd.CmdGet, + cmd.CmdBin, + cmd.CmdGen, + cmd.CmdRun, + cmd.CmdBuild, + cmd.CmdInstall, + cmd.CmdUpdate, + cmd.CmdConfig, + } + app.Flags = append(app.Flags, []cli.Flag{ + cli.BoolFlag{"noterm", "disable color output"}, + }...) + app.Run(os.Args) +} diff --git a/log/log.go b/log/log.go new file mode 100644 index 000000000..89b2b39d0 --- /dev/null +++ b/log/log.go @@ -0,0 +1,121 @@ +// Copyright 2013 gopm authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +// +build !windows + +// Package log provides npm-like style log output. +package log + +import ( + "fmt" + "os" + + "github.com/aybabtme/color/brush" +) + +func Error(hl, msg string) { + if PureMode { + errorP(hl, msg) + } + + if len(hl) > 0 { + hl = " " + brush.Red(hl).String() + } + fmt.Printf("gopm %s%s %s\n", brush.Red("ERR!"), hl, msg) +} + +func Fatal(hl, msg string) { + if PureMode { + fatal(hl, msg) + } + + Error(hl, msg) + os.Exit(2) +} + +func Warn(format string, args ...interface{}) { + if PureMode { + warn(format, args...) + return + } + + fmt.Printf("gopm %s %s\n", brush.Purple("WARN"), + fmt.Sprintf(format, args...)) +} + +func Log(format string, args ...interface{}) { + if PureMode { + log(format, args...) + return + } + + if !Verbose { + return + } + fmt.Printf("gopm %s %s\n", brush.White("INFO"), + fmt.Sprintf(format, args...)) +} + +func Trace(format string, args ...interface{}) { + if PureMode { + trace(format, args...) + return + } + + if !Verbose { + return + } + fmt.Printf("gopm %s %s\n", brush.Blue("TRAC"), + fmt.Sprintf(format, args...)) +} + +func Success(title, hl, msg string) { + if PureMode { + success(title, hl, msg) + return + } + + if !Verbose { + return + } + if len(hl) > 0 { + hl = " " + brush.Green(hl).String() + } + fmt.Printf("gopm %s%s %s\n", brush.Green(title), hl, msg) +} + +func Message(hl, msg string) { + if PureMode { + message(hl, msg) + return + } + + if !Verbose { + return + } + if len(hl) > 0 { + hl = " " + brush.Yellow(hl).String() + } + fmt.Printf("gopm %s%s %s\n", brush.Yellow("MSG!"), hl, msg) +} + +func Help(format string, args ...interface{}) { + if PureMode { + help(format, args...) + } + + fmt.Printf("gopm %s %s\n", brush.Cyan("HELP"), + fmt.Sprintf(format, args...)) + os.Exit(2) +} diff --git a/log/logP.go b/log/logP.go new file mode 100644 index 000000000..406718ad6 --- /dev/null +++ b/log/logP.go @@ -0,0 +1,80 @@ +// Copyright 2013 gopm authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package log + +import ( + "fmt" + "os" +) + +var ( + PureMode = false + Verbose = false +) + +func errorP(hl, msg string) { + if len(hl) > 0 { + hl = " " + hl + } + fmt.Printf("gopm ERR!%s %s\n", hl, msg) +} + +func fatal(hl, msg string) { + errorP(hl, msg) + os.Exit(2) +} + +func warn(format string, args ...interface{}) { + fmt.Printf("gopm WARN %s\n", fmt.Sprintf(format, args...)) +} + +func log(format string, args ...interface{}) { + if !Verbose { + return + } + fmt.Printf("gopm INFO %s\n", fmt.Sprintf(format, args...)) +} + +func trace(format string, args ...interface{}) { + if !Verbose { + return + } + fmt.Printf("gopm TRAC %s\n", fmt.Sprintf(format, args...)) +} + +func success(title, hl, msg string) { + if !Verbose { + return + } + if len(hl) > 0 { + hl = " " + hl + } + fmt.Printf("gopm %s%s %s\n", title, hl, msg) +} + +func message(hl, msg string) { + if !Verbose { + return + } + if len(hl) > 0 { + hl = " " + hl + } + fmt.Printf("gopm MSG!%s %s\n", hl, msg) +} + +func help(format string, args ...interface{}) { + fmt.Printf("gopm HELP %s\n", fmt.Sprintf(format, args...)) + os.Exit(2) +} diff --git a/log/log_windows.go b/log/log_windows.go new file mode 100644 index 000000000..661d5e8d2 --- /dev/null +++ b/log/log_windows.go @@ -0,0 +1,48 @@ +// Copyright 2013 gopm authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +// Package log provides npm-like style log output. +package log + +func Error(hl, msg string) { + errorP(hl, msg) +} + +func Fatal(hl, msg string) { + fatal(hl, msg) +} + +func Warn(format string, args ...interface{}) { + warn(format, args...) +} + +func Log(format string, args ...interface{}) { + log(format, args...) +} + +func Trace(format string, args ...interface{}) { + trace(format, args...) +} + +func Success(title, hl, msg string) { + success(title, hl, msg) +} + +func Message(hl, msg string) { + message(hl, msg) +} + +func Help(format string, args ...interface{}) { + help(format, args...) +}