Merge remote-tracking branch 'origin/master' into issue-update

This commit is contained in:
Corey Johnson 2019-11-18 11:09:00 -08:00
parent 75a3496bf1
commit e5af5be940
23 changed files with 1354 additions and 65 deletions

View file

@ -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

View file

@ -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 {

View file

@ -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

View file

@ -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,
},
)

View file

@ -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
View 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
View 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
View 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
`)
}

View file

@ -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
`)
}

View file

@ -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))

View file

@ -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)
}

View file

@ -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) {

View file

@ -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

View file

@ -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
View 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
View file

@ -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
View file

@ -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
View file

@ -0,0 +1,9 @@
{
"data": {
"createPullRequest": {
"pullRequest": {
"url": "https://github.com/vilmibm/testing/pull/14"
}
}
}
}

View file

@ -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
View file

@ -0,0 +1,7 @@
{
"data": {
"repository": {
"id": "a repo id"
}
}
}

View file

@ -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()

View file

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

View file

@ -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()