diff --git a/cmd/pulls/create.go b/cmd/pulls/create.go
index b1dd6a3..bdeb879 100644
--- a/cmd/pulls/create.go
+++ b/cmd/pulls/create.go
@@ -37,6 +37,14 @@ var CmdPullsCreate = cli.Command{
Usage: "Enable maintainers to push to the base branch of created pull",
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...),
}
@@ -61,6 +69,18 @@ func runPullsCreate(_ stdctx.Context, cmd *cli.Command) error {
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
if ctx.IsSet("allow-maintainer-edits") {
allowMaintainerEdits = gitea.OptionalBool(ctx.Bool("allow-maintainer-edits"))
diff --git a/docs/CLI.md b/docs/CLI.md
index 59467dc..2bca4e0 100644
--- a/docs/CLI.md
+++ b/docs/CLI.md
@@ -345,6 +345,8 @@ Deletes local & remote feature-branches for a closed 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
**--assignees, -a**="": Comma-separated list of usernames to assign
@@ -371,6 +373,8 @@ Create a pull-request
**--title, -t**="":
+**--topic**="": Topic name for agit flow pull request
+
### close
Change state of one or more pull requests to 'closed'
diff --git a/modules/git/branch.go b/modules/git/branch.go
index 2f2bc79..7963c2e 100644
--- a/modules/git/branch.go
+++ b/modules/git/branch.go
@@ -4,8 +4,10 @@
package git
import (
+ "encoding/base64"
"fmt"
"strings"
+ "unicode"
"github.com/go-git/go-git/v5"
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
}
+
+// PushToCreatAgitFlowPR pushes the given head to the refs/for// 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
+}
diff --git a/modules/git/repo.go b/modules/git/repo.go
index 8baec1f..938e4e6 100644
--- a/modules/git/repo.go
+++ b/modules/git/repo.go
@@ -4,6 +4,8 @@
package git
import (
+ "net/url"
+
"github.com/go-git/go-git/v5"
)
@@ -33,3 +35,13 @@ func RepoFromPath(path string) (*TeaRepo, error) {
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])
+}
diff --git a/modules/interact/pull_create.go b/modules/interact/pull_create.go
index 6233405..802d6e6 100644
--- a/modules/interact/pull_create.go
+++ b/modules/interact/pull_create.go
@@ -7,6 +7,7 @@ import (
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/task"
+ "code.gitea.io/tea/modules/theme"
"github.com/charmbracelet/huh"
)
@@ -16,6 +17,8 @@ func CreatePull(ctx *context.TeaContext) (err error) {
var (
base, head string
allowMaintainerEdits = true
+
+ agit bool
)
// 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(
huh.NewGroup(
huh.NewInput().
diff --git a/modules/task/pull_create.go b/modules/task/pull_create.go
index b07a28a..424854d 100644
--- a/modules/task/pull_create.go
+++ b/modules/task/pull_create.go
@@ -153,3 +153,67 @@ func GetDefaultPRTitle(header string) string {
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)
+}