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:
Norwin 2020-12-09 05:41:50 +08:00 committed by 6543
parent 6d6922efa6
commit adb2382aa5
3 changed files with 299 additions and 102 deletions

View File

@ -5,18 +5,11 @@
package pulls
import (
"fmt"
"log"
"strings"
"code.gitea.io/tea/cmd/flags"
"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"
"code.gitea.io/tea/modules/interact"
"code.gitea.io/tea/modules/task"
"code.gitea.io/sdk/gitea"
"github.com/go-git/go-git/v5"
"github.com/urfave/cli/v2"
)
@ -51,100 +44,20 @@ var CmdPullsCreate = cli.Command{
func runPullsCreate(ctx *cli.Context) error {
login, ownerArg, repoArg := config.InitCommand(flags.GlobalRepoValue, flags.GlobalLoginValue, flags.GlobalRemoteValue)
client := login.Client()
repo, _, err := client.GetRepo(ownerArg, repoArg)
if err != nil {
log.Fatal("could not fetch repo meta: ", err)
// no args -> interactive mode
if ctx.NumFlags() == 0 {
return interact.CreatePull(login, ownerArg, repoArg)
}
// 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())
}
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
// else use args to create PR
return task.CreatePull(
login,
ownerArg,
repoArg,
ctx.String("base"),
ctx.String("head"),
ctx.String("title"),
ctx.String("description"),
)
}

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