diff --git a/cmd/kratos/internal/base/get.go b/cmd/kratos/internal/base/get.go index afed66aa558..0cbe99d5b6f 100644 --- a/cmd/kratos/internal/base/get.go +++ b/cmd/kratos/internal/base/get.go @@ -1,9 +1,17 @@ package base import ( + "encoding/json" + "errors" "fmt" + "io" + "io/ioutil" + "net/http" "os" "os/exec" + "regexp" + "strings" + "time" ) // GoGet go get path. @@ -19,3 +27,200 @@ func GoGet(path ...string) error { } return nil } + +type ReleaseInfo struct { + Author struct { + Login string `json:"login"` + } `json:"author"` + PublishedAt string `json:"published_at"` + Body string `json:"body"` + HtmlUrl string `json:"html_url"` +} + +type CommitInfo struct { + Commit struct { + Message string `json:"message"` + } `json:"commit"` +} + +type ErrorInfo struct { + Message string +} + +type GithubApi struct { + Owner string + Repo string + Token string +} + +// GetReleaseInfo for getting kratos release info. +func (g *GithubApi) GetReleaseInfo(version string) ReleaseInfo { + api := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", g.Owner, g.Repo) + if version != "latest" { + api = fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/tags/%s", g.Owner, g.Repo, version) + } + resp, code := requestGithubAPI(api, "GET", nil, g.Token) + if code != 200 { + printGithubErrorInfo(resp) + } + releaseInfo := ReleaseInfo{} + err := json.Unmarshal(resp, &releaseInfo) + if err != nil { + fatal(err) + } + return releaseInfo +} + +// GetCommitsInfo for getting kratos commits info. +func (g *GithubApi) GetCommitsInfo() []CommitInfo { + info := g.GetReleaseInfo("latest") + page := 1 + var list []CommitInfo + for { + url := fmt.Sprintf("https://api.github.com/repos/%s/%s/commits?page=%d&since=%s", g.Owner, g.Repo, page, info.PublishedAt) + resp, code := requestGithubAPI(url, "GET", nil, g.Token) + if code != 200 { + printGithubErrorInfo(resp) + } + var res []CommitInfo + err := json.Unmarshal(resp, &res) + if err != nil { + fatal(err) + } + list = append(list, res...) + if len(res) < 30 || len(res) == 0 { + break + } + page++ + } + return list +} + +func printGithubErrorInfo(body []byte) { + errorInfo := &ErrorInfo{} + err := json.Unmarshal(body, errorInfo) + if err != nil { + fatal(err) + } + fatal(errors.New(errorInfo.Message)) +} + +func requestGithubAPI(url string, method string, body io.Reader, token string) ([]byte, int) { + cli := &http.Client{Timeout: 60 * time.Second} + request, err := http.NewRequest(method, url, body) + if err != nil { + fatal(err) + } + if token != "" { + request.Header.Add("Authorization", token) + } + resp, err := cli.Do(request) + if err != nil { + fatal(err) + } + defer resp.Body.Close() + resBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + fatal(err) + } + return resBody, resp.StatusCode +} + +func ParseCommitsInfo(info []CommitInfo) string { + group := map[string][]string{ + "fix": {}, + "feat": {}, + "deps": {}, + "break": {}, + "other": {}, + } + for i, commitInfo := range info { + msg := commitInfo.Commit.Message + index := strings.Index(fmt.Sprintf("%q", msg), `\n`) + if index != -1 { + msg = commitInfo.Commit.Message[:index-1] + } + prefix := []string{"fix", "feat", "deps", "break"} + for _, v := range prefix { + if strings.HasPrefix(msg, v) { + group[v] = append(group[v], msg) + info = append(info[:i], info[i+1:]...) + } + } + } + // other + for _, value := range info { + msg := value.Commit.Message + index := strings.Index(fmt.Sprintf("%q", msg), `\n`) + if index != -1 { + msg = value.Commit.Message[:index-1] + } + group["other"] = append(group["other"], msg) + } + md := make(map[string]string) + for key, value := range group { + var text string + switch key { + case "break": + text = "### Breaking Changes\n" + case "deps": + text = "### Dependencies\n" + case "feat": + text = "### New Features\n" + case "fix": + text = "### Bug Fixes\n" + case "other": + text = "### Others\n" + } + if len(value) > 0 { + md[key] += text + for _, value := range value { + md[key] += fmt.Sprintf("- %s\n", value) + } + } + } + return fmt.Sprint(md["break"], md["deps"], md["feat"], md["fix"], md["other"]) +} + +func ParseReleaseInfo(info ReleaseInfo) string { + reg := regexp.MustCompile(`(?m)^\s*$[\r\n]*|[\r\n]+\s+\z|<[\S\s]+?>`) + body := reg.ReplaceAll([]byte(info.Body), []byte("")) + if string(body) == "" { + body = []byte("no release info") + } + splitters := "--------------------------------------------" + return fmt.Sprintf( + "Author: %s\nDate: %s\nUrl: %s\n\n%s\n\n%s\n\n%s\n", + info.Author.Login, + info.PublishedAt, + info.HtmlUrl, + splitters, + body, + splitters, + ) +} + +func ParseGithubUrl(url string) (owner string, repo string) { + var start int + start = strings.Index(url, "//") + if start == -1 { + start = strings.Index(url, ":") + 1 + } else { + start += 2 + } + end := strings.LastIndex(url, "/") + gitIndex := strings.LastIndex(url, ".git") + if gitIndex == -1 { + repo = url[strings.LastIndex(url, "/")+1:] + } else { + repo = url[strings.LastIndex(url, "/")+1 : gitIndex] + } + tmp := url[start:end] + owner = tmp[strings.Index(tmp, "/")+1:] + return +} + +func fatal(err error) { + fmt.Fprintf(os.Stderr, "\033[31mERROR: %s\033[m\n", err) + os.Exit(1) +} diff --git a/cmd/kratos/internal/change/change.go b/cmd/kratos/internal/change/change.go new file mode 100644 index 00000000000..3548e8c3502 --- /dev/null +++ b/cmd/kratos/internal/change/change.go @@ -0,0 +1,43 @@ +package change + +import ( + "fmt" + "os" + + "github.com/go-kratos/kratos/cmd/kratos/v2/internal/base" + "github.com/spf13/cobra" +) + +// CmdNew represents the new command. +var CmdNew = &cobra.Command{ + Use: "changelog", + Short: "Get a kratos change log", + Long: "Get a kratos release or commits info. Example: kratos changelog dev or kratos changelog {version}", + Run: run, +} +var token string +var repoURL string + +func init() { + if repoURL = os.Getenv("KRATOS_REPO"); repoURL == "" { + repoURL = "https://github.com/go-kratos/kratos.git" + } + CmdNew.Flags().StringVarP(&repoURL, "repo-url", "r", repoURL, "github repo") + token = os.Getenv("GITHUB_TOKEN") +} + +func run(cmd *cobra.Command, args []string) { + owner, repo := base.ParseGithubUrl(repoURL) + api := base.GithubApi{Owner: owner, Repo: repo, Token: token} + version := "latest" + if len(args) > 0 { + version = args[0] + } + if version == "dev" { + info := api.GetCommitsInfo() + fmt.Print(base.ParseCommitsInfo(info)) + } else { + info := api.GetReleaseInfo(version) + fmt.Print(base.ParseReleaseInfo(info)) + } +} \ No newline at end of file diff --git a/cmd/kratos/main.go b/cmd/kratos/main.go index 6315cd0030b..b5c077f25fd 100644 --- a/cmd/kratos/main.go +++ b/cmd/kratos/main.go @@ -3,6 +3,7 @@ package main import ( "log" + "github.com/go-kratos/kratos/cmd/kratos/v2/internal/change" "github.com/go-kratos/kratos/cmd/kratos/v2/internal/project" "github.com/go-kratos/kratos/cmd/kratos/v2/internal/proto" "github.com/go-kratos/kratos/cmd/kratos/v2/internal/upgrade" @@ -25,10 +26,11 @@ func init() { rootCmd.AddCommand(project.CmdNew) rootCmd.AddCommand(proto.CmdProto) rootCmd.AddCommand(upgrade.CmdUpgrade) + rootCmd.AddCommand(change.CmdNew) } func main() { if err := rootCmd.Execute(); err != nil { log.Fatal(err) } -} +} \ No newline at end of file