support create agit flow pull request (#867)

while looks the alibaba has not maintain
[`git-repo-go`](https://github.com/alibaba/git-repo-go/)
tool, to make agit flow pull requst can be create quickly.
add creating agit flow pull request feature
in tea tool

example:

```SHELL
tea pulls create --agit --remote=origin --topic=test-topic
--title="hello world" --description="test1
test 2
test 3"
```

Signed-off-by: a1012112796 <1012112796@qq.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/867
Co-authored-by: a1012112796 <1012112796@qq.com>
Co-committed-by: a1012112796 <1012112796@qq.com>
This commit is contained in:
a1012112796
2026-02-03 20:36:04 +00:00
committed by techknowlogick
parent 82d8a14c73
commit 0d5bf60632
6 changed files with 209 additions and 0 deletions

View File

@@ -37,6 +37,14 @@ var CmdPullsCreate = cli.Command{
Usage: "Enable maintainers to push to the base branch of created pull", Usage: "Enable maintainers to push to the base branch of created pull",
Value: true, Value: true,
}, },
&cli.BoolFlag{
Name: "agit",
Usage: "Create an agit flow pull request",
},
&cli.StringFlag{
Name: "topic",
Usage: "Topic name for agit flow pull request",
},
}, flags.IssuePRCreateFlags...), }, flags.IssuePRCreateFlags...),
} }
@@ -61,6 +69,18 @@ func runPullsCreate(_ stdctx.Context, cmd *cli.Command) error {
return err return err
} }
if ctx.Bool("agit") {
return task.CreateAgitFlowPull(
ctx,
ctx.String("remote"),
ctx.String("head"),
ctx.String("base"),
ctx.String("topic"),
opts,
interact.PromptPassword,
)
}
var allowMaintainerEdits *bool var allowMaintainerEdits *bool
if ctx.IsSet("allow-maintainer-edits") { if ctx.IsSet("allow-maintainer-edits") {
allowMaintainerEdits = gitea.OptionalBool(ctx.Bool("allow-maintainer-edits")) allowMaintainerEdits = gitea.OptionalBool(ctx.Bool("allow-maintainer-edits"))

View File

@@ -345,6 +345,8 @@ Deletes local & remote feature-branches for a closed pull request
Create a pull-request Create a pull-request
**--agit**: Create an agit flow pull request
**--allow-maintainer-edits, --edits**: Enable maintainers to push to the base branch of created pull **--allow-maintainer-edits, --edits**: Enable maintainers to push to the base branch of created pull
**--assignees, -a**="": Comma-separated list of usernames to assign **--assignees, -a**="": Comma-separated list of usernames to assign
@@ -371,6 +373,8 @@ Create a pull-request
**--title, -t**="": **--title, -t**="":
**--topic**="": Topic name for agit flow pull request
### close ### close
Change state of one or more pull requests to 'closed' Change state of one or more pull requests to 'closed'

View File

@@ -4,8 +4,10 @@
package git package git
import ( import (
"encoding/base64"
"fmt" "fmt"
"strings" "strings"
"unicode"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
git_config "github.com/go-git/go-git/v5/config" git_config "github.com/go-git/go-git/v5/config"
@@ -247,3 +249,47 @@ func (r TeaRepo) TeaGetCurrentBranchNameAndSHA() (string, string, error) {
return localHead.Name().Short(), localHead.Hash().String(), nil return localHead.Name().Short(), localHead.Hash().String(), nil
} }
// PushToCreatAgitFlowPR pushes the given head to the refs/for/<base>/<topic> ref on the remote to create an agit flow PR.
func (r TeaRepo) PushToCreatAgitFlowPR(remoteName, head, base, topic, title, description string, auth git_transport.AuthMethod) error {
if !strings.HasPrefix(head, "refs/") {
head = "refs/heads/" + head
}
ref := fmt.Sprintf("%s:refs/for/%s/%s", head, base, topic)
pushOptions := make(map[string]string)
if len(title) > 0 {
pushOptions["title"] = b64Encode(title)
}
if len(description) > 0 {
pushOptions["description"] = b64Encode(description)
}
opts := &git.PushOptions{
RemoteName: remoteName,
RefSpecs: []git_config.RefSpec{git_config.RefSpec(ref)},
Options: pushOptions,
Auth: auth,
}
return r.Push(opts)
}
// b64Encode implements base64 encode for string if necessary.
func b64Encode(s string) string {
if strings.Contains(s, "\n") || !isASCII(s) {
return "{base64}" + base64.StdEncoding.EncodeToString([]byte(s))
}
return s
}
// isASCII indicates string contains only ASCII.
func isASCII(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] > unicode.MaxASCII {
return false
}
}
return true
}

View File

@@ -4,6 +4,8 @@
package git package git
import ( import (
"net/url"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
) )
@@ -33,3 +35,13 @@ func RepoFromPath(path string) (*TeaRepo, error) {
return &TeaRepo{repo}, nil return &TeaRepo{repo}, nil
} }
// RemoteURL returns the URL of the given remote
func (r TeaRepo) RemoteURL(remoteName string) (*url.URL, error) {
remote, err := r.Remote(remoteName)
if err != nil {
return nil, err
}
return url.Parse(remote.Config().URLs[0])
}

View File

@@ -7,6 +7,7 @@ import (
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
"code.gitea.io/tea/modules/context" "code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/task" "code.gitea.io/tea/modules/task"
"code.gitea.io/tea/modules/theme"
"github.com/charmbracelet/huh" "github.com/charmbracelet/huh"
) )
@@ -16,6 +17,8 @@ func CreatePull(ctx *context.TeaContext) (err error) {
var ( var (
base, head string base, head string
allowMaintainerEdits = true allowMaintainerEdits = true
agit bool
) )
// owner, repo // owner, repo
@@ -37,6 +40,66 @@ func CreatePull(ctx *context.TeaContext) (err error) {
} }
} }
if err := huh.NewConfirm().
Title("Do you want to create an agit flow pull request?").
Value(&agit).
WithTheme(theme.GetTheme()).
Run(); err != nil {
return err
}
if agit {
var (
topic string
baseRemote string
)
topic = headBranch
head = "HEAD"
baseRemote = "origin"
if err := huh.NewForm(
huh.NewGroup(
huh.NewInput().
Title("Target branch:").
Value(&base).
Validate(huh.ValidateNotEmpty()),
huh.NewInput().
Title("Source repo remote:").
Value(&baseRemote),
huh.NewInput().
Title("Topic branch:").
Value(&topic).
Validate(validator),
huh.NewInput().
Title("Head branch:").
Value(&head).
Validate(validator),
),
).Run(); err != nil {
return err
}
opts := gitea.CreateIssueOption{Title: task.GetDefaultPRTitle(head)}
if err = promptIssueProperties(ctx.Login, ctx.Owner, ctx.Repo, &opts); err != nil {
return err
}
return task.CreateAgitFlowPull(
ctx,
baseRemote,
head,
base,
topic,
&opts,
PromptPassword,
)
}
if err := huh.NewForm( if err := huh.NewForm(
huh.NewGroup( huh.NewGroup(
huh.NewInput(). huh.NewInput().

View File

@@ -153,3 +153,67 @@ func GetDefaultPRTitle(header string) string {
return title return title
} }
// CreateAgitFlowPull creates a agit flow PR in the given repo and prints the result
func CreateAgitFlowPull(ctx *context.TeaContext, remote, head, base, topic string,
opts *gitea.CreateIssueOption,
callback func(string) (string, error)) (err error) {
// default is default branch
if len(base) == 0 {
base, err = GetDefaultPRBase(ctx.Login, ctx.Owner, ctx.Repo)
if err != nil {
return err
}
}
// default is current one
if len(head) == 0 {
if ctx.LocalRepo == nil {
return fmt.Errorf("no local git repo detected, please specify topic branch")
}
headOwner, headBranch, err := GetDefaultPRHead(ctx.LocalRepo)
if err != nil {
return err
}
head = GetHeadSpec(headOwner, headBranch, ctx.Owner)
}
if len(remote) == 0 {
return fmt.Errorf("remote is required for agit flow PR")
}
if len(topic) == 0 {
topic = head
}
if head == base || topic == base {
return fmt.Errorf("can't create PR from %s to %s", topic, base)
}
// default is head branch name
if len(opts.Title) == 0 {
opts.Title = GetDefaultPRTitle(head)
}
// title is required
if len(opts.Title) == 0 {
return fmt.Errorf("title is required")
}
localRepo, err := local_git.RepoForWorkdir()
if err != nil {
return err
}
url, err := localRepo.RemoteURL(remote)
if err != nil {
return err
}
auth, err := local_git.GetAuthForURL(url, ctx.Login.Token, ctx.Login.SSHKey, callback)
if err != nil {
return err
}
return localRepo.PushToCreatAgitFlowPR(remote, head, base, topic, opts.Title, opts.Body, auth)
}