Compare commits

...

8 Commits
v0.0.3 ... main

Author SHA1 Message Date
jolheiser 8027ad224b
chore: update nu
ci/woodpecker/push/goreleaser Pipeline was successful Details
Signed-off-by: jolheiser <john.olheiser@gmail.com>
2023-02-20 21:54:56 -06:00
jolheiser fd9f898315
chore: regen docs
ci/woodpecker/push/goreleaser Pipeline was successful Details
ci/woodpecker/tag/goreleaser Pipeline was successful Details
Signed-off-by: jolheiser <john.olheiser@gmail.com>
2023-01-30 12:59:16 -06:00
jolheiser 50d40aacc1
feat: cd, nu completions
Signed-off-by: jolheiser <john.olheiser@gmail.com>
2023-01-30 12:59:04 -06:00
jolheiser f335f5805f
fix: cleanup from daily usage
ci/woodpecker/push/goreleaser Pipeline was successful Details
ci/woodpecker/tag/goreleaser Pipeline was successful Details
Signed-off-by: jolheiser <john.olheiser@gmail.com>
2023-01-23 22:41:10 -06:00
jolheiser 190ea6c7f3
fix: add index to back/frontport
ci/woodpecker/push/goreleaser Pipeline was successful Details
ci/woodpecker/tag/goreleaser Pipeline was successful Details
Signed-off-by: jolheiser <john.olheiser@gmail.com>
2023-01-10 21:57:47 -06:00
jolheiser fe0e2e5707
feat: tag command and force cleanup
ci/woodpecker/tag/goreleaser Pipeline was successful Details
Signed-off-by: jolheiser <john.olheiser@gmail.com>
2022-12-27 22:40:45 -06:00
jolheiser b49dbe94b9
fix: ide should inherit stdin/out/err
ci/woodpecker/push/goreleaser Pipeline was successful Details
ci/woodpecker/tag/goreleaser Pipeline was successful Details
Signed-off-by: jolheiser <john.olheiser@gmail.com>
2022-12-26 21:14:53 -06:00
jolheiser 42853c9e12
feat: post command and cleanup
ci/woodpecker/push/goreleaser Pipeline was successful Details
ci/woodpecker/tag/goreleaser Pipeline was successful Details
Signed-off-by: jolheiser <john.olheiser@gmail.com>
2022-12-26 21:01:50 -06:00
15 changed files with 772 additions and 68 deletions

152
DOCS.md
View File

@ -6,11 +6,14 @@ git-ea is the base command
git-ea
├─ backport
├─ branch
├─ cd
├─ cleanup
├─ frontport
├─ ide
├─ init
└─ pr
├─ post
├─ pr
└─ tag
```
@ -30,25 +33,6 @@ git-ea <cmd>
**--version,-v**: Print git-ea version
-----
## cleanup
cleanup removes named branches, or interactive if no arguments
```
[--help]
```
**Usage**:
```
cleanup [branches...]
```
**--help**: Show help
-----
## backport
@ -60,6 +44,7 @@ backport cherry-picks a commit and applies it to a clean branch based on `releas
[--from,-f]=[value]
[--help]
[--list,-l]
[--push,-p]
[--to,-t]=[value]
```
**Usage**:
@ -77,7 +62,10 @@ 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`)
**--push,-p**: Push immediately
**--to,-t**="": Release to backport to (ex: `17`, default: `latest`)
-----
@ -92,7 +80,7 @@ branch creates a new branch called `name` based on `base`
[--help]
[--ide,-i]
[--list,-l]
[--no-fetch,-nf]
[--no-fetch,-n]
```
**Usage**:
@ -112,7 +100,57 @@ branch --base [ref=main] <name>
**--list,-l**: List branches available
**--no-fetch,-nf**: Skip fetching
**--no-fetch,-n**: Skip fetching
-----
## cleanup
cleanup removes named branches, or interactive if no arguments
```
[--force,-f]
[--help]
[--prune,-p]
```
**Usage**:
```
cleanup [branches...]
```
**--force,-f**: Force cleanup
**--help**: Show help
**--prune,-p**: Prune worktrees
-----
## cd
Open a shell in the specified `workspace`
```
[--help]
[--print,-p]
```
**Usage**:
```
cd [workspace=base]
```
**--help**: Show help
**--print,-p**: Print workspace dir instead of spawning a shell
-----
@ -125,6 +163,7 @@ frontport cherry-picks a commit and applies it to a clean branch based on `relea
```
[--from,-f]=[value]
[--help]
[--push,-p]
[--to,-t]=[value]
```
**Usage**:
@ -133,12 +172,15 @@ 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: <latest>)
**--from,-f**="": Release to frontport from (ex: `17`, default: <latest>)
**--help**: Show help
**--push,-p**: Push immediately
**--to,-t**="": Release to frontport to (ex: `main`, default: `main`)
@ -150,6 +192,7 @@ ide starts an IDE for `branch`
```
[--gui,-g]
[--help]
```
**Usage**:
@ -158,6 +201,9 @@ ide starts an IDE for `branch`
ide <branch>
```
**--gui,-g**: Prefer GUI editor
**--help**: Show help
@ -180,6 +226,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
@ -205,3 +286,26 @@ pr <index>
-----
## tag
tag makes a signed tag for `branch`
```
[--help]
[--push,-p]
```
**Usage**:
```
tag <branch>
```
**--help**: Show help
**--push,-p**: Push immediately
-----

24
blog/pulls.go 100644
View File

@ -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
}

108
blog/release.go 100644
View File

@ -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")
}

31
blog/release.md 100644
View File

@ -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.
<!-- Security Thanks! -->
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:**
<!--more-->
## Changelog
## [{{.Milestone}}](https://github.com/go-gitea/gitea/releases/tag/v{{.Milestone}}) - {{.DateShort}}
<!-- Changelog Details -->
{{.Changelog}}

View File

@ -4,24 +4,28 @@ 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")
pushFlag := fs.Bool("push", false, "Push immediately")
fs.BoolVar(pushFlag, "p", *pushFlag, "--push")
return &ffcli.Command{
Name: "backport",
FlagSet: fs,
@ -58,12 +62,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 +81,27 @@ 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-%s", to, 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())
if err := run(ctx, h.Config.WorkspaceBranch(branch), "git", "cherry-pick", optMap[resp]); err != nil {
return err
}
if *pushFlag {
return run(ctx, h.Config.WorkspaceBranch(branch), "git", "push", "origin", "HEAD")
}
return nil
},
}
}

View File

@ -14,7 +14,7 @@ import (
func (h *Handler) Branch() *ffcli.Command {
fs := flag.NewFlagSet("branch", flag.ContinueOnError)
noFetchFlag := fs.Bool("no-fetch", false, "Skip fetching")
fs.BoolVar(noFetchFlag, "nf", *noFetchFlag, "--no-fetch")
fs.BoolVar(noFetchFlag, "n", *noFetchFlag, "--no-fetch")
baseFlag := fs.String("base", "main", "Ref to base from")
fs.StringVar(baseFlag, "b", *baseFlag, "--base")
listFlag := fs.Bool("list", false, "List branches available")

59
cmd/cd.go 100644
View File

@ -0,0 +1,59 @@
package cmd
import (
"context"
"flag"
"fmt"
"os"
"os/exec"
"runtime"
"go.jolheiser.com/git-ea/config"
"github.com/peterbourgon/ff/v3/ffcli"
)
func (h *Handler) CD() *ffcli.Command {
fs := flag.NewFlagSet("cd", flag.ContinueOnError)
printFlag := fs.Bool("print", false, "Print workspace dir instead of spawning a shell")
fs.BoolVar(printFlag, "p", *printFlag, "--print")
return &ffcli.Command{
Name: "cd",
FlagSet: fs,
ShortUsage: "cd [workspace=base]",
ShortHelp: "Open a shell in the specified `workspace`",
Exec: func(ctx context.Context, _ []string) error {
dir := h.Config.Workspace()
if fs.NArg() > 0 {
dir = h.Config.WorkspaceBranch(fs.Arg(0))
}
if *printFlag {
fmt.Println(dir)
return nil
}
command, args := cd(h.Config)
cmd := exec.CommandContext(ctx, command, args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Dir = dir
return cmd.Run()
},
}
}
func cd(cfg *config.Config) (string, []string) {
if cfg.CD.Command != "" {
return cfg.CD.Command, cfg.CD.Args
}
var shell string
switch runtime.GOOS {
case "windows":
shell = "powershell.exe"
case "linux":
shell = "/bin/bash"
}
return shell, nil
}

View File

@ -11,6 +11,10 @@ import (
func (h *Handler) Cleanup() *ffcli.Command {
fs := flag.NewFlagSet("cleanup", flag.ContinueOnError)
forceFlag := fs.Bool("force", false, "Force cleanup")
fs.BoolVar(forceFlag, "f", *forceFlag, "--force")
pruneFlag := fs.Bool("prune", false, "Prune worktrees")
fs.BoolVar(pruneFlag, "p", *pruneFlag, "--prune")
return &ffcli.Command{
Name: "cleanup",
FlagSet: fs,
@ -21,9 +25,13 @@ func (h *Handler) Cleanup() *ffcli.Command {
return err
}
if *pruneFlag {
return run(ctx, h.Config.Workspace(), "git", "worktree", "prune")
}
if len(args) > 0 {
for _, arg := range args {
if err := removeWorktree(h, ctx, arg); err != nil {
if err := removeWorktree(h, ctx, arg, *forceFlag); err != nil {
return err
}
}
@ -48,7 +56,7 @@ func (h *Handler) Cleanup() *ffcli.Command {
}
for _, rm := range remove {
if err := removeWorktree(h, ctx, rm); err != nil {
if err := removeWorktree(h, ctx, rm, *forceFlag); err != nil {
return err
}
}
@ -58,8 +66,13 @@ func (h *Handler) Cleanup() *ffcli.Command {
}
}
func removeWorktree(h *Handler, ctx context.Context, name string) error {
if err := h.run(ctx, "git", "worktree", "remove", name); err != nil {
func removeWorktree(h *Handler, ctx context.Context, name string, force bool) error {
args := []string{"worktree", "remove"}
if force {
args = append(args, "--force")
}
args = append(args, name)
if err := h.run(ctx, "git", args...); err != nil {
return nil
}
return h.run(ctx, "git", "branch", "-D", name)

View File

@ -7,6 +7,7 @@ import (
"fmt"
"os"
"os/exec"
"os/user"
"regexp"
"strings"
@ -44,13 +45,16 @@ func New() (*ffcli.Command, error) {
ShortUsage: "git-ea <cmd>",
ShortHelp: "git-ea is the base command",
Subcommands: []*ffcli.Command{
handler.Cleanup(),
handler.Backport(),
handler.Branch(),
handler.Cleanup(),
handler.CD(),
handler.Frontport(),
handler.IDE(),
handler.Init(),
handler.Post(),
handler.PR(),
handler.Tag(),
},
}
c.Exec = func(_ context.Context, _ []string) error {
@ -59,16 +63,7 @@ func New() (*ffcli.Command, error) {
return nil
}
dir := cfg.Base
if fs.NArg() > 0 {
if strings.EqualFold(fs.Arg(0), "help") {
fmt.Println(ffcli.DefaultUsageFunc(c))
return nil
}
dir = cfg.WorkspaceBranch(fs.Arg(0))
}
fmt.Println(dir)
fmt.Println(ffcli.DefaultUsageFunc(c))
return nil
}
return c, nil
@ -100,15 +95,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 +143,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
}

View File

@ -8,17 +8,18 @@ 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: <latest>)")
fromFlag := fs.String("from", "", "Release to frontport from (ex: `17`, default: <latest>)")
fs.StringVar(fromFlag, "f", *fromFlag, "--from")
toFlag := fs.String("to", "", "Release to frontport to (ex: `main`, default: `main`)")
fs.StringVar(toFlag, "t", *toFlag, "--to")
pushFlag := fs.Bool("push", false, "Push immediately")
fs.BoolVar(pushFlag, "p", *pushFlag, "--push")
return &ffcli.Command{
Name: "frontport",
FlagSet: fs,
@ -46,12 +47,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 +66,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-%s", from, index)
base := *toFlag
if base == "" {
@ -79,7 +85,15 @@ func (h *Handler) Frontport() *ffcli.Command {
return err
}
return run(ctx, h.Config.WorkspaceBranch(branch), "git", "cherry-pick", hash.String())
if err := run(ctx, h.Config.WorkspaceBranch(branch), "git", "cherry-pick", optMap[resp]); err != nil {
return err
}
if *pushFlag {
return run(ctx, h.Config.WorkspaceBranch(branch), "git", "push", "origin", "HEAD")
}
return nil
},
}
}

View File

@ -3,6 +3,7 @@ package cmd
import (
"context"
"flag"
"os"
"os/exec"
"github.com/AlecAivazis/survey/v2"
@ -11,6 +12,8 @@ import (
func (h *Handler) IDE() *ffcli.Command {
fs := flag.NewFlagSet("ide", flag.ContinueOnError)
guiFlag := fs.Bool("gui", false, "Prefer GUI editor")
fs.BoolVar(guiFlag, "g", *guiFlag, "--gui")
return &ffcli.Command{
Name: "ide",
FlagSet: fs,
@ -38,7 +41,38 @@ func (h *Handler) IDE() *ffcli.Command {
}
path := h.Config.WorkspaceBranch(branch)
return exec.Command("nvim", path).Start()
cmd := exec.Command(getIDE(*guiFlag), path)
cmd.Dir = path
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
},
}
}
func getIDE(preferGUI bool) string {
term := []string{
"hx",
"nvim",
}
gui := []string{
"lapce",
"goland",
}
def := []string{
"vi",
}
ides := append(term, gui...)
if preferGUI {
ides = append(gui, term...)
}
for _, ide := range append(ides, def...) {
if i, err := exec.LookPath(ide); err == nil {
return i
}
}
return os.Getenv("EDITOR")
}

104
cmd/post.go 100644
View File

@ -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
},
}
}

122
cmd/tag.go 100644
View File

@ -0,0 +1,122 @@
package cmd
import (
"context"
"errors"
"flag"
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"github.com/AlecAivazis/survey/v2"
"github.com/go-git/go-git/v5"
"github.com/peterbourgon/ff/v3/ffcli"
)
var (
treeURL = "https://github.com/go-gitea/gitea/tree/%s"
versionRe = regexp.MustCompile(`v\d+\.\d+\.\d+`)
)
func (h *Handler) Tag() *ffcli.Command {
fs := flag.NewFlagSet("tag", flag.ContinueOnError)
pushFlag := fs.Bool("push", false, "Push immediately")
fs.BoolVar(pushFlag, "p", *pushFlag, "--push")
return &ffcli.Command{
Name: "tag",
FlagSet: fs,
ShortUsage: "tag <branch>",
ShortHelp: "tag makes a signed tag for `branch`",
Exec: func(ctx context.Context, args []string) error {
if err := h.checkInit(); err != nil {
return err
}
h.fetch(ctx)
var branch string
if len(args) > 0 {
branch = args[0]
}
if branch == "" {
var branches []string
remote, err := h.repo().Remote("upstream")
if err != nil {
return err
}
refs, err := remote.List(&git.ListOptions{})
if err != nil {
return err
}
for _, ref := range refs {
if ref.Name().IsBranch() {
branches = append(branches, strings.TrimPrefix(ref.Name().String(), "refs/heads/"))
}
}
sort.Strings(branches)
if err := survey.AskOne(&survey.Select{
Message: "Branch",
Options: branches,
}, &branch, survey.WithValidator(survey.Required)); err != nil {
return err
}
}
if !strings.HasPrefix(branch, "release") {
if !strings.HasPrefix(branch, "v") {
branch = fmt.Sprintf("v%s", branch)
}
branch = fmt.Sprintf("release/%s", branch)
}
var version string
if err := survey.AskOne(&survey.Input{
Message: "Version",
}, &version, survey.WithValidator(func(ans any) error {
if !versionRe.MatchString(fmt.Sprint(ans)) {
return errors.New("version isn't valid v$maj.$min.$")
}
return nil
})); err != nil {
return err
}
workspaceName := fmt.Sprintf("release-%s", version)
if err := h.Branch().ParseAndRun(ctx, []string{"--base", branch, workspaceName}); err != nil {
return err
}
var changelog string
if err := survey.AskOne(&survey.Editor{
Message: "Changelog",
FileName: "*.md",
}, &changelog, survey.WithValidator(survey.Required)); err != nil {
return err
}
fi, err := os.Create(filepath.Join(h.Config.WorkspaceBranch(workspaceName), "release.notes"))
if err != nil {
return err
}
if _, err := fi.WriteString(changelog); err != nil {
return err
}
if err := fi.Close(); err != nil {
return err
}
if err := run(ctx, h.Config.WorkspaceBranch(workspaceName), "git", "tag", "-s", "-F", "release.notes", version); err != nil {
return err
}
if *pushFlag {
return run(ctx, h.Config.WorkspaceBranch(workspaceName), "git", "push", "upstream", version)
}
fmt.Printf("cd %s && git push upstream %s \n", h.Config.WorkspaceBranch(workspaceName), version)
return nil
},
}
}

View File

@ -8,8 +8,14 @@ import (
"path/filepath"
)
type CD struct {
Command string `json:"command"`
Args []string `json:"args"`
}
type Config struct {
Base string `json:"base"`
CD CD `json:"cd"`
}
func (c *Config) Workspace() string {
@ -40,9 +46,13 @@ func (c *Config) Branches() ([]string, error) {
}
func path() (string, error) {
userCfgDir, err := os.UserConfigDir()
if err != nil {
return "", err
userCfgDir, ok := os.LookupEnv("XDG_CONFIG_HOME")
if !ok {
var err error
userCfgDir, err = os.UserConfigDir()
if err != nil {
return "", err
}
}
cfgDir := filepath.Join(userCfgDir, "git-ea")
return filepath.Join(cfgDir, "config.json"), os.MkdirAll(cfgDir, os.ModePerm)
@ -63,7 +73,7 @@ func Load() (*Config, error) {
if err != nil {
return nil, err
}
if _, err := fi.WriteString("{}"); err != nil {
if _, err := fi.WriteString(`{}`); err != nil {
return nil, err
}
if _, err := fi.Seek(0, 0); err != nil {

70
contrib/git-ea.nu 100644
View File

@ -0,0 +1,70 @@
def "nu-complete git-ea branches" [] {
^git-ea branch -l | lines | each { |line| $line | str trim }
}
# Backport
export extern "git ea backport" [
--from(-f) # From branch
--list(-l) # Open a web browser to view needed backports
--push(-p) # Push immediately
--to(-t) # To branch
]
# Branch
export extern "git ea branch" [
name?: string # Branch name
--base(-b) # Branch base
--ide(-i) # Open an IDE for the new branch
--list(-l) # List branches
--no-fetch(-n) # Skip fetching
]
# CD
export extern "git ea cd" [
workspace?: string@"nu-complete git-ea branches" # Workspace
--print(-p) # Print instead of spawning shell
]
# Cleanup
export extern "git ea cleanup" [
...branches: string # Branch names
--force(-f) # Force cleanup
--prune(-p) # Prune worktrees
]
# Frontport
export extern "git ea frontport" [
--from(-f) # From branch
--push(-p) # Push immediately
--to(-t) # To branch
]
# IDE
export extern "git ea ide" [
workspace?: string@"nu-complete git-ea branches" # Workspace
--gui(-g) # Prefer GUI
]
# Init
export extern "git ea init" []
# Post
export extern "git ea post" [
--author(-a) # Author
--changelog(-c) # Changelog (no header)
--milestone(-m) # Milestone
--output(-o) # Output file (default: `content/post/release-of-${milestone}.md`)
]
# PR
export extern "git ea pr" [
index: int # PR index
--ide(-i) # Open IDE for this PR
]
# Tag
export extern "git ea tag" [
branch?: string # The branch to tag
--push(-p) # Push immediately
]