From db7b332b98f196e2c1f628ddc9c2ce78cfe93a0f Mon Sep 17 00:00:00 2001 From: Norwin Roosen Date: Mon, 7 Dec 2020 23:22:48 +0100 Subject: [PATCH 1/6] refactor pull create into task & interact module --- cmd/pulls/create.go | 117 ++++---------------------------- modules/interact/pull_create.go | 24 +++++++ modules/task/pull_create.go | 117 ++++++++++++++++++++++++++++++++ 3 files changed, 156 insertions(+), 102 deletions(-) create mode 100644 modules/interact/pull_create.go create mode 100644 modules/task/pull_create.go diff --git a/cmd/pulls/create.go b/cmd/pulls/create.go index 7f91c0a..5ca8a3c 100644 --- a/cmd/pulls/create.go +++ b/cmd/pulls/create.go @@ -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 ") - } - - 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) - - 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"), + ) } diff --git a/modules/interact/pull_create.go b/modules/interact/pull_create.go new file mode 100644 index 0000000..4145778 --- /dev/null +++ b/modules/interact/pull_create.go @@ -0,0 +1,24 @@ +// 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 ( + "code.gitea.io/tea/modules/config" + "code.gitea.io/tea/modules/task" +) + +// CreatePull interactively creates a PR +func CreatePull(login *config.Login, ownerHint, repoHint string) error { + var owner, repo, base, head, title, description string + + return task.CreatePull( + login, + owner, + repo, + base, + head, + title, + description) +} diff --git a/modules/task/pull_create.go b/modules/task/pull_create.go new file mode 100644 index 0000000..4d52601 --- /dev/null +++ b/modules/task/pull_create.go @@ -0,0 +1,117 @@ +// 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" +) + +// PullCreate creates a PR in the given repo and prints the result +func CreatePull(login *config.Login, repoOwner, repoName, base, head, title, description string) error { + client := login.Client() + + repo, _, err := client.GetRepo(repoOwner, repoName) + if err != nil { + log.Fatal("could not fetch repo meta: ", err) + } + + // 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 = repo.DefaultBranch + } + + // 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 ") + } + + 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 + } + } + + // 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(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) + + fmt.Println(pr.HTMLURL) + + return err +} -- 2.40.1 From e9ac2048869f9bd412c59eac098982eb74ad07d7 Mon Sep 17 00:00:00 2001 From: Norwin Roosen Date: Mon, 7 Dec 2020 23:25:39 +0100 Subject: [PATCH 2/6] avoid creation of invalid PRs --- modules/task/pull_create.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/task/pull_create.go b/modules/task/pull_create.go index 4d52601..f9f31af 100644 --- a/modules/task/pull_create.go +++ b/modules/task/pull_create.go @@ -82,6 +82,11 @@ func CreatePull(login *config.Login, repoOwner, repoName, base, head, title, des } } + // head & base may not be the same + if head == base { + return fmt.Errorf("Can't create PR from %s to %s\n", head, base) + } + // default is head branch name if len(title) == 0 { title = head @@ -94,8 +99,7 @@ func CreatePull(login *config.Login, repoOwner, repoName, base, head, title, des } // title is required if len(title) == 0 { - fmt.Printf("Title is required") - return nil + return fmt.Errorf("Title is required") } pr, _, err := client.CreatePullRequest(repoOwner, repoName, gitea.CreatePullRequestOption{ -- 2.40.1 From 84e11b9085ee45d891a583ec9d2eadb28a73181e Mon Sep 17 00:00:00 2001 From: Norwin Roosen Date: Tue, 8 Dec 2020 01:06:20 +0100 Subject: [PATCH 3/6] refactor task.CreatePull to make functionality reusable in interact module --- modules/task/pull_create.go | 118 ++++++++++++++++++++++-------------- 1 file changed, 73 insertions(+), 45 deletions(-) diff --git a/modules/task/pull_create.go b/modules/task/pull_create.go index f9f31af..fd13dfd 100644 --- a/modules/task/pull_create.go +++ b/modules/task/pull_create.go @@ -20,12 +20,6 @@ import ( // PullCreate creates a PR in the given repo and prints the result func CreatePull(login *config.Login, repoOwner, repoName, base, head, title, description string) error { - client := login.Client() - - repo, _, err := client.GetRepo(repoOwner, repoName) - if err != nil { - log.Fatal("could not fetch repo meta: ", err) - } // open local git repo localRepo, err := local_git.RepoForWorkdir() @@ -42,44 +36,20 @@ func CreatePull(login *config.Login, repoOwner, repoName, base, head, title, des // default is default branch if len(base) == 0 { - base = repo.DefaultBranch + base, err = GetDefaultPRBase(login, repoOwner, repoName) + if err != nil { + return err + } } // default is current one if len(head) == 0 { - headBranch, err := localRepo.Head() + headOwner, headBranch, err := GetDefaultPRHead(localRepo) 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) + return 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 ") - } - - 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 - } + head = GetHeadSpec(headOwner, headBranch, repoOwner) } // head & base may not be the same @@ -89,20 +59,14 @@ func CreatePull(login *config.Login, repoOwner, repoName, base, head, title, des // 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 = GetDefaultPRTitle(head) } // title is required if len(title) == 0 { return fmt.Errorf("Title is required") } - pr, _, err := client.CreatePullRequest(repoOwner, repoName, gitea.CreatePullRequestOption{ + pr, _, err := login.Client().CreatePullRequest(repoOwner, repoName, gitea.CreatePullRequestOption{ Head: head, Base: base, Title: title, @@ -119,3 +83,67 @@ func CreatePull(login *config.Login, repoOwner, repoName, base, head, title, des return err } + +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: ", 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 ") + 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 +} + +func GetHeadSpec(owner, branch, baseOwner string) string { + if len(owner) != 0 && owner != baseOwner { + return fmt.Sprintf("%s:%s", owner, branch) + } else { + return branch + } +} + +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 +} -- 2.40.1 From 57bd2ffc429422c39f784d66fd516d5331e3f27b Mon Sep 17 00:00:00 2001 From: Norwin Roosen Date: Tue, 8 Dec 2020 01:09:44 +0100 Subject: [PATCH 4/6] implement interactive.CreatePull --- modules/interact/pull_create.go | 113 +++++++++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 2 deletions(-) diff --git a/modules/interact/pull_create.go b/modules/interact/pull_create.go index 4145778..a1ec947 100644 --- a/modules/interact/pull_create.go +++ b/modules/interact/pull_create.go @@ -5,13 +5,83 @@ 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, ownerHint, repoHint string) error { - var owner, repo, base, head, title, description string +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, @@ -22,3 +92,42 @@ func CreatePull(login *config.Login, ownerHint, repoHint string) error { 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 / 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 +} -- 2.40.1 From 13b2566291f2c3710c16731ca75f43464cab3efc Mon Sep 17 00:00:00 2001 From: Norwin Roosen Date: Tue, 8 Dec 2020 01:11:27 +0100 Subject: [PATCH 5/6] lint --- modules/task/pull_create.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/task/pull_create.go b/modules/task/pull_create.go index fd13dfd..f3b3b81 100644 --- a/modules/task/pull_create.go +++ b/modules/task/pull_create.go @@ -104,7 +104,7 @@ func GetDefaultPRHead(localRepo *local_git.TeaRepo) (owner, branch string, err e remote, err := localRepo.TeaFindBranchRemote("", sha) if err != nil { - err = fmt.Errorf("could not determine remote for current branch: ", err) + err = fmt.Errorf("could not determine remote for current branch: %s", err) return } -- 2.40.1 From eee204a78ebc9dc35b76986beb881cc86502542e Mon Sep 17 00:00:00 2001 From: Norwin Roosen Date: Tue, 8 Dec 2020 01:16:22 +0100 Subject: [PATCH 6/6] more linting --- modules/task/pull_create.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/modules/task/pull_create.go b/modules/task/pull_create.go index f3b3b81..50a416d 100644 --- a/modules/task/pull_create.go +++ b/modules/task/pull_create.go @@ -18,7 +18,7 @@ import ( "github.com/go-git/go-git/v5" ) -// PullCreate creates a PR in the given repo and prints the result +// 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 @@ -54,7 +54,7 @@ func CreatePull(login *config.Login, repoOwner, repoName, base, head, title, des // head & base may not be the same if head == base { - return fmt.Errorf("Can't create PR from %s to %s\n", head, base) + return fmt.Errorf("can't create PR from %s to %s", head, base) } // default is head branch name @@ -84,6 +84,7 @@ func CreatePull(login *config.Login, repoOwner, repoName, base, head, title, des 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 { @@ -129,14 +130,15 @@ func GetDefaultPRHead(localRepo *local_git.TeaRepo) (owner, branch string, err e 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) - } else { - return 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, ":") { -- 2.40.1