mirror of
https://gitea.com/gitea/tea.git
synced 2024-11-22 02:21:36 +01:00
Add interactive mode for tea pr create
(#279)
refactor pull create into task & interact module avoid creation of invalid PRs refactor task.CreatePull to make functionality reusable in interact module implement interactive.CreatePull Co-authored-by: Norwin Roosen <git@nroo.de> Reviewed-on: https://gitea.com/gitea/tea/pulls/279 Reviewed-by: 6543 <6543@obermui.de> Reviewed-by: techknowlogick <techknowlogick@gitea.io> Co-Authored-By: Norwin <noerw@noreply.gitea.io> Co-Committed-By: Norwin <noerw@noreply.gitea.io>
This commit is contained in:
parent
6d6922efa6
commit
adb2382aa5
@ -5,18 +5,11 @@
|
|||||||
package pulls
|
package pulls
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"code.gitea.io/tea/cmd/flags"
|
"code.gitea.io/tea/cmd/flags"
|
||||||
"code.gitea.io/tea/modules/config"
|
"code.gitea.io/tea/modules/config"
|
||||||
local_git "code.gitea.io/tea/modules/git"
|
"code.gitea.io/tea/modules/interact"
|
||||||
"code.gitea.io/tea/modules/print"
|
"code.gitea.io/tea/modules/task"
|
||||||
"code.gitea.io/tea/modules/utils"
|
|
||||||
|
|
||||||
"code.gitea.io/sdk/gitea"
|
|
||||||
"github.com/go-git/go-git/v5"
|
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -51,100 +44,20 @@ var CmdPullsCreate = cli.Command{
|
|||||||
|
|
||||||
func runPullsCreate(ctx *cli.Context) error {
|
func runPullsCreate(ctx *cli.Context) error {
|
||||||
login, ownerArg, repoArg := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue)
|
login, ownerArg, repoArg := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue)
|
||||||
client := login.Client()
|
|
||||||
|
|
||||||
repo, _, err := client.GetRepo(ownerArg, repoArg)
|
// no args -> interactive mode
|
||||||
if err != nil {
|
if ctx.NumFlags() == 0 {
|
||||||
log.Fatal("could not fetch repo meta: ", err)
|
return interact.CreatePull(login, ownerArg, repoArg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// open local git repo
|
// else use args to create PR
|
||||||
localRepo, err := local_git.RepoForWorkdir()
|
return task.CreatePull(
|
||||||
if err != nil {
|
login,
|
||||||
log.Fatal("could not open local repo: ", err)
|
ownerArg,
|
||||||
}
|
repoArg,
|
||||||
|
ctx.String("base"),
|
||||||
// push if possible
|
ctx.String("head"),
|
||||||
log.Println("git push")
|
ctx.String("title"),
|
||||||
err = localRepo.Push(&git.PushOptions{})
|
ctx.String("description"),
|
||||||
if err != nil && err != git.NoErrAlreadyUpToDate {
|
)
|
||||||
log.Printf("Error occurred during 'git push':\n%s\n", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
base := ctx.String("base")
|
|
||||||
// default is default branch
|
|
||||||
if len(base) == 0 {
|
|
||||||
base = repo.DefaultBranch
|
|
||||||
}
|
|
||||||
|
|
||||||
head := ctx.String("head")
|
|
||||||
// default is current one
|
|
||||||
if len(head) == 0 {
|
|
||||||
headBranch, err := localRepo.Head()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
sha := headBranch.Hash().String()
|
|
||||||
|
|
||||||
remote, err := localRepo.TeaFindBranchRemote("", sha)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("could not determine remote for current branch: ", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if remote == nil {
|
|
||||||
// if no remote branch is found for the local hash, we abort:
|
|
||||||
// user has probably not configured a remote for the local branch,
|
|
||||||
// or local branch does not represent remote state.
|
|
||||||
log.Fatal("no matching remote found for this branch. try git push -u <remote> <branch>")
|
|
||||||
}
|
|
||||||
|
|
||||||
branchName, err := localRepo.TeaGetCurrentBranchName()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
url, err := local_git.ParseURL(remote.Config().URLs[0])
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
owner, _ := utils.GetOwnerAndRepo(strings.TrimLeft(url.Path, "/"), "")
|
|
||||||
if owner != repo.Owner.UserName {
|
|
||||||
head = fmt.Sprintf("%s:%s", owner, branchName)
|
|
||||||
} else {
|
|
||||||
head = branchName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
title := ctx.String("title")
|
|
||||||
// default is head branch name
|
|
||||||
if len(title) == 0 {
|
|
||||||
title = head
|
|
||||||
if strings.Contains(title, ":") {
|
|
||||||
title = strings.SplitN(title, ":", 2)[1]
|
|
||||||
}
|
|
||||||
title = strings.Replace(title, "-", " ", -1)
|
|
||||||
title = strings.Replace(title, "_", " ", -1)
|
|
||||||
title = strings.Title(strings.ToLower(title))
|
|
||||||
}
|
|
||||||
// title is required
|
|
||||||
if len(title) == 0 {
|
|
||||||
fmt.Printf("Title is required")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
pr, _, err := client.CreatePullRequest(ownerArg, repoArg, gitea.CreatePullRequestOption{
|
|
||||||
Head: head,
|
|
||||||
Base: base,
|
|
||||||
Title: title,
|
|
||||||
Body: ctx.String("description"),
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("could not create PR from %s to %s:%s: %s", head, ownerArg, base, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
print.PullDetails(pr, nil)
|
|
||||||
|
|
||||||
fmt.Println(pr.HTMLURL)
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
133
modules/interact/pull_create.go
Normal file
133
modules/interact/pull_create.go
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package interact
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/tea/modules/config"
|
||||||
|
"code.gitea.io/tea/modules/git"
|
||||||
|
"code.gitea.io/tea/modules/task"
|
||||||
|
|
||||||
|
"github.com/AlecAivazis/survey/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreatePull interactively creates a PR
|
||||||
|
func CreatePull(login *config.Login, owner, repo string) error {
|
||||||
|
var base, head, title, description string
|
||||||
|
|
||||||
|
// owner, repo
|
||||||
|
owner, repo, err := promptRepoSlug(owner, repo)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// base
|
||||||
|
baseBranch, err := task.GetDefaultPRBase(login, owner, repo)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
promptI := &survey.Input{Message: "Target branch [" + baseBranch + "]:"}
|
||||||
|
if err := survey.AskOne(promptI, &base); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(base) == 0 {
|
||||||
|
base = baseBranch
|
||||||
|
}
|
||||||
|
|
||||||
|
// head
|
||||||
|
localRepo, err := git.RepoForWorkdir()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
promptOpts := survey.WithValidator(survey.Required)
|
||||||
|
headOwner, headBranch, err := task.GetDefaultPRHead(localRepo)
|
||||||
|
if err == nil {
|
||||||
|
promptOpts = nil
|
||||||
|
}
|
||||||
|
var headOwnerInput, headBranchInput string
|
||||||
|
promptI = &survey.Input{Message: "Source repo owner [" + headOwner + "]:"}
|
||||||
|
if err := survey.AskOne(promptI, &headOwnerInput); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(headOwnerInput) != 0 {
|
||||||
|
headOwner = headOwnerInput
|
||||||
|
}
|
||||||
|
promptI = &survey.Input{Message: "Source branch [" + headBranch + "]:"}
|
||||||
|
if err := survey.AskOne(promptI, &headBranchInput, promptOpts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(headBranchInput) != 0 {
|
||||||
|
headBranch = headBranchInput
|
||||||
|
}
|
||||||
|
|
||||||
|
head = task.GetHeadSpec(headOwner, headBranch, owner)
|
||||||
|
|
||||||
|
// title
|
||||||
|
title = task.GetDefaultPRTitle(head)
|
||||||
|
promptOpts = survey.WithValidator(survey.Required)
|
||||||
|
if len(title) != 0 {
|
||||||
|
promptOpts = nil
|
||||||
|
}
|
||||||
|
promptI = &survey.Input{Message: "PR title [" + title + "]:"}
|
||||||
|
if err := survey.AskOne(promptI, &title, promptOpts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// description
|
||||||
|
promptM := &survey.Multiline{Message: "PR description:"}
|
||||||
|
if err := survey.AskOne(promptM, &description); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return task.CreatePull(
|
||||||
|
login,
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
base,
|
||||||
|
head,
|
||||||
|
title,
|
||||||
|
description)
|
||||||
|
}
|
||||||
|
|
||||||
|
func promptRepoSlug(defaultOwner, defaultRepo string) (owner, repo string, err error) {
|
||||||
|
prompt := "Target repo:"
|
||||||
|
required := true
|
||||||
|
if len(defaultOwner) != 0 && len(defaultRepo) != 0 {
|
||||||
|
prompt = fmt.Sprintf("Target repo [%s/%s]:", defaultOwner, defaultRepo)
|
||||||
|
required = false
|
||||||
|
}
|
||||||
|
var repoSlug string
|
||||||
|
|
||||||
|
owner = defaultOwner
|
||||||
|
repo = defaultRepo
|
||||||
|
|
||||||
|
err = survey.AskOne(
|
||||||
|
&survey.Input{Message: prompt},
|
||||||
|
&repoSlug,
|
||||||
|
survey.WithValidator(func(input interface{}) error {
|
||||||
|
if str, ok := input.(string); ok {
|
||||||
|
if !required && len(str) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
split := strings.Split(str, "/")
|
||||||
|
if len(split) != 2 || len(split[0]) == 0 || len(split[1]) == 0 {
|
||||||
|
return fmt.Errorf("must follow the <owner>/<repo> syntax")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("invalid result type")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err == nil && len(repoSlug) != 0 {
|
||||||
|
repoSlugSplit := strings.Split(repoSlug, "/")
|
||||||
|
owner = repoSlugSplit[0]
|
||||||
|
repo = repoSlugSplit[1]
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
151
modules/task/pull_create.go
Normal file
151
modules/task/pull_create.go
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package task
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
"code.gitea.io/tea/modules/config"
|
||||||
|
local_git "code.gitea.io/tea/modules/git"
|
||||||
|
"code.gitea.io/tea/modules/print"
|
||||||
|
"code.gitea.io/tea/modules/utils"
|
||||||
|
|
||||||
|
"github.com/go-git/go-git/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreatePull creates a PR in the given repo and prints the result
|
||||||
|
func CreatePull(login *config.Login, repoOwner, repoName, base, head, title, description string) error {
|
||||||
|
|
||||||
|
// open local git repo
|
||||||
|
localRepo, err := local_git.RepoForWorkdir()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("could not open local repo: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// push if possible
|
||||||
|
log.Println("git push")
|
||||||
|
err = localRepo.Push(&git.PushOptions{})
|
||||||
|
if err != nil && err != git.NoErrAlreadyUpToDate {
|
||||||
|
log.Printf("Error occurred during 'git push':\n%s\n", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// default is default branch
|
||||||
|
if len(base) == 0 {
|
||||||
|
base, err = GetDefaultPRBase(login, repoOwner, repoName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// default is current one
|
||||||
|
if len(head) == 0 {
|
||||||
|
headOwner, headBranch, err := GetDefaultPRHead(localRepo)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
head = GetHeadSpec(headOwner, headBranch, repoOwner)
|
||||||
|
}
|
||||||
|
|
||||||
|
// head & base may not be the same
|
||||||
|
if head == base {
|
||||||
|
return fmt.Errorf("can't create PR from %s to %s", head, base)
|
||||||
|
}
|
||||||
|
|
||||||
|
// default is head branch name
|
||||||
|
if len(title) == 0 {
|
||||||
|
title = GetDefaultPRTitle(head)
|
||||||
|
}
|
||||||
|
// title is required
|
||||||
|
if len(title) == 0 {
|
||||||
|
return fmt.Errorf("Title is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
pr, _, err := login.Client().CreatePullRequest(repoOwner, repoName, gitea.CreatePullRequestOption{
|
||||||
|
Head: head,
|
||||||
|
Base: base,
|
||||||
|
Title: title,
|
||||||
|
Body: description,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("could not create PR from %s to %s:%s: %s", head, repoOwner, base, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
print.PullDetails(pr, nil)
|
||||||
|
|
||||||
|
fmt.Println(pr.HTMLURL)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefaultPRBase retrieves the default base branch for the given repo
|
||||||
|
func GetDefaultPRBase(login *config.Login, owner, repo string) (string, error) {
|
||||||
|
meta, _, err := login.Client().GetRepo(owner, repo)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("could not fetch repo meta: %s", err)
|
||||||
|
}
|
||||||
|
return meta.DefaultBranch, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefaultPRHead uses the currently checked out branch, checks if
|
||||||
|
// a remote currently holds the commit it points to, extracts the owner
|
||||||
|
// from its URL, and assembles the result to a valid head spec for gitea.
|
||||||
|
func GetDefaultPRHead(localRepo *local_git.TeaRepo) (owner, branch string, err error) {
|
||||||
|
headBranch, err := localRepo.Head()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sha := headBranch.Hash().String()
|
||||||
|
|
||||||
|
remote, err := localRepo.TeaFindBranchRemote("", sha)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("could not determine remote for current branch: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if remote == nil {
|
||||||
|
// if no remote branch is found for the local hash, we abort:
|
||||||
|
// user has probably not configured a remote for the local branch,
|
||||||
|
// or local branch does not represent remote state.
|
||||||
|
err = fmt.Errorf("no matching remote found for this branch. try git push -u <remote> <branch>")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
branch, err = localRepo.TeaGetCurrentBranchName()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
url, err := local_git.ParseURL(remote.Config().URLs[0])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
owner, _ = utils.GetOwnerAndRepo(strings.TrimLeft(url.Path, "/"), "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHeadSpec creates a head string as expected by gitea API
|
||||||
|
func GetHeadSpec(owner, branch, baseOwner string) string {
|
||||||
|
if len(owner) != 0 && owner != baseOwner {
|
||||||
|
return fmt.Sprintf("%s:%s", owner, branch)
|
||||||
|
}
|
||||||
|
return branch
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefaultPRTitle transforms a string like a branchname to a readable text
|
||||||
|
func GetDefaultPRTitle(head string) string {
|
||||||
|
title := head
|
||||||
|
if strings.Contains(title, ":") {
|
||||||
|
title = strings.SplitN(title, ":", 2)[1]
|
||||||
|
}
|
||||||
|
title = strings.Replace(title, "-", " ", -1)
|
||||||
|
title = strings.Replace(title, "_", " ", -1)
|
||||||
|
title = strings.Title(strings.ToLower(title))
|
||||||
|
return title
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user