Merge remote-tracking branch 'origin/master' into issue-update
This commit is contained in:
parent
75a3496bf1
commit
e5af5be940
23 changed files with 1354 additions and 65 deletions
17
README.md
17
README.md
|
|
@ -4,8 +4,25 @@ The #ce-cli team is working on a publicly available CLI tool to reduce the frict
|
|||
|
||||
This tool is an endeavor separate from [github/hub](https://github.com/github/hub), which acts as a proxy to `git`, since our aim is to reimagine from scratch the kind of command line interface to GitHub that would serve our users' interests best.
|
||||
|
||||
# Installation
|
||||
|
||||
_warning, gh is in a very alpha phase_
|
||||
|
||||
`brew install github/gh/gh`
|
||||
|
||||
That's it. You are now ready to use `gh` on the command line. 🥳
|
||||
|
||||
# Process
|
||||
|
||||
- [Demo planning doc](https://docs.google.com/document/d/18ym-_xjFTSXe0-xzgaBn13Su7MEhWfLE5qSNPJV4M0A/edit)
|
||||
- [Weekly tracking issue](https://github.com/github/gh-cli/labels/tracking%20issue)
|
||||
- [Weekly sync notes](https://docs.google.com/document/d/1eUo9nIzXbC1DG26Y3dk9hOceLua2yFlwlvFPZ82MwHg/edit)
|
||||
|
||||
# How to create a release
|
||||
|
||||
This can all be done from your local terminal.
|
||||
|
||||
1. `git tag 'vVERSION_NUMBER' # example git tag 'v0.0.1'`
|
||||
2. `git push origin vVERSION_NUMBER`
|
||||
3. Wait a few minutes for the build to run and CI to pass. Look at the [actions tab](https://github.com/github/gh-cli/actions) to check the progress.
|
||||
4. Go to https://github.com/github/homebrew-gh/releases and look at the release
|
||||
|
|
|
|||
215
api/queries.go
215
api/queries.go
|
|
@ -16,6 +16,93 @@ type PullRequest struct {
|
|||
State string
|
||||
URL string
|
||||
HeadRefName string
|
||||
|
||||
HeadRepositoryOwner struct {
|
||||
Login string
|
||||
}
|
||||
HeadRepository struct {
|
||||
Name string
|
||||
DefaultBranchRef struct {
|
||||
Name string
|
||||
}
|
||||
}
|
||||
IsCrossRepository bool
|
||||
MaintainerCanModify bool
|
||||
|
||||
ReviewDecision string
|
||||
|
||||
Commits struct {
|
||||
Nodes []struct {
|
||||
Commit struct {
|
||||
StatusCheckRollup struct {
|
||||
Contexts struct {
|
||||
Nodes []struct {
|
||||
State string
|
||||
Conclusion string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (pr PullRequest) HeadLabel() string {
|
||||
if pr.IsCrossRepository {
|
||||
return fmt.Sprintf("%s:%s", pr.HeadRepositoryOwner.Login, pr.HeadRefName)
|
||||
}
|
||||
return pr.HeadRefName
|
||||
}
|
||||
|
||||
type PullRequestReviewStatus struct {
|
||||
ChangesRequested bool
|
||||
Approved bool
|
||||
ReviewRequired bool
|
||||
}
|
||||
|
||||
func (pr *PullRequest) ReviewStatus() PullRequestReviewStatus {
|
||||
status := PullRequestReviewStatus{}
|
||||
switch pr.ReviewDecision {
|
||||
case "CHANGES_REQUESTED":
|
||||
status.ChangesRequested = true
|
||||
case "APPROVED":
|
||||
status.Approved = true
|
||||
case "REVIEW_REQUIRED":
|
||||
status.ReviewRequired = true
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
type PullRequestChecksStatus struct {
|
||||
Pending int
|
||||
Failing int
|
||||
Passing int
|
||||
Total int
|
||||
}
|
||||
|
||||
func (pr *PullRequest) ChecksStatus() (summary PullRequestChecksStatus) {
|
||||
if len(pr.Commits.Nodes) == 0 {
|
||||
return
|
||||
}
|
||||
commit := pr.Commits.Nodes[0].Commit
|
||||
for _, c := range commit.StatusCheckRollup.Contexts.Nodes {
|
||||
state := c.State
|
||||
if state == "" {
|
||||
state = c.Conclusion
|
||||
}
|
||||
switch state {
|
||||
case "SUCCESS", "NEUTRAL", "SKIPPED":
|
||||
summary.Passing++
|
||||
case "ERROR", "FAILURE", "CANCELLED", "TIMED_OUT", "ACTION_REQUIRED":
|
||||
summary.Failing++
|
||||
case "EXPECTED", "QUEUED", "PENDING", "IN_PROGRESS":
|
||||
summary.Pending++
|
||||
default:
|
||||
panic(fmt.Errorf("unsupported status: %q", state))
|
||||
}
|
||||
summary.Total++
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type Repo interface {
|
||||
|
|
@ -43,19 +130,46 @@ func PullRequests(client *Client, ghRepo Repo, currentBranch, currentUsername st
|
|||
}
|
||||
|
||||
query := `
|
||||
fragment pr on PullRequest {
|
||||
number
|
||||
title
|
||||
url
|
||||
headRefName
|
||||
}
|
||||
fragment pr on PullRequest {
|
||||
number
|
||||
title
|
||||
url
|
||||
headRefName
|
||||
headRefName
|
||||
headRepositoryOwner {
|
||||
login
|
||||
}
|
||||
isCrossRepository
|
||||
commits(last: 1) {
|
||||
nodes {
|
||||
commit {
|
||||
statusCheckRollup {
|
||||
contexts(last: 100) {
|
||||
nodes {
|
||||
...on StatusContext {
|
||||
state
|
||||
}
|
||||
...on CheckRun {
|
||||
conclusion
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fragment prWithReviews on PullRequest {
|
||||
...pr
|
||||
reviewDecision
|
||||
}
|
||||
|
||||
query($owner: String!, $repo: String!, $headRefName: String!, $viewerQuery: String!, $reviewerQuery: String!, $per_page: Int = 10) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequests(headRefName: $headRefName, states: OPEN, first: 1) {
|
||||
edges {
|
||||
node {
|
||||
...pr
|
||||
...prWithReviews
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -63,7 +177,7 @@ func PullRequests(client *Client, ghRepo Repo, currentBranch, currentUsername st
|
|||
viewerCreated: search(query: $viewerQuery, type: ISSUE, first: $per_page) {
|
||||
edges {
|
||||
node {
|
||||
...pr
|
||||
...prWithReviews
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
|
|
@ -127,6 +241,48 @@ func PullRequests(client *Client, ghRepo Repo, currentBranch, currentUsername st
|
|||
return &payload, nil
|
||||
}
|
||||
|
||||
func PullRequestByNumber(client *Client, ghRepo Repo, number int) (*PullRequest, error) {
|
||||
type response struct {
|
||||
Repository struct {
|
||||
PullRequest PullRequest
|
||||
}
|
||||
}
|
||||
|
||||
query := `
|
||||
query($owner: String!, $repo: String!, $pr_number: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequest(number: $pr_number) {
|
||||
headRefName
|
||||
headRepositoryOwner {
|
||||
login
|
||||
}
|
||||
headRepository {
|
||||
name
|
||||
defaultBranchRef {
|
||||
name
|
||||
}
|
||||
}
|
||||
isCrossRepository
|
||||
maintainerCanModify
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"owner": ghRepo.RepoOwner(),
|
||||
"repo": ghRepo.RepoName(),
|
||||
"pr_number": number,
|
||||
}
|
||||
|
||||
var resp response
|
||||
err := client.GraphQL(query, variables, &resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &resp.Repository.PullRequest, nil
|
||||
}
|
||||
|
||||
func PullRequestsForBranch(client *Client, ghRepo Repo, branch string) ([]PullRequest, error) {
|
||||
type response struct {
|
||||
Repository struct {
|
||||
|
|
@ -173,6 +329,45 @@ func PullRequestsForBranch(client *Client, ghRepo Repo, branch string) ([]PullRe
|
|||
return prs, nil
|
||||
}
|
||||
|
||||
func CreatePullRequest(client *Client, ghRepo Repo, params map[string]interface{}) (*PullRequest, error) {
|
||||
repoID, err := GitHubRepoId(client, ghRepo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := `
|
||||
mutation CreatePullRequest($input: CreatePullRequestInput!) {
|
||||
createPullRequest(input: $input) {
|
||||
pullRequest {
|
||||
url
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
inputParams := map[string]interface{}{
|
||||
"repositoryId": repoID,
|
||||
}
|
||||
for key, val := range params {
|
||||
inputParams[key] = val
|
||||
}
|
||||
variables := map[string]interface{}{
|
||||
"input": inputParams,
|
||||
}
|
||||
|
||||
result := struct {
|
||||
CreatePullRequest struct {
|
||||
PullRequest PullRequest
|
||||
}
|
||||
}{}
|
||||
|
||||
err = client.GraphQL(query, variables, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &result.CreatePullRequest.PullRequest, nil
|
||||
}
|
||||
|
||||
func PullRequestList(client *Client, vars map[string]interface{}, limit int) ([]PullRequest, error) {
|
||||
type response struct {
|
||||
Repository struct {
|
||||
|
|
@ -214,6 +409,10 @@ func PullRequestList(client *Client, vars map[string]interface{}, limit int) ([]
|
|||
state
|
||||
url
|
||||
headRefName
|
||||
headRepositoryOwner {
|
||||
login
|
||||
}
|
||||
isCrossRepository
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ func IssueStatus(client *Client, ghRepo Repo, currentUsername string) (*IssuesPa
|
|||
}
|
||||
query($owner: String!, $repo: String!, $since: DateTime!, $viewer: String!, $per_page: Int = 10) {
|
||||
assigned: repository(owner: $owner, name: $repo) {
|
||||
issues(filterBy: {assignee: $viewer}, first: $per_page, orderBy: {field: CREATED_AT, direction: DESC}) {
|
||||
issues(filterBy: {assignee: $viewer, states: OPEN}, first: $per_page, orderBy: {field: CREATED_AT, direction: DESC}) {
|
||||
edges {
|
||||
node {
|
||||
...issue
|
||||
|
|
@ -101,7 +101,7 @@ func IssueStatus(client *Client, ghRepo Repo, currentUsername string) (*IssuesPa
|
|||
}
|
||||
}
|
||||
mentioned: repository(owner: $owner, name: $repo) {
|
||||
issues(filterBy: {mentioned: $viewer}, first: $per_page, orderBy: {field: CREATED_AT, direction: DESC}) {
|
||||
issues(filterBy: {mentioned: $viewer, states: OPEN}, first: $per_page, orderBy: {field: CREATED_AT, direction: DESC}) {
|
||||
edges {
|
||||
node {
|
||||
...issue
|
||||
|
|
@ -110,7 +110,7 @@ func IssueStatus(client *Client, ghRepo Repo, currentUsername string) (*IssuesPa
|
|||
}
|
||||
}
|
||||
recent: repository(owner: $owner, name: $repo) {
|
||||
issues(filterBy: {since: $since}, first: $per_page, orderBy: {field: CREATED_AT, direction: DESC}) {
|
||||
issues(filterBy: {since: $since, states: OPEN}, first: $per_page, orderBy: {field: CREATED_AT, direction: DESC}) {
|
||||
edges {
|
||||
node {
|
||||
...issue
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ func init() {
|
|||
&cobra.Command{
|
||||
Use: "view <issue-number>",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Short: "Open an issue in the browser",
|
||||
Short: "View an issue in the browser",
|
||||
RunE: issueView,
|
||||
},
|
||||
)
|
||||
|
|
|
|||
144
command/pr.go
144
command/pr.go
|
|
@ -3,9 +3,11 @@ package command
|
|||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
|
||||
"github.com/github/gh-cli/api"
|
||||
"github.com/github/gh-cli/git"
|
||||
"github.com/github/gh-cli/utils"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
|
|
@ -13,6 +15,8 @@ import (
|
|||
|
||||
func init() {
|
||||
RootCmd.AddCommand(prCmd)
|
||||
prCmd.AddCommand(prCheckoutCmd)
|
||||
prCmd.AddCommand(prCreateCmd)
|
||||
prCmd.AddCommand(prListCmd)
|
||||
prCmd.AddCommand(prStatusCmd)
|
||||
prCmd.AddCommand(prViewCmd)
|
||||
|
|
@ -28,6 +32,12 @@ var prCmd = &cobra.Command{
|
|||
Short: "Work with pull requests",
|
||||
Long: `Helps you work with pull requests.`,
|
||||
}
|
||||
var prCheckoutCmd = &cobra.Command{
|
||||
Use: "checkout <pr-number>",
|
||||
Short: "check out a pull request in git",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: prCheckout,
|
||||
}
|
||||
var prListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List pull requests",
|
||||
|
|
@ -40,7 +50,7 @@ var prStatusCmd = &cobra.Command{
|
|||
}
|
||||
var prViewCmd = &cobra.Command{
|
||||
Use: "view [pr-number]",
|
||||
Short: "Open a pull request in the browser",
|
||||
Short: "View a pull request in the browser",
|
||||
RunE: prView,
|
||||
}
|
||||
|
||||
|
|
@ -199,10 +209,10 @@ func prList(cmd *cobra.Command, args []string) error {
|
|||
case "MERGED":
|
||||
prNum = utils.Magenta(prNum)
|
||||
}
|
||||
prBranch := utils.Cyan(truncate(branchWidth, pr.HeadRefName))
|
||||
prBranch := utils.Cyan(truncate(branchWidth, pr.HeadLabel()))
|
||||
fmt.Fprintf(out, "%s %-*s %s\n", prNum, titleWidth, truncate(titleWidth, pr.Title), prBranch)
|
||||
} else {
|
||||
fmt.Fprintf(out, "%d\t%s\t%s\n", pr.Number, pr.Title, pr.HeadRefName)
|
||||
fmt.Fprintf(out, "%d\t%s\t%s\n", pr.Number, pr.Title, pr.HeadLabel())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
|
@ -246,9 +256,135 @@ func prView(cmd *cobra.Command, args []string) error {
|
|||
return utils.OpenInBrowser(openURL)
|
||||
}
|
||||
|
||||
func prCheckout(cmd *cobra.Command, args []string) error {
|
||||
prNumber, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := contextForCommand(cmd)
|
||||
currentBranch, _ := ctx.Branch()
|
||||
remotes, err := ctx.Remotes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// FIXME: duplicates logic from fsContext.BaseRepo
|
||||
baseRemote, err := remotes.FindByName("upstream", "github", "origin", "*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiClient, err := apiClientForContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pr, err := api.PullRequestByNumber(apiClient, baseRemote, prNumber)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
headRemote := baseRemote
|
||||
if pr.IsCrossRepository {
|
||||
headRemote, _ = remotes.FindByRepo(pr.HeadRepositoryOwner.Login, pr.HeadRepository.Name)
|
||||
}
|
||||
|
||||
cmdQueue := [][]string{}
|
||||
|
||||
newBranchName := pr.HeadRefName
|
||||
if headRemote != nil {
|
||||
// there is an existing git remote for PR head
|
||||
remoteBranch := fmt.Sprintf("%s/%s", headRemote.Name, pr.HeadRefName)
|
||||
refSpec := fmt.Sprintf("+refs/heads/%s:refs/remotes/%s", pr.HeadRefName, remoteBranch)
|
||||
|
||||
cmdQueue = append(cmdQueue, []string{"git", "fetch", headRemote.Name, refSpec})
|
||||
|
||||
// local branch already exists
|
||||
if git.VerifyRef("refs/heads/" + newBranchName) {
|
||||
cmdQueue = append(cmdQueue, []string{"git", "checkout", newBranchName})
|
||||
cmdQueue = append(cmdQueue, []string{"git", "merge", "--ff-only", fmt.Sprintf("refs/remotes/%s", remoteBranch)})
|
||||
} else {
|
||||
cmdQueue = append(cmdQueue, []string{"git", "checkout", "-b", newBranchName, "--no-track", remoteBranch})
|
||||
cmdQueue = append(cmdQueue, []string{"git", "config", fmt.Sprintf("branch.%s.remote", newBranchName), headRemote.Name})
|
||||
cmdQueue = append(cmdQueue, []string{"git", "config", fmt.Sprintf("branch.%s.merge", newBranchName), "refs/heads/" + pr.HeadRefName})
|
||||
}
|
||||
} else {
|
||||
// no git remote for PR head
|
||||
|
||||
// avoid naming the new branch the same as the default branch
|
||||
if newBranchName == pr.HeadRepository.DefaultBranchRef.Name {
|
||||
newBranchName = fmt.Sprintf("%s/%s", pr.HeadRepositoryOwner.Login, newBranchName)
|
||||
}
|
||||
|
||||
ref := fmt.Sprintf("refs/pull/%d/head", prNumber)
|
||||
if newBranchName == currentBranch {
|
||||
// PR head matches currently checked out branch
|
||||
cmdQueue = append(cmdQueue, []string{"git", "fetch", baseRemote.Name, ref})
|
||||
cmdQueue = append(cmdQueue, []string{"git", "merge", "--ff-only", "FETCH_HEAD"})
|
||||
} else {
|
||||
// create a new branch
|
||||
cmdQueue = append(cmdQueue, []string{"git", "fetch", baseRemote.Name, fmt.Sprintf("%s:%s", ref, newBranchName)})
|
||||
cmdQueue = append(cmdQueue, []string{"git", "checkout", newBranchName})
|
||||
}
|
||||
|
||||
remote := baseRemote.Name
|
||||
mergeRef := ref
|
||||
if pr.MaintainerCanModify {
|
||||
remote = fmt.Sprintf("https://github.com/%s/%s.git", pr.HeadRepositoryOwner.Login, pr.HeadRepository.Name)
|
||||
mergeRef = fmt.Sprintf("refs/heads/%s", pr.HeadRefName)
|
||||
}
|
||||
if mc, err := git.Config(fmt.Sprintf("branch.%s.merge", newBranchName)); err != nil || mc == "" {
|
||||
cmdQueue = append(cmdQueue, []string{"git", "config", fmt.Sprintf("branch.%s.remote", newBranchName), remote})
|
||||
cmdQueue = append(cmdQueue, []string{"git", "config", fmt.Sprintf("branch.%s.merge", newBranchName), mergeRef})
|
||||
}
|
||||
}
|
||||
|
||||
for _, args := range cmdQueue {
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := utils.PrepareCmd(cmd).Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func printPrs(prs ...api.PullRequest) {
|
||||
for _, pr := range prs {
|
||||
fmt.Printf(" #%d %s %s\n", pr.Number, truncate(50, pr.Title), utils.Cyan("["+pr.HeadRefName+"]"))
|
||||
prNumber := fmt.Sprintf("#%d", pr.Number)
|
||||
fmt.Printf(" %s %s %s", utils.Yellow(prNumber), truncate(50, pr.Title), utils.Cyan("["+pr.HeadLabel()+"]"))
|
||||
|
||||
checks := pr.ChecksStatus()
|
||||
reviews := pr.ReviewStatus()
|
||||
if checks.Total > 0 || reviews.ChangesRequested || reviews.Approved {
|
||||
fmt.Printf("\n ")
|
||||
}
|
||||
|
||||
if checks.Total > 0 {
|
||||
var ratio string
|
||||
if checks.Failing > 0 {
|
||||
ratio = fmt.Sprintf("%d/%d", checks.Passing, checks.Total)
|
||||
ratio = utils.Red(ratio)
|
||||
} else if checks.Pending > 0 {
|
||||
ratio = fmt.Sprintf("%d/%d", checks.Passing, checks.Total)
|
||||
ratio = utils.Yellow(ratio)
|
||||
} else if checks.Passing == checks.Total {
|
||||
ratio = fmt.Sprintf("%d", checks.Total)
|
||||
ratio = utils.Green(ratio)
|
||||
}
|
||||
fmt.Printf(" - checks: %s", ratio)
|
||||
}
|
||||
|
||||
if reviews.ChangesRequested {
|
||||
fmt.Printf(" - %s", utils.Red("changes requested"))
|
||||
} else if reviews.ReviewRequired {
|
||||
fmt.Printf(" - %s", utils.Yellow("review required"))
|
||||
} else if reviews.Approved {
|
||||
fmt.Printf(" - %s", utils.Green("approved"))
|
||||
}
|
||||
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
364
command/pr_checkout_test.go
Normal file
364
command/pr_checkout_test.go
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/github/gh-cli/context"
|
||||
"github.com/github/gh-cli/utils"
|
||||
)
|
||||
|
||||
func TestPRCheckout_sameRepo(t *testing.T) {
|
||||
ctx := context.NewBlank()
|
||||
ctx.SetBranch("master")
|
||||
ctx.SetRemotes(map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
})
|
||||
initContext = func() context.Context {
|
||||
return ctx
|
||||
}
|
||||
http := initFakeHTTP()
|
||||
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"headRefName": "feature",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "REPO",
|
||||
"defaultBranchRef": {
|
||||
"name": "master"
|
||||
}
|
||||
},
|
||||
"isCrossRepository": false,
|
||||
"maintainerCanModify": false
|
||||
} } } }
|
||||
`))
|
||||
|
||||
ranCommands := [][]string{}
|
||||
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
|
||||
switch strings.Join(cmd.Args, " ") {
|
||||
case "git show-ref --verify --quiet refs/heads/feature":
|
||||
return &errorStub{"exit status: 1"}
|
||||
default:
|
||||
ranCommands = append(ranCommands, cmd.Args)
|
||||
return &outputStub{}
|
||||
}
|
||||
})
|
||||
defer restoreCmd()
|
||||
|
||||
RootCmd.SetArgs([]string{"pr", "checkout", "123"})
|
||||
_, err := prCheckoutCmd.ExecuteC()
|
||||
eq(t, err, nil)
|
||||
|
||||
eq(t, len(ranCommands), 4)
|
||||
eq(t, strings.Join(ranCommands[0], " "), "git fetch origin +refs/heads/feature:refs/remotes/origin/feature")
|
||||
eq(t, strings.Join(ranCommands[1], " "), "git checkout -b feature --no-track origin/feature")
|
||||
eq(t, strings.Join(ranCommands[2], " "), "git config branch.feature.remote origin")
|
||||
eq(t, strings.Join(ranCommands[3], " "), "git config branch.feature.merge refs/heads/feature")
|
||||
}
|
||||
|
||||
func TestPRCheckout_existingBranch(t *testing.T) {
|
||||
ctx := context.NewBlank()
|
||||
ctx.SetBranch("master")
|
||||
ctx.SetRemotes(map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
})
|
||||
initContext = func() context.Context {
|
||||
return ctx
|
||||
}
|
||||
http := initFakeHTTP()
|
||||
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"headRefName": "feature",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "REPO",
|
||||
"defaultBranchRef": {
|
||||
"name": "master"
|
||||
}
|
||||
},
|
||||
"isCrossRepository": false,
|
||||
"maintainerCanModify": false
|
||||
} } } }
|
||||
`))
|
||||
|
||||
ranCommands := [][]string{}
|
||||
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
|
||||
switch strings.Join(cmd.Args, " ") {
|
||||
case "git show-ref --verify --quiet refs/heads/feature":
|
||||
return &outputStub{}
|
||||
default:
|
||||
ranCommands = append(ranCommands, cmd.Args)
|
||||
return &outputStub{}
|
||||
}
|
||||
})
|
||||
defer restoreCmd()
|
||||
|
||||
RootCmd.SetArgs([]string{"pr", "checkout", "123"})
|
||||
_, err := prCheckoutCmd.ExecuteC()
|
||||
eq(t, err, nil)
|
||||
|
||||
eq(t, len(ranCommands), 3)
|
||||
eq(t, strings.Join(ranCommands[0], " "), "git fetch origin +refs/heads/feature:refs/remotes/origin/feature")
|
||||
eq(t, strings.Join(ranCommands[1], " "), "git checkout feature")
|
||||
eq(t, strings.Join(ranCommands[2], " "), "git merge --ff-only refs/remotes/origin/feature")
|
||||
}
|
||||
|
||||
func TestPRCheckout_differentRepo_remoteExists(t *testing.T) {
|
||||
ctx := context.NewBlank()
|
||||
ctx.SetBranch("master")
|
||||
ctx.SetRemotes(map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
"robot-fork": "hubot/REPO",
|
||||
})
|
||||
initContext = func() context.Context {
|
||||
return ctx
|
||||
}
|
||||
http := initFakeHTTP()
|
||||
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"headRefName": "feature",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "REPO",
|
||||
"defaultBranchRef": {
|
||||
"name": "master"
|
||||
}
|
||||
},
|
||||
"isCrossRepository": true,
|
||||
"maintainerCanModify": false
|
||||
} } } }
|
||||
`))
|
||||
|
||||
ranCommands := [][]string{}
|
||||
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
|
||||
switch strings.Join(cmd.Args, " ") {
|
||||
case "git show-ref --verify --quiet refs/heads/feature":
|
||||
return &errorStub{"exit status: 1"}
|
||||
default:
|
||||
ranCommands = append(ranCommands, cmd.Args)
|
||||
return &outputStub{}
|
||||
}
|
||||
})
|
||||
defer restoreCmd()
|
||||
|
||||
RootCmd.SetArgs([]string{"pr", "checkout", "123"})
|
||||
_, err := prCheckoutCmd.ExecuteC()
|
||||
eq(t, err, nil)
|
||||
|
||||
eq(t, len(ranCommands), 4)
|
||||
eq(t, strings.Join(ranCommands[0], " "), "git fetch robot-fork +refs/heads/feature:refs/remotes/robot-fork/feature")
|
||||
eq(t, strings.Join(ranCommands[1], " "), "git checkout -b feature --no-track robot-fork/feature")
|
||||
eq(t, strings.Join(ranCommands[2], " "), "git config branch.feature.remote robot-fork")
|
||||
eq(t, strings.Join(ranCommands[3], " "), "git config branch.feature.merge refs/heads/feature")
|
||||
}
|
||||
|
||||
func TestPRCheckout_differentRepo(t *testing.T) {
|
||||
ctx := context.NewBlank()
|
||||
ctx.SetBranch("master")
|
||||
ctx.SetRemotes(map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
})
|
||||
initContext = func() context.Context {
|
||||
return ctx
|
||||
}
|
||||
http := initFakeHTTP()
|
||||
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"headRefName": "feature",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "REPO",
|
||||
"defaultBranchRef": {
|
||||
"name": "master"
|
||||
}
|
||||
},
|
||||
"isCrossRepository": true,
|
||||
"maintainerCanModify": false
|
||||
} } } }
|
||||
`))
|
||||
|
||||
ranCommands := [][]string{}
|
||||
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
|
||||
switch strings.Join(cmd.Args, " ") {
|
||||
case "git config branch.feature.merge":
|
||||
return &errorStub{"exit status 1"}
|
||||
default:
|
||||
ranCommands = append(ranCommands, cmd.Args)
|
||||
return &outputStub{}
|
||||
}
|
||||
})
|
||||
defer restoreCmd()
|
||||
|
||||
RootCmd.SetArgs([]string{"pr", "checkout", "123"})
|
||||
_, err := prCheckoutCmd.ExecuteC()
|
||||
eq(t, err, nil)
|
||||
|
||||
eq(t, len(ranCommands), 4)
|
||||
eq(t, strings.Join(ranCommands[0], " "), "git fetch origin refs/pull/123/head:feature")
|
||||
eq(t, strings.Join(ranCommands[1], " "), "git checkout feature")
|
||||
eq(t, strings.Join(ranCommands[2], " "), "git config branch.feature.remote origin")
|
||||
eq(t, strings.Join(ranCommands[3], " "), "git config branch.feature.merge refs/pull/123/head")
|
||||
}
|
||||
|
||||
func TestPRCheckout_differentRepo_existingBranch(t *testing.T) {
|
||||
ctx := context.NewBlank()
|
||||
ctx.SetBranch("master")
|
||||
ctx.SetRemotes(map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
})
|
||||
initContext = func() context.Context {
|
||||
return ctx
|
||||
}
|
||||
http := initFakeHTTP()
|
||||
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"headRefName": "feature",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "REPO",
|
||||
"defaultBranchRef": {
|
||||
"name": "master"
|
||||
}
|
||||
},
|
||||
"isCrossRepository": true,
|
||||
"maintainerCanModify": false
|
||||
} } } }
|
||||
`))
|
||||
|
||||
ranCommands := [][]string{}
|
||||
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
|
||||
switch strings.Join(cmd.Args, " ") {
|
||||
case "git config branch.feature.merge":
|
||||
return &outputStub{[]byte("refs/heads/feature\n")}
|
||||
default:
|
||||
ranCommands = append(ranCommands, cmd.Args)
|
||||
return &outputStub{}
|
||||
}
|
||||
})
|
||||
defer restoreCmd()
|
||||
|
||||
RootCmd.SetArgs([]string{"pr", "checkout", "123"})
|
||||
_, err := prCheckoutCmd.ExecuteC()
|
||||
eq(t, err, nil)
|
||||
|
||||
eq(t, len(ranCommands), 2)
|
||||
eq(t, strings.Join(ranCommands[0], " "), "git fetch origin refs/pull/123/head:feature")
|
||||
eq(t, strings.Join(ranCommands[1], " "), "git checkout feature")
|
||||
}
|
||||
|
||||
func TestPRCheckout_differentRepo_currentBranch(t *testing.T) {
|
||||
ctx := context.NewBlank()
|
||||
ctx.SetBranch("feature")
|
||||
ctx.SetRemotes(map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
})
|
||||
initContext = func() context.Context {
|
||||
return ctx
|
||||
}
|
||||
http := initFakeHTTP()
|
||||
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"headRefName": "feature",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "REPO",
|
||||
"defaultBranchRef": {
|
||||
"name": "master"
|
||||
}
|
||||
},
|
||||
"isCrossRepository": true,
|
||||
"maintainerCanModify": false
|
||||
} } } }
|
||||
`))
|
||||
|
||||
ranCommands := [][]string{}
|
||||
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
|
||||
switch strings.Join(cmd.Args, " ") {
|
||||
case "git config branch.feature.merge":
|
||||
return &outputStub{[]byte("refs/heads/feature\n")}
|
||||
default:
|
||||
ranCommands = append(ranCommands, cmd.Args)
|
||||
return &outputStub{}
|
||||
}
|
||||
})
|
||||
defer restoreCmd()
|
||||
|
||||
RootCmd.SetArgs([]string{"pr", "checkout", "123"})
|
||||
_, err := prCheckoutCmd.ExecuteC()
|
||||
eq(t, err, nil)
|
||||
|
||||
eq(t, len(ranCommands), 2)
|
||||
eq(t, strings.Join(ranCommands[0], " "), "git fetch origin refs/pull/123/head")
|
||||
eq(t, strings.Join(ranCommands[1], " "), "git merge --ff-only FETCH_HEAD")
|
||||
}
|
||||
|
||||
func TestPRCheckout_maintainerCanModify(t *testing.T) {
|
||||
ctx := context.NewBlank()
|
||||
ctx.SetBranch("master")
|
||||
ctx.SetRemotes(map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
})
|
||||
initContext = func() context.Context {
|
||||
return ctx
|
||||
}
|
||||
http := initFakeHTTP()
|
||||
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": { "pullRequest": {
|
||||
"headRefName": "feature",
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
},
|
||||
"headRepository": {
|
||||
"name": "REPO",
|
||||
"defaultBranchRef": {
|
||||
"name": "master"
|
||||
}
|
||||
},
|
||||
"isCrossRepository": true,
|
||||
"maintainerCanModify": true
|
||||
} } } }
|
||||
`))
|
||||
|
||||
ranCommands := [][]string{}
|
||||
restoreCmd := utils.SetPrepareCmd(func(cmd *exec.Cmd) utils.Runnable {
|
||||
switch strings.Join(cmd.Args, " ") {
|
||||
case "git config branch.feature.merge":
|
||||
return &errorStub{"exit status 1"}
|
||||
default:
|
||||
ranCommands = append(ranCommands, cmd.Args)
|
||||
return &outputStub{}
|
||||
}
|
||||
})
|
||||
defer restoreCmd()
|
||||
|
||||
RootCmd.SetArgs([]string{"pr", "checkout", "123"})
|
||||
_, err := prCheckoutCmd.ExecuteC()
|
||||
eq(t, err, nil)
|
||||
|
||||
eq(t, len(ranCommands), 4)
|
||||
eq(t, strings.Join(ranCommands[0], " "), "git fetch origin refs/pull/123/head:feature")
|
||||
eq(t, strings.Join(ranCommands[1], " "), "git checkout feature")
|
||||
eq(t, strings.Join(ranCommands[2], " "), "git config branch.feature.remote https://github.com/hubot/REPO.git")
|
||||
eq(t, strings.Join(ranCommands[3], " "), "git config branch.feature.merge refs/heads/feature")
|
||||
}
|
||||
223
command/pr_create.go
Normal file
223
command/pr_create.go
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/github/gh-cli/api"
|
||||
"github.com/github/gh-cli/context"
|
||||
"github.com/github/gh-cli/git"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func prCreate(cmd *cobra.Command, _ []string) error {
|
||||
ctx := contextForCommand(cmd)
|
||||
|
||||
ucc, err := git.UncommittedChangeCount()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ucc > 0 {
|
||||
noun := "change"
|
||||
if ucc > 1 {
|
||||
// TODO: use pluralize helper
|
||||
noun = noun + "s"
|
||||
}
|
||||
|
||||
cmd.Printf("Warning: %d uncommitted %s\n", ucc, noun)
|
||||
}
|
||||
|
||||
head, err := ctx.Branch()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not determine current branch")
|
||||
}
|
||||
|
||||
remote, err := guessRemote(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = git.Push(remote, fmt.Sprintf("HEAD:%s", head)); err != nil {
|
||||
return fmt.Errorf("was not able to push to remote '%s': %s", remote, err)
|
||||
}
|
||||
|
||||
title, err := cmd.Flags().GetString("title")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not parse title")
|
||||
}
|
||||
body, err := cmd.Flags().GetString("body")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not parse body")
|
||||
}
|
||||
|
||||
interactive := title == "" || body == ""
|
||||
|
||||
inProgress := struct {
|
||||
Body string
|
||||
Title string
|
||||
}{}
|
||||
|
||||
if interactive {
|
||||
confirmed := false
|
||||
editor := determineEditor()
|
||||
|
||||
for !confirmed {
|
||||
titleQuestion := &survey.Question{
|
||||
Name: "title",
|
||||
Prompt: &survey.Input{
|
||||
Message: "PR Title",
|
||||
Default: inProgress.Title,
|
||||
},
|
||||
}
|
||||
bodyQuestion := &survey.Question{
|
||||
Name: "body",
|
||||
Prompt: &survey.Editor{
|
||||
Message: fmt.Sprintf("PR Body (%s)", editor),
|
||||
FileName: "*.md",
|
||||
Default: inProgress.Body,
|
||||
AppendDefault: true,
|
||||
Editor: editor,
|
||||
},
|
||||
}
|
||||
|
||||
qs := []*survey.Question{}
|
||||
if title == "" {
|
||||
qs = append(qs, titleQuestion)
|
||||
}
|
||||
if body == "" {
|
||||
qs = append(qs, bodyQuestion)
|
||||
}
|
||||
|
||||
err := survey.Ask(qs, &inProgress)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not prompt")
|
||||
}
|
||||
confirmAnswers := struct {
|
||||
Confirmation string
|
||||
}{}
|
||||
confirmQs := []*survey.Question{
|
||||
{
|
||||
Name: "confirmation",
|
||||
Prompt: &survey.Select{
|
||||
Message: "Submit?",
|
||||
Options: []string{
|
||||
"Yes",
|
||||
"Edit",
|
||||
"Cancel",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err = survey.Ask(confirmQs, &confirmAnswers)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not prompt")
|
||||
}
|
||||
|
||||
switch confirmAnswers.Confirmation {
|
||||
case "Yes":
|
||||
confirmed = true
|
||||
case "Edit":
|
||||
continue
|
||||
case "Cancel":
|
||||
cmd.Println("Discarding pull request")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if title == "" {
|
||||
title = inProgress.Title
|
||||
}
|
||||
if body == "" {
|
||||
body = inProgress.Body
|
||||
}
|
||||
base, err := cmd.Flags().GetString("base")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not parse base")
|
||||
}
|
||||
if base == "" {
|
||||
// TODO: use default branch for the repo
|
||||
base = "master"
|
||||
}
|
||||
|
||||
client, err := apiClientForContext(ctx)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not initialize api client")
|
||||
}
|
||||
|
||||
repo, err := ctx.BaseRepo()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not determine GitHub repo")
|
||||
}
|
||||
|
||||
isDraft, err := cmd.Flags().GetBool("draft")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not parse draft")
|
||||
}
|
||||
|
||||
params := map[string]interface{}{
|
||||
"title": title,
|
||||
"body": body,
|
||||
"draft": isDraft,
|
||||
"baseRefName": base,
|
||||
"headRefName": head,
|
||||
}
|
||||
|
||||
pr, err := api.CreatePullRequest(client, repo, params)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create PR")
|
||||
}
|
||||
|
||||
fmt.Fprintln(cmd.OutOrStdout(), pr.URL)
|
||||
return nil
|
||||
}
|
||||
|
||||
func guessRemote(ctx context.Context) (string, error) {
|
||||
remotes, err := ctx.Remotes()
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "could not read git remotes")
|
||||
}
|
||||
|
||||
// TODO: consolidate logic with fsContext.BaseRepo
|
||||
// TODO: check if the GH repo that the remote points to is writeable
|
||||
remote, err := remotes.FindByName("upstream", "github", "origin", "*")
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "could not determine suitable remote")
|
||||
}
|
||||
|
||||
return remote.Name, nil
|
||||
}
|
||||
|
||||
func determineEditor() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return "notepad"
|
||||
}
|
||||
if v := os.Getenv("VISUAL"); v != "" {
|
||||
return v
|
||||
}
|
||||
if e := os.Getenv("EDITOR"); e != "" {
|
||||
return e
|
||||
}
|
||||
return "nano"
|
||||
}
|
||||
|
||||
var prCreateCmd = &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "Create a pull request",
|
||||
RunE: prCreate,
|
||||
}
|
||||
|
||||
func init() {
|
||||
prCreateCmd.Flags().BoolP("draft", "d", false,
|
||||
"Mark PR as a draft")
|
||||
prCreateCmd.Flags().StringP("title", "t", "",
|
||||
"Supply a title. Will prompt for one otherwise.")
|
||||
prCreateCmd.Flags().StringP("body", "b", "",
|
||||
"Supply a body. Will prompt for one otherwise.")
|
||||
prCreateCmd.Flags().StringP("base", "T", "",
|
||||
"The branch into which you want your code merged")
|
||||
}
|
||||
136
command/pr_create_test.go
Normal file
136
command/pr_create_test.go
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/github/gh-cli/context"
|
||||
"github.com/github/gh-cli/git"
|
||||
"github.com/github/gh-cli/test"
|
||||
)
|
||||
|
||||
func TestPrCreateHelperProcess(*testing.T) {
|
||||
if test.SkipTestHelperProcess() {
|
||||
return
|
||||
}
|
||||
|
||||
args := test.GetTestHelperProcessArgs()
|
||||
switch args[1] {
|
||||
case "status":
|
||||
switch args[0] {
|
||||
case "clean":
|
||||
case "dirty":
|
||||
fmt.Println(" M git/git.go")
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown scenario: %q", args[0])
|
||||
os.Exit(1)
|
||||
}
|
||||
case "push":
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown command: %q", args[1])
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func TestPRCreate(t *testing.T) {
|
||||
ctx := context.NewBlank()
|
||||
ctx.SetBranch("feature")
|
||||
ctx.SetRemotes(map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
})
|
||||
initContext = func() context.Context {
|
||||
return ctx
|
||||
}
|
||||
http := initFakeHTTP()
|
||||
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": {
|
||||
"id": "REPOID"
|
||||
} } }
|
||||
`))
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "createPullRequest": { "pullRequest": {
|
||||
"URL": "https://github.com/OWNER/REPO/pull/12"
|
||||
} } } }
|
||||
`))
|
||||
|
||||
origGitCommand := git.GitCommand
|
||||
git.GitCommand = test.StubExecCommand("TestPrCreateHelperProcess", "clean")
|
||||
defer func() {
|
||||
git.GitCommand = origGitCommand
|
||||
}()
|
||||
|
||||
out := bytes.Buffer{}
|
||||
prCreateCmd.SetOut(&out)
|
||||
|
||||
RootCmd.SetArgs([]string{"pr", "create", "-t", "mytitle", "-b", "mybody"})
|
||||
_, err := prCreateCmd.ExecuteC()
|
||||
eq(t, err, nil)
|
||||
|
||||
bodyBytes, _ := ioutil.ReadAll(http.Requests[1].Body)
|
||||
reqBody := struct {
|
||||
Variables struct {
|
||||
Input struct {
|
||||
RepositoryID string
|
||||
Title string
|
||||
Body string
|
||||
BaseRefName string
|
||||
HeadRefName string
|
||||
}
|
||||
}
|
||||
}{}
|
||||
json.Unmarshal(bodyBytes, &reqBody)
|
||||
|
||||
eq(t, reqBody.Variables.Input.RepositoryID, "REPOID")
|
||||
eq(t, reqBody.Variables.Input.Title, "mytitle")
|
||||
eq(t, reqBody.Variables.Input.Body, "mybody")
|
||||
eq(t, reqBody.Variables.Input.BaseRefName, "master")
|
||||
eq(t, reqBody.Variables.Input.HeadRefName, "feature")
|
||||
|
||||
eq(t, out.String(), "https://github.com/OWNER/REPO/pull/12\n")
|
||||
}
|
||||
|
||||
func TestPRCreate_ReportsUncommittedChanges(t *testing.T) {
|
||||
ctx := context.NewBlank()
|
||||
ctx.SetBranch("feature")
|
||||
ctx.SetRemotes(map[string]string{
|
||||
"origin": "OWNER/REPO",
|
||||
})
|
||||
initContext = func() context.Context {
|
||||
return ctx
|
||||
}
|
||||
http := initFakeHTTP()
|
||||
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "repository": {
|
||||
"id": "REPOID"
|
||||
} } }
|
||||
`))
|
||||
http.StubResponse(200, bytes.NewBufferString(`
|
||||
{ "data": { "createPullRequest": { "pullRequest": {
|
||||
"URL": "https://github.com/OWNER/REPO/pull/12"
|
||||
} } } }
|
||||
`))
|
||||
|
||||
origGitCommand := git.GitCommand
|
||||
git.GitCommand = test.StubExecCommand("TestPrCreateHelperProcess", "dirty")
|
||||
defer func() {
|
||||
git.GitCommand = origGitCommand
|
||||
}()
|
||||
|
||||
out := bytes.Buffer{}
|
||||
prCreateCmd.SetOut(&out)
|
||||
|
||||
RootCmd.SetArgs([]string{"pr", "create", "-t", "mytitle", "-b", "mybody"})
|
||||
_, err := prCreateCmd.ExecuteC()
|
||||
eq(t, err, nil)
|
||||
|
||||
eq(t, out.String(), `Warning: 1 uncommitted change
|
||||
https://github.com/OWNER/REPO/pull/12
|
||||
`)
|
||||
}
|
||||
|
|
@ -66,7 +66,7 @@ func TestPRList(t *testing.T) {
|
|||
}
|
||||
|
||||
eq(t, out.String(), `32 New feature feature
|
||||
29 Fixed bad bug bug-fix
|
||||
29 Fixed bad bug hubot:bug-fix
|
||||
28 Improve documentation docs
|
||||
`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,6 +72,10 @@ var apiClientForContext = func(ctx context.Context) (*api.Client, error) {
|
|||
opts := []api.ClientOption{
|
||||
api.AddHeader("Authorization", fmt.Sprintf("token %s", token)),
|
||||
api.AddHeader("User-Agent", fmt.Sprintf("GitHub CLI %s", Version)),
|
||||
// antiope-preview: Checks
|
||||
// shadow-cat-preview: Draft pull requests
|
||||
api.AddHeader("Accept", "application/vnd.github.antiope-preview+json, application/vnd.github.shadow-cat-preview"),
|
||||
api.AddHeader("GraphQL-Features", "pe_mobile"),
|
||||
}
|
||||
if verbose := os.Getenv("DEBUG"); verbose != "" {
|
||||
opts = append(opts, api.VerboseLog(os.Stderr))
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/github/gh-cli/api"
|
||||
"github.com/github/gh-cli/context"
|
||||
)
|
||||
|
|
@ -34,3 +36,15 @@ func (s outputStub) Output() ([]byte, error) {
|
|||
func (s outputStub) Run() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type errorStub struct {
|
||||
message string
|
||||
}
|
||||
|
||||
func (s errorStub) Output() ([]byte, error) {
|
||||
return nil, errors.New(s.message)
|
||||
}
|
||||
|
||||
func (s errorStub) Run() error {
|
||||
return errors.New(s.message)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@ package context
|
|||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/github/gh-cli/git"
|
||||
)
|
||||
|
||||
// NewBlank initializes a blank Context suitable for testing
|
||||
func NewBlank() Context {
|
||||
func NewBlank() *blankContext {
|
||||
return &blankContext{}
|
||||
}
|
||||
|
||||
|
|
@ -16,6 +18,7 @@ type blankContext struct {
|
|||
authLogin string
|
||||
branch string
|
||||
baseRepo GitHubRepository
|
||||
remotes Remotes
|
||||
}
|
||||
|
||||
type ghRepo struct {
|
||||
|
|
@ -54,14 +57,36 @@ func (c *blankContext) SetBranch(b string) {
|
|||
}
|
||||
|
||||
func (c *blankContext) Remotes() (Remotes, error) {
|
||||
return Remotes{}, nil
|
||||
if c.remotes == nil {
|
||||
return nil, fmt.Errorf("remotes were not initialized")
|
||||
}
|
||||
return c.remotes, nil
|
||||
}
|
||||
|
||||
func (c *blankContext) SetRemotes(stubs map[string]string) {
|
||||
c.remotes = Remotes{}
|
||||
for remoteName, repo := range stubs {
|
||||
ownerWithName := strings.SplitN(repo, "/", 2)
|
||||
c.remotes = append(c.remotes, &Remote{
|
||||
Remote: &git.Remote{Name: remoteName},
|
||||
Owner: ownerWithName[0],
|
||||
Repo: ownerWithName[1],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (c *blankContext) BaseRepo() (GitHubRepository, error) {
|
||||
if c.baseRepo == nil {
|
||||
return nil, fmt.Errorf("base repo was not initialized")
|
||||
if c.baseRepo != nil {
|
||||
return c.baseRepo, nil
|
||||
}
|
||||
return c.baseRepo, nil
|
||||
remotes, err := c.Remotes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(remotes) < 1 {
|
||||
return nil, fmt.Errorf("remotes are empty")
|
||||
}
|
||||
return remotes[0], nil
|
||||
}
|
||||
|
||||
func (c *blankContext) SetBaseRepo(nwo string) {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,16 @@ func (r Remotes) FindByName(names ...string) (*Remote, error) {
|
|||
return nil, fmt.Errorf("no GitHub remotes found")
|
||||
}
|
||||
|
||||
// FindByRepo returns the first Remote that points to a specific GitHub repository
|
||||
func (r Remotes) FindByRepo(owner, name string) (*Remote, error) {
|
||||
for _, rem := range r {
|
||||
if strings.EqualFold(rem.RepoOwner(), owner) && strings.EqualFold(rem.RepoName(), name) {
|
||||
return rem, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no matching remote found")
|
||||
}
|
||||
|
||||
// Remote represents a git remote mapped to a GitHub repository
|
||||
type Remote struct {
|
||||
*git.Remote
|
||||
|
|
|
|||
58
git/git.go
58
git/git.go
|
|
@ -33,7 +33,6 @@ func Dir() (string, error) {
|
|||
|
||||
func WorkdirName() (string, error) {
|
||||
toplevelCmd := exec.Command("git", "rev-parse", "--show-toplevel")
|
||||
toplevelCmd.Stderr = nil
|
||||
output, err := utils.PrepareCmd(toplevelCmd).Output()
|
||||
dir := firstLine(output)
|
||||
if dir == "" {
|
||||
|
|
@ -42,31 +41,10 @@ func WorkdirName() (string, error) {
|
|||
return dir, err
|
||||
}
|
||||
|
||||
func HasFile(segments ...string) bool {
|
||||
// The blessed way to resolve paths within git dir since Git 2.5.0
|
||||
pathCmd := exec.Command("git", "rev-parse", "-q", "--git-path", filepath.Join(segments...))
|
||||
if output, err := utils.PrepareCmd(pathCmd).Output(); err == nil {
|
||||
if lines := outputLines(output); len(lines) == 1 {
|
||||
if _, err := os.Stat(lines[0]); err == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for older git versions
|
||||
dir, err := Dir()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
s := []string{dir}
|
||||
s = append(s, segments...)
|
||||
path := filepath.Join(s...)
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
func VerifyRef(ref string) bool {
|
||||
showRef := exec.Command("git", "show-ref", "--verify", "--quiet", ref)
|
||||
err := utils.PrepareCmd(showRef).Run()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func BranchAtRef(paths ...string) (name string, err error) {
|
||||
|
|
@ -206,6 +184,34 @@ func LocalBranches() ([]string, error) {
|
|||
return branches, nil
|
||||
}
|
||||
|
||||
var GitCommand = func(args ...string) *exec.Cmd {
|
||||
return exec.Command("git", args...)
|
||||
}
|
||||
|
||||
func UncommittedChangeCount() (int, error) {
|
||||
statusCmd := GitCommand("status", "--porcelain")
|
||||
output, err := utils.PrepareCmd(statusCmd).Output()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
lines := strings.Split(string(output), "\n")
|
||||
|
||||
count := 0
|
||||
|
||||
for _, l := range lines {
|
||||
if l != "" {
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func Push(remote string, ref string) error {
|
||||
cmd := GitCommand("push", "--set-upstream", remote, ref)
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func outputLines(output []byte) []string {
|
||||
lines := strings.TrimSuffix(string(output), "\n")
|
||||
return strings.Split(lines, "\n")
|
||||
|
|
|
|||
57
git/git_test.go
Normal file
57
git/git_test.go
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/github/gh-cli/test"
|
||||
)
|
||||
|
||||
func TestGitStatusHelperProcess(*testing.T) {
|
||||
if test.SkipTestHelperProcess() {
|
||||
return
|
||||
}
|
||||
|
||||
args := test.GetTestHelperProcessArgs()
|
||||
switch args[0] {
|
||||
case "no changes":
|
||||
case "one change":
|
||||
fmt.Println(" M poem.txt")
|
||||
case "untracked file":
|
||||
fmt.Println(" M poem.txt")
|
||||
fmt.Println("?? new.txt")
|
||||
case "boom":
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func Test_UncommittedChangeCount(t *testing.T) {
|
||||
origGitCommand := GitCommand
|
||||
defer func() {
|
||||
GitCommand = origGitCommand
|
||||
}()
|
||||
|
||||
cases := map[string]int{
|
||||
"no changes": 0,
|
||||
"one change": 1,
|
||||
"untracked file": 2,
|
||||
}
|
||||
|
||||
for k, v := range cases {
|
||||
GitCommand = test.StubExecCommand("TestGitStatusHelperProcess", k)
|
||||
ucc, _ := UncommittedChangeCount()
|
||||
|
||||
if ucc != v {
|
||||
t.Errorf("got unexpected ucc value: %d for case %s", ucc, k)
|
||||
}
|
||||
}
|
||||
|
||||
GitCommand = test.StubExecCommand("TestGitStatusHelperProcess", "boom")
|
||||
_, err := UncommittedChangeCount()
|
||||
if !strings.HasSuffix(err.Error(), "git.test: exit status 1") {
|
||||
t.Errorf("got unexpected error message: %s", err)
|
||||
}
|
||||
}
|
||||
5
go.mod
5
go.mod
|
|
@ -3,12 +3,15 @@ module github.com/github/gh-cli
|
|||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.0.4
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/mattn/go-colorable v0.1.2
|
||||
github.com/mattn/go-isatty v0.0.9
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/pkg/errors v0.8.1
|
||||
github.com/spf13/cobra v0.0.5
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9
|
||||
github.com/stretchr/testify v1.3.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5
|
||||
gopkg.in/yaml.v3 v3.0.0-20191010095647-fc94e3f71652
|
||||
)
|
||||
|
|
|
|||
22
go.sum
22
go.sum
|
|
@ -1,18 +1,27 @@
|
|||
github.com/AlecAivazis/survey/v2 v2.0.4 h1:qzXnJSzXEvmUllWqMBWpZndvT2YfoAUzAMvZUax3L2M=
|
||||
github.com/AlecAivazis/survey/v2 v2.0.4/go.mod h1:WYBhg6f0y/fNYUuesWQc0PKbJcEliGcYHB9sNT3Bg74=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw=
|
||||
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ=
|
||||
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kr/pty v1.1.4 h1:5Myjjh3JY/NaAi4IsUbHADytDyl1VE1Y9PXDlL+P/VQ=
|
||||
github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
|
|
@ -25,6 +34,8 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG
|
|||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
|
|
@ -36,13 +47,24 @@ github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb6
|
|||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5 h1:8dUaAV7K4uHsF56JQWkprecIQKdPHtR9jCHF5nB8uzc=
|
||||
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
|
|
|||
9
test/fixtures/createPr.json
vendored
Normal file
9
test/fixtures/createPr.json
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"data": {
|
||||
"createPullRequest": {
|
||||
"pullRequest": {
|
||||
"url": "https://github.com/vilmibm/testing/pull/14"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
6
test/fixtures/prList.json
vendored
6
test/fixtures/prList.json
vendored
|
|
@ -16,7 +16,11 @@
|
|||
"number": 29,
|
||||
"title": "Fixed bad bug",
|
||||
"url": "https://github.com/monalisa/hello/pull/29",
|
||||
"headRefName": "bug-fix"
|
||||
"headRefName": "bug-fix",
|
||||
"isCrossRepository": true,
|
||||
"headRepositoryOwner": {
|
||||
"login": "hubot"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
|||
7
test/fixtures/repoId.json
vendored
Normal file
7
test/fixtures/repoId.json
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"data": {
|
||||
"repository": {
|
||||
"id": "a repo id"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,38 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func GetTestHelperProcessArgs() []string {
|
||||
args := os.Args
|
||||
for len(args) > 0 {
|
||||
if args[0] == "--" {
|
||||
args = args[1:]
|
||||
break
|
||||
}
|
||||
args = args[1:]
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
func SkipTestHelperProcess() bool {
|
||||
return os.Getenv("GO_WANT_HELPER_PROCESS") != "1"
|
||||
}
|
||||
|
||||
func StubExecCommand(testHelper string, desiredOutput string) func(...string) *exec.Cmd {
|
||||
return func(args ...string) *exec.Cmd {
|
||||
cs := []string{
|
||||
fmt.Sprintf("-test.run=%s", testHelper),
|
||||
"--", desiredOutput}
|
||||
cs = append(cs, args...)
|
||||
env := []string{
|
||||
"GO_WANT_HELPER_PROCESS=1",
|
||||
}
|
||||
|
||||
cmd := exec.Command(os.Args[0], cs...)
|
||||
cmd.Env = append(env, os.Environ()...)
|
||||
return cmd
|
||||
}
|
||||
}
|
||||
|
||||
type TempGitRepo struct {
|
||||
Remote string
|
||||
TearDown func()
|
||||
|
|
|
|||
|
|
@ -1,24 +1,39 @@
|
|||
package utils
|
||||
|
||||
import "github.com/mgutz/ansi"
|
||||
import (
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/mgutz/ansi"
|
||||
"os"
|
||||
)
|
||||
|
||||
var Black = ansi.ColorFunc("black")
|
||||
var White = ansi.ColorFunc("white")
|
||||
func makeColorFunc(color string) func(string) string {
|
||||
return func(arg string) string {
|
||||
output := arg
|
||||
if isatty.IsTerminal(os.Stdout.Fd()) {
|
||||
output = ansi.Color(color+arg+ansi.Reset, "")
|
||||
}
|
||||
|
||||
func Gray(arg string) string {
|
||||
return ansi.Color(ansi.LightBlack+arg, "")
|
||||
return output
|
||||
}
|
||||
}
|
||||
|
||||
var Red = ansi.ColorFunc("red")
|
||||
var Green = ansi.ColorFunc("green")
|
||||
var Yellow = ansi.ColorFunc("yellow")
|
||||
var Blue = ansi.ColorFunc("blue")
|
||||
var Magenta = ansi.ColorFunc("magenta")
|
||||
var Cyan = ansi.ColorFunc("cyan")
|
||||
var Black = makeColorFunc(ansi.Black)
|
||||
var White = makeColorFunc(ansi.White)
|
||||
var Magenta = makeColorFunc(ansi.Magenta)
|
||||
var Cyan = makeColorFunc(ansi.Cyan)
|
||||
var Red = makeColorFunc(ansi.Red)
|
||||
var Yellow = makeColorFunc(ansi.Yellow)
|
||||
var Blue = makeColorFunc(ansi.Blue)
|
||||
var Green = makeColorFunc(ansi.Green)
|
||||
var Gray = makeColorFunc(ansi.LightBlack)
|
||||
|
||||
func Bold(arg string) string {
|
||||
// This is really annoying. If you just define Bold as ColorFunc("+b") it will properly bold but
|
||||
// will not use the default color, resulting in black and probably unreadable text. This forces
|
||||
// the default color before bolding.
|
||||
return ansi.Color(ansi.DefaultFG+arg, "+b")
|
||||
output := arg
|
||||
if isatty.IsTerminal(os.Stdout.Fd()) {
|
||||
// This is really annoying. If you just define Bold as ColorFunc("+b") it will properly bold but
|
||||
// will not use the default color, resulting in black and probably unreadable text. This forces
|
||||
// the default color before bolding.
|
||||
output = ansi.Color(ansi.DefaultFG+arg+ansi.Reset, "+b")
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,9 @@ func (c cmdWithStderr) Output() ([]byte, error) {
|
|||
if os.Getenv("DEBUG") != "" {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", c.Cmd.Args)
|
||||
}
|
||||
if c.Cmd.Stderr != nil {
|
||||
return c.Cmd.Output()
|
||||
}
|
||||
errStream := &bytes.Buffer{}
|
||||
c.Cmd.Stderr = errStream
|
||||
out, err := c.Cmd.Output()
|
||||
|
|
@ -51,6 +54,9 @@ func (c cmdWithStderr) Run() error {
|
|||
if os.Getenv("DEBUG") != "" {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", c.Cmd.Args)
|
||||
}
|
||||
if c.Cmd.Stderr != nil {
|
||||
return c.Cmd.Run()
|
||||
}
|
||||
errStream := &bytes.Buffer{}
|
||||
c.Cmd.Stderr = errStream
|
||||
err := c.Cmd.Run()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue