From 42853c9e129b5db208e1fa95d59df5299ceeebc0 Mon Sep 17 00:00:00 2001 From: jolheiser Date: Mon, 26 Dec 2022 21:01:50 -0600 Subject: [PATCH] feat: post command and cleanup Signed-off-by: jolheiser --- DOCS.md | 40 +++++++++++++++++- blog/pulls.go | 24 +++++++++++ blog/release.go | 108 +++++++++++++++++++++++++++++++++++++++++++++++ blog/release.md | 31 ++++++++++++++ cmd/backport.go | 21 ++++++--- cmd/cmd.go | 19 +++++---- cmd/frontport.go | 18 +++++--- cmd/ide.go | 2 +- cmd/post.go | 104 +++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 341 insertions(+), 26 deletions(-) create mode 100644 blog/pulls.go create mode 100644 blog/release.go create mode 100644 blog/release.md create mode 100644 cmd/post.go diff --git a/DOCS.md b/DOCS.md index 755f67b..9ecb192 100644 --- a/DOCS.md +++ b/DOCS.md @@ -10,6 +10,7 @@ git-ea ├─ frontport ├─ ide ├─ init +├─ post └─ pr ``` @@ -77,7 +78,7 @@ backport --from [release=main] --to [release=latest] **--list,-l**: Open repository to see needed backports -**--to,-t**="": Release to backport to (ex: `1.17`, default: `latest`) +**--to,-t**="": Release to backport to (ex: `17`, default: `latest`) ----- @@ -133,7 +134,7 @@ frontport cherry-picks a commit and applies it to a clean branch based on `relea frontport --from [release=latest] --to [release=main] ``` -**--from,-f**="": Release to frontport from (ex: `1.17`, default: ) +**--from,-f**="": Release to frontport from (ex: `17`, default: ) **--help**: Show help @@ -180,6 +181,41 @@ init **--help**: Show help +----- + +## post + +post creates a new blog release post + + +``` +[--author,-a]=[value] +[--changelog,-c]=[value] +[--help] +[--milestone,-m]=[value] +[--output,-o]=[value] +``` +**Usage**: + +``` +post +``` + +**--author,-a**="": Post author + + +**--changelog,-c**="": Post changelog (no header) + + +**--help**: Show help + + +**--milestone,-m**="": Post milestone + + +**--output,-o**="": Output file (default: `content/post/release-of-${milestone}.md`) + + ----- ## pr diff --git a/blog/pulls.go b/blog/pulls.go new file mode 100644 index 0000000..316f95b --- /dev/null +++ b/blog/pulls.go @@ -0,0 +1,24 @@ +package blog + +import ( + "io" + "regexp" +) + +var ( + pullGiteaURL = "https://github.com/go-gitea/gitea/pull/" + pullRegex = regexp.MustCompile(`#(\d+)\)`) +) + +// FormatPR formats input by replacing pull refs #1234 with markdown links [#1234](...pull/1234) +func FormatPR(r io.Reader) ([]byte, error) { + data, err := io.ReadAll(r) + if err != nil { + return nil, err + } + + pullURL := pullGiteaURL + + repl := pullRegex.ReplaceAll(data, []byte(`[#$1](`+pullURL+`$1))`)) + return repl, nil +} diff --git a/blog/release.go b/blog/release.go new file mode 100644 index 0000000..eb8e88f --- /dev/null +++ b/blog/release.go @@ -0,0 +1,108 @@ +package blog + +import ( + "bytes" + _ "embed" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "text/template" + "time" + + "github.com/skratchdot/open-golang/open" +) + +var ( + mergedURLFmt = "https://api.github.com/search/issues?q=repo:go-gitea/gitea+is:pr+is:merged+milestone:%s" + changelogURLFmt = "https://api.github.com/search/issues?q=repo:go-gitea/gitea+is:pr+is:merged+Changelog+%s" + + //go:embed release.md + tmplContent string + tmpl = template.Must(template.New("").Parse(tmplContent)) +) + +// Merged is the API response for getting count of merged PRs in a milestone +type Merged struct { + TotalCount int `json:"total_count"` +} + +// FormatRelease formats a release template and injects author, milestone, changelog, and merged PR counts +func FormatRelease(author, milestone, changelog string) ([]byte, error) { + resp, err := http.Get(fmt.Sprintf(mergedURLFmt, milestone)) + if err != nil { + return nil, err + } + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var merged Merged + if err := json.Unmarshal(body, &merged); err != nil { + return nil, err + } + + date := time.Now() + m := map[string]interface{}{ + "Author": author, + "Milestone": milestone, + "Changelog": changelog, + "Merged": merged.TotalCount, + "DateLong": date.Format("2006-01-02T15:04:05+07:00"), + "DateShort": date.Format("2006-01-02"), + } + + var buf bytes.Buffer + if err := tmpl.Execute(&buf, m); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +// Release is the API response for a release +type Release struct { + Name string `json:"name"` + PublishedAt time.Time `json:"published_at"` +} + +// LatestRelease gets the latest release, used as a default for the generator +func LatestRelease() (Release, error) { + var rel Release + resp, err := http.Get("https://api.github.com/repos/go-gitea/gitea/releases/latest") + if err != nil { + return rel, err + } + defer resp.Body.Close() + return rel, json.NewDecoder(resp.Body).Decode(&rel) +} + +// ChangelogPR is the API response when searching for the milestone changelog PR +type ChangelogPR struct { + Items []struct { + HTMLURL string `json:"html_url"` + } `json:"items"` +} + +// OpenChangelogPullRequest attempts to open a browser to the changelog PR of a given milestone +func OpenChangelogPullRequest(milestone string) error { + resp, err := http.Get(fmt.Sprintf(changelogURLFmt, milestone)) + if err != nil { + return err + } + defer resp.Body.Close() + + var pr ChangelogPR + if err := json.NewDecoder(resp.Body).Decode(&pr); err != nil { + return err + } + + if len(pr.Items) == 0 { + return errors.New("could not find changelog PR") + } + + return open.Start(pr.Items[0].HTMLURL + "/files") +} diff --git a/blog/release.md b/blog/release.md new file mode 100644 index 0000000..40e8806 --- /dev/null +++ b/blog/release.md @@ -0,0 +1,31 @@ +--- +date: "{{.DateLong}}" +author: "{{.Author}}" +title: "Gitea {{.Milestone}} is released" +tags: ["release"] +draft: false +--- + +We are proud to present the release of Gitea version {{.Milestone}}. + +We highly encourage users to update to this version for some important bug-fixes. + +We have merged [{{.Merged}}](https://github.com/go-gitea/gitea/pulls?q=is%3Apr+milestone%3A{{.Milestone}}+is%3Amerged) pull requests to release this version. + + + +You can download one of our pre-built binaries from our [downloads page](https://dl.gitea.io/gitea/{{.Milestone}}/) - make sure to select the correct platform! For further details on how to install, follow our [installation guide](https://docs.gitea.io/en-us/install-from-binary/). + + +We would also like to thank all of our supporters on [Open Collective](https://opencollective.com/gitea) who are helping to sustain us financially. + +**Have you heard? We now have a [swag shop](https://shop.gitea.io)! :shirt: :tea:** + + + +## Changelog + +## [{{.Milestone}}](https://github.com/go-gitea/gitea/releases/tag/v{{.Milestone}}) - {{.DateShort}} + + +{{.Changelog}} \ No newline at end of file diff --git a/cmd/backport.go b/cmd/backport.go index 2164c11..5e843c4 100644 --- a/cmd/backport.go +++ b/cmd/backport.go @@ -4,21 +4,23 @@ import ( "context" "flag" "fmt" + "regexp" "strings" "github.com/AlecAivazis/survey/v2" "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" "github.com/peterbourgon/ff/v3/ffcli" "github.com/skratchdot/open-golang/open" ) +var indexRe = regexp.MustCompile(`\(#(\d+)\)`) + func (h *Handler) Backport() *ffcli.Command { fs := flag.NewFlagSet("backport", flag.ContinueOnError) fromFlag := fs.String("from", "", "Release to backport from (ex: `main`, default: main)") fs.StringVar(fromFlag, "f", *fromFlag, "--from") - toFlag := fs.String("to", "", "Release to backport to (ex: `1.17`, default: `latest`)") + toFlag := fs.String("to", "", "Release to backport to (ex: `17`, default: `latest`)") fs.StringVar(toFlag, "t", *toFlag, "--to") listFlag := fs.Bool("list", false, "Open repository to see needed backports") fs.BoolVar(listFlag, "l", *listFlag, "--list") @@ -58,12 +60,12 @@ func (h *Handler) Backport() *ffcli.Command { return err } - optMap := make(map[string]plumbing.Hash) + optMap := make(map[string]string) var opts []string if err := commits.ForEach(func(c *object.Commit) error { title := strings.Split(c.Message, "\n")[0] opts = append(opts, title) - optMap[title] = c.Hash + optMap[title] = c.Hash.String() return nil }); err != nil { return err @@ -77,14 +79,19 @@ func (h *Handler) Backport() *ffcli.Command { return err } - hash := optMap[resp] - branch := fmt.Sprintf("backport-%s", hash) + index := optMap[resp] + m := indexRe.FindStringSubmatch(resp) + if m != nil { + index = m[1] + } + + branch := fmt.Sprintf("backport-%s", index) base := fmt.Sprintf("upstream/release/v1.%s", to) if err := h.Branch().ParseAndRun(ctx, []string{"--base", base, branch}); err != nil { return err } - return run(ctx, h.Config.WorkspaceBranch(branch), "git", "cherry-pick", hash.String()) + return run(ctx, h.Config.WorkspaceBranch(branch), "git", "cherry-pick", optMap[resp]) }, } } diff --git a/cmd/cmd.go b/cmd/cmd.go index a2fa8d8..7ca65e1 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -7,6 +7,7 @@ import ( "fmt" "os" "os/exec" + "os/user" "regexp" "strings" @@ -50,6 +51,7 @@ func New() (*ffcli.Command, error) { handler.Frontport(), handler.IDE(), handler.Init(), + handler.Post(), handler.PR(), }, } @@ -100,15 +102,6 @@ func (h *Handler) fetch(ctx context.Context) { } } -func isClean() bool { - c := exec.Command("git", "status", "--porcelain") - o, err := c.Output() - if err != nil { - log.Fatal().Err(err).Msg("could not get git status") - } - return len(o) == 0 -} - func (h *Handler) repo() *git.Repository { repo, err := git.PlainOpen(h.Config.Base) if err != nil { @@ -157,3 +150,11 @@ func (h *Handler) latestRelease() string { return strings.TrimLeft(latest, "0") } + +func currentUser() string { + u, err := user.Current() + if err != nil { + return "" + } + return u.Username +} diff --git a/cmd/frontport.go b/cmd/frontport.go index 86b4bdb..c096fd8 100644 --- a/cmd/frontport.go +++ b/cmd/frontport.go @@ -8,14 +8,13 @@ import ( "github.com/AlecAivazis/survey/v2" "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" "github.com/peterbourgon/ff/v3/ffcli" ) func (h *Handler) Frontport() *ffcli.Command { fs := flag.NewFlagSet("frontport", flag.ContinueOnError) - fromFlag := fs.String("from", "", "Release to frontport from (ex: `1.17`, default: )") + fromFlag := fs.String("from", "", "Release to frontport from (ex: `17`, default: )") fs.StringVar(fromFlag, "f", *fromFlag, "--from") toFlag := fs.String("to", "", "Release to frontport to (ex: `main`, default: `main`)") fs.StringVar(toFlag, "t", *toFlag, "--to") @@ -46,12 +45,12 @@ func (h *Handler) Frontport() *ffcli.Command { return err } - optMap := make(map[string]plumbing.Hash) + optMap := make(map[string]string) var opts []string if err := commits.ForEach(func(c *object.Commit) error { title := strings.Split(c.Message, "\n")[0] opts = append(opts, title) - optMap[title] = c.Hash + optMap[title] = c.Hash.String() return nil }); err != nil { return err @@ -65,8 +64,13 @@ func (h *Handler) Frontport() *ffcli.Command { return err } - hash := optMap[resp] - branch := fmt.Sprintf("frontport-%s", hash) + index := optMap[resp] + m := indexRe.FindStringSubmatch(resp) + if m != nil { + index = m[1] + } + + branch := fmt.Sprintf("frontport-%s", index) base := *toFlag if base == "" { @@ -79,7 +83,7 @@ func (h *Handler) Frontport() *ffcli.Command { return err } - return run(ctx, h.Config.WorkspaceBranch(branch), "git", "cherry-pick", hash.String()) + return run(ctx, h.Config.WorkspaceBranch(branch), "git", "cherry-pick", optMap[resp]) }, } } diff --git a/cmd/ide.go b/cmd/ide.go index 1c246cc..a70b9b1 100644 --- a/cmd/ide.go +++ b/cmd/ide.go @@ -38,7 +38,7 @@ func (h *Handler) IDE() *ffcli.Command { } path := h.Config.WorkspaceBranch(branch) - return exec.Command("nvim", path).Start() + return exec.Command("hx", path).Start() }, } } diff --git a/cmd/post.go b/cmd/post.go new file mode 100644 index 0000000..04c7fd7 --- /dev/null +++ b/cmd/post.go @@ -0,0 +1,104 @@ +package cmd + +import ( + "bytes" + "context" + "flag" + "fmt" + "os" + "strings" + + "go.jolheiser.com/git-ea/blog" + + "github.com/AlecAivazis/survey/v2" + "github.com/peterbourgon/ff/v3/ffcli" +) + +func (h *Handler) Post() *ffcli.Command { + fs := flag.NewFlagSet("post", flag.ContinueOnError) + authorFlag := fs.String("author", "", "Post author") + fs.StringVar(authorFlag, "a", *authorFlag, "--author") + milestoneFlag := fs.String("milestone", "", "Post milestone") + fs.StringVar(milestoneFlag, "m", *milestoneFlag, "--milestone") + changelogFlag := fs.String("changelog", "", "Post changelog (no header)") + fs.StringVar(changelogFlag, "c", *changelogFlag, "--changelog") + outputFlag := fs.String("output", "content/post/release-of-${milestone}.md", "Output file") + fs.StringVar(outputFlag, "o", *outputFlag, "--output") + return &ffcli.Command{ + Name: "post", + FlagSet: fs, + ShortUsage: "post", + ShortHelp: "post creates a new blog release post", + Exec: func(ctx context.Context, args []string) error { + author := *authorFlag + if author == "" { + if err := survey.AskOne(&survey.Input{ + Message: "Blog post author", + Default: currentUser(), + }, &author, survey.WithValidator(survey.Required)); err != nil { + return err + } + } + + milestone := *milestoneFlag + if milestone == "" { + var defMilestone blog.Release + r, err := blog.LatestRelease() + if err == nil { + defMilestone = r + } + if err := survey.AskOne(&survey.Input{ + Message: "Release milestone", + Default: strings.TrimPrefix(defMilestone.Name, "v"), + Help: fmt.Sprintf("Default was published on %s", defMilestone.PublishedAt.Format("01/02/2006")), + }, &milestone, survey.WithValidator(survey.Required)); err != nil { + return err + } + } + + changelog := *changelogFlag + if changelog == "" { + if err := blog.OpenChangelogPullRequest(milestone); err != nil { + fmt.Println(err) + } + if err := survey.AskOne(&survey.Editor{ + Message: "Blog post changelog", + FileName: "*.md", + }, &changelog, survey.WithValidator(survey.Required)); err != nil { + return err + } + } + + post, err := blog.FormatRelease(author, milestone, changelog) + if err != nil { + return err + } + + complete, err := blog.FormatPR(bytes.NewReader(post)) + if err != nil { + return err + } + + output := os.Expand(*outputFlag, func(s string) string { + switch s { + case "milestone": + return milestone + default: + return s + } + }) + + fi, err := os.Create(output) + if err != nil { + return err + } + defer fi.Close() + + if _, err := fi.Write(complete); err != nil { + fmt.Println(err) + } + fmt.Printf("Blog post created at %q\n", output) + return nil + }, + } +}