Compare commits
19 Commits
Author | SHA1 | Date |
---|---|---|
jolheiser | 8027ad224b | |
jolheiser | fd9f898315 | |
jolheiser | 50d40aacc1 | |
jolheiser | f335f5805f | |
jolheiser | 190ea6c7f3 | |
jolheiser | fe0e2e5707 | |
jolheiser | b49dbe94b9 | |
jolheiser | 42853c9e12 | |
jolheiser | c404599204 | |
jolheiser | 9a09094080 | |
jolheiser | 1622f99d5d | |
jolheiser | 64cfe82797 | |
jolheiser | 693c85ad18 | |
jolheiser | 8d5ec322a3 | |
jolheiser | 37d0e9f29b | |
jolheiser | c950b9e903 | |
jolheiser | b3d15829a2 | |
jolheiser | 9c0fec9616 | |
jolheiser | 86ce1bed45 |
|
@ -6,7 +6,7 @@ builds:
|
||||||
- windows
|
- windows
|
||||||
- darwin
|
- darwin
|
||||||
ldflags:
|
ldflags:
|
||||||
- "-s -w -X main.Version={{.Version}}"
|
- "-s -w -X go.jolheiser.com/git-ea/cmd.Version={{.Version}}"
|
||||||
archives:
|
archives:
|
||||||
- replacements:
|
- replacements:
|
||||||
386: i386
|
386: i386
|
||||||
|
|
|
@ -0,0 +1,311 @@
|
||||||
|
# git-ea
|
||||||
|
|
||||||
|
git-ea is the base command
|
||||||
|
|
||||||
|
```
|
||||||
|
git-ea
|
||||||
|
├─ backport
|
||||||
|
├─ branch
|
||||||
|
├─ cd
|
||||||
|
├─ cleanup
|
||||||
|
├─ frontport
|
||||||
|
├─ ide
|
||||||
|
├─ init
|
||||||
|
├─ post
|
||||||
|
├─ pr
|
||||||
|
└─ tag
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
[--help]
|
||||||
|
[--version,-v]
|
||||||
|
```
|
||||||
|
**Usage**:
|
||||||
|
|
||||||
|
```
|
||||||
|
git-ea <cmd>
|
||||||
|
```
|
||||||
|
|
||||||
|
**--help**: Show help
|
||||||
|
|
||||||
|
|
||||||
|
**--version,-v**: Print git-ea version
|
||||||
|
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
|
## backport
|
||||||
|
|
||||||
|
backport cherry-picks a commit and applies it to a clean branch based on `release`
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
[--from,-f]=[value]
|
||||||
|
[--help]
|
||||||
|
[--list,-l]
|
||||||
|
[--push,-p]
|
||||||
|
[--to,-t]=[value]
|
||||||
|
```
|
||||||
|
**Usage**:
|
||||||
|
|
||||||
|
```
|
||||||
|
backport --from [release=main] --to [release=latest]
|
||||||
|
```
|
||||||
|
|
||||||
|
**--from,-f**="": Release to backport from (ex: `main`, default: main)
|
||||||
|
|
||||||
|
|
||||||
|
**--help**: Show help
|
||||||
|
|
||||||
|
|
||||||
|
**--list,-l**: Open repository to see needed backports
|
||||||
|
|
||||||
|
|
||||||
|
**--push,-p**: Push immediately
|
||||||
|
|
||||||
|
|
||||||
|
**--to,-t**="": Release to backport to (ex: `17`, default: `latest`)
|
||||||
|
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
|
## branch
|
||||||
|
|
||||||
|
branch creates a new branch called `name` based on `base`
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
[--base,-b]=[value]
|
||||||
|
[--help]
|
||||||
|
[--ide,-i]
|
||||||
|
[--list,-l]
|
||||||
|
[--no-fetch,-n]
|
||||||
|
```
|
||||||
|
**Usage**:
|
||||||
|
|
||||||
|
```
|
||||||
|
branch --base [ref=main] <name>
|
||||||
|
```
|
||||||
|
|
||||||
|
**--base,-b**="": Ref to base from (default: `main`)
|
||||||
|
|
||||||
|
|
||||||
|
**--help**: Show help
|
||||||
|
|
||||||
|
|
||||||
|
**--ide,-i**: Open an IDE for the new branch
|
||||||
|
|
||||||
|
|
||||||
|
**--list,-l**: List branches available
|
||||||
|
|
||||||
|
|
||||||
|
**--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
|
||||||
|
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
|
## frontport
|
||||||
|
|
||||||
|
frontport cherry-picks a commit and applies it to a clean branch based on `release`
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
[--from,-f]=[value]
|
||||||
|
[--help]
|
||||||
|
[--push,-p]
|
||||||
|
[--to,-t]=[value]
|
||||||
|
```
|
||||||
|
**Usage**:
|
||||||
|
|
||||||
|
```
|
||||||
|
frontport --from [release=latest] --to [release=main]
|
||||||
|
```
|
||||||
|
|
||||||
|
**--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`)
|
||||||
|
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
|
## ide
|
||||||
|
|
||||||
|
ide starts an IDE for `branch`
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
[--gui,-g]
|
||||||
|
[--help]
|
||||||
|
```
|
||||||
|
**Usage**:
|
||||||
|
|
||||||
|
```
|
||||||
|
ide <branch>
|
||||||
|
```
|
||||||
|
|
||||||
|
**--gui,-g**: Prefer GUI editor
|
||||||
|
|
||||||
|
|
||||||
|
**--help**: Show help
|
||||||
|
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
|
## init
|
||||||
|
|
||||||
|
init initializes a workspace for Gitea
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
[--help]
|
||||||
|
```
|
||||||
|
**Usage**:
|
||||||
|
|
||||||
|
```
|
||||||
|
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
|
||||||
|
|
||||||
|
pr pulls down a pull request for testing
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
[--help]
|
||||||
|
[--ide,-i]
|
||||||
|
```
|
||||||
|
**Usage**:
|
||||||
|
|
||||||
|
```
|
||||||
|
pr <index>
|
||||||
|
```
|
||||||
|
|
||||||
|
**--help**: Show help
|
||||||
|
|
||||||
|
|
||||||
|
**--ide,-i**: Start an IDE for this PR
|
||||||
|
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
|
## tag
|
||||||
|
|
||||||
|
tag makes a signed tag for `branch`
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
[--help]
|
||||||
|
[--push,-p]
|
||||||
|
```
|
||||||
|
**Usage**:
|
||||||
|
|
||||||
|
```
|
||||||
|
tag <branch>
|
||||||
|
```
|
||||||
|
|
||||||
|
**--help**: Show help
|
||||||
|
|
||||||
|
|
||||||
|
**--push,-p**: Push immediately
|
||||||
|
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
|
@ -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}}
|
|
@ -2,50 +2,72 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AlecAivazis/survey/v2"
|
"github.com/AlecAivazis/survey/v2"
|
||||||
"github.com/go-git/go-git/v5"
|
"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/go-git/go-git/v5/plumbing/object"
|
||||||
"github.com/peterbourgon/ff/v3/ffcli"
|
"github.com/peterbourgon/ff/v3/ffcli"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/skratchdot/open-golang/open"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var indexRe = regexp.MustCompile(`\(#(\d+)\)`)
|
||||||
backportFS = flag.NewFlagSet("backport", flag.ContinueOnError)
|
|
||||||
Backport = &ffcli.Command{
|
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: `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",
|
Name: "backport",
|
||||||
FlagSet: backportFS,
|
FlagSet: fs,
|
||||||
ShortUsage: "backport <release>",
|
ShortUsage: "backport --from [release=main] --to [release=latest]",
|
||||||
ShortHelp: "backport cherry-picks a <commit> and applies it to a clean branch based on <release>",
|
ShortHelp: "backport cherry-picks a commit and applies it to a clean branch based on `release`",
|
||||||
Exec: func(ctx context.Context, args []string) error {
|
Exec: func(ctx context.Context, _ []string) error {
|
||||||
if len(args) < 1 {
|
if err := h.checkInit(); err != nil {
|
||||||
return errors.New("backport requires one argument")
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch(ctx)
|
to := *toFlag
|
||||||
|
if to == "" {
|
||||||
if !isClean() {
|
to = h.latestRelease()
|
||||||
log.Fatal().Msg("working tree is dirty")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
commits, err := repo().Log(&git.LogOptions{
|
if *listFlag {
|
||||||
From: head("main"),
|
if err := open.Run(fmt.Sprintf("https://github.com/go-gitea/gitea/pulls?q=is%%3Apr+-label%%3Abackport%%2Fdone+label%%3Abackport%%2Fv1.%s+is%%3Amerged", to)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
h.fetch(ctx)
|
||||||
|
|
||||||
|
from := *fromFlag
|
||||||
|
if from == "" {
|
||||||
|
from = "main"
|
||||||
|
}
|
||||||
|
|
||||||
|
commits, err := h.repo().Log(&git.LogOptions{
|
||||||
|
From: h.head(from),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
optMap := make(map[string]plumbing.Hash)
|
optMap := make(map[string]string)
|
||||||
var opts []string
|
var opts []string
|
||||||
if err := commits.ForEach(func(c *object.Commit) error {
|
if err := commits.ForEach(func(c *object.Commit) error {
|
||||||
title := strings.Split(c.Message, "\n")[0]
|
title := strings.Split(c.Message, "\n")[0]
|
||||||
opts = append(opts, title)
|
opts = append(opts, title)
|
||||||
optMap[title] = c.Hash
|
optMap[title] = c.Hash.String()
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -59,14 +81,27 @@ var (
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
hash := optMap[resp]
|
index := optMap[resp]
|
||||||
branch := fmt.Sprintf("backport-%s", hash)
|
m := indexRe.FindStringSubmatch(resp)
|
||||||
base := fmt.Sprintf("upstream/release/v%s", args[0])
|
if m != nil {
|
||||||
if err := Branch.ParseAndRun(ctx, []string{branch, base}); err != 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 err
|
||||||
}
|
}
|
||||||
|
|
||||||
return run(ctx, "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
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
|
|
|
@ -4,38 +4,76 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/peterbourgon/ff/v3/ffcli"
|
"github.com/peterbourgon/ff/v3/ffcli"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
func (h *Handler) Branch() *ffcli.Command {
|
||||||
branchFS = flag.NewFlagSet("branch", flag.ContinueOnError)
|
fs := flag.NewFlagSet("branch", flag.ContinueOnError)
|
||||||
branchNoFetch = branchFS.Bool("no-fetch", false, "Skip fetching")
|
noFetchFlag := fs.Bool("no-fetch", false, "Skip fetching")
|
||||||
Branch = &ffcli.Command{
|
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")
|
||||||
|
fs.BoolVar(listFlag, "l", *listFlag, "--list")
|
||||||
|
ideFlag := fs.Bool("ide", false, "Open an IDE for the new branch")
|
||||||
|
fs.BoolVar(ideFlag, "i", *ideFlag, "--ide")
|
||||||
|
return &ffcli.Command{
|
||||||
Name: "branch",
|
Name: "branch",
|
||||||
FlagSet: branchFS,
|
FlagSet: fs,
|
||||||
ShortUsage: "branch <name> [base=main]",
|
ShortUsage: "branch --base [ref=main] <name>",
|
||||||
ShortHelp: "branch creates a new branch called <name> based on [base]",
|
ShortHelp: "branch creates a new branch called `name` based on `base`",
|
||||||
Exec: func(ctx context.Context, args []string) error {
|
Exec: func(ctx context.Context, args []string) error {
|
||||||
|
if err := h.checkInit(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if *listFlag {
|
||||||
|
dirs, err := os.ReadDir(h.Config.Workspace())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, dir := range dirs {
|
||||||
|
if !dir.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.Println(dir.Name())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
return errors.New("branch requires at least one argument")
|
return errors.New("branch requires a name")
|
||||||
}
|
}
|
||||||
|
|
||||||
name, base := args[0], "upstream/main"
|
name := args[0]
|
||||||
if len(args) > 1 {
|
base := *baseFlag
|
||||||
base = args[1]
|
switch {
|
||||||
|
case strings.HasPrefix(base, "o/"):
|
||||||
|
base = "origin/" + strings.TrimPrefix(base, "o/")
|
||||||
|
case strings.HasPrefix(base, "u/"):
|
||||||
|
base = "upstream/" + strings.TrimPrefix(base, "u/")
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(base, "upstream") && !strings.HasPrefix(base, "origin") && !strings.HasPrefix(base, "pr-") {
|
||||||
|
base = fmt.Sprintf("upstream/%s", base)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isClean() {
|
if !*noFetchFlag {
|
||||||
log.Fatal().Msg("working tree is dirty")
|
h.fetch(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !*branchNoFetch {
|
if err := h.run(ctx, "git", "worktree", "add", "-B", name, h.Config.WorkspaceBranch(name), base); err != nil {
|
||||||
fetch(ctx)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return run(ctx, "git", "checkout", "-b", name, base)
|
if *ideFlag {
|
||||||
|
return h.IDE().ParseAndRun(ctx, []string{name})
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
|
||||||
|
"github.com/AlecAivazis/survey/v2"
|
||||||
|
"github.com/peterbourgon/ff/v3/ffcli"
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
ShortUsage: "cleanup [branches...]",
|
||||||
|
ShortHelp: "cleanup removes named branches, or interactive if no arguments",
|
||||||
|
Exec: func(ctx context.Context, args []string) error {
|
||||||
|
if err := h.checkInit(); err != nil {
|
||||||
|
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, *forceFlag); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
opts, err := h.Config.Branches()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(opts) == 0 {
|
||||||
|
return errors.New("no worktrees currently exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
var remove []string
|
||||||
|
if err := survey.AskOne(&survey.MultiSelect{
|
||||||
|
Message: "Worktrees to remove",
|
||||||
|
Options: opts,
|
||||||
|
}, &remove); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rm := range remove {
|
||||||
|
if err := removeWorktree(h, ctx, rm, *forceFlag); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 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)
|
||||||
|
}
|
133
cmd/cmd.go
133
cmd/cmd.go
|
@ -2,59 +2,152 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"os/user"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go.jolheiser.com/git-ea/config"
|
||||||
|
|
||||||
"github.com/go-git/go-git/v5"
|
"github.com/go-git/go-git/v5"
|
||||||
"github.com/go-git/go-git/v5/plumbing"
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
|
"github.com/peterbourgon/ff/v3/ffcli"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func run(ctx context.Context, cmd string, args ...string) error {
|
var Version = "x.y.z"
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
Config *config.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() (*ffcli.Command, error) {
|
||||||
|
cfg, err := config.Load()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := Handler{
|
||||||
|
Config: cfg,
|
||||||
|
}
|
||||||
|
|
||||||
|
fs := flag.NewFlagSet("git-ea", flag.ContinueOnError)
|
||||||
|
versionFlag := fs.Bool("version", false, "Print git-ea version")
|
||||||
|
fs.BoolVar(versionFlag, "v", *versionFlag, "--version")
|
||||||
|
|
||||||
|
c := &ffcli.Command{
|
||||||
|
Name: "git-ea",
|
||||||
|
FlagSet: fs,
|
||||||
|
ShortUsage: "git-ea <cmd>",
|
||||||
|
ShortHelp: "git-ea is the base command",
|
||||||
|
Subcommands: []*ffcli.Command{
|
||||||
|
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 {
|
||||||
|
if *versionFlag {
|
||||||
|
log.Info().Msgf("git-ea v%s", Version)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(ffcli.DefaultUsageFunc(c))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) checkInit() error {
|
||||||
|
if !h.Config.IsInit() {
|
||||||
|
return errors.New("git-ea must be initialized first with `git ea init` in an appropriate directory")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) run(ctx context.Context, cmd string, args ...string) error {
|
||||||
|
return run(ctx, h.Config.Base, cmd, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(ctx context.Context, dir, cmd string, args ...string) error {
|
||||||
c := exec.CommandContext(ctx, cmd, args...)
|
c := exec.CommandContext(ctx, cmd, args...)
|
||||||
|
c.Dir = dir
|
||||||
c.Stdout = os.Stdout
|
c.Stdout = os.Stdout
|
||||||
c.Stderr = os.Stderr
|
c.Stderr = os.Stderr
|
||||||
c.Stdin = os.Stdin
|
c.Stdin = os.Stdin
|
||||||
return c.Run()
|
return c.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetch(ctx context.Context) {
|
func (h *Handler) fetch(ctx context.Context) {
|
||||||
if err := run(ctx, "git", "fetch", "upstream"); err != nil {
|
if err := run(ctx, h.Config.Base, "git", "fetch", "upstream"); err != nil {
|
||||||
log.Err(err).Msg("")
|
log.Err(err).Msg("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func isClean() bool {
|
func (h *Handler) repo() *git.Repository {
|
||||||
c := exec.Command("git", "status", "--porcelain")
|
repo, err := git.PlainOpen(h.Config.Base)
|
||||||
o, err := c.Output()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("could not get git status")
|
|
||||||
}
|
|
||||||
return len(o) == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func repo() *git.Repository {
|
|
||||||
repo, err := git.PlainOpenWithOptions(".", &git.PlainOpenOptions{
|
|
||||||
DetectDotGit: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("cannot open git repository")
|
log.Fatal().Err(err).Msg("cannot open git repository")
|
||||||
}
|
}
|
||||||
return repo
|
return repo
|
||||||
}
|
}
|
||||||
|
|
||||||
func worktree() *git.Worktree {
|
func (h *Handler) worktree() *git.Worktree {
|
||||||
tree, err := repo().Worktree()
|
tree, err := h.repo().Worktree()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("cannot get git worktree")
|
log.Fatal().Err(err).Msg("cannot get git worktree")
|
||||||
}
|
}
|
||||||
return tree
|
return tree
|
||||||
}
|
}
|
||||||
|
|
||||||
func head(name string) plumbing.Hash {
|
func (h *Handler) head(name string) plumbing.Hash {
|
||||||
ref, err := repo().Reference(plumbing.NewRemoteReferenceName("upstream", name), false)
|
ref, err := h.repo().Reference(plumbing.NewRemoteReferenceName("upstream", name), false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msgf("cannot get remote upstream %s HEAD", name)
|
log.Fatal().Err(err).Msgf("cannot get remote upstream %s HEAD", name)
|
||||||
}
|
}
|
||||||
return ref.Hash()
|
return ref.Hash()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var releaseRe = regexp.MustCompile(`release/v1\.(\d+)`)
|
||||||
|
|
||||||
|
func (h *Handler) latestRelease() string {
|
||||||
|
cmd := exec.Command("git", "ls-remote", "upstream", "release/*")
|
||||||
|
cmd.Dir = h.Config.Base
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("could not get latest release")
|
||||||
|
}
|
||||||
|
matches := releaseRe.FindAllStringSubmatch(string(out), -1)
|
||||||
|
|
||||||
|
var latest string
|
||||||
|
for _, match := range matches {
|
||||||
|
m := match[1]
|
||||||
|
if len(m) < 3 {
|
||||||
|
m = strings.Repeat("0", 3-len(m)) + m
|
||||||
|
}
|
||||||
|
if m > latest {
|
||||||
|
latest = m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimLeft(latest, "0")
|
||||||
|
}
|
||||||
|
|
||||||
|
func currentUser() string {
|
||||||
|
u, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return u.Username
|
||||||
|
}
|
||||||
|
|
|
@ -2,54 +2,57 @@ package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AlecAivazis/survey/v2"
|
"github.com/AlecAivazis/survey/v2"
|
||||||
"github.com/go-git/go-git/v5"
|
"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/go-git/go-git/v5/plumbing/object"
|
||||||
"github.com/peterbourgon/ff/v3/ffcli"
|
"github.com/peterbourgon/ff/v3/ffcli"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
func (h *Handler) Frontport() *ffcli.Command {
|
||||||
frontportFS = flag.NewFlagSet("frontport", flag.ContinueOnError)
|
fs := flag.NewFlagSet("frontport", flag.ContinueOnError)
|
||||||
Frontport = &ffcli.Command{
|
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",
|
Name: "frontport",
|
||||||
FlagSet: frontportFS,
|
FlagSet: fs,
|
||||||
ShortUsage: "frontport <from> [release=main]",
|
ShortUsage: "frontport --from [release=latest] --to [release=main]",
|
||||||
ShortHelp: "frontport cherry-picks a <commit> and applies it to a clean branch based on <release>",
|
ShortHelp: "frontport cherry-picks a commit and applies it to a clean branch based on `release`",
|
||||||
Exec: func(ctx context.Context, args []string) error {
|
Exec: func(ctx context.Context, _ []string) error {
|
||||||
if len(args) < 1 {
|
if err := h.checkInit(); err != nil {
|
||||||
return errors.New("frontport requires at least one argument")
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
from := *fromFlag
|
||||||
|
if from == "" {
|
||||||
|
from = h.latestRelease()
|
||||||
}
|
}
|
||||||
from := args[0]
|
|
||||||
if !strings.HasPrefix(from, "release") {
|
if !strings.HasPrefix(from, "release") {
|
||||||
from = fmt.Sprintf("release/v%s", args[0])
|
from = fmt.Sprintf("release/v%s", from)
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch(ctx)
|
h.fetch(ctx)
|
||||||
|
|
||||||
if !isClean() {
|
commits, err := h.repo().Log(&git.LogOptions{
|
||||||
log.Fatal().Msg("working tree is dirty")
|
From: h.head(from),
|
||||||
}
|
|
||||||
|
|
||||||
commits, err := repo().Log(&git.LogOptions{
|
|
||||||
From: head(from),
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
optMap := make(map[string]plumbing.Hash)
|
optMap := make(map[string]string)
|
||||||
var opts []string
|
var opts []string
|
||||||
if err := commits.ForEach(func(c *object.Commit) error {
|
if err := commits.ForEach(func(c *object.Commit) error {
|
||||||
title := strings.Split(c.Message, "\n")[0]
|
title := strings.Split(c.Message, "\n")[0]
|
||||||
opts = append(opts, title)
|
opts = append(opts, title)
|
||||||
optMap[title] = c.Hash
|
optMap[title] = c.Hash.String()
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -63,17 +66,34 @@ var (
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
hash := optMap[resp]
|
index := optMap[resp]
|
||||||
branch := fmt.Sprintf("frontport-%s", hash)
|
m := indexRe.FindStringSubmatch(resp)
|
||||||
base := "upstream/main"
|
if m != nil {
|
||||||
if len(args) > 1 {
|
index = m[1]
|
||||||
base = fmt.Sprintf("upstream/release/v%s", args[1])
|
|
||||||
}
|
}
|
||||||
if err := Branch.ParseAndRun(ctx, []string{branch, base}); err != nil {
|
|
||||||
|
branch := fmt.Sprintf("frontport-%s-%s", from, index)
|
||||||
|
|
||||||
|
base := *toFlag
|
||||||
|
if base == "" {
|
||||||
|
base = "upstream/main"
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(base, "upstream") {
|
||||||
|
base = fmt.Sprintf("upstream/release/v1.%s", base)
|
||||||
|
}
|
||||||
|
if err := h.Branch().ParseAndRun(ctx, []string{branch, base}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return run(ctx, "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
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/AlecAivazis/survey/v2"
|
||||||
|
"github.com/peterbourgon/ff/v3/ffcli"
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
ShortUsage: "ide <branch>",
|
||||||
|
ShortHelp: "ide starts an IDE for `branch`",
|
||||||
|
Exec: func(ctx context.Context, args []string) error {
|
||||||
|
if err := h.checkInit(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var branch string
|
||||||
|
if len(args) > 0 {
|
||||||
|
branch = args[0]
|
||||||
|
} else {
|
||||||
|
opts, err := h.Config.Branches()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := survey.AskOne(&survey.Select{
|
||||||
|
Message: "Branch to open",
|
||||||
|
Options: opts,
|
||||||
|
}, &branch); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
path := h.Config.WorkspaceBranch(branch)
|
||||||
|
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")
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"go.jolheiser.com/git-ea/config"
|
||||||
|
|
||||||
|
"github.com/peterbourgon/ff/v3/ffcli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *Handler) Init() *ffcli.Command {
|
||||||
|
fs := flag.NewFlagSet("init", flag.ContinueOnError)
|
||||||
|
return &ffcli.Command{
|
||||||
|
Name: "init",
|
||||||
|
FlagSet: fs,
|
||||||
|
ShortUsage: "init",
|
||||||
|
ShortHelp: "init initializes a workspace for Gitea",
|
||||||
|
Exec: func(ctx context.Context, args []string) error {
|
||||||
|
if h.Config.Base != "" {
|
||||||
|
return fmt.Errorf("a git-ea workspace already exists at %q", h.Config.Base)
|
||||||
|
}
|
||||||
|
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := run(ctx, ".", "git", "clone", "--bare", "git@github.com:jolheiser/gitea.git", "."); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := run(ctx, ".", "git", "remote", "add", "upstream", "https://github.com/go-gitea/gitea.git"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := config.Config{
|
||||||
|
Base: cwd,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(cfg.Workspace(), os.ModePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.Save(cfg)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/peterbourgon/ff/v3/ffcli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *Handler) PR() *ffcli.Command {
|
||||||
|
fs := flag.NewFlagSet("pr", flag.ContinueOnError)
|
||||||
|
ideFlag := fs.Bool("ide", false, "Start an IDE for this PR")
|
||||||
|
fs.BoolVar(ideFlag, "i", *ideFlag, "--ide")
|
||||||
|
return &ffcli.Command{
|
||||||
|
Name: "pr",
|
||||||
|
FlagSet: fs,
|
||||||
|
ShortUsage: "pr <index>",
|
||||||
|
ShortHelp: "pr pulls down a pull request for testing",
|
||||||
|
Exec: func(ctx context.Context, args []string) error {
|
||||||
|
if err := h.checkInit(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) == 0 {
|
||||||
|
return errors.New("pr requires an index")
|
||||||
|
}
|
||||||
|
|
||||||
|
idx := args[0]
|
||||||
|
branch := fmt.Sprintf("pr-%s", idx)
|
||||||
|
if err := h.run(ctx, "git", "fetch", "upstream", fmt.Sprintf("pull/%s/head:%s", idx, branch)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.Branch().ParseAndRun(ctx, []string{"--base", branch, branch}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if *ideFlag {
|
||||||
|
return h.IDE().ParseAndRun(ctx, []string{branch})
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"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 {
|
||||||
|
return filepath.Join(c.Base, ".workspace")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) WorkspaceBranch(name string) string {
|
||||||
|
return filepath.Join(c.Workspace(), name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) IsInit() bool {
|
||||||
|
return c.Base != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) Branches() ([]string, error) {
|
||||||
|
dirs, err := os.ReadDir(c.Workspace())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
branches := make([]string, 0, len(dirs))
|
||||||
|
for _, dir := range dirs {
|
||||||
|
if !dir.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
branches = append(branches, dir.Name())
|
||||||
|
}
|
||||||
|
return branches, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func path() (string, error) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Load() (*Config, error) {
|
||||||
|
cfgPath, err := path()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fi, err := os.Open(cfgPath)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, fs.ErrNotExist) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fi, err = os.Create(cfgPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := fi.WriteString(`{}`); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := fi.Seek(0, 0); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer fi.Close()
|
||||||
|
|
||||||
|
var cfg Config
|
||||||
|
return &cfg, json.NewDecoder(fi).Decode(&cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Save(c Config) error {
|
||||||
|
cfgPath, err := path()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fi, err := os.Create(cfgPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer fi.Close()
|
||||||
|
return json.NewEncoder(fi).Encode(c)
|
||||||
|
}
|
|
@ -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
|
||||||
|
]
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
//go:build generate
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"go.jolheiser.com/git-ea/cmd"
|
||||||
|
|
||||||
|
"go.jolheiser.com/ffmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:generate go run ffmd.go
|
||||||
|
func main() {
|
||||||
|
c, err := cmd.New()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
docs, err := ffmd.Command(c)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fi, err := os.Create("DOCS.md")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer fi.Close()
|
||||||
|
|
||||||
|
if _, err := fi.WriteString(docs); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
2
go.mod
2
go.mod
|
@ -7,6 +7,8 @@ require (
|
||||||
github.com/go-git/go-git/v5 v5.4.2
|
github.com/go-git/go-git/v5 v5.4.2
|
||||||
github.com/peterbourgon/ff/v3 v3.3.0
|
github.com/peterbourgon/ff/v3 v3.3.0
|
||||||
github.com/rs/zerolog v1.27.0
|
github.com/rs/zerolog v1.27.0
|
||||||
|
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
|
||||||
|
go.jolheiser.com/ffmd v0.0.3
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|
10
go.sum
10
go.sum
|
@ -1,5 +1,6 @@
|
||||||
github.com/AlecAivazis/survey/v2 v2.3.5 h1:A8cYupsAZkjaUmhtTYv3sSqc7LO5mp1XDfqe5E/9wRQ=
|
github.com/AlecAivazis/survey/v2 v2.3.5 h1:A8cYupsAZkjaUmhtTYv3sSqc7LO5mp1XDfqe5E/9wRQ=
|
||||||
github.com/AlecAivazis/survey/v2 v2.3.5/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI=
|
github.com/AlecAivazis/survey/v2 v2.3.5/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
|
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
|
||||||
github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk=
|
github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk=
|
||||||
github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
|
github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
|
||||||
|
@ -56,8 +57,9 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A=
|
|
||||||
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
|
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
|
||||||
|
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
|
||||||
|
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||||
|
@ -69,6 +71,8 @@ github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyex
|
||||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
|
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
|
||||||
|
github.com/peterbourgon/ff/v3 v3.1.2/go.mod h1:XNJLY8EIl6MjMVjBS4F0+G0LYoAqs0DTa4rmHHukKDE=
|
||||||
github.com/peterbourgon/ff/v3 v3.3.0 h1:PaKe7GW8orVFh8Unb5jNHS+JZBwWUMa2se0HM6/BI24=
|
github.com/peterbourgon/ff/v3 v3.3.0 h1:PaKe7GW8orVFh8Unb5jNHS+JZBwWUMa2se0HM6/BI24=
|
||||||
github.com/peterbourgon/ff/v3 v3.3.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ=
|
github.com/peterbourgon/ff/v3 v3.3.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
@ -82,6 +86,8 @@ github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Q
|
||||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||||
|
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
|
||||||
|
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
@ -91,6 +97,8 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI=
|
github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI=
|
||||||
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
|
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
|
||||||
|
go.jolheiser.com/ffmd v0.0.3 h1:QSKpK0cEZlAswGZuzr9b0dvHG+2HQd362k1DRDwMfrY=
|
||||||
|
go.jolheiser.com/ffmd v0.0.3/go.mod h1:NLhcXZqO+dwvQ2/X9z3TUA3gORxUuRlgOjKC931vCZ4=
|
||||||
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg=
|
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg=
|
||||||
|
|
19
main.go
19
main.go
|
@ -6,29 +6,16 @@ import (
|
||||||
|
|
||||||
"go.jolheiser.com/git-ea/cmd"
|
"go.jolheiser.com/git-ea/cmd"
|
||||||
|
|
||||||
"github.com/peterbourgon/ff/v3/ffcli"
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Version = "x.y.z"
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
||||||
|
|
||||||
c := &ffcli.Command{
|
c, err := cmd.New()
|
||||||
Name: "ea",
|
if err != nil {
|
||||||
ShortUsage: "ea <cmd>",
|
log.Fatal().Err(err).Msg("could not initialize command")
|
||||||
ShortHelp: "ea is the base command",
|
|
||||||
Subcommands: []*ffcli.Command{
|
|
||||||
cmd.Backport,
|
|
||||||
cmd.Branch,
|
|
||||||
cmd.Frontport,
|
|
||||||
},
|
|
||||||
Exec: func(_ context.Context, _ []string) error {
|
|
||||||
log.Info().Msgf("git-ea v%s", Version)
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.ParseAndRun(context.Background(), os.Args[1:]); err != nil {
|
if err := c.ParseAndRun(context.Background(), os.Args[1:]); err != nil {
|
||||||
|
|
Loading…
Reference in New Issue